Completed
Branch master (90e9fc)
by
unknown
29:23
created

DatabaseBase::nativeInsertSelect()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 39
Code Lines 23

Duplication

Lines 11
Ratio 28.21 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 23
nc 24
nop 7
dl 11
loc 39
rs 8.439
c 1
b 0
f 0
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 $mTrxSlaveLag = 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
		$indicativeOfSlaveRuntime = true;
1032
		if ( $runtime > self::SLOW_WRITE_SEC ) {
1033
			$verb = $this->getQueryVerb( $sql );
1034
			// insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
1035
			if ( $verb === 'INSERT' ) {
1036
				$indicativeOfSlaveRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
1037
			} elseif ( $verb === 'REPLACE' ) {
1038
				$indicativeOfSlaveRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
1039
			}
1040
		}
1041
1042
		$this->mTrxWriteDuration += $runtime;
1043
		$this->mTrxWriteQueryCount += 1;
1044
		if ( $indicativeOfSlaveRuntime ) {
1045
			$this->mTrxWriteAdjDuration += $runtime;
1046
			$this->mTrxWriteAdjQueryCount += 1;
1047
		}
1048
	}
1049
1050
	private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
1051
		# Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1052
		# Dropped connections also mean that named locks are automatically released.
1053
		# Only allow error suppression in autocommit mode or when the lost transaction
1054
		# didn't matter anyway (aside from DBO_TRX snapshot loss).
1055
		if ( $this->mNamedLocksHeld ) {
1056
			return false; // possible critical section violation
1057
		} elseif ( $sql === 'COMMIT' ) {
1058
			return !$priorWritesPending; // nothing written anyway? (T127428)
1059
		} elseif ( $sql === 'ROLLBACK' ) {
1060
			return true; // transaction lost...which is also what was requested :)
1061
		} elseif ( $this->explicitTrxActive() ) {
1062
			return false; // don't drop atomocity
1063
		} elseif ( $priorWritesPending ) {
1064
			return false; // prior writes lost from implicit transaction
1065
		}
1066
1067
		return true;
1068
	}
1069
1070
	private function handleTransactionLoss() {
1071
		$this->mTrxLevel = 0;
1072
		$this->mTrxIdleCallbacks = []; // bug 65263
1073
		$this->mTrxPreCommitCallbacks = []; // bug 65263
1074
		try {
1075
			// Handle callbacks in mTrxEndCallbacks
1076
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1077
			$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1078
			return null;
1079
		} catch ( Exception $e ) {
1080
			// Already logged; move on...
1081
			return $e;
1082
		}
1083
	}
1084
1085
	public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1086
		if ( $this->ignoreErrors() || $tempIgnore ) {
1087
			wfDebug( "SQL ERROR (ignored): $error\n" );
1088
		} else {
1089
			$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1090
			wfLogDBError(
1091
				"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1092
				$this->getLogContext( [
1093
					'method' => __METHOD__,
1094
					'errno' => $errno,
1095
					'error' => $error,
1096
					'sql1line' => $sql1line,
1097
					'fname' => $fname,
1098
				] )
1099
			);
1100
			wfDebug( "SQL ERROR: " . $error . "\n" );
1101
			throw new DBQueryError( $this, $error, $errno, $sql, $fname );
1102
		}
1103
	}
1104
1105
	/**
1106
	 * Intended to be compatible with the PEAR::DB wrapper functions.
1107
	 * http://pear.php.net/manual/en/package.database.db.intro-execute.php
1108
	 *
1109
	 * ? = scalar value, quoted as necessary
1110
	 * ! = raw SQL bit (a function for instance)
1111
	 * & = filename; reads the file and inserts as a blob
1112
	 *     (we don't use this though...)
1113
	 *
1114
	 * @param string $sql
1115
	 * @param string $func
1116
	 *
1117
	 * @return array
1118
	 */
1119
	protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
1120
		/* MySQL doesn't support prepared statements (yet), so just
1121
		 * pack up the query for reference. We'll manually replace
1122
		 * the bits later.
1123
		 */
1124
		return [ 'query' => $sql, 'func' => $func ];
1125
	}
1126
1127
	/**
1128
	 * Free a prepared query, generated by prepare().
1129
	 * @param string $prepared
1130
	 */
1131
	protected function freePrepared( $prepared ) {
1132
		/* No-op by default */
1133
	}
1134
1135
	/**
1136
	 * Execute a prepared query with the various arguments
1137
	 * @param string $prepared The prepared sql
1138
	 * @param mixed $args Either an array here, or put scalars as varargs
1139
	 *
1140
	 * @return ResultWrapper
1141
	 */
1142
	public function execute( $prepared, $args = null ) {
1143
		if ( !is_array( $args ) ) {
1144
			# Pull the var args
1145
			$args = func_get_args();
1146
			array_shift( $args );
1147
		}
1148
1149
		$sql = $this->fillPrepared( $prepared['query'], $args );
1150
1151
		return $this->query( $sql, $prepared['func'] );
1152
	}
1153
1154
	/**
1155
	 * For faking prepared SQL statements on DBs that don't support it directly.
1156
	 *
1157
	 * @param string $preparedQuery A 'preparable' SQL statement
1158
	 * @param array $args Array of Arguments to fill it with
1159
	 * @return string Executable SQL
1160
	 */
1161
	public function fillPrepared( $preparedQuery, $args ) {
1162
		reset( $args );
1163
		$this->preparedArgs =& $args;
1164
1165
		return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
1166
			[ &$this, 'fillPreparedArg' ], $preparedQuery );
1167
	}
1168
1169
	/**
1170
	 * preg_callback func for fillPrepared()
1171
	 * The arguments should be in $this->preparedArgs and must not be touched
1172
	 * while we're doing this.
1173
	 *
1174
	 * @param array $matches
1175
	 * @throws DBUnexpectedError
1176
	 * @return string
1177
	 */
1178
	protected function fillPreparedArg( $matches ) {
1179
		switch ( $matches[1] ) {
1180
			case '\\?':
1181
				return '?';
1182
			case '\\!':
1183
				return '!';
1184
			case '\\&':
1185
				return '&';
1186
		}
1187
1188
		list( /* $n */, $arg ) = each( $this->preparedArgs );
1189
1190
		switch ( $matches[1] ) {
1191
			case '?':
1192
				return $this->addQuotes( $arg );
1193
			case '!':
1194
				return $arg;
1195
			case '&':
1196
				# return $this->addQuotes( file_get_contents( $arg ) );
1197
				throw new DBUnexpectedError(
1198
					$this,
1199
					'& mode is not implemented. If it\'s really needed, uncomment the line above.'
1200
				);
1201
			default:
1202
				throw new DBUnexpectedError(
1203
					$this,
1204
					'Received invalid match. This should never happen!'
1205
				);
1206
		}
1207
	}
1208
1209
	public function freeResult( $res ) {
1210
	}
1211
1212
	public function selectField(
1213
		$table, $var, $cond = '', $fname = __METHOD__, $options = []
1214
	) {
1215
		if ( $var === '*' ) { // sanity
1216
			throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1217
		}
1218
1219
		if ( !is_array( $options ) ) {
1220
			$options = [ $options ];
1221
		}
1222
1223
		$options['LIMIT'] = 1;
1224
1225
		$res = $this->select( $table, $var, $cond, $fname, $options );
1226
		if ( $res === false || !$this->numRows( $res ) ) {
1227
			return false;
1228
		}
1229
1230
		$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 1225 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...
1231
1232
		if ( $row !== false ) {
1233
			return reset( $row );
1234
		} else {
1235
			return false;
1236
		}
1237
	}
1238
1239
	public function selectFieldValues(
1240
		$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1241
	) {
1242
		if ( $var === '*' ) { // sanity
1243
			throw new DBUnexpectedError( $this, "Cannot use a * field" );
1244
		} elseif ( !is_string( $var ) ) { // sanity
1245
			throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1246
		}
1247
1248
		if ( !is_array( $options ) ) {
1249
			$options = [ $options ];
1250
		}
1251
1252
		$res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1253
		if ( $res === false ) {
1254
			return false;
1255
		}
1256
1257
		$values = [];
1258
		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...
1259
			$values[] = $row->$var;
1260
		}
1261
1262
		return $values;
1263
	}
1264
1265
	/**
1266
	 * Returns an optional USE INDEX clause to go after the table, and a
1267
	 * string to go at the end of the query.
1268
	 *
1269
	 * @param array $options Associative array of options to be turned into
1270
	 *   an SQL query, valid keys are listed in the function.
1271
	 * @return array
1272
	 * @see DatabaseBase::select()
1273
	 */
1274
	public function makeSelectOptions( $options ) {
1275
		$preLimitTail = $postLimitTail = '';
1276
		$startOpts = '';
1277
1278
		$noKeyOptions = [];
1279
1280
		foreach ( $options as $key => $option ) {
1281
			if ( is_numeric( $key ) ) {
1282
				$noKeyOptions[$option] = true;
1283
			}
1284
		}
1285
1286
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1287
1288
		$preLimitTail .= $this->makeOrderBy( $options );
1289
1290
		// if (isset($options['LIMIT'])) {
1291
		// 	$tailOpts .= $this->limitResult('', $options['LIMIT'],
1292
		// 		isset($options['OFFSET']) ? $options['OFFSET']
1293
		// 		: false);
1294
		// }
1295
1296
		if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1297
			$postLimitTail .= ' FOR UPDATE';
1298
		}
1299
1300
		if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1301
			$postLimitTail .= ' LOCK IN SHARE MODE';
1302
		}
1303
1304
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1305
			$startOpts .= 'DISTINCT';
1306
		}
1307
1308
		# Various MySQL extensions
1309
		if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1310
			$startOpts .= ' /*! STRAIGHT_JOIN */';
1311
		}
1312
1313
		if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1314
			$startOpts .= ' HIGH_PRIORITY';
1315
		}
1316
1317
		if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1318
			$startOpts .= ' SQL_BIG_RESULT';
1319
		}
1320
1321
		if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1322
			$startOpts .= ' SQL_BUFFER_RESULT';
1323
		}
1324
1325
		if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1326
			$startOpts .= ' SQL_SMALL_RESULT';
1327
		}
1328
1329
		if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1330
			$startOpts .= ' SQL_CALC_FOUND_ROWS';
1331
		}
1332
1333
		if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1334
			$startOpts .= ' SQL_CACHE';
1335
		}
1336
1337
		if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1338
			$startOpts .= ' SQL_NO_CACHE';
1339
		}
1340
1341 View Code Duplication
		if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1342
			$useIndex = $this->useIndexClause( $options['USE INDEX'] );
1343
		} else {
1344
			$useIndex = '';
1345
		}
1346
1347
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
1348
	}
1349
1350
	/**
1351
	 * Returns an optional GROUP BY with an optional HAVING
1352
	 *
1353
	 * @param array $options Associative array of options
1354
	 * @return string
1355
	 * @see DatabaseBase::select()
1356
	 * @since 1.21
1357
	 */
