Completed
Branch master (8d5465)
by
unknown
31:25
created

DatabaseBase::tableNamesWithUseIndexOrJOIN()   C

Complexity

Conditions 10
Paths 68

Size

Total Lines 52
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 32
nc 68
nop 3
dl 0
loc 52
rs 6.2553
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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