Completed
Branch master (7e350b)
by
unknown
30:36
created

DatabaseBase::buildStringCast()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
c 1
b 0
f 1
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * @defgroup Database Database
5
 *
6
 * This file deals with database interface functions
7
 * and query specifics/optimisations.
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program; if not, write to the Free Software Foundation, Inc.,
21
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
 * http://www.gnu.org/copyleft/gpl.html
23
 *
24
 * @file
25
 * @ingroup Database
26
 */
27
28
/**
29
 * Database abstraction object
30
 * @ingroup Database
31
 */
32
abstract class DatabaseBase implements IDatabase {
33
	/** Number of times to re-try an operation in case of deadlock */
34
	const DEADLOCK_TRIES = 4;
35
	/** Minimum time to wait before retry, in microseconds */
36
	const DEADLOCK_DELAY_MIN = 500000;
37
	/** Maximum time to wait before retry */
38
	const DEADLOCK_DELAY_MAX = 1500000;
39
40
	/** How long before it is worth doing a dummy query to test the connection */
41
	const PING_TTL = 1.0;
42
	const PING_QUERY = 'SELECT 1 AS ping';
43
44
	const TINY_WRITE_SEC = .010;
45
	const SLOW_WRITE_SEC = .500;
46
	const SMALL_WRITE_ROWS = 100;
47
48
	/** @var string SQL query */
49
	protected $mLastQuery = '';
50
	/** @var bool */
51
	protected $mDoneWrites = false;
52
	/** @var string|bool */
53
	protected $mPHPError = false;
54
	/** @var string */
55
	protected $mServer;
56
	/** @var string */
57
	protected $mUser;
58
	/** @var string */
59
	protected $mPassword;
60
	/** @var string */
61
	protected $mDBname;
62
	/** @var bool */
63
	protected $cliMode;
64
65
	/** @var BagOStuff APC cache */
66
	protected $srvCache;
67
68
	/** @var resource Database connection */
69
	protected $mConn = null;
70
	/** @var bool */
71
	protected $mOpened = false;
72
73
	/** @var array[] List of (callable, method name) */
74
	protected $mTrxIdleCallbacks = [];
75
	/** @var array[] List of (callable, method name) */
76
	protected $mTrxPreCommitCallbacks = [];
77
	/** @var array[] List of (callable, method name) */
78
	protected $mTrxEndCallbacks = [];
79
	/** @var array[] Map of (name => (callable, method name)) */
80
	protected $mTrxRecurringCallbacks = [];
81
	/** @var bool Whether to suppress triggering of transaction end callbacks */
82
	protected $mTrxEndCallbacksSuppressed = false;
83
84
	/** @var string */
85
	protected $mTablePrefix;
86
	/** @var string */
87
	protected $mSchema;
88
	/** @var integer */
89
	protected $mFlags;
90
	/** @var bool */
91
	protected $mForeign;
92
	/** @var array */
93
	protected $mLBInfo = [];
94
	/** @var bool|null */
95
	protected $mDefaultBigSelects = null;
96
	/** @var array|bool */
97
	protected $mSchemaVars = false;
98
	/** @var array */
99
	protected $mSessionVars = [];
100
	/** @var array|null */
101
	protected $preparedArgs;
102
	/** @var string|bool|null Stashed value of html_errors INI setting */
103
	protected $htmlErrors;
104
	/** @var string */
105
	protected $delimiter = ';';
106
107
	/**
108
	 * Either 1 if a transaction is active or 0 otherwise.
109
	 * The other Trx fields may not be meaningfull if this is 0.
110
	 *
111
	 * @var int
112
	 */
113
	protected $mTrxLevel = 0;
114
	/**
115
	 * Either a short hexidecimal string if a transaction is active or ""
116
	 *
117
	 * @var string
118
	 * @see DatabaseBase::mTrxLevel
119
	 */
120
	protected $mTrxShortId = '';
121
	/**
122
	 * The UNIX time that the transaction started. Callers can assume that if
123
	 * snapshot isolation is used, then the data is *at least* up to date to that
124
	 * point (possibly more up-to-date since the first SELECT defines the snapshot).
125
	 *
126
	 * @var float|null
127
	 * @see DatabaseBase::mTrxLevel
128
	 */
129
	private $mTrxTimestamp = null;
130
	/** @var float Lag estimate at the time of BEGIN */
131
	private $mTrxReplicaLag = null;
132
	/**
133
	 * Remembers the function name given for starting the most recent transaction via begin().
134
	 * Used to provide additional context for error reporting.
135
	 *
136
	 * @var string
137
	 * @see DatabaseBase::mTrxLevel
138
	 */
139
	private $mTrxFname = null;
140
	/**
141
	 * Record if possible write queries were done in the last transaction started
142
	 *
143
	 * @var bool
144
	 * @see DatabaseBase::mTrxLevel
145
	 */
146
	private $mTrxDoneWrites = false;
147
	/**
148
	 * Record if the current transaction was started implicitly due to DBO_TRX being set.
149
	 *
150
	 * @var bool
151
	 * @see DatabaseBase::mTrxLevel
152
	 */
153
	private $mTrxAutomatic = false;
154
	/**
155
	 * Array of levels of atomicity within transactions
156
	 *
157
	 * @var array
158
	 */
159
	private $mTrxAtomicLevels = [];
160
	/**
161
	 * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
162
	 *
163
	 * @var bool
164
	 */
165
	private $mTrxAutomaticAtomic = false;
166
	/**
167
	 * Track the write query callers of the current transaction
168
	 *
169
	 * @var string[]
170
	 */
171
	private $mTrxWriteCallers = [];
172
	/**
173
	 * @var float Seconds spent in write queries for the current transaction
174
	 */
175
	private $mTrxWriteDuration = 0.0;
176
	/**
177
	 * @var integer Number of write queries for the current transaction
178
	 */
179
	private $mTrxWriteQueryCount = 0;
180
	/**
181
	 * @var float Like mTrxWriteQueryCount but excludes lock-bound, easy to replicate, queries
182
	 */
183
	private $mTrxWriteAdjDuration = 0.0;
184
	/**
185
	 * @var integer Number of write queries counted in mTrxWriteAdjDuration
186
	 */
187
	private $mTrxWriteAdjQueryCount = 0;
188
	/**
189
	 * @var float RTT time estimate
190
	 */
191
	private $mRTTEstimate = 0.0;
192
193
	/** @var array Map of (name => 1) for locks obtained via lock() */
194
	private $mNamedLocksHeld = [];
195
196
	/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
197
	private $lazyMasterHandle;
198
199
	/**
200
	 * @since 1.21
201
	 * @var resource File handle for upgrade
202
	 */
203
	protected $fileHandle = null;
204
205
	/**
206
	 * @since 1.22
207
	 * @var string[] Process cache of VIEWs names in the database
208
	 */
209
	protected $allViews = null;
210
211
	/** @var float UNIX timestamp */
212
	protected $lastPing = 0.0;
213
214
	/** @var int[] Prior mFlags values */
215
	private $priorFlags = [];
216
217
	/** @var Profiler */
218
	protected $profiler;
219
	/** @var TransactionProfiler */
220
	protected $trxProfiler;
221
222
	public function getServerInfo() {
223
		return $this->getServerVersion();
224
	}
225
226
	/**
227
	 * @return string Command delimiter used by this database engine
228
	 */
229
	public function getDelimiter() {
230
		return $this->delimiter;
231
	}
232
233
	/**
234
	 * Boolean, controls output of large amounts of debug information.
235
	 * @param bool|null $debug
236
	 *   - true to enable debugging
237
	 *   - false to disable debugging
238
	 *   - omitted or null to do nothing
239
	 *
240
	 * @return bool|null Previous value of the flag
241
	 */
242
	public function debug( $debug = null ) {
243
		return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
0 ignored issues
show
Bug introduced by
It seems like $debug defined by parameter $debug on line 242 can also be of type null; however, wfSetBit() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
244
	}
245
246
	public function bufferResults( $buffer = null ) {
247
		if ( is_null( $buffer ) ) {
248
			return !(bool)( $this->mFlags & DBO_NOBUFFER );
249
		} else {
250
			return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
251
		}
252
	}
253
254
	/**
255
	 * Turns on (false) or off (true) the automatic generation and sending
256
	 * of a "we're sorry, but there has been a database error" page on
257
	 * database errors. Default is on (false). When turned off, the
258
	 * code should use lastErrno() and lastError() to handle the
259
	 * situation as appropriate.
260
	 *
261
	 * Do not use this function outside of the Database classes.
262
	 *
263
	 * @param null|bool $ignoreErrors
264
	 * @return bool The previous value of the flag.
265
	 */
266
	protected function ignoreErrors( $ignoreErrors = null ) {
267
		return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
0 ignored issues
show
Bug introduced by
It seems like $ignoreErrors defined by parameter $ignoreErrors on line 266 can also be of type null; however, wfSetBit() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
268
	}
269
270
	public function trxLevel() {
271
		return $this->mTrxLevel;
272
	}
273
274
	public function trxTimestamp() {
275
		return $this->mTrxLevel ? $this->mTrxTimestamp : null;
276
	}
277
278
	public function tablePrefix( $prefix = null ) {
279
		return wfSetVar( $this->mTablePrefix, $prefix );
280
	}
281
282
	public function dbSchema( $schema = null ) {
283
		return wfSetVar( $this->mSchema, $schema );
284
	}
285
286
	/**
287
	 * Set the filehandle to copy write statements to.
288
	 *
289
	 * @param resource $fh File handle
290
	 */
291
	public function setFileHandle( $fh ) {
292
		$this->fileHandle = $fh;
293
	}
294
295
	public function getLBInfo( $name = null ) {
296
		if ( is_null( $name ) ) {
297
			return $this->mLBInfo;
298
		} else {
299
			if ( array_key_exists( $name, $this->mLBInfo ) ) {
300
				return $this->mLBInfo[$name];
301
			} else {
302
				return null;
303
			}
304
		}
305
	}
306
307
	public function setLBInfo( $name, $value = null ) {
308
		if ( is_null( $value ) ) {
309
			$this->mLBInfo = $name;
0 ignored issues
show
Documentation Bug introduced by
It seems like $name of type string is incompatible with the declared type array of property $mLBInfo.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
310
		} else {
311
			$this->mLBInfo[$name] = $value;
312
		}
313
	}
314
315
	/**
316
	 * Set a lazy-connecting DB handle to the master DB (for replication status purposes)
317
	 *
318
	 * @param IDatabase $conn
319
	 * @since 1.27
320
	 */
321
	public function setLazyMasterHandle( IDatabase $conn ) {
322
		$this->lazyMasterHandle = $conn;
323
	}
324
325
	/**
326
	 * @return IDatabase|null
327
	 * @see setLazyMasterHandle()
328
	 * @since 1.27
329
	 */
330
	public function getLazyMasterHandle() {
331
		return $this->lazyMasterHandle;
332
	}
333
334
	/**
335
	 * @return TransactionProfiler
336
	 */
337
	protected function getTransactionProfiler() {
338
		return $this->trxProfiler;
339
	}
340
341
	/**
342
	 * @param TransactionProfiler $profiler
343
	 * @since 1.27
344
	 */
345
	public function setTransactionProfiler( TransactionProfiler $profiler ) {
346
		$this->trxProfiler = $profiler;
347
	}
348
349
	/**
350
	 * Returns true if this database supports (and uses) cascading deletes
351
	 *
352
	 * @return bool
353
	 */
354
	public function cascadingDeletes() {
355
		return false;
356
	}
357
358
	/**
359
	 * Returns true if this database supports (and uses) triggers (e.g. on the page table)
360
	 *
361
	 * @return bool
362
	 */
363
	public function cleanupTriggers() {
364
		return false;
365
	}
366
367
	/**
368
	 * Returns true if this database is strict about what can be put into an IP field.
369
	 * Specifically, it uses a NULL value instead of an empty string.
370
	 *
371
	 * @return bool
372
	 */
373
	public function strictIPs() {
374
		return false;
375
	}
376
377
	/**
378
	 * Returns true if this database uses timestamps rather than integers
379
	 *
380
	 * @return bool
381
	 */
382
	public function realTimestamps() {
383
		return false;
384
	}
385
386
	public function implicitGroupby() {
387
		return true;
388
	}
389
390
	public function implicitOrderby() {
391
		return true;
392
	}
393
394
	/**
395
	 * Returns true if this database can do a native search on IP columns
396
	 * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
397
	 *
398
	 * @return bool
399
	 */
400
	public function searchableIPs() {
401
		return false;
402
	}
403
404
	/**
405
	 * Returns true if this database can use functional indexes
406
	 *
407
	 * @return bool
408
	 */
409
	public function functionalIndexes() {
410
		return false;
411
	}
412
413
	public function lastQuery() {
414
		return $this->mLastQuery;
415
	}
416
417
	public function doneWrites() {
418
		return (bool)$this->mDoneWrites;
419
	}
420
421
	public function lastDoneWrites() {
422
		return $this->mDoneWrites ?: false;
423
	}
424
425
	public function writesPending() {
426
		return $this->mTrxLevel && $this->mTrxDoneWrites;
427
	}
428
429
	public function writesOrCallbacksPending() {
430
		return $this->mTrxLevel && (
431
			$this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
432
		);
433
	}
434
435
	public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
436
		if ( !$this->mTrxLevel ) {
437
			return false;
438
		} elseif ( !$this->mTrxDoneWrites ) {
439
			return 0.0;
440
		}
441
442
		switch ( $type ) {
443
			case self::ESTIMATE_DB_APPLY:
444
				$this->ping( $rtt );
445
				$rttAdjTotal = $this->mTrxWriteAdjQueryCount * $rtt;
446
				$applyTime = max( $this->mTrxWriteAdjDuration - $rttAdjTotal, 0 );
447
				// For omitted queries, make them count as something at least
448
				$omitted = $this->mTrxWriteQueryCount - $this->mTrxWriteAdjQueryCount;
449
				$applyTime += self::TINY_WRITE_SEC * $omitted;
450
451
				return $applyTime;
452
			default: // everything
453
				return $this->mTrxWriteDuration;
454
		}
455
	}
456
457
	public function pendingWriteCallers() {
458
		return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
459
	}
460
461
	public function isOpen() {
462
		return $this->mOpened;
463
	}
464
465
	public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
466
		if ( $remember === self::REMEMBER_PRIOR ) {
467
			array_push( $this->priorFlags, $this->mFlags );
468
		}
469
		$this->mFlags |= $flag;
470
	}
471
472
	public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
473
		if ( $remember === self::REMEMBER_PRIOR ) {
474
			array_push( $this->priorFlags, $this->mFlags );
475
		}
476
		$this->mFlags &= ~$flag;
477
	}
478
479
	public function restoreFlags( $state = self::RESTORE_PRIOR ) {
480
		if ( !$this->priorFlags ) {
481
			return;
482
		}
483
484
		if ( $state === self::RESTORE_INITIAL ) {
485
			$this->mFlags = reset( $this->priorFlags );
0 ignored issues
show
Documentation Bug introduced by
It seems like reset($this->priorFlags) can also be of type false. However, the property $mFlags is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
486
			$this->priorFlags = [];
487
		} else {
488
			$this->mFlags = array_pop( $this->priorFlags );
489
		}
490
	}
