Completed
Branch master (4cbefc)
by
unknown
27:08
created

DatabaseBase::ignoreIndexClause()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
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 $mTrxReplicaLag = null;
132
	/**
133
	 * Remembers the function name given for starting the most recent transaction via begin().
134
	 * Used to provide additional context for error reporting.
135
	 *
136
	 * @var string
137
	 * @see DatabaseBase::mTrxLevel
138
	 */
139
	private $mTrxFname = null;
140
	/**
141
	 * Record if possible write queries were done in the last transaction started
142
	 *
143
	 * @var bool
144
	 * @see DatabaseBase::mTrxLevel
145
	 */
146
	private $mTrxDoneWrites = false;
147
	/**
148
	 * Record if the current transaction was started implicitly due to DBO_TRX being set.
149
	 *
150
	 * @var bool
151
	 * @see DatabaseBase::mTrxLevel
152
	 */
153
	private $mTrxAutomatic = false;
154
	/**
155
	 * Array of levels of atomicity within transactions
156
	 *
157
	 * @var array
158
	 */
159
	private $mTrxAtomicLevels = [];
160
	/**
161
	 * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
162
	 *
163
	 * @var bool
164
	 */
165
	private $mTrxAutomaticAtomic = false;
166
	/**
167
	 * Track the write query callers of the current transaction
168
	 *
169
	 * @var string[]
170
	 */
171
	private $mTrxWriteCallers = [];
172
	/**
173
	 * @var float Seconds spent in write queries for the current transaction
174
	 */
175
	private $mTrxWriteDuration = 0.0;
176
	/**
177
	 * @var integer Number of write queries for the current transaction
178
	 */
179
	private $mTrxWriteQueryCount = 0;
180
	/**
181
	 * @var float Like mTrxWriteQueryCount but excludes lock-bound, easy to replicate, queries
182
	 */
183
	private $mTrxWriteAdjDuration = 0.0;
184
	/**
185
	 * @var integer Number of write queries counted in mTrxWriteAdjDuration
186
	 */
187
	private $mTrxWriteAdjQueryCount = 0;
188
	/**
189
	 * @var float RTT time estimate
190
	 */
191
	private $mRTTEstimate = 0.0;
192
193
	/** @var array Map of (name => 1) for locks obtained via lock() */
194
	private $mNamedLocksHeld = [];
195
196
	/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
197
	private $lazyMasterHandle;
198
199
	/**
200
	 * @since 1.21
201
	 * @var resource File handle for upgrade
202
	 */
203
	protected $fileHandle = null;
204
205
	/**
206
	 * @since 1.22
207
	 * @var string[] Process cache of VIEWs names in the database
208
	 */
209
	protected $allViews = null;
210
211
	/** @var float UNIX timestamp */
212
	protected $lastPing = 0.0;
213
214
	/** @var int[] Prior mFlags values */
215
	private $priorFlags = [];
216
217
	/** @var Profiler */
218
	protected $profiler;
219
	/** @var TransactionProfiler */
220
	protected $trxProfiler;
221
222
	public function getServerInfo() {
223
		return $this->getServerVersion();
224
	}
225
226
	/**
227
	 * @return string Command delimiter used by this database engine
228
	 */
229
	public function getDelimiter() {
230
		return $this->delimiter;
231
	}
232
233
	/**
234
	 * Boolean, controls output of large amounts of debug information.
235
	 * @param bool|null $debug
236
	 *   - true to enable debugging
237
	 *   - false to disable debugging
238
	 *   - omitted or null to do nothing
239
	 *
240
	 * @return bool|null Previous value of the flag
241
	 */
242
	public function debug( $debug = null ) {
243
		return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
0 ignored issues
show
Bug introduced by
It seems like $debug defined by parameter $debug on line 242 can also be of type null; however, wfSetBit() does only seem to accept boolean, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

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

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

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

An additional type check may prevent trouble.

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

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

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

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

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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

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

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

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

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

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

$answer = 42;

$correct = false;

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

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

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

    return array();
}

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

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

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

There are different options of fixing this problem.

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

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

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

Loading history...
1260
			$values[] = $row->$var;
1261
		}
1262
1263
		return $values;
1264
	}
1265
1266
	/**
1267
	 * Returns an optional USE INDEX clause to go after the table, and a
1268
	 * string to go at the end of the query.
1269
	 *
1270
	 * @param array $options Associative array of options to be turned into
1271
	 *   an SQL query, valid keys are listed in the function.
1272
	 * @return array
1273
	 * @see DatabaseBase::select()
1274
	 */
1275
	public function makeSelectOptions( $options ) {
1276
		$preLimitTail = $postLimitTail = '';
1277
		$startOpts = '';
1278
1279
		$noKeyOptions = [];
1280
1281
		foreach ( $options as $key => $option ) {
1282
			if ( is_numeric( $key ) ) {
1283
				$noKeyOptions[$option] = true;
1284
			}
1285
		}
1286
1287
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1288
1289
		$preLimitTail .= $this->makeOrderBy( $options );
1290
1291
		// if (isset($options['LIMIT'])) {
1292
		// 	$tailOpts .= $this->limitResult('', $options['LIMIT'],
1293
		// 		isset($options['OFFSET']) ? $options['OFFSET']
1294
		// 		: false);
1295
		// }
1296
1297
		if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1298
			$postLimitTail .= ' FOR UPDATE';
1299
		}
1300
1301
		if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1302
			$postLimitTail .= ' LOCK IN SHARE MODE';
1303
		}
1304
1305
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1306
			$startOpts .= 'DISTINCT';
1307
		}
1308
1309
		# Various MySQL extensions
1310
		if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1311
			$startOpts .= ' /*! STRAIGHT_JOIN */';
1312
		}
1313
1314
		if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1315
			$startOpts .= ' HIGH_PRIORITY';
1316
		}
1317
1318
		if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1319
			$startOpts .= ' SQL_BIG_RESULT';
1320
		}
1321
1322
		if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1323
			$startOpts .= ' SQL_BUFFER_RESULT';
1324
		}
1325
1326
		if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1327
			$startOpts .= ' SQL_SMALL_RESULT';
1328
		}
1329
1330
		if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1331
			$startOpts .= ' SQL_CALC_FOUND_ROWS';
1332
		}
1333
1334
		if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1335
			$startOpts .= ' SQL_CACHE';
1336
		}
1337
1338
		if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1339
			$startOpts .= ' SQL_NO_CACHE';
1340
		}
1341
1342 View Code Duplication
		if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1343
			$useIndex = $this->useIndexClause( $options['USE INDEX'] );
1344
		} else {
1345
			$useIndex = '';
1346
		}
1347 View Code Duplication
		if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1348
			$ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1349
		} else {
1350
			$ignoreIndex = '';
1351
		}
1352
1353
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1354
	}
1355
1356
	/**
1357
	 * Returns an optional GROUP BY with an optional HAVING
1358
	 *
1359
	 * @param array $options Associative array of options
1360
	 * @return string
1361
	 * @see DatabaseBase::select()
1362
	 * @since 1.21
1363
	 */
1364
	public function makeGroupByWithHaving( $options ) {
1365
		$sql = '';
1366 View Code Duplication
		if ( isset( $options['GROUP BY'] ) ) {
1367
			$gb = is_array( $options['GROUP BY'] )
1368
				? implode( ',', $options['GROUP BY'] )
1369
				: $options['GROUP BY'];
1370
			$sql .= ' GROUP BY ' . $gb;
1371
		}
1372 View Code Duplication
		if ( isset( $options['HAVING'] ) ) {
1373
			$having = is_array( $options['HAVING'] )
1374
				? $this->makeList( $options['HAVING'], LIST_AND )
1375
				: $options['HAVING'];
1376
			$sql .= ' HAVING ' . $having;
1377
		}
1378
1379
		return $sql;
1380
	}
1381
1382
	/**
1383
	 * Returns an optional ORDER BY
1384
	 *
1385
	 * @param array $options Associative array of options
1386
	 * @return string
1387
	 * @see DatabaseBase::select()
1388
	 * @since 1.21
1389
	 */
1390
	public function makeOrderBy( $options ) {
1391 View Code Duplication
		if ( isset( $options['ORDER BY'] ) ) {
1392
			$ob = is_array( $options['ORDER BY'] )
1393
				? implode( ',', $options['ORDER BY'] )
1394
				: $options['ORDER BY'];
1395
1396
			return ' ORDER BY ' . $ob;
1397
		}
1398
1399
		return '';
1400
	}
1401
1402
	// See IDatabase::select for the docs for this function
1403
	public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1404
		$options = [], $join_conds = [] ) {
1405
		$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1406
1407
		return $this->query( $sql, $fname );
1408
	}
1409
1410
	public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1411
		$options = [], $join_conds = []
1412
	) {
1413
		if ( is_array( $vars ) ) {
1414
			$vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1415
		}
1416
1417
		$options = (array)$options;
1418
		$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1419
			? $options['USE INDEX']
1420
			: [];
1421
		$ignoreIndexes = ( isset( $options['IGNORE INDEX'] ) && is_array( $options['IGNORE INDEX'] ) )
1422
			? $options['IGNORE INDEX']
1423
			: [];
1424
1425
		if ( is_array( $table ) ) {
1426
			$from = ' FROM ' .
1427
				$this->tableNamesWithIndexClauseOrJOIN( $table, $useIndexes, $ignoreIndexes, $join_conds );
1428
		} elseif ( $table != '' ) {
1429
			if ( $table[0] == ' ' ) {
1430
				$from = ' FROM ' . $table;
1431
			} else {
1432
				$from = ' FROM ' .
1433
					$this->tableNamesWithIndexClauseOrJOIN( [ $table ], $useIndexes, $ignoreIndexes, [] );
1434
			}
1435
		} else {
1436
			$from = '';
1437
		}
1438
1439
		list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1440
			$this->makeSelectOptions( $options );
1441
1442
		if ( !empty( $conds ) ) {
1443
			if ( is_array( $conds ) ) {
1444
				$conds = $this->makeList( $conds, LIST_AND );
1445
			}
1446
			$sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex WHERE $conds $preLimitTail";
1447
		} else {
1448
			$sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
1449
		}
1450
1451
		if ( isset( $options['LIMIT'] ) ) {
1452
			$sql = $this->limitResult( $sql, $options['LIMIT'],
1453
				isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1454
		}
1455
		$sql = "$sql $postLimitTail";
1456
1457
		if ( isset( $options['EXPLAIN'] ) ) {
1458
			$sql = 'EXPLAIN ' . $sql;
1459
		}
1460
1461
		return $sql;
1462
	}
1463
1464
	public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1465
		$options = [], $join_conds = []
1466
	) {
1467
		$options = (array)$options;
1468
		$options['LIMIT'] = 1;
1469
		$res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1470
1471
		if ( $res === false ) {
1472
			return false;
1473
		}
1474
1475
		if ( !$this->numRows( $res ) ) {
1476
			return false;
1477
		}
1478
1479
		$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 1469 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...
1480
1481
		return $obj;
1482
	}
