Completed
Branch master (a9d73a)
by
unknown
30:07
created

DatabaseBase::doProfiledQuery()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 36
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 22
c 1
b 0
f 0
nc 12
nop 4
dl 0
loc 36
rs 8.439
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
43
	/** @var string SQL query */
44
	protected $mLastQuery = '';
45
	/** @var bool */
46
	protected $mDoneWrites = false;
47
	/** @var string|bool */
48
	protected $mPHPError = false;
49
	/** @var string */
50
	protected $mServer;
51
	/** @var string */
52
	protected $mUser;
53
	/** @var string */
54
	protected $mPassword;
55
	/** @var string */
56
	protected $mDBname;
57
58
	/** @var BagOStuff APC cache */
59
	protected $srvCache;
60
61
	/** @var resource Database connection */
62
	protected $mConn = null;
63
	/** @var bool */
64
	protected $mOpened = false;
65
66
	/** @var array[] List of (callable, method name) */
67
	protected $mTrxIdleCallbacks = [];
68
	/** @var array[] List of (callable, method name) */
69
	protected $mTrxPreCommitCallbacks = [];
70
	/** @var array[] List of (callable, method name) */
71
	protected $mTrxEndCallbacks = [];
72
	/** @var bool Whether to suppress triggering of post-commit callbacks */
73
	protected $suppressPostCommitCallbacks = false;
74
75
	/** @var string */
76
	protected $mTablePrefix;
77
	/** @var string */
78
	protected $mSchema;
79
	/** @var integer */
80
	protected $mFlags;
81
	/** @var bool */
82
	protected $mForeign;
83
	/** @var array */
84
	protected $mLBInfo = [];
85
	/** @var bool|null */
86
	protected $mDefaultBigSelects = null;
87
	/** @var array|bool */
88
	protected $mSchemaVars = false;
89
	/** @var array */
90
	protected $mSessionVars = [];
91
	/** @var array|null */
92
	protected $preparedArgs;
93
	/** @var string|bool|null Stashed value of html_errors INI setting */
94
	protected $htmlErrors;
95
	/** @var string */
96
	protected $delimiter = ';';
97
98
	/**
99
	 * Either 1 if a transaction is active or 0 otherwise.
100
	 * The other Trx fields may not be meaningfull if this is 0.
101
	 *
102
	 * @var int
103
	 */
104
	protected $mTrxLevel = 0;
105
106
	/**
107
	 * Either a short hexidecimal string if a transaction is active or ""
108
	 *
109
	 * @var string
110
	 * @see DatabaseBase::mTrxLevel
111
	 */
112
	protected $mTrxShortId = '';
113
114
	/**
115
	 * The UNIX time that the transaction started. Callers can assume that if
116
	 * snapshot isolation is used, then the data is *at least* up to date to that
117
	 * point (possibly more up-to-date since the first SELECT defines the snapshot).
118
	 *
119
	 * @var float|null
120
	 * @see DatabaseBase::mTrxLevel
121
	 */
122
	private $mTrxTimestamp = null;
123
124
	/** @var float Lag estimate at the time of BEGIN */
125
	private $mTrxSlaveLag = null;
126
127
	/**
128
	 * Remembers the function name given for starting the most recent transaction via begin().
129
	 * Used to provide additional context for error reporting.
130
	 *
131
	 * @var string
132
	 * @see DatabaseBase::mTrxLevel
133
	 */
134
	private $mTrxFname = null;
135
136
	/**
137
	 * Record if possible write queries were done in the last transaction started
138
	 *
139
	 * @var bool
140
	 * @see DatabaseBase::mTrxLevel
141
	 */
142
	private $mTrxDoneWrites = false;
143
144
	/**
145
	 * Record if the current transaction was started implicitly due to DBO_TRX being set.
146
	 *
147
	 * @var bool
148
	 * @see DatabaseBase::mTrxLevel
149
	 */
150
	private $mTrxAutomatic = false;
151
152
	/**
153
	 * Array of levels of atomicity within transactions
154
	 *
155
	 * @var array
156
	 */
157
	private $mTrxAtomicLevels = [];
158
159
	/**
160
	 * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
161
	 *
162
	 * @var bool
163
	 */
164
	private $mTrxAutomaticAtomic = false;
165
166
	/**
167
	 * Track the write query callers of the current transaction
168
	 *
169
	 * @var string[]
170
	 */
171
	private $mTrxWriteCallers = [];
172
173
	/**
174
	 * Track the seconds spent in write queries for the current transaction
175
	 *
176
	 * @var float
177
	 */
178
	private $mTrxWriteDuration = 0.0;
179
180
	/** @var array Map of (name => 1) for locks obtained via lock() */
181
	private $mNamedLocksHeld = [];
182
183
	/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
184
	private $lazyMasterHandle;
185
186
	/**
187
	 * @since 1.21
188
	 * @var resource File handle for upgrade
189
	 */
190
	protected $fileHandle = null;
191
192
	/**
193
	 * @since 1.22
194
	 * @var string[] Process cache of VIEWs names in the database
195
	 */
196
	protected $allViews = null;
197
198
	/** @var float UNIX timestamp */
199
	protected $lastPing = 0.0;
200
201
	/** @var int[] Prior mFlags values */
202
	private $priorFlags = [];
203
204
	/** @var Profiler */
205
	protected $profiler;
206
	/** @var TransactionProfiler */
207
	protected $trxProfiler;
208
209
	public function getServerInfo() {
210
		return $this->getServerVersion();
211
	}
212
213
	/**
214
	 * @return string Command delimiter used by this database engine
215
	 */
216
	public function getDelimiter() {
217
		return $this->delimiter;
218
	}
219
220
	/**
221
	 * Boolean, controls output of large amounts of debug information.
222
	 * @param bool|null $debug
223
	 *   - true to enable debugging
224
	 *   - false to disable debugging
225
	 *   - omitted or null to do nothing
226
	 *
227
	 * @return bool|null Previous value of the flag
228
	 */
229
	public function debug( $debug = null ) {
230
		return wfSetBit( $this->mFlags, DBO_DEBUG, $debug );
0 ignored issues
show
Bug introduced by
It seems like $debug defined by parameter $debug on line 229 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...
231
	}
232
233
	public function bufferResults( $buffer = null ) {
234
		if ( is_null( $buffer ) ) {
235
			return !(bool)( $this->mFlags & DBO_NOBUFFER );
236
		} else {
237
			return !wfSetBit( $this->mFlags, DBO_NOBUFFER, !$buffer );
238
		}
239
	}
240
241
	/**
242
	 * Turns on (false) or off (true) the automatic generation and sending
243
	 * of a "we're sorry, but there has been a database error" page on
244
	 * database errors. Default is on (false). When turned off, the
245
	 * code should use lastErrno() and lastError() to handle the
246
	 * situation as appropriate.
247
	 *
248
	 * Do not use this function outside of the Database classes.
249
	 *
250
	 * @param null|bool $ignoreErrors
251
	 * @return bool The previous value of the flag.
252
	 */
253
	protected function ignoreErrors( $ignoreErrors = null ) {
254
		return wfSetBit( $this->mFlags, DBO_IGNORE, $ignoreErrors );
0 ignored issues
show
Bug introduced by
It seems like $ignoreErrors defined by parameter $ignoreErrors on line 253 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...
255
	}
256
257
	public function trxLevel() {
258
		return $this->mTrxLevel;
259
	}
260
261
	public function trxTimestamp() {
262
		return $this->mTrxLevel ? $this->mTrxTimestamp : null;
263
	}
264
265
	public function tablePrefix( $prefix = null ) {
266
		return wfSetVar( $this->mTablePrefix, $prefix );
267
	}
268
269
	public function dbSchema( $schema = null ) {
270
		return wfSetVar( $this->mSchema, $schema );
271
	}
272
273
	/**
274
	 * Set the filehandle to copy write statements to.
275
	 *
276
	 * @param resource $fh File handle
277
	 */
278
	public function setFileHandle( $fh ) {
279
		$this->fileHandle = $fh;
280
	}
281
282
	public function getLBInfo( $name = null ) {
283
		if ( is_null( $name ) ) {
284
			return $this->mLBInfo;
285
		} else {
286
			if ( array_key_exists( $name, $this->mLBInfo ) ) {
287
				return $this->mLBInfo[$name];
288
			} else {
289
				return null;
290
			}
291
		}
292
	}
293
294
	public function setLBInfo( $name, $value = null ) {
295
		if ( is_null( $value ) ) {
296
			$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...
297
		} else {
298
			$this->mLBInfo[$name] = $value;
299
		}
300
	}
301
302
	/**
303
	 * Set a lazy-connecting DB handle to the master DB (for replication status purposes)
304
	 *
305
	 * @param IDatabase $conn
306
	 * @since 1.27
307
	 */
308
	public function setLazyMasterHandle( IDatabase $conn ) {
309
		$this->lazyMasterHandle = $conn;
310
	}
311
312
	/**
313
	 * @return IDatabase|null
314
	 * @see setLazyMasterHandle()
315
	 * @since 1.27
316
	 */
317
	public function getLazyMasterHandle() {
318
		return $this->lazyMasterHandle;
319
	}
320
321
	/**
322
	 * @return TransactionProfiler
323
	 */
324
	protected function getTransactionProfiler() {
325
		return $this->trxProfiler;
326
	}
327
328
	/**
329
	 * @param TransactionProfiler $profiler
330
	 * @since 1.27
331
	 */
332
	public function setTransactionProfiler( TransactionProfiler $profiler ) {
333
		$this->trxProfiler = $profiler;
334
	}
335
336
	/**
337
	 * Returns true if this database supports (and uses) cascading deletes
338
	 *
339
	 * @return bool
340
	 */
341
	public function cascadingDeletes() {
342
		return false;
343
	}
344
345
	/**
346
	 * Returns true if this database supports (and uses) triggers (e.g. on the page table)
347
	 *
348
	 * @return bool
349
	 */
350
	public function cleanupTriggers() {
351
		return false;
352
	}
353
354
	/**
355
	 * Returns true if this database is strict about what can be put into an IP field.
356
	 * Specifically, it uses a NULL value instead of an empty string.
357
	 *
358
	 * @return bool
359
	 */
360
	public function strictIPs() {
361
		return false;
362
	}
363
364
	/**
365
	 * Returns true if this database uses timestamps rather than integers
366
	 *
367
	 * @return bool
368
	 */
369
	public function realTimestamps() {
370
		return false;
371
	}
372
373
	public function implicitGroupby() {
374
		return true;
375
	}
376
377
	public function implicitOrderby() {
378
		return true;
379
	}
380
381
	/**
382
	 * Returns true if this database can do a native search on IP columns
383
	 * e.g. this works as expected: .. WHERE rc_ip = '127.42.12.102/32';
384
	 *
385
	 * @return bool
386
	 */
387
	public function searchableIPs() {
388
		return false;
389
	}
390
391
	/**
392
	 * Returns true if this database can use functional indexes
393
	 *
394
	 * @return bool
395
	 */
396
	public function functionalIndexes() {
397
		return false;
398
	}
399
400
	public function lastQuery() {
401
		return $this->mLastQuery;
402
	}
403
404
	public function doneWrites() {
405
		return (bool)$this->mDoneWrites;
406
	}
407
408
	public function lastDoneWrites() {
409
		return $this->mDoneWrites ?: false;
410
	}
411
412
	public function writesPending() {
413
		return $this->mTrxLevel && $this->mTrxDoneWrites;
414
	}
415
416
	public function writesOrCallbacksPending() {
417
		return $this->mTrxLevel && (
418
			$this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
419
		);
420
	}
421
422
	public function pendingWriteQueryDuration() {
423
		return $this->mTrxLevel ? $this->mTrxWriteDuration : false;
424
	}
425
426
	public function pendingWriteCallers() {
427
		return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
428
	}
429
430
	public function isOpen() {
431
		return $this->mOpened;
432
	}
433
434
	public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
435
		if ( $remember === self::REMEMBER_PRIOR ) {
436
			array_push( $this->priorFlags, $this->mFlags );
437
		}
438
		$this->mFlags |= $flag;
439
	}
440
441
	public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
442
		if ( $remember === self::REMEMBER_PRIOR ) {
443
			array_push( $this->priorFlags, $this->mFlags );
444
		}
445
		$this->mFlags &= ~$flag;
446
	}
447
448
	public function restoreFlags( $state = self::RESTORE_PRIOR ) {
449
		if ( !$this->priorFlags ) {
450
			return;
451
		}
452
453
		if ( $state === self::RESTORE_INITIAL ) {
454
			$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...
455
			$this->priorFlags = [];
456
		} else {
457
			$this->mFlags = array_pop( $this->priorFlags );
458
		}
459
	}
460
461
	public function getFlag( $flag ) {
462
		return !!( $this->mFlags & $flag );
463
	}
464
465
	public function getProperty( $name ) {
466
		return $this->$name;
467
	}
468
469
	public function getWikiID() {
470
		if ( $this->mTablePrefix ) {
471
			return "{$this->mDBname}-{$this->mTablePrefix}";
472
		} else {
473
			return $this->mDBname;
474
		}
475
	}
476
477
	/**
478
	 * Return a path to the DBMS-specific SQL file if it exists,
479
	 * otherwise default SQL file
480
	 *
481
	 * @param string $filename
482
	 * @return string
483
	 */
484 View Code Duplication
	private function getSqlFilePath( $filename ) {
485
		global $IP;
486
		$dbmsSpecificFilePath = "$IP/maintenance/" . $this->getType() . "/$filename";
487
		if ( file_exists( $dbmsSpecificFilePath ) ) {
488
			return $dbmsSpecificFilePath;
489
		} else {
490
			return "$IP/maintenance/$filename";
491
		}
492
	}
493
494
	/**
495
	 * Return a path to the DBMS-specific schema file,
496
	 * otherwise default to tables.sql
497
	 *
498
	 * @return string
499
	 */
500
	public function getSchemaPath() {
501
		return $this->getSqlFilePath( 'tables.sql' );
502
	}
503
504
	/**
505
	 * Return a path to the DBMS-specific update key file,
506
	 * otherwise default to update-keys.sql
507
	 *
508
	 * @return string
509
	 */
510
	public function getUpdateKeysPath() {
511
		return $this->getSqlFilePath( 'update-keys.sql' );
512
	}