491
492
	public function getFlag( $flag ) {
493
		return !!( $this->mFlags & $flag );
494
	}
495
496
	public function getProperty( $name ) {
497
		return $this->$name;
498
	}
499
500
	public function getWikiID() {
501
		if ( $this->mTablePrefix ) {
502
			return "{$this->mDBname}-{$this->mTablePrefix}";
503
		} else {
504
			return $this->mDBname;
505
		}
506
	}
507
508
	/**
509
	 * Return a path to the DBMS-specific SQL file if it exists,
510
	 * otherwise default SQL file
511
	 *
512
	 * @param string $filename
513
	 * @return string
514
	 */
515 View Code Duplication
	private function getSqlFilePath( $filename ) {
516
		global $IP;
517
		$dbmsSpecificFilePath = "$IP/maintenance/" . $this->getType() . "/$filename";
518
		if ( file_exists( $dbmsSpecificFilePath ) ) {
519
			return $dbmsSpecificFilePath;
520
		} else {
521
			return "$IP/maintenance/$filename";
522
		}
523
	}
524
525
	/**
526
	 * Return a path to the DBMS-specific schema file,
527
	 * otherwise default to tables.sql
528
	 *
529
	 * @return string
530
	 */
531
	public function getSchemaPath() {
532
		return $this->getSqlFilePath( 'tables.sql' );
533
	}
534
535
	/**
536
	 * Return a path to the DBMS-specific update key file,
537
	 * otherwise default to update-keys.sql
538
	 *
539
	 * @return string
540
	 */
541
	public function getUpdateKeysPath() {
542
		return $this->getSqlFilePath( 'update-keys.sql' );
543
	}
544
545
	/**
546
	 * Get information about an index into an object
547
	 * @param string $table Table name
548
	 * @param string $index Index name
549
	 * @param string $fname Calling function name
550
	 * @return mixed Database-specific index description class or false if the index does not exist
551
	 */
552
	abstract function indexInfo( $table, $index, $fname = __METHOD__ );
553
554
	/**
555
	 * Wrapper for addslashes()
556
	 *
557
	 * @param string $s String to be slashed.
558
	 * @return string Slashed string.
559
	 */
560
	abstract function strencode( $s );
561
562
	/**
563
	 * Constructor.
564
	 *
565
	 * FIXME: It is possible to construct a Database object with no associated
566
	 * connection object, by specifying no parameters to __construct(). This
567
	 * feature is deprecated and should be removed.
568
	 *
569
	 * DatabaseBase subclasses should not be constructed directly in external
570
	 * code. DatabaseBase::factory() should be used instead.
571
	 *
572
	 * @param array $params Parameters passed from DatabaseBase::factory()
573
	 */
574
	function __construct( array $params ) {
575
		global $wgDBprefix, $wgDBmwschema;
576
577
		$this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
578
579
		$server = $params['host'];
580
		$user = $params['user'];
581
		$password = $params['password'];
582
		$dbName = $params['dbname'];
583
		$flags = $params['flags'];
584
		$tablePrefix = $params['tablePrefix'];
585
		$schema = $params['schema'];
586
		$foreign = $params['foreign'];
587
588
		$this->cliMode = isset( $params['cliMode'] )
589
			? $params['cliMode']
590
			: ( PHP_SAPI === 'cli' );
591
592
		$this->mFlags = $flags;
593
		if ( $this->mFlags & DBO_DEFAULT ) {
594
			if ( $this->cliMode ) {
595
				$this->mFlags &= ~DBO_TRX;
596
			} else {
597
				$this->mFlags |= DBO_TRX;
598
			}
599
		}
600
601
		$this->mSessionVars = $params['variables'];
602
603
		/** Get the default table prefix*/
604
		if ( $tablePrefix === 'get from global' ) {
605
			$this->mTablePrefix = $wgDBprefix;
606
		} else {
607
			$this->mTablePrefix = $tablePrefix;
608
		}
609
610
		/** Get the database schema*/
611
		if ( $schema === 'get from global' ) {
612
			$this->mSchema = $wgDBmwschema;
613
		} else {
614
			$this->mSchema = $schema;
615
		}
616
617
		$this->mForeign = $foreign;
618
619
		$this->profiler = isset( $params['profiler'] )
620
			? $params['profiler']
621
			: Profiler::instance(); // @TODO: remove global state
622
		$this->trxProfiler = isset( $params['trxProfiler'] )
623
			? $params['trxProfiler']
624
			: new TransactionProfiler();
625
626
		if ( $user ) {
627
			$this->open( $server, $user, $password, $dbName );
628
		}
629
630
	}
631
632
	/**
633
	 * Called by serialize. Throw an exception when DB connection is serialized.
634
	 * This causes problems on some database engines because the connection is
635
	 * not restored on unserialize.
636
	 */
637
	public function __sleep() {
638
		throw new MWException( 'Database serialization may cause problems, since ' .
639
			'the connection is not restored on wakeup.' );
640
	}
641
642
	/**
643
	 * Given a DB type, construct the name of the appropriate child class of
644
	 * DatabaseBase. This is designed to replace all of the manual stuff like:
645
	 *    $class = 'Database' . ucfirst( strtolower( $dbType ) );
646
	 * as well as validate against the canonical list of DB types we have
647
	 *
648
	 * This factory function is mostly useful for when you need to connect to a
649
	 * database other than the MediaWiki default (such as for external auth,
650
	 * an extension, et cetera). Do not use this to connect to the MediaWiki
651
	 * database. Example uses in core:
652
	 * @see LoadBalancer::reallyOpenConnection()
653
	 * @see ForeignDBRepo::getMasterDB()
654
	 * @see WebInstallerDBConnect::execute()
655
	 *
656
	 * @since 1.18
657
	 *
658
	 * @param string $dbType A possible DB type
659
	 * @param array $p An array of options to pass to the constructor.
660
	 *    Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
661
	 * @throws MWException If the database driver or extension cannot be found
662
	 * @return DatabaseBase|null DatabaseBase subclass or null
663
	 */
664
	final public static function factory( $dbType, $p = [] ) {
665
		global $wgCommandLineMode;
666
667
		$canonicalDBTypes = [
668
			'mysql' => [ 'mysqli', 'mysql' ],
669
			'postgres' => [],
670
			'sqlite' => [],
671
			'oracle' => [],
672
			'mssql' => [],
673
		];
674
675
		$driver = false;
676
		$dbType = strtolower( $dbType );
677
		if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
678
			$possibleDrivers = $canonicalDBTypes[$dbType];
679
			if ( !empty( $p['driver'] ) ) {
680
				if ( in_array( $p['driver'], $possibleDrivers ) ) {
681
					$driver = $p['driver'];
682
				} else {
683
					throw new MWException( __METHOD__ .
684
						" cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
685
				}
686
			} else {
687
				foreach ( $possibleDrivers as $posDriver ) {
688
					if ( extension_loaded( $posDriver ) ) {
689
						$driver = $posDriver;
690
						break;
691
					}
692
				}
693
			}
694
		} else {
695
			$driver = $dbType;
696
		}
697
		if ( $driver === false ) {
698
			throw new MWException( __METHOD__ .
699
				" no viable database extension found for type '$dbType'" );
700
		}
701
702
		// Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
703
		// and everything else doesn't use a schema (e.g. null)
704
		// Although postgres and oracle support schemas, we don't use them (yet)
705
		// to maintain backwards compatibility
706
		$defaultSchemas = [
707
			'mssql' => 'get from global',
708
		];
709
710
		$class = 'Database' . ucfirst( $driver );
711
		if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
712
			// Resolve some defaults for b/c
713
			$p['host'] = isset( $p['host'] ) ? $p['host'] : false;
714
			$p['user'] = isset( $p['user'] ) ? $p['user'] : false;
715
			$p['password'] = isset( $p['password'] ) ? $p['password'] : false;
716
			$p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
717
			$p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
718
			$p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
719
			$p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
720
			if ( !isset( $p['schema'] ) ) {
721
				$p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
722
			}
723
			$p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
724
			$p['cliMode'] = $wgCommandLineMode;
725
726
			return new $class( $p );
727
		} else {
728
			return null;
729
		}
730
	}
731
732
	protected function installErrorHandler() {
733
		$this->mPHPError = false;
734
		$this->htmlErrors = ini_set( 'html_errors', '0' );
735
		set_error_handler( [ $this, 'connectionErrorHandler' ] );
736
	}
737
738
	/**
739
	 * @return bool|string
740
	 */
741
	protected function restoreErrorHandler() {
742
		restore_error_handler();
743
		if ( $this->htmlErrors !== false ) {
744
			ini_set( 'html_errors', $this->htmlErrors );
745
		}
746
		if ( $this->mPHPError ) {
747
			$error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
748
			$error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
749
750
			return $error;
751
		} else {
752
			return false;
753
		}
754
	}
755
756
	/**
757
	 * @param int $errno
758
	 * @param string $errstr
759
	 */
760
	public function connectionErrorHandler( $errno, $errstr ) {
761
		$this->mPHPError = $errstr;
762
	}
763
764
	/**
765
	 * Create a log context to pass to wfLogDBError or other logging functions.
766
	 *
767
	 * @param array $extras Additional data to add to context
768
	 * @return array
769
	 */
770
	protected function getLogContext( array $extras = [] ) {
771
		return array_merge(
772
			[
773
				'db_server' => $this->mServer,
774
				'db_name' => $this->mDBname,
775
				'db_user' => $this->mUser,
776
			],
777
			$extras
778
		);
779
	}
780
781
	public function close() {
782
		if ( $this->mConn ) {
783
			if ( $this->trxLevel() ) {
784
				if ( !$this->mTrxAutomatic ) {
785
					wfWarn( "Transaction still in progress (from {$this->mTrxFname}), " .
786
						" performing implicit commit before closing connection!" );
787
				}
788
789
				$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
790
			}
791
792
			$closed = $this->closeConnection();
793
			$this->mConn = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type resource of property $mConn.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
794
		} elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
795
			throw new MWException( "Transaction callbacks still pending." );
796
		} else {
797
			$closed = true;
798
		}
799
		$this->mOpened = false;
800
801
		return $closed;
802
	}
803
804
	/**
805
	 * Make sure isOpen() returns true as a sanity check
806
	 *
807
	 * @throws DBUnexpectedError
808
	 */
809
	protected function assertOpen() {
810
		if ( !$this->isOpen() ) {
811
			throw new DBUnexpectedError( $this, "DB connection was already closed." );
812
		}
813
	}
814
815
	/**
816
	 * Closes underlying database connection
817
	 * @since 1.20
818
	 * @return bool Whether connection was closed successfully
819
	 */
820
	abstract protected function closeConnection();
821
822
	function reportConnectionError( $error = 'Unknown error' ) {
823
		$myError = $this->lastError();
824
		if ( $myError ) {
825
			$error = $myError;
826
		}
827
828
		# New method
829
		throw new DBConnectionError( $this, $error );
830
	}
831
832
	/**
833
	 * The DBMS-dependent part of query()
834
	 *
835
	 * @param string $sql SQL query.
836
	 * @return ResultWrapper|bool Result object to feed to fetchObject,
837
	 *   fetchRow, ...; or false on failure
838
	 */
839
	abstract protected function doQuery( $sql );
840
841
	/**
842
	 * Determine whether a query writes to the DB.
843
	 * Should return true if unsure.
844
	 *
845
	 * @param string $sql
846
	 * @return bool
847
	 */
848
	protected function isWriteQuery( $sql ) {
849
		return !preg_match(
850
			'/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
851
	}
852
853
	/**
854
	 * @param $sql
855
	 * @return string|null
856
	 */
857
	protected function getQueryVerb( $sql ) {
858
		return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
859
	}
860
861
	/**
862
	 * Determine whether a SQL statement is sensitive to isolation level.
863
	 * A SQL statement is considered transactable if its result could vary
864
	 * depending on the transaction isolation level. Operational commands
865
	 * such as 'SET' and 'SHOW' are not considered to be transactable.
866
	 *
867
	 * @param string $sql
868
	 * @return bool
869
	 */
870
	protected function isTransactableQuery( $sql ) {
871
		$verb = $this->getQueryVerb( $sql );
872
		return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
873
	}
874
875
	public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
876
		global $wgUser;
877
878
		$priorWritesPending = $this->writesOrCallbacksPending();
879
		$this->mLastQuery = $sql;
880
881
		$isWrite = $this->isWriteQuery( $sql );
882
		if ( $isWrite ) {
883
			$reason = $this->getReadOnlyReason();
884
			if ( $reason !== false ) {
885
				throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
886
			}
887
			# Set a flag indicating that writes have been done
888
			$this->mDoneWrites = microtime( true );
0 ignored issues
show
Documentation Bug introduced by
The property $mDoneWrites was declared of type boolean, but microtime(true) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
889
		}
890
891
		# Add a comment for easy SHOW PROCESSLIST interpretation
892
		if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
893
			$userName = $wgUser->getName();
894
			if ( mb_strlen( $userName ) > 15 ) {
895
				$userName = mb_substr( $userName, 0, 15 ) . '...';
896
			}
897
			$userName = str_replace( '/', '', $userName );
898
		} else {
899
			$userName = '';
900
		}
901
902
		// Add trace comment to the begin of the sql string, right after the operator.
903
		// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
904
		$commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
905
906
		# Start implicit transactions that wrap the request if DBO_TRX is enabled
907
		if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
908
			&& $this->isTransactableQuery( $sql )
909
		) {
910
			$this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
911
			$this->mTrxAutomatic = true;
912
		}
913
914
		# Keep track of whether the transaction has write queries pending
915
		if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
916
			$this->mTrxDoneWrites = true;
917
			$this->getTransactionProfiler()->transactionWritingIn(
918
				$this->mServer, $this->mDBname, $this->mTrxShortId );
919
		}
920
921
		if ( $this->debug() ) {
922
			wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
923
		}
924
925
		# Avoid fatals if close() was called
926
		$this->assertOpen();
927
928
		# Send the query to the server
929
		$ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
930
931
		# Try reconnecting if the connection was lost
932
		if ( false === $ret && $this->wasErrorReissuable() ) {
933
			$recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
934
			# Stash the last error values before anything might clear them
935
			$lastError = $this->lastError();
936
			$lastErrno = $this->lastErrno();
937
			# Update state tracking to reflect transaction loss due to disconnection
938
			$this->handleTransactionLoss();
939
			wfDebug( "Connection lost, reconnecting...\n" );
940
			if ( $this->reconnect() ) {
941
				wfDebug( "Reconnected\n" );
942
				$msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
943
				wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
944
945
				if ( !$recoverable ) {
946
					# Callers may catch the exception and continue to use the DB
947
					$this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
948
				} else {
949
					# Should be safe to silently retry the query
950
					$ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
951
				}
952
			} else {
953
				wfDebug( "Failed\n" );
954
			}
955
		}