1483
1484
	public function estimateRowCount(
1485
		$table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1486
	) {
1487
		$rows = 0;
1488
		$res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1489
1490 View Code Duplication
		if ( $res ) {
1491
			$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 1488 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...
1492
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1493
		}
1494
1495
		return $rows;
1496
	}
1497
1498
	public function selectRowCount(
1499
		$tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1500
	) {
1501
		$rows = 0;
1502
		$sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1503
		$res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
1504
1505 View Code Duplication
		if ( $res ) {
1506
			$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 1503 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...
1507
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1508
		}
1509
1510
		return $rows;
1511
	}
1512
1513
	/**
1514
	 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1515
	 * It's only slightly flawed. Don't use for anything important.
1516
	 *
1517
	 * @param string $sql A SQL Query
1518
	 *
1519
	 * @return string
1520
	 */
1521
	protected static function generalizeSQL( $sql ) {
1522
		# This does the same as the regexp below would do, but in such a way
1523
		# as to avoid crashing php on some large strings.
1524
		# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1525
1526
		$sql = str_replace( "\\\\", '', $sql );
1527
		$sql = str_replace( "\\'", '', $sql );
1528
		$sql = str_replace( "\\\"", '', $sql );
1529
		$sql = preg_replace( "/'.*'/s", "'X'", $sql );
1530
		$sql = preg_replace( '/".*"/s', "'X'", $sql );
1531
1532
		# All newlines, tabs, etc replaced by single space
1533
		$sql = preg_replace( '/\s+/', ' ', $sql );
1534
1535
		# All numbers => N,
1536
		# except the ones surrounded by characters, e.g. l10n
1537
		$sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1538
		$sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1539
1540
		return $sql;
1541
	}
1542
1543
	public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1544
		$info = $this->fieldInfo( $table, $field );
1545
1546
		return (bool)$info;
1547
	}
1548
1549
	public function indexExists( $table, $index, $fname = __METHOD__ ) {
1550
		if ( !$this->tableExists( $table ) ) {
1551
			return null;
1552
		}
1553
1554
		$info = $this->indexInfo( $table, $index, $fname );
1555
		if ( is_null( $info ) ) {
1556
			return null;
1557
		} else {
1558
			return $info !== false;
1559
		}
1560
	}
1561
1562
	public function tableExists( $table, $fname = __METHOD__ ) {
1563
		$table = $this->tableName( $table );
1564
		$old = $this->ignoreErrors( true );
1565
		$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
1566
		$this->ignoreErrors( $old );
1567
1568
		return (bool)$res;
1569
	}
1570
1571
	public function indexUnique( $table, $index ) {
1572
		$indexInfo = $this->indexInfo( $table, $index );
1573
1574
		if ( !$indexInfo ) {
1575
			return null;
1576
		}
1577
1578
		return !$indexInfo[0]->Non_unique;
1579
	}
1580
1581
	/**
1582
	 * Helper for DatabaseBase::insert().
1583
	 *
1584
	 * @param array $options
1585
	 * @return string
1586
	 */
1587
	protected function makeInsertOptions( $options ) {
1588
		return implode( ' ', $options );
1589
	}
1590
1591
	public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1592
		# No rows to insert, easy just return now
1593
		if ( !count( $a ) ) {
1594
			return true;
1595
		}
1596
1597
		$table = $this->tableName( $table );
1598
1599
		if ( !is_array( $options ) ) {
1600
			$options = [ $options ];
1601
		}
1602
1603
		$fh = null;
1604
		if ( isset( $options['fileHandle'] ) ) {
1605
			$fh = $options['fileHandle'];
1606
		}
1607
		$options = $this->makeInsertOptions( $options );
1608
1609
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1610
			$multi = true;
1611
			$keys = array_keys( $a[0] );
1612
		} else {
1613
			$multi = false;
1614
			$keys = array_keys( $a );
1615
		}
1616
1617
		$sql = 'INSERT ' . $options .
1618
			" INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1619
1620
		if ( $multi ) {
1621
			$first = true;
1622 View Code Duplication
			foreach ( $a as $row ) {
1623
				if ( $first ) {
1624
					$first = false;
1625
				} else {
1626
					$sql .= ',';
1627
				}
1628
				$sql .= '(' . $this->makeList( $row ) . ')';
1629
			}
1630
		} else {
1631
			$sql .= '(' . $this->makeList( $a ) . ')';
1632
		}
1633
1634
		if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1635
			return false;
1636
		} elseif ( $fh !== null ) {
1637
			return true;
1638
		}
1639
1640
		return (bool)$this->query( $sql, $fname );
1641
	}
1642
1643
	/**
1644
	 * Make UPDATE options array for DatabaseBase::makeUpdateOptions
1645
	 *
1646
	 * @param array $options
1647
	 * @return array
1648
	 */
1649
	protected function makeUpdateOptionsArray( $options ) {
1650
		if ( !is_array( $options ) ) {
1651
			$options = [ $options ];
1652
		}
1653
1654
		$opts = [];
1655
1656
		if ( in_array( 'LOW_PRIORITY', $options ) ) {
1657
			$opts[] = $this->lowPriorityOption();
1658
		}
1659
1660
		if ( in_array( 'IGNORE', $options ) ) {
1661
			$opts[] = 'IGNORE';
1662
		}
1663
1664
		return $opts;
1665
	}
1666
1667
	/**
1668
	 * Make UPDATE options for the DatabaseBase::update function
1669
	 *
1670
	 * @param array $options The options passed to DatabaseBase::update
1671
	 * @return string
1672
	 */
1673
	protected function makeUpdateOptions( $options ) {
1674
		$opts = $this->makeUpdateOptionsArray( $options );
1675
1676
		return implode( ' ', $opts );
1677
	}
1678
1679
	function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1680
		$table = $this->tableName( $table );
1681
		$opts = $this->makeUpdateOptions( $options );
1682
		$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
1683
1684 View Code Duplication
		if ( $conds !== [] && $conds !== '*' ) {
1685
			$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
1686
		}
1687
1688
		return $this->query( $sql, $fname );
1689
	}
1690
1691
	public function makeList( $a, $mode = LIST_COMMA ) {
1692
		if ( !is_array( $a ) ) {
1693
			throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
1694
		}
1695
1696
		$first = true;
1697
		$list = '';
1698
1699
		foreach ( $a as $field => $value ) {
1700
			if ( !$first ) {
1701
				if ( $mode == LIST_AND ) {
1702
					$list .= ' AND ';
1703
				} elseif ( $mode == LIST_OR ) {
1704
					$list .= ' OR ';
1705
				} else {
1706
					$list .= ',';
1707
				}
1708
			} else {
1709
				$first = false;
1710
			}
1711
1712
			if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
1713
				$list .= "($value)";
1714
			} elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
1715
				$list .= "$value";
1716
			} elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
1717
				// Remove null from array to be handled separately if found
1718
				$includeNull = false;
1719
				foreach ( array_keys( $value, null, true ) as $nullKey ) {
1720
					$includeNull = true;
1721
					unset( $value[$nullKey] );
1722
				}
1723
				if ( count( $value ) == 0 && !$includeNull ) {
1724
					throw new MWException( __METHOD__ . ": empty input for field $field" );
1725
				} elseif ( count( $value ) == 0 ) {
1726
					// only check if $field is null
1727
					$list .= "$field IS NULL";
1728
				} else {
1729
					// IN clause contains at least one valid element
1730
					if ( $includeNull ) {
1731
						// Group subconditions to ensure correct precedence
1732
						$list .= '(';
1733
					}
1734
					if ( count( $value ) == 1 ) {
1735
						// Special-case single values, as IN isn't terribly efficient
1736
						// Don't necessarily assume the single key is 0; we don't
1737
						// enforce linear numeric ordering on other arrays here.
1738
						$value = array_values( $value )[0];
1739
						$list .= $field . " = " . $this->addQuotes( $value );
1740
					} else {
1741
						$list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1742
					}
1743
					// if null present in array, append IS NULL
1744
					if ( $includeNull ) {
1745
						$list .= " OR $field IS NULL)";
1746
					}
1747
				}
1748
			} elseif ( $value === null ) {
1749 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR ) {
1750
					$list .= "$field IS ";
1751
				} elseif ( $mode == LIST_SET ) {
1752
					$list .= "$field = ";
1753
				}
1754
				$list .= 'NULL';
1755
			} else {
1756 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
1757
					$list .= "$field = ";
1758
				}
1759
				$list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
1760
			}
1761
		}
1762
1763
		return $list;
1764
	}
1765
1766
	public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1767
		$conds = [];
1768
1769
		foreach ( $data as $base => $sub ) {
1770
			if ( count( $sub ) ) {
1771
				$conds[] = $this->makeList(
1772
					[ $baseKey => $base, $subKey => array_keys( $sub ) ],
1773
					LIST_AND );
1774
			}
1775
		}
1776
1777
		if ( $conds ) {
1778
			return $this->makeList( $conds, LIST_OR );
1779
		} else {
1780
			// Nothing to search for...
1781
			return false;
1782
		}
1783
	}
1784
1785
	/**
1786
	 * Return aggregated value alias
1787
	 *
1788
	 * @param array $valuedata
1789
	 * @param string $valuename
1790
	 *
1791
	 * @return string
1792
	 */
1793
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
1794
		return $valuename;
1795
	}
1796
1797
	public function bitNot( $field ) {
1798
		return "(~$field)";
1799
	}
1800
1801
	public function bitAnd( $fieldLeft, $fieldRight ) {
1802
		return "($fieldLeft & $fieldRight)";
1803
	}
1804
1805
	public function bitOr( $fieldLeft, $fieldRight ) {
1806
		return "($fieldLeft | $fieldRight)";
1807
	}
1808
1809
	public function buildConcat( $stringList ) {
1810
		return 'CONCAT(' . implode( ',', $stringList ) . ')';
1811
	}
1812
1813 View Code Duplication
	public function buildGroupConcatField(
1814
		$delim, $table, $field, $conds = '', $join_conds = []
1815
	) {
1816
		$fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1817
1818
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1819
	}
1820
1821
	/**
1822
	 * @param string $field Field or column to cast
1823
	 * @return string
1824
	 * @since 1.28
1825
	 */