1358
	public function makeGroupByWithHaving( $options ) {
1359
		$sql = '';
1360 View Code Duplication
		if ( isset( $options['GROUP BY'] ) ) {
1361
			$gb = is_array( $options['GROUP BY'] )
1362
				? implode( ',', $options['GROUP BY'] )
1363
				: $options['GROUP BY'];
1364
			$sql .= ' GROUP BY ' . $gb;
1365
		}
1366 View Code Duplication
		if ( isset( $options['HAVING'] ) ) {
1367
			$having = is_array( $options['HAVING'] )
1368
				? $this->makeList( $options['HAVING'], LIST_AND )
1369
				: $options['HAVING'];
1370
			$sql .= ' HAVING ' . $having;
1371
		}
1372
1373
		return $sql;
1374
	}
1375
1376
	/**
1377
	 * Returns an optional ORDER BY
1378
	 *
1379
	 * @param array $options Associative array of options
1380
	 * @return string
1381
	 * @see DatabaseBase::select()
1382
	 * @since 1.21
1383
	 */
1384
	public function makeOrderBy( $options ) {
1385 View Code Duplication
		if ( isset( $options['ORDER BY'] ) ) {
1386
			$ob = is_array( $options['ORDER BY'] )
1387
				? implode( ',', $options['ORDER BY'] )
1388
				: $options['ORDER BY'];
1389
1390
			return ' ORDER BY ' . $ob;
1391
		}
1392
1393
		return '';
1394
	}
1395
1396
	// See IDatabase::select for the docs for this function
1397
	public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1398
		$options = [], $join_conds = [] ) {
1399
		$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1400
1401
		return $this->query( $sql, $fname );
1402
	}
1403
1404
	public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1405
		$options = [], $join_conds = []
1406
	) {
1407
		if ( is_array( $vars ) ) {
1408
			$vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1409
		}
1410
1411
		$options = (array)$options;
1412
		$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1413
			? $options['USE INDEX']
1414
			: [];
1415
1416
		if ( is_array( $table ) ) {
1417
			$from = ' FROM ' .
1418
				$this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
1419
		} elseif ( $table != '' ) {
1420
			if ( $table[0] == ' ' ) {
1421
				$from = ' FROM ' . $table;
1422
			} else {
1423
				$from = ' FROM ' .
1424
					$this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
1425
			}
1426
		} else {
1427
			$from = '';
1428
		}
1429
1430
		list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
1431
			$this->makeSelectOptions( $options );
1432
1433
		if ( !empty( $conds ) ) {
1434
			if ( is_array( $conds ) ) {
1435
				$conds = $this->makeList( $conds, LIST_AND );
1436
			}
1437
			$sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
1438
		} else {
1439
			$sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
1440
		}
1441
1442
		if ( isset( $options['LIMIT'] ) ) {
1443
			$sql = $this->limitResult( $sql, $options['LIMIT'],
1444
				isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1445
		}
1446
		$sql = "$sql $postLimitTail";
1447
1448
		if ( isset( $options['EXPLAIN'] ) ) {
1449
			$sql = 'EXPLAIN ' . $sql;
1450
		}
1451
1452
		return $sql;
1453
	}
1454
1455
	public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1456
		$options = [], $join_conds = []
1457
	) {
1458
		$options = (array)$options;
1459
		$options['LIMIT'] = 1;
1460
		$res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1461
1462
		if ( $res === false ) {
1463
			return false;
1464
		}
1465
1466
		if ( !$this->numRows( $res ) ) {
1467
			return false;
1468
		}
1469
1470
		$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 1460 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...
1471
1472
		return $obj;
1473
	}
1474
1475
	public function estimateRowCount(
1476
		$table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1477
	) {
1478
		$rows = 0;
1479
		$res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1480
1481 View Code Duplication
		if ( $res ) {
1482
			$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 1479 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...
1483
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1484
		}
1485
1486
		return $rows;
1487
	}
1488
1489
	public function selectRowCount(
1490
		$tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1491
	) {
1492
		$rows = 0;
1493
		$sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1494
		$res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
1495
1496 View Code Duplication
		if ( $res ) {
1497
			$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 1494 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...
1498
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1499
		}
1500
1501
		return $rows;
1502
	}
1503
1504
	/**
1505
	 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1506
	 * It's only slightly flawed. Don't use for anything important.
1507
	 *
1508
	 * @param string $sql A SQL Query
1509
	 *
1510
	 * @return string
1511
	 */
1512
	protected static function generalizeSQL( $sql ) {
1513
		# This does the same as the regexp below would do, but in such a way
1514
		# as to avoid crashing php on some large strings.
1515
		# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1516
1517
		$sql = str_replace( "\\\\", '', $sql );
1518
		$sql = str_replace( "\\'", '', $sql );
1519
		$sql = str_replace( "\\\"", '', $sql );
1520
		$sql = preg_replace( "/'.*'/s", "'X'", $sql );
1521
		$sql = preg_replace( '/".*"/s', "'X'", $sql );
1522
1523
		# All newlines, tabs, etc replaced by single space
1524
		$sql = preg_replace( '/\s+/', ' ', $sql );
1525
1526
		# All numbers => N,
1527
		# except the ones surrounded by characters, e.g. l10n
1528
		$sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1529
		$sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1530
1531
		return $sql;
1532
	}
1533
1534
	public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1535
		$info = $this->fieldInfo( $table, $field );
1536
1537
		return (bool)$info;
1538
	}
1539
1540
	public function indexExists( $table, $index, $fname = __METHOD__ ) {
1541
		if ( !$this->tableExists( $table ) ) {
1542
			return null;
1543
		}
1544
1545
		$info = $this->indexInfo( $table, $index, $fname );
1546
		if ( is_null( $info ) ) {
1547
			return null;
1548
		} else {
1549
			return $info !== false;
1550
		}
1551
	}
1552
1553
	public function tableExists( $table, $fname = __METHOD__ ) {
1554
		$table = $this->tableName( $table );
1555
		$old = $this->ignoreErrors( true );
1556
		$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
1557
		$this->ignoreErrors( $old );
1558
1559
		return (bool)$res;
1560
	}
1561
1562
	public function indexUnique( $table, $index ) {
1563
		$indexInfo = $this->indexInfo( $table, $index );
1564
1565
		if ( !$indexInfo ) {
1566
			return null;
1567
		}
1568
1569
		return !$indexInfo[0]->Non_unique;
1570
	}
1571
1572
	/**
1573
	 * Helper for DatabaseBase::insert().
1574
	 *
1575
	 * @param array $options
1576
	 * @return string
1577
	 */
1578
	protected function makeInsertOptions( $options ) {
1579
		return implode( ' ', $options );
1580
	}
1581
1582
	public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1583
		# No rows to insert, easy just return now
1584
		if ( !count( $a ) ) {
1585
			return true;
1586
		}
1587
1588
		$table = $this->tableName( $table );
1589
1590
		if ( !is_array( $options ) ) {
1591
			$options = [ $options ];
1592
		}
1593
1594
		$fh = null;
1595
		if ( isset( $options['fileHandle'] ) ) {
1596
			$fh = $options['fileHandle'];
1597
		}
1598
		$options = $this->makeInsertOptions( $options );
1599
1600
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1601
			$multi = true;
1602
			$keys = array_keys( $a[0] );
1603
		} else {
1604
			$multi = false;
1605
			$keys = array_keys( $a );
1606
		}
1607
1608
		$sql = 'INSERT ' . $options .
1609
			" INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1610
1611
		if ( $multi ) {
1612
			$first = true;
1613 View Code Duplication
			foreach ( $a as $row ) {
1614
				if ( $first ) {
1615
					$first = false;
1616
				} else {
1617
					$sql .= ',';
1618
				}
1619
				$sql .= '(' . $this->makeList( $row ) . ')';
1620
			}
1621
		} else {
1622
			$sql .= '(' . $this->makeList( $a ) . ')';
1623
		}
1624
1625
		if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1626
			return false;
1627
		} elseif ( $fh !== null ) {
1628
			return true;
1629
		}
1630
1631
		return (bool)$this->query( $sql, $fname );
1632
	}
1633
1634
	/**
1635
	 * Make UPDATE options array for DatabaseBase::makeUpdateOptions
1636
	 *
1637
	 * @param array $options
1638
	 * @return array
1639
	 */
1640
	protected function makeUpdateOptionsArray( $options ) {
1641
		if ( !is_array( $options ) ) {
1642
			$options = [ $options ];
1643
		}
1644
1645
		$opts = [];
1646
1647
		if ( in_array( 'LOW_PRIORITY', $options ) ) {
1648
			$opts[] = $this->lowPriorityOption();
1649
		}
1650
1651
		if ( in_array( 'IGNORE', $options ) ) {
1652
			$opts[] = 'IGNORE';
1653
		}
1654
1655
		return $opts;
1656
	}
1657
1658
	/**
1659
	 * Make UPDATE options for the DatabaseBase::update function
1660
	 *
1661
	 * @param array $options The options passed to DatabaseBase::update
1662
	 * @return string
1663
	 */
1664
	protected function makeUpdateOptions( $options ) {
1665
		$opts = $this->makeUpdateOptionsArray( $options );
1666
1667
		return implode( ' ', $opts );
1668
	}
1669
1670
	function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1671
		$table = $this->tableName( $table );
1672
		$opts = $this->makeUpdateOptions( $options );
1673
		$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
1674
1675 View Code Duplication
		if ( $conds !== [] && $conds !== '*' ) {
1676
			$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
1677
		}
1678
1679
		return $this->query( $sql, $fname );
1680
	}
1681
1682
	public function makeList( $a, $mode = LIST_COMMA ) {
1683
		if ( !is_array( $a ) ) {
1684
			throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
1685
		}
1686
1687
		$first = true;
1688
		$list = '';
1689
1690
		foreach ( $a as $field => $value ) {
1691
			if ( !$first ) {
1692
				if ( $mode == LIST_AND ) {
1693
					$list .= ' AND ';
1694
				} elseif ( $mode == LIST_OR ) {
1695
					$list .= ' OR ';
1696
				} else {
1697
					$list .= ',';
1698
				}
1699
			} else {
1700
				$first = false;
1701
			}
1702
1703
			if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
1704
				$list .= "($value)";
1705
			} elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
1706
				$list .= "$value";
1707
			} elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
1708
				// Remove null from array to be handled separately if found
1709
				$includeNull = false;
1710
				foreach ( array_keys( $value, null, true ) as $nullKey ) {
1711
					$includeNull = true;
1712
					unset( $value[$nullKey] );
1713
				}