513
514
	/**
515
	 * Get information about an index into an object
516
	 * @param string $table Table name
517
	 * @param string $index Index name
518
	 * @param string $fname Calling function name
519
	 * @return mixed Database-specific index description class or false if the index does not exist
520
	 */
521
	abstract function indexInfo( $table, $index, $fname = __METHOD__ );
522
523
	/**
524
	 * Wrapper for addslashes()
525
	 *
526
	 * @param string $s String to be slashed.
527
	 * @return string Slashed string.
528
	 */
529
	abstract function strencode( $s );
530
531
	/**
532
	 * Constructor.
533
	 *
534
	 * FIXME: It is possible to construct a Database object with no associated
535
	 * connection object, by specifying no parameters to __construct(). This
536
	 * feature is deprecated and should be removed.
537
	 *
538
	 * DatabaseBase subclasses should not be constructed directly in external
539
	 * code. DatabaseBase::factory() should be used instead.
540
	 *
541
	 * @param array $params Parameters passed from DatabaseBase::factory()
542
	 */
543
	function __construct( array $params ) {
544
		global $wgDBprefix, $wgDBmwschema, $wgCommandLineMode;
545
546
		$this->srvCache = ObjectCache::getLocalServerInstance( 'hash' );
547
548
		$server = $params['host'];
549
		$user = $params['user'];
550
		$password = $params['password'];
551
		$dbName = $params['dbname'];
552
		$flags = $params['flags'];
553
		$tablePrefix = $params['tablePrefix'];
554
		$schema = $params['schema'];
555
		$foreign = $params['foreign'];
556
557
		$this->mFlags = $flags;
558
		if ( $this->mFlags & DBO_DEFAULT ) {
559
			if ( $wgCommandLineMode ) {
560
				$this->mFlags &= ~DBO_TRX;
561
			} else {
562
				$this->mFlags |= DBO_TRX;
563
			}
564
		}
565
566
		$this->mSessionVars = $params['variables'];
567
568
		/** Get the default table prefix*/
569
		if ( $tablePrefix === 'get from global' ) {
570
			$this->mTablePrefix = $wgDBprefix;
571
		} else {
572
			$this->mTablePrefix = $tablePrefix;
573
		}
574
575
		/** Get the database schema*/
576
		if ( $schema === 'get from global' ) {
577
			$this->mSchema = $wgDBmwschema;
578
		} else {
579
			$this->mSchema = $schema;
580
		}
581
582
		$this->mForeign = $foreign;
583
584
		$this->profiler = isset( $params['profiler'] )
585
			? $params['profiler']
586
			: Profiler::instance(); // @TODO: remove global state
587
		$this->trxProfiler = isset( $params['trxProfiler'] )
588
			? $params['trxProfiler']
589
			: new TransactionProfiler();
590
591
		if ( $user ) {
592
			$this->open( $server, $user, $password, $dbName );
593
		}
594
595
	}
596
597
	/**
598
	 * Called by serialize. Throw an exception when DB connection is serialized.
599
	 * This causes problems on some database engines because the connection is
600
	 * not restored on unserialize.
601
	 */
602
	public function __sleep() {
603
		throw new MWException( 'Database serialization may cause problems, since ' .
604
			'the connection is not restored on wakeup.' );
605
	}
606
607
	/**
608
	 * Given a DB type, construct the name of the appropriate child class of
609
	 * DatabaseBase. This is designed to replace all of the manual stuff like:
610
	 *    $class = 'Database' . ucfirst( strtolower( $dbType ) );
611
	 * as well as validate against the canonical list of DB types we have
612
	 *
613
	 * This factory function is mostly useful for when you need to connect to a
614
	 * database other than the MediaWiki default (such as for external auth,
615
	 * an extension, et cetera). Do not use this to connect to the MediaWiki
616
	 * database. Example uses in core:
617
	 * @see LoadBalancer::reallyOpenConnection()
618
	 * @see ForeignDBRepo::getMasterDB()
619
	 * @see WebInstallerDBConnect::execute()
620
	 *
621
	 * @since 1.18
622
	 *
623
	 * @param string $dbType A possible DB type
624
	 * @param array $p An array of options to pass to the constructor.
625
	 *    Valid options are: host, user, password, dbname, flags, tablePrefix, schema, driver
626
	 * @throws MWException If the database driver or extension cannot be found
627
	 * @return DatabaseBase|null DatabaseBase subclass or null
628
	 */
629
	final public static function factory( $dbType, $p = [] ) {
630
		$canonicalDBTypes = [
631
			'mysql' => [ 'mysqli', 'mysql' ],
632
			'postgres' => [],
633
			'sqlite' => [],
634
			'oracle' => [],
635
			'mssql' => [],
636
		];
637
638
		$driver = false;
639
		$dbType = strtolower( $dbType );
640
		if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
641
			$possibleDrivers = $canonicalDBTypes[$dbType];
642
			if ( !empty( $p['driver'] ) ) {
643
				if ( in_array( $p['driver'], $possibleDrivers ) ) {
644
					$driver = $p['driver'];
645
				} else {
646
					throw new MWException( __METHOD__ .
647
						" cannot construct Database with type '$dbType' and driver '{$p['driver']}'" );
648
				}
649
			} else {
650
				foreach ( $possibleDrivers as $posDriver ) {
651
					if ( extension_loaded( $posDriver ) ) {
652
						$driver = $posDriver;
653
						break;
654
					}
655
				}
656
			}
657
		} else {
658
			$driver = $dbType;
659
		}
660
		if ( $driver === false ) {
661
			throw new MWException( __METHOD__ .
662
				" no viable database extension found for type '$dbType'" );
663
		}
664
665
		// Determine schema defaults. Currently Microsoft SQL Server uses $wgDBmwschema,
666
		// and everything else doesn't use a schema (e.g. null)
667
		// Although postgres and oracle support schemas, we don't use them (yet)
668
		// to maintain backwards compatibility
669
		$defaultSchemas = [
670
			'mssql' => 'get from global',
671
		];
672
673
		$class = 'Database' . ucfirst( $driver );
674
		if ( class_exists( $class ) && is_subclass_of( $class, 'DatabaseBase' ) ) {
675
			// Resolve some defaults for b/c
676
			$p['host'] = isset( $p['host'] ) ? $p['host'] : false;
677
			$p['user'] = isset( $p['user'] ) ? $p['user'] : false;
678
			$p['password'] = isset( $p['password'] ) ? $p['password'] : false;
679
			$p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
680
			$p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
681
			$p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
682
			$p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : 'get from global';
683
			if ( !isset( $p['schema'] ) ) {
684
				$p['schema'] = isset( $defaultSchemas[$dbType] ) ? $defaultSchemas[$dbType] : null;
685
			}
686
			$p['foreign'] = isset( $p['foreign'] ) ? $p['foreign'] : false;
687
688
			return new $class( $p );
689
		} else {
690
			return null;
691
		}
692
	}
693
694
	protected function installErrorHandler() {
695
		$this->mPHPError = false;
696
		$this->htmlErrors = ini_set( 'html_errors', '0' );
697
		set_error_handler( [ $this, 'connectionErrorHandler' ] );
698
	}
699
700
	/**
701
	 * @return bool|string
702
	 */
703
	protected function restoreErrorHandler() {
704
		restore_error_handler();
705
		if ( $this->htmlErrors !== false ) {
706
			ini_set( 'html_errors', $this->htmlErrors );
707
		}
708
		if ( $this->mPHPError ) {
709
			$error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
710
			$error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
711
712
			return $error;
713
		} else {
714
			return false;
715
		}
716
	}
717
718
	/**
719
	 * @param int $errno
720
	 * @param string $errstr
721
	 */
722
	public function connectionErrorHandler( $errno, $errstr ) {
723
		$this->mPHPError = $errstr;
724
	}
725
726
	/**
727
	 * Create a log context to pass to wfLogDBError or other logging functions.
728
	 *
729
	 * @param array $extras Additional data to add to context
730
	 * @return array
731
	 */
732
	protected function getLogContext( array $extras = [] ) {
733
		return array_merge(
734
			[
735
				'db_server' => $this->mServer,
736
				'db_name' => $this->mDBname,
737
				'db_user' => $this->mUser,
738
			],
739
			$extras
740
		);
741
	}
742
743
	public function close() {
744
		if ( $this->mConn ) {
745
			if ( $this->trxLevel() ) {
746
				if ( !$this->mTrxAutomatic ) {
747
					wfWarn( "Transaction still in progress (from {$this->mTrxFname}), " .
748
						" performing implicit commit before closing connection!" );
749
				}
750
751
				$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
752
			}
753
754
			$closed = $this->closeConnection();
755
			$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...
756
		} elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
757
			throw new MWException( "Transaction callbacks still pending." );
758
		} else {
759
			$closed = true;
760
		}
761
		$this->mOpened = false;
762
763
		return $closed;
764
	}
765
766
	/**
767
	 * Make sure isOpen() returns true as a sanity check
768
	 *
769
	 * @throws DBUnexpectedError
770
	 */
771
	protected function assertOpen() {
772
		if ( !$this->isOpen() ) {
773
			throw new DBUnexpectedError( $this, "DB connection was already closed." );
774
		}
775
	}
776
777
	/**
778
	 * Closes underlying database connection
779
	 * @since 1.20
780
	 * @return bool Whether connection was closed successfully
781
	 */
782
	abstract protected function closeConnection();
783
784
	function reportConnectionError( $error = 'Unknown error' ) {
785
		$myError = $this->lastError();
786
		if ( $myError ) {
787
			$error = $myError;
788
		}
789
790
		# New method
791
		throw new DBConnectionError( $this, $error );
792
	}
793
794
	/**
795
	 * The DBMS-dependent part of query()
796
	 *
797
	 * @param string $sql SQL query.
798
	 * @return ResultWrapper|bool Result object to feed to fetchObject,
799
	 *   fetchRow, ...; or false on failure
800
	 */
801
	abstract protected function doQuery( $sql );
802
803
	/**
804
	 * Determine whether a query writes to the DB.
805
	 * Should return true if unsure.
806
	 *
807
	 * @param string $sql
808
	 * @return bool
809
	 */
810
	protected function isWriteQuery( $sql ) {
811
		return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
812
	}
813
814
	/**
815
	 * Determine whether a SQL statement is sensitive to isolation level.
816
	 * A SQL statement is considered transactable if its result could vary
817
	 * depending on the transaction isolation level. Operational commands
818
	 * such as 'SET' and 'SHOW' are not considered to be transactable.
819
	 *
820
	 * @param string $sql
821
	 * @return bool
822
	 */
823
	protected function isTransactableQuery( $sql ) {
824
		$verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) );
825
		return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ] );
826
	}
827
828
	public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
829
		global $wgUser;
830
831
		$priorWritesPending = $this->writesOrCallbacksPending();
832
		$this->mLastQuery = $sql;
833
834
		$isWrite = $this->isWriteQuery( $sql );
835
		if ( $isWrite ) {
836
			$reason = $this->getReadOnlyReason();
837
			if ( $reason !== false ) {
838
				throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
839
			}
840
			# Set a flag indicating that writes have been done
841
			$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...
842
		}
843
844
		# Add a comment for easy SHOW PROCESSLIST interpretation
845
		if ( is_object( $wgUser ) && $wgUser->isItemLoaded( 'name' ) ) {
846
			$userName = $wgUser->getName();
847
			if ( mb_strlen( $userName ) > 15 ) {
848
				$userName = mb_substr( $userName, 0, 15 ) . '...';
849
			}
850
			$userName = str_replace( '/', '', $userName );
851
		} else {
852
			$userName = '';
853
		}
854
855
		// Add trace comment to the begin of the sql string, right after the operator.
856
		// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
857
		$commentedSql = preg_replace( '/\s|$/', " /* $fname $userName */ ", $sql, 1 );
858
859
		# Start implicit transactions that wrap the request if DBO_TRX is enabled
860
		if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
861
			&& $this->isTransactableQuery( $sql )
862
		) {
863
			$this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
864
			$this->mTrxAutomatic = true;
865
		}
866
867
		# Keep track of whether the transaction has write queries pending
868
		if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
869
			$this->mTrxDoneWrites = true;
870
			$this->getTransactionProfiler()->transactionWritingIn(
871
				$this->mServer, $this->mDBname, $this->mTrxShortId );
872
		}
873
874
		if ( $this->debug() ) {
875
			wfDebugLog( 'queries', sprintf( "%s: %s", $this->mDBname, $commentedSql ) );
876
		}
877
878
		# Avoid fatals if close() was called
879
		$this->assertOpen();
880
881
		# Send the query to the server
882
		$ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
883
884
		# Try reconnecting if the connection was lost
885
		if ( false === $ret && $this->wasErrorReissuable() ) {
886
			$recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
887
			# Stash the last error values before anything might clear them
888
			$lastError = $this->lastError();
889
			$lastErrno = $this->lastErrno();
890
			# Update state tracking to reflect transaction loss due to disconnection
891
			$this->handleTransactionLoss();
892
			wfDebug( "Connection lost, reconnecting...\n" );
893
			if ( $this->reconnect() ) {
894
				wfDebug( "Reconnected\n" );
895
				$msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
896
				wfDebugLog( 'DBPerformance', "$msg:\n" . wfBacktrace( true ) );
897
898
				if ( !$recoverable ) {
899
					# Callers may catch the exception and continue to use the DB
900
					$this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
901
				} else {
902
					# Should be safe to silently retry the query
903
					$ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
904
				}
905
			} else {
906
				wfDebug( "Failed\n" );
907
			}
908
		}
909
910
		if ( false === $ret ) {
911
			# Deadlocks cause the entire transaction to abort, not just the statement.
912
			# http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
913
			# https://www.postgresql.org/docs/9.1/static/explicit-locking.html
914
			if ( $this->wasDeadlock() ) {
915
				if ( $this->explicitTrxActive() || $priorWritesPending ) {
916
					$tempIgnore = false; // not recoverable
917
				}
918
				# Update state tracking to reflect transaction loss
919
				$this->handleTransactionLoss();
920
			}
921
922
			$this->reportQueryError(
923
				$this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
924
		}
925
926
		$res = $this->resultObject( $ret );
927
928
		return $res;
929
	}
930
931
	private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
932
		$isMaster = !is_null( $this->getLBInfo( 'master' ) );
933
		# generalizeSQL() will probably cut down the query to reasonable