956
957
		if ( false === $ret ) {
958
			# Deadlocks cause the entire transaction to abort, not just the statement.
959
			# http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
960
			# https://www.postgresql.org/docs/9.1/static/explicit-locking.html
961
			if ( $this->wasDeadlock() ) {
962
				if ( $this->explicitTrxActive() || $priorWritesPending ) {
963
					$tempIgnore = false; // not recoverable
964
				}
965
				# Update state tracking to reflect transaction loss
966
				$this->handleTransactionLoss();
967
			}
968
969
			$this->reportQueryError(
970
				$this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
971
		}
972
973
		$res = $this->resultObject( $ret );
974
975
		return $res;
976
	}
977
978
	private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
979
		$isMaster = !is_null( $this->getLBInfo( 'master' ) );
980
		# generalizeSQL() will probably cut down the query to reasonable
981
		# logging size most of the time. The substr is really just a sanity check.
982
		if ( $isMaster ) {
983
			$queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
984
		} else {
985
			$queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
986
		}
987
988
		# Include query transaction state
989
		$queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
990
991
		$startTime = microtime( true );
992
		$this->profiler->profileIn( $queryProf );
993
		$ret = $this->doQuery( $commentedSql );
994
		$this->profiler->profileOut( $queryProf );
995
		$queryRuntime = max( microtime( true ) - $startTime, 0.0 );
996
997
		unset( $queryProfSection ); // profile out (if set)
998
999
		if ( $ret !== false ) {
1000
			$this->lastPing = $startTime;
1001
			if ( $isWrite && $this->mTrxLevel ) {
1002
				$this->updateTrxWriteQueryTime( $sql, $queryRuntime );
1003
				$this->mTrxWriteCallers[] = $fname;
1004
			}
1005
		}
1006
1007
		if ( $sql === self::PING_QUERY ) {
1008
			$this->mRTTEstimate = $queryRuntime;
1009
		}
1010
1011
		$this->getTransactionProfiler()->recordQueryCompletion(
1012
			$queryProf, $startTime, $isWrite, $this->affectedRows()
1013
		);
1014
		MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
1015
1016
		return $ret;
1017
	}
1018
1019
	/**
1020
	 * Update the estimated run-time of a query, not counting large row lock times
1021
	 *
1022
	 * LoadBalancer can be set to rollback transactions that will create huge replication
1023
	 * lag. It bases this estimate off of pendingWriteQueryDuration(). Certain simple
1024
	 * queries, like inserting a row can take a long time due to row locking. This method
1025
	 * uses some simple heuristics to discount those cases.
1026
	 *
1027
	 * @param string $sql A SQL write query
1028
	 * @param float $runtime Total runtime, including RTT
1029
	 */
1030
	private function updateTrxWriteQueryTime( $sql, $runtime ) {
1031
		// Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
1032
		$indicativeOfReplicaRuntime = true;
1033
		if ( $runtime > self::SLOW_WRITE_SEC ) {
1034
			$verb = $this->getQueryVerb( $sql );
1035
			// insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1036
			if ( $verb === 'INSERT' ) {
1037
				$indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
1038
			} elseif ( $verb === 'REPLACE' ) {
1039
				$indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
1040
			}
1041
		}
1042
1043
		$this->mTrxWriteDuration += $runtime;
1044
		$this->mTrxWriteQueryCount += 1;
1045
		if ( $indicativeOfReplicaRuntime ) {
1046
			$this->mTrxWriteAdjDuration += $runtime;
1047
			$this->mTrxWriteAdjQueryCount += 1;
1048
		}
1049
	}
1050
1051
	private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1052
		# Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1053
		# Dropped connections also mean that named locks are automatically released.
1054
		# Only allow error suppression in autocommit mode or when the lost transaction
1055
		# didn't matter anyway (aside from DBO_TRX snapshot loss).
1056
		if ( $this->mNamedLocksHeld ) {
1057
			return false; // possible critical section violation
1058
		} elseif ( $sql === 'COMMIT' ) {
1059
			return !$priorWritesPending; // nothing written anyway? (T127428)
1060
		} elseif ( $sql === 'ROLLBACK' ) {
1061
			return true; // transaction lost...which is also what was requested :)
1062
		} elseif ( $this->explicitTrxActive() ) {
1063
			return false; // don't drop atomocity
1064
		} elseif ( $priorWritesPending ) {
1065
			return false; // prior writes lost from implicit transaction
1066
		}
1067
1068
		return true;
1069
	}
1070
1071
	private function handleTransactionLoss() {
1072
		$this->mTrxLevel = 0;
1073
		$this->mTrxIdleCallbacks = []; // bug 65263
1074
		$this->mTrxPreCommitCallbacks = []; // bug 65263
1075
		try {
1076
			// Handle callbacks in mTrxEndCallbacks
1077
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1078
			$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1079
			return null;
1080
		} catch ( Exception $e ) {
1081
			// Already logged; move on...
1082
			return $e;
1083
		}
1084
	}
1085
1086
	public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1087
		if ( $this->ignoreErrors() || $tempIgnore ) {
1088
			wfDebug( "SQL ERROR (ignored): $error\n" );
1089
		} else {
1090
			$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1091
			wfLogDBError(
1092
				"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1093
				$this->getLogContext( [
1094
					'method' => __METHOD__,
1095
					'errno' => $errno,
1096
					'error' => $error,
1097
					'sql1line' => $sql1line,
1098
					'fname' => $fname,
1099
				] )
1100
			);
1101
			wfDebug( "SQL ERROR: " . $error . "\n" );
1102
			throw new DBQueryError( $this, $error, $errno, $sql, $fname );
1103
		}
1104
	}
1105
1106
	/**
1107
	 * Intended to be compatible with the PEAR::DB wrapper functions.
1108
	 * http://pear.php.net/manual/en/package.database.db.intro-execute.php
1109
	 *
1110
	 * ? = scalar value, quoted as necessary
1111
	 * ! = raw SQL bit (a function for instance)
1112
	 * & = filename; reads the file and inserts as a blob
1113
	 *     (we don't use this though...)
1114
	 *
1115
	 * @param string $sql
1116
	 * @param string $func
1117
	 *
1118
	 * @return array
1119
	 */
1120
	protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
1121
		/* MySQL doesn't support prepared statements (yet), so just
1122
		 * pack up the query for reference. We'll manually replace
1123
		 * the bits later.
1124
		 */
1125
		return [ 'query' => $sql, 'func' => $func ];
1126
	}
1127
1128
	/**
1129
	 * Free a prepared query, generated by prepare().
1130
	 * @param string $prepared
1131
	 */
1132
	protected function freePrepared( $prepared ) {
1133
		/* No-op by default */
1134
	}
1135
1136
	/**
1137
	 * Execute a prepared query with the various arguments
1138
	 * @param string $prepared The prepared sql
1139
	 * @param mixed $args Either an array here, or put scalars as varargs
1140
	 *
1141
	 * @return ResultWrapper
1142
	 */
1143
	public function execute( $prepared, $args = null ) {
1144
		if ( !is_array( $args ) ) {
1145
			# Pull the var args
1146
			$args = func_get_args();
1147
			array_shift( $args );
1148
		}
1149
1150
		$sql = $this->fillPrepared( $prepared['query'], $args );
1151
1152
		return $this->query( $sql, $prepared['func'] );
1153
	}
1154
1155
	/**
1156
	 * For faking prepared SQL statements on DBs that don't support it directly.
1157
	 *
1158
	 * @param string $preparedQuery A 'preparable' SQL statement
1159
	 * @param array $args Array of Arguments to fill it with
1160
	 * @return string Executable SQL
1161
	 */
1162
	public function fillPrepared( $preparedQuery, $args ) {
1163
		reset( $args );
1164
		$this->preparedArgs =& $args;
1165
1166
		return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
1167
			[ &$this, 'fillPreparedArg' ], $preparedQuery );
1168
	}
1169
1170
	/**
1171
	 * preg_callback func for fillPrepared()
1172
	 * The arguments should be in $this->preparedArgs and must not be touched
1173
	 * while we're doing this.
1174
	 *
1175
	 * @param array $matches
1176
	 * @throws DBUnexpectedError
1177
	 * @return string
1178
	 */
1179
	protected function fillPreparedArg( $matches ) {
1180
		switch ( $matches[1] ) {
1181
			case '\\?':
1182
				return '?';
1183
			case '\\!':
1184
				return '!';
1185
			case '\\&':
1186
				return '&';
1187
		}
1188
1189
		list( /* $n */, $arg ) = each( $this->preparedArgs );
1190
1191
		switch ( $matches[1] ) {
1192
			case '?':
1193
				return $this->addQuotes( $arg );
1194
			case '!':
1195
				return $arg;
1196
			case '&':
1197
				# return $this->addQuotes( file_get_contents( $arg ) );
1198
				throw new DBUnexpectedError(
1199
					$this,
1200
					'& mode is not implemented. If it\'s really needed, uncomment the line above.'
1201
				);
1202
			default:
1203
				throw new DBUnexpectedError(
1204
					$this,
1205
					'Received invalid match. This should never happen!'
1206
				);
1207
		}
1208
	}
1209
1210
	public function freeResult( $res ) {
1211
	}
1212
1213
	public function selectField(
1214
		$table, $var, $cond = '', $fname = __METHOD__, $options = []
1215
	) {
1216
		if ( $var === '*' ) { // sanity
1217
			throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1218
		}
1219
1220
		if ( !is_array( $options ) ) {
1221
			$options = [ $options ];
1222
		}
1223
1224
		$options['LIMIT'] = 1;
1225
1226
		$res = $this->select( $table, $var, $cond, $fname, $options );
1227
		if ( $res === false || !$this->numRows( $res ) ) {
1228
			return false;
1229
		}
1230
1231
		$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->select($table, $v...cond, $fname, $options) on line 1226 can also be of type boolean; however, IDatabase::fetchRow() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1232
1233
		if ( $row !== false ) {
1234
			return reset( $row );
1235
		} else {
1236
			return false;
1237
		}
1238
	}
1239
1240
	public function selectFieldValues(
1241
		$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1242
	) {
1243
		if ( $var === '*' ) { // sanity
1244
			throw new DBUnexpectedError( $this, "Cannot use a * field" );
1245
		} elseif ( !is_string( $var ) ) { // sanity
1246
			throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1247
		}
1248
1249
		if ( !is_array( $options ) ) {
1250
			$options = [ $options ];
1251
		}
1252
1253
		$res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1254
		if ( $res === false ) {
1255
			return false;
1256
		}
1257
1258
		$values = [];
1259
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type boolean|object<ResultWrapper> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1260
			$values[] = $row->$var;
1261
		}
1262
1263
		return $values;
1264
	}
1265
1266
	/**
1267
	 * Returns an optional USE INDEX clause to go after the table, and a
1268
	 * string to go at the end of the query.
1269
	 *
1270
	 * @param array $options Associative array of options to be turned into
1271
	 *   an SQL query, valid keys are listed in the function.
1272
	 * @return array
1273
	 * @see DatabaseBase::select()
1274
	 */
1275
	public function makeSelectOptions( $options ) {
1276
		$preLimitTail = $postLimitTail = '';
1277
		$startOpts = '';
1278
1279
		$noKeyOptions = [];
1280
1281
		foreach ( $options as $key => $option ) {
1282
			if ( is_numeric( $key ) ) {
1283
				$noKeyOptions[$option] = true;
1284
			}
1285
		}
1286
1287
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1288
1289
		$preLimitTail .= $this->makeOrderBy( $options );
1290
1291
		// if (isset($options['LIMIT'])) {
1292
		// 	$tailOpts .= $this->limitResult('', $options['LIMIT'],
1293
		// 		isset($options['OFFSET']) ? $options['OFFSET']
1294
		// 		: false);
1295
		// }
1296
1297
		if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1298
			$postLimitTail .= ' FOR UPDATE';
1299
		}
1300
1301
		if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1302
			$postLimitTail .= ' LOCK IN SHARE MODE';
1303
		}
1304
1305
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1306
			$startOpts .= 'DISTINCT';
1307
		}
1308
1309
		# Various MySQL extensions
1310
		if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1311
			$startOpts .= ' /*! STRAIGHT_JOIN */';
1312
		}
1313
1314
		if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1315
			$startOpts .= ' HIGH_PRIORITY';
1316
		}
1317
1318
		if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1319
			$startOpts .= ' SQL_BIG_RESULT';
1320
		}
1321
1322
		if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1323
			$startOpts .= ' SQL_BUFFER_RESULT';
1324
		}
1325
1326
		if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1327
			$startOpts .= ' SQL_SMALL_RESULT';
1328
		}
1329
1330
		if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1331
			$startOpts .= ' SQL_CALC_FOUND_ROWS';
1332
		}
1333
1334
		if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1335
			$startOpts .= ' SQL_CACHE';
1336
		}
1337
1338
		if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1339
			$startOpts .= ' SQL_NO_CACHE';
1340
		}
1341
1342 View Code Duplication
		if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1343
			$useIndex = $this->useIndexClause( $options['USE INDEX'] );
1344
		} else {
1345
			$useIndex = '';
1346
		}
1347
1348
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
1349
	}
1350
1351
	/**
1352
	 * Returns an optional GROUP BY with an optional HAVING
1353
	 *
1354
	 * @param array $options Associative array of options
1355
	 * @return string
1356
	 * @see DatabaseBase::select()
1357
	 * @since 1.21
1358
	 */
1359
	public function makeGroupByWithHaving( $options ) {
1360
		$sql = '';
1361 View Code Duplication
		if ( isset( $options['GROUP BY'] ) ) {
1362
			$gb = is_array( $options['GROUP BY'] )
1363
				? implode( ',', $options['GROUP BY'] )
1364
				: $options['GROUP BY'];
1365
			$sql .= ' GROUP BY ' . $gb;
1366
		}
1367 View Code Duplication
		if ( isset( $options['HAVING'] ) ) {
1368
			$having = is_array( $options['HAVING'] )
1369
				? $this->makeList( $options['HAVING'], LIST_AND )
1370
				: $options['HAVING'];
1371
			$sql .= ' HAVING ' . $having;
1372
		}
1373
1374
		return $sql;
1375
	}
1376
1377
	/**
1378
	 * Returns an optional ORDER BY
1379
	 *
1380
	 * @param array $options Associative array of options
1381
	 * @return string
1382
	 * @see DatabaseBase::select()
1383
	 * @since 1.21
1384
	 */
1385
	public function makeOrderBy( $options ) {
1386 View Code Duplication
		if ( isset( $options['ORDER BY'] ) ) {
1387
			$ob = is_array( $options['ORDER BY'] )
1388
				? implode( ',', $options['ORDER BY'] )
1389
				: $options['ORDER BY'];
1390
1391
			return ' ORDER BY ' . $ob;
1392
		}
1393
1394
		return '';
1395
	}
1396
1397
	// See IDatabase::select for the docs for this function
1398
	public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1399
		$options = [], $join_conds = [] ) {
1400
		$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1401
1402
		return $this->query( $sql, $fname );
1403
	}