1714
				if ( count( $value ) == 0 && !$includeNull ) {
1715
					throw new MWException( __METHOD__ . ": empty input for field $field" );
1716
				} elseif ( count( $value ) == 0 ) {
1717
					// only check if $field is null
1718
					$list .= "$field IS NULL";
1719
				} else {
1720
					// IN clause contains at least one valid element
1721
					if ( $includeNull ) {
1722
						// Group subconditions to ensure correct precedence
1723
						$list .= '(';
1724
					}
1725
					if ( count( $value ) == 1 ) {
1726
						// Special-case single values, as IN isn't terribly efficient
1727
						// Don't necessarily assume the single key is 0; we don't
1728
						// enforce linear numeric ordering on other arrays here.
1729
						$value = array_values( $value )[0];
1730
						$list .= $field . " = " . $this->addQuotes( $value );
1731
					} else {
1732
						$list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1733
					}
1734
					// if null present in array, append IS NULL
1735
					if ( $includeNull ) {
1736
						$list .= " OR $field IS NULL)";
1737
					}
1738
				}
1739
			} elseif ( $value === null ) {
1740 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR ) {
1741
					$list .= "$field IS ";
1742
				} elseif ( $mode == LIST_SET ) {
1743
					$list .= "$field = ";
1744
				}
1745
				$list .= 'NULL';
1746
			} else {
1747 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
1748
					$list .= "$field = ";
1749
				}
1750
				$list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
1751
			}
1752
		}
1753
1754
		return $list;
1755
	}
1756
1757
	public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1758
		$conds = [];
1759
1760
		foreach ( $data as $base => $sub ) {
1761
			if ( count( $sub ) ) {
1762
				$conds[] = $this->makeList(
1763
					[ $baseKey => $base, $subKey => array_keys( $sub ) ],
1764
					LIST_AND );
1765
			}
1766
		}
1767
1768
		if ( $conds ) {
1769
			return $this->makeList( $conds, LIST_OR );
1770
		} else {
1771
			// Nothing to search for...
1772
			return false;
1773
		}
1774
	}
1775
1776
	/**
1777
	 * Return aggregated value alias
1778
	 *
1779
	 * @param array $valuedata
1780
	 * @param string $valuename
1781
	 *
1782
	 * @return string
1783
	 */
1784
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
1785
		return $valuename;
1786
	}
1787
1788
	public function bitNot( $field ) {
1789
		return "(~$field)";
1790
	}
1791
1792
	public function bitAnd( $fieldLeft, $fieldRight ) {
1793
		return "($fieldLeft & $fieldRight)";
1794
	}
1795
1796
	public function bitOr( $fieldLeft, $fieldRight ) {
1797
		return "($fieldLeft | $fieldRight)";
1798
	}
1799
1800
	public function buildConcat( $stringList ) {
1801
		return 'CONCAT(' . implode( ',', $stringList ) . ')';
1802
	}
1803
1804 View Code Duplication
	public function buildGroupConcatField(
1805
		$delim, $table, $field, $conds = '', $join_conds = []
1806
	) {
1807
		$fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1808
1809
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1810
	}
1811
1812
	/**
1813
	 * @param string $field Field or column to cast
1814
	 * @return string
1815
	 * @since 1.28
1816
	 */
1817
	public function buildStringCast( $field ) {
1818
		return $field;
1819
	}
1820
1821
	public function selectDB( $db ) {
1822
		# Stub. Shouldn't cause serious problems if it's not overridden, but
1823
		# if your database engine supports a concept similar to MySQL's
1824
		# databases you may as well.
1825
		$this->mDBname = $db;
1826
1827
		return true;
1828
	}
1829
1830
	public function getDBname() {
1831
		return $this->mDBname;
1832
	}
1833
1834
	public function getServer() {
1835
		return $this->mServer;
1836
	}
1837
1838
	/**
1839
	 * Format a table name ready for use in constructing an SQL query
1840
	 *
1841
	 * This does two important things: it quotes the table names to clean them up,
1842
	 * and it adds a table prefix if only given a table name with no quotes.
1843
	 *
1844
	 * All functions of this object which require a table name call this function
1845
	 * themselves. Pass the canonical name to such functions. This is only needed
1846
	 * when calling query() directly.
1847
	 *
1848
	 * @note This function does not sanitize user input. It is not safe to use
1849
	 *   this function to escape user input.
1850
	 * @param string $name Database table name
1851
	 * @param string $format One of:
1852
	 *   quoted - Automatically pass the table name through addIdentifierQuotes()
1853
	 *            so that it can be used in a query.
1854
	 *   raw - Do not add identifier quotes to the table name
1855
	 * @return string Full database name
1856
	 */
1857
	public function tableName( $name, $format = 'quoted' ) {
1858
		global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema;
1859
		# Skip the entire process when we have a string quoted on both ends.
1860
		# Note that we check the end so that we will still quote any use of
1861
		# use of `database`.table. But won't break things if someone wants
1862
		# to query a database table with a dot in the name.
1863
		if ( $this->isQuotedIdentifier( $name ) ) {
1864
			return $name;
1865
		}
1866
1867
		# Lets test for any bits of text that should never show up in a table
1868
		# name. Basically anything like JOIN or ON which are actually part of
1869
		# SQL queries, but may end up inside of the table value to combine
1870
		# sql. Such as how the API is doing.
1871
		# Note that we use a whitespace test rather than a \b test to avoid
1872
		# any remote case where a word like on may be inside of a table name
1873
		# surrounded by symbols which may be considered word breaks.
1874
		if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1875
			return $name;
1876
		}
1877
1878
		# Split database and table into proper variables.
1879
		# We reverse the explode so that database.table and table both output
1880
		# the correct table.
1881
		$dbDetails = explode( '.', $name, 3 );