934
		# logging size most of the time. The substr is really just a sanity check.
935
		if ( $isMaster ) {
936
			$queryProf = 'query-m: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
937
		} else {
938
			$queryProf = 'query: ' . substr( DatabaseBase::generalizeSQL( $sql ), 0, 255 );
939
		}
940
941
		# Include query transaction state
942
		$queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
943
944
		$startTime = microtime( true );
945
		$this->profiler->profileIn( $queryProf );
946
		$ret = $this->doQuery( $commentedSql );
947
		$this->profiler->profileOut( $queryProf );
948
		$queryRuntime = microtime( true ) - $startTime;
949
950
		unset( $queryProfSection ); // profile out (if set)
951
952
		if ( $ret !== false ) {
953
			$this->lastPing = $startTime;
954
			if ( $isWrite && $this->mTrxLevel ) {
955
				$this->mTrxWriteDuration += $queryRuntime;
956
				$this->mTrxWriteCallers[] = $fname;
957
			}
958
		}
959
960
		$this->getTransactionProfiler()->recordQueryCompletion(
961
			$queryProf, $startTime, $isWrite, $this->affectedRows()
962
		);
963
		MWDebug::query( $sql, $fname, $isMaster, $queryRuntime );
964
965
		return $ret;
966
	}
967
968
	private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
969
		# Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
970
		# Dropped connections also mean that named locks are automatically released.
971
		# Only allow error suppression in autocommit mode or when the lost transaction
972
		# didn't matter anyway (aside from DBO_TRX snapshot loss).
973
		if ( $this->mNamedLocksHeld ) {
974
			return false; // possible critical section violation
975
		} elseif ( $sql === 'COMMIT' ) {
976
			return !$priorWritesPending; // nothing written anyway? (T127428)
977
		} elseif ( $sql === 'ROLLBACK' ) {
978
			return true; // transaction lost...which is also what was requested :)
979
		} elseif ( $this->explicitTrxActive() ) {
980
			return false; // don't drop atomocity
981
		} elseif ( $priorWritesPending ) {
982
			return false; // prior writes lost from implicit transaction
983
		}
984
985
		return true;
986
	}
987
988
	private function handleTransactionLoss() {
989
		$this->mTrxLevel = 0;
990
		$this->mTrxIdleCallbacks = []; // bug 65263
991
		$this->mTrxPreCommitCallbacks = []; // bug 65263
992
		try {
993
			// Handle callbacks in mTrxEndCallbacks
994
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
995
			return null;
996
		} catch ( Exception $e ) {
997
			// Already logged; move on...
998
			return $e;
999
		}
1000
	}
1001
1002
	public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1003
		if ( $this->ignoreErrors() || $tempIgnore ) {
1004
			wfDebug( "SQL ERROR (ignored): $error\n" );
1005
		} else {
1006
			$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1007
			wfLogDBError(
1008
				"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1009
				$this->getLogContext( [
1010
					'method' => __METHOD__,
1011
					'errno' => $errno,
1012
					'error' => $error,
1013
					'sql1line' => $sql1line,
1014
					'fname' => $fname,
1015
				] )
1016
			);
1017
			wfDebug( "SQL ERROR: " . $error . "\n" );
1018
			throw new DBQueryError( $this, $error, $errno, $sql, $fname );
1019
		}
1020
	}
1021
1022
	/**
1023
	 * Intended to be compatible with the PEAR::DB wrapper functions.
1024
	 * http://pear.php.net/manual/en/package.database.db.intro-execute.php
1025
	 *
1026
	 * ? = scalar value, quoted as necessary
1027
	 * ! = raw SQL bit (a function for instance)
1028
	 * & = filename; reads the file and inserts as a blob
1029
	 *     (we don't use this though...)
1030
	 *
1031
	 * @param string $sql
1032
	 * @param string $func
1033
	 *
1034
	 * @return array
1035
	 */
1036
	protected function prepare( $sql, $func = 'DatabaseBase::prepare' ) {
1037
		/* MySQL doesn't support prepared statements (yet), so just
1038
		 * pack up the query for reference. We'll manually replace
1039
		 * the bits later.
1040
		 */
1041
		return [ 'query' => $sql, 'func' => $func ];
1042
	}
1043
1044
	/**
1045
	 * Free a prepared query, generated by prepare().
1046
	 * @param string $prepared
1047
	 */
1048
	protected function freePrepared( $prepared ) {
1049
		/* No-op by default */
1050
	}
1051
1052
	/**
1053
	 * Execute a prepared query with the various arguments
1054
	 * @param string $prepared The prepared sql
1055
	 * @param mixed $args Either an array here, or put scalars as varargs
1056
	 *
1057
	 * @return ResultWrapper
1058
	 */
1059
	public function execute( $prepared, $args = null ) {
1060
		if ( !is_array( $args ) ) {
1061
			# Pull the var args
1062
			$args = func_get_args();
1063
			array_shift( $args );
1064
		}
1065
1066
		$sql = $this->fillPrepared( $prepared['query'], $args );
1067
1068
		return $this->query( $sql, $prepared['func'] );
1069
	}
1070
1071
	/**
1072
	 * For faking prepared SQL statements on DBs that don't support it directly.
1073
	 *
1074
	 * @param string $preparedQuery A 'preparable' SQL statement
1075
	 * @param array $args Array of Arguments to fill it with
1076
	 * @return string Executable SQL
1077
	 */
1078
	public function fillPrepared( $preparedQuery, $args ) {
1079
		reset( $args );
1080
		$this->preparedArgs =& $args;
1081
1082
		return preg_replace_callback( '/(\\\\[?!&]|[?!&])/',
1083
			[ &$this, 'fillPreparedArg' ], $preparedQuery );
1084
	}
1085
1086
	/**
1087
	 * preg_callback func for fillPrepared()
1088
	 * The arguments should be in $this->preparedArgs and must not be touched
1089
	 * while we're doing this.
1090
	 *
1091
	 * @param array $matches
1092
	 * @throws DBUnexpectedError
1093
	 * @return string
1094
	 */
1095
	protected function fillPreparedArg( $matches ) {
1096
		switch ( $matches[1] ) {
1097
			case '\\?':
1098
				return '?';
1099
			case '\\!':
1100
				return '!';
1101
			case '\\&':
1102
				return '&';
1103
		}
1104
1105
		list( /* $n */, $arg ) = each( $this->preparedArgs );
1106
1107
		switch ( $matches[1] ) {
1108
			case '?':
1109
				return $this->addQuotes( $arg );
1110
			case '!':
1111
				return $arg;
1112
			case '&':
1113
				# return $this->addQuotes( file_get_contents( $arg ) );
1114
				throw new DBUnexpectedError(
1115
					$this,
1116
					'& mode is not implemented. If it\'s really needed, uncomment the line above.'
1117
				);
1118
			default:
1119
				throw new DBUnexpectedError(
1120
					$this,
1121
					'Received invalid match. This should never happen!'
1122
				);
1123
		}
1124
	}
1125
1126
	public function freeResult( $res ) {
1127
	}
1128
1129
	public function selectField(
1130
		$table, $var, $cond = '', $fname = __METHOD__, $options = []
1131
	) {
1132
		if ( $var === '*' ) { // sanity
1133
			throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1134
		}
1135
1136
		if ( !is_array( $options ) ) {
1137
			$options = [ $options ];
1138
		}
1139
1140
		$options['LIMIT'] = 1;
1141
1142
		$res = $this->select( $table, $var, $cond, $fname, $options );
1143
		if ( $res === false || !$this->numRows( $res ) ) {
1144
			return false;
1145
		}
1146
1147
		$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 1142 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...
1148
1149
		if ( $row !== false ) {
1150
			return reset( $row );
1151
		} else {
1152
			return false;
1153
		}
1154
	}
1155
1156
	public function selectFieldValues(
1157
		$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1158
	) {
1159
		if ( $var === '*' ) { // sanity
1160
			throw new DBUnexpectedError( $this, "Cannot use a * field" );
1161
		} elseif ( !is_string( $var ) ) { // sanity
1162
			throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1163
		}
1164
1165
		if ( !is_array( $options ) ) {
1166
			$options = [ $options ];
1167
		}
1168
1169
		$res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1170
		if ( $res === false ) {
1171
			return false;
1172
		}
1173
1174
		$values = [];
1175
		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...
1176
			$values[] = $row->$var;
1177
		}
1178
1179
		return $values;
1180
	}
1181
1182
	/**
1183
	 * Returns an optional USE INDEX clause to go after the table, and a
1184
	 * string to go at the end of the query.
1185
	 *
1186
	 * @param array $options Associative array of options to be turned into
1187
	 *   an SQL query, valid keys are listed in the function.
1188
	 * @return array
1189
	 * @see DatabaseBase::select()
1190
	 */
1191
	public function makeSelectOptions( $options ) {
1192
		$preLimitTail = $postLimitTail = '';
1193
		$startOpts = '';
1194
1195
		$noKeyOptions = [];
1196
1197
		foreach ( $options as $key => $option ) {
1198
			if ( is_numeric( $key ) ) {
1199
				$noKeyOptions[$option] = true;
1200
			}
1201
		}
1202
1203
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1204
1205
		$preLimitTail .= $this->makeOrderBy( $options );
1206
1207
		// if (isset($options['LIMIT'])) {
1208
		// 	$tailOpts .= $this->limitResult('', $options['LIMIT'],
1209
		// 		isset($options['OFFSET']) ? $options['OFFSET']
1210
		// 		: false);
1211
		// }
1212
1213
		if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1214
			$postLimitTail .= ' FOR UPDATE';
1215
		}
1216
1217
		if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1218
			$postLimitTail .= ' LOCK IN SHARE MODE';
1219
		}
1220
1221
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1222
			$startOpts .= 'DISTINCT';
1223
		}
1224
1225
		# Various MySQL extensions
1226
		if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1227
			$startOpts .= ' /*! STRAIGHT_JOIN */';
1228
		}
1229
1230
		if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1231
			$startOpts .= ' HIGH_PRIORITY';
1232
		}
1233
1234
		if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1235
			$startOpts .= ' SQL_BIG_RESULT';
1236
		}
1237
1238
		if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1239
			$startOpts .= ' SQL_BUFFER_RESULT';
1240
		}
1241
1242
		if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1243
			$startOpts .= ' SQL_SMALL_RESULT';
1244
		}
1245
1246
		if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1247
			$startOpts .= ' SQL_CALC_FOUND_ROWS';
1248
		}
1249
1250
		if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1251
			$startOpts .= ' SQL_CACHE';
1252
		}
1253
1254
		if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1255
			$startOpts .= ' SQL_NO_CACHE';
1256
		}
1257
1258 View Code Duplication
		if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1259
			$useIndex = $this->useIndexClause( $options['USE INDEX'] );
1260
		} else {
1261
			$useIndex = '';
1262
		}
1263
1264
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail ];
1265
	}
1266
1267
	/**
1268
	 * Returns an optional GROUP BY with an optional HAVING
1269
	 *
1270
	 * @param array $options Associative array of options
1271
	 * @return string
1272
	 * @see DatabaseBase::select()
1273
	 * @since 1.21
1274
	 */
1275
	public function makeGroupByWithHaving( $options ) {
1276
		$sql = '';
1277 View Code Duplication
		if ( isset( $options['GROUP BY'] ) ) {
1278
			$gb = is_array( $options['GROUP BY'] )
1279
				? implode( ',', $options['GROUP BY'] )
1280
				: $options['GROUP BY'];
1281
			$sql .= ' GROUP BY ' . $gb;
1282
		}
1283 View Code Duplication
		if ( isset( $options['HAVING'] ) ) {
1284
			$having = is_array( $options['HAVING'] )
1285
				? $this->makeList( $options['HAVING'], LIST_AND )
1286
				: $options['HAVING'];
1287
			$sql .= ' HAVING ' . $having;
1288
		}
1289
1290
		return $sql;
1291
	}
1292
1293
	/**
1294
	 * Returns an optional ORDER BY
1295
	 *
1296
	 * @param array $options Associative array of options
1297
	 * @return string
1298
	 * @see DatabaseBase::select()
1299
	 * @since 1.21
1300
	 */
1301
	public function makeOrderBy( $options ) {
1302 View Code Duplication
		if ( isset( $options['ORDER BY'] ) ) {
1303
			$ob = is_array( $options['ORDER BY'] )
1304
				? implode( ',', $options['ORDER BY'] )
1305
				: $options['ORDER BY'];
1306
1307
			return ' ORDER BY ' . $ob;
1308
		}
1309
1310
		return '';
1311
	}
1312
1313
	// See IDatabase::select for the docs for this function
1314
	public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1315
		$options = [], $join_conds = [] ) {
1316
		$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1317
1318
		return $this->query( $sql, $fname );
1319
	}
1320
1321
	public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1322
		$options = [], $join_conds = []
1323
	) {
1324
		if ( is_array( $vars ) ) {
1325
			$vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1326
		}
1327
1328
		$options = (array)$options;
1329
		$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1330
			? $options['USE INDEX']
1331
			: [];
1332
1333
		if ( is_array( $table ) ) {
1334
			$from = ' FROM ' .
1335
				$this->tableNamesWithUseIndexOrJOIN( $table, $useIndexes, $join_conds );
1336
		} elseif ( $table != '' ) {
1337
			if ( $table[0] == ' ' ) {
1338
				$from = ' FROM ' . $table;
1339
			} else {
1340
				$from = ' FROM ' .
1341
					$this->tableNamesWithUseIndexOrJOIN( [ $table ], $useIndexes, [] );
1342
			}
1343
		} else {
1344
			$from = '';
1345
		}
1346
1347
		list( $startOpts, $useIndex, $preLimitTail, $postLimitTail ) =
1348
			$this->makeSelectOptions( $options );
1349
1350
		if ( !empty( $conds ) ) {
1351
			if ( is_array( $conds ) ) {
1352
				$conds = $this->makeList( $conds, LIST_AND );
1353
			}
1354
			$sql = "SELECT $startOpts $vars $from $useIndex WHERE $conds $preLimitTail";
1355
		} else {
1356
			$sql = "SELECT $startOpts $vars $from $useIndex $preLimitTail";
1357
		}
1358
1359
		if ( isset( $options['LIMIT'] ) ) {
1360
			$sql = $this->limitResult( $sql, $options['LIMIT'],
1361
				isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1362
		}
1363
		$sql = "$sql $postLimitTail";
1364
1365
		if ( isset( $options['EXPLAIN'] ) ) {
1366
			$sql = 'EXPLAIN ' . $sql;
1367
		}
1368
1369
		return $sql;
1370
	}
1371
1372
	public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1373
		$options = [], $join_conds = []
1374
	) {
1375
		$options = (array)$options;
1376
		$options['LIMIT'] = 1;
1377
		$res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1378
1379
		if ( $res === false ) {
1380
			return false;
1381
		}
1382
1383
		if ( !$this->numRows( $res ) ) {
1384
			return false;
1385
		}
1386
1387
		$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 1377 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...
1388
1389
		return $obj;
1390
	}