1826
	public function buildStringCast( $field ) {
1827
		return $field;
1828
	}
1829
1830
	public function selectDB( $db ) {
1831
		# Stub. Shouldn't cause serious problems if it's not overridden, but
1832
		# if your database engine supports a concept similar to MySQL's
1833
		# databases you may as well.
1834
		$this->mDBname = $db;
1835
1836
		return true;
1837
	}
1838
1839
	public function getDBname() {
1840
		return $this->mDBname;
1841
	}
1842
1843
	public function getServer() {
1844
		return $this->mServer;
1845
	}
1846
1847
	/**
1848
	 * Format a table name ready for use in constructing an SQL query
1849
	 *
1850
	 * This does two important things: it quotes the table names to clean them up,
1851
	 * and it adds a table prefix if only given a table name with no quotes.
1852
	 *
1853
	 * All functions of this object which require a table name call this function
1854
	 * themselves. Pass the canonical name to such functions. This is only needed
1855
	 * when calling query() directly.
1856
	 *
1857
	 * @note This function does not sanitize user input. It is not safe to use
1858
	 *   this function to escape user input.
1859
	 * @param string $name Database table name
1860
	 * @param string $format One of:
1861
	 *   quoted - Automatically pass the table name through addIdentifierQuotes()
1862
	 *            so that it can be used in a query.
1863
	 *   raw - Do not add identifier quotes to the table name
1864
	 * @return string Full database name
1865
	 */
1866
	public function tableName( $name, $format = 'quoted' ) {
1867
		global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema;
1868
		# Skip the entire process when we have a string quoted on both ends.
1869
		# Note that we check the end so that we will still quote any use of
1870
		# use of `database`.table. But won't break things if someone wants
1871
		# to query a database table with a dot in the name.
1872
		if ( $this->isQuotedIdentifier( $name ) ) {
1873
			return $name;
1874
		}
1875
1876
		# Lets test for any bits of text that should never show up in a table
1877
		# name. Basically anything like JOIN or ON which are actually part of
1878
		# SQL queries, but may end up inside of the table value to combine
1879
		# sql. Such as how the API is doing.
1880
		# Note that we use a whitespace test rather than a \b test to avoid
1881
		# any remote case where a word like on may be inside of a table name
1882
		# surrounded by symbols which may be considered word breaks.
1883
		if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1884
			return $name;
1885
		}
1886
1887
		# Split database and table into proper variables.
1888
		# We reverse the explode so that database.table and table both output
1889
		# the correct table.
1890
		$dbDetails = explode( '.', $name, 3 );