1882
		if ( count( $dbDetails ) == 3 ) {
1883
			list( $database, $schema, $table ) = $dbDetails;
1884
			# We don't want any prefix added in this case
1885
			$prefix = '';
1886
		} elseif ( count( $dbDetails ) == 2 ) {
1887
			list( $database, $table ) = $dbDetails;
1888
			# We don't want any prefix added in this case
1889
			# In dbs that support it, $database may actually be the schema
1890
			# but that doesn't affect any of the functionality here
1891
			$prefix = '';
1892
			$schema = null;
1893
		} else {
1894
			list( $table ) = $dbDetails;
1895
			if ( $wgSharedDB !== null # We have a shared database
1896
				&& $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...
1897
				&& !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`'
1898
				&& in_array( $table, $wgSharedTables ) # A shared table is selected
1899
			) {
1900
				$database = $wgSharedDB;
1901
				$schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema;
1902
				$prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
1903
			} else {
1904
				$database = null;
1905
				$schema = $this->mSchema; # Default schema
1906
				$prefix = $this->mTablePrefix; # Default prefix
1907
			}
1908
		}
1909
1910
		# Quote $table and apply the prefix if not quoted.
1911
		# $tableName might be empty if this is called from Database::replaceVars()
1912
		$tableName = "{$prefix}{$table}";
1913
		if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) {
1914
			$tableName = $this->addIdentifierQuotes( $tableName );
1915
		}
1916
1917
		# Quote $schema and merge it with the table name if needed
1918 View Code Duplication
		if ( strlen( $schema ) ) {
1919
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
1920
				$schema = $this->addIdentifierQuotes( $schema );
1921
			}
1922
			$tableName = $schema . '.' . $tableName;
1923
		}
1924
1925
		# Quote $database and merge it with the table name if needed
1926 View Code Duplication
		if ( $database !== null ) {
1927
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
1928
				$database = $this->addIdentifierQuotes( $database );
1929
			}
1930
			$tableName = $database . '.' . $tableName;
1931
		}
1932
1933
		return $tableName;
1934
	}
1935
1936
	/**
1937
	 * Fetch a number of table names into an array
1938
	 * This is handy when you need to construct SQL for joins
1939
	 *
1940
	 * Example:
1941
	 * extract( $dbr->tableNames( 'user', 'watchlist' ) );
1942
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1943
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1944
	 *
1945
	 * @return array
1946
	 */
1947 View Code Duplication
	public function tableNames() {
1948
		$inArray = func_get_args();
1949
		$retVal = [];
1950
1951
		foreach ( $inArray as $name ) {
1952
			$retVal[$name] = $this->tableName( $name );
1953
		}
1954
1955
		return $retVal;
1956
	}
1957
1958
	/**
1959
	 * Fetch a number of table names into an zero-indexed numerical array
1960
	 * This is handy when you need to construct SQL for joins
1961
	 *
1962
	 * Example:
1963
	 * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
1964
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1965
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1966
	 *
1967
	 * @return array
1968
	 */
1969 View Code Duplication
	public function tableNamesN() {
1970
		$inArray = func_get_args();
1971
		$retVal = [];
1972
1973
		foreach ( $inArray as $name ) {
1974
			$retVal[] = $this->tableName( $name );
1975
		}
1976
1977
		return $retVal;
1978
	}
1979
1980
	/**
1981
	 * Get an aliased table name
1982
	 * e.g. tableName AS newTableName
1983
	 *
1984
	 * @param string $name Table name, see tableName()
1985
	 * @param string|bool $alias Alias (optional)
1986
	 * @return string SQL name for aliased table. Will not alias a table to its own name
1987
	 */
1988
	public function tableNameWithAlias( $name, $alias = false ) {
1989
		if ( !$alias || $alias == $name ) {
1990
			return $this->tableName( $name );
1991
		} else {
1992
			return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1988 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...
1993
		}
1994
	}
1995
1996
	/**
1997
	 * Gets an array of aliased table names
1998
	 *
1999
	 * @param array $tables [ [alias] => table ]
2000
	 * @return string[] See tableNameWithAlias()
2001
	 */
2002
	public function tableNamesWithAlias( $tables ) {
2003
		$retval = [];
2004
		foreach ( $tables as $alias => $table ) {
2005
			if ( is_numeric( $alias ) ) {
2006
				$alias = $table;
2007
			}
2008
			$retval[] = $this->tableNameWithAlias( $table, $alias );
2009
		}
2010
2011
		return $retval;
2012
	}
2013
2014
	/**
2015
	 * Get an aliased field name
2016
	 * e.g. fieldName AS newFieldName
2017
	 *
2018
	 * @param string $name Field name
2019
	 * @param string|bool $alias Alias (optional)
2020
	 * @return string SQL name for aliased field. Will not alias a field to its own name
2021
	 */
2022
	public function fieldNameWithAlias( $name, $alias = false ) {
2023
		if ( !$alias || (string)$alias === (string)$name ) {
2024
			return $name;
2025
		} else {
2026
			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 2022 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...
2027
		}
2028
	}
2029
2030
	/**
2031
	 * Gets an array of aliased field names
2032
	 *
2033
	 * @param array $fields [ [alias] => field ]
2034
	 * @return string[] See fieldNameWithAlias()
2035
	 */
2036
	public function fieldNamesWithAlias( $fields ) {
2037
		$retval = [];
2038
		foreach ( $fields as $alias => $field ) {
2039
			if ( is_numeric( $alias ) ) {
2040
				$alias = $field;
2041
			}
2042
			$retval[] = $this->fieldNameWithAlias( $field, $alias );
2043
		}
2044
2045
		return $retval;
2046
	}
2047
2048
	/**
2049
	 * Get the aliased table name clause for a FROM clause
2050
	 * which might have a JOIN and/or USE INDEX clause
2051
	 *
2052
	 * @param array $tables ( [alias] => table )
2053
	 * @param array $use_index Same as for select()
2054
	 * @param array $join_conds Same as for select()
2055
	 * @return string
2056
	 */
2057
	protected function tableNamesWithUseIndexOrJOIN(
2058
		$tables, $use_index = [], $join_conds = []
2059
	) {
2060
		$ret = [];
2061
		$retJOIN = [];
2062
		$use_index = (array)$use_index;
2063
		$join_conds = (array)$join_conds;
2064
2065
		foreach ( $tables as $alias => $table ) {
2066
			if ( !is_string( $alias ) ) {
2067
				// No alias? Set it equal to the table name
2068
				$alias = $table;
2069
			}
2070
			// Is there a JOIN clause for this table?
2071
			if ( isset( $join_conds[$alias] ) ) {
2072
				list( $joinType, $conds ) = $join_conds[$alias];
2073
				$tableClause = $joinType;
2074
				$tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
2075
				if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
2076
					$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
2077
					if ( $use != '' ) {
2078
						$tableClause .= ' ' . $use;
2079
					}
2080
				}
2081
				$on = $this->makeList( (array)$conds, LIST_AND );
2082
				if ( $on != '' ) {
2083
					$tableClause .= ' ON (' . $on . ')';
2084
				}
2085
2086
				$retJOIN[] = $tableClause;
2087
			} elseif ( isset( $use_index[$alias] ) ) {
2088
				// Is there an INDEX clause for this table?
2089
				$tableClause = $this->tableNameWithAlias( $table, $alias );
2090
				$tableClause .= ' ' . $this->useIndexClause(
2091
					implode( ',', (array)$use_index[$alias] )
2092
				);
2093
2094
				$ret[] = $tableClause;
2095
			} else {
2096
				$tableClause = $this->tableNameWithAlias( $table, $alias );
2097
2098
				$ret[] = $tableClause;
2099
			}
2100
		}
2101
2102
		// We can't separate explicit JOIN clauses with ',', use ' ' for those
2103
		$implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
2104
		$explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
2105
2106
		// Compile our final table clause
2107
		return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2108
	}
2109
2110
	/**
2111
	 * Get the name of an index in a given table.
2112
	 *
2113
	 * @param string $index
2114
	 * @return string
2115
	 */
2116
	protected function indexName( $index ) {
2117
		// Backwards-compatibility hack
2118
		$renamed = [
2119
			'ar_usertext_timestamp' => 'usertext_timestamp',
2120
			'un_user_id' => 'user_id',
2121
			'un_user_ip' => 'user_ip',
2122
		];
2123
2124
		if ( isset( $renamed[$index] ) ) {
2125
			return $renamed[$index];
2126
		} else {
2127
			return $index;
2128
		}
2129
	}
2130
2131
	public function addQuotes( $s ) {
2132
		if ( $s instanceof Blob ) {
2133
			$s = $s->fetch();
2134
		}
2135
		if ( $s === null ) {
2136
			return 'NULL';
2137
		} else {
2138
			# This will also quote numeric values. This should be harmless,
2139
			# and protects against weird problems that occur when they really
2140
			# _are_ strings such as article titles and string->number->string
2141
			# conversion is not 1:1.
2142
			return "'" . $this->strencode( $s ) . "'";
2143
		}
2144
	}
2145
2146
	/**
2147
	 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
2148
	 * MySQL uses `backticks` while basically everything else uses double quotes.
2149
	 * Since MySQL is the odd one out here the double quotes are our generic
2150
	 * and we implement backticks in DatabaseMysql.
2151
	 *
2152
	 * @param string $s
2153
	 * @return string
2154
	 */
2155
	public function addIdentifierQuotes( $s ) {
2156
		return '"' . str_replace( '"', '""', $s ) . '"';
2157
	}
2158
2159
	/**
2160
	 * Returns if the given identifier looks quoted or not according to
2161
	 * the database convention for quoting identifiers .
2162
	 *
2163
	 * @note Do not use this to determine if untrusted input is safe.
2164
	 *   A malicious user can trick this function.
2165
	 * @param string $name
2166
	 * @return bool
2167
	 */
2168
	public function isQuotedIdentifier( $name ) {
2169
		return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2170
	}
2171
2172
	/**
2173
	 * @param string $s
2174
	 * @return string
2175
	 */
2176
	protected function escapeLikeInternal( $s ) {
2177
		return addcslashes( $s, '\%_' );
2178
	}
2179
2180
	public function buildLike() {
2181
		$params = func_get_args();
2182
2183
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2184
			$params = $params[0];
2185
		}
2186
2187
		$s = '';
2188
2189
		foreach ( $params as $value ) {
2190
			if ( $value instanceof LikeMatch ) {
2191
				$s .= $value->toString();
2192
			} else {
2193
				$s .= $this->escapeLikeInternal( $value );
2194
			}
2195
		}
2196
2197
		return " LIKE {$this->addQuotes( $s )} ";
2198
	}
2199
2200
	public function anyChar() {
2201
		return new LikeMatch( '_' );
2202
	}
2203
2204
	public function anyString() {
2205
		return new LikeMatch( '%' );
2206
	}
2207
2208
	public function nextSequenceValue( $seqName ) {
2209
		return null;
2210
	}
2211
2212
	/**
2213
	 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2214
	 * is only needed because a) MySQL must be as efficient as possible due to
2215
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2216
	 * which index to pick. Anyway, other databases might have different
2217
	 * indexes on a given table. So don't bother overriding this unless you're
2218
	 * MySQL.
2219
	 * @param string $index
2220
	 * @return string
2221
	 */
2222
	public function useIndexClause( $index ) {
2223
		return '';
2224
	}
2225
2226
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2227
		$quotedTable = $this->tableName( $table );
2228
2229
		if ( count( $rows ) == 0 ) {
2230
			return;
2231
		}
2232
2233
		# Single row case
2234
		if ( !is_array( reset( $rows ) ) ) {
2235
			$rows = [ $rows ];
2236
		}
2237
2238
		// @FXIME: this is not atomic, but a trx would break affectedRows()
2239
		foreach ( $rows as $row ) {
2240
			# Delete rows which collide
2241
			if ( $uniqueIndexes ) {
2242
				$sql = "DELETE FROM $quotedTable WHERE ";
2243
				$first = true;
2244
				foreach ( $uniqueIndexes as $index ) {
2245
					if ( $first ) {
2246
						$first = false;
2247
						$sql .= '( ';
2248
					} else {
2249
						$sql .= ' ) OR ( ';
2250
					}
2251
					if ( is_array( $index ) ) {
2252
						$first2 = true;
2253
						foreach ( $index as $col ) {
2254
							if ( $first2 ) {
2255
								$first2 = false;
2256
							} else {
2257
								$sql .= ' AND ';
2258
							}
2259
							$sql .= $col . '=' . $this->addQuotes( $row[$col] );
2260
						}
2261
					} else {
2262
						$sql .= $index . '=' . $this->addQuotes( $row[$index] );
2263
					}
2264
				}
2265
				$sql .= ' )';
2266
				$this->query( $sql, $fname );
2267
			}
2268
2269
			# Now insert the row
2270
			$this->insert( $table, $row, $fname );
2271
		}
2272
	}
2273
2274
	/**
2275
	 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2276
	 * statement.
2277
	 *
2278
	 * @param string $table Table name
2279
	 * @param array|string $rows Row(s) to insert
2280
	 * @param string $fname Caller function name
2281
	 *
2282
	 * @return ResultWrapper
2283
	 */
2284
	protected function nativeReplace( $table, $rows, $fname ) {
2285
		$table = $this->tableName( $table );
2286
2287
		# Single row case
2288
		if ( !is_array( reset( $rows ) ) ) {
2289
			$rows = [ $rows ];
2290
		}
2291
2292
		$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2293
		$first = true;
2294
2295 View Code Duplication
		foreach ( $rows as $row ) {
2296
			if ( $first ) {
2297
				$first = false;
2298
			} else {
2299
				$sql .= ',';
2300
			}
2301
2302
			$sql .= '(' . $this->makeList( $row ) . ')';
2303
		}
2304
2305
		return $this->query( $sql, $fname );
2306
	}
2307
2308
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2309
		$fname = __METHOD__
2310
	) {
2311
		if ( !count( $rows ) ) {
2312
			return true; // nothing to do
2313
		}
2314
2315
		if ( !is_array( reset( $rows ) ) ) {
2316
			$rows = [ $rows ];
2317
		}
2318
2319
		if ( count( $uniqueIndexes ) ) {
2320
			$clauses = []; // list WHERE clauses that each identify a single row
2321
			foreach ( $rows as $row ) {
2322
				foreach ( $uniqueIndexes as $index ) {
2323
					$index = is_array( $index ) ? $index : [ $index ]; // columns
2324
					$rowKey = []; // unique key to this row
2325
					foreach ( $index as $column ) {
2326
						$rowKey[$column] = $row[$column];
2327
					}
2328
					$clauses[] = $this->makeList( $rowKey, LIST_AND );
2329
				}
2330
			}
2331
			$where = [ $this->makeList( $clauses, LIST_OR ) ];
2332
		} else {
2333
			$where = false;
2334
		}
2335
2336
		$useTrx = !$this->mTrxLevel;
2337
		if ( $useTrx ) {
2338
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2339
		}
2340
		try {
2341
			# Update any existing conflicting row(s)
2342
			if ( $where !== false ) {
2343
				$ok = $this->update( $table, $set, $where, $fname );
2344
			} else {
2345
				$ok = true;
2346
			}
2347
			# Now insert any non-conflicting row(s)
2348
			$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2349
		} catch ( Exception $e ) {
2350
			if ( $useTrx ) {
2351
				$this->rollback( $fname, self::FLUSHING_INTERNAL );
2352
			}
2353
			throw $e;
2354
		}
2355
		if ( $useTrx ) {
2356
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2357
		}
2358
2359
		return $ok;
2360
	}
2361
2362 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2363
		$fname = __METHOD__
2364
	) {
2365
		if ( !$conds ) {
2366
			throw new DBUnexpectedError( $this,
2367
				'DatabaseBase::deleteJoin() called with empty $conds' );
2368
		}
2369
2370
		$delTable = $this->tableName( $delTable );
2371
		$joinTable = $this->tableName( $joinTable );
2372
		$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2373
		if ( $conds != '*' ) {
2374
			$sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
2375
		}
2376
		$sql .= ')';
2377
2378
		$this->query( $sql, $fname );
2379
	}
2380
2381
	/**
2382
	 * Returns the size of a text field, or -1 for "unlimited"
2383
	 *
2384
	 * @param string $table
2385
	 * @param string $field
2386
	 * @return int
2387
	 */
2388
	public function textFieldSize( $table, $field ) {
2389
		$table = $this->tableName( $table );
2390
		$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2391
		$res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
2392
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql, 'DatabaseBase::textFieldSize') on line 2391 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...
2393
2394
		$m = [];
2395
2396
		if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2397
			$size = $m[1];
2398
		} else {
2399
			$size = -1;
2400
		}
2401
2402
		return $size;
2403
	}
2404
2405
	/**
2406
	 * A string to insert into queries to show that they're low-priority, like
2407
	 * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
2408
	 * string and nothing bad should happen.
2409
	 *
2410
	 * @return string Returns the text of the low priority option if it is
2411
	 *   supported, or a blank string otherwise
2412
	 */
2413
	public function lowPriorityOption() {
2414
		return '';
2415
	}
2416
2417
	public function delete( $table, $conds, $fname = __METHOD__ ) {
2418
		if ( !$conds ) {
2419
			throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
2420
		}
2421
2422
		$table = $this->tableName( $table );
2423
		$sql = "DELETE FROM $table";
2424
2425 View Code Duplication
		if ( $conds != '*' ) {
2426
			if ( is_array( $conds ) ) {
2427
				$conds = $this->makeList( $conds, LIST_AND );
2428
			}
2429
			$sql .= ' WHERE ' . $conds;
2430
		}
2431
2432
		return $this->query( $sql, $fname );
2433
	}
2434
2435
	public function insertSelect(
2436
		$destTable, $srcTable, $varMap, $conds,
2437
		$fname = __METHOD__, $insertOptions = [], $selectOptions = []
2438
	) {
2439
		if ( $this->cliMode ) {
2440
			// For massive migrations with downtime, we don't want to select everything
2441
			// into memory and OOM, so do all this native on the server side if possible.
2442
			return $this->nativeInsertSelect(
2443
				$destTable,
2444
				$srcTable,
2445
				$varMap,
2446
				$conds,
2447
				$fname,
2448
				$insertOptions,
2449
				$selectOptions
2450
			);
2451
		}
2452
2453
		// For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
2454
		// on only the master (without needing row-based-replication). It also makes it easy to
2455
		// know how big the INSERT is going to be.
2456
		$fields = [];
2457
		foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
2458
			$fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
2459
		}
2460
		$selectOptions[] = 'FOR UPDATE';
2461
		$res = $this->select( $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions );
2462
		if ( !$res ) {
2463
			return false;
2464
		}
2465
2466
		$rows = [];
2467
		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...
2468
			$rows[] = (array)$row;
2469
		}
2470
2471
		return $this->insert( $destTable, $rows, $fname, $insertOptions );
2472
	}
2473
2474
	public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
2475
		$fname = __METHOD__,
2476
		$insertOptions = [], $selectOptions = []
2477
	) {
2478
		$destTable = $this->tableName( $destTable );
2479
2480
		if ( !is_array( $insertOptions ) ) {
2481
			$insertOptions = [ $insertOptions ];
2482
		}
2483
2484
		$insertOptions = $this->makeInsertOptions( $insertOptions );
2485
2486
		if ( !is_array( $selectOptions ) ) {
2487
			$selectOptions = [ $selectOptions ];
2488
		}
2489
2490
		list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
2491
2492 View Code Duplication
		if ( is_array( $srcTable ) ) {
2493
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2494
		} else {
2495
			$srcTable = $this->tableName( $srcTable );
2496
		}
2497
2498
		$sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2499
			" SELECT $startOpts " . implode( ',', $varMap ) .
2500
			" FROM $srcTable $useIndex ";
2501
2502 View Code Duplication
		if ( $conds != '*' ) {
2503
			if ( is_array( $conds ) ) {
2504
				$conds = $this->makeList( $conds, LIST_AND );
2505
			}
2506
			$sql .= " WHERE $conds";
2507
		}
2508
2509
		$sql .= " $tailOpts";
2510
2511
		return $this->query( $sql, $fname );
2512
	}
2513
2514
	/**
2515
	 * Construct a LIMIT query with optional offset. This is used for query
2516
	 * pages. The SQL should be adjusted so that only the first $limit rows
2517
	 * are returned. If $offset is provided as well, then the first $offset
2518
	 * rows should be discarded, and the next $limit rows should be returned.
2519
	 * If the result of the query is not ordered, then the rows to be returned
2520
	 * are theoretically arbitrary.
2521
	 *
2522
	 * $sql is expected to be a SELECT, if that makes a difference.
2523
	 *
2524
	 * The version provided by default works in MySQL and SQLite. It will very
2525
	 * likely need to be overridden for most other DBMSes.
2526
	 *
2527
	 * @param string $sql SQL query we will append the limit too
2528
	 * @param int $limit The SQL limit
2529
	 * @param int|bool $offset The SQL offset (default false)
2530
	 * @throws DBUnexpectedError
2531
	 * @return string
2532
	 */
2533
	public function limitResult( $sql, $limit, $offset = false ) {
2534
		if ( !is_numeric( $limit ) ) {
2535
			throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
2536
		}
2537
2538
		return "$sql LIMIT "
2539
			. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2540
			. "{$limit} ";
2541
	}
2542
2543
	public function unionSupportsOrderAndLimit() {
2544
		return true; // True for almost every DB supported
2545
	}
2546
2547
	public function unionQueries( $sqls, $all ) {
2548
		$glue = $all ? ') UNION ALL (' : ') UNION (';
2549
2550
		return '(' . implode( $glue, $sqls ) . ')';
2551
	}
2552
2553
	public function conditional( $cond, $trueVal, $falseVal ) {
2554
		if ( is_array( $cond ) ) {
2555
			$cond = $this->makeList( $cond, LIST_AND );
2556
		}
2557
2558
		return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2559
	}
2560
2561
	public function strreplace( $orig, $old, $new ) {
2562
		return "REPLACE({$orig}, {$old}, {$new})";
2563
	}
2564
2565
	public function getServerUptime() {
2566
		return 0;
2567
	}
2568
2569
	public function wasDeadlock() {
2570
		return false;
2571
	}
2572
2573
	public function wasLockTimeout() {
2574
		return false;
2575
	}
2576
2577
	public function wasErrorReissuable() {
2578
		return false;
2579
	}
2580
2581
	public function wasReadOnlyError() {
2582
		return false;
2583
	}
2584
2585
	/**
2586
	 * Determines if the given query error was a connection drop
2587
	 * STUB
2588
	 *
2589
	 * @param integer|string $errno
2590
	 * @return bool
2591
	 */
2592
	public function wasConnectionError( $errno ) {
2593
		return false;
2594
	}
2595
2596
	/**
2597
	 * Perform a deadlock-prone transaction.
2598
	 *
2599
	 * This function invokes a callback function to perform a set of write
2600
	 * queries. If a deadlock occurs during the processing, the transaction
2601
	 * will be rolled back and the callback function will be called again.
2602
	 *
2603
	 * Avoid using this method outside of Job or Maintenance classes.
2604
	 *
2605
	 * Usage:
2606
	 *   $dbw->deadlockLoop( callback, ... );
2607
	 *
2608
	 * Extra arguments are passed through to the specified callback function.
2609
	 * This method requires that no transactions are already active to avoid
2610
	 * causing premature commits or exceptions.
2611
	 *
2612
	 * Returns whatever the callback function returned on its successful,
2613
	 * iteration, or false on error, for example if the retry limit was
2614
	 * reached.
2615
	 *
2616
	 * @return mixed
2617
	 * @throws DBUnexpectedError
2618
	 * @throws Exception
2619
	 */
2620
	public function deadlockLoop() {
2621
		$args = func_get_args();
2622
		$function = array_shift( $args );
2623
		$tries = self::DEADLOCK_TRIES;
2624
2625
		$this->begin( __METHOD__ );
2626
2627
		$retVal = null;
2628
		/** @var Exception $e */
2629
		$e = null;
2630
		do {
2631
			try {
2632
				$retVal = call_user_func_array( $function, $args );
2633
				break;
2634
			} catch ( DBQueryError $e ) {
2635
				if ( $this->wasDeadlock() ) {
2636
					// Retry after a randomized delay
2637
					usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2638
				} else {
2639
					// Throw the error back up
2640
					throw $e;
2641
				}
2642
			}
2643
		} while ( --$tries > 0 );
2644
2645
		if ( $tries <= 0 ) {
2646
			// Too many deadlocks; give up
2647
			$this->rollback( __METHOD__ );
2648
			throw $e;
2649
		} else {
2650
			$this->commit( __METHOD__ );
2651
2652
			return $retVal;
2653
		}
2654
	}
2655
2656
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
2657
		# Real waits are implemented in the subclass.
2658
		return 0;
2659
	}
2660
2661
	public function getSlavePos() {
2662
		# Stub
2663
		return false;
2664
	}
2665
2666
	public function getMasterPos() {
2667
		# Stub
2668
		return false;
2669
	}
2670
2671
	public function serverIsReadOnly() {
2672
		return false;
2673
	}
2674
2675
	final public function onTransactionResolution( callable $callback ) {
2676
		if ( !$this->mTrxLevel ) {
2677
			throw new DBUnexpectedError( $this, "No transaction is active." );
2678
		}
2679
		$this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
2680
	}
2681
2682
	final public function onTransactionIdle( callable $callback ) {
2683
		$this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
2684
		if ( !$this->mTrxLevel ) {
2685
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2686
		}
2687
	}
2688
2689
	final public function onTransactionPreCommitOrIdle( callable $callback ) {
2690
		if ( $this->mTrxLevel ) {
2691
			$this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
2692
		} else {
2693
			// If no transaction is active, then make one for this callback
2694
			$this->startAtomic( __METHOD__ );
2695
			try {
2696
				call_user_func( $callback );
2697
				$this->endAtomic( __METHOD__ );
2698
			} catch ( Exception $e ) {
2699
				$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2700
				throw $e;
2701
			}
2702
		}
2703
	}
2704
2705
	final public function setTransactionListener( $name, callable $callback = null ) {
2706
		if ( $callback ) {
2707
			$this->mTrxRecurringCallbacks[$name] = [ $callback, wfGetCaller() ];
2708
		} else {
2709
			unset( $this->mTrxRecurringCallbacks[$name] );
2710
		}
2711
	}
2712
2713
	/**
2714
	 * Whether to disable running of post-COMMIT/ROLLBACK callbacks
2715
	 *
2716
	 * This method should not be used outside of Database/LoadBalancer
2717
	 *
2718
	 * @param bool $suppress
2719
	 * @since 1.28
2720
	 */
2721
	final public function setTrxEndCallbackSuppression( $suppress ) {
2722
		$this->mTrxEndCallbacksSuppressed = $suppress;
2723
	}
2724
2725
	/**
2726
	 * Actually run and consume any "on transaction idle/resolution" callbacks.
2727
	 *
2728
	 * This method should not be used outside of Database/LoadBalancer
2729
	 *
2730
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2731
	 * @since 1.20
2732
	 * @throws Exception
2733
	 */
2734
	public function runOnTransactionIdleCallbacks( $trigger ) {
2735
		if ( $this->mTrxEndCallbacksSuppressed ) {
2736
			return;
2737
		}
2738
2739
		$autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
2740
		/** @var Exception $e */
2741
		$e = null; // first exception
2742
		do { // callbacks may add callbacks :)
2743
			$callbacks = array_merge(
2744
				$this->mTrxIdleCallbacks,
2745
				$this->mTrxEndCallbacks // include "transaction resolution" callbacks
2746
			);
2747
			$this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2748
			$this->mTrxEndCallbacks = []; // consumed (recursion guard)
2749
			foreach ( $callbacks as $callback ) {
2750
				try {
2751
					list( $phpCallback ) = $callback;
2752
					$this->clearFlag( DBO_TRX ); // make each query its own transaction
2753
					call_user_func_array( $phpCallback, [ $trigger ] );
2754
					if ( $autoTrx ) {
2755
						$this->setFlag( DBO_TRX ); // restore automatic begin()
2756
					} else {
2757
						$this->clearFlag( DBO_TRX ); // restore auto-commit
2758
					}
2759
				} catch ( Exception $ex ) {
2760
					MWExceptionHandler::logException( $ex );
2761
					$e = $e ?: $ex;
2762
					// Some callbacks may use startAtomic/endAtomic, so make sure
2763
					// their transactions are ended so other callbacks don't fail
2764
					if ( $this->trxLevel() ) {
2765
						$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2766
					}
2767
				}
2768
			}
2769
		} while ( count( $this->mTrxIdleCallbacks ) );
2770
2771
		if ( $e instanceof Exception ) {
2772
			throw $e; // re-throw any first exception
2773
		}
2774
	}
2775
2776
	/**
2777
	 * Actually run and consume any "on transaction pre-commit" callbacks.
2778
	 *
2779
	 * This method should not be used outside of Database/LoadBalancer
2780
	 *
2781
	 * @since 1.22
2782
	 * @throws Exception
2783
	 */
2784
	public function runOnTransactionPreCommitCallbacks() {
2785
		$e = null; // first exception
2786
		do { // callbacks may add callbacks :)
2787
			$callbacks = $this->mTrxPreCommitCallbacks;
2788
			$this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2789 View Code Duplication
			foreach ( $callbacks as $callback ) {
2790
				try {
2791
					list( $phpCallback ) = $callback;
2792
					call_user_func( $phpCallback );
2793
				} catch ( Exception $ex ) {
2794
					MWExceptionHandler::logException( $ex );
2795
					$e = $e ?: $ex;
2796
				}
2797
			}
2798
		} while ( count( $this->mTrxPreCommitCallbacks ) );
2799
2800
		if ( $e instanceof Exception ) {
2801
			throw $e; // re-throw any first exception
2802
		}
2803
	}
2804
2805
	/**
2806
	 * Actually run any "transaction listener" callbacks.
2807
	 *
2808
	 * This method should not be used outside of Database/LoadBalancer
2809
	 *
2810
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2811
	 * @throws Exception
2812
	 * @since 1.20
2813
	 */
2814
	public function runTransactionListenerCallbacks( $trigger ) {
2815
		if ( $this->mTrxEndCallbacksSuppressed ) {
2816
			return;
2817
		}
2818
2819
		/** @var Exception $e */
2820
		$e = null; // first exception
2821
2822 View Code Duplication
		foreach ( $this->mTrxRecurringCallbacks as $callback ) {
2823
			try {
2824
				list( $phpCallback ) = $callback;
2825
				$phpCallback( $trigger, $this );
2826
			} catch ( Exception $ex ) {
2827
				MWExceptionHandler::logException( $ex );
2828
				$e = $e ?: $ex;
2829
			}
2830
		}
2831
2832
		if ( $e instanceof Exception ) {
2833
			throw $e; // re-throw any first exception
2834
		}
2835
	}
2836
2837
	final public function startAtomic( $fname = __METHOD__ ) {
2838
		if ( !$this->mTrxLevel ) {
2839
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2840
			// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2841
			// in all changes being in one transaction to keep requests transactional.
2842
			if ( !$this->getFlag( DBO_TRX ) ) {
2843
				$this->mTrxAutomaticAtomic = true;
2844
			}
2845
		}
2846
2847
		$this->mTrxAtomicLevels[] = $fname;
2848
	}
2849
2850
	final public function endAtomic( $fname = __METHOD__ ) {
2851
		if ( !$this->mTrxLevel ) {
2852
			throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2853
		}
2854
		if ( !$this->mTrxAtomicLevels ||
2855
			array_pop( $this->mTrxAtomicLevels ) !== $fname
2856
		) {
2857
			throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2858
		}
2859
2860
		if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2861
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2862
		}
2863
	}
2864
2865
	final public function doAtomicSection( $fname, callable $callback ) {
2866
		$this->startAtomic( $fname );
2867
		try {
2868
			$res = call_user_func_array( $callback, [ $this, $fname ] );
2869
		} catch ( Exception $e ) {
2870
			$this->rollback( $fname, self::FLUSHING_INTERNAL );
2871
			throw $e;
2872
		}
2873
		$this->endAtomic( $fname );
2874
2875
		return $res;
2876
	}
2877
2878
	final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2879
		// Protect against mismatched atomic section, transaction nesting, and snapshot loss
2880
		if ( $this->mTrxLevel ) {
2881
			if ( $this->mTrxAtomicLevels ) {
2882
				$levels = implode( ', ', $this->mTrxAtomicLevels );
2883
				$msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2884
				throw new DBUnexpectedError( $this, $msg );
2885
			} elseif ( !$this->mTrxAutomatic ) {
2886
				$msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2887
				throw new DBUnexpectedError( $this, $msg );
2888
			} else {
2889
				// @TODO: make this an exception at some point
2890
				$msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2891
				wfLogDBError( $msg );
2892
				wfWarn( $msg );
2893
				return; // join the main transaction set
2894
			}
2895
		} elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2896
			// @TODO: make this an exception at some point
2897
			$msg = "$fname: Implicit transaction expected (DBO_TRX set).";
2898
			wfLogDBError( $msg );
2899
			wfWarn( $msg );
2900
			return; // let any writes be in the main transaction
2901
		}
2902
2903
		// Avoid fatals if close() was called
2904
		$this->assertOpen();
2905
2906
		$this->doBegin( $fname );
2907
		$this->mTrxTimestamp = microtime( true );
2908
		$this->mTrxFname = $fname;
2909
		$this->mTrxDoneWrites = false;
2910
		$this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
2911
		$this->mTrxAutomaticAtomic = false;
2912
		$this->mTrxAtomicLevels = [];
2913
		$this->mTrxShortId = wfRandomString( 12 );
2914
		$this->mTrxWriteDuration = 0.0;
2915
		$this->mTrxWriteQueryCount = 0;
2916
		$this->mTrxWriteAdjDuration = 0.0;
2917
		$this->mTrxWriteAdjQueryCount = 0;
2918
		$this->mTrxWriteCallers = [];
2919
		// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2920
		// Get an estimate of the replica DB lag before then, treating estimate staleness
2921
		// as lag itself just to be safe
2922
		$status = $this->getApproximateLagStatus();
2923
		$this->mTrxSlaveLag = $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 $mTrxSlaveLag 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...
2924
	}
2925
2926
	/**
2927
	 * Issues the BEGIN command to the database server.
2928
	 *
2929
	 * @see DatabaseBase::begin()
2930
	 * @param string $fname
2931
	 */
2932
	protected function doBegin( $fname ) {
2933
		$this->query( 'BEGIN', $fname );
2934
		$this->mTrxLevel = 1;
2935
	}
2936
2937
	final public function commit( $fname = __METHOD__, $flush = '' ) {
2938
		if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2939
			// There are still atomic sections open. This cannot be ignored
2940
			$levels = implode( ', ', $this->mTrxAtomicLevels );
2941
			throw new DBUnexpectedError(
2942
				$this,
2943
				"$fname: Got COMMIT while atomic sections $levels are still open."
2944
			);
2945
		}
2946
2947
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2948
			if ( !$this->mTrxLevel ) {
2949
				return; // nothing to do
2950
			} elseif ( !$this->mTrxAutomatic ) {
2951
				throw new DBUnexpectedError(
2952
					$this,
2953
					"$fname: Flushing an explicit transaction, getting out of sync."
2954
				);
2955
			}
2956
		} else {
2957
			if ( !$this->mTrxLevel ) {
2958
				wfWarn( "$fname: No transaction to commit, something got out of sync." );
2959
				return; // nothing to do
2960
			} elseif ( $this->mTrxAutomatic ) {
2961
				// @TODO: make this an exception at some point
2962
				$msg = "$fname: Explicit commit of implicit transaction.";
2963
				wfLogDBError( $msg );
2964
				wfWarn( $msg );
2965
				return; // wait for the main transaction set commit round
2966
			}
2967
		}
2968
2969
		// Avoid fatals if close() was called
2970
		$this->assertOpen();
2971
2972
		$this->runOnTransactionPreCommitCallbacks();
2973
		$writeTime = $this->pendingWriteQueryDuration();
2974
		$this->doCommit( $fname );
2975
		if ( $this->mTrxDoneWrites ) {
2976
			$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...
2977
			$this->getTransactionProfiler()->transactionWritingOut(
2978
				$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 2973 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...
2979
		}
2980
2981
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2982
		$this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
2983
	}
2984
2985
	/**
2986
	 * Issues the COMMIT command to the database server.
2987
	 *
2988
	 * @see DatabaseBase::commit()
2989
	 * @param string $fname
2990
	 */
2991
	protected function doCommit( $fname ) {
2992
		if ( $this->mTrxLevel ) {
2993
			$this->query( 'COMMIT', $fname );
2994
			$this->mTrxLevel = 0;
2995
		}
2996
	}
2997
2998
	final public function rollback( $fname = __METHOD__, $flush = '' ) {
2999
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
3000
			if ( !$this->mTrxLevel ) {
3001
				return; // nothing to do
3002
			}
3003
		} else {
3004
			if ( !$this->mTrxLevel ) {
3005
				wfWarn( "$fname: No transaction to rollback, something got out of sync." );
3006
				return; // nothing to do
3007
			} elseif ( $this->getFlag( DBO_TRX ) ) {
3008
				throw new DBUnexpectedError(
3009
					$this,
3010
					"$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
3011
				);
3012
			}
3013
		}
3014
3015
		// Avoid fatals if close() was called
3016
		$this->assertOpen();
3017
3018
		$this->doRollback( $fname );
3019
		$this->mTrxAtomicLevels = [];
3020
		if ( $this->mTrxDoneWrites ) {
3021
			$this->getTransactionProfiler()->transactionWritingOut(
3022
				$this->mServer, $this->mDBname, $this->mTrxShortId );
3023
		}
3024
3025
		$this->mTrxIdleCallbacks = []; // clear
3026
		$this->mTrxPreCommitCallbacks = []; // clear
3027
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
3028
		$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
3029
	}
3030
3031
	/**
3032
	 * Issues the ROLLBACK command to the database server.
3033
	 *
3034
	 * @see DatabaseBase::rollback()
3035
	 * @param string $fname
3036
	 */
3037
	protected function doRollback( $fname ) {
3038
		if ( $this->mTrxLevel ) {
3039
			# Disconnects cause rollback anyway, so ignore those errors
3040
			$ignoreErrors = true;
3041
			$this->query( 'ROLLBACK', $fname, $ignoreErrors );
3042
			$this->mTrxLevel = 0;
3043
		}
3044
	}
3045
3046
	public function clearSnapshot( $fname = __METHOD__ ) {
3047
		if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
3048
			// This only flushes transactions to clear snapshots, not to write data
3049
			throw new DBUnexpectedError(
3050
				$this,
3051
				"$fname: Cannot COMMIT to clear snapshot because writes are pending."
3052
			);
3053
		}
3054
3055
		$this->commit( $fname, self::FLUSHING_INTERNAL );
3056
	}
3057
3058
	public function explicitTrxActive() {
3059
		return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
3060
	}
3061
3062
	/**
3063
	 * Creates a new table with structure copied from existing table
3064
	 * Note that unlike most database abstraction functions, this function does not
3065
	 * automatically append database prefix, because it works at a lower
3066
	 * abstraction level.
3067
	 * The table names passed to this function shall not be quoted (this
3068
	 * function calls addIdentifierQuotes when needed).
3069
	 *
3070
	 * @param string $oldName Name of table whose structure should be copied
3071
	 * @param string $newName Name of table to be created
3072
	 * @param bool $temporary Whether the new table should be temporary
3073
	 * @param string $fname Calling function name
3074
	 * @throws MWException
3075
	 * @return bool True if operation was successful
3076
	 */
3077
	public function duplicateTableStructure( $oldName, $newName, $temporary = false,
3078
		$fname = __METHOD__
3079
	) {
3080
		throw new MWException(
3081
			'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
3082
	}
3083
3084
	function listTables( $prefix = null, $fname = __METHOD__ ) {
3085
		throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
3086
	}
3087
3088
	/**
3089
	 * Reset the views process cache set by listViews()
3090
	 * @since 1.22
3091
	 */
3092
	final public function clearViewsCache() {
3093
		$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...
3094
	}
3095
3096
	/**
3097
	 * Lists all the VIEWs in the database
3098
	 *
3099
	 * For caching purposes the list of all views should be stored in
3100
	 * $this->allViews. The process cache can be cleared with clearViewsCache()
3101
	 *
3102
	 * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
3103
	 * @param string $fname Name of calling function
3104
	 * @throws MWException
3105
	 * @return array
3106
	 * @since 1.22
3107
	 */
3108
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
3109
		throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
3110
	}
3111
3112
	/**
3113
	 * Differentiates between a TABLE and a VIEW
3114
	 *
3115
	 * @param string $name Name of the database-structure to test.
3116
	 * @throws MWException
3117
	 * @return bool
3118
	 * @since 1.22
3119
	 */
3120
	public function isView( $name ) {
3121
		throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
3122
	}
3123
3124
	public function timestamp( $ts = 0 ) {
3125
		return wfTimestamp( TS_MW, $ts );
3126
	}
3127
3128
	public function timestampOrNull( $ts = null ) {
3129
		if ( is_null( $ts ) ) {
3130
			return null;
3131
		} else {
3132
			return $this->timestamp( $ts );
3133
		}
3134
	}
3135
3136
	/**
3137
	 * Take the result from a query, and wrap it in a ResultWrapper if
3138
	 * necessary. Boolean values are passed through as is, to indicate success
3139
	 * of write queries or failure.
3140
	 *
3141
	 * Once upon a time, DatabaseBase::query() returned a bare MySQL result
3142
	 * resource, and it was necessary to call this function to convert it to
3143
	 * a wrapper. Nowadays, raw database objects are never exposed to external
3144
	 * callers, so this is unnecessary in external code.
3145
	 *
3146
	 * @param bool|ResultWrapper|resource|object $result
3147
	 * @return bool|ResultWrapper
3148
	 */
3149
	protected function resultObject( $result ) {
3150
		if ( !$result ) {
3151
			return false;
3152
		} elseif ( $result instanceof ResultWrapper ) {
3153
			return $result;
3154
		} elseif ( $result === true ) {
3155
			// Successful write query
3156
			return $result;
3157
		} else {
3158
			return new ResultWrapper( $this, $result );
3159
		}
3160
	}
3161
3162
	public function ping( &$rtt = null ) {
3163
		// Avoid hitting the server if it was hit recently
3164
		if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
3165
			if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
3166
				$rtt = $this->mRTTEstimate;
3167
				return true; // don't care about $rtt
3168
			}
3169
		}
3170
3171
		// This will reconnect if possible or return false if not
3172
		$this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
3173
		$ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
3174
		$this->restoreFlags( self::RESTORE_PRIOR );
3175
3176
		if ( $ok ) {
3177
			$rtt = $this->mRTTEstimate;
3178
		}
3179
3180
		return $ok;
3181
	}
3182
3183
	/**
3184
	 * @return bool
3185
	 */
3186
	protected function reconnect() {
3187
		$this->closeConnection();
3188
		$this->mOpened = false;
3189
		$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...
3190
		try {
3191
			$this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
3192
			$this->lastPing = microtime( true );
3193
			$ok = true;
3194
		} catch ( DBConnectionError $e ) {
3195
			$ok = false;
3196
		}
3197
3198
		return $ok;
3199
	}
3200
3201
	public function getSessionLagStatus() {
3202
		return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
3203
	}
3204
3205
	/**
3206
	 * Get the replica DB lag when the current transaction started
3207
	 *
3208
	 * This is useful when transactions might use snapshot isolation
3209
	 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
3210
	 * is this lag plus transaction duration. If they don't, it is still
3211
	 * safe to be pessimistic. This returns null if there is no transaction.
3212
	 *
3213
	 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
3214
	 * @since 1.27
3215
	 */
3216
	public function getTransactionLagStatus() {
3217
		return $this->mTrxLevel
3218
			? [ 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() ]
3219
			: null;
3220
	}
3221
3222
	/**
3223
	 * Get a replica DB lag estimate for this server
3224
	 *
3225
	 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
3226
	 * @since 1.27
3227
	 */
3228
	public function getApproximateLagStatus() {
3229
		return [
3230
			'lag'   => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
3231
			'since' => microtime( true )
3232
		];
3233
	}
3234
3235
	/**
3236
	 * Merge the result of getSessionLagStatus() for several DBs
3237
	 * using the most pessimistic values to estimate the lag of
3238
	 * any data derived from them in combination
3239
	 *
3240
	 * This is information is useful for caching modules
3241
	 *
3242
	 * @see WANObjectCache::set()
3243
	 * @see WANObjectCache::getWithSetCallback()
3244
	 *
3245
	 * @param IDatabase $db1
3246
	 * @param IDatabase ...
3247
	 * @return array Map of values:
3248
	 *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
3249
	 *   - since: oldest UNIX timestamp of any of the DB lag estimates
3250
	 *   - pending: whether any of the DBs have uncommitted changes
3251
	 * @since 1.27
3252
	 */
3253
	public static function getCacheSetOptions( IDatabase $db1 ) {
3254
		$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3255
		foreach ( func_get_args() as $db ) {
3256
			/** @var IDatabase $db */
3257
			$status = $db->getSessionLagStatus();
3258
			if ( $status['lag'] === false ) {
3259
				$res['lag'] = false;
3260
			} elseif ( $res['lag'] !== false ) {
3261
				$res['lag'] = max( $res['lag'], $status['lag'] );
3262
			}
3263
			$res['since'] = min( $res['since'], $status['since'] );
3264
			$res['pending'] = $res['pending'] ?: $db->writesPending();
3265
		}
3266
3267
		return $res;
3268
	}
3269
3270
	public function getLag() {
3271
		return 0;
3272
	}
3273
3274
	function maxListLen() {
3275
		return 0;
3276
	}
3277
3278
	public function encodeBlob( $b ) {
3279
		return $b;
3280
	}
3281
3282
	public function decodeBlob( $b ) {
3283
		if ( $b instanceof Blob ) {
3284
			$b = $b->fetch();
3285
		}
3286
		return $b;
3287
	}
3288
3289
	public function setSessionOptions( array $options ) {
3290
	}
3291
3292
	/**
3293
	 * Read and execute SQL commands from a file.
3294
	 *
3295
	 * Returns true on success, error string or exception on failure (depending
3296
	 * on object's error ignore settings).
3297
	 *
3298
	 * @param string $filename File name to open
3299
	 * @param bool|callable $lineCallback Optional function called before reading each line
3300
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3301
	 * @param bool|string $fname Calling function name or false if name should be
3302
	 *   generated dynamically using $filename
3303
	 * @param bool|callable $inputCallback Optional function called for each
3304
	 *   complete line sent
3305
	 * @throws Exception|MWException
3306
	 * @return bool|string
3307
	 */
3308
	public function sourceFile(
3309
		$filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
3310
	) {
3311
		MediaWiki\suppressWarnings();
3312
		$fp = fopen( $filename, 'r' );
3313
		MediaWiki\restoreWarnings();
3314
3315
		if ( false === $fp ) {
3316
			throw new MWException( "Could not open \"{$filename}\".\n" );
3317
		}
3318
3319
		if ( !$fname ) {
3320
			$fname = __METHOD__ . "( $filename )";
3321
		}
3322
3323
		try {
3324
			$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 3309 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...
3325
		} catch ( Exception $e ) {
3326
			fclose( $fp );
3327
			throw $e;
3328
		}
3329
3330
		fclose( $fp );
3331
3332
		return $error;
3333
	}
3334
3335
	/**
3336
	 * Get the full path of a patch file. Originally based on archive()
3337
	 * from updaters.inc. Keep in mind this always returns a patch, as
3338
	 * it fails back to MySQL if no DB-specific patch can be found
3339
	 *
3340
	 * @param string $patch The name of the patch, like patch-something.sql
3341
	 * @return string Full path to patch file
3342
	 */
3343 View Code Duplication
	public function patchPath( $patch ) {
3344
		global $IP;
3345
3346
		$dbType = $this->getType();
3347
		if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
3348
			return "$IP/maintenance/$dbType/archives/$patch";
3349
		} else {
3350
			return "$IP/maintenance/archives/$patch";
3351
		}
3352
	}
3353
3354
	public function setSchemaVars( $vars ) {
3355
		$this->mSchemaVars = $vars;
3356
	}
3357
3358
	/**
3359
	 * Read and execute commands from an open file handle.
3360
	 *
3361
	 * Returns true on success, error string or exception on failure (depending
3362
	 * on object's error ignore settings).
3363
	 *
3364
	 * @param resource $fp File handle
3365
	 * @param bool|callable $lineCallback Optional function called before reading each query
3366
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3367
	 * @param string $fname Calling function name
3368
	 * @param bool|callable $inputCallback Optional function called for each complete query sent
3369
	 * @return bool|string
3370
	 */
3371
	public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
3372
		$fname = __METHOD__, $inputCallback = false
3373
	) {
3374
		$cmd = '';
3375
3376
		while ( !feof( $fp ) ) {
3377
			if ( $lineCallback ) {
3378
				call_user_func( $lineCallback );
3379
			}
3380
3381
			$line = trim( fgets( $fp ) );
3382
3383
			if ( $line == '' ) {
3384
				continue;
3385
			}
3386
3387
			if ( '-' == $line[0] && '-' == $line[1] ) {
3388
				continue;
3389
			}
3390
3391
			if ( $cmd != '' ) {
3392
				$cmd .= ' ';
3393
			}
3394
3395
			$done = $this->streamStatementEnd( $cmd, $line );
3396
3397
			$cmd .= "$line\n";
3398
3399
			if ( $done || feof( $fp ) ) {
3400
				$cmd = $this->replaceVars( $cmd );
3401
3402
				if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
3403
					$res = $this->query( $cmd, $fname );
3404
3405
					if ( $resultCallback ) {
3406
						call_user_func( $resultCallback, $res, $this );
3407
					}
3408
3409
					if ( false === $res ) {
3410
						$err = $this->lastError();
3411
3412
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3413
					}
3414
				}
3415
				$cmd = '';
3416
			}
3417
		}
3418
3419
		return true;
3420
	}
3421
3422
	/**
3423
	 * Called by sourceStream() to check if we've reached a statement end
3424
	 *
3425
	 * @param string $sql SQL assembled so far
3426
	 * @param string $newLine New line about to be added to $sql
3427
	 * @return bool Whether $newLine contains end of the statement
3428
	 */
3429
	public function streamStatementEnd( &$sql, &$newLine ) {
3430
		if ( $this->delimiter ) {
3431
			$prev = $newLine;
3432
			$newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3433
			if ( $newLine != $prev ) {
3434
				return true;
3435
			}
3436
		}
3437
3438
		return false;
3439
	}
3440
3441
	/**
3442
	 * Database independent variable replacement. Replaces a set of variables
3443
	 * in an SQL statement with their contents as given by $this->getSchemaVars().
3444
	 *
3445
	 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3446
	 *
3447
	 * - '{$var}' should be used for text and is passed through the database's
3448
	 *   addQuotes method.
3449
	 * - `{$var}` should be used for identifiers (e.g. table and database names).
3450
	 *   It is passed through the database's addIdentifierQuotes method which
3451
	 *   can be overridden if the database uses something other than backticks.
3452
	 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3453
	 *   database's tableName method.
3454
	 * - / *i* / passes the name that follows through the database's indexName method.
3455
	 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3456
	 *   its use should be avoided. In 1.24 and older, string encoding was applied.
3457
	 *
3458
	 * @param string $ins SQL statement to replace variables in
3459
	 * @return string The new SQL statement with variables replaced
3460
	 */
3461
	protected function replaceVars( $ins ) {
3462
		$vars = $this->getSchemaVars();
3463
		return preg_replace_callback(
3464
			'!
3465
				/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3466
				\'\{\$ (\w+) }\'                  | # 3. addQuotes
3467
				`\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
3468
				/\*\$ (\w+) \*/                     # 5. leave unencoded
3469
			!x',
3470
			function ( $m ) use ( $vars ) {
3471
				// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3472
				// check for both nonexistent keys *and* the empty string.
3473
				if ( isset( $m[1] ) && $m[1] !== '' ) {
3474
					if ( $m[1] === 'i' ) {
3475
						return $this->indexName( $m[2] );
3476
					} else {
3477
						return $this->tableName( $m[2] );
3478
					}
3479 View Code Duplication
				} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3480
					return $this->addQuotes( $vars[$m[3]] );
3481
				} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3482
					return $this->addIdentifierQuotes( $vars[$m[4]] );
3483 View Code Duplication
				} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3484
					return $vars[$m[5]];
3485
				} else {
3486
					return $m[0];
3487
				}