1391
1392
	public function estimateRowCount(
1393
		$table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1394
	) {
1395
		$rows = 0;
1396
		$res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1397
1398 View Code Duplication
		if ( $res ) {
1399
			$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 1396 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...
1400
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1401
		}
1402
1403
		return $rows;
1404
	}
1405
1406
	public function selectRowCount(
1407
		$tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1408
	) {
1409
		$rows = 0;
1410
		$sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1411
		$res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
1412
1413 View Code Duplication
		if ( $res ) {
1414
			$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 1411 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...
1415
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1416
		}
1417
1418
		return $rows;
1419
	}
1420
1421
	/**
1422
	 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1423
	 * It's only slightly flawed. Don't use for anything important.
1424
	 *
1425
	 * @param string $sql A SQL Query
1426
	 *
1427
	 * @return string
1428
	 */
1429
	protected static function generalizeSQL( $sql ) {
1430
		# This does the same as the regexp below would do, but in such a way
1431
		# as to avoid crashing php on some large strings.
1432
		# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1433
1434
		$sql = str_replace( "\\\\", '', $sql );
1435
		$sql = str_replace( "\\'", '', $sql );
1436
		$sql = str_replace( "\\\"", '', $sql );
1437
		$sql = preg_replace( "/'.*'/s", "'X'", $sql );
1438
		$sql = preg_replace( '/".*"/s', "'X'", $sql );
1439
1440
		# All newlines, tabs, etc replaced by single space
1441
		$sql = preg_replace( '/\s+/', ' ', $sql );
1442
1443
		# All numbers => N,
1444
		# except the ones surrounded by characters, e.g. l10n
1445
		$sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1446
		$sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1447
1448
		return $sql;
1449
	}
1450
1451
	public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1452
		$info = $this->fieldInfo( $table, $field );
1453
1454
		return (bool)$info;
1455
	}
1456
1457
	public function indexExists( $table, $index, $fname = __METHOD__ ) {
1458
		if ( !$this->tableExists( $table ) ) {
1459
			return null;
1460
		}
1461
1462
		$info = $this->indexInfo( $table, $index, $fname );
1463
		if ( is_null( $info ) ) {
1464
			return null;
1465
		} else {
1466
			return $info !== false;
1467
		}
1468
	}
1469
1470
	public function tableExists( $table, $fname = __METHOD__ ) {
1471
		$table = $this->tableName( $table );
1472
		$old = $this->ignoreErrors( true );
1473
		$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
1474
		$this->ignoreErrors( $old );
1475
1476
		return (bool)$res;
1477
	}
1478
1479
	public function indexUnique( $table, $index ) {
1480
		$indexInfo = $this->indexInfo( $table, $index );
1481
1482
		if ( !$indexInfo ) {
1483
			return null;
1484
		}
1485
1486
		return !$indexInfo[0]->Non_unique;
1487
	}
1488
1489
	/**
1490
	 * Helper for DatabaseBase::insert().
1491
	 *
1492
	 * @param array $options
1493
	 * @return string
1494
	 */
1495
	protected function makeInsertOptions( $options ) {
1496
		return implode( ' ', $options );
1497
	}
1498
1499
	public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1500
		# No rows to insert, easy just return now
1501
		if ( !count( $a ) ) {
1502
			return true;
1503
		}
1504
1505
		$table = $this->tableName( $table );
1506
1507
		if ( !is_array( $options ) ) {
1508
			$options = [ $options ];
1509
		}
1510
1511
		$fh = null;
1512
		if ( isset( $options['fileHandle'] ) ) {
1513
			$fh = $options['fileHandle'];
1514
		}
1515
		$options = $this->makeInsertOptions( $options );
1516
1517
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1518
			$multi = true;
1519
			$keys = array_keys( $a[0] );
1520
		} else {
1521
			$multi = false;
1522
			$keys = array_keys( $a );
1523
		}
1524
1525
		$sql = 'INSERT ' . $options .
1526
			" INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1527
1528
		if ( $multi ) {
1529
			$first = true;
1530 View Code Duplication
			foreach ( $a as $row ) {
1531
				if ( $first ) {
1532
					$first = false;
1533
				} else {
1534
					$sql .= ',';
1535
				}
1536
				$sql .= '(' . $this->makeList( $row ) . ')';
1537
			}
1538
		} else {
1539
			$sql .= '(' . $this->makeList( $a ) . ')';
1540
		}
1541
1542
		if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1543
			return false;
1544
		} elseif ( $fh !== null ) {
1545
			return true;
1546
		}
1547
1548
		return (bool)$this->query( $sql, $fname );
1549
	}
1550
1551
	/**
1552
	 * Make UPDATE options array for DatabaseBase::makeUpdateOptions
1553
	 *
1554
	 * @param array $options
1555
	 * @return array
1556
	 */
1557
	protected function makeUpdateOptionsArray( $options ) {
1558
		if ( !is_array( $options ) ) {
1559
			$options = [ $options ];
1560
		}
1561
1562
		$opts = [];
1563
1564
		if ( in_array( 'LOW_PRIORITY', $options ) ) {
1565
			$opts[] = $this->lowPriorityOption();
1566
		}
1567
1568
		if ( in_array( 'IGNORE', $options ) ) {
1569
			$opts[] = 'IGNORE';
1570
		}
1571
1572
		return $opts;
1573
	}
1574
1575
	/**
1576
	 * Make UPDATE options for the DatabaseBase::update function
1577
	 *
1578
	 * @param array $options The options passed to DatabaseBase::update
1579
	 * @return string
1580
	 */
1581
	protected function makeUpdateOptions( $options ) {
1582
		$opts = $this->makeUpdateOptionsArray( $options );
1583
1584
		return implode( ' ', $opts );
1585
	}
1586
1587
	function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1588
		$table = $this->tableName( $table );
1589
		$opts = $this->makeUpdateOptions( $options );
1590
		$sql = "UPDATE $opts $table SET " . $this->makeList( $values, LIST_SET );
1591
1592 View Code Duplication
		if ( $conds !== [] && $conds !== '*' ) {
1593
			$sql .= " WHERE " . $this->makeList( $conds, LIST_AND );
1594
		}
1595
1596
		return $this->query( $sql, $fname );
1597
	}
1598
1599
	public function makeList( $a, $mode = LIST_COMMA ) {
1600
		if ( !is_array( $a ) ) {
1601
			throw new DBUnexpectedError( $this, 'DatabaseBase::makeList called with incorrect parameters' );
1602
		}
1603
1604
		$first = true;
1605
		$list = '';
1606
1607
		foreach ( $a as $field => $value ) {
1608
			if ( !$first ) {
1609
				if ( $mode == LIST_AND ) {
1610
					$list .= ' AND ';
1611
				} elseif ( $mode == LIST_OR ) {
1612
					$list .= ' OR ';
1613
				} else {
1614
					$list .= ',';
1615
				}
1616
			} else {
1617
				$first = false;
1618
			}
1619
1620
			if ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_numeric( $field ) ) {
1621
				$list .= "($value)";
1622
			} elseif ( ( $mode == LIST_SET ) && is_numeric( $field ) ) {
1623
				$list .= "$value";
1624
			} elseif ( ( $mode == LIST_AND || $mode == LIST_OR ) && is_array( $value ) ) {
1625
				// Remove null from array to be handled separately if found
1626
				$includeNull = false;
1627
				foreach ( array_keys( $value, null, true ) as $nullKey ) {
1628
					$includeNull = true;
1629
					unset( $value[$nullKey] );
1630
				}
1631
				if ( count( $value ) == 0 && !$includeNull ) {
1632
					throw new MWException( __METHOD__ . ": empty input for field $field" );
1633
				} elseif ( count( $value ) == 0 ) {
1634
					// only check if $field is null
1635
					$list .= "$field IS NULL";
1636
				} else {
1637
					// IN clause contains at least one valid element
1638
					if ( $includeNull ) {
1639
						// Group subconditions to ensure correct precedence
1640
						$list .= '(';
1641
					}
1642
					if ( count( $value ) == 1 ) {
1643
						// Special-case single values, as IN isn't terribly efficient
1644
						// Don't necessarily assume the single key is 0; we don't
1645
						// enforce linear numeric ordering on other arrays here.
1646
						$value = array_values( $value )[0];
1647
						$list .= $field . " = " . $this->addQuotes( $value );
1648
					} else {
1649
						$list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1650
					}
1651
					// if null present in array, append IS NULL
1652
					if ( $includeNull ) {
1653
						$list .= " OR $field IS NULL)";
1654
					}
1655
				}
1656
			} elseif ( $value === null ) {
1657 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR ) {
1658
					$list .= "$field IS ";
1659
				} elseif ( $mode == LIST_SET ) {
1660
					$list .= "$field = ";
1661
				}
1662
				$list .= 'NULL';
1663
			} else {
1664 View Code Duplication
				if ( $mode == LIST_AND || $mode == LIST_OR || $mode == LIST_SET ) {
1665
					$list .= "$field = ";
1666
				}
1667
				$list .= $mode == LIST_NAMES ? $value : $this->addQuotes( $value );
1668
			}
1669
		}
1670
1671
		return $list;
1672
	}
1673
1674
	public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1675
		$conds = [];
1676
1677
		foreach ( $data as $base => $sub ) {
1678
			if ( count( $sub ) ) {
1679
				$conds[] = $this->makeList(
1680
					[ $baseKey => $base, $subKey => array_keys( $sub ) ],
1681
					LIST_AND );
1682
			}
1683
		}
1684
1685
		if ( $conds ) {
1686
			return $this->makeList( $conds, LIST_OR );
1687
		} else {
1688
			// Nothing to search for...
1689
			return false;
1690
		}
1691
	}
1692
1693
	/**
1694
	 * Return aggregated value alias
1695
	 *
1696
	 * @param array $valuedata
1697
	 * @param string $valuename
1698
	 *
1699
	 * @return string
1700
	 */
1701
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
1702
		return $valuename;
1703
	}
1704
1705
	public function bitNot( $field ) {
1706
		return "(~$field)";
1707
	}
1708
1709
	public function bitAnd( $fieldLeft, $fieldRight ) {
1710
		return "($fieldLeft & $fieldRight)";
1711
	}
1712
1713
	public function bitOr( $fieldLeft, $fieldRight ) {
1714
		return "($fieldLeft | $fieldRight)";
1715
	}
1716
1717
	public function buildConcat( $stringList ) {
1718
		return 'CONCAT(' . implode( ',', $stringList ) . ')';
1719
	}
1720
1721 View Code Duplication
	public function buildGroupConcatField(
1722
		$delim, $table, $field, $conds = '', $join_conds = []
1723
	) {
1724
		$fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1725
1726
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1727
	}
1728
1729
	public function selectDB( $db ) {
1730
		# Stub. Shouldn't cause serious problems if it's not overridden, but
1731
		# if your database engine supports a concept similar to MySQL's
1732
		# databases you may as well.
1733
		$this->mDBname = $db;
1734
1735
		return true;
1736
	}
1737
1738
	public function getDBname() {
1739
		return $this->mDBname;
1740
	}
1741
1742
	public function getServer() {
1743
		return $this->mServer;
1744
	}
1745
1746
	/**
1747
	 * Format a table name ready for use in constructing an SQL query
1748
	 *
1749
	 * This does two important things: it quotes the table names to clean them up,
1750
	 * and it adds a table prefix if only given a table name with no quotes.
1751
	 *
1752
	 * All functions of this object which require a table name call this function
1753
	 * themselves. Pass the canonical name to such functions. This is only needed
1754
	 * when calling query() directly.
1755
	 *
1756
	 * @note This function does not sanitize user input. It is not safe to use
1757
	 *   this function to escape user input.
1758
	 * @param string $name Database table name
1759
	 * @param string $format One of:
1760
	 *   quoted - Automatically pass the table name through addIdentifierQuotes()
1761
	 *            so that it can be used in a query.
1762
	 *   raw - Do not add identifier quotes to the table name
1763
	 * @return string Full database name
1764
	 */
1765
	public function tableName( $name, $format = 'quoted' ) {
1766
		global $wgSharedDB, $wgSharedPrefix, $wgSharedTables, $wgSharedSchema;
1767
		# Skip the entire process when we have a string quoted on both ends.
1768
		# Note that we check the end so that we will still quote any use of
1769
		# use of `database`.table. But won't break things if someone wants
1770
		# to query a database table with a dot in the name.
1771
		if ( $this->isQuotedIdentifier( $name ) ) {
1772
			return $name;
1773
		}
1774
1775
		# Lets test for any bits of text that should never show up in a table
1776
		# name. Basically anything like JOIN or ON which are actually part of
1777
		# SQL queries, but may end up inside of the table value to combine
1778
		# sql. Such as how the API is doing.
1779
		# Note that we use a whitespace test rather than a \b test to avoid
1780
		# any remote case where a word like on may be inside of a table name
1781
		# surrounded by symbols which may be considered word breaks.
1782
		if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1783
			return $name;
1784
		}
1785
1786
		# Split database and table into proper variables.
1787
		# We reverse the explode so that database.table and table both output
1788
		# the correct table.
1789
		$dbDetails = explode( '.', $name, 3 );
1790
		if ( count( $dbDetails ) == 3 ) {
1791
			list( $database, $schema, $table ) = $dbDetails;
1792
			# We don't want any prefix added in this case
1793
			$prefix = '';
1794
		} elseif ( count( $dbDetails ) == 2 ) {
1795
			list( $database, $table ) = $dbDetails;
1796
			# We don't want any prefix added in this case
1797
			# In dbs that support it, $database may actually be the schema
1798
			# but that doesn't affect any of the functionality here
1799
			$prefix = '';
1800
			$schema = null;
1801
		} else {
1802
			list( $table ) = $dbDetails;
1803
			if ( $wgSharedDB !== null # We have a shared database
1804
				&& $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...
1805
				&& !$this->isQuotedIdentifier( $table ) # Prevent shared tables listing '`table`'
1806
				&& in_array( $table, $wgSharedTables ) # A shared table is selected
1807
			) {
1808
				$database = $wgSharedDB;
1809
				$schema = $wgSharedSchema === null ? $this->mSchema : $wgSharedSchema;
1810
				$prefix = $wgSharedPrefix === null ? $this->mTablePrefix : $wgSharedPrefix;
1811
			} else {
1812
				$database = null;
1813
				$schema = $this->mSchema; # Default schema
1814
				$prefix = $this->mTablePrefix; # Default prefix
1815
			}
1816
		}
1817
1818
		# Quote $table and apply the prefix if not quoted.
1819
		# $tableName might be empty if this is called from Database::replaceVars()
1820
		$tableName = "{$prefix}{$table}";
1821
		if ( $format == 'quoted' && !$this->isQuotedIdentifier( $tableName ) && $tableName !== '' ) {
1822
			$tableName = $this->addIdentifierQuotes( $tableName );
1823
		}
1824
1825
		# Quote $schema and merge it with the table name if needed
1826 View Code Duplication
		if ( strlen( $schema ) ) {
1827
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
1828
				$schema = $this->addIdentifierQuotes( $schema );
1829
			}
1830
			$tableName = $schema . '.' . $tableName;
1831
		}
1832
1833
		# Quote $database and merge it with the table name if needed
1834 View Code Duplication
		if ( $database !== null ) {
1835
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
1836
				$database = $this->addIdentifierQuotes( $database );
1837
			}
1838
			$tableName = $database . '.' . $tableName;
1839
		}
1840
1841
		return $tableName;
1842
	}
1843
1844
	/**
1845
	 * Fetch a number of table names into an array
1846
	 * This is handy when you need to construct SQL for joins
1847
	 *
1848
	 * Example:
1849
	 * extract( $dbr->tableNames( 'user', 'watchlist' ) );
1850
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1851
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1852
	 *
1853
	 * @return array
1854
	 */
1855 View Code Duplication
	public function tableNames() {
1856
		$inArray = func_get_args();
1857
		$retVal = [];
1858
1859
		foreach ( $inArray as $name ) {
1860
			$retVal[$name] = $this->tableName( $name );
1861
		}
1862
1863
		return $retVal;
1864
	}
1865
1866
	/**
1867
	 * Fetch a number of table names into an zero-indexed numerical array
1868
	 * This is handy when you need to construct SQL for joins
1869
	 *
1870
	 * Example:
1871
	 * list( $user, $watchlist ) = $dbr->tableNamesN( 'user', 'watchlist' );
1872
	 * $sql = "SELECT wl_namespace,wl_title FROM $watchlist,$user
1873
	 *         WHERE wl_user=user_id AND wl_user=$nameWithQuotes";
1874
	 *
1875
	 * @return array
1876
	 */
1877 View Code Duplication
	public function tableNamesN() {
1878
		$inArray = func_get_args();
1879
		$retVal = [];
1880
1881
		foreach ( $inArray as $name ) {
1882
			$retVal[] = $this->tableName( $name );
1883
		}
1884
1885
		return $retVal;
1886
	}
1887
1888
	/**
1889
	 * Get an aliased table name
1890
	 * e.g. tableName AS newTableName
1891
	 *
1892
	 * @param string $name Table name, see tableName()
1893
	 * @param string|bool $alias Alias (optional)
1894
	 * @return string SQL name for aliased table. Will not alias a table to its own name
1895
	 */
1896
	public function tableNameWithAlias( $name, $alias = false ) {
1897
		if ( !$alias || $alias == $name ) {
1898
			return $this->tableName( $name );
1899
		} else {
1900
			return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1896 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...
1901
		}
1902
	}
1903
1904
	/**
1905
	 * Gets an array of aliased table names
1906
	 *
1907
	 * @param array $tables [ [alias] => table ]
1908
	 * @return string[] See tableNameWithAlias()
1909
	 */
1910
	public function tableNamesWithAlias( $tables ) {
1911
		$retval = [];
1912
		foreach ( $tables as $alias => $table ) {
1913
			if ( is_numeric( $alias ) ) {
1914
				$alias = $table;
1915
			}
1916
			$retval[] = $this->tableNameWithAlias( $table, $alias );
1917
		}
1918
1919
		return $retval;
1920
	}
1921
1922
	/**
1923
	 * Get an aliased field name
1924
	 * e.g. fieldName AS newFieldName
1925
	 *
1926
	 * @param string $name Field name
1927
	 * @param string|bool $alias Alias (optional)
1928
	 * @return string SQL name for aliased field. Will not alias a field to its own name
1929
	 */
1930
	public function fieldNameWithAlias( $name, $alias = false ) {
1931
		if ( !$alias || (string)$alias === (string)$name ) {
1932
			return $name;
1933
		} else {
1934
			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 1930 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...
1935
		}
1936
	}
1937
1938
	/**
1939
	 * Gets an array of aliased field names
1940
	 *
1941
	 * @param array $fields [ [alias] => field ]
1942
	 * @return string[] See fieldNameWithAlias()
1943
	 */
1944
	public function fieldNamesWithAlias( $fields ) {
1945
		$retval = [];
1946
		foreach ( $fields as $alias => $field ) {
1947
			if ( is_numeric( $alias ) ) {
1948
				$alias = $field;
1949
			}
1950
			$retval[] = $this->fieldNameWithAlias( $field, $alias );
1951
		}
1952
1953
		return $retval;
1954
	}
1955
1956
	/**
1957
	 * Get the aliased table name clause for a FROM clause
1958
	 * which might have a JOIN and/or USE INDEX clause
1959
	 *
1960
	 * @param array $tables ( [alias] => table )
1961
	 * @param array $use_index Same as for select()
1962
	 * @param array $join_conds Same as for select()
1963
	 * @return string
1964
	 */
1965
	protected function tableNamesWithUseIndexOrJOIN(
1966
		$tables, $use_index = [], $join_conds = []
1967
	) {
1968
		$ret = [];
1969
		$retJOIN = [];
1970
		$use_index = (array)$use_index;
1971
		$join_conds = (array)$join_conds;
1972
1973
		foreach ( $tables as $alias => $table ) {
1974
			if ( !is_string( $alias ) ) {
1975
				// No alias? Set it equal to the table name
1976
				$alias = $table;
1977
			}
1978
			// Is there a JOIN clause for this table?
1979
			if ( isset( $join_conds[$alias] ) ) {
1980
				list( $joinType, $conds ) = $join_conds[$alias];
1981
				$tableClause = $joinType;
1982
				$tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
1983
				if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
1984
					$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
1985
					if ( $use != '' ) {
1986
						$tableClause .= ' ' . $use;
1987
					}
1988
				}
1989
				$on = $this->makeList( (array)$conds, LIST_AND );
1990
				if ( $on != '' ) {
1991
					$tableClause .= ' ON (' . $on . ')';
1992
				}
1993
1994
				$retJOIN[] = $tableClause;
1995
			} elseif ( isset( $use_index[$alias] ) ) {
1996
				// Is there an INDEX clause for this table?
1997
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1998
				$tableClause .= ' ' . $this->useIndexClause(
1999
					implode( ',', (array)$use_index[$alias] )
2000
				);
2001
2002
				$ret[] = $tableClause;
2003
			} else {
2004
				$tableClause = $this->tableNameWithAlias( $table, $alias );
2005
2006
				$ret[] = $tableClause;
2007
			}
2008
		}
2009
2010
		// We can't separate explicit JOIN clauses with ',', use ' ' for those
2011
		$implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
2012
		$explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
2013
2014
		// Compile our final table clause
2015
		return implode( ' ', [ $implicitJoins, $explicitJoins ] );
2016
	}
2017
2018
	/**
2019
	 * Get the name of an index in a given table.
2020
	 *
2021
	 * @param string $index
2022
	 * @return string
2023
	 */
2024
	protected function indexName( $index ) {
2025
		// Backwards-compatibility hack
2026
		$renamed = [
2027
			'ar_usertext_timestamp' => 'usertext_timestamp',
2028
			'un_user_id' => 'user_id',
2029
			'un_user_ip' => 'user_ip',
2030
		];
2031
2032
		if ( isset( $renamed[$index] ) ) {
2033
			return $renamed[$index];
2034
		} else {
2035
			return $index;
2036
		}
2037
	}
2038
2039
	public function addQuotes( $s ) {
2040
		if ( $s instanceof Blob ) {
2041
			$s = $s->fetch();
2042
		}
2043
		if ( $s === null ) {
2044
			return 'NULL';
2045
		} else {
2046
			# This will also quote numeric values. This should be harmless,
2047
			# and protects against weird problems that occur when they really
2048
			# _are_ strings such as article titles and string->number->string
2049
			# conversion is not 1:1.
2050
			return "'" . $this->strencode( $s ) . "'";
2051
		}
2052
	}
2053
2054
	/**
2055
	 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
2056
	 * MySQL uses `backticks` while basically everything else uses double quotes.
2057
	 * Since MySQL is the odd one out here the double quotes are our generic
2058
	 * and we implement backticks in DatabaseMysql.
2059
	 *
2060
	 * @param string $s
2061
	 * @return string
2062
	 */
2063
	public function addIdentifierQuotes( $s ) {
2064
		return '"' . str_replace( '"', '""', $s ) . '"';
2065
	}
2066
2067
	/**
2068
	 * Returns if the given identifier looks quoted or not according to
2069
	 * the database convention for quoting identifiers .
2070
	 *
2071
	 * @note Do not use this to determine if untrusted input is safe.
2072
	 *   A malicious user can trick this function.
2073
	 * @param string $name
2074
	 * @return bool
2075
	 */
2076
	public function isQuotedIdentifier( $name ) {
2077
		return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
2078
	}
2079
2080
	/**
2081
	 * @param string $s
2082
	 * @return string
2083
	 */
2084
	protected function escapeLikeInternal( $s ) {
2085
		return addcslashes( $s, '\%_' );
2086
	}
2087
2088
	public function buildLike() {
2089
		$params = func_get_args();
2090
2091
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2092
			$params = $params[0];
2093
		}
2094
2095
		$s = '';
2096
2097
		foreach ( $params as $value ) {
2098
			if ( $value instanceof LikeMatch ) {
2099
				$s .= $value->toString();
2100
			} else {
2101
				$s .= $this->escapeLikeInternal( $value );
2102
			}
2103
		}
2104
2105
		return " LIKE {$this->addQuotes( $s )} ";
2106
	}
2107
2108
	public function anyChar() {
2109
		return new LikeMatch( '_' );
2110
	}
2111
2112
	public function anyString() {
2113
		return new LikeMatch( '%' );
2114
	}
2115
2116
	public function nextSequenceValue( $seqName ) {
2117
		return null;
2118
	}
2119
2120
	/**
2121
	 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2122
	 * is only needed because a) MySQL must be as efficient as possible due to
2123
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2124
	 * which index to pick. Anyway, other databases might have different
2125
	 * indexes on a given table. So don't bother overriding this unless you're
2126
	 * MySQL.
2127
	 * @param string $index
2128
	 * @return string
2129
	 */
2130
	public function useIndexClause( $index ) {
2131
		return '';
2132
	}
2133
2134
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2135
		$quotedTable = $this->tableName( $table );
2136
2137
		if ( count( $rows ) == 0 ) {
2138
			return;
2139
		}
2140
2141
		# Single row case
2142
		if ( !is_array( reset( $rows ) ) ) {
2143
			$rows = [ $rows ];
2144
		}
2145
2146
		// @FXIME: this is not atomic, but a trx would break affectedRows()
2147
		foreach ( $rows as $row ) {
2148
			# Delete rows which collide
2149
			if ( $uniqueIndexes ) {
2150
				$sql = "DELETE FROM $quotedTable WHERE ";
2151
				$first = true;
2152
				foreach ( $uniqueIndexes as $index ) {
2153
					if ( $first ) {
2154
						$first = false;
2155
						$sql .= '( ';
2156
					} else {
2157
						$sql .= ' ) OR ( ';
2158
					}
2159
					if ( is_array( $index ) ) {
2160
						$first2 = true;
2161
						foreach ( $index as $col ) {
2162
							if ( $first2 ) {
2163
								$first2 = false;
2164
							} else {
2165
								$sql .= ' AND ';
2166
							}
2167
							$sql .= $col . '=' . $this->addQuotes( $row[$col] );
2168
						}
2169
					} else {
2170
						$sql .= $index . '=' . $this->addQuotes( $row[$index] );
2171
					}
2172
				}
2173
				$sql .= ' )';
2174
				$this->query( $sql, $fname );
2175
			}
2176
2177
			# Now insert the row
2178
			$this->insert( $table, $row, $fname );
2179
		}
2180
	}
2181
2182
	/**
2183
	 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2184
	 * statement.
2185
	 *
2186
	 * @param string $table Table name
2187
	 * @param array|string $rows Row(s) to insert
2188
	 * @param string $fname Caller function name
2189
	 *
2190
	 * @return ResultWrapper
2191
	 */
2192
	protected function nativeReplace( $table, $rows, $fname ) {
2193
		$table = $this->tableName( $table );
2194
2195
		# Single row case
2196
		if ( !is_array( reset( $rows ) ) ) {
2197
			$rows = [ $rows ];
2198
		}
2199
2200
		$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2201
		$first = true;
2202
2203 View Code Duplication
		foreach ( $rows as $row ) {
2204
			if ( $first ) {
2205
				$first = false;
2206
			} else {
2207
				$sql .= ',';
2208
			}
2209
2210
			$sql .= '(' . $this->makeList( $row ) . ')';
2211
		}
2212
2213
		return $this->query( $sql, $fname );
2214
	}
2215
2216
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2217
		$fname = __METHOD__