1891
		if ( count( $dbDetails ) == 3 ) {
1892
			list( $database, $schema, $table ) = $dbDetails;
1893
			# We don't want any prefix added in this case
1894
			$prefix = '';
1895
		} elseif ( count( $dbDetails ) == 2 ) {
1896
			list( $database, $table ) = $dbDetails;
1897
			# We don't want any prefix added in this case
1898
			# In dbs that support it, $database may actually be the schema
1899
			# but that doesn't affect any of the functionality here
1900
			$prefix = '';
1901
			$schema = null;
1902
		} else {
1903
			list( $table ) = $dbDetails;
1904
			if ( $wgSharedDB !== null # We have a shared database
1905
				&& $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...
1906
				&& !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`'
1907
				&& in_array( $table, $wgSharedTables ) # A shared table is selected
1908
			) {
1909
				$database = $wgSharedDB;
1910
				$schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema;
1911
				$prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
1912
			} else {
1913
				$database = null;
1914
				$schema = $this->mSchema; # Default schema
1915
				$prefix = $this->mTablePrefix; # Default prefix
1916
			}
1917
		}
1918
1919
		# Quote $table and apply the prefix if not quoted.
1920
		# $tableName might be empty if this is called from Database::replaceVars()
1921
		$tableName = "{$prefix}{$table}";
1922
		if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) {
1923
			$tableName = $this->addIdentifierQuotes( $tableName );
1924
		}
1925
1926
		# Quote $schema and merge it with the table name if needed
1927 View Code Duplication
		if ( strlen( $schema ) ) {
1928
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
1929
				$schema = $this->addIdentifierQuotes( $schema );
1930
			}
1931
			$tableName = $schema . '.' . $tableName;
1932
		}
1933
1934
		# Quote $database and merge it with the table name if needed
1935 View Code Duplication
		if ( $database !== null ) {
1936
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
1937
				$database = $this->addIdentifierQuotes( $database );
1938
			}
1939
			$tableName = $database . '.' . $tableName;
1940
		}
1941
1942
		return $tableName;
1943
	}
1944
1945
	/**
1946
	 * Fetch a number of table names into an array
1947
	 * This is handy when you need to construct SQL for joins
1948
	 *
1949
	 * Example:
1950
	 * extract( $dbr->tableNames( 'user', 'watchlist' ) );
1951
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1952
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1953
	 *
1954
	 * @return array
1955
	 */
1956 View Code Duplication
	public function tableNames() {
1957
		$inArray = func_get_args();
1958
		$retVal = [];
1959
1960
		foreach ( $inArray as $name ) {
1961
			$retVal[$name] = $this->tableName( $name );
1962
		}
1963
1964
		return $retVal;
1965
	}
1966
1967
	/**
1968
	 * Fetch a number of table names into an zero-indexed numerical array
1969
	 * This is handy when you need to construct SQL for joins
1970
	 *
1971
	 * Example:
1972
	 * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
1973
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1974
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1975
	 *
1976
	 * @return array
1977
	 */
1978 View Code Duplication
	public function tableNamesN() {
1979
		$inArray = func_get_args();
1980
		$retVal = [];
1981
1982
		foreach ( $inArray as $name ) {
1983
			$retVal[] = $this->tableName( $name );
1984
		}
1985
1986
		return $retVal;
1987
	}
1988
1989
	/**
1990
	 * Get an aliased table name
1991
	 * e.g. tableName AS newTableName
1992
	 *
1993
	 * @param string $name Table name, see tableName()
1994
	 * @param string|bool $alias Alias (optional)
1995
	 * @return string SQL name for aliased table. Will not alias a table to its own name
1996
	 */
1997
	public function tableNameWithAlias( $name, $alias = false ) {
1998
		if ( !$alias || $alias == $name ) {
1999
			return $this->tableName( $name );
2000
		} else {
2001
			return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1997 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...
2002
		}
2003
	}
2004
2005
	/**
2006
	 * Gets an array of aliased table names
2007
	 *
2008
	 * @param array $tables [ [alias] => table ]
2009
	 * @return string[] See tableNameWithAlias()
2010
	 */
2011
	public function tableNamesWithAlias( $tables ) {
2012
		$retval = [];
2013
		foreach ( $tables as $alias => $table ) {
2014
			if ( is_numeric( $alias ) ) {
2015
				$alias = $table;
2016
			}
2017
			$retval[] = $this->tableNameWithAlias( $table, $alias );
2018
		}
2019
2020
		return $retval;
2021
	}
2022
2023
	/**
2024
	 * Get an aliased field name
2025
	 * e.g. fieldName AS newFieldName
2026
	 *
2027
	 * @param string $name Field name
2028
	 * @param string|bool $alias Alias (optional)
2029
	 * @return string SQL name for aliased field. Will not alias a field to its own name
2030
	 */
2031
	public function fieldNameWithAlias( $name, $alias = false ) {
2032
		if ( !$alias || (string)$alias === (string)$name ) {
2033
			return $name;
2034
		} else {
2035
			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 2031 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...
2036
		}
2037
	}
2038
2039
	/**
2040
	 * Gets an array of aliased field names
2041
	 *
2042
	 * @param array $fields [ [alias] => field ]
2043
	 * @return string[] See fieldNameWithAlias()
2044
	 */
2045
	public function fieldNamesWithAlias( $fields ) {
2046
		$retval = [];
2047
		foreach ( $fields as $alias => $field ) {
2048
			if ( is_numeric( $alias ) ) {
2049
				$alias = $field;
2050
			}
2051
			$retval[] = $this->fieldNameWithAlias( $field, $alias );
2052
		}
2053
2054
		return $retval;
2055
	}
2056
2057
	/**
2058
	 * Get the aliased table name clause for a FROM clause
2059
	 * which might have a JOIN and/or USE INDEX or IGNORE INDEX clause
2060
	 *
2061
	 * @param array $tables ( [alias] => table )
2062
	 * @param array $use_index Same as for select()
2063
	 * @param array $ignore_index Same as for select()
2064
	 * @param array $join_conds Same as for select()
2065
	 * @return string
2066
	 */
2067
	protected function tableNamesWithIndexClauseOrJOIN(
2068
		$tables, $use_index = [], $ignore_index = [], $join_conds = []
2069
	) {
2070
		$ret = [];
2071
		$retJOIN = [];
2072
		$use_index = (array)$use_index;
2073
		$ignore_index = (array)$ignore_index;
2074
		$join_conds = (array)$join_conds;
2075
2076
		foreach ( $tables as $alias => $table ) {
2077
			if ( !is_string( $alias ) ) {
2078
				// No alias? Set it equal to the table name
2079
				$alias = $table;
2080
			}
2081
			// Is there a JOIN clause for this table?
2082
			if ( isset( $join_conds[$alias] ) ) {
2083
				list( $joinType, $conds ) = $join_conds[$alias];
2084
				$tableClause = $joinType;
2085
				$tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
2086 View Code Duplication
				if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
2087
					$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
2088
					if ( $use != '' ) {
2089
						$tableClause .= ' ' . $use;
2090
					}
2091
				}
2092 View Code Duplication
				if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
2093
					$ignore = $this->ignoreIndexClause( implode( ',', (array)$ignore_index[$alias] ) );
2094
					if ( $ignore != '' ) {
2095
						$tableClause .= ' ' . $ignore;
2096
					}
2097
				}
2098
				$on = $this->makeList( (array)$conds, LIST_AND );
2099
				if ( $on != '' ) {
2100
					$tableClause .= ' ON (' . $on . ')';
2101
				}
2102
2103
				$retJOIN[] = $tableClause;
2104
			} elseif ( isset( $use_index[$alias] ) ) {
2105
				// Is there an INDEX clause for this table?
2106
				$tableClause = $this->tableNameWithAlias( $table, $alias );
2107
				$tableClause .= ' ' . $this->useIndexClause(
2108
					implode( ',', (array)$use_index[$alias] )
2109
				);
2110
2111
				$ret[] = $tableClause;
2112
			} elseif ( isset( $ignore_index[$alias] ) ) {
2113
				// Is there an INDEX clause for this table?
2114
				$tableClause = $this->tableNameWithAlias( $table, $alias );
2115
				$tableClause .= ' ' . $this->ignoreIndexClause(
2116
					implode( ',', (array)$ignore_index[$alias] )
2117
				);
2118
2119
				$ret[] = $tableClause;
2120
			} else {
2121
				$tableClause = $this->tableNameWithAlias( $table, $alias );
2122
2123
				$ret[] = $tableClause;
2124
			}
2125
		}
2126
2127
		// We can't separate explicit JOIN clauses with ',', use ' ' for those
2128
		$implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
2129
		$explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
2130
2131
		// Compile our final table clause
2132
		return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2133
	}
2134
2135
	/**
2136
	 * Get the name of an index in a given table.
2137
	 *
2138
	 * @param string $index
2139
	 * @return string
2140
	 */
2141
	protected function indexName( $index ) {
2142
		// Backwards-compatibility hack
2143
		$renamed = [
2144
			'ar_usertext_timestamp' => 'usertext_timestamp',
2145
			'un_user_id' => 'user_id',
2146
			'un_user_ip' => 'user_ip',
2147
		];
2148
2149
		if ( isset( $renamed[$index] ) ) {
2150
			return $renamed[$index];
2151
		} else {
2152
			return $index;
2153
		}
2154
	}
2155
2156
	public function addQuotes( $s ) {
2157
		if ( $s instanceof Blob ) {
2158
			$s = $s->fetch();
2159
		}
2160
		if ( $s === null ) {
2161
			return 'NULL';
2162
		} else {
2163
			# This will also quote numeric values. This should be harmless,
2164
			# and protects against weird problems that occur when they really
2165
			# _are_ strings such as article titles and string->number->string
2166
			# conversion is not 1:1.
2167
			return "'" . $this->strencode( $s ) . "'";
2168
		}
2169
	}
2170
2171
	/**
2172
	 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
2173
	 * MySQL uses `backticks` while basically everything else uses double quotes.
2174
	 * Since MySQL is the odd one out here the double quotes are our generic
2175
	 * and we implement backticks in DatabaseMysql.
2176
	 *
2177
	 * @param string $s
2178
	 * @return string
2179
	 */
2180
	public function addIdentifierQuotes( $s ) {
2181
		return '"' . str_replace( '"', '""', $s ) . '"';
2182
	}
2183
2184
	/**
2185
	 * Returns if the given identifier looks quoted or not according to
2186
	 * the database convention for quoting identifiers .
2187
	 *
2188
	 * @note Do not use this to determine if untrusted input is safe.
2189
	 *   A malicious user can trick this function.
2190
	 * @param string $name
2191
	 * @return bool
2192
	 */
2193
	public function isQuotedIdentifier( $name ) {
2194
		return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2195
	}
2196
2197
	/**
2198
	 * @param string $s
2199
	 * @return string
2200
	 */
2201
	protected function escapeLikeInternal( $s ) {
2202
		return addcslashes( $s, '\%_' );
2203
	}
2204
2205
	public function buildLike() {
2206
		$params = func_get_args();
2207
2208
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2209
			$params = $params[0];
2210
		}
2211
2212
		$s = '';
2213
2214
		foreach ( $params as $value ) {
2215
			if ( $value instanceof LikeMatch ) {
2216
				$s .= $value->toString();
2217
			} else {
2218
				$s .= $this->escapeLikeInternal( $value );
2219
			}
2220
		}
2221
2222
		return " LIKE {$this->addQuotes( $s )} ";
2223
	}
2224
2225
	public function anyChar() {
2226
		return new LikeMatch( '_' );
2227
	}
2228
2229
	public function anyString() {
2230
		return new LikeMatch( '%' );
2231
	}
2232
2233
	public function nextSequenceValue( $seqName ) {
2234
		return null;
2235
	}
2236
2237
	/**
2238
	 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2239
	 * is only needed because a) MySQL must be as efficient as possible due to
2240
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2241
	 * which index to pick. Anyway, other databases might have different
2242
	 * indexes on a given table. So don't bother overriding this unless you're
2243
	 * MySQL.
2244
	 * @param string $index
2245
	 * @return string
2246
	 */
2247
	public function useIndexClause( $index ) {
2248
		return '';
2249
	}
2250
2251
	/**
2252
	 * IGNORE INDEX clause. Unlikely to be useful for anything but MySQL. This
2253
	 * is only needed because a) MySQL must be as efficient as possible due to
2254
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2255
	 * which index to pick. Anyway, other databases might have different
2256
	 * indexes on a given table. So don't bother overriding this unless you're
2257
	 * MySQL.
2258
	 * @param string $index
2259
	 * @return string
2260
	 */
2261
	public function ignoreIndexClause( $index ) {
2262
		return '';
2263
	}
2264
2265
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2266
		$quotedTable = $this->tableName( $table );
2267
2268
		if ( count( $rows ) == 0 ) {
2269
			return;
2270
		}
2271
2272
		# Single row case
2273
		if ( !is_array( reset( $rows ) ) ) {
2274
			$rows = [ $rows ];
2275
		}
2276
2277
		// @FXIME: this is not atomic, but a trx would break affectedRows()
2278
		foreach ( $rows as $row ) {
2279
			# Delete rows which collide
2280
			if ( $uniqueIndexes ) {
2281
				$sql = "DELETE FROM $quotedTable WHERE ";
2282
				$first = true;
2283
				foreach ( $uniqueIndexes as $index ) {
2284
					if ( $first ) {
2285
						$first = false;
2286
						$sql .= '( ';
2287
					} else {
2288
						$sql .= ' ) OR ( ';
2289
					}
2290
					if ( is_array( $index ) ) {
2291
						$first2 = true;
2292
						foreach ( $index as $col ) {
2293
							if ( $first2 ) {
2294
								$first2 = false;
2295
							} else {
2296
								$sql .= ' AND ';
2297
							}
2298
							$sql .= $col . '=' . $this->addQuotes( $row[$col] );
2299
						}
2300
					} else {
2301
						$sql .= $index . '=' . $this->addQuotes( $row[$index] );
2302
					}
2303
				}
2304
				$sql .= ' )';
2305
				$this->query( $sql, $fname );
2306
			}
2307
2308
			# Now insert the row
2309
			$this->insert( $table, $row, $fname );
2310
		}
2311
	}
2312
2313
	/**
2314
	 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2315
	 * statement.
2316
	 *
2317
	 * @param string $table Table name
2318
	 * @param array|string $rows Row(s) to insert
2319
	 * @param string $fname Caller function name
2320
	 *
2321
	 * @return ResultWrapper
2322
	 */
2323
	protected function nativeReplace( $table, $rows, $fname ) {
2324
		$table = $this->tableName( $table );
2325
2326
		# Single row case
2327
		if ( !is_array( reset( $rows ) ) ) {
2328
			$rows = [ $rows ];
2329
		}
2330
2331
		$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2332
		$first = true;
2333
2334 View Code Duplication
		foreach ( $rows as $row ) {
2335
			if ( $first ) {
2336
				$first = false;
2337
			} else {
2338
				$sql .= ',';
2339
			}
2340
2341
			$sql .= '(' . $this->makeList( $row ) . ')';
2342
		}
2343
2344
		return $this->query( $sql, $fname );
2345
	}
2346
2347
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2348
		$fname = __METHOD__
2349
	) {
2350
		if ( !count( $rows ) ) {
2351
			return true; // nothing to do
2352
		}
2353
2354
		if ( !is_array( reset( $rows ) ) ) {
2355
			$rows = [ $rows ];
2356
		}
2357
2358
		if ( count( $uniqueIndexes ) ) {
2359
			$clauses = []; // list WHERE clauses that each identify a single row
2360
			foreach ( $rows as $row ) {
2361
				foreach ( $uniqueIndexes as $index ) {
2362
					$index = is_array( $index ) ? $index : [ $index ]; // columns
2363
					$rowKey = []; // unique key to this row
2364
					foreach ( $index as $column ) {
2365
						$rowKey[$column] = $row[$column];
2366
					}
2367
					$clauses[] = $this->makeList( $rowKey, LIST_AND );
2368
				}
2369
			}
2370
			$where = [ $this->makeList( $clauses, LIST_OR ) ];
2371
		} else {
2372
			$where = false;
2373
		}
2374
2375
		$useTrx = !$this->mTrxLevel;
2376
		if ( $useTrx ) {
2377
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2378
		}
2379
		try {
2380
			# Update any existing conflicting row(s)
2381
			if ( $where !== false ) {
2382
				$ok = $this->update( $table, $set, $where, $fname );
2383
			} else {
2384
				$ok = true;
2385
			}
2386
			# Now insert any non-conflicting row(s)
2387
			$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2388
		} catch ( Exception $e ) {
2389
			if ( $useTrx ) {
2390
				$this->rollback( $fname, self::FLUSHING_INTERNAL );
2391
			}
2392
			throw $e;
2393
		}
2394
		if ( $useTrx ) {
2395
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2396
		}
2397
2398
		return $ok;
2399
	}
2400
2401 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2402
		$fname = __METHOD__
2403
	) {
2404
		if ( !$conds ) {
2405
			throw new DBUnexpectedError( $this,
2406
				'DatabaseBase::deleteJoin() called with empty $conds' );
2407
		}
2408
2409
		$delTable = $this->tableName( $delTable );
2410
		$joinTable = $this->tableName( $joinTable );
2411
		$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2412
		if ( $conds != '*' ) {
2413
			$sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
2414
		}
2415
		$sql .= ')';
2416
2417
		$this->query( $sql, $fname );
2418
	}
2419
2420
	/**
2421
	 * Returns the size of a text field, or -1 for "unlimited"
2422
	 *
2423
	 * @param string $table
2424
	 * @param string $field
2425
	 * @return int
2426
	 */
2427
	public function textFieldSize( $table, $field ) {
2428
		$table = $this->tableName( $table );
2429
		$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2430
		$res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
2431
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql, 'DatabaseBase::textFieldSize') on line 2430 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...
2432
2433
		$m = [];
2434
2435
		if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2436
			$size = $m[1];
2437
		} else {
2438
			$size = -1;
2439
		}
2440
2441
		return $size;
2442
	}
2443
2444
	/**
2445
	 * A string to insert into queries to show that they're low-priority, like
2446
	 * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
2447
	 * string and nothing bad should happen.
2448
	 *
2449
	 * @return string Returns the text of the low priority option if it is
2450
	 *   supported, or a blank string otherwise
2451
	 */
2452
	public function lowPriorityOption() {
2453
		return '';
2454
	}
2455
2456
	public function delete( $table, $conds, $fname = __METHOD__ ) {
2457
		if ( !$conds ) {
2458
			throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
2459
		}
2460
2461
		$table = $this->tableName( $table );
2462
		$sql = "DELETE FROM $table";
2463
2464 View Code Duplication
		if ( $conds != '*' ) {
2465
			if ( is_array( $conds ) ) {
2466
				$conds = $this->makeList( $conds, LIST_AND );
2467
			}
2468
			$sql .= ' WHERE ' . $conds;
2469
		}
2470
2471
		return $this->query( $sql, $fname );
2472
	}
2473
2474
	public function insertSelect(
2475
		$destTable, $srcTable, $varMap, $conds,
2476
		$fname = __METHOD__, $insertOptions = [], $selectOptions = []
2477
	) {
2478
		if ( $this->cliMode ) {
2479
			// For massive migrations with downtime, we don't want to select everything
2480
			// into memory and OOM, so do all this native on the server side if possible.
2481
			return $this->nativeInsertSelect(
2482
				$destTable,
2483
				$srcTable,
2484
				$varMap,
2485
				$conds,
2486
				$fname,
2487
				$insertOptions,
2488
				$selectOptions
2489
			);
2490
		}
2491
2492
		// For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
2493
		// on only the master (without needing row-based-replication). It also makes it easy to
2494
		// know how big the INSERT is going to be.
2495
		$fields = [];
2496
		foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
2497
			$fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
2498
		}
2499
		$selectOptions[] = 'FOR UPDATE';
2500
		$res = $this->select( $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions );
2501
		if ( !$res ) {
2502
			return false;
2503
		}
2504
2505
		$rows = [];
2506
		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...
2507
			$rows[] = (array)$row;
2508
		}
2509
2510
		return $this->insert( $destTable, $rows, $fname, $insertOptions );
2511
	}
2512
2513
	public function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
2514
		$fname = __METHOD__,
2515
		$insertOptions = [], $selectOptions = []
2516
	) {
2517
		$destTable = $this->tableName( $destTable );
2518
2519
		if ( !is_array( $insertOptions ) ) {
2520
			$insertOptions = [ $insertOptions ];
2521
		}
2522
2523
		$insertOptions = $this->makeInsertOptions( $insertOptions );
2524
2525
		if ( !is_array( $selectOptions ) ) {
2526
			$selectOptions = [ $selectOptions ];
2527
		}
2528
2529
		list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions(
2530
			$selectOptions );
2531
2532 View Code Duplication
		if ( is_array( $srcTable ) ) {
2533
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2534
		} else {
2535
			$srcTable = $this->tableName( $srcTable );
2536
		}
2537
2538
		$sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2539
			" SELECT $startOpts " . implode( ',', $varMap ) .
2540
			" FROM $srcTable $useIndex $ignoreIndex ";
2541
2542 View Code Duplication
		if ( $conds != '*' ) {
2543
			if ( is_array( $conds ) ) {
2544
				$conds = $this->makeList( $conds, LIST_AND );
2545
			}
2546
			$sql .= " WHERE $conds";
2547
		}
2548
2549
		$sql .= " $tailOpts";
2550
2551
		return $this->query( $sql, $fname );
2552
	}
2553
2554
	/**
2555
	 * Construct a LIMIT query with optional offset. This is used for query
2556
	 * pages. The SQL should be adjusted so that only the first $limit rows
2557
	 * are returned. If $offset is provided as well, then the first $offset
2558
	 * rows should be discarded, and the next $limit rows should be returned.
2559
	 * If the result of the query is not ordered, then the rows to be returned
2560
	 * are theoretically arbitrary.
2561
	 *
2562
	 * $sql is expected to be a SELECT, if that makes a difference.
2563
	 *
2564
	 * The version provided by default works in MySQL and SQLite. It will very
2565
	 * likely need to be overridden for most other DBMSes.
2566
	 *
2567
	 * @param string $sql SQL query we will append the limit too
2568
	 * @param int $limit The SQL limit
2569
	 * @param int|bool $offset The SQL offset (default false)
2570
	 * @throws DBUnexpectedError
2571
	 * @return string
2572
	 */
2573
	public function limitResult( $sql, $limit, $offset = false ) {
2574
		if ( !is_numeric( $limit ) ) {
2575
			throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
2576
		}
2577
2578
		return "$sql LIMIT "
2579
			. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2580
			. "{$limit} ";
2581
	}
2582
2583
	public function unionSupportsOrderAndLimit() {
2584
		return true; // True for almost every DB supported
2585
	}
2586
2587
	public function unionQueries( $sqls, $all ) {
2588
		$glue = $all ? ') UNION ALL (' : ') UNION (';
2589
2590
		return '(' . implode( $glue, $sqls ) . ')';
2591
	}
2592
2593
	public function conditional( $cond, $trueVal, $falseVal ) {
2594
		if ( is_array( $cond ) ) {
2595
			$cond = $this->makeList( $cond, LIST_AND );
2596
		}
2597
2598
		return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2599
	}
2600
2601
	public function strreplace( $orig, $old, $new ) {
2602
		return "REPLACE({$orig}, {$old}, {$new})";
2603
	}
2604
2605
	public function getServerUptime() {
2606
		return 0;
2607
	}
2608
2609
	public function wasDeadlock() {
2610
		return false;
2611
	}
2612
2613
	public function wasLockTimeout() {
2614
		return false;
2615
	}
2616
2617
	public function wasErrorReissuable() {
2618
		return false;
2619
	}
2620
2621
	public function wasReadOnlyError() {
2622
		return false;
2623
	}
2624
2625
	/**
2626
	 * Determines if the given query error was a connection drop
2627
	 * STUB
2628
	 *
2629
	 * @param integer|string $errno
2630
	 * @return bool
2631
	 */
2632
	public function wasConnectionError( $errno ) {
2633
		return false;
2634
	}
2635
2636
	/**
2637
	 * Perform a deadlock-prone transaction.
2638
	 *
2639
	 * This function invokes a callback function to perform a set of write
2640
	 * queries. If a deadlock occurs during the processing, the transaction
2641
	 * will be rolled back and the callback function will be called again.
2642
	 *
2643
	 * Avoid using this method outside of Job or Maintenance classes.
2644
	 *
2645
	 * Usage:
2646
	 *   $dbw->deadlockLoop( callback, ... );
2647
	 *
2648
	 * Extra arguments are passed through to the specified callback function.
2649
	 * This method requires that no transactions are already active to avoid
2650
	 * causing premature commits or exceptions.
2651
	 *
2652
	 * Returns whatever the callback function returned on its successful,
2653
	 * iteration, or false on error, for example if the retry limit was
2654
	 * reached.
2655
	 *
2656
	 * @return mixed
2657
	 * @throws DBUnexpectedError
2658
	 * @throws Exception
2659
	 */
2660
	public function deadlockLoop() {
2661
		$args = func_get_args();
2662
		$function = array_shift( $args );
2663
		$tries = self::DEADLOCK_TRIES;
2664
2665
		$this->begin( __METHOD__ );
2666
2667
		$retVal = null;
2668
		/** @var Exception $e */
2669
		$e = null;
2670
		do {
2671
			try {
2672
				$retVal = call_user_func_array( $function, $args );
2673
				break;
2674
			} catch ( DBQueryError $e ) {
2675
				if ( $this->wasDeadlock() ) {
2676
					// Retry after a randomized delay
2677
					usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2678
				} else {
2679
					// Throw the error back up
2680
					throw $e;
2681
				}
2682
			}
2683
		} while ( --$tries > 0 );
2684
2685
		if ( $tries <= 0 ) {
2686
			// Too many deadlocks; give up
2687
			$this->rollback( __METHOD__ );
2688
			throw $e;
2689
		} else {
2690
			$this->commit( __METHOD__ );
2691
2692
			return $retVal;
2693
		}
2694
	}
2695
2696
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
2697
		# Real waits are implemented in the subclass.
2698
		return 0;
2699
	}
2700
2701
	public function getSlavePos() {
2702
		# Stub
2703
		return false;
2704
	}
2705
2706
	public function getMasterPos() {
2707
		# Stub
2708
		return false;
2709
	}
2710
2711
	public function serverIsReadOnly() {
2712
		return false;
2713
	}
2714
2715
	final public function onTransactionResolution( callable $callback ) {
2716
		if ( !$this->mTrxLevel ) {
2717
			throw new DBUnexpectedError( $this, "No transaction is active." );
2718
		}
2719
		$this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
2720
	}
2721
2722
	final public function onTransactionIdle( callable $callback ) {
2723
		$this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
2724
		if ( !$this->mTrxLevel ) {
2725
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2726
		}
2727
	}
2728
2729
	final public function onTransactionPreCommitOrIdle( callable $callback ) {
2730
		if ( $this->mTrxLevel ) {
2731
			$this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
2732
		} else {
2733
			// If no transaction is active, then make one for this callback
2734
			$this->startAtomic( __METHOD__ );
2735
			try {
2736
				call_user_func( $callback );
2737
				$this->endAtomic( __METHOD__ );
2738
			} catch ( Exception $e ) {
2739
				$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2740
				throw $e;
2741
			}
2742
		}
2743
	}
2744
2745
	final public function setTransactionListener( $name, callable $callback = null ) {
2746
		if ( $callback ) {
2747
			$this->mTrxRecurringCallbacks[$name] = [ $callback, wfGetCaller() ];
2748
		} else {
2749
			unset( $this->mTrxRecurringCallbacks[$name] );
2750
		}
2751
	}
2752
2753
	/**
2754
	 * Whether to disable running of post-COMMIT/ROLLBACK callbacks
2755
	 *
2756
	 * This method should not be used outside of Database/LoadBalancer
2757
	 *
2758
	 * @param bool $suppress
2759
	 * @since 1.28
2760
	 */
2761
	final public function setTrxEndCallbackSuppression( $suppress ) {
2762
		$this->mTrxEndCallbacksSuppressed = $suppress;
2763
	}
2764
2765
	/**
2766
	 * Actually run and consume any "on transaction idle/resolution" callbacks.
2767
	 *
2768
	 * This method should not be used outside of Database/LoadBalancer
2769
	 *
2770
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2771
	 * @since 1.20
2772
	 * @throws Exception
2773
	 */
2774
	public function runOnTransactionIdleCallbacks( $trigger ) {
2775
		if ( $this->mTrxEndCallbacksSuppressed ) {
2776
			return;
2777
		}
2778
2779
		$autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
2780
		/** @var Exception $e */
2781
		$e = null; // first exception
2782
		do { // callbacks may add callbacks :)
2783
			$callbacks = array_merge(
2784
				$this->mTrxIdleCallbacks,
2785
				$this->mTrxEndCallbacks // include "transaction resolution" callbacks
2786
			);
2787
			$this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2788
			$this->mTrxEndCallbacks = []; // consumed (recursion guard)
2789
			foreach ( $callbacks as $callback ) {
2790
				try {
2791
					list( $phpCallback ) = $callback;
2792
					$this->clearFlag( DBO_TRX ); // make each query its own transaction
2793
					call_user_func_array( $phpCallback, [ $trigger ] );
2794
					if ( $autoTrx ) {
2795
						$this->setFlag( DBO_TRX ); // restore automatic begin()
2796
					} else {
2797
						$this->clearFlag( DBO_TRX ); // restore auto-commit
2798
					}
2799
				} catch ( Exception $ex ) {
2800
					MWExceptionHandler::logException( $ex );
2801
					$e = $e ?: $ex;
2802
					// Some callbacks may use startAtomic/endAtomic, so make sure
2803
					// their transactions are ended so other callbacks don't fail
2804
					if ( $this->trxLevel() ) {
2805
						$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2806
					}
2807
				}
2808
			}
2809
		} while ( count( $this->mTrxIdleCallbacks ) );
2810
2811
		if ( $e instanceof Exception ) {
2812
			throw $e; // re-throw any first exception
2813
		}
2814
	}
2815
2816
	/**
2817
	 * Actually run and consume any "on transaction pre-commit" callbacks.
2818
	 *
2819
	 * This method should not be used outside of Database/LoadBalancer
2820
	 *
2821
	 * @since 1.22
2822
	 * @throws Exception
2823
	 */
2824
	public function runOnTransactionPreCommitCallbacks() {
2825
		$e = null; // first exception
2826
		do { // callbacks may add callbacks :)
2827
			$callbacks = $this->mTrxPreCommitCallbacks;
2828
			$this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2829 View Code Duplication
			foreach ( $callbacks as $callback ) {
2830
				try {
2831
					list( $phpCallback ) = $callback;
2832
					call_user_func( $phpCallback );
2833
				} catch ( Exception $ex ) {
2834
					MWExceptionHandler::logException( $ex );
2835
					$e = $e ?: $ex;
2836
				}
2837
			}
2838
		} while ( count( $this->mTrxPreCommitCallbacks ) );
2839
2840
		if ( $e instanceof Exception ) {
2841
			throw $e; // re-throw any first exception
2842
		}
2843
	}
2844
2845
	/**
2846
	 * Actually run any "transaction listener" callbacks.
2847
	 *
2848
	 * This method should not be used outside of Database/LoadBalancer
2849
	 *
2850
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2851
	 * @throws Exception
2852
	 * @since 1.20
2853
	 */
2854
	public function runTransactionListenerCallbacks( $trigger ) {
2855
		if ( $this->mTrxEndCallbacksSuppressed ) {
2856
			return;
2857
		}
2858
2859
		/** @var Exception $e */
2860
		$e = null; // first exception
2861
2862 View Code Duplication
		foreach ( $this->mTrxRecurringCallbacks as $callback ) {
2863
			try {
2864
				list( $phpCallback ) = $callback;
2865
				$phpCallback( $trigger, $this );
2866
			} catch ( Exception $ex ) {
2867
				MWExceptionHandler::logException( $ex );
2868
				$e = $e ?: $ex;
2869
			}
2870
		}
2871
2872
		if ( $e instanceof Exception ) {
2873
			throw $e; // re-throw any first exception
2874
		}
2875
	}
2876
2877
	final public function startAtomic( $fname = __METHOD__ ) {
2878
		if ( !$this->mTrxLevel ) {
2879
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2880
			// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2881
			// in all changes being in one transaction to keep requests transactional.
2882
			if ( !$this->getFlag( DBO_TRX ) ) {
2883
				$this->mTrxAutomaticAtomic = true;
2884
			}
2885
		}
2886
2887
		$this->mTrxAtomicLevels[] = $fname;
2888
	}
2889
2890
	final public function endAtomic( $fname = __METHOD__ ) {
2891
		if ( !$this->mTrxLevel ) {
2892
			throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2893
		}
2894
		if ( !$this->mTrxAtomicLevels ||
2895
			array_pop( $this->mTrxAtomicLevels ) !== $fname
2896
		) {
2897
			throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2898
		}
2899
2900
		if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2901
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2902
		}
2903
	}
2904
2905
	final public function doAtomicSection( $fname, callable $callback ) {
2906
		$this->startAtomic( $fname );
2907
		try {
2908
			$res = call_user_func_array( $callback, [ $this, $fname ] );
2909
		} catch ( Exception $e ) {
2910
			$this->rollback( $fname, self::FLUSHING_INTERNAL );
2911
			throw $e;
2912
		}
2913
		$this->endAtomic( $fname );
2914
2915
		return $res;
2916
	}
2917
2918
	final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2919
		// Protect against mismatched atomic section, transaction nesting, and snapshot loss
2920
		if ( $this->mTrxLevel ) {
2921
			if ( $this->mTrxAtomicLevels ) {
2922
				$levels = implode( ', ', $this->mTrxAtomicLevels );
2923
				$msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2924
				throw new DBUnexpectedError( $this, $msg );
2925
			} elseif ( !$this->mTrxAutomatic ) {
2926
				$msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2927
				throw new DBUnexpectedError( $this, $msg );
2928
			} else {
2929
				// @TODO: make this an exception at some point
2930
				$msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2931
				wfLogDBError( $msg );
2932
				wfWarn( $msg );
2933
				return; // join the main transaction set
2934
			}
2935
		} elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2936
			// @TODO: make this an exception at some point
2937
			$msg = "$fname: Implicit transaction expected (DBO_TRX set).";
2938
			wfLogDBError( $msg );
2939
			wfWarn( $msg );
2940
			return; // let any writes be in the main transaction
2941
		}
2942
2943
		// Avoid fatals if close() was called
2944
		$this->assertOpen();
2945
2946
		$this->doBegin( $fname );
2947
		$this->mTrxTimestamp = microtime( true );
2948
		$this->mTrxFname = $fname;
2949
		$this->mTrxDoneWrites = false;
2950
		$this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
2951
		$this->mTrxAutomaticAtomic = false;
2952
		$this->mTrxAtomicLevels = [];
2953
		$this->mTrxShortId = wfRandomString( 12 );
2954
		$this->mTrxWriteDuration = 0.0;
2955
		$this->mTrxWriteQueryCount = 0;
2956
		$this->mTrxWriteAdjDuration = 0.0;
2957
		$this->mTrxWriteAdjQueryCount = 0;
2958
		$this->mTrxWriteCallers = [];
2959
		// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2960
		// Get an estimate of the replica DB lag before then, treating estimate staleness
2961
		// as lag itself just to be safe
2962
		$status = $this->getApproximateLagStatus();
2963
		$this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
0 ignored issues
show
Documentation Bug introduced by
It seems like $status['lag'] + (microt...ue) - $status['since']) can also be of type integer. However, the property $mTrxReplicaLag is declared as type double. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2964
	}
2965
2966
	/**
2967
	 * Issues the BEGIN command to the database server.
2968
	 *
2969
	 * @see DatabaseBase::begin()
2970
	 * @param string $fname
2971
	 */
2972
	protected function doBegin( $fname ) {
2973
		$this->query( 'BEGIN', $fname );
2974
		$this->mTrxLevel = 1;
2975
	}
2976
2977
	final public function commit( $fname = __METHOD__, $flush = '' ) {
2978
		if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2979
			// There are still atomic sections open. This cannot be ignored
2980
			$levels = implode( ', ', $this->mTrxAtomicLevels );
2981
			throw new DBUnexpectedError(
2982
				$this,
2983
				"$fname: Got COMMIT while atomic sections $levels are still open."
2984
			);
2985
		}
2986
2987
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2988
			if ( !$this->mTrxLevel ) {
2989
				return; // nothing to do
2990
			} elseif ( !$this->mTrxAutomatic ) {
2991
				throw new DBUnexpectedError(
2992
					$this,
2993
					"$fname: Flushing an explicit transaction, getting out of sync."
2994
				);
2995
			}
2996
		} else {
2997
			if ( !$this->mTrxLevel ) {
2998
				wfWarn( "$fname: No transaction to commit, something got out of sync." );
2999
				return; // nothing to do
3000
			} elseif ( $this->mTrxAutomatic ) {
3001
				// @TODO: make this an exception at some point
3002
				$msg = "$fname: Explicit commit of implicit transaction.";
3003
				wfLogDBError( $msg );
3004
				wfWarn( $msg );
3005
				return; // wait for the main transaction set commit round
3006
			}
3007
		}
3008
3009
		// Avoid fatals if close() was called
3010
		$this->assertOpen();
3011
3012
		$this->runOnTransactionPreCommitCallbacks();
3013
		$writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
3014
		$this->doCommit( $fname );
3015
		if ( $this->mTrxDoneWrites ) {
3016
			$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...
3017
			$this->getTransactionProfiler()->transactionWritingOut(
3018
				$this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
0 ignored issues
show
Security Bug introduced by
It seems like $writeTime defined by $this->pendingWriteQuery...elf::ESTIMATE_DB_APPLY) on line 3013 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...
3019
		}
3020
3021
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
3022
		$this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
3023
	}
3024
3025
	/**
3026
	 * Issues the COMMIT command to the database server.
3027
	 *
3028
	 * @see DatabaseBase::commit()
3029
	 * @param string $fname
3030
	 */
3031
	protected function doCommit( $fname ) {
3032
		if ( $this->mTrxLevel ) {
3033
			$this->query( 'COMMIT', $fname );
3034
			$this->mTrxLevel = 0;
3035
		}
3036
	}
3037
3038
	final public function rollback( $fname = __METHOD__, $flush = '' ) {
3039
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
3040
			if ( !$this->mTrxLevel ) {
3041
				return; // nothing to do
3042
			}
3043
		} else {
3044
			if ( !$this->mTrxLevel ) {
3045
				wfWarn( "$fname: No transaction to rollback, something got out of sync." );
3046
				return; // nothing to do
3047
			} elseif ( $this->getFlag( DBO_TRX ) ) {
3048
				throw new DBUnexpectedError(
3049
					$this,
3050
					"$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
3051
				);
3052
			}
3053
		}
3054
3055
		// Avoid fatals if close() was called
3056
		$this->assertOpen();
3057
3058
		$this->doRollback( $fname );
3059
		$this->mTrxAtomicLevels = [];
3060
		if ( $this->mTrxDoneWrites ) {
3061
			$this->getTransactionProfiler()->transactionWritingOut(
3062
				$this->mServer, $this->mDBname, $this->mTrxShortId );
3063
		}
3064
3065
		$this->mTrxIdleCallbacks = []; // clear
3066
		$this->mTrxPreCommitCallbacks = []; // clear
3067
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
3068
		$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
3069
	}
3070
3071
	/**
3072
	 * Issues the ROLLBACK command to the database server.
3073
	 *
3074
	 * @see DatabaseBase::rollback()
3075
	 * @param string $fname
3076
	 */
3077
	protected function doRollback( $fname ) {
3078
		if ( $this->mTrxLevel ) {
3079
			# Disconnects cause rollback anyway, so ignore those errors
3080
			$ignoreErrors = true;
3081
			$this->query( 'ROLLBACK', $fname, $ignoreErrors );
3082
			$this->mTrxLevel = 0;
3083
		}
3084
	}
3085
3086
	public function flushSnapshot( $fname = __METHOD__ ) {
3087
		if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
3088
			// This only flushes transactions to clear snapshots, not to write data
3089
			throw new DBUnexpectedError(
3090
				$this,
3091
				"$fname: Cannot COMMIT to clear snapshot because writes are pending."
3092
			);
3093
		}
3094
3095
		$this->commit( $fname, self::FLUSHING_INTERNAL );
3096
	}
3097
3098
	public function explicitTrxActive() {
3099
		return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
3100
	}
3101
3102
	/**
3103
	 * Creates a new table with structure copied from existing table
3104
	 * Note that unlike most database abstraction functions, this function does not
3105
	 * automatically append database prefix, because it works at a lower
3106
	 * abstraction level.
3107
	 * The table names passed to this function shall not be quoted (this
3108
	 * function calls addIdentifierQuotes when needed).
3109
	 *
3110
	 * @param string $oldName Name of table whose structure should be copied
3111
	 * @param string $newName Name of table to be created
3112
	 * @param bool $temporary Whether the new table should be temporary
3113
	 * @param string $fname Calling function name
3114
	 * @throws MWException
3115
	 * @return bool True if operation was successful
3116
	 */
3117
	public function duplicateTableStructure( $oldName, $newName, $temporary = false,
3118
		$fname = __METHOD__
3119
	) {
3120
		throw new MWException(
3121
			'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
3122
	}
3123
3124
	function listTables( $prefix = null, $fname = __METHOD__ ) {
3125
		throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
3126
	}
3127
3128
	/**
3129
	 * Reset the views process cache set by listViews()
3130
	 * @since 1.22
3131
	 */
3132
	final public function clearViewsCache() {
3133
		$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...
3134
	}
3135
3136
	/**
3137
	 * Lists all the VIEWs in the database
3138
	 *
3139
	 * For caching purposes the list of all views should be stored in
3140
	 * $this->allViews. The process cache can be cleared with clearViewsCache()
3141
	 *
3142
	 * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
3143
	 * @param string $fname Name of calling function
3144
	 * @throws MWException
3145
	 * @return array
3146
	 * @since 1.22
3147
	 */
3148
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
3149
		throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
3150
	}
3151
3152
	/**
3153
	 * Differentiates between a TABLE and a VIEW
3154
	 *
3155
	 * @param string $name Name of the database-structure to test.
3156
	 * @throws MWException
3157
	 * @return bool
3158
	 * @since 1.22
3159
	 */
3160
	public function isView( $name ) {
3161
		throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
3162
	}
3163
3164
	public function timestamp( $ts = 0 ) {
3165
		return wfTimestamp( TS_MW, $ts );
3166
	}
3167
3168
	public function timestampOrNull( $ts = null ) {
3169
		if ( is_null( $ts ) ) {
3170
			return null;
3171
		} else {
3172
			return $this->timestamp( $ts );
3173
		}
3174
	}
3175
3176
	/**
3177
	 * Take the result from a query, and wrap it in a ResultWrapper if
3178
	 * necessary. Boolean values are passed through as is, to indicate success
3179
	 * of write queries or failure.
3180
	 *
3181
	 * Once upon a time, DatabaseBase::query() returned a bare MySQL result
3182
	 * resource, and it was necessary to call this function to convert it to
3183
	 * a wrapper. Nowadays, raw database objects are never exposed to external
3184
	 * callers, so this is unnecessary in external code.
3185
	 *
3186
	 * @param bool|ResultWrapper|resource|object $result
3187
	 * @return bool|ResultWrapper
3188
	 */
3189
	protected function resultObject( $result ) {
3190
		if ( !$result ) {
3191
			return false;
3192
		} elseif ( $result instanceof ResultWrapper ) {
3193
			return $result;
3194
		} elseif ( $result === true ) {
3195
			// Successful write query
3196
			return $result;
3197
		} else {
3198
			return new ResultWrapper( $this, $result );
3199
		}
3200
	}
3201
3202
	public function ping( &$rtt = null ) {
3203
		// Avoid hitting the server if it was hit recently
3204
		if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
3205
			if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
3206
				$rtt = $this->mRTTEstimate;
3207
				return true; // don't care about $rtt
3208
			}
3209
		}
3210
3211
		// This will reconnect if possible or return false if not
3212
		$this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
3213
		$ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
3214
		$this->restoreFlags( self::RESTORE_PRIOR );
3215
3216
		if ( $ok ) {
3217
			$rtt = $this->mRTTEstimate;
3218
		}
3219
3220
		return $ok;
3221
	}
3222
3223
	/**
3224
	 * @return bool
3225
	 */
3226
	protected function reconnect() {
3227
		$this->closeConnection();
3228
		$this->mOpened = false;
3229
		$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...
3230
		try {
3231
			$this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
3232
			$this->lastPing = microtime( true );
3233
			$ok = true;
3234
		} catch ( DBConnectionError $e ) {
3235
			$ok = false;
3236
		}
3237
3238
		return $ok;
3239
	}
3240
3241
	public function getSessionLagStatus() {
3242
		return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
3243
	}
3244
3245
	/**
3246
	 * Get the replica DB lag when the current transaction started
3247
	 *
3248
	 * This is useful when transactions might use snapshot isolation
3249
	 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
3250
	 * is this lag plus transaction duration. If they don't, it is still
3251
	 * safe to be pessimistic. This returns null if there is no transaction.
3252
	 *
3253
	 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
3254
	 * @since 1.27
3255
	 */
3256
	public function getTransactionLagStatus() {
3257
		return $this->mTrxLevel
3258
			? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
3259
			: null;
3260
	}
3261
3262
	/**
3263
	 * Get a replica DB lag estimate for this server
3264
	 *
3265
	 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
3266
	 * @since 1.27
3267
	 */
3268
	public function getApproximateLagStatus() {
3269
		return [
3270
			'lag'   => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
3271
			'since' => microtime( true )
3272
		];
3273
	}
3274
3275
	/**
3276
	 * Merge the result of getSessionLagStatus() for several DBs
3277
	 * using the most pessimistic values to estimate the lag of
3278
	 * any data derived from them in combination
3279
	 *
3280
	 * This is information is useful for caching modules
3281
	 *
3282
	 * @see WANObjectCache::set()
3283
	 * @see WANObjectCache::getWithSetCallback()
3284
	 *
3285
	 * @param IDatabase $db1
3286
	 * @param IDatabase ...
3287
	 * @return array Map of values:
3288
	 *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
3289
	 *   - since: oldest UNIX timestamp of any of the DB lag estimates
3290
	 *   - pending: whether any of the DBs have uncommitted changes
3291
	 * @since 1.27
3292
	 */
3293
	public static function getCacheSetOptions( IDatabase $db1 ) {
3294
		$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3295
		foreach ( func_get_args() as $db ) {
3296
			/** @var IDatabase $db */
3297
			$status = $db->getSessionLagStatus();
3298
			if ( $status['lag'] === false ) {
3299
				$res['lag'] = false;
3300
			} elseif ( $res['lag'] !== false ) {
3301
				$res['lag'] = max( $res['lag'], $status['lag'] );
3302
			}
3303
			$res['since'] = min( $res['since'], $status['since'] );
3304
			$res['pending'] = $res['pending'] ?: $db->writesPending();
3305
		}
3306
3307
		return $res;
3308
	}
3309
3310
	public function getLag() {
3311
		return 0;
3312
	}
3313
3314
	function maxListLen() {
3315
		return 0;
3316
	}
3317
3318
	public function encodeBlob( $b ) {
3319
		return $b;
3320
	}
3321
3322
	public function decodeBlob( $b ) {
3323
		if ( $b instanceof Blob ) {
3324
			$b = $b->fetch();
3325
		}
3326
		return $b;
3327
	}
3328
3329
	public function setSessionOptions( array $options ) {
3330
	}
3331
3332
	/**
3333
	 * Read and execute SQL commands from a file.
3334
	 *
3335
	 * Returns true on success, error string or exception on failure (depending
3336
	 * on object's error ignore settings).
3337
	 *
3338
	 * @param string $filename File name to open
3339
	 * @param bool|callable $lineCallback Optional function called before reading each line
3340
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3341
	 * @param bool|string $fname Calling function name or false if name should be
3342
	 *   generated dynamically using $filename
3343
	 * @param bool|callable $inputCallback Optional function called for each
3344
	 *   complete line sent
3345
	 * @throws Exception|MWException
3346
	 * @return bool|string
3347
	 */
3348
	public function sourceFile(
3349
		$filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
3350
	) {
3351
		MediaWiki\suppressWarnings();
3352
		$fp = fopen( $filename, 'r' );
3353
		MediaWiki\restoreWarnings();
3354
3355
		if ( false === $fp ) {
3356
			throw new MWException( "Could not open \"{$filename}\".\n" );
3357
		}
3358
3359
		if ( !$fname ) {
3360
			$fname = __METHOD__ . "( $filename )";
3361
		}
3362
3363
		try {
3364
			$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 3349 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...
3365
		} catch ( Exception $e ) {
3366
			fclose( $fp );
3367
			throw $e;
3368
		}
3369
3370
		fclose( $fp );
3371
3372
		return $error;
3373
	}
3374
3375
	/**
3376
	 * Get the full path of a patch file. Originally based on archive()
3377
	 * from updaters.inc. Keep in mind this always returns a patch, as
3378
	 * it fails back to MySQL if no DB-specific patch can be found
3379
	 *
3380
	 * @param string $patch The name of the patch, like patch-something.sql
3381
	 * @return string Full path to patch file
3382
	 */
3383 View Code Duplication
	public function patchPath( $patch ) {
3384
		global $IP;
3385
3386
		$dbType = $this->getType();
3387
		if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
3388
			return "$IP/maintenance/$dbType/archives/$patch";
3389
		} else {
3390
			return "$IP/maintenance/archives/$patch";
3391
		}
3392
	}
3393
3394
	public function setSchemaVars( $vars ) {
3395
		$this->mSchemaVars = $vars;
3396
	}
3397
3398
	/**
3399
	 * Read and execute commands from an open file handle.
3400
	 *
3401
	 * Returns true on success, error string or exception on failure (depending
3402
	 * on object's error ignore settings).
3403
	 *
3404
	 * @param resource $fp File handle
3405
	 * @param bool|callable $lineCallback Optional function called before reading each query
3406
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3407
	 * @param string $fname Calling function name
3408
	 * @param bool|callable $inputCallback Optional function called for each complete query sent
3409
	 * @return bool|string
3410
	 */
3411
	public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
3412
		$fname = __METHOD__, $inputCallback = false
3413
	) {
3414
		$cmd = '';
3415
3416
		while ( !feof( $fp ) ) {
3417
			if ( $lineCallback ) {
3418
				call_user_func( $lineCallback );
3419
			}
3420
3421
			$line = trim( fgets( $fp ) );
3422
3423
			if ( $line == '' ) {
3424
				continue;
3425
			}
3426
3427
			if ( '-' == $line[0] && '-' == $line[1] ) {
3428
				continue;
3429
			}
3430
3431
			if ( $cmd != '' ) {
3432
				$cmd .= ' ';
3433
			}
3434
3435
			$done = $this->streamStatementEnd( $cmd, $line );
3436
3437
			$cmd .= "$line\n";
3438
3439
			if ( $done || feof( $fp ) ) {
3440
				$cmd = $this->replaceVars( $cmd );
3441
3442
				if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
3443
					$res = $this->query( $cmd, $fname );
3444
3445
					if ( $resultCallback ) {
3446
						call_user_func( $resultCallback, $res, $this );
3447
					}
3448
3449
					if ( false === $res ) {
3450
						$err = $this->lastError();
3451
3452
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3453
					}
3454
				}
3455
				$cmd = '';
3456
			}
3457
		}
3458
3459
		return true;
3460
	}
3461
3462
	/**
3463
	 * Called by sourceStream() to check if we've reached a statement end
3464
	 *
3465
	 * @param string $sql SQL assembled so far
3466
	 * @param string $newLine New line about to be added to $sql
3467
	 * @return bool Whether $newLine contains end of the statement
3468
	 */
3469
	public function streamStatementEnd( &$sql, &$newLine ) {
3470
		if ( $this->delimiter ) {
3471
			$prev = $newLine;
3472
			$newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3473
			if ( $newLine != $prev ) {
3474
				return true;
3475
			}
3476
		}
3477
3478
		return false;
3479
	}
3480
3481
	/**
3482
	 * Database independent variable replacement. Replaces a set of variables
3483
	 * in an SQL statement with their contents as given by $this->getSchemaVars().
3484
	 *
3485
	 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3486
	 *
3487
	 * - '{$var}' should be used for text and is passed through the database's
3488
	 *   addQuotes method.
3489
	 * - `{$var}` should be used for identifiers (e.g. table and database names).
3490
	 *   It is passed through the database's addIdentifierQuotes method which
3491
	 *   can be overridden if the database uses something other than backticks.
3492
	 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3493
	 *   database's tableName method.
3494
	 * - / *i* / passes the name that follows through the database's indexName method.
3495
	 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3496
	 *   its use should be avoided. In 1.24 and older, string encoding was applied.
3497
	 *
3498
	 * @param string $ins SQL statement to replace variables in
3499
	 * @return string The new SQL statement with variables replaced
3500
	 */
3501
	protected function replaceVars( $ins ) {
3502
		$vars = $this->getSchemaVars();
3503
		return preg_replace_callback(
3504
			'!
3505
				/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3506
				\'\{\$ (\w+) }\'                  | # 3. addQuotes
3507
				`\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
3508
				/\*\$ (\w+) \*/                     # 5. leave unencoded
3509
			!x',
3510
			function ( $m ) use ( $vars ) {
3511
				// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3512
				// check for both nonexistent keys *and* the empty string.
3513
				if ( isset( $m[1] ) && $m[1] !== '' ) {
3514
					if ( $m[1] === 'i' ) {
3515
						return $this->indexName( $m[2] );
3516
					} else {
3517
						return $this->tableName( $m[2] );
3518
					}
3519 View Code Duplication
				} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3520
					return $this->addQuotes( $vars[$m[3]] );
3521
				} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3522
					return $this->addIdentifierQuotes( $vars[$m[4]] );
3523 View Code Duplication
				} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3524
					return $vars[$m[5]];
3525
				} else {
3526
					return $m[0];
3527
				}