3488
			},
3489
			$ins
3490
		);
3491
	}
3492
3493
	/**
3494
	 * Get schema variables. If none have been set via setSchemaVars(), then
3495
	 * use some defaults from the current object.
3496
	 *
3497
	 * @return array
3498
	 */
3499
	protected function getSchemaVars() {
3500
		if ( $this->mSchemaVars ) {
3501
			return $this->mSchemaVars;
3502
		} else {
3503
			return $this->getDefaultSchemaVars();
3504
		}
3505
	}
3506
3507
	/**
3508
	 * Get schema variables to use if none have been set via setSchemaVars().
3509
	 *
3510
	 * Override this in derived classes to provide variables for tables.sql
3511
	 * and SQL patch files.
3512
	 *
3513
	 * @return array
3514
	 */
3515
	protected function getDefaultSchemaVars() {
3516
		return [];
3517
	}
3518
3519
	public function lockIsFree( $lockName, $method ) {
3520
		return true;
3521
	}
3522
3523
	public function lock( $lockName, $method, $timeout = 5 ) {
3524
		$this->mNamedLocksHeld[$lockName] = 1;
3525
3526
		return true;
3527
	}
3528
3529
	public function unlock( $lockName, $method ) {
3530
		unset( $this->mNamedLocksHeld[$lockName] );
3531
3532
		return true;
3533
	}