2218
	) {
2219
		if ( !count( $rows ) ) {
2220
			return true; // nothing to do
2221
		}
2222
2223
		if ( !is_array( reset( $rows ) ) ) {
2224
			$rows = [ $rows ];
2225
		}
2226
2227
		if ( count( $uniqueIndexes ) ) {
2228
			$clauses = []; // list WHERE clauses that each identify a single row
2229
			foreach ( $rows as $row ) {
2230
				foreach ( $uniqueIndexes as $index ) {
2231
					$index = is_array( $index ) ? $index : [ $index ]; // columns
2232
					$rowKey = []; // unique key to this row
2233
					foreach ( $index as $column ) {
2234
						$rowKey[$column] = $row[$column];
2235
					}
2236
					$clauses[] = $this->makeList( $rowKey, LIST_AND );
2237
				}
2238
			}
2239
			$where = [ $this->makeList( $clauses, LIST_OR ) ];
2240
		} else {
2241
			$where = false;
2242
		}
2243
2244
		$useTrx = !$this->mTrxLevel;
2245
		if ( $useTrx ) {
2246
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2247
		}
2248
		try {
2249
			# Update any existing conflicting row(s)
2250
			if ( $where !== false ) {
2251
				$ok = $this->update( $table, $set, $where, $fname );
2252
			} else {
2253
				$ok = true;
2254
			}
2255
			# Now insert any non-conflicting row(s)
2256
			$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2257
		} catch ( Exception $e ) {
2258
			if ( $useTrx ) {
2259
				$this->rollback( $fname );
2260
			}
2261
			throw $e;
2262
		}
2263
		if ( $useTrx ) {
2264
			$this->commit( $fname, self::TRANSACTION_INTERNAL );
2265
		}
2266
2267
		return $ok;
2268
	}
2269
2270 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2271
		$fname = __METHOD__
2272
	) {
2273
		if ( !$conds ) {
2274
			throw new DBUnexpectedError( $this,
2275
				'DatabaseBase::deleteJoin() called with empty $conds' );
2276
		}
2277
2278
		$delTable = $this->tableName( $delTable );
2279
		$joinTable = $this->tableName( $joinTable );
2280
		$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2281
		if ( $conds != '*' ) {
2282
			$sql .= 'WHERE ' . $this->makeList( $conds, LIST_AND );
2283
		}
2284
		$sql .= ')';
2285
2286
		$this->query( $sql, $fname );
2287
	}
2288
2289
	/**
2290
	 * Returns the size of a text field, or -1 for "unlimited"
2291
	 *
2292
	 * @param string $table
2293
	 * @param string $field
2294
	 * @return int
2295
	 */
2296
	public function textFieldSize( $table, $field ) {
2297
		$table = $this->tableName( $table );
2298
		$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2299
		$res = $this->query( $sql, 'DatabaseBase::textFieldSize' );
2300
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql, 'DatabaseBase::textFieldSize') on line 2299 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...
2301
2302
		$m = [];
2303
2304
		if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2305
			$size = $m[1];
2306
		} else {
2307
			$size = -1;
2308
		}
2309
2310
		return $size;
2311
	}
2312
2313
	/**
2314
	 * A string to insert into queries to show that they're low-priority, like
2315
	 * MySQL's LOW_PRIORITY. If no such feature exists, return an empty
2316
	 * string and nothing bad should happen.
2317
	 *
2318
	 * @return string Returns the text of the low priority option if it is
2319
	 *   supported, or a blank string otherwise
2320
	 */
2321
	public function lowPriorityOption() {
2322
		return '';
2323
	}
2324
2325
	public function delete( $table, $conds, $fname = __METHOD__ ) {
2326
		if ( !$conds ) {
2327
			throw new DBUnexpectedError( $this, 'DatabaseBase::delete() called with no conditions' );
2328
		}
2329
2330
		$table = $this->tableName( $table );
2331
		$sql = "DELETE FROM $table";
2332
2333 View Code Duplication
		if ( $conds != '*' ) {
2334
			if ( is_array( $conds ) ) {
2335
				$conds = $this->makeList( $conds, LIST_AND );
2336
			}
2337
			$sql .= ' WHERE ' . $conds;
2338
		}
2339
2340
		return $this->query( $sql, $fname );
2341
	}
2342
2343
	public function insertSelect( $destTable, $srcTable, $varMap, $conds,
2344
		$fname = __METHOD__,
2345
		$insertOptions = [], $selectOptions = []
2346
	) {
2347
		$destTable = $this->tableName( $destTable );
2348
2349
		if ( !is_array( $insertOptions ) ) {
2350
			$insertOptions = [ $insertOptions ];
2351
		}
2352
2353
		$insertOptions = $this->makeInsertOptions( $insertOptions );
2354
2355
		if ( !is_array( $selectOptions ) ) {
2356
			$selectOptions = [ $selectOptions ];
2357
		}
2358
2359
		list( $startOpts, $useIndex, $tailOpts ) = $this->makeSelectOptions( $selectOptions );
2360
2361 View Code Duplication
		if ( is_array( $srcTable ) ) {
2362
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2363
		} else {
2364
			$srcTable = $this->tableName( $srcTable );
2365
		}
2366
2367
		$sql = "INSERT $insertOptions INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2368
			" SELECT $startOpts " . implode( ',', $varMap ) .
2369
			" FROM $srcTable $useIndex ";
2370
2371 View Code Duplication
		if ( $conds != '*' ) {
2372
			if ( is_array( $conds ) ) {
2373
				$conds = $this->makeList( $conds, LIST_AND );
2374
			}
2375
			$sql .= " WHERE $conds";
2376
		}
2377
2378
		$sql .= " $tailOpts";
2379
2380
		return $this->query( $sql, $fname );
2381
	}
2382
2383
	/**
2384
	 * Construct a LIMIT query with optional offset. This is used for query
2385
	 * pages. The SQL should be adjusted so that only the first $limit rows
2386
	 * are returned. If $offset is provided as well, then the first $offset
2387
	 * rows should be discarded, and the next $limit rows should be returned.
2388
	 * If the result of the query is not ordered, then the rows to be returned
2389
	 * are theoretically arbitrary.
2390
	 *
2391
	 * $sql is expected to be a SELECT, if that makes a difference.
2392
	 *
2393
	 * The version provided by default works in MySQL and SQLite. It will very
2394
	 * likely need to be overridden for most other DBMSes.
2395
	 *
2396
	 * @param string $sql SQL query we will append the limit too
2397
	 * @param int $limit The SQL limit
2398
	 * @param int|bool $offset The SQL offset (default false)
2399
	 * @throws DBUnexpectedError
2400
	 * @return string
2401
	 */
2402
	public function limitResult( $sql, $limit, $offset = false ) {
2403
		if ( !is_numeric( $limit ) ) {
2404
			throw new DBUnexpectedError( $this, "Invalid non-numeric limit passed to limitResult()\n" );
2405
		}
2406
2407
		return "$sql LIMIT "
2408
			. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2409
			. "{$limit} ";
2410
	}
2411
2412
	public function unionSupportsOrderAndLimit() {
2413
		return true; // True for almost every DB supported
2414
	}
2415
2416
	public function unionQueries( $sqls, $all ) {
2417
		$glue = $all ? ') UNION ALL (' : ') UNION (';
2418
2419
		return '(' . implode( $glue, $sqls ) . ')';
2420
	}
2421
2422
	public function conditional( $cond, $trueVal, $falseVal ) {
2423
		if ( is_array( $cond ) ) {
2424
			$cond = $this->makeList( $cond, LIST_AND );
2425
		}
2426
2427
		return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2428
	}
2429
2430
	public function strreplace( $orig, $old, $new ) {
2431
		return "REPLACE({$orig}, {$old}, {$new})";
2432
	}
2433
2434
	public function getServerUptime() {
2435
		return 0;
2436
	}
2437
2438
	public function wasDeadlock() {
2439
		return false;
2440
	}
2441
2442
	public function wasLockTimeout() {
2443
		return false;
2444
	}
2445
2446
	public function wasErrorReissuable() {
2447
		return false;
2448
	}
2449
2450
	public function wasReadOnlyError() {
2451
		return false;
2452
	}
2453
2454
	/**
2455
	 * Determines if the given query error was a connection drop
2456
	 * STUB
2457
	 *
2458
	 * @param integer|string $errno
2459
	 * @return bool
2460
	 */
2461
	public function wasConnectionError( $errno ) {
2462
		return false;
2463
	}
2464
2465
	/**
2466
	 * Perform a deadlock-prone transaction.
2467
	 *
2468
	 * This function invokes a callback function to perform a set of write
2469
	 * queries. If a deadlock occurs during the processing, the transaction
2470
	 * will be rolled back and the callback function will be called again.
2471
	 *
2472
	 * Avoid using this method outside of Job or Maintenance classes.
2473
	 *
2474
	 * Usage:
2475
	 *   $dbw->deadlockLoop( callback, ... );
2476
	 *
2477
	 * Extra arguments are passed through to the specified callback function.
2478
	 * This method requires that no transactions are already active to avoid
2479
	 * causing premature commits or exceptions.
2480
	 *
2481
	 * Returns whatever the callback function returned on its successful,
2482
	 * iteration, or false on error, for example if the retry limit was
2483
	 * reached.
2484
	 *
2485
	 * @return mixed
2486
	 * @throws DBUnexpectedError
2487
	 * @throws Exception
2488
	 */
2489
	public function deadlockLoop() {
2490
		$args = func_get_args();
2491
		$function = array_shift( $args );
2492
		$tries = self::DEADLOCK_TRIES;
2493
2494
		$this->begin( __METHOD__ );
2495
2496
		$retVal = null;
2497
		/** @var Exception $e */
2498
		$e = null;
2499
		do {
2500
			try {
2501
				$retVal = call_user_func_array( $function, $args );
2502
				break;
2503
			} catch ( DBQueryError $e ) {
2504
				if ( $this->wasDeadlock() ) {
2505
					// Retry after a randomized delay
2506
					usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2507
				} else {
2508
					// Throw the error back up
2509
					throw $e;
2510
				}
2511
			}
2512
		} while ( --$tries > 0 );
2513
2514
		if ( $tries <= 0 ) {
2515
			// Too many deadlocks; give up
2516
			$this->rollback( __METHOD__ );
2517
			throw $e;
2518
		} else {
2519
			$this->commit( __METHOD__ );
2520
2521
			return $retVal;
2522
		}
2523
	}
2524
2525
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
2526
		# Real waits are implemented in the subclass.
2527
		return 0;
2528
	}
2529
2530
	public function getSlavePos() {
2531
		# Stub
2532
		return false;
2533
	}
2534
2535
	public function getMasterPos() {
2536
		# Stub
2537
		return false;
2538
	}
2539
2540
	public function serverIsReadOnly() {
2541
		return false;
2542
	}
2543
2544
	final public function onTransactionResolution( callable $callback ) {
2545
		if ( !$this->mTrxLevel ) {
2546
			throw new DBUnexpectedError( $this, "No transaction is active." );
2547
		}
2548
		$this->mTrxEndCallbacks[] = [ $callback, wfGetCaller() ];
2549
	}
2550
2551
	final public function onTransactionIdle( callable $callback ) {
2552
		$this->mTrxIdleCallbacks[] = [ $callback, wfGetCaller() ];
2553
		if ( !$this->mTrxLevel ) {
2554
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2555
		}
2556
	}
2557
2558
	final public function onTransactionPreCommitOrIdle( callable $callback ) {
2559
		if ( $this->mTrxLevel ) {
2560
			$this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
2561
		} else {
2562
			// If no transaction is active, then make one for this callback
2563
			$this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
2564
			try {
2565
				call_user_func( $callback );
2566
				$this->commit( __METHOD__ );
2567
			} catch ( Exception $e ) {
2568
				$this->rollback( __METHOD__ );
2569
				throw $e;
2570
			}
2571
		}
2572
	}
2573
2574
	/**
2575
	 * Whether to disable running of post-commit callbacks
2576
	 *
2577
	 * This method should not be used outside of Database/LoadBalancer
2578
	 *
2579
	 * @param bool $suppress
2580
	 * @since 1.28
2581
	 */
2582
	final public function setPostCommitCallbackSupression( $suppress ) {
2583
		$this->suppressPostCommitCallbacks = $suppress;
2584
	}
2585
2586
	/**
2587
	 * Actually run and consume any "on transaction idle/resolution" callbacks.
2588
	 *
2589
	 * This method should not be used outside of Database/LoadBalancer
2590
	 *
2591
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2592
	 * @since 1.20
2593
	 * @throws Exception
2594
	 */
2595
	public function runOnTransactionIdleCallbacks( $trigger ) {
2596
		if ( $this->suppressPostCommitCallbacks ) {
2597
			return;
2598
		}
2599
2600
		$autoTrx = $this->getFlag( DBO_TRX ); // automatic begin() enabled?
2601
		/** @var Exception $e */
2602
		$e = null; // first exception
2603
		do { // callbacks may add callbacks :)
2604
			$callbacks = array_merge(
2605
				$this->mTrxIdleCallbacks,
2606
				$this->mTrxEndCallbacks // include "transaction resolution" callbacks
2607
			);
2608
			$this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2609
			$this->mTrxEndCallbacks = []; // consumed (recursion guard)
2610
			foreach ( $callbacks as $callback ) {
2611
				try {
2612
					list( $phpCallback ) = $callback;
2613
					$this->clearFlag( DBO_TRX ); // make each query its own transaction
2614
					call_user_func_array( $phpCallback, [ $trigger ] );
2615
					if ( $autoTrx ) {
2616
						$this->setFlag( DBO_TRX ); // restore automatic begin()
2617
					} else {
2618
						$this->clearFlag( DBO_TRX ); // restore auto-commit
2619
					}
2620
				} catch ( Exception $ex ) {
2621
					MWExceptionHandler::logException( $ex );
2622
					$e = $e ?: $ex;
2623
					// Some callbacks may use startAtomic/endAtomic, so make sure
2624
					// their transactions are ended so other callbacks don't fail
2625
					if ( $this->trxLevel() ) {
2626
						$this->rollback( __METHOD__ );
2627
					}
2628
				}
2629
			}
2630
		} while ( count( $this->mTrxIdleCallbacks ) );
2631
2632
		if ( $e instanceof Exception ) {
2633
			throw $e; // re-throw any first exception
2634
		}
2635
	}
2636
2637
	/**
2638
	 * Actually run and consume any "on transaction pre-commit" callbacks.
2639
	 *
2640
	 * This method should not be used outside of Database/LoadBalancer
2641
	 *
2642
	 * @since 1.22
2643
	 * @throws Exception
2644
	 */