3528
			},
3529
			$ins
3530
		);
3531
	}
3532
3533
	/**
3534
	 * Get schema variables. If none have been set via setSchemaVars(), then
3535
	 * use some defaults from the current object.
3536
	 *
3537
	 * @return array
3538
	 */
3539
	protected function getSchemaVars() {
3540
		if ( $this->mSchemaVars ) {
3541
			return $this->mSchemaVars;
3542
		} else {
3543
			return $this->getDefaultSchemaVars();
3544
		}
3545
	}
3546
3547
	/**
3548
	 * Get schema variables to use if none have been set via setSchemaVars().
3549
	 *
3550
	 * Override this in derived classes to provide variables for tables.sql
3551
	 * and SQL patch files.
3552
	 *
3553
	 * @return array
3554
	 */
3555
	protected function getDefaultSchemaVars() {
3556
		return [];
3557
	}
3558
3559
	public function lockIsFree( $lockName, $method ) {
3560
		return true;
3561
	}
3562
3563
	public function lock( $lockName, $method, $timeout = 5 ) {
3564
		$this->mNamedLocksHeld[$lockName] = 1;
3565
3566
		return true;
3567
	}
3568
3569
	public function unlock( $lockName, $method ) {
3570
		unset( $this->mNamedLocksHeld[$lockName] );
3571
3572
		return true;
3573
	}