3534
3535
	public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3536
		if ( $this->writesOrCallbacksPending() ) {
3537
			// This only flushes transactions to clear snapshots, not to write data
3538
			throw new DBUnexpectedError(
3539
				$this,
3540
				"$fname: Cannot COMMIT to clear snapshot because writes are pending."
3541
			);
3542
		}
3543
3544
		if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3545
			return null;
3546
		}
3547
3548
		$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3549
			if ( $this->trxLevel() ) {
3550
				// There is a good chance an exception was thrown, causing any early return
3551
				// from the caller. Let any error handler get a chance to issue rollback().
3552
				// If there isn't one, let the error bubble up and trigger server-side rollback.
3553
				$this->onTransactionResolution( function () use ( $lockKey, $fname ) {
3554
					$this->unlock( $lockKey, $fname );
3555
				} );
3556
			} else {
3557
				$this->unlock( $lockKey, $fname );
3558
			}
3559
		} );
3560
3561
		$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
3562
3563
		return $unlocker;
3564
	}
3565
3566
	public function namedLocksEnqueue() {
3567
		return false;
3568
	}
3569
3570
	/**
3571
	 * Lock specific tables
3572
	 *
3573
	 * @param array $read Array of tables to lock for read access
3574
	 * @param array $write Array of tables to lock for write access
3575
	 * @param string $method Name of caller
3576
	 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3577
	 * @return bool
3578
	 */