2645
	public function runOnTransactionPreCommitCallbacks() {
2646
		$e = null; // first exception
2647
		do { // callbacks may add callbacks :)
2648
			$callbacks = $this->mTrxPreCommitCallbacks;
2649
			$this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2650
			foreach ( $callbacks as $callback ) {
2651
				try {
2652
					list( $phpCallback ) = $callback;
2653
					call_user_func( $phpCallback );
2654
				} catch ( Exception $ex ) {
2655
					MWExceptionHandler::logException( $ex );
2656
					$e = $e ?: $ex;
2657
				}
2658
			}
2659
		} while ( count( $this->mTrxPreCommitCallbacks ) );
2660
2661
		if ( $e instanceof Exception ) {
2662
			throw $e; // re-throw any first exception
2663
		}
2664
	}
2665
2666
	final public function startAtomic( $fname = __METHOD__ ) {
2667
		if ( !$this->mTrxLevel ) {
2668
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2669
			$this->mTrxAutomatic = true;
2670
			// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2671
			// in all changes being in one transaction to keep requests transactional.
2672
			if ( !$this->getFlag( DBO_TRX ) ) {
2673
				$this->mTrxAutomaticAtomic = true;
2674
			}
2675
		}
2676
2677
		$this->mTrxAtomicLevels[] = $fname;
2678
	}
2679
2680
	final public function endAtomic( $fname = __METHOD__ ) {
2681
		if ( !$this->mTrxLevel ) {
2682
			throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2683
		}
2684
		if ( !$this->mTrxAtomicLevels ||
2685
			array_pop( $this->mTrxAtomicLevels ) !== $fname
2686
		) {
2687
			throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2688
		}
2689
2690
		if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2691
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2692
		}
2693
	}
2694
2695
	final public function doAtomicSection( $fname, callable $callback ) {
2696
		$this->startAtomic( $fname );
2697
		try {
2698
			$res = call_user_func_array( $callback, [ $this, $fname ] );
2699
		} catch ( Exception $e ) {
2700
			$this->rollback( $fname );
2701
			throw $e;
2702
		}
2703
		$this->endAtomic( $fname );
2704
2705
		return $res;
2706
	}
2707
2708
	final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2709
		// Protect against mismatched atomic section, transaction nesting, and snapshot loss
2710
		if ( $this->mTrxLevel ) {
2711
			if ( $this->mTrxAtomicLevels ) {
2712
				$levels = implode( ', ', $this->mTrxAtomicLevels );
2713
				$msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2714
				throw new DBUnexpectedError( $this, $msg );
2715
			} elseif ( !$this->mTrxAutomatic ) {
2716
				$msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2717
				throw new DBUnexpectedError( $this, $msg );
2718
			} else {
2719
				// @TODO: make this an exception at some point
2720
				$msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2721
				wfLogDBError( $msg );
2722
				return; // join the main transaction set
2723
			}
2724
		} elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2725
			// @TODO: make this an exception at some point
2726
			wfLogDBError( "$fname: Implicit transaction expected (DBO_TRX set)." );
2727
			return; // let any writes be in the main transaction
2728
		}
2729
2730
		// Avoid fatals if close() was called
2731
		$this->assertOpen();
2732
2733
		$this->doBegin( $fname );
2734
		$this->mTrxTimestamp = microtime( true );
2735
		$this->mTrxFname = $fname;
2736
		$this->mTrxDoneWrites = false;
2737
		$this->mTrxAutomatic = false;
2738
		$this->mTrxAutomaticAtomic = false;
2739
		$this->mTrxAtomicLevels = [];
2740
		$this->mTrxShortId = wfRandomString( 12 );
2741
		$this->mTrxWriteDuration = 0.0;
2742
		$this->mTrxWriteCallers = [];
2743
		// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2744
		// Get an estimate of the slave lag before then, treating estimate staleness
2745
		// as lag itself just to be safe
2746
		$status = $this->getApproximateLagStatus();
2747
		$this->mTrxSlaveLag = $status['lag'] + ( microtime( true ) - $status['since'] );
0 ignored issues
show
Documentation Bug introduced by
It seems like $status['lag'] + (microt...ue) - $status['since']) can also be of type integer. However, the property $mTrxSlaveLag is declared as type double. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2748
	}
2749
2750
	/**
2751
	 * Issues the BEGIN command to the database server.
2752
	 *
2753
	 * @see DatabaseBase::begin()
2754
	 * @param string $fname
2755
	 */
2756
	protected function doBegin( $fname ) {
2757
		$this->query( 'BEGIN', $fname );
2758
		$this->mTrxLevel = 1;
2759
	}
2760
2761
	final public function commit( $fname = __METHOD__, $flush = '' ) {
2762
		if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2763
			// There are still atomic sections open. This cannot be ignored
2764
			$levels = implode( ', ', $this->mTrxAtomicLevels );
2765
			throw new DBUnexpectedError(
2766
				$this,
2767
				"$fname: Got COMMIT while atomic sections $levels are still open."
2768
			);
2769
		}
2770
2771
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2772
			if ( !$this->mTrxLevel ) {
2773
				return; // nothing to do
2774
			} elseif ( !$this->mTrxAutomatic ) {
2775
				throw new DBUnexpectedError(
2776
					$this,
2777
					"$fname: Flushing an explicit transaction, getting out of sync."
2778
				);
2779
			}
2780
		} else {
2781
			if ( !$this->mTrxLevel ) {
2782
				wfWarn( "$fname: No transaction to commit, something got out of sync." );
2783
				return; // nothing to do
2784
			} elseif ( $this->mTrxAutomatic ) {
2785
				// @TODO: make this an exception at some point
2786
				wfLogDBError( "$fname: Explicit commit of implicit transaction." );
2787
				return; // wait for the main transaction set commit round
2788
			}
2789
		}
2790
2791
		// Avoid fatals if close() was called
2792
		$this->assertOpen();
2793
2794
		$this->runOnTransactionPreCommitCallbacks();
2795
		$writeTime = $this->pendingWriteQueryDuration();
2796
		$this->doCommit( $fname );
2797
		if ( $this->mTrxDoneWrites ) {
2798
			$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...
2799
			$this->getTransactionProfiler()->transactionWritingOut(
2800
				$this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
0 ignored issues
show
Security Bug introduced by
It seems like $writeTime defined by $this->pendingWriteQueryDuration() on line 2795 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...
2801
		}
2802
2803
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2804
	}
2805
2806
	/**
2807
	 * Issues the COMMIT command to the database server.
2808
	 *
2809
	 * @see DatabaseBase::commit()
2810
	 * @param string $fname
2811
	 */
2812
	protected function doCommit( $fname ) {
2813
		if ( $this->mTrxLevel ) {
2814
			$this->query( 'COMMIT', $fname );
2815
			$this->mTrxLevel = 0;
2816
		}
2817
	}
2818
2819
	final public function rollback( $fname = __METHOD__, $flush = '' ) {
2820
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2821
			if ( !$this->mTrxLevel ) {
2822
				return; // nothing to do
2823
			}
2824
		} else {
2825
			if ( !$this->mTrxLevel ) {
2826
				wfWarn( "$fname: No transaction to rollback, something got out of sync." );
2827
				return; // nothing to do
2828
			} elseif ( $this->getFlag( DBO_TRX ) ) {
2829
				throw new DBUnexpectedError(
2830
					$this,
2831
					"$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
2832
				);
2833
			}
2834
		}
2835
2836
		// Avoid fatals if close() was called
2837
		$this->assertOpen();
2838
2839
		$this->doRollback( $fname );
2840
		$this->mTrxAtomicLevels = [];
2841
		if ( $this->mTrxDoneWrites ) {
2842
			$this->getTransactionProfiler()->transactionWritingOut(
2843
				$this->mServer, $this->mDBname, $this->mTrxShortId );
2844
		}
2845
2846
		$this->mTrxIdleCallbacks = []; // clear
2847
		$this->mTrxPreCommitCallbacks = []; // clear
2848
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
2849
	}
2850
2851
	/**
2852
	 * Issues the ROLLBACK command to the database server.
2853
	 *
2854
	 * @see DatabaseBase::rollback()
2855
	 * @param string $fname
2856
	 */
2857
	protected function doRollback( $fname ) {
2858
		if ( $this->mTrxLevel ) {
2859
			# Disconnects cause rollback anyway, so ignore those errors
2860
			$ignoreErrors = true;
2861
			$this->query( 'ROLLBACK', $fname, $ignoreErrors );
2862
			$this->mTrxLevel = 0;
2863
		}
2864
	}
2865
2866
	public function explicitTrxActive() {
2867
		return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
2868
	}
2869
2870
	/**
2871
	 * Creates a new table with structure copied from existing table
2872
	 * Note that unlike most database abstraction functions, this function does not
2873
	 * automatically append database prefix, because it works at a lower
2874
	 * abstraction level.
2875
	 * The table names passed to this function shall not be quoted (this
2876
	 * function calls addIdentifierQuotes when needed).
2877
	 *
2878
	 * @param string $oldName Name of table whose structure should be copied
2879
	 * @param string $newName Name of table to be created
2880
	 * @param bool $temporary Whether the new table should be temporary
2881
	 * @param string $fname Calling function name
2882
	 * @throws MWException
2883
	 * @return bool True if operation was successful
2884
	 */
2885
	public function duplicateTableStructure( $oldName, $newName, $temporary = false,
2886
		$fname = __METHOD__
2887
	) {
2888
		throw new MWException(
2889
			'DatabaseBase::duplicateTableStructure is not implemented in descendant class' );
2890
	}
2891
2892
	function listTables( $prefix = null, $fname = __METHOD__ ) {
2893
		throw new MWException( 'DatabaseBase::listTables is not implemented in descendant class' );
2894
	}
2895
2896
	/**
2897
	 * Reset the views process cache set by listViews()
2898
	 * @since 1.22
2899
	 */
2900
	final public function clearViewsCache() {
2901
		$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...
2902
	}
2903
2904
	/**
2905
	 * Lists all the VIEWs in the database
2906
	 *
2907
	 * For caching purposes the list of all views should be stored in
2908
	 * $this->allViews. The process cache can be cleared with clearViewsCache()
2909
	 *
2910
	 * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
2911
	 * @param string $fname Name of calling function
2912
	 * @throws MWException
2913
	 * @return array
2914
	 * @since 1.22
2915
	 */
2916
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
2917
		throw new MWException( 'DatabaseBase::listViews is not implemented in descendant class' );
2918
	}
2919
2920
	/**
2921
	 * Differentiates between a TABLE and a VIEW
2922
	 *
2923
	 * @param string $name Name of the database-structure to test.
2924
	 * @throws MWException
2925
	 * @return bool
2926
	 * @since 1.22
2927
	 */
2928
	public function isView( $name ) {
2929
		throw new MWException( 'DatabaseBase::isView is not implemented in descendant class' );
2930
	}
2931
2932
	public function timestamp( $ts = 0 ) {
2933
		return wfTimestamp( TS_MW, $ts );
2934
	}
2935
2936
	public function timestampOrNull( $ts = null ) {
2937
		if ( is_null( $ts ) ) {
2938
			return null;
2939
		} else {
2940
			return $this->timestamp( $ts );
2941
		}
2942
	}
2943
2944
	/**
2945
	 * Take the result from a query, and wrap it in a ResultWrapper if
2946
	 * necessary. Boolean values are passed through as is, to indicate success
2947
	 * of write queries or failure.
2948
	 *
2949
	 * Once upon a time, DatabaseBase::query() returned a bare MySQL result
2950
	 * resource, and it was necessary to call this function to convert it to
2951
	 * a wrapper. Nowadays, raw database objects are never exposed to external
2952
	 * callers, so this is unnecessary in external code.
2953
	 *
2954
	 * @param bool|ResultWrapper|resource|object $result
2955
	 * @return bool|ResultWrapper
2956
	 */
2957
	protected function resultObject( $result ) {
2958
		if ( !$result ) {
2959
			return false;
2960
		} elseif ( $result instanceof ResultWrapper ) {
2961
			return $result;
2962
		} elseif ( $result === true ) {
2963
			// Successful write query
2964
			return $result;
2965
		} else {
2966
			return new ResultWrapper( $this, $result );
2967
		}
2968
	}
2969
2970
	public function ping() {
2971
		if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
2972
			return true;
2973
		}
2974
2975
		$ignoreErrors = true;
2976
		$this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
2977
		// This will reconnect if possible or return false if not
2978
		$ok = (bool)$this->query( "SELECT 1 AS ping", __METHOD__, $ignoreErrors );
2979
		$this->restoreFlags( self::RESTORE_PRIOR );
2980
2981
		return $ok;
2982
	}
2983
2984
	/**
2985
	 * @return bool
2986
	 */
2987
	protected function reconnect() {
2988
		$this->closeConnection();
2989
		$this->mOpened = false;
2990
		$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...
2991
		try {
2992
			$this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
2993
			$this->lastPing = microtime( true );
2994
			$ok = true;
2995
		} catch ( DBConnectionError $e ) {
2996
			$ok = false;
2997
		}
2998
2999
		return $ok;
3000
	}
3001
3002
	public function getSessionLagStatus() {
3003
		return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
3004
	}
3005
3006
	/**
3007
	 * Get the slave lag when the current transaction started
3008
	 *
3009
	 * This is useful when transactions might use snapshot isolation
3010
	 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
3011
	 * is this lag plus transaction duration. If they don't, it is still
3012
	 * safe to be pessimistic. This returns null if there is no transaction.
3013
	 *
3014
	 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
3015
	 * @since 1.27
3016
	 */
3017
	public function getTransactionLagStatus() {
3018
		return $this->mTrxLevel
3019
			? [ 'lag' => $this->mTrxSlaveLag, 'since' => $this->trxTimestamp() ]
3020
			: null;
3021
	}
3022
3023
	/**
3024
	 * Get a slave lag estimate for this server
3025
	 *
3026
	 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
3027
	 * @since 1.27
3028
	 */
3029
	public function getApproximateLagStatus() {
3030
		return [
3031
			'lag'   => $this->getLBInfo( 'slave' ) ? $this->getLag() : 0,
3032
			'since' => microtime( true )
3033
		];
3034
	}
3035
3036
	/**
3037
	 * Merge the result of getSessionLagStatus() for several DBs
3038
	 * using the most pessimistic values to estimate the lag of
3039
	 * any data derived from them in combination
3040
	 *
3041
	 * This is information is useful for caching modules
3042
	 *
3043
	 * @see WANObjectCache::set()
3044
	 * @see WANObjectCache::getWithSetCallback()
3045
	 *
3046
	 * @param IDatabase $db1
3047
	 * @param IDatabase ...
3048
	 * @return array Map of values:
3049
	 *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
3050
	 *   - since: oldest UNIX timestamp of any of the DB lag estimates
3051
	 *   - pending: whether any of the DBs have uncommitted changes
3052
	 * @since 1.27
3053
	 */
3054
	public static function getCacheSetOptions( IDatabase $db1 ) {
3055
		$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3056
		foreach ( func_get_args() as $db ) {
3057
			/** @var IDatabase $db */
3058
			$status = $db->getSessionLagStatus();
3059
			if ( $status['lag'] === false ) {
3060
				$res['lag'] = false;
3061
			} elseif ( $res['lag'] !== false ) {
3062
				$res['lag'] = max( $res['lag'], $status['lag'] );
3063
			}
3064
			$res['since'] = min( $res['since'], $status['since'] );
3065
			$res['pending'] = $res['pending'] ?: $db->writesPending();
3066
		}
3067
3068
		return $res;
3069
	}
3070
3071
	public function getLag() {
3072
		return 0;
3073
	}
3074
3075
	function maxListLen() {
3076
		return 0;
3077
	}
3078
3079
	public function encodeBlob( $b ) {
3080
		return $b;
3081
	}
3082
3083
	public function decodeBlob( $b ) {
3084
		if ( $b instanceof Blob ) {
3085
			$b = $b->fetch();
3086
		}
3087
		return $b;
3088
	}
3089
3090
	public function setSessionOptions( array $options ) {
3091
	}
3092
3093
	/**
3094
	 * Read and execute SQL commands from a file.
3095
	 *
3096
	 * Returns true on success, error string or exception on failure (depending
3097
	 * on object's error ignore settings).
3098
	 *
3099
	 * @param string $filename File name to open
3100
	 * @param bool|callable $lineCallback Optional function called before reading each line
3101
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3102
	 * @param bool|string $fname Calling function name or false if name should be
3103
	 *   generated dynamically using $filename
3104
	 * @param bool|callable $inputCallback Optional function called for each
3105
	 *   complete line sent
3106
	 * @throws Exception|MWException
3107
	 * @return bool|string
3108
	 */
3109
	public function sourceFile(
3110
		$filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
3111
	) {
3112
		MediaWiki\suppressWarnings();
3113
		$fp = fopen( $filename, 'r' );
3114
		MediaWiki\restoreWarnings();
3115
3116
		if ( false === $fp ) {
3117
			throw new MWException( "Could not open \"{$filename}\".\n" );
3118
		}
3119
3120
		if ( !$fname ) {
3121
			$fname = __METHOD__ . "( $filename )";
3122
		}
3123
3124
		try {
3125
			$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 3110 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...
3126
		} catch ( Exception $e ) {
3127
			fclose( $fp );
3128
			throw $e;
3129
		}
3130
3131
		fclose( $fp );
3132
3133
		return $error;
3134
	}
3135
3136
	/**
3137
	 * Get the full path of a patch file. Originally based on archive()
3138
	 * from updaters.inc. Keep in mind this always returns a patch, as
3139
	 * it fails back to MySQL if no DB-specific patch can be found
3140
	 *
3141
	 * @param string $patch The name of the patch, like patch-something.sql
3142
	 * @return string Full path to patch file
3143
	 */
3144 View Code Duplication
	public function patchPath( $patch ) {
3145
		global $IP;
3146
3147
		$dbType = $this->getType();
3148
		if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
3149
			return "$IP/maintenance/$dbType/archives/$patch";
3150
		} else {
3151
			return "$IP/maintenance/archives/$patch";
3152
		}
3153
	}
3154
3155
	public function setSchemaVars( $vars ) {
3156
		$this->mSchemaVars = $vars;
3157
	}
3158
3159
	/**
3160
	 * Read and execute commands from an open file handle.
3161
	 *
3162
	 * Returns true on success, error string or exception on failure (depending
3163
	 * on object's error ignore settings).
3164
	 *
3165
	 * @param resource $fp File handle
3166
	 * @param bool|callable $lineCallback Optional function called before reading each query
3167
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3168
	 * @param string $fname Calling function name
3169
	 * @param bool|callable $inputCallback Optional function called for each complete query sent
3170
	 * @return bool|string
3171
	 */
3172
	public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
3173
		$fname = __METHOD__, $inputCallback = false
3174
	) {
3175
		$cmd = '';
3176
3177
		while ( !feof( $fp ) ) {
3178
			if ( $lineCallback ) {
3179
				call_user_func( $lineCallback );
3180
			}
3181
3182
			$line = trim( fgets( $fp ) );
3183
3184
			if ( $line == '' ) {
3185
				continue;
3186
			}
3187
3188
			if ( '-' == $line[0] && '-' == $line[1] ) {
3189
				continue;
3190
			}
3191
3192
			if ( $cmd != '' ) {
3193
				$cmd .= ' ';
3194
			}
3195
3196
			$done = $this->streamStatementEnd( $cmd, $line );
3197
3198
			$cmd .= "$line\n";
3199
3200
			if ( $done || feof( $fp ) ) {
3201
				$cmd = $this->replaceVars( $cmd );
3202
3203
				if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
3204
					$res = $this->query( $cmd, $fname );
3205
3206
					if ( $resultCallback ) {
3207
						call_user_func( $resultCallback, $res, $this );
3208
					}
3209
3210
					if ( false === $res ) {
3211
						$err = $this->lastError();
3212
3213
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3214
					}
3215
				}
3216
				$cmd = '';
3217
			}
3218
		}
3219
3220
		return true;
3221
	}
3222
3223
	/**
3224
	 * Called by sourceStream() to check if we've reached a statement end
3225
	 *
3226
	 * @param string $sql SQL assembled so far
3227
	 * @param string $newLine New line about to be added to $sql
3228
	 * @return bool Whether $newLine contains end of the statement
3229
	 */
3230
	public function streamStatementEnd( &$sql, &$newLine ) {
3231
		if ( $this->delimiter ) {
3232
			$prev = $newLine;
3233
			$newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3234
			if ( $newLine != $prev ) {
3235
				return true;
3236
			}
3237
		}
3238
3239
		return false;
3240
	}
3241
3242
	/**
3243
	 * Database independent variable replacement. Replaces a set of variables
3244
	 * in an SQL statement with their contents as given by $this->getSchemaVars().
3245
	 *
3246
	 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3247
	 *
3248
	 * - '{$var}' should be used for text and is passed through the database's
3249
	 *   addQuotes method.
3250
	 * - `{$var}` should be used for identifiers (e.g. table and database names).
3251
	 *   It is passed through the database's addIdentifierQuotes method which
3252
	 *   can be overridden if the database uses something other than backticks.
3253
	 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3254
	 *   database's tableName method.
3255
	 * - / *i* / passes the name that follows through the database's indexName method.
3256
	 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3257
	 *   its use should be avoided. In 1.24 and older, string encoding was applied.
3258
	 *
3259
	 * @param string $ins SQL statement to replace variables in
3260
	 * @return string The new SQL statement with variables replaced
3261
	 */
3262
	protected function replaceVars( $ins ) {
3263
		$vars = $this->getSchemaVars();
3264
		return preg_replace_callback(
3265
			'!
3266
				/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3267
				\'\{\$ (\w+) }\'                  | # 3. addQuotes
3268
				`\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
3269
				/\*\$ (\w+) \*/                     # 5. leave unencoded
3270
			!x',
3271
			function ( $m ) use ( $vars ) {
3272
				// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3273
				// check for both nonexistent keys *and* the empty string.
3274
				if ( isset( $m[1] ) && $m[1] !== '' ) {
3275
					if ( $m[1] === 'i' ) {
3276
						return $this->indexName( $m[2] );
3277
					} else {
3278
						return $this->tableName( $m[2] );
3279
					}
3280 View Code Duplication
				} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3281
					return $this->addQuotes( $vars[$m[3]] );
3282
				} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3283
					return $this->addIdentifierQuotes( $vars[$m[4]] );
3284 View Code Duplication
				} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3285
					return $vars[$m[5]];
3286
				} else {
3287
					return $m[0];
3288
				}
3289
			},
3290
			$ins
3291
		);
3292
	}
3293
3294
	/**
3295
	 * Get schema variables. If none have been set via setSchemaVars(), then
3296
	 * use some defaults from the current object.
3297
	 *
3298
	 * @return array
3299
	 */
3300
	protected function getSchemaVars() {
3301
		if ( $this->mSchemaVars ) {
3302
			return $this->mSchemaVars;
3303
		} else {
3304
			return $this->getDefaultSchemaVars();
3305
		}
3306
	}
3307
3308
	/**
3309
	 * Get schema variables to use if none have been set via setSchemaVars().
3310
	 *
3311
	 * Override this in derived classes to provide variables for tables.sql
3312
	 * and SQL patch files.
3313
	 *
3314
	 * @return array
3315
	 */
3316
	protected function getDefaultSchemaVars() {
3317
		return [];
3318
	}
3319
3320
	public function lockIsFree( $lockName, $method ) {
3321
		return true;
3322
	}
3323
3324
	public function lock( $lockName, $method, $timeout = 5 ) {
3325
		$this->mNamedLocksHeld[$lockName] = 1;
3326
3327
		return true;
3328
	}
3329
3330
	public function unlock( $lockName, $method ) {
3331
		unset( $this->mNamedLocksHeld[$lockName] );
3332
3333
		return true;
3334
	}
3335
3336
	public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3337
		if ( $this->writesOrCallbacksPending() ) {
3338
			// This only flushes transactions to clear snapshots, not to write data
3339
			throw new DBUnexpectedError(
3340
				$this,
3341
				"$fname: Cannot COMMIT to clear snapshot because writes are pending."
3342
			);
3343
		}
3344
3345
		if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3346
			return null;
3347
		}
3348
3349
		$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3350
			if ( $this->trxLevel() ) {
3351
				// There is a good chance an exception was thrown, causing any early return
3352
				// from the caller. Let any error handler get a chance to issue rollback().
3353
				// If there isn't one, let the error bubble up and trigger server-side rollback.
3354
				$this->onTransactionResolution( function () use ( $lockKey, $fname ) {
3355
					$this->unlock( $lockKey, $fname );
3356
				} );
3357
			} else {
3358
				$this->unlock( $lockKey, $fname );
3359
			}
3360
		} );
3361
3362
		$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
3363
3364
		return $unlocker;
3365
	}
3366
3367
	public function namedLocksEnqueue() {
3368
		return false;
3369
	}
3370
3371
	/**
3372
	 * Lock specific tables
3373
	 *
3374
	 * @param array $read Array of tables to lock for read access
3375
	 * @param array $write Array of tables to lock for write access
3376
	 * @param string $method Name of caller
3377
	 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3378
	 * @return bool
3379
	 */
3380
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
3381
		return true;
3382
	}
3383
3384
	/**
3385
	 * Unlock specific tables
3386
	 *
3387
	 * @param string $method The caller
3388
	 * @return bool
3389
	 */
3390
	public function unlockTables( $method ) {
3391
		return true;
3392
	}
3393
3394
	/**
3395
	 * Delete a table
3396
	 * @param string $tableName
3397
	 * @param string $fName
3398
	 * @return bool|ResultWrapper
3399
	 * @since 1.18
3400
	 */
3401 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
3402
		if ( !$this->tableExists( $tableName, $fName ) ) {
3403
			return false;
3404
		}
3405
		$sql = "DROP TABLE " . $this->tableName( $tableName );
3406
		if ( $this->cascadingDeletes() ) {
3407
			$sql .= " CASCADE";
3408
		}
3409
3410
		return $this->query( $sql, $fName );
3411
	}
3412
3413
	/**
3414
	 * Get search engine class. All subclasses of this need to implement this
3415
	 * if they wish to use searching.
3416
	 *
3417
	 * @return string
3418
	 */
3419
	public function getSearchEngine() {
3420
		return 'SearchEngineDummy';
3421
	}
3422
3423
	public function getInfinity() {
3424
		return 'infinity';
3425
	}
3426
3427
	public function encodeExpiry( $expiry ) {
3428
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3429
			? $this->getInfinity()
3430
			: $this->timestamp( $expiry );
3431
	}
3432
3433
	public function decodeExpiry( $expiry, $format = TS_MW ) {
3434
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3435
			? 'infinity'
3436
			: wfTimestamp( $format, $expiry );
3437
	}
3438
3439
	public function setBigSelects( $value = true ) {
3440
		// no-op
3441
	}
3442
3443
	public function isReadOnly() {
3444
		return ( $this->getReadOnlyReason() !== false );
3445
	}
3446
3447
	/**
3448
	 * @return string|bool Reason this DB is read-only or false if it is not
3449
	 */
3450
	protected function getReadOnlyReason() {
3451
		$reason = $this->getLBInfo( 'readOnlyReason' );
3452
3453
		return is_string( $reason ) ? $reason : false;
3454
	}
3455
3456
	/**
3457
	 * @since 1.19
3458
	 * @return string
3459
	 */
3460
	public function __toString() {
3461
		return (string)$this->mConn;
3462
	}
3463
3464
	/**
3465
	 * Run a few simple sanity checks
3466
	 */
3467
	public function __destruct() {
3468
		if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3469
			trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3470
		}
3471
		$danglingCallbacks = array_merge(
3472
			$this->mTrxIdleCallbacks,
3473
			$this->mTrxPreCommitCallbacks,
3474
			$this->mTrxEndCallbacks
3475
		);
3476
		if ( $danglingCallbacks ) {
3477
			$callers = [];
3478
			foreach ( $danglingCallbacks as $callbackInfo ) {
3479
				$callers[] = $callbackInfo[1];
3480
			}
3481
			$callers = implode( ', ', $callers );
3482
			trigger_error( "DB transaction callbacks still pending (from $callers)." );
3483
		}
3484
	}
3485
}
3486
3487
/**
3488
 * @since 1.27
3489
 */
3490
abstract class Database extends DatabaseBase {
3491
	// B/C until nothing type hints for DatabaseBase
3492
	// @TODO: finish renaming DatabaseBase => Database
3493
}
3494