1404
1405
	public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1406
		$options = [], $join_conds = []
1407
	) {
1408
		if ( is_array( $vars ) ) {
1409
			$vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1410
		}
1411
1412
		$options = (array)$options;
1413
		$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1414
			? $options['USE INDEX']
1415
			: [];
1416
1417
		if ( is_array( $table ) ) {
1418
			$from = ' FROM ' .
1419
				$this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
1420
		} elseif ( $table != '' ) {
1421
			if ( $table[0] == ' ' ) {
1422
				$from = ' FROM ' . $table;
1423
			} else {
1424
				$from = ' FROM ' .
1425
					$this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
1426
			}
1427
		} else {
1428
			$from = '';
1429
		}
1430
1431
		list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
1432
			$this->makeSelectOptions( $options );
1433
1434
		if ( !empty( $conds ) ) {
1435
			if ( is_array( $conds ) ) {
1436
				$conds = $this->makeList( $conds, LIST_AND );
1437
			}
1438
			$sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
1439
		} else {
1440
			$sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
1441
		}
1442
1443
		if ( isset( $options['LIMIT'] ) ) {
1444
			$sql = $this->limitResult( $sql, $options['LIMIT'],
1445
				isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1446
		}
1447
		$sql = "$sql $postLimitTail";
1448
1449
		if ( isset( $options['EXPLAIN'] ) ) {
1450
			$sql = 'EXPLAIN ' . $sql;
1451
		}
1452
1453
		return $sql;
1454
	}
1455
1456
	public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1457
		$options = [], $join_conds = []
1458
	) {
1459
		$options = (array)$options;
1460
		$options['LIMIT'] = 1;
1461
		$res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1462
1463
		if ( $res === false ) {
1464
			return false;
1465
		}
1466
1467
		if ( !$this->numRows( $res ) ) {
1468
			return false;
1469
		}
1470
1471
		$obj = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->select($table, $v... $options, $join_conds) on line 1461 can also be of type boolean; however, IDatabase::fetchObject() does only seem to accept object<ResultWrapper>|object<stdClass>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1472
1473
		return $obj;
1474
	}
1475
1476
	public function estimateRowCount(
1477
		$table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1478
	) {
1479
		$rows = 0;
1480
		$res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1481
1482 View Code Duplication
		if ( $res ) {
1483
			$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->select($table, ar...onds, $fname, $options) on line 1480 can also be of type boolean; however, IDatabase::fetchRow() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1484
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1485
		}
1486
1487
		return $rows;
1488
	}
1489
1490
	public function selectRowCount(
1491
		$tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1492
	) {
1493
		$rows = 0;
1494
		$sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1495
		$res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
1496
1497 View Code Duplication
		if ( $res ) {
1498
			$row = $this->fetchRow( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query("SELECT COU...l}) tmp_count", $fname) on line 1495 can also be of type boolean; however, IDatabase::fetchRow() does only seem to accept object<ResultWrapper>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1499
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1500
		}
1501
1502
		return $rows;
1503
	}
1504
1505
	/**
1506
	 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1507
	 * It's only slightly flawed. Don't use for anything important.
1508
	 *
1509
	 * @param string $sql A SQL Query
1510
	 *
1511
	 * @return string
1512
	 */
1513
	protected static function generalizeSQL( $sql ) {
1514
		# This does the same as the regexp below would do, but in such a way
1515
		# as to avoid crashing php on some large strings.
1516
		# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1517
1518
		$sql = str_replace( "\\\\", '', $sql );
1519
		$sql = str_replace( "\\'", '', $sql );
1520
		$sql = str_replace( "\\\"", '', $sql );
1521
		$sql = preg_replace( "/'.*'/s", "'X'", $sql );
1522
		$sql = preg_replace( '/".*"/s', "'X'", $sql );
1523
1524
		# All newlines, tabs, etc replaced by single space
1525
		$sql = preg_replace( '/\s+/', ' ', $sql );
1526
1527
		# All numbers => N,
1528
		# except the ones surrounded by characters, e.g. l10n
1529
		$sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1530
		$sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1531
1532
		return $sql;
1533
	}
1534
1535
	public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1536
		$info = $this->fieldInfo( $table, $field );
1537
1538
		return (bool)$info;
1539
	}
1540
1541
	public function indexExists( $table, $index, $fname = __METHOD__ ) {
1542
		if ( !$this->tableExists( $table ) ) {
1543
			return null;
1544
		}
1545
1546
		$info = $this->indexInfo( $table, $index, $fname );
1547
		if ( is_null( $info ) ) {
1548
			return null;
1549
		} else {
1550
			return $info !== false;
1551
		}
1552
	}
1553
1554
	public function tableExists( $table, $fname = __METHOD__ ) {
1555
		$table = $this->tableName( $table );
1556
		$old = $this->ignoreErrors( true );
1557
		$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
1558
		$this->ignoreErrors( $old );
1559
1560
		return (bool)$res;
1561
	}
1562
1563
	public function indexUnique( $table, $index ) {
1564
		$indexInfo = $this->indexInfo( $table, $index );
1565
1566
		if ( !$indexInfo ) {
1567
			return null;
1568
		}
1569
1570
		return !$indexInfo[0]->Non_unique;
1571
	}
1572
1573
	/**
1574
	 * Helper for DatabaseBase::insert().
1575
	 *
1576
	 * @param array $options
1577
	 * @return string
1578
	 */
1579
	protected function makeInsertOptions( $options ) {
1580
		return implode( ' ', $options );
1581
	}
1582
1583
	public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1584
		# No rows to insert, easy just return now
1585
		if ( !count( $a ) ) {
1586
			return true;
1587
		}
1588
1589
		$table = $this->tableName( $table );
1590
1591
		if ( !is_array( $options ) ) {
1592
			$options = [ $options ];
1593
		}
1594
1595
		$fh = null;
1596
		if ( isset( $options['fileHandle'] ) ) {
1597
			$fh = $options['fileHandle'];
1598
		}
1599
		$options = $this->makeInsertOptions( $options );
1600
1601
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1602
			$multi = true;
1603
			$keys = array_keys( $a[0] );
1604
		} else {
1605
			$multi = false;
1606
			$keys = array_keys( $a );
1607
		}
1608
1609
		$sql = 'INSERT ' . $options .
1610
			" INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1611
1612
		if ( $multi ) {
1613
			$first = true;
1614 View Code Duplication
			foreach ( $a as $row ) {
1615
				if ( $first ) {
1616
					$first = false;
1617
				} else {
1618
					$sql .= ',';
1619
				}
1620
				$sql .= '(' . $this->makeList( $row ) . ')';
1621
			}
1622
		} else {
1623
			$sql .= '(' . $this->makeList( $a ) . ')';
1624
		}
1625
1626
		if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1627
			return false;
1628
		} elseif ( $fh !== null ) {
1629
			return true;
1630
		}
1631
1632
		return (bool)$this->query( $sql, $fname );
1633
	}
1634
1635
	/**
1636
	 * Make UPDATE options array for DatabaseBase::makeUpdateOptions
1637
	 *
1638
	 * @param array $options
1639
	 * @return array
1640
	 */
1641
	protected function makeUpdateOptionsArray( $options ) {
1642
		if ( !is_array( $options ) ) {
1643
			$options = [ $options ];
1644
		}
1645
1646
		$opts = [];
1647
1648
		if ( in_array( 'LOW_PRIORITY', $options ) ) {
1649
			$opts[] = $this->lowPriorityOption();
1650
		}
1651
1652
		if ( in_array( 'IGNORE', $options ) ) {
1653
			$opts[] = 'IGNORE';
1654
		}
1655
1656
		return $opts;
1657
	}
1658
1659
	/**
1660
	 * Make UPDATE options for the DatabaseBase::update function
1661
	 *
1662
	 * @param array $options The options passed to DatabaseBase::update
1663
	 * @return string
1664
	 */
1665
	protected function makeUpdateOptions( $options ) {
1666
		$opts = $this->makeUpdateOptionsArray( $options );
1667
1668
		return implode( ' ', $opts );
1669
	}
1670
1671
	function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1672
		$table = $this->tableName( $table );
1673
		$opts = $this->makeUpdateOptions( $options );
1674
		$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
1675
1676 View Code Duplication
		if ( $conds !== [] && $conds !== '*' ) {
1677
			$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
1678
		}
1679
1680
		return $this->query( $sql, $fname );
1681
	}
1682
1683
	public function makeList( $a, $mode = LIST_COMMA ) {
1684
		if ( !is_array( $a ) ) {
1685
			throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
1686
		}
1687
1688
		$first = true;
1689
		$list = '';
1690
1691
		foreach ( $a as $field => $value ) {
1692
			if ( !$first ) {
1693
				if ( $mode == LIST_AND ) {
1694
					$list .= ' AND ';
1695
				} elseif ( $mode == LIST_OR ) {
1696
					$list .= ' OR ';
1697
				} else {
1698
					$list .= ',';
1699
				}
1700
			} else {
1701
				$first = false;
1702
			}
1703
1704
			if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
1705
				$list .= "($value)";
1706
			} elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
1707
				$list .= "$value";
1708
			} elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
1709
				// Remove null from array to be handled separately if found
1710
				$includeNull = false;
1711
				foreach ( array_keys( $value, null, true ) as $nullKey ) {
1712
					$includeNull = true;
1713
					unset( $value[$nullKey] );
1714
				}
1715
				if ( count( $value ) == 0 && !$includeNull ) {
1716
					throw new MWException( __METHOD__ . ": empty input for field $field" );
1717
				} elseif ( count( $value ) == 0 ) {
1718
					// only check if $field is null
1719
					$list .= "$field IS NULL";
1720
				} else {
1721
					// IN clause contains at least one valid element
1722
					if ( $includeNull ) {
1723
						// Group subconditions to ensure correct precedence
1724
						$list .= '(';
1725
					}
1726
					if ( count( $value ) == 1 ) {
1727
						// Special-case single values, as IN isn't terribly efficient
1728
						// Don't necessarily assume the single key is 0; we don't
1729
						// enforce linear numeric ordering on other arrays here.
1730
						$value = array_values( $value )[0];
1731
						$list .= $field . " = " . $this->addQuotes( $value );
1732
					} else {
1733
						$list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1734
					}
1735
					// if null present in array, append IS NULL
1736
					if ( $includeNull ) {
1737
						$list .= " OR $field IS NULL)";
1738
					}
1739
				}
1740
			} elseif ( $value === null ) {
1741 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR ) {
1742
					$list .= "$field IS ";
1743
				} elseif ( $mode == LIST_SET ) {
1744
					$list .= "$field = ";
1745
				}
1746
				$list .= 'NULL';
1747
			} else {
1748 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
1749
					$list .= "$field = ";
1750
				}
1751
				$list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
1752
			}
1753
		}
1754
1755
		return $list;
1756
	}
1757
1758
	public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1759
		$conds = [];
1760
1761
		foreach ( $data as $base => $sub ) {
1762
			if ( count( $sub ) ) {
1763
				$conds[] = $this->makeList(
1764
					[ $baseKey => $base, $subKey => array_keys( $sub ) ],
1765
					LIST_AND );
1766
			}
1767
		}
1768
1769
		if ( $conds ) {
1770
			return $this->makeList( $conds, LIST_OR );
1771
		} else {
1772
			// Nothing to search for...
1773
			return false;
1774
		}
1775
	}
1776
1777
	/**
1778
	 * Return aggregated value alias
1779
	 *
1780
	 * @param array $valuedata
1781
	 * @param string $valuename
1782
	 *
1783
	 * @return string
1784
	 */
1785
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
1786
		return $valuename;
1787
	}
1788
1789
	public function bitNot( $field ) {
1790
		return "(~$field)";
1791
	}
1792
1793
	public function bitAnd( $fieldLeft, $fieldRight ) {
1794
		return "($fieldLeft & $fieldRight)";
1795
	}
1796
1797
	public function bitOr( $fieldLeft, $fieldRight ) {
1798
		return "($fieldLeft | $fieldRight)";
1799
	}
1800
1801
	public function buildConcat( $stringList ) {
1802
		return 'CONCAT(' . implode( ',', $stringList ) . ')';
1803
	}
1804
1805 View Code Duplication
	public function buildGroupConcatField(
1806
		$delim, $table, $field, $conds = '', $join_conds = []
1807
	) {
1808
		$fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1809
1810
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1811
	}
1812
1813
	/**
1814
	 * @param string $field Field or column to cast
1815
	 * @return string
1816
	 * @since 1.28
1817
	 */
1818
	public function buildStringCast( $field ) {
1819
		return $field;
1820
	}
1821
1822
	public function selectDB( $db ) {
1823
		# Stub. Shouldn't cause serious problems if it's not overridden, but
1824
		# if your database engine supports a concept similar to MySQL's
1825
		# databases you may as well.
1826
		$this->mDBname = $db;
1827
1828
		return true;
1829
	}
1830
1831
	public function getDBname() {
1832
		return $this->mDBname;
1833
	}
1834
1835
	public function getServer() {
1836
		return $this->mServer;
1837
	}
1838
1839
	/**
1840
	 * Format a table name ready for use in constructing an SQL query
1841
	 *
1842
	 * This does two important things: it quotes the table names to clean them up,
1843
	 * and it adds a table prefix if only given a table name with no quotes.
1844
	 *
1845
	 * All functions of this object which require a table name call this function
1846
	 * themselves. Pass the canonical name to such functions. This is only needed
1847
	 * when calling query() directly.
1848
	 *
1849
	 * @note This function does not sanitize user input. It is not safe to use
1850
	 *   this function to escape user input.
1851
	 * @param string $name Database table name
1852
	 * @param string $format One of:
1853
	 *   quoted - Automatically pass the table name through addIdentifierQuotes()
1854
	 *            so that it can be used in a query.
1855
	 *   raw - Do not add identifier quotes to the table name
1856
	 * @return string Full database name
1857
	 */
1858
	public function tableName( $name, $format = 'quoted' ) {
1859
		global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema;
1860
		# Skip the entire process when we have a string quoted on both ends.
1861
		# Note that we check the end so that we will still quote any use of
1862
		# use of `database`.table. But won't break things if someone wants
1863
		# to query a database table with a dot in the name.
1864
		if ( $this->isQuotedIdentifier( $name ) ) {
1865
			return $name;
1866
		}
1867
1868
		# Lets test for any bits of text that should never show up in a table
1869
		# name. Basically anything like JOIN or ON which are actually part of
1870
		# SQL queries, but may end up inside of the table value to combine
1871
		# sql. Such as how the API is doing.
1872
		# Note that we use a whitespace test rather than a \b test to avoid
1873
		# any remote case where a word like on may be inside of a table name
1874
		# surrounded by symbols which may be considered word breaks.
1875
		if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1876
			return $name;
1877
		}
1878
1879
		# Split database and table into proper variables.
1880
		# We reverse the explode so that database.table and table both output
1881
		# the correct table.
1882
		$dbDetails = explode( '.', $name, 3 );