3579
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
3580
		return true;
3581
	}
3582
3583
	/**
3584
	 * Unlock specific tables
3585
	 *
3586
	 * @param string $method The caller
3587
	 * @return bool
3588
	 */
3589
	public function unlockTables( $method ) {
3590
		return true;
3591
	}
3592
3593
	/**
3594
	 * Delete a table
3595
	 * @param string $tableName
3596
	 * @param string $fName
3597
	 * @return bool|ResultWrapper
3598
	 * @since 1.18
3599
	 */
3600 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
3601
		if ( !$this->tableExists( $tableName, $fName ) ) {
3602
			return false;
3603
		}
3604
		$sql = "DROP TABLE " . $this->tableName( $tableName );
3605
		if ( $this->cascadingDeletes() ) {
3606
			$sql .= " CASCADE";
3607
		}
3608
3609
		return $this->query( $sql, $fName );
3610
	}
3611
3612
	/**
3613
	 * Get search engine class. All subclasses of this need to implement this
3614
	 * if they wish to use searching.
3615
	 *
3616
	 * @return string
3617
	 */
3618
	public function getSearchEngine() {
3619
		return 'SearchEngineDummy';
3620
	}
3621
3622
	public function getInfinity() {
3623
		return 'infinity';
3624
	}
3625
3626
	public function encodeExpiry( $expiry ) {
3627
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3628
			? $this->getInfinity()
3629
			: $this->timestamp( $expiry );
3630
	}
3631
3632
	public function decodeExpiry( $expiry, $format = TS_MW ) {
3633
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3634
			? 'infinity'
3635
			: wfTimestamp( $format, $expiry );
3636
	}
3637
3638
	public function setBigSelects( $value = true ) {
3639
		// no-op
3640
	}
3641
3642
	public function isReadOnly() {
3643
		return ( $this->getReadOnlyReason() !== false );
3644
	}
3645
3646
	/**
3647
	 * @return string|bool Reason this DB is read-only or false if it is not
3648
	 */
3649
	protected function getReadOnlyReason() {
3650
		$reason = $this->getLBInfo( 'readOnlyReason' );
3651
3652
		return is_string( $reason ) ? $reason : false;
3653
	}
3654
3655
	/**
3656
	 * @since 1.19
3657
	 * @return string
3658
	 */
3659
	public function __toString() {
3660
		return (string)$this->mConn;
3661
	}
3662
3663
	/**
3664
	 * Run a few simple sanity checks
3665
	 */
3666
	public function __destruct() {
3667
		if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3668
			trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3669
		}
3670
		$danglingCallbacks = array_merge(
3671
			$this->mTrxIdleCallbacks,
3672
			$this->mTrxPreCommitCallbacks,
3673
			$this->mTrxEndCallbacks
3674
		);
3675
		if ( $danglingCallbacks ) {
3676
			$callers = [];
3677
			foreach ( $danglingCallbacks as $callbackInfo ) {
3678
				$callers[] = $callbackInfo[1];
3679
			}
3680
			$callers = implode( ', ', $callers );
3681
			trigger_error( "DB transaction callbacks still pending (from $callers)." );
3682
		}
3683
	}
3684
}
3685
3686
/**
3687
 * @since 1.27
3688
 */
3689
abstract class Database extends DatabaseBase {
3690
	// B/C until nothing type hints for DatabaseBase
3691
	// @TODO: finish renaming DatabaseBase => Database
3692
}
3693