3574
3575
	public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3576
		if ( $this->writesOrCallbacksPending() ) {
3577
			// This only flushes transactions to clear snapshots, not to write data
3578
			throw new DBUnexpectedError(
3579
				$this,
3580
				"$fname: Cannot COMMIT to clear snapshot because writes are pending."
3581
			);
3582
		}
3583
3584
		if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3585
			return null;
3586
		}
3587
3588
		$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3589
			if ( $this->trxLevel() ) {
3590
				// There is a good chance an exception was thrown, causing any early return
3591
				// from the caller. Let any error handler get a chance to issue rollback().
3592
				// If there isn't one, let the error bubble up and trigger server-side rollback.
3593
				$this->onTransactionResolution( function () use ( $lockKey, $fname ) {
3594
					$this->unlock( $lockKey, $fname );
3595
				} );
3596
			} else {
3597
				$this->unlock( $lockKey, $fname );
3598
			}
3599
		} );
3600
3601
		$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
3602
3603
		return $unlocker;
3604
	}
3605
3606
	public function namedLocksEnqueue() {
3607
		return false;
3608
	}
3609
3610
	/**
3611
	 * Lock specific tables
3612
	 *
3613
	 * @param array $read Array of tables to lock for read access
3614
	 * @param array $write Array of tables to lock for write access
3615
	 * @param string $method Name of caller
3616
	 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3617
	 * @return bool
3618
	 */