1883
		if ( count( $dbDetails ) == 3 ) {
1884
			list( $database, $schema, $table ) = $dbDetails;
1885
			# We don't want any prefix added in this case
1886
			$prefix = '';
1887
		} elseif ( count( $dbDetails ) == 2 ) {
1888
			list( $database, $table ) = $dbDetails;
1889
			# We don't want any prefix added in this case
1890
			# In dbs that support it, $database may actually be the schema
1891
			# but that doesn't affect any of the functionality here
1892
			$prefix = '';
1893
			$schema = null;
1894
		} else {
1895
			list( $table ) = $dbDetails;
1896
			if ( $wgSharedDB !== null # We have a shared database
1897
				&& $this->mForeign == false # We're not working on a foreign database
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
1898
				&& !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`'
1899
				&& in_array( $table, $wgSharedTables ) # A shared table is selected
1900
			) {
1901
				$database = $wgSharedDB;
1902
				$schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema;
1903
				$prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
1904
			} else {
1905
				$database = null;
1906
				$schema = $this->mSchema; # Default schema
1907
				$prefix = $this->mTablePrefix; # Default prefix
1908
			}
1909
		}
1910
1911
		# Quote $table and apply the prefix if not quoted.
1912
		# $tableName might be empty if this is called from Database::replaceVars()
1913
		$tableName = "{$prefix}{$table}";
1914
		if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) {
1915
			$tableName = $this->addIdentifierQuotes( $tableName );
1916
		}
1917
1918
		# Quote $schema and merge it with the table name if needed
1919 View Code Duplication
		if ( strlen( $schema ) ) {
1920
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
1921
				$schema = $this->addIdentifierQuotes( $schema );
1922
			}
1923
			$tableName = $schema . '.' . $tableName;
1924
		}
1925
1926
		# Quote $database and merge it with the table name if needed
1927 View Code Duplication
		if ( $database !== null ) {
1928
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
1929
				$database = $this->addIdentifierQuotes( $database );
1930
			}
1931
			$tableName = $database . '.' . $tableName;
1932
		}
1933
1934
		return $tableName;
1935
	}
1936
1937
	/**
1938
	 * Fetch a number of table names into an array
1939
	 * This is handy when you need to construct SQL for joins
1940
	 *
1941
	 * Example:
1942
	 * extract( $dbr->tableNames( 'user', 'watchlist' ) );
1943
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1944
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1945
	 *
1946
	 * @return array
1947
	 */
1948 View Code Duplication
	public function tableNames() {
1949
		$inArray = func_get_args();
1950
		$retVal = [];
1951
1952
		foreach ( $inArray as $name ) {
1953
			$retVal[$name] = $this->tableName( $name );
1954
		}
1955
1956
		return $retVal;
1957
	}
1958
1959
	/**
1960
	 * Fetch a number of table names into an zero-indexed numerical array
1961
	 * This is handy when you need to construct SQL for joins
1962
	 *
1963
	 * Example:
1964
	 * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
1965
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1966
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1967
	 *
1968
	 * @return array
1969
	 */
1970 View Code Duplication
	public function tableNamesN() {
1971
		$inArray = func_get_args();
1972
		$retVal = [];
1973
1974
		foreach ( $inArray as $name ) {
1975
			$retVal[] = $this->tableName( $name );
1976
		}
1977
1978
		return $retVal;
1979
	}
1980
1981
	/**
1982
	 * Get an aliased table name
1983
	 * e.g. tableName AS newTableName
1984
	 *
1985
	 * @param string $name Table name, see tableName()
1986
	 * @param string|bool $alias Alias (optional)
1987
	 * @return string SQL name for aliased table. Will not alias a table to its own name
1988
	 */
1989
	public function tableNameWithAlias( $name, $alias = false ) {
1990
		if ( !$alias || $alias == $name ) {
1991
			return $this->tableName( $name );
1992
		} else {
1993
			return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1989 can also be of type boolean; however, DatabaseBase::addIdentifierQuotes() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1994
		}
1995
	}
1996
1997
	/**
1998
	 * Gets an array of aliased table names
1999
	 *
2000
	 * @param array $tables [ [alias] => table ]
2001
	 * @return string[] See tableNameWithAlias()
2002
	 */
2003
	public function tableNamesWithAlias( $tables ) {
2004
		$retval = [];
2005
		foreach ( $tables as $alias => $table ) {
2006
			if ( is_numeric( $alias ) ) {
2007
				$alias = $table;
2008
			}
2009
			$retval[] = $this->tableNameWithAlias( $table, $alias );
2010
		}
2011
2012
		return $retval;
2013
	}
2014
2015
	/**
2016
	 * Get an aliased field name
2017
	 * e.g. fieldName AS newFieldName
2018
	 *
2019
	 * @param string $name Field name
2020
	 * @param string|bool $alias Alias (optional)
2021
	 * @return string SQL name for aliased field. Will not alias a field to its own name
2022
	 */
2023
	public function fieldNameWithAlias( $name, $alias = false ) {
2024
		if ( !$alias || (string)$alias === (string)$name ) {
2025
			return $name;
2026
		} else {
2027
			return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 2023 can also be of type boolean; however, DatabaseBase::addIdentifierQuotes() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2028
		}
2029
	}
2030
2031
	/**
2032
	 * Gets an array of aliased field names
2033
	 *
2034
	 * @param array $fields [ [alias] => field ]
2035
	 * @return string[] See fieldNameWithAlias()
2036
	 */
2037
	public function fieldNamesWithAlias( $fields ) {
2038
		$retval = [];
2039
		foreach ( $fields as $alias => $field ) {
2040
			if ( is_numeric( $alias ) ) {
2041
				$alias = $field;
2042
			}
2043
			$retval[] = $this->fieldNameWithAlias( $field, $alias );
2044
		}
2045
2046
		return $retval;
2047
	}
2048
2049
	/**
2050
	 * Get the aliased table name clause for a FROM clause
2051
	 * which might have a JOIN and/or USE INDEX clause
2052
	 *
2053
	 * @param array $tables ( [alias] => table )
2054
	 * @param array $use_index Same as for select()
2055
	 * @param array $join_conds Same as for select()
2056
	 * @return string
2057
	 */
2058
	protected function tableNamesWithUseIndexOrJOIN(
2059
		$tables, $use_index = [], $join_conds = []
2060
	) {
2061
		$ret = [];
2062
		$retJOIN = [];
2063
		$use_index = (array)$use_index;
2064
		$join_conds = (array)$join_conds;
2065
2066
		foreach ( $tables as $alias => $table ) {
2067
			if ( !is_string( $alias ) ) {
2068
				// No alias? Set it equal to the table name
2069
				$alias = $table;
2070
			}
2071
			// Is there a JOIN clause for this table?
2072
			if ( isset( $join_conds[$alias] ) ) {
2073
				list( $joinType, $conds ) = $join_conds[$alias];
2074
				$tableClause = $joinType;
2075
				$tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
2076
				if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
2077
					$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
2078
					if ( $use != '' ) {
2079
						$tableClause .= ' ' . $use;
2080
					}
2081
				}
2082
				$on = $this->makeList( (array)$conds, LIST_AND );
2083
				if ( $on != '' ) {
2084
					$tableClause .= ' ON (' . $on . ')';
2085
				}
2086
2087
				$retJOIN[] = $tableClause;
2088
			} elseif ( isset( $use_index[$alias] ) ) {
2089
				// Is there an INDEX clause for this table?
2090
				$tableClause = $this->tableNameWithAlias( $table, $alias );
2091
				$tableClause .= ' ' . $this->useIndexClause(
2092
					implode( ',', (array)$use_index[$alias] )
2093
				);
2094
2095
				$ret[] = $tableClause;
2096
			} else {
2097
				$tableClause = $this->tableNameWithAlias( $table, $alias );
2098
2099
				$ret[] = $tableClause;
2100
			}
2101
		}
2102
2103
		// We can't separate explicit JOIN clauses with ',', use ' ' for those
2104
		$implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
2105
		$explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
2106
2107
		// Compile our final table clause
2108
		return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2109
	}
2110
2111
	/**
2112
	 * Get the name of an index in a given table.
2113
	 *
2114
	 * @param string $index
2115
	 * @return string
2116
	 */
2117
	protected function indexName( $index ) {
2118
		// Backwards-compatibility hack
2119
		$renamed = [
2120
			'ar_usertext_timestamp' => 'usertext_timestamp',
2121
			'un_user_id' => 'user_id',
2122
			'un_user_ip' => 'user_ip',
2123
		];
2124
2125
		if ( isset( $renamed[$index] ) ) {
2126
			return $renamed[$index];
2127
		} else {
2128
			return $index;
2129
		}
2130
	}
2131
2132
	public function addQuotes( $s ) {
2133
		if ( $s instanceof Blob ) {
2134
			$s = $s->fetch();
2135
		}
2136
		if ( $s === null ) {
2137
			return 'NULL';
2138
		} else {
2139
			# This will also quote numeric values. This should be harmless,
2140
			# and protects against weird problems that occur when they really
2141
			# _are_ strings such as article titles and string->number->string
2142
			# conversion is not 1:1.
2143
			return "'" . $this->strencode( $s ) . "'";
2144
		}
2145
	}
2146
2147
	/**
2148
	 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
2149
	 * MySQL uses `backticks` while basically everything else uses double quotes.
2150
	 * Since MySQL is the odd one out here the double quotes are our generic
2151
	 * and we implement backticks in DatabaseMysql.
2152
	 *
2153
	 * @param string $s
2154
	 * @return string
2155
	 */
2156
	public function addIdentifierQuotes( $s ) {
2157
		return '"' . str_replace( '"', '""', $s ) . '"';
2158
	}
2159
2160
	/**
2161
	 * Returns if the given identifier looks quoted or not according to
2162
	 * the database convention for quoting identifiers .
2163
	 *
2164
	 * @note Do not use this to determine if untrusted input is safe.
2165
	 *   A malicious user can trick this function.
2166
	 * @param string $name
2167
	 * @return bool
2168
	 */
2169
	public function isQuotedIdentifier( $name ) {
2170
		return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2171
	}
2172
2173
	/**
2174
	 * @param string $s
2175
	 * @return string
2176
	 */
2177
	protected function escapeLikeInternal( $s ) {
2178
		return addcslashes( $s, '\%_' );
2179
	}
2180
2181
	public function buildLike() {
2182
		$params = func_get_args();
2183
2184
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2185
			$params = $params[0];
2186
		}
2187
2188
		$s = '';
2189
2190
		foreach ( $params as $value ) {
2191
			if ( $value instanceof LikeMatch ) {
2192
				$s .= $value->toString();
2193
			} else {
2194
				$s .= $this->escapeLikeInternal( $value );
2195
			}
2196
		}
2197
2198
		return " LIKE {$this->addQuotes( $s )} ";
2199
	}
2200
2201
	public function anyChar() {
2202
		return new LikeMatch( '_' );
2203
	}
2204
2205
	public function anyString() {
2206
		return new LikeMatch( '%' );
2207
	}
2208
2209
	public function nextSequenceValue( $seqName ) {
2210
		return null;
2211
	}
2212
2213
	/**
2214
	 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2215
	 * is only needed because a) MySQL must be as efficient as possible due to
2216
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2217
	 * which index to pick. Anyway, other databases might have different
2218
	 * indexes on a given table. So don't bother overriding this unless you're
2219
	 * MySQL.
2220
	 * @param string $index
2221
	 * @return string
2222
	 */
2223
	public function useIndexClause( $index ) {
2224
		return '';
2225
	}
2226
2227
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2228
		$quotedTable = $this->tableName( $table );
2229
2230
		if ( count( $rows ) == 0 ) {
2231
			return;
2232
		}
2233
2234
		# Single row case
2235
		if ( !is_array( reset( $rows ) ) ) {
2236
			$rows = [ $rows ];
2237
		}
2238
2239
		// @FXIME: this is not atomic, but a trx would break affectedRows()
2240
		foreach ( $rows as $row ) {
2241
			# Delete rows which collide
2242
			if ( $uniqueIndexes ) {
2243
				$sql = "DELETE FROM $quotedTable WHERE ";
2244
				$first = true;
2245
				foreach ( $uniqueIndexes as $index ) {
2246
					if ( $first ) {
2247
						$first = false;
2248
						$sql .= '( ';
2249
					} else {
2250
						$sql .= ' ) OR ( ';
2251
					}
2252
					if ( is_array( $index ) ) {
2253
						$first2 = true;
2254
						foreach ( $index as $col ) {
2255
							if ( $first2 ) {
2256
								$first2 = false;
2257
							} else {
2258
								$sql .= ' AND ';
2259
							}
2260
							$sql .= $col . '=' . $this->addQuotes( $row[$col] );
2261
						}
2262
					} else {
2263
						$sql .= $index . '=' . $this->addQuotes( $row[$index] );
2264
					}
2265
				}
2266
				$sql .= ' )';
2267
				$this->query( $sql, $fname );
2268
			}
2269
2270
			# Now insert the row
2271
			$this->insert( $table, $row, $fname );
2272
		}
2273
	}
2274
2275
	/**
2276
	 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2277
	 * statement.
2278
	 *
2279
	 * @param string $table Table name
2280
	 * @param array|string $rows Row(s) to insert
2281
	 * @param string $fname Caller function name
2282
	 *
2283
	 * @return ResultWrapper
2284
	 */
2285
	protected function nativeReplace( $table, $rows, $fname ) {
2286
		$table = $this->tableName( $table );
2287
2288
		# Single row case
2289
		if ( !is_array( reset( $rows ) ) ) {
2290
			$rows = [ $rows ];
2291
		}
2292
2293
		$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2294
		$first = true;
2295
2296 View Code Duplication
		foreach ( $rows as $row ) {
2297
			if ( $first ) {
2298
				$first = false;
2299
			} else {
2300
				$sql .= ',';
2301
			}
2302
2303
			$sql .= '(' . $this->makeList( $row ) . ')';
2304
		}
2305
2306
		return $this->query( $sql, $fname );
2307
	}
2308
2309
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2310
		$fname = __METHOD__
2311
	) {
2312
		if ( !count( $rows ) ) {
2313
			return true; // nothing to do
2314
		}
2315
2316
		if ( !is_array( reset( $rows ) ) ) {
2317
			$rows = [ $rows ];
2318
		}
2319
2320
		if ( count( $uniqueIndexes ) ) {
2321
			$clauses = []; // list WHERE clauses that each identify a single row
2322
			foreach ( $rows as $row ) {
2323
				foreach ( $uniqueIndexes as $index ) {
2324
					$index = is_array( $index ) ? $index : [ $index ]; // columns
2325
					$rowKey = []; // unique key to this row
2326
					foreach ( $index as $column ) {
2327
						$rowKey[$column] = $row[$column];
2328
					}
2329
					$clauses[] = $this->makeList( $rowKey, LIST_AND );
2330
				}
2331
			}
2332
			$where = [ $this->makeList( $clauses, LIST_OR ) ];
2333
		} else {
2334
			$where = false;
2335
		}
2336
2337
		$useTrx = !$this->mTrxLevel;
2338
		if ( $useTrx ) {
2339
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2340
		}
2341
		try {
2342
			# Update any existing conflicting row(s)
2343
			if ( $where !== false ) {
2344
				$ok = $this->update( $table, $set, $where, $fname );
2345
			} else {
2346
				$ok = true;
2347
			}
2348
			# Now insert any non-conflicting row(s)
2349
			$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2350
		} catch ( Exception $e ) {
2351
			if ( $useTrx ) {
2352
				$this->rollback( $fname, self::FLUSHING_INTERNAL );
2353
			}
2354
			throw $e;
2355
		}
2356
		if ( $useTrx ) {
2357
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2358
		}
2359
2360
		return $ok;
2361
	}
2362
2363 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2364
		$fname = __METHOD__
2365
	) {
2366
		if ( !$conds ) {
2367
			throw new DBUnexpectedError( $this,
2368
				'DatabaseBase::deleteJoin() called with empty $conds' );
2369
		}
2370
2371
		$delTable = $this->tableName( $delTable );
2372
		$joinTable = $this->tableName( $joinTable );
2373
		$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2374
		if ( $conds != '*' ) {
2375
			$sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
2376
		}
2377
		$sql .= ')';
2378
2379
		$this->query( $sql, $fname );
2380
	}
2381
2382
	/**
2383
	 * Returns the size of a text field, or -1 for "unlimited"
2384
	 *
2385
	 * @param string $table
2386
	 * @param string $field
2387
	 * @return int
2388
	 */
2389
	public function textFieldSize( $table, $field ) {
2390
		$table = $this->tableName( $table );
2391
		$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2392
		$res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
2393
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql, 'DatabaseBase::textFieldSize') on line 2392 can also be of type boolean; however, IDatabase::fetchObject() does only seem to accept object<ResultWrapper>|object<stdClass>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2394
2395
		$m = [];
2396
2397
		if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2398
			$size = $m[1];
2399
		} else {
2400
			$size = -1;
2401
		}
2402
2403
		return $size;
2404
	}
2405
2406
	/**
2407
	 * A string to insert into queries to show that they're low-priority, like
2408
	 * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
2409
	 * string and nothing bad should happen.
2410
	 *
2411
	 * @return string Returns the text of the low priority option if it is
2412
	 *   supported, or a blank string otherwise
2413
	 */
2414
	public function lowPriorityOption() {
2415
		return '';
2416
	}
2417
2418
	public function delete( $table, $conds, $fname = __METHOD__ ) {
2419
		if ( !$conds ) {
2420
			throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
2421
		}
2422
2423
		$table = $this->tableName( $table );
2424
		$sql = "DELETE FROM $table";
2425
2426 View Code Duplication
		if ( $conds != '*' ) {
2427
			if ( is_array( $conds ) ) {
2428
				$conds = $this->makeList( $conds, LIST_AND );
2429
			}
2430
			$sql .= ' WHERE ' . $conds;
2431
		}
2432
2433
		return $this->query( $sql, $fname );
2434
	}
2435
2436
	public function insertSelect(
2437
		$destTable, $srcTable, $varMap, $conds,
2438
		$fname = __METHOD__, $insertOptions = [], $selectOptions = []
2439
	) {
2440
		if ( $this->cliMode ) {
2441
			// For massive migrations with downtime, we don't want to select everything
2442
			// into memory and OOM, so do all this native on the server side if possible.
2443
			return $this->nativeInsertSelect(
2444
				$destTable,
2445
				$srcTable,
2446
				$varMap,
2447
				$conds,
2448
				$fname,
2449
				$insertOptions,
2450
				$selectOptions
2451
			);
2452
		}
2453
2454
		// For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
2455
		// on only the master (without needing row-based-replication). It also makes it easy to
2456
		// know how big the INSERT is going to be.
2457
		$fields = [];
2458
		foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
2459
			$fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
2460
		}
2461
		$selectOptions[] = 'FOR UPDATE';
2462
		$res = $this->select( $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions );
2463
		if ( !$res ) {
2464
			return false;
2465
		}
2466
2467
		$rows = [];
2468
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
2469
			$rows[] = (array)$row;
2470
		}
2471
2472
		return $this->insert( $destTable, $rows, $fname, $insertOptions );
2473
	}
2474
2475
	public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
2476
		$fname = __METHOD__,
2477
		$insertOptions = [], $selectOptions = []
2478
	) {
2479
		$destTable = $this->tableName( $destTable );
2480
2481
		if ( !is_array( $insertOptions ) ) {
2482
			$insertOptions = [ $insertOptions ];
2483
		}
2484
2485
		$insertOptions = $this->makeInsertOptions( $insertOptions );
2486
2487
		if ( !is_array( $selectOptions ) ) {
2488
			$selectOptions = [ $selectOptions ];
2489
		}
2490
2491
		list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
2492
2493 View Code Duplication
		if ( is_array( $srcTable ) ) {
2494
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2495
		} else {
2496
			$srcTable = $this->tableName( $srcTable );
2497
		}
2498
2499
		$sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2500
			" SELECT $startOpts " . implode( ',', $varMap ) .
2501
			" FROM $srcTable $useIndex ";
2502
2503 View Code Duplication
		if ( $conds != '*' ) {
2504
			if ( is_array( $conds ) ) {
2505
				$conds = $this->makeList( $conds, LIST_AND );
2506
			}
2507
			$sql .= " WHERE $conds";
2508
		}
2509
2510
		$sql .= " $tailOpts";
2511
2512
		return $this->query( $sql, $fname );
2513
	}
2514
2515
	/**
2516
	 * Construct a LIMIT query with optional offset. This is used for query
2517
	 * pages. The SQL should be adjusted so that only the first $limit rows
2518
	 * are returned. If $offset is provided as well, then the first $offset
2519
	 * rows should be discarded, and the next $limit rows should be returned.
2520
	 * If the result of the query is not ordered, then the rows to be returned
2521
	 * are theoretically arbitrary.
2522
	 *
2523
	 * $sql is expected to be a SELECT, if that makes a difference.
2524
	 *
2525
	 * The version provided by default works in MySQL and SQLite. It will very
2526
	 * likely need to be overridden for most other DBMSes.
2527
	 *
2528
	 * @param string $sql SQL query we will append the limit too
2529
	 * @param int $limit The SQL limit
2530
	 * @param int|bool $offset The SQL offset (default false)
2531
	 * @throws DBUnexpectedError
2532
	 * @return string
2533
	 */
2534
	public function limitResult( $sql, $limit, $offset = false ) {
2535
		if ( !is_numeric( $limit ) ) {
2536
			throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
2537
		}
2538
2539
		return "$sql LIMIT "
2540
			. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2541
			. "{$limit} ";
2542
	}
2543
2544
	public function unionSupportsOrderAndLimit() {
2545
		return true; // True for almost every DB supported
2546
	}
2547
2548
	public function unionQueries( $sqls, $all ) {
2549
		$glue = $all ? ') UNION ALL (' : ') UNION (';
2550
2551
		return '(' . implode( $glue, $sqls ) . ')';
2552
	}
2553
2554
	public function conditional( $cond, $trueVal, $falseVal ) {
2555
		if ( is_array( $cond ) ) {
2556
			$cond = $this->makeList( $cond, LIST_AND );
2557
		}
2558
2559
		return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2560
	}
2561
2562
	public function strreplace( $orig, $old, $new ) {
2563
		return "REPLACE({$orig}, {$old}, {$new})";
2564
	}
2565
2566
	public function getServerUptime() {
2567
		return 0;
2568
	}
2569
2570
	public function wasDeadlock() {
2571
		return false;
2572
	}
2573
2574
	public function wasLockTimeout() {
2575
		return false;
2576
	}
2577
2578
	public function wasErrorReissuable() {
2579
		return false;
2580
	}
2581
2582
	public function wasReadOnlyError() {
2583
		return false;
2584
	}
2585
2586
	/**
2587
	 * Determines if the given query error was a connection drop
2588
	 * STUB
2589
	 *
2590
	 * @param integer|string $errno
2591
	 * @return bool
2592
	 */
2593
	public function wasConnectionError( $errno ) {
2594
		return false;
2595
	}
2596
2597
	/**
2598
	 * Perform a deadlock-prone transaction.
2599
	 *
2600
	 * This function invokes a callback function to perform a set of write
2601
	 * queries. If a deadlock occurs during the processing, the transaction
2602
	 * will be rolled back and the callback function will be called again.
2603
	 *
2604
	 * Avoid using this method outside of Job or Maintenance classes.
2605
	 *
2606
	 * Usage:
2607
	 *   $dbw->deadlockLoop( callback, ... );
2608
	 *
2609
	 * Extra arguments are passed through to the specified callback function.
2610
	 * This method requires that no transactions are already active to avoid
2611
	 * causing premature commits or exceptions.
2612
	 *
2613
	 * Returns whatever the callback function returned on its successful,
2614
	 * iteration, or false on error, for example if the retry limit was
2615
	 * reached.
2616
	 *
2617
	 * @return mixed
2618
	 * @throws DBUnexpectedError
2619
	 * @throws Exception
2620
	 */
2621
	public function deadlockLoop() {
2622
		$args = func_get_args();
2623
		$function = array_shift( $args );
2624
		$tries = self::DEADLOCK_TRIES;
2625
2626
		$this->begin( __METHOD__ );
2627
2628
		$retVal = null;
2629
		/** @var Exception $e */
2630
		$e = null;
2631
		do {
2632
			try {
2633
				$retVal = call_user_func_array( $function, $args );
2634
				break;
2635
			} catch ( DBQueryError $e ) {
2636
				if ( $this->wasDeadlock() ) {
2637
					// Retry after a randomized delay
2638
					usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2639
				} else {
2640
					// Throw the error back up
2641
					throw $e;
2642
				}
2643
			}
2644
		} while ( --$tries > 0 );
2645
2646
		if ( $tries <= 0 ) {
2647
			// Too many deadlocks; give up
2648
			$this->rollback( __METHOD__ );
2649
			throw $e;
2650
		} else {
2651
			$this->commit( __METHOD__ );
2652
2653
			return $retVal;
2654
		}
2655
	}
2656
2657
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
2658
		# Real waits are implemented in the subclass.
2659
		return 0;
2660
	}
2661
2662
	public function getSlavePos() {
2663
		# Stub
2664
		return false;
2665
	}
2666
2667
	public function getMasterPos() {
2668
		# Stub
2669
		return false;
2670
	}
2671
2672
	public function serverIsReadOnly() {
2673
		return false;
2674
	}
2675
2676
	final public function onTransactionResolution( callable $callback ) {
2677
		if ( !$this->mTrxLevel ) {
2678
			throw new DBUnexpectedError( $this, "No transaction is active." );
2679
		}
2680
		$this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
2681
	}
2682
2683
	final public function onTransactionIdle( callable $callback ) {
2684
		$this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
2685
		if ( !$this->mTrxLevel ) {
2686
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2687
		}
2688
	}
2689
2690
	final public function onTransactionPreCommitOrIdle( callable $callback ) {
2691
		if ( $this->mTrxLevel ) {
2692
			$this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
2693
		} else {
2694
			// If no transaction is active, then make one for this callback
2695
			$this->startAtomic( __METHOD__ );
2696
			try {
2697
				call_user_func( $callback );
2698
				$this->endAtomic( __METHOD__ );
2699
			} catch ( Exception $e ) {
2700
				$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2701
				throw $e;
2702
			}
2703
		}
2704
	}
2705
2706
	final public function setTransactionListener( $name, callable $callback = null ) {
2707
		if ( $callback ) {
2708
			$this->mTrxRecurringCallbacks[$name] = [ $callback, wfGetCaller() ];
2709
		} else {
2710
			unset( $this->mTrxRecurringCallbacks[$name] );
2711
		}
2712
	}
2713
2714
	/**
2715
	 * Whether to disable running of post-COMMIT/ROLLBACK callbacks
2716
	 *
2717
	 * This method should not be used outside of Database/LoadBalancer
2718
	 *
2719
	 * @param bool $suppress
2720
	 * @since 1.28
2721
	 */
2722
	final public function setTrxEndCallbackSuppression( $suppress ) {
2723
		$this->mTrxEndCallbacksSuppressed = $suppress;
2724
	}
2725
2726
	/**
2727
	 * Actually run and consume any "on transaction idle/resolution" callbacks.
2728
	 *
2729
	 * This method should not be used outside of Database/LoadBalancer
2730
	 *
2731
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2732
	 * @since 1.20
2733
	 * @throws Exception
2734
	 */
2735
	public function runOnTransactionIdleCallbacks( $trigger ) {
2736
		if ( $this->mTrxEndCallbacksSuppressed ) {
2737
			return;
2738
		}
2739
2740
		$autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
2741
		/** @var Exception $e */
2742
		$e = null; // first exception
2743
		do { // callbacks may add callbacks :)
2744
			$callbacks = array_merge(
2745
				$this->mTrxIdleCallbacks,
2746
				$this->mTrxEndCallbacks // include "transaction resolution" callbacks
2747
			);
2748
			$this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2749
			$this->mTrxEndCallbacks = []; // consumed (recursion guard)
2750
			foreach ( $callbacks as $callback ) {
2751
				try {
2752
					list( $phpCallback ) = $callback;
2753
					$this->clearFlag( DBO_TRX ); // make each query its own transaction
2754
					call_user_func_array( $phpCallback, [ $trigger ] );
2755
					if ( $autoTrx ) {
2756
						$this->setFlag( DBO_TRX ); // restore automatic begin()
2757
					} else {
2758
						$this->clearFlag( DBO_TRX ); // restore auto-commit
2759
					}
2760
				} catch ( Exception $ex ) {
2761
					MWExceptionHandler::logException( $ex );
2762
					$e = $e ?: $ex;
2763
					// Some callbacks may use startAtomic/endAtomic, so make sure
2764
					// their transactions are ended so other callbacks don't fail
2765
					if ( $this->trxLevel() ) {
2766
						$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2767
					}
2768
				}
2769
			}
2770
		} while ( count( $this->mTrxIdleCallbacks ) );
2771
2772
		if ( $e instanceof Exception ) {
2773
			throw $e; // re-throw any first exception
2774
		}
2775
	}
2776
2777
	/**
2778
	 * Actually run and consume any "on transaction pre-commit" callbacks.
2779
	 *
2780
	 * This method should not be used outside of Database/LoadBalancer
2781
	 *
2782
	 * @since 1.22
2783
	 * @throws Exception
2784
	 */
2785
	public function runOnTransactionPreCommitCallbacks() {
2786
		$e = null; // first exception
2787
		do { // callbacks may add callbacks :)
2788
			$callbacks = $this->mTrxPreCommitCallbacks;
2789
			$this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2790 View Code Duplication
			foreach ( $callbacks as $callback ) {
2791
				try {
2792
					list( $phpCallback ) = $callback;
2793
					call_user_func( $phpCallback );
2794
				} catch ( Exception $ex ) {
2795
					MWExceptionHandler::logException( $ex );
2796
					$e = $e ?: $ex;
2797
				}
2798
			}
2799
		} while ( count( $this->mTrxPreCommitCallbacks ) );
2800
2801
		if ( $e instanceof Exception ) {
2802
			throw $e; // re-throw any first exception
2803
		}
2804
	}
2805
2806
	/**
2807
	 * Actually run any "transaction listener" callbacks.
2808
	 *
2809
	 * This method should not be used outside of Database/LoadBalancer
2810
	 *
2811
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2812
	 * @throws Exception
2813
	 * @since 1.20
2814
	 */
2815
	public function runTransactionListenerCallbacks( $trigger ) {
2816
		if ( $this->mTrxEndCallbacksSuppressed ) {
2817
			return;
2818
		}
2819
2820
		/** @var Exception $e */
2821
		$e = null; // first exception
2822
2823 View Code Duplication
		foreach ( $this->mTrxRecurringCallbacks as $callback ) {
2824
			try {
2825
				list( $phpCallback ) = $callback;
2826
				$phpCallback( $trigger, $this );
2827
			} catch ( Exception $ex ) {
2828
				MWExceptionHandler::logException( $ex );
2829
				$e = $e ?: $ex;
2830
			}
2831
		}
2832
2833
		if ( $e instanceof Exception ) {
2834
			throw $e; // re-throw any first exception
2835
		}
2836
	}
2837
2838
	final public function startAtomic( $fname = __METHOD__ ) {
2839
		if ( !$this->mTrxLevel ) {
2840
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2841
			// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2842
			// in all changes being in one transaction to keep requests transactional.
2843
			if ( !$this->getFlag( DBO_TRX ) ) {
2844
				$this->mTrxAutomaticAtomic = true;
2845
			}
2846
		}
2847
2848
		$this->mTrxAtomicLevels[] = $fname;
2849
	}
2850
2851
	final public function endAtomic( $fname = __METHOD__ ) {
2852
		if ( !$this->mTrxLevel ) {
2853
			throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2854
		}
2855
		if ( !$this->mTrxAtomicLevels ||
2856
			array_pop( $this->mTrxAtomicLevels ) !== $fname
2857
		) {
2858
			throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2859
		}
2860
2861
		if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2862
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2863
		}
2864
	}
2865
2866
	final public function doAtomicSection( $fname, callable $callback ) {
2867
		$this->startAtomic( $fname );
2868
		try {
2869
			$res = call_user_func_array( $callback, [ $this, $fname ] );
2870
		} catch ( Exception $e ) {
2871
			$this->rollback( $fname, self::FLUSHING_INTERNAL );
2872
			throw $e;
2873
		}
2874
		$this->endAtomic( $fname );
2875
2876
		return $res;
2877
	}
2878
2879
	final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2880
		// Protect against mismatched atomic section, transaction nesting, and snapshot loss
2881
		if ( $this->mTrxLevel ) {
2882
			if ( $this->mTrxAtomicLevels ) {
2883
				$levels = implode( ', ', $this->mTrxAtomicLevels );
2884
				$msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2885
				throw new DBUnexpectedError( $this, $msg );
2886
			} elseif ( !$this->mTrxAutomatic ) {
2887
				$msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2888
				throw new DBUnexpectedError( $this, $msg );
2889
			} else {
2890
				// @TODO: make this an exception at some point
2891
				$msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2892
				wfLogDBError( $msg );
2893
				wfWarn( $msg );
2894
				return; // join the main transaction set
2895
			}
2896
		} elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2897
			// @TODO: make this an exception at some point
2898
			$msg = "$fname: Implicit transaction expected (DBO_TRX set).";
2899
			wfLogDBError( $msg );
2900
			wfWarn( $msg );
2901
			return; // let any writes be in the main transaction
2902
		}
2903
2904
		// Avoid fatals if close() was called
2905
		$this->assertOpen();
2906
2907
		$this->doBegin( $fname );
2908
		$this->mTrxTimestamp = microtime( true );
2909
		$this->mTrxFname = $fname;
2910
		$this->mTrxDoneWrites = false;
2911
		$this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
2912
		$this->mTrxAutomaticAtomic = false;
2913
		$this->mTrxAtomicLevels = [];
2914
		$this->mTrxShortId = wfRandomString( 12 );
2915
		$this->mTrxWriteDuration = 0.0;
2916
		$this->mTrxWriteQueryCount = 0;
2917
		$this->mTrxWriteAdjDuration = 0.0;
2918
		$this->mTrxWriteAdjQueryCount = 0;
2919
		$this->mTrxWriteCallers = [];
2920
		// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2921
		// Get an estimate of the replica DB lag before then, treating estimate staleness
2922
		// as lag itself just to be safe
2923
		$status = $this->getApproximateLagStatus();
2924
		$this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
0 ignored issues
show
Documentation Bug introduced by
It seems like $status['lag'] + (microt...ue) - $status['since']) can also be of type integer. However, the property $mTrxReplicaLag is declared as type double. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2925
	}
2926
2927
	/**
2928
	 * Issues the BEGIN command to the database server.
2929
	 *
2930
	 * @see DatabaseBase::begin()
2931
	 * @param string $fname
2932
	 */
2933
	protected function doBegin( $fname ) {
2934
		$this->query( 'BEGIN', $fname );
2935
		$this->mTrxLevel = 1;
2936
	}
2937
2938
	final public function commit( $fname = __METHOD__, $flush = '' ) {
2939
		if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2940
			// There are still atomic sections open. This cannot be ignored
2941
			$levels = implode( ', ', $this->mTrxAtomicLevels );
2942
			throw new DBUnexpectedError(
2943
				$this,
2944
				"$fname: Got COMMIT while atomic sections $levels are still open."
2945
			);
2946
		}
2947
2948
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2949
			if ( !$this->mTrxLevel ) {
2950
				return; // nothing to do
2951
			} elseif ( !$this->mTrxAutomatic ) {
2952
				throw new DBUnexpectedError(
2953
					$this,
2954
					"$fname: Flushing an explicit transaction, getting out of sync."
2955
				);
2956
			}
2957
		} else {
2958
			if ( !$this->mTrxLevel ) {
2959
				wfWarn( "$fname: No transaction to commit, something got out of sync." );
2960
				return; // nothing to do
2961
			} elseif ( $this->mTrxAutomatic ) {
2962
				// @TODO: make this an exception at some point
2963
				$msg = "$fname: Explicit commit of implicit transaction.";
2964
				wfLogDBError( $msg );
2965
				wfWarn( $msg );
2966
				return; // wait for the main transaction set commit round
2967
			}
2968
		}
2969
2970
		// Avoid fatals if close() was called
2971
		$this->assertOpen();
2972
2973
		$this->runOnTransactionPreCommitCallbacks();
2974
		$writeTime = $this->pendingWriteQueryDuration();
2975
		$this->doCommit( $fname );
2976
		if ( $this->mTrxDoneWrites ) {
2977
			$this->mDoneWrites = microtime( true );
0 ignored issues
show
Documentation Bug introduced by
The property $mDoneWrites was declared of type boolean, but microtime(true) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
2978
			$this->getTransactionProfiler()->transactionWritingOut(
2979
				$this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
0 ignored issues
show
Security Bug introduced by
It seems like $writeTime defined by $this->pendingWriteQueryDuration() on line 2974 can also be of type false; however, TransactionProfiler::transactionWritingOut() does only seem to accept double, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2980
		}
2981
2982
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2983
		$this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
2984
	}
2985
2986
	/**
2987
	 * Issues the COMMIT command to the database server.
2988
	 *
2989
	 * @see DatabaseBase::commit()
2990
	 * @param string $fname
2991
	 */
2992
	protected function doCommit( $fname ) {
2993
		if ( $this->mTrxLevel ) {
2994
			$this->query( 'COMMIT', $fname );
2995
			$this->mTrxLevel = 0;
2996
		}
2997
	}
2998
2999
	final public function rollback( $fname = __METHOD__, $flush = '' ) {
3000
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
3001
			if ( !$this->mTrxLevel ) {
3002
				return; // nothing to do
3003
			}
3004
		} else {
3005
			if ( !$this->mTrxLevel ) {
3006
				wfWarn( "$fname: No transaction to rollback, something got out of sync." );
3007
				return; // nothing to do
3008
			} elseif ( $this->getFlag( DBO_TRX ) ) {
3009
				throw new DBUnexpectedError(
3010
					$this,
3011
					"$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
3012
				);
3013
			}
3014
		}
3015
3016
		// Avoid fatals if close() was called
3017
		$this->assertOpen();
3018
3019
		$this->doRollback( $fname );
3020
		$this->mTrxAtomicLevels = [];
3021
		if ( $this->mTrxDoneWrites ) {
3022
			$this->getTransactionProfiler()->transactionWritingOut(
3023
				$this->mServer, $this->mDBname, $this->mTrxShortId );
3024
		}
3025
3026
		$this->mTrxIdleCallbacks = []; // clear
3027
		$this->mTrxPreCommitCallbacks = []; // clear
3028
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
3029
		$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
3030
	}
3031
3032
	/**
3033
	 * Issues the ROLLBACK command to the database server.
3034
	 *
3035
	 * @see DatabaseBase::rollback()
3036
	 * @param string $fname
3037
	 */
3038
	protected function doRollback( $fname ) {
3039
		if ( $this->mTrxLevel ) {
3040
			# Disconnects cause rollback anyway, so ignore those errors
3041
			$ignoreErrors = true;
3042
			$this->query( 'ROLLBACK', $fname, $ignoreErrors );
3043
			$this->mTrxLevel = 0;
3044
		}
3045
	}
3046
3047
	public function clearSnapshot( $fname = __METHOD__ ) {
3048
		if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
3049
			// This only flushes transactions to clear snapshots, not to write data
3050
			throw new DBUnexpectedError(
3051
				$this,
3052
				"$fname: Cannot COMMIT to clear snapshot because writes are pending."
3053
			);
3054
		}
3055
3056
		$this->commit( $fname, self::FLUSHING_INTERNAL );
3057
	}
3058
3059
	public function explicitTrxActive() {
3060
		return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
3061
	}
3062
3063
	/**
3064
	 * Creates a new table with structure copied from existing table
3065
	 * Note that unlike most database abstraction functions, this function does not
3066
	 * automatically append database prefix, because it works at a lower
3067
	 * abstraction level.
3068
	 * The table names passed to this function shall not be quoted (this
3069
	 * function calls addIdentifierQuotes when needed).
3070
	 *
3071
	 * @param string $oldName Name of table whose structure should be copied
3072
	 * @param string $newName Name of table to be created
3073
	 * @param bool $temporary Whether the new table should be temporary
3074
	 * @param string $fname Calling function name
3075
	 * @throws MWException
3076
	 * @return bool True if operation was successful
3077
	 */
3078
	public function duplicateTableStructure( $oldName, $newName, $temporary = false,
3079
		$fname = __METHOD__
3080
	) {
3081
		throw new MWException(
3082
			'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
3083
	}
3084
3085
	function listTables( $prefix = null, $fname = __METHOD__ ) {
3086
		throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
3087
	}
3088
3089
	/**
3090
	 * Reset the views process cache set by listViews()
3091
	 * @since 1.22
3092
	 */
3093
	final public function clearViewsCache() {
3094
		$this->allViews = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array<integer,string> of property $allViews.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
3095
	}
3096
3097
	/**
3098
	 * Lists all the VIEWs in the database
3099
	 *
3100
	 * For caching purposes the list of all views should be stored in
3101
	 * $this->allViews. The process cache can be cleared with clearViewsCache()
3102
	 *
3103
	 * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
3104
	 * @param string $fname Name of calling function
3105
	 * @throws MWException
3106
	 * @return array
3107
	 * @since 1.22
3108
	 */
3109
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
3110
		throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
3111
	}
3112
3113
	/**
3114
	 * Differentiates between a TABLE and a VIEW
3115
	 *
3116
	 * @param string $name Name of the database-structure to test.
3117
	 * @throws MWException
3118
	 * @return bool
3119
	 * @since 1.22
3120
	 */
3121
	public function isView( $name ) {
3122
		throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
3123
	}
3124
3125
	public function timestamp( $ts = 0 ) {
3126
		return wfTimestamp( TS_MW, $ts );
3127
	}
3128
3129
	public function timestampOrNull( $ts = null ) {
3130
		if ( is_null( $ts ) ) {
3131
			return null;
3132
		} else {
3133
			return $this->timestamp( $ts );
3134
		}
3135
	}
3136
3137
	/**
3138
	 * Take the result from a query, and wrap it in a ResultWrapper if
3139
	 * necessary. Boolean values are passed through as is, to indicate success
3140
	 * of write queries or failure.
3141
	 *
3142
	 * Once upon a time, DatabaseBase::query() returned a bare MySQL result
3143
	 * resource, and it was necessary to call this function to convert it to
3144
	 * a wrapper. Nowadays, raw database objects are never exposed to external
3145
	 * callers, so this is unnecessary in external code.
3146
	 *
3147
	 * @param bool|ResultWrapper|resource|object $result
3148
	 * @return bool|ResultWrapper
3149
	 */
3150
	protected function resultObject( $result ) {
3151
		if ( !$result ) {
3152
			return false;
3153
		} elseif ( $result instanceof ResultWrapper ) {
3154
			return $result;
3155
		} elseif ( $result === true ) {
3156
			// Successful write query
3157
			return $result;
3158
		} else {
3159
			return new ResultWrapper( $this, $result );
3160
		}
3161
	}
3162
3163
	public function ping( &$rtt = null ) {
3164
		// Avoid hitting the server if it was hit recently
3165
		if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
3166
			if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
3167
				$rtt = $this->mRTTEstimate;
3168
				return true; // don't care about $rtt
3169
			}
3170
		}
3171
3172
		// This will reconnect if possible or return false if not
3173
		$this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
3174
		$ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
3175
		$this->restoreFlags( self::RESTORE_PRIOR );
3176
3177
		if ( $ok ) {
3178
			$rtt = $this->mRTTEstimate;
3179
		}
3180
3181
		return $ok;
3182
	}
3183
3184
	/**
3185
	 * @return bool
3186
	 */
3187
	protected function reconnect() {
3188
		$this->closeConnection();
3189
		$this->mOpened = false;
3190
		$this->mConn = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type resource of property $mConn.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
3191
		try {
3192
			$this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
3193
			$this->lastPing = microtime( true );
3194
			$ok = true;
3195
		} catch ( DBConnectionError $e ) {
3196
			$ok = false;
3197
		}
3198
3199
		return $ok;
3200
	}
3201
3202
	public function getSessionLagStatus() {
3203
		return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
3204
	}
3205
3206
	/**
3207
	 * Get the replica DB lag when the current transaction started
3208
	 *
3209
	 * This is useful when transactions might use snapshot isolation
3210
	 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
3211
	 * is this lag plus transaction duration. If they don't, it is still
3212
	 * safe to be pessimistic. This returns null if there is no transaction.
3213
	 *
3214
	 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
3215
	 * @since 1.27
3216
	 */
3217
	public function getTransactionLagStatus() {
3218
		return $this->mTrxLevel
3219
			? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
3220
			: null;
3221
	}
3222
3223
	/**
3224
	 * Get a replica DB lag estimate for this server
3225
	 *
3226
	 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
3227
	 * @since 1.27
3228
	 */
3229
	public function getApproximateLagStatus() {
3230
		return [
3231
			'lag'   => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
3232
			'since' => microtime( true )
3233
		];
3234
	}
3235
3236
	/**
3237
	 * Merge the result of getSessionLagStatus() for several DBs
3238
	 * using the most pessimistic values to estimate the lag of
3239
	 * any data derived from them in combination
3240
	 *
3241
	 * This is information is useful for caching modules
3242
	 *
3243
	 * @see WANObjectCache::set()
3244
	 * @see WANObjectCache::getWithSetCallback()
3245
	 *
3246
	 * @param IDatabase $db1
3247
	 * @param IDatabase ...
3248
	 * @return array Map of values:
3249
	 *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
3250
	 *   - since: oldest UNIX timestamp of any of the DB lag estimates
3251
	 *   - pending: whether any of the DBs have uncommitted changes
3252
	 * @since 1.27
3253
	 */
3254
	public static function getCacheSetOptions( IDatabase $db1 ) {
3255
		$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3256
		foreach ( func_get_args() as $db ) {
3257
			/** @var IDatabase $db */
3258
			$status = $db->getSessionLagStatus();
3259
			if ( $status['lag'] === false ) {
3260
				$res['lag'] = false;
3261
			} elseif ( $res['lag'] !== false ) {
3262
				$res['lag'] = max( $res['lag'], $status['lag'] );
3263
			}
3264
			$res['since'] = min( $res['since'], $status['since'] );
3265
			$res['pending'] = $res['pending'] ?: $db->writesPending();
3266
		}
3267
3268
		return $res;
3269
	}
3270
3271
	public function getLag() {
3272
		return 0;
3273
	}
3274
3275
	function maxListLen() {
3276
		return 0;
3277
	}
3278
3279
	public function encodeBlob( $b ) {
3280
		return $b;
3281
	}
3282
3283
	public function decodeBlob( $b ) {
3284
		if ( $b instanceof Blob ) {
3285
			$b = $b->fetch();
3286
		}
3287
		return $b;
3288
	}
3289
3290
	public function setSessionOptions( array $options ) {
3291
	}
3292
3293
	/**
3294
	 * Read and execute SQL commands from a file.
3295
	 *
3296
	 * Returns true on success, error string or exception on failure (depending
3297
	 * on object's error ignore settings).
3298
	 *
3299
	 * @param string $filename File name to open
3300
	 * @param bool|callable $lineCallback Optional function called before reading each line
3301
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3302
	 * @param bool|string $fname Calling function name or false if name should be
3303
	 *   generated dynamically using $filename
3304
	 * @param bool|callable $inputCallback Optional function called for each
3305
	 *   complete line sent
3306
	 * @throws Exception|MWException
3307
	 * @return bool|string
3308
	 */
3309
	public function sourceFile(
3310
		$filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
3311
	) {
3312
		MediaWiki\suppressWarnings();
3313
		$fp = fopen( $filename, 'r' );
3314
		MediaWiki\restoreWarnings();
3315
3316
		if ( false === $fp ) {
3317
			throw new MWException( "Could not open \"{$filename}\".\n" );
3318
		}
3319
3320
		if ( !$fname ) {
3321
			$fname = __METHOD__ . "( $filename )";
3322
		}
3323
3324
		try {
3325
			$error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
0 ignored issues
show
Bug introduced by
It seems like $fname defined by parameter $fname on line 3310 can also be of type boolean; however, DatabaseBase::sourceStream() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
3326
		} catch ( Exception $e ) {
3327
			fclose( $fp );
3328
			throw $e;
3329
		}
3330
3331
		fclose( $fp );
3332
3333
		return $error;
3334
	}
3335
3336
	/**
3337
	 * Get the full path of a patch file. Originally based on archive()
3338
	 * from updaters.inc. Keep in mind this always returns a patch, as
3339
	 * it fails back to MySQL if no DB-specific patch can be found
3340
	 *
3341
	 * @param string $patch The name of the patch, like patch-something.sql
3342
	 * @return string Full path to patch file
3343
	 */
3344 View Code Duplication
	public function patchPath( $patch ) {
3345
		global $IP;
3346
3347
		$dbType = $this->getType();
3348
		if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
3349
			return "$IP/maintenance/$dbType/archives/$patch";
3350
		} else {
3351
			return "$IP/maintenance/archives/$patch";
3352
		}
3353
	}
3354
3355
	public function setSchemaVars( $vars ) {
3356
		$this->mSchemaVars = $vars;
3357
	}
3358
3359
	/**
3360
	 * Read and execute commands from an open file handle.
3361
	 *
3362
	 * Returns true on success, error string or exception on failure (depending
3363
	 * on object's error ignore settings).
3364
	 *
3365
	 * @param resource $fp File handle
3366
	 * @param bool|callable $lineCallback Optional function called before reading each query
3367
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3368
	 * @param string $fname Calling function name
3369
	 * @param bool|callable $inputCallback Optional function called for each complete query sent
3370
	 * @return bool|string
3371
	 */
3372
	public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
3373
		$fname = __METHOD__, $inputCallback = false
3374
	) {
3375
		$cmd = '';
3376
3377
		while ( !feof( $fp ) ) {
3378
			if ( $lineCallback ) {
3379
				call_user_func( $lineCallback );
3380
			}
3381
3382
			$line = trim( fgets( $fp ) );
3383
3384
			if ( $line == '' ) {
3385
				continue;
3386
			}
3387
3388
			if ( '-' == $line[0] && '-' == $line[1] ) {
3389
				continue;
3390
			}
3391
3392
			if ( $cmd != '' ) {
3393
				$cmd .= ' ';
3394
			}
3395
3396
			$done = $this->streamStatementEnd( $cmd, $line );
3397
3398
			$cmd .= "$line\n";
3399
3400
			if ( $done || feof( $fp ) ) {
3401
				$cmd = $this->replaceVars( $cmd );
3402
3403
				if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
3404
					$res = $this->query( $cmd, $fname );
3405
3406
					if ( $resultCallback ) {
3407
						call_user_func( $resultCallback, $res, $this );
3408
					}
3409
3410
					if ( false === $res ) {
3411
						$err = $this->lastError();
3412
3413
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3414
					}
3415
				}
3416
				$cmd = '';
3417
			}
3418
		}
3419
3420
		return true;
3421
	}
3422
3423
	/**
3424
	 * Called by sourceStream() to check if we've reached a statement end
3425
	 *
3426
	 * @param string $sql SQL assembled so far
3427
	 * @param string $newLine New line about to be added to $sql
3428
	 * @return bool Whether $newLine contains end of the statement
3429
	 */
3430
	public function streamStatementEnd( &$sql, &$newLine ) {
3431
		if ( $this->delimiter ) {
3432
			$prev = $newLine;
3433
			$newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3434
			if ( $newLine != $prev ) {
3435
				return true;
3436
			}
3437
		}
3438
3439
		return false;
3440
	}
3441
3442
	/**
3443
	 * Database independent variable replacement. Replaces a set of variables
3444
	 * in an SQL statement with their contents as given by $this->getSchemaVars().
3445
	 *
3446
	 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3447
	 *
3448
	 * - '{$var}' should be used for text and is passed through the database's
3449
	 *   addQuotes method.
3450
	 * - `{$var}` should be used for identifiers (e.g. table and database names).
3451
	 *   It is passed through the database's addIdentifierQuotes method which
3452
	 *   can be overridden if the database uses something other than backticks.
3453
	 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3454
	 *   database's tableName method.
3455
	 * - / *i* / passes the name that follows through the database's indexName method.
3456
	 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3457
	 *   its use should be avoided. In 1.24 and older, string encoding was applied.
3458
	 *
3459
	 * @param string $ins SQL statement to replace variables in
3460
	 * @return string The new SQL statement with variables replaced
3461
	 */
3462
	protected function replaceVars( $ins ) {
3463
		$vars = $this->getSchemaVars();
3464
		return preg_replace_callback(
3465
			'!
3466
				/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3467
				\'\{\$ (\w+) }\'                  | # 3. addQuotes
3468
				`\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
3469
				/\*\$ (\w+) \*/                     # 5. leave unencoded
3470
			!x',
3471
			function ( $m ) use ( $vars ) {
3472
				// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3473
				// check for both nonexistent keys *and* the empty string.
3474
				if ( isset( $m[1] ) && $m[1] !== '' ) {
3475
					if ( $m[1] === 'i' ) {
3476
						return $this->indexName( $m[2] );
3477
					} else {
3478
						return $this->tableName( $m[2] );
3479
					}
3480 View Code Duplication
				} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3481
					return $this->addQuotes( $vars[$m[3]] );
3482
				} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3483
					return $this->addIdentifierQuotes( $vars[$m[4]] );
3484 View Code Duplication
				} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3485
					return $vars[$m[5]];
3486
				} else {
3487
					return $m[0];
3488
				}