3619
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
3620
		return true;
3621
	}
3622
3623
	/**
3624
	 * Unlock specific tables
3625
	 *
3626
	 * @param string $method The caller
3627
	 * @return bool
3628
	 */
3629
	public function unlockTables( $method ) {
3630
		return true;
3631
	}
3632
3633
	/**
3634
	 * Delete a table
3635
	 * @param string $tableName
3636
	 * @param string $fName
3637
	 * @return bool|ResultWrapper
3638
	 * @since 1.18
3639
	 */
3640 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
3641
		if ( !$this->tableExists( $tableName, $fName ) ) {
3642
			return false;
3643
		}
3644
		$sql = "DROP TABLE " . $this->tableName( $tableName );
3645
		if ( $this->cascadingDeletes() ) {
3646
			$sql .= " CASCADE";
3647
		}
3648
3649
		return $this->query( $sql, $fName );
3650
	}
3651
3652
	/**
3653
	 * Get search engine class. All subclasses of this need to implement this
3654
	 * if they wish to use searching.
3655
	 *
3656
	 * @return string
3657
	 */
3658
	public function getSearchEngine() {
3659
		return 'SearchEngineDummy';
3660
	}
3661
3662
	public function getInfinity() {
3663
		return 'infinity';
3664
	}
3665
3666
	public function encodeExpiry( $expiry ) {
3667
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3668
			? $this->getInfinity()
3669
			: $this->timestamp( $expiry );
3670
	}
3671
3672
	public function decodeExpiry( $expiry, $format = TS_MW ) {
3673
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3674
			? 'infinity'
3675
			: wfTimestamp( $format, $expiry );
3676
	}
3677
3678
	public function setBigSelects( $value = true ) {
3679
		// no-op
3680
	}
3681
3682
	public function isReadOnly() {
3683
		return ( $this->getReadOnlyReason() !== false );
3684
	}
3685
3686
	/**
3687
	 * @return string|bool Reason this DB is read-only or false if it is not
3688
	 */
3689
	protected function getReadOnlyReason() {
3690
		$reason = $this->getLBInfo( 'readOnlyReason' );
3691
3692
		return is_string( $reason ) ? $reason : false;
3693
	}
3694
3695
	/**
3696
	 * @since 1.19
3697
	 * @return string
3698
	 */
3699
	public function __toString() {
3700
		return (string)$this->mConn;
3701
	}
3702
3703
	/**
3704
	 * Run a few simple sanity checks
3705
	 */
3706
	public function __destruct() {
3707
		if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3708
			trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3709
		}
3710
		$danglingCallbacks = array_merge(
3711
			$this->mTrxIdleCallbacks,
3712
			$this->mTrxPreCommitCallbacks,
3713
			$this->mTrxEndCallbacks
3714
		);
3715
		if ( $danglingCallbacks ) {
3716
			$callers = [];
3717
			foreach ( $danglingCallbacks as $callbackInfo ) {
3718
				$callers[] = $callbackInfo[1];
3719
			}
3720
			$callers = implode( ', ', $callers );
3721
			trigger_error( "DB transaction callbacks still pending (from $callers)." );
3722
		}
3723
	}
3724
}
3725
3726
/**
3727
 * @since 1.27
3728
 */
3729
abstract class Database extends DatabaseBase {
3730
	// B/C until nothing type hints for DatabaseBase
3731
	// @TODO: finish renaming DatabaseBase => Database
3732
}
3733