3489
			},
3490
			$ins
3491
		);
3492
	}
3493
3494
	/**
3495
	 * Get schema variables. If none have been set via setSchemaVars(), then
3496
	 * use some defaults from the current object.
3497
	 *
3498
	 * @return array
3499
	 */
3500
	protected function getSchemaVars() {
3501
		if ( $this->mSchemaVars ) {
3502
			return $this->mSchemaVars;
3503
		} else {
3504
			return $this->getDefaultSchemaVars();
3505
		}
3506
	}
3507
3508
	/**
3509
	 * Get schema variables to use if none have been set via setSchemaVars().
3510
	 *
3511
	 * Override this in derived classes to provide variables for tables.sql
3512
	 * and SQL patch files.
3513
	 *
3514
	 * @return array
3515
	 */
3516
	protected function getDefaultSchemaVars() {
3517
		return [];
3518
	}
3519
3520
	public function lockIsFree( $lockName, $method ) {
3521
		return true;
3522
	}
3523
3524
	public function lock( $lockName, $method, $timeout = 5 ) {
3525
		$this->mNamedLocksHeld[$lockName] = 1;
3526
3527
		return true;
3528
	}
3529
3530
	public function unlock( $lockName, $method ) {
3531
		unset( $this->mNamedLocksHeld[$lockName] );
3532
3533
		return true;
3534
	}
3535
3536
	public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3537
		if ( $this->writesOrCallbacksPending() ) {
3538
			// This only flushes transactions to clear snapshots, not to write data
3539
			throw new DBUnexpectedError(
3540
				$this,
3541
				"$fname: Cannot COMMIT to clear snapshot because writes are pending."
3542
			);
3543
		}
3544
3545
		if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3546
			return null;
3547
		}
3548
3549
		$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3550
			if ( $this->trxLevel() ) {
3551
				// There is a good chance an exception was thrown, causing any early return
3552
				// from the caller. Let any error handler get a chance to issue rollback().
3553
				// If there isn't one, let the error bubble up and trigger server-side rollback.
3554
				$this->onTransactionResolution( function () use ( $lockKey, $fname ) {
3555
					$this->unlock( $lockKey, $fname );
3556
				} );
3557
			} else {
3558
				$this->unlock( $lockKey, $fname );
3559
			}
3560
		} );
3561
3562
		$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
3563
3564
		return $unlocker;
3565
	}
3566
3567
	public function namedLocksEnqueue() {
3568
		return false;
3569
	}
3570
3571
	/**
3572
	 * Lock specific tables
3573
	 *
3574
	 * @param array $read Array of tables to lock for read access
3575
	 * @param array $write Array of tables to lock for write access
3576
	 * @param string $method Name of caller
3577
	 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3578
	 * @return bool
3579
	 */
3580
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
3581
		return true;
3582
	}
3583
3584
	/**
3585
	 * Unlock specific tables
3586
	 *
3587
	 * @param string $method The caller
3588
	 * @return bool
3589
	 */
3590
	public function unlockTables( $method ) {
3591
		return true;
3592
	}
3593
3594
	/**
3595
	 * Delete a table
3596
	 * @param string $tableName
3597
	 * @param string $fName
3598
	 * @return bool|ResultWrapper
3599
	 * @since 1.18
3600
	 */
3601 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
3602
		if ( !$this->tableExists( $tableName, $fName ) ) {
3603
			return false;
3604
		}
3605
		$sql = "DROP TABLE " . $this->tableName( $tableName );
3606
		if ( $this->cascadingDeletes() ) {
3607
			$sql .= " CASCADE";
3608
		}
3609
3610
		return $this->query( $sql, $fName );
3611
	}
3612
3613
	/**
3614
	 * Get search engine class. All subclasses of this need to implement this
3615
	 * if they wish to use searching.
3616
	 *
3617
	 * @return string
3618
	 */
3619
	public function getSearchEngine() {
3620
		return 'SearchEngineDummy';
3621
	}
3622
3623
	public function getInfinity() {
3624
		return 'infinity';
3625
	}
3626
3627
	public function encodeExpiry( $expiry ) {
3628
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3629
			? $this->getInfinity()
3630
			: $this->timestamp( $expiry );
3631
	}
3632
3633
	public function decodeExpiry( $expiry, $format = TS_MW ) {
3634
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3635
			? 'infinity'
3636
			: wfTimestamp( $format, $expiry );
3637
	}
3638
3639
	public function setBigSelects( $value = true ) {
3640
		// no-op
3641
	}
3642
3643
	public function isReadOnly() {
3644
		return ( $this->getReadOnlyReason() !== false );
3645
	}
3646
3647
	/**
3648
	 * @return string|bool Reason this DB is read-only or false if it is not
3649
	 */
3650
	protected function getReadOnlyReason() {
3651
		$reason = $this->getLBInfo( 'readOnlyReason' );
3652
3653
		return is_string( $reason ) ? $reason : false;
3654
	}
3655
3656
	/**
3657
	 * @since 1.19
3658
	 * @return string
3659
	 */
3660
	public function __toString() {
3661
		return (string)$this->mConn;
3662
	}
3663
3664
	/**
3665
	 * Run a few simple sanity checks
3666
	 */
3667
	public function __destruct() {
3668
		if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3669
			trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3670
		}
3671
		$danglingCallbacks = array_merge(
3672
			$this->mTrxIdleCallbacks,
3673
			$this->mTrxPreCommitCallbacks,
3674
			$this->mTrxEndCallbacks
3675
		);
3676
		if ( $danglingCallbacks ) {
3677
			$callers = [];
3678
			foreach ( $danglingCallbacks as $callbackInfo ) {
3679
				$callers[] = $callbackInfo[1];
3680
			}
3681
			$callers = implode( ', ', $callers );
3682
			trigger_error( "DB transaction callbacks still pending (from $callers)." );
3683
		}
3684
	}
3685
}
3686
3687
/**
3688
 * @since 1.27
3689
 */
3690
abstract class Database extends DatabaseBase {
3691
	// B/C until nothing type hints for DatabaseBase
3692
	// @TODO: finish renaming DatabaseBase => Database
3693
}
3694