Completed
Branch master (098997)
by
unknown
28:44
created

Database::getSlavePos()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @defgroup Database Database
4
 *
5
 * This file deals with database interface functions
6
 * and query specifics/optimisations.
7
 *
8
 * This program is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License along
19
 * with this program; if not, write to the Free Software Foundation, Inc.,
20
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21
 * http://www.gnu.org/copyleft/gpl.html
22
 *
23
 * @file
24
 * @ingroup Database
25
 */
26
use Psr\Log\LoggerAwareInterface;
27
use Psr\Log\LoggerInterface;
28
29
/**
30
 * Relational database abstraction object
31
 *
32
 * @ingroup Database
33
 * @since 1.28
34
 */
35
abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAwareInterface {
36
	/** Number of times to re-try an operation in case of deadlock */
37
	const DEADLOCK_TRIES = 4;
38
	/** Minimum time to wait before retry, in microseconds */
39
	const DEADLOCK_DELAY_MIN = 500000;
40
	/** Maximum time to wait before retry */
41
	const DEADLOCK_DELAY_MAX = 1500000;
42
43
	/** How long before it is worth doing a dummy query to test the connection */
44
	const PING_TTL = 1.0;
45
	const PING_QUERY = 'SELECT 1 AS ping';
46
47
	const TINY_WRITE_SEC = .010;
48
	const SLOW_WRITE_SEC = .500;
49
	const SMALL_WRITE_ROWS = 100;
50
51
	/** @var string SQL query */
52
	protected $mLastQuery = '';
53
	/** @var bool */
54
	protected $mDoneWrites = false;
55
	/** @var string|bool */
56
	protected $mPHPError = false;
57
	/** @var string */
58
	protected $mServer;
59
	/** @var string */
60
	protected $mUser;
61
	/** @var string */
62
	protected $mPassword;
63
	/** @var string */
64
	protected $mDBname;
65
	/** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
66
	protected $tableAliases = [];
67
	/** @var bool Whether this PHP instance is for a CLI script */
68
	protected $cliMode;
69
	/** @var string Agent name for query profiling */
70
	protected $agent;
71
72
	/** @var BagOStuff APC cache */
73
	protected $srvCache;
74
	/** @var LoggerInterface */
75
	protected $connLogger;
76
	/** @var LoggerInterface */
77
	protected $queryLogger;
78
	/** @var callback Error logging callback */
79
	protected $errorLogger;
80
81
	/** @var resource Database connection */
82
	protected $mConn = null;
83
	/** @var bool */
84
	protected $mOpened = false;
85
86
	/** @var array[] List of (callable, method name) */
87
	protected $mTrxIdleCallbacks = [];
88
	/** @var array[] List of (callable, method name) */
89
	protected $mTrxPreCommitCallbacks = [];
90
	/** @var array[] List of (callable, method name) */
91
	protected $mTrxEndCallbacks = [];
92
	/** @var callable[] Map of (name => callable) */
93
	protected $mTrxRecurringCallbacks = [];
94
	/** @var bool Whether to suppress triggering of transaction end callbacks */
95
	protected $mTrxEndCallbacksSuppressed = false;
96
97
	/** @var string */
98
	protected $mTablePrefix = '';
99
	/** @var string */
100
	protected $mSchema = '';
101
	/** @var integer */
102
	protected $mFlags;
103
	/** @var array */
104
	protected $mLBInfo = [];
105
	/** @var bool|null */
106
	protected $mDefaultBigSelects = null;
107
	/** @var array|bool */
108
	protected $mSchemaVars = false;
109
	/** @var array */
110
	protected $mSessionVars = [];
111
	/** @var array|null */
112
	protected $preparedArgs;
113
	/** @var string|bool|null Stashed value of html_errors INI setting */
114
	protected $htmlErrors;
115
	/** @var string */
116
	protected $delimiter = ';';
117
	/** @var DatabaseDomain */
118
	protected $currentDomain;
119
120
	/**
121
	 * Either 1 if a transaction is active or 0 otherwise.
122
	 * The other Trx fields may not be meaningfull if this is 0.
123
	 *
124
	 * @var int
125
	 */
126
	protected $mTrxLevel = 0;
127
	/**
128
	 * Either a short hexidecimal string if a transaction is active or ""
129
	 *
130
	 * @var string
131
	 * @see DatabaseBase::mTrxLevel
132
	 */
133
	protected $mTrxShortId = '';
134
	/**
135
	 * The UNIX time that the transaction started. Callers can assume that if
136
	 * snapshot isolation is used, then the data is *at least* up to date to that
137
	 * point (possibly more up-to-date since the first SELECT defines the snapshot).
138
	 *
139
	 * @var float|null
140
	 * @see DatabaseBase::mTrxLevel
141
	 */
142
	private $mTrxTimestamp = null;
143
	/** @var float Lag estimate at the time of BEGIN */
144
	private $mTrxReplicaLag = null;
145
	/**
146
	 * Remembers the function name given for starting the most recent transaction via begin().
147
	 * Used to provide additional context for error reporting.
148
	 *
149
	 * @var string
150
	 * @see DatabaseBase::mTrxLevel
151
	 */
152
	private $mTrxFname = null;
153
	/**
154
	 * Record if possible write queries were done in the last transaction started
155
	 *
156
	 * @var bool
157
	 * @see DatabaseBase::mTrxLevel
158
	 */
159
	private $mTrxDoneWrites = false;
160
	/**
161
	 * Record if the current transaction was started implicitly due to DBO_TRX being set.
162
	 *
163
	 * @var bool
164
	 * @see DatabaseBase::mTrxLevel
165
	 */
166
	private $mTrxAutomatic = false;
167
	/**
168
	 * Array of levels of atomicity within transactions
169
	 *
170
	 * @var array
171
	 */
172
	private $mTrxAtomicLevels = [];
173
	/**
174
	 * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
175
	 *
176
	 * @var bool
177
	 */
178
	private $mTrxAutomaticAtomic = false;
179
	/**
180
	 * Track the write query callers of the current transaction
181
	 *
182
	 * @var string[]
183
	 */
184
	private $mTrxWriteCallers = [];
185
	/**
186
	 * @var float Seconds spent in write queries for the current transaction
187
	 */
188
	private $mTrxWriteDuration = 0.0;
189
	/**
190
	 * @var integer Number of write queries for the current transaction
191
	 */
192
	private $mTrxWriteQueryCount = 0;
193
	/**
194
	 * @var float Like mTrxWriteQueryCount but excludes lock-bound, easy to replicate, queries
195
	 */
196
	private $mTrxWriteAdjDuration = 0.0;
197
	/**
198
	 * @var integer Number of write queries counted in mTrxWriteAdjDuration
199
	 */
200
	private $mTrxWriteAdjQueryCount = 0;
201
	/**
202
	 * @var float RTT time estimate
203
	 */
204
	private $mRTTEstimate = 0.0;
205
206
	/** @var array Map of (name => 1) for locks obtained via lock() */
207
	private $mNamedLocksHeld = [];
208
	/** @var array Map of (table name => 1) for TEMPORARY tables */
209
	protected $mSessionTempTables = [];
210
211
	/** @var IDatabase|null Lazy handle to the master DB this server replicates from */
212
	private $lazyMasterHandle;
213
214
	/** @var float UNIX timestamp */
215
	protected $lastPing = 0.0;
216
217
	/** @var int[] Prior mFlags values */
218
	private $priorFlags = [];
219
220
	/** @var object|string Class name or object With profileIn/profileOut methods */
221
	protected $profiler;
222
	/** @var TransactionProfiler */
223
	protected $trxProfiler;
224
225
	/**
226
	 * Constructor and database handle and attempt to connect to the DB server
227
	 *
228
	 * IDatabase classes should not be constructed directly in external
229
	 * code. Database::factory() should be used instead.
230
	 *
231
	 * @param array $params Parameters passed from Database::factory()
232
	 */
233
	function __construct( array $params ) {
234
		$server = $params['host'];
235
		$user = $params['user'];
236
		$password = $params['password'];
237
		$dbName = $params['dbname'];
238
239
		$this->mSchema = $params['schema'];
240
		$this->mTablePrefix = $params['tablePrefix'];
241
242
		$this->cliMode = $params['cliMode'];
243
		// Agent name is added to SQL queries in a comment, so make sure it can't break out
244
		$this->agent = str_replace( '/', '-', $params['agent'] );
245
246
		$this->mFlags = $params['flags'];
247
		if ( $this->mFlags & self::DBO_DEFAULT ) {
248
			if ( $this->cliMode ) {
249
				$this->mFlags &= ~self::DBO_TRX;
250
			} else {
251
				$this->mFlags |= self::DBO_TRX;
252
			}
253
		}
254
255
		$this->mSessionVars = $params['variables'];
256
257
		$this->srvCache = isset( $params['srvCache'] )
258
			? $params['srvCache']
259
			: new HashBagOStuff();
260
261
		$this->profiler = $params['profiler'];
262
		$this->trxProfiler = $params['trxProfiler'];
263
		$this->connLogger = $params['connLogger'];
264
		$this->queryLogger = $params['queryLogger'];
265
		$this->errorLogger = $params['errorLogger'];
266
267
		// Set initial dummy domain until open() sets the final DB/prefix
268
		$this->currentDomain = DatabaseDomain::newUnspecified();
269
270
		if ( $user ) {
271
			$this->open( $server, $user, $password, $dbName );
272
		} elseif ( $this->requiresDatabaseUser() ) {
273
			throw new InvalidArgumentException( "No database user provided." );
274
		}
275
276
		// Set the domain object after open() sets the relevant fields
277
		if ( $this->mDBname != '' ) {
278
			// Domains with server scope but a table prefix are not used by IDatabase classes
279
			$this->currentDomain = new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix );
280
		}
281
	}
282
283
	/**
284
	 * Construct a Database subclass instance given a database type and parameters
285
	 *
286
	 * This also connects to the database immediately upon object construction
287
	 *
288
	 * @param string $dbType A possible DB type (sqlite, mysql, postgres)
289
	 * @param array $p Parameter map with keys:
290
	 *   - host : The hostname of the DB server
291
	 *   - user : The name of the database user the client operates under
292
	 *   - password : The password for the database user
293
	 *   - dbname : The name of the database to use where queries do not specify one.
294
	 *      The database must exist or an error might be thrown. Setting this to the empty string
295
	 *      will avoid any such errors and make the handle have no implicit database scope. This is
296
	 *      useful for queries like SHOW STATUS, CREATE DATABASE, or DROP DATABASE. Note that a
297
	 *      "database" in Postgres is rougly equivalent to an entire MySQL server. This the domain
298
	 *      in which user names and such are defined, e.g. users are database-specific in Postgres.
299
	 *   - schema : The database schema to use (if supported). A "schema" in Postgres is roughly
300
	 *      equivalent to a "database" in MySQL. Note that MySQL and SQLite do not use schemas.
301
	 *   - tablePrefix : Optional table prefix that is implicitly added on to all table names
302
	 *      recognized in queries. This can be used in place of schemas for handle site farms.
303
	 *   - flags : Optional bitfield of DBO_* constants that define connection, protocol,
304
	 *      buffering, and transaction behavior. It is STRONGLY adviced to leave the DBO_DEFAULT
305
	 *      flag in place UNLESS this this database simply acts as a key/value store.
306
	 *   - driver: Optional name of a specific DB client driver. For MySQL, there is the old
307
	 *      'mysql' driver and the newer 'mysqli' driver.
308
	 *   - variables: Optional map of session variables to set after connecting. This can be
309
	 *      used to adjust lock timeouts or encoding modes and the like.
310
	 *   - connLogger: Optional PSR-3 logger interface instance.
311
	 *   - queryLogger: Optional PSR-3 logger interface instance.
312
	 *   - profiler: Optional class name or object with profileIn()/profileOut() methods.
313
	 *      These will be called in query(), using a simplified version of the SQL that also
314
	 *      includes the agent as a SQL comment.
315
	 *   - trxProfiler: Optional TransactionProfiler instance.
316
	 *   - errorLogger: Optional callback that takes an Exception and logs it.
317
	 *   - cliMode: Whether to consider the execution context that of a CLI script.
318
	 *   - agent: Optional name used to identify the end-user in query profiling/logging.
319
	 *   - srvCache: Optional BagOStuff instance to an APC-style cache.
320
	 * @return Database|null If the database driver or extension cannot be found
321
	 * @throws InvalidArgumentException If the database driver or extension cannot be found
322
	 * @since 1.18
323
	 */
324
	final public static function factory( $dbType, $p = [] ) {
325
		static $canonicalDBTypes = [
326
			'mysql' => [ 'mysqli', 'mysql' ],
327
			'postgres' => [],
328
			'sqlite' => [],
329
			'oracle' => [],
330
			'mssql' => [],
331
		];
332
333
		$driver = false;
334
		$dbType = strtolower( $dbType );
335
		if ( isset( $canonicalDBTypes[$dbType] ) && $canonicalDBTypes[$dbType] ) {
336
			$possibleDrivers = $canonicalDBTypes[$dbType];
337
			if ( !empty( $p['driver'] ) ) {
338
				if ( in_array( $p['driver'], $possibleDrivers ) ) {
339
					$driver = $p['driver'];
340
				} else {
341
					throw new InvalidArgumentException( __METHOD__ .
342
						" type '$dbType' does not support driver '{$p['driver']}'" );
343
				}
344
			} else {
345
				foreach ( $possibleDrivers as $posDriver ) {
346
					if ( extension_loaded( $posDriver ) ) {
347
						$driver = $posDriver;
348
						break;
349
					}
350
				}
351
			}
352
		} else {
353
			$driver = $dbType;
354
		}
355
		if ( $driver === false ) {
356
			throw new InvalidArgumentException( __METHOD__ .
357
				" no viable database extension found for type '$dbType'" );
358
		}
359
360
		$class = 'Database' . ucfirst( $driver );
361
		if ( class_exists( $class ) && is_subclass_of( $class, 'IDatabase' ) ) {
362
			// Resolve some defaults for b/c
363
			$p['host'] = isset( $p['host'] ) ? $p['host'] : false;
364
			$p['user'] = isset( $p['user'] ) ? $p['user'] : false;
365
			$p['password'] = isset( $p['password'] ) ? $p['password'] : false;
366
			$p['dbname'] = isset( $p['dbname'] ) ? $p['dbname'] : false;
367
			$p['flags'] = isset( $p['flags'] ) ? $p['flags'] : 0;
368
			$p['variables'] = isset( $p['variables'] ) ? $p['variables'] : [];
369
			$p['tablePrefix'] = isset( $p['tablePrefix'] ) ? $p['tablePrefix'] : '';
370
			$p['schema'] = isset( $p['schema'] ) ? $p['schema'] : '';
371
			$p['cliMode'] = isset( $p['cliMode'] ) ? $p['cliMode'] : ( PHP_SAPI === 'cli' );
372
			$p['agent'] = isset( $p['agent'] ) ? $p['agent'] : '';
373
			if ( !isset( $p['connLogger'] ) ) {
374
				$p['connLogger'] = new \Psr\Log\NullLogger();
375
			}
376
			if ( !isset( $p['queryLogger'] ) ) {
377
				$p['queryLogger'] = new \Psr\Log\NullLogger();
378
			}
379
			$p['profiler'] = isset( $p['profiler'] ) ? $p['profiler'] : null;
380
			if ( !isset( $p['trxProfiler'] ) ) {
381
				$p['trxProfiler'] = new TransactionProfiler();
382
			}
383
			if ( !isset( $p['errorLogger'] ) ) {
384
				$p['errorLogger'] = function ( Exception $e ) {
385
					trigger_error( get_class( $e ) . ': ' . $e->getMessage(), E_WARNING );
386
				};
387
			}
388
389
			$conn = new $class( $p );
390
		} else {
391
			$conn = null;
392
		}
393
394
		return $conn;
395
	}
396
397
	public function setLogger( LoggerInterface $logger ) {
398
		$this->queryLogger = $logger;
399
	}
400
401
	public function getServerInfo() {
402
		return $this->getServerVersion();
403
	}
404
405 View Code Duplication
	public function bufferResults( $buffer = null ) {
406
		$res = !$this->getFlag( self::DBO_NOBUFFER );
407
		if ( $buffer !== null ) {
408
			$buffer
409
				? $this->clearFlag( self::DBO_NOBUFFER )
410
				: $this->setFlag( self::DBO_NOBUFFER );
411
		}
412
413
		return $res;
414
	}
415
416
	/**
417
	 * Turns on (false) or off (true) the automatic generation and sending
418
	 * of a "we're sorry, but there has been a database error" page on
419
	 * database errors. Default is on (false). When turned off, the
420
	 * code should use lastErrno() and lastError() to handle the
421
	 * situation as appropriate.
422
	 *
423
	 * Do not use this function outside of the Database classes.
424
	 *
425
	 * @param null|bool $ignoreErrors
426
	 * @return bool The previous value of the flag.
427
	 */
428 View Code Duplication
	protected function ignoreErrors( $ignoreErrors = null ) {
429
		$res = $this->getFlag( self::DBO_IGNORE );
430
		if ( $ignoreErrors !== null ) {
431
			$ignoreErrors
432
				? $this->setFlag( self::DBO_IGNORE )
433
				: $this->clearFlag( self::DBO_IGNORE );
434
		}
435
436
		return $res;
437
	}
438
439
	public function trxLevel() {
440
		return $this->mTrxLevel;
441
	}
442
443
	public function trxTimestamp() {
444
		return $this->mTrxLevel ? $this->mTrxTimestamp : null;
445
	}
446
447
	public function tablePrefix( $prefix = null ) {
448
		$old = $this->mTablePrefix;
449
		if ( $prefix !== null ) {
450
			$this->mTablePrefix = $prefix;
451
			$this->currentDomain = ( $this->mDBname != '' )
452
				? new DatabaseDomain( $this->mDBname, null, $this->mTablePrefix )
453
				: DatabaseDomain::newUnspecified();
454
		}
455
456
		return $old;
457
	}
458
459
	public function dbSchema( $schema = null ) {
460
		$old = $this->mSchema;
461
		if ( $schema !== null ) {
462
			$this->mSchema = $schema;
463
		}
464
465
		return $old;
466
	}
467
468
	public function getLBInfo( $name = null ) {
469
		if ( is_null( $name ) ) {
470
			return $this->mLBInfo;
471
		} else {
472
			if ( array_key_exists( $name, $this->mLBInfo ) ) {
473
				return $this->mLBInfo[$name];
474
			} else {
475
				return null;
476
			}
477
		}
478
	}
479
480
	public function setLBInfo( $name, $value = null ) {
481
		if ( is_null( $value ) ) {
482
			$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...
483
		} else {
484
			$this->mLBInfo[$name] = $value;
485
		}
486
	}
487
488
	public function setLazyMasterHandle( IDatabase $conn ) {
489
		$this->lazyMasterHandle = $conn;
490
	}
491
492
	/**
493
	 * @return IDatabase|null
494
	 * @see setLazyMasterHandle()
495
	 * @since 1.27
496
	 */
497
	protected function getLazyMasterHandle() {
498
		return $this->lazyMasterHandle;
499
	}
500
501
	public function implicitGroupby() {
502
		return true;
503
	}
504
505
	public function implicitOrderby() {
506
		return true;
507
	}
508
509
	public function lastQuery() {
510
		return $this->mLastQuery;
511
	}
512
513
	public function doneWrites() {
514
		return (bool)$this->mDoneWrites;
515
	}
516
517
	public function lastDoneWrites() {
518
		return $this->mDoneWrites ?: false;
519
	}
520
521
	public function writesPending() {
522
		return $this->mTrxLevel && $this->mTrxDoneWrites;
523
	}
524
525
	public function writesOrCallbacksPending() {
526
		return $this->mTrxLevel && (
527
			$this->mTrxDoneWrites || $this->mTrxIdleCallbacks || $this->mTrxPreCommitCallbacks
528
		);
529
	}
530
531
	public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
532
		if ( !$this->mTrxLevel ) {
533
			return false;
534
		} elseif ( !$this->mTrxDoneWrites ) {
535
			return 0.0;
536
		}
537
538
		switch ( $type ) {
539
			case self::ESTIMATE_DB_APPLY:
540
				$this->ping( $rtt );
541
				$rttAdjTotal = $this->mTrxWriteAdjQueryCount * $rtt;
542
				$applyTime = max( $this->mTrxWriteAdjDuration - $rttAdjTotal, 0 );
543
				// For omitted queries, make them count as something at least
544
				$omitted = $this->mTrxWriteQueryCount - $this->mTrxWriteAdjQueryCount;
545
				$applyTime += self::TINY_WRITE_SEC * $omitted;
546
547
				return $applyTime;
548
			default: // everything
549
				return $this->mTrxWriteDuration;
550
		}
551
	}
552
553
	public function pendingWriteCallers() {
554
		return $this->mTrxLevel ? $this->mTrxWriteCallers : [];
555
	}
556
557
	protected function pendingWriteAndCallbackCallers() {
558
		if ( !$this->mTrxLevel ) {
559
			return [];
560
		}
561
562
		$fnames = $this->mTrxWriteCallers;
563
		foreach ( [
564
			$this->mTrxIdleCallbacks,
565
			$this->mTrxPreCommitCallbacks,
566
			$this->mTrxEndCallbacks
567
		] as $callbacks ) {
568
			foreach ( $callbacks as $callback ) {
569
				$fnames[] = $callback[1];
570
			}
571
		}
572
573
		return $fnames;
574
	}
575
576
	public function isOpen() {
577
		return $this->mOpened;
578
	}
579
580
	public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
581
		if ( $remember === self::REMEMBER_PRIOR ) {
582
			array_push( $this->priorFlags, $this->mFlags );
583
		}
584
		$this->mFlags |= $flag;
585
	}
586
587
	public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
588
		if ( $remember === self::REMEMBER_PRIOR ) {
589
			array_push( $this->priorFlags, $this->mFlags );
590
		}
591
		$this->mFlags &= ~$flag;
592
	}
593
594
	public function restoreFlags( $state = self::RESTORE_PRIOR ) {
595
		if ( !$this->priorFlags ) {
596
			return;
597
		}
598
599
		if ( $state === self::RESTORE_INITIAL ) {
600
			$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...
601
			$this->priorFlags = [];
602
		} else {
603
			$this->mFlags = array_pop( $this->priorFlags );
604
		}
605
	}
606
607
	public function getFlag( $flag ) {
608
		return !!( $this->mFlags & $flag );
609
	}
610
611
	public function getProperty( $name ) {
612
		return $this->$name;
613
	}
614
615
	public function getDomainID() {
616
		return $this->currentDomain->getId();
617
	}
618
619
	final public function getWikiID() {
620
		return $this->getDomainID();
621
	}
622
623
	/**
624
	 * Get information about an index into an object
625
	 * @param string $table Table name
626
	 * @param string $index Index name
627
	 * @param string $fname Calling function name
628
	 * @return mixed Database-specific index description class or false if the index does not exist
629
	 */
630
	abstract function indexInfo( $table, $index, $fname = __METHOD__ );
631
632
	/**
633
	 * Wrapper for addslashes()
634
	 *
635
	 * @param string $s String to be slashed.
636
	 * @return string Slashed string.
637
	 */
638
	abstract function strencode( $s );
639
640
	protected function installErrorHandler() {
641
		$this->mPHPError = false;
642
		$this->htmlErrors = ini_set( 'html_errors', '0' );
643
		set_error_handler( [ $this, 'connectionErrorLogger' ] );
644
	}
645
646
	/**
647
	 * @return bool|string
648
	 */
649
	protected function restoreErrorHandler() {
650
		restore_error_handler();
651
		if ( $this->htmlErrors !== false ) {
652
			ini_set( 'html_errors', $this->htmlErrors );
653
		}
654
		if ( $this->mPHPError ) {
655
			$error = preg_replace( '!\[<a.*</a>\]!', '', $this->mPHPError );
656
			$error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
657
658
			return $error;
659
		} else {
660
			return false;
661
		}
662
	}
663
664
	/**
665
	 * This method should not be used outside of Database classes
666
	 *
667
	 * @param int $errno
668
	 * @param string $errstr
669
	 */
670
	public function connectionErrorLogger( $errno, $errstr ) {
671
		$this->mPHPError = $errstr;
672
	}
673
674
	/**
675
	 * Create a log context to pass to PSR-3 logger functions.
676
	 *
677
	 * @param array $extras Additional data to add to context
678
	 * @return array
679
	 */
680
	protected function getLogContext( array $extras = [] ) {
681
		return array_merge(
682
			[
683
				'db_server' => $this->mServer,
684
				'db_name' => $this->mDBname,
685
				'db_user' => $this->mUser,
686
			],
687
			$extras
688
		);
689
	}
690
691
	public function close() {
692
		if ( $this->mConn ) {
693
			if ( $this->trxLevel() ) {
694
				$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
695
			}
696
697
			$closed = $this->closeConnection();
698
			$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...
699
		} elseif ( $this->mTrxIdleCallbacks || $this->mTrxEndCallbacks ) { // sanity
700
			throw new RuntimeException( "Transaction callbacks still pending." );
701
		} else {
702
			$closed = true;
703
		}
704
		$this->mOpened = false;
705
706
		return $closed;
707
	}
708
709
	/**
710
	 * Make sure isOpen() returns true as a sanity check
711
	 *
712
	 * @throws DBUnexpectedError
713
	 */
714
	protected function assertOpen() {
715
		if ( !$this->isOpen() ) {
716
			throw new DBUnexpectedError( $this, "DB connection was already closed." );
717
		}
718
	}
719
720
	/**
721
	 * Closes underlying database connection
722
	 * @since 1.20
723
	 * @return bool Whether connection was closed successfully
724
	 */
725
	abstract protected function closeConnection();
726
727
	public function reportConnectionError( $error = 'Unknown error' ) {
728
		$myError = $this->lastError();
729
		if ( $myError ) {
730
			$error = $myError;
731
		}
732
733
		# New method
734
		throw new DBConnectionError( $this, $error );
735
	}
736
737
	/**
738
	 * The DBMS-dependent part of query()
739
	 *
740
	 * @param string $sql SQL query.
741
	 * @return ResultWrapper|bool Result object to feed to fetchObject,
742
	 *   fetchRow, ...; or false on failure
743
	 */
744
	abstract protected function doQuery( $sql );
745
746
	/**
747
	 * Determine whether a query writes to the DB.
748
	 * Should return true if unsure.
749
	 *
750
	 * @param string $sql
751
	 * @return bool
752
	 */
753
	protected function isWriteQuery( $sql ) {
754
		return !preg_match(
755
			'/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
756
	}
757
758
	/**
759
	 * @param $sql
760
	 * @return string|null
761
	 */
762
	protected function getQueryVerb( $sql ) {
763
		return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
764
	}
765
766
	/**
767
	 * Determine whether a SQL statement is sensitive to isolation level.
768
	 * A SQL statement is considered transactable if its result could vary
769
	 * depending on the transaction isolation level. Operational commands
770
	 * such as 'SET' and 'SHOW' are not considered to be transactable.
771
	 *
772
	 * @param string $sql
773
	 * @return bool
774
	 */
775
	protected function isTransactableQuery( $sql ) {
776
		$verb = $this->getQueryVerb( $sql );
777
		return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
778
	}
779
780
	/**
781
	 * @param string $sql A SQL query
782
	 * @return bool Whether $sql is SQL for creating/dropping a new TEMPORARY table
783
	 */
784
	protected function registerTempTableOperation( $sql ) {
785
		if ( preg_match(
786
			'/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
787
			$sql,
788
			$matches
789
		) ) {
790
			$this->mSessionTempTables[$matches[1]] = 1;
791
792
			return true;
793
		} elseif ( preg_match(
794
			'/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
795
			$sql,
796
			$matches
797
		) ) {
798
			unset( $this->mSessionTempTables[$matches[1]] );
799
800
			return true;
801
		} elseif ( preg_match(
802
			'/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
803
			$sql,
804
			$matches
805
		) ) {
806
			return isset( $this->mSessionTempTables[$matches[1]] );
807
		}
808
809
		return false;
810
	}
811
812
	public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
813
		$priorWritesPending = $this->writesOrCallbacksPending();
814
		$this->mLastQuery = $sql;
815
816
		$isWrite = $this->isWriteQuery( $sql ) && !$this->registerTempTableOperation( $sql );
817
		if ( $isWrite ) {
818
			$reason = $this->getReadOnlyReason();
819
			if ( $reason !== false ) {
820
				throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
821
			}
822
			# Set a flag indicating that writes have been done
823
			$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...
824
		}
825
826
		// Add trace comment to the begin of the sql string, right after the operator.
827
		// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (bug 42598)
828
		$commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
829
830
		# Start implicit transactions that wrap the request if DBO_TRX is enabled
831
		if ( !$this->mTrxLevel && $this->getFlag( self::DBO_TRX )
832
			&& $this->isTransactableQuery( $sql )
833
		) {
834
			$this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
835
			$this->mTrxAutomatic = true;
836
		}
837
838
		# Keep track of whether the transaction has write queries pending
839
		if ( $this->mTrxLevel && !$this->mTrxDoneWrites && $isWrite ) {
840
			$this->mTrxDoneWrites = true;
841
			$this->trxProfiler->transactionWritingIn(
842
				$this->mServer, $this->mDBname, $this->mTrxShortId );
843
		}
844
845
		if ( $this->getFlag( self::DBO_DEBUG ) ) {
846
			$this->queryLogger->debug( "{$this->mDBname} {$commentedSql}" );
847
		}
848
849
		# Avoid fatals if close() was called
850
		$this->assertOpen();
851
852
		# Send the query to the server
853
		$ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
854
855
		# Try reconnecting if the connection was lost
856
		if ( false === $ret && $this->wasErrorReissuable() ) {
857
			$recoverable = $this->canRecoverFromDisconnect( $sql, $priorWritesPending );
858
			# Stash the last error values before anything might clear them
859
			$lastError = $this->lastError();
860
			$lastErrno = $this->lastErrno();
861
			# Update state tracking to reflect transaction loss due to disconnection
862
			$this->handleSessionLoss();
863
			if ( $this->reconnect() ) {
864
				$msg = __METHOD__ . ": lost connection to {$this->getServer()}; reconnected";
865
				$this->connLogger->warning( $msg );
866
				$this->queryLogger->warning(
867
					"$msg:\n" . ( new RuntimeException() )->getTraceAsString() );
868
869
				if ( !$recoverable ) {
870
					# Callers may catch the exception and continue to use the DB
871
					$this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
872
				} else {
873
					# Should be safe to silently retry the query
874
					$ret = $this->doProfiledQuery( $sql, $commentedSql, $isWrite, $fname );
875
				}
876
			} else {
877
				$msg = __METHOD__ . ": lost connection to {$this->getServer()} permanently";
878
				$this->connLogger->error( $msg );
879
			}
880
		}
881
882
		if ( false === $ret ) {
883
			# Deadlocks cause the entire transaction to abort, not just the statement.
884
			# http://dev.mysql.com/doc/refman/5.7/en/innodb-error-handling.html
885
			# https://www.postgresql.org/docs/9.1/static/explicit-locking.html
886
			if ( $this->wasDeadlock() ) {
887
				if ( $this->explicitTrxActive() || $priorWritesPending ) {
888
					$tempIgnore = false; // not recoverable
889
				}
890
				# Update state tracking to reflect transaction loss
891
				$this->handleSessionLoss();
892
			}
893
894
			$this->reportQueryError(
895
				$this->lastError(), $this->lastErrno(), $sql, $fname, $tempIgnore );
896
		}
897
898
		$res = $this->resultObject( $ret );
899
900
		return $res;
901
	}
902
903
	private function doProfiledQuery( $sql, $commentedSql, $isWrite, $fname ) {
904
		$isMaster = !is_null( $this->getLBInfo( 'master' ) );
905
		# generalizeSQL() will probably cut down the query to reasonable
906
		# logging size most of the time. The substr is really just a sanity check.
907
		if ( $isMaster ) {
908
			$queryProf = 'query-m: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
909
		} else {
910
			$queryProf = 'query: ' . substr( self::generalizeSQL( $sql ), 0, 255 );
911
		}
912
913
		# Include query transaction state
914
		$queryProf .= $this->mTrxShortId ? " [TRX#{$this->mTrxShortId}]" : "";
915
916
		$startTime = microtime( true );
917
		if ( $this->profiler ) {
918
			call_user_func( [ $this->profiler, 'profileIn' ], $queryProf );
919
		}
920
		$ret = $this->doQuery( $commentedSql );
921
		if ( $this->profiler ) {
922
			call_user_func( [ $this->profiler, 'profileOut' ], $queryProf );
923
		}
924
		$queryRuntime = max( microtime( true ) - $startTime, 0.0 );
925
926
		unset( $queryProfSection ); // profile out (if set)
927
928
		if ( $ret !== false ) {
929
			$this->lastPing = $startTime;
930
			if ( $isWrite && $this->mTrxLevel ) {
931
				$this->updateTrxWriteQueryTime( $sql, $queryRuntime );
932
				$this->mTrxWriteCallers[] = $fname;
933
			}
934
		}
935
936
		if ( $sql === self::PING_QUERY ) {
937
			$this->mRTTEstimate = $queryRuntime;
938
		}
939
940
		$this->trxProfiler->recordQueryCompletion(
941
			$queryProf, $startTime, $isWrite, $this->affectedRows()
942
		);
943
		$this->queryLogger->debug( $sql, [
944
			'method' => $fname,
945
			'master' => $isMaster,
946
			'runtime' => $queryRuntime,
947
		] );
948
949
		return $ret;
950
	}
951
952
	/**
953
	 * Update the estimated run-time of a query, not counting large row lock times
954
	 *
955
	 * LoadBalancer can be set to rollback transactions that will create huge replication
956
	 * lag. It bases this estimate off of pendingWriteQueryDuration(). Certain simple
957
	 * queries, like inserting a row can take a long time due to row locking. This method
958
	 * uses some simple heuristics to discount those cases.
959
	 *
960
	 * @param string $sql A SQL write query
961
	 * @param float $runtime Total runtime, including RTT
962
	 */
963
	private function updateTrxWriteQueryTime( $sql, $runtime ) {
964
		// Whether this is indicative of replica DB runtime (except for RBR or ws_repl)
965
		$indicativeOfReplicaRuntime = true;
966
		if ( $runtime > self::SLOW_WRITE_SEC ) {
967
			$verb = $this->getQueryVerb( $sql );
968
			// insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
969
			if ( $verb === 'INSERT' ) {
970
				$indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
971
			} elseif ( $verb === 'REPLACE' ) {
972
				$indicativeOfReplicaRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
973
			}
974
		}
975
976
		$this->mTrxWriteDuration += $runtime;
977
		$this->mTrxWriteQueryCount += 1;
978
		if ( $indicativeOfReplicaRuntime ) {
979
			$this->mTrxWriteAdjDuration += $runtime;
980
			$this->mTrxWriteAdjQueryCount += 1;
981
		}
982
	}
983
984
	private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
985
		# Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
986
		# Dropped connections also mean that named locks are automatically released.
987
		# Only allow error suppression in autocommit mode or when the lost transaction
988
		# didn't matter anyway (aside from DBO_TRX snapshot loss).
989
		if ( $this->mNamedLocksHeld ) {
990
			return false; // possible critical section violation
991
		} elseif ( $sql === 'COMMIT' ) {
992
			return !$priorWritesPending; // nothing written anyway? (T127428)
993
		} elseif ( $sql === 'ROLLBACK' ) {
994
			return true; // transaction lost...which is also what was requested :)
995
		} elseif ( $this->explicitTrxActive() ) {
996
			return false; // don't drop atomocity
997
		} elseif ( $priorWritesPending ) {
998
			return false; // prior writes lost from implicit transaction
999
		}
1000
1001
		return true;
1002
	}
1003
1004
	private function handleSessionLoss() {
1005
		$this->mTrxLevel = 0;
1006
		$this->mTrxIdleCallbacks = []; // bug 65263
1007
		$this->mTrxPreCommitCallbacks = []; // bug 65263
1008
		$this->mSessionTempTables = [];
1009
		$this->mNamedLocksHeld = [];
1010
		try {
1011
			// Handle callbacks in mTrxEndCallbacks
1012
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
1013
			$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
1014
			return null;
1015
		} catch ( Exception $e ) {
1016
			// Already logged; move on...
1017
			return $e;
1018
		}
1019
	}
1020
1021
	public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
1022
		if ( $this->ignoreErrors() || $tempIgnore ) {
1023
			$this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
1024
		} else {
1025
			$sql1line = mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 );
1026
			$this->queryLogger->error(
1027
				"{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1028
				$this->getLogContext( [
1029
					'method' => __METHOD__,
1030
					'errno' => $errno,
1031
					'error' => $error,
1032
					'sql1line' => $sql1line,
1033
					'fname' => $fname,
1034
				] )
1035
			);
1036
			$this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
1037
			throw new DBQueryError( $this, $error, $errno, $sql, $fname );
1038
		}
1039
	}
1040
1041
	public function freeResult( $res ) {
1042
	}
1043
1044
	public function selectField(
1045
		$table, $var, $cond = '', $fname = __METHOD__, $options = []
1046
	) {
1047
		if ( $var === '*' ) { // sanity
1048
			throw new DBUnexpectedError( $this, "Cannot use a * field: got '$var'" );
1049
		}
1050
1051
		if ( !is_array( $options ) ) {
1052
			$options = [ $options ];
1053
		}
1054
1055
		$options['LIMIT'] = 1;
1056
1057
		$res = $this->select( $table, $var, $cond, $fname, $options );
1058
		if ( $res === false || !$this->numRows( $res ) ) {
1059
			return false;
1060
		}
1061
1062
		$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 1057 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...
1063
1064
		if ( $row !== false ) {
1065
			return reset( $row );
1066
		} else {
1067
			return false;
1068
		}
1069
	}
1070
1071
	public function selectFieldValues(
1072
		$table, $var, $cond = '', $fname = __METHOD__, $options = [], $join_conds = []
1073
	) {
1074
		if ( $var === '*' ) { // sanity
1075
			throw new DBUnexpectedError( $this, "Cannot use a * field" );
1076
		} elseif ( !is_string( $var ) ) { // sanity
1077
			throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1078
		}
1079
1080
		if ( !is_array( $options ) ) {
1081
			$options = [ $options ];
1082
		}
1083
1084
		$res = $this->select( $table, $var, $cond, $fname, $options, $join_conds );
1085
		if ( $res === false ) {
1086
			return false;
1087
		}
1088
1089
		$values = [];
1090
		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...
1091
			$values[] = $row->$var;
1092
		}
1093
1094
		return $values;
1095
	}
1096
1097
	/**
1098
	 * Returns an optional USE INDEX clause to go after the table, and a
1099
	 * string to go at the end of the query.
1100
	 *
1101
	 * @param array $options Associative array of options to be turned into
1102
	 *   an SQL query, valid keys are listed in the function.
1103
	 * @return array
1104
	 * @see DatabaseBase::select()
1105
	 */
1106
	protected function makeSelectOptions( $options ) {
1107
		$preLimitTail = $postLimitTail = '';
1108
		$startOpts = '';
1109
1110
		$noKeyOptions = [];
1111
1112
		foreach ( $options as $key => $option ) {
1113
			if ( is_numeric( $key ) ) {
1114
				$noKeyOptions[$option] = true;
1115
			}
1116
		}
1117
1118
		$preLimitTail .= $this->makeGroupByWithHaving( $options );
1119
1120
		$preLimitTail .= $this->makeOrderBy( $options );
1121
1122
		// if (isset($options['LIMIT'])) {
1123
		// 	$tailOpts .= $this->limitResult('', $options['LIMIT'],
1124
		// 		isset($options['OFFSET']) ? $options['OFFSET']
1125
		// 		: false);
1126
		// }
1127
1128
		if ( isset( $noKeyOptions['FOR UPDATE'] ) ) {
1129
			$postLimitTail .= ' FOR UPDATE';
1130
		}
1131
1132
		if ( isset( $noKeyOptions['LOCK IN SHARE MODE'] ) ) {
1133
			$postLimitTail .= ' LOCK IN SHARE MODE';
1134
		}
1135
1136
		if ( isset( $noKeyOptions['DISTINCT'] ) || isset( $noKeyOptions['DISTINCTROW'] ) ) {
1137
			$startOpts .= 'DISTINCT';
1138
		}
1139
1140
		# Various MySQL extensions
1141
		if ( isset( $noKeyOptions['STRAIGHT_JOIN'] ) ) {
1142
			$startOpts .= ' /*! STRAIGHT_JOIN */';
1143
		}
1144
1145
		if ( isset( $noKeyOptions['HIGH_PRIORITY'] ) ) {
1146
			$startOpts .= ' HIGH_PRIORITY';
1147
		}
1148
1149
		if ( isset( $noKeyOptions['SQL_BIG_RESULT'] ) ) {
1150
			$startOpts .= ' SQL_BIG_RESULT';
1151
		}
1152
1153
		if ( isset( $noKeyOptions['SQL_BUFFER_RESULT'] ) ) {
1154
			$startOpts .= ' SQL_BUFFER_RESULT';
1155
		}
1156
1157
		if ( isset( $noKeyOptions['SQL_SMALL_RESULT'] ) ) {
1158
			$startOpts .= ' SQL_SMALL_RESULT';
1159
		}
1160
1161
		if ( isset( $noKeyOptions['SQL_CALC_FOUND_ROWS'] ) ) {
1162
			$startOpts .= ' SQL_CALC_FOUND_ROWS';
1163
		}
1164
1165
		if ( isset( $noKeyOptions['SQL_CACHE'] ) ) {
1166
			$startOpts .= ' SQL_CACHE';
1167
		}
1168
1169
		if ( isset( $noKeyOptions['SQL_NO_CACHE'] ) ) {
1170
			$startOpts .= ' SQL_NO_CACHE';
1171
		}
1172
1173 View Code Duplication
		if ( isset( $options['USE INDEX'] ) && is_string( $options['USE INDEX'] ) ) {
1174
			$useIndex = $this->useIndexClause( $options['USE INDEX'] );
1175
		} else {
1176
			$useIndex = '';
1177
		}
1178 View Code Duplication
		if ( isset( $options['IGNORE INDEX'] ) && is_string( $options['IGNORE INDEX'] ) ) {
1179
			$ignoreIndex = $this->ignoreIndexClause( $options['IGNORE INDEX'] );
1180
		} else {
1181
			$ignoreIndex = '';
1182
		}
1183
1184
		return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1185
	}
1186
1187
	/**
1188
	 * Returns an optional GROUP BY with an optional HAVING
1189
	 *
1190
	 * @param array $options Associative array of options
1191
	 * @return string
1192
	 * @see DatabaseBase::select()
1193
	 * @since 1.21
1194
	 */
1195
	protected function makeGroupByWithHaving( $options ) {
1196
		$sql = '';
1197 View Code Duplication
		if ( isset( $options['GROUP BY'] ) ) {
1198
			$gb = is_array( $options['GROUP BY'] )
1199
				? implode( ',', $options['GROUP BY'] )
1200
				: $options['GROUP BY'];
1201
			$sql .= ' GROUP BY ' . $gb;
1202
		}
1203 View Code Duplication
		if ( isset( $options['HAVING'] ) ) {
1204
			$having = is_array( $options['HAVING'] )
1205
				? $this->makeList( $options['HAVING'], self::LIST_AND )
1206
				: $options['HAVING'];
1207
			$sql .= ' HAVING ' . $having;
1208
		}
1209
1210
		return $sql;
1211
	}
1212
1213
	/**
1214
	 * Returns an optional ORDER BY
1215
	 *
1216
	 * @param array $options Associative array of options
1217
	 * @return string
1218
	 * @see DatabaseBase::select()
1219
	 * @since 1.21
1220
	 */
1221
	protected function makeOrderBy( $options ) {
1222 View Code Duplication
		if ( isset( $options['ORDER BY'] ) ) {
1223
			$ob = is_array( $options['ORDER BY'] )
1224
				? implode( ',', $options['ORDER BY'] )
1225
				: $options['ORDER BY'];
1226
1227
			return ' ORDER BY ' . $ob;
1228
		}
1229
1230
		return '';
1231
	}
1232
1233
	public function select( $table, $vars, $conds = '', $fname = __METHOD__,
1234
		$options = [], $join_conds = [] ) {
1235
		$sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1236
1237
		return $this->query( $sql, $fname );
1238
	}
1239
1240
	public function selectSQLText( $table, $vars, $conds = '', $fname = __METHOD__,
1241
		$options = [], $join_conds = []
1242
	) {
1243
		if ( is_array( $vars ) ) {
1244
			$vars = implode( ',', $this->fieldNamesWithAlias( $vars ) );
1245
		}
1246
1247
		$options = (array)$options;
1248
		$useIndexes = ( isset( $options['USE INDEX'] ) && is_array( $options['USE INDEX'] ) )
1249
			? $options['USE INDEX']
1250
			: [];
1251
		$ignoreIndexes = (
1252
			isset( $options['IGNORE INDEX'] ) &&
1253
			is_array( $options['IGNORE INDEX'] )
1254
		)
1255
			? $options['IGNORE INDEX']
1256
			: [];
1257
1258
		if ( is_array( $table ) ) {
1259
			$from = ' FROM ' .
1260
				$this->tableNamesWithIndexClauseOrJOIN(
1261
					$table, $useIndexes, $ignoreIndexes, $join_conds );
1262
		} elseif ( $table != '' ) {
1263
			if ( $table[0] == ' ' ) {
1264
				$from = ' FROM ' . $table;
1265
			} else {
1266
				$from = ' FROM ' .
1267
					$this->tableNamesWithIndexClauseOrJOIN(
1268
						[ $table ], $useIndexes, $ignoreIndexes, [] );
1269
			}
1270
		} else {
1271
			$from = '';
1272
		}
1273
1274
		list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1275
			$this->makeSelectOptions( $options );
1276
1277
		if ( !empty( $conds ) ) {
1278
			if ( is_array( $conds ) ) {
1279
				$conds = $this->makeList( $conds, self::LIST_AND );
1280
			}
1281
			$sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex " .
1282
				"WHERE $conds $preLimitTail";
1283
		} else {
1284
			$sql = "SELECT $startOpts $vars $from $useIndex $ignoreIndex $preLimitTail";
1285
		}
1286
1287
		if ( isset( $options['LIMIT'] ) ) {
1288
			$sql = $this->limitResult( $sql, $options['LIMIT'],
1289
				isset( $options['OFFSET'] ) ? $options['OFFSET'] : false );
1290
		}
1291
		$sql = "$sql $postLimitTail";
1292
1293
		if ( isset( $options['EXPLAIN'] ) ) {
1294
			$sql = 'EXPLAIN ' . $sql;
1295
		}
1296
1297
		return $sql;
1298
	}
1299
1300
	public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1301
		$options = [], $join_conds = []
1302
	) {
1303
		$options = (array)$options;
1304
		$options['LIMIT'] = 1;
1305
		$res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
1306
1307
		if ( $res === false ) {
1308
			return false;
1309
		}
1310
1311
		if ( !$this->numRows( $res ) ) {
1312
			return false;
1313
		}
1314
1315
		$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 1305 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...
1316
1317
		return $obj;
1318
	}
1319
1320
	public function estimateRowCount(
1321
		$table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
1322
	) {
1323
		$rows = 0;
1324
		$res = $this->select( $table, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options );
1325
1326 View Code Duplication
		if ( $res ) {
1327
			$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 1324 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...
1328
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1329
		}
1330
1331
		return $rows;
1332
	}
1333
1334
	public function selectRowCount(
1335
		$tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
1336
	) {
1337
		$rows = 0;
1338
		$sql = $this->selectSQLText( $tables, '1', $conds, $fname, $options, $join_conds );
1339
		$res = $this->query( "SELECT COUNT(*) AS rowcount FROM ($sql) tmp_count", $fname );
1340
1341 View Code Duplication
		if ( $res ) {
1342
			$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 1339 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...
1343
			$rows = ( isset( $row['rowcount'] ) ) ? (int)$row['rowcount'] : 0;
1344
		}
1345
1346
		return $rows;
1347
	}
1348
1349
	/**
1350
	 * Removes most variables from an SQL query and replaces them with X or N for numbers.
1351
	 * It's only slightly flawed. Don't use for anything important.
1352
	 *
1353
	 * @param string $sql A SQL Query
1354
	 *
1355
	 * @return string
1356
	 */
1357
	protected static function generalizeSQL( $sql ) {
1358
		# This does the same as the regexp below would do, but in such a way
1359
		# as to avoid crashing php on some large strings.
1360
		# $sql = preg_replace( "/'([^\\\\']|\\\\.)*'|\"([^\\\\\"]|\\\\.)*\"/", "'X'", $sql );
1361
1362
		$sql = str_replace( "\\\\", '', $sql );
1363
		$sql = str_replace( "\\'", '', $sql );
1364
		$sql = str_replace( "\\\"", '', $sql );
1365
		$sql = preg_replace( "/'.*'/s", "'X'", $sql );
1366
		$sql = preg_replace( '/".*"/s', "'X'", $sql );
1367
1368
		# All newlines, tabs, etc replaced by single space
1369
		$sql = preg_replace( '/\s+/', ' ', $sql );
1370
1371
		# All numbers => N,
1372
		# except the ones surrounded by characters, e.g. l10n
1373
		$sql = preg_replace( '/-?\d+(,-?\d+)+/s', 'N,...,N', $sql );
1374
		$sql = preg_replace( '/(?<![a-zA-Z])-?\d+(?![a-zA-Z])/s', 'N', $sql );
1375
1376
		return $sql;
1377
	}
1378
1379
	public function fieldExists( $table, $field, $fname = __METHOD__ ) {
1380
		$info = $this->fieldInfo( $table, $field );
1381
1382
		return (bool)$info;
1383
	}
1384
1385
	public function indexExists( $table, $index, $fname = __METHOD__ ) {
1386
		if ( !$this->tableExists( $table ) ) {
1387
			return null;
1388
		}
1389
1390
		$info = $this->indexInfo( $table, $index, $fname );
1391
		if ( is_null( $info ) ) {
1392
			return null;
1393
		} else {
1394
			return $info !== false;
1395
		}
1396
	}
1397
1398
	public function tableExists( $table, $fname = __METHOD__ ) {
1399
		$tableRaw = $this->tableName( $table, 'raw' );
1400
		if ( isset( $this->mSessionTempTables[$tableRaw] ) ) {
1401
			return true; // already known to exist
1402
		}
1403
1404
		$table = $this->tableName( $table );
1405
		$old = $this->ignoreErrors( true );
1406
		$res = $this->query( "SELECT 1 FROM $table LIMIT 1", $fname );
1407
		$this->ignoreErrors( $old );
1408
1409
		return (bool)$res;
1410
	}
1411
1412
	public function indexUnique( $table, $index ) {
1413
		$indexInfo = $this->indexInfo( $table, $index );
1414
1415
		if ( !$indexInfo ) {
1416
			return null;
1417
		}
1418
1419
		return !$indexInfo[0]->Non_unique;
1420
	}
1421
1422
	/**
1423
	 * Helper for DatabaseBase::insert().
1424
	 *
1425
	 * @param array $options
1426
	 * @return string
1427
	 */
1428
	protected function makeInsertOptions( $options ) {
1429
		return implode( ' ', $options );
1430
	}
1431
1432
	public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
1433
		# No rows to insert, easy just return now
1434
		if ( !count( $a ) ) {
1435
			return true;
1436
		}
1437
1438
		$table = $this->tableName( $table );
1439
1440
		if ( !is_array( $options ) ) {
1441
			$options = [ $options ];
1442
		}
1443
1444
		$fh = null;
1445
		if ( isset( $options['fileHandle'] ) ) {
1446
			$fh = $options['fileHandle'];
1447
		}
1448
		$options = $this->makeInsertOptions( $options );
1449
1450
		if ( isset( $a[0] ) && is_array( $a[0] ) ) {
1451
			$multi = true;
1452
			$keys = array_keys( $a[0] );
1453
		} else {
1454
			$multi = false;
1455
			$keys = array_keys( $a );
1456
		}
1457
1458
		$sql = 'INSERT ' . $options .
1459
			" INTO $table (" . implode( ',', $keys ) . ') VALUES ';
1460
1461
		if ( $multi ) {
1462
			$first = true;
1463 View Code Duplication
			foreach ( $a as $row ) {
1464
				if ( $first ) {
1465
					$first = false;
1466
				} else {
1467
					$sql .= ',';
1468
				}
1469
				$sql .= '(' . $this->makeList( $row ) . ')';
1470
			}
1471
		} else {
1472
			$sql .= '(' . $this->makeList( $a ) . ')';
1473
		}
1474
1475
		if ( $fh !== null && false === fwrite( $fh, $sql ) ) {
1476
			return false;
1477
		} elseif ( $fh !== null ) {
1478
			return true;
1479
		}
1480
1481
		return (bool)$this->query( $sql, $fname );
1482
	}
1483
1484
	/**
1485
	 * Make UPDATE options array for DatabaseBase::makeUpdateOptions
1486
	 *
1487
	 * @param array $options
1488
	 * @return array
1489
	 */
1490
	protected function makeUpdateOptionsArray( $options ) {
1491
		if ( !is_array( $options ) ) {
1492
			$options = [ $options ];
1493
		}
1494
1495
		$opts = [];
1496
1497
		if ( in_array( 'IGNORE', $options ) ) {
1498
			$opts[] = 'IGNORE';
1499
		}
1500
1501
		return $opts;
1502
	}
1503
1504
	/**
1505
	 * Make UPDATE options for the DatabaseBase::update function
1506
	 *
1507
	 * @param array $options The options passed to DatabaseBase::update
1508
	 * @return string
1509
	 */
1510
	protected function makeUpdateOptions( $options ) {
1511
		$opts = $this->makeUpdateOptionsArray( $options );
1512
1513
		return implode( ' ', $opts );
1514
	}
1515
1516
	public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
1517
		$table = $this->tableName( $table );
1518
		$opts = $this->makeUpdateOptions( $options );
1519
		$sql = "UPDATE $opts $table SET " . $this->makeList( $values, self::LIST_SET );
1520
1521
		if ( $conds !== [] && $conds !== '*' ) {
1522
			$sql .= " WHERE " . $this->makeList( $conds, self::LIST_AND );
1523
		}
1524
1525
		return $this->query( $sql, $fname );
1526
	}
1527
1528
	public function makeList( $a, $mode = self::LIST_COMMA ) {
1529
		if ( !is_array( $a ) ) {
1530
			throw new DBUnexpectedError( $this, __METHOD__ . ' called with incorrect parameters' );
1531
		}
1532
1533
		$first = true;
1534
		$list = '';
1535
1536
		foreach ( $a as $field => $value ) {
1537
			if ( !$first ) {
1538
				if ( $mode == self::LIST_AND ) {
1539
					$list .= ' AND ';
1540
				} elseif ( $mode == self::LIST_OR ) {
1541
					$list .= ' OR ';
1542
				} else {
1543
					$list .= ',';
1544
				}
1545
			} else {
1546
				$first = false;
1547
			}
1548
1549
			if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
1550
				$list .= "($value)";
1551
			} elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
1552
				$list .= "$value";
1553
			} elseif (
1554
				( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
1555
			) {
1556
				// Remove null from array to be handled separately if found
1557
				$includeNull = false;
1558
				foreach ( array_keys( $value, null, true ) as $nullKey ) {
1559
					$includeNull = true;
1560
					unset( $value[$nullKey] );
1561
				}
1562
				if ( count( $value ) == 0 && !$includeNull ) {
1563
					throw new InvalidArgumentException(
1564
						__METHOD__ . ": empty input for field $field" );
1565
				} elseif ( count( $value ) == 0 ) {
1566
					// only check if $field is null
1567
					$list .= "$field IS NULL";
1568
				} else {
1569
					// IN clause contains at least one valid element
1570
					if ( $includeNull ) {
1571
						// Group subconditions to ensure correct precedence
1572
						$list .= '(';
1573
					}
1574
					if ( count( $value ) == 1 ) {
1575
						// Special-case single values, as IN isn't terribly efficient
1576
						// Don't necessarily assume the single key is 0; we don't
1577
						// enforce linear numeric ordering on other arrays here.
1578
						$value = array_values( $value )[0];
1579
						$list .= $field . " = " . $this->addQuotes( $value );
1580
					} else {
1581
						$list .= $field . " IN (" . $this->makeList( $value ) . ") ";
1582
					}
1583
					// if null present in array, append IS NULL
1584
					if ( $includeNull ) {
1585
						$list .= " OR $field IS NULL)";
1586
					}
1587
				}
1588
			} elseif ( $value === null ) {
1589 View Code Duplication
				if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
1590
					$list .= "$field IS ";
1591
				} elseif ( $mode == self::LIST_SET ) {
1592
					$list .= "$field = ";
1593
				}
1594
				$list .= 'NULL';
1595
			} else {
1596 View Code Duplication
				if (
1597
					$mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
1598
				) {
1599
					$list .= "$field = ";
1600
				}
1601
				$list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
1602
			}
1603
		}
1604
1605
		return $list;
1606
	}
1607
1608
	public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
1609
		$conds = [];
1610
1611
		foreach ( $data as $base => $sub ) {
1612
			if ( count( $sub ) ) {
1613
				$conds[] = $this->makeList(
1614
					[ $baseKey => $base, $subKey => array_keys( $sub ) ],
1615
					self::LIST_AND );
1616
			}
1617
		}
1618
1619
		if ( $conds ) {
1620
			return $this->makeList( $conds, self::LIST_OR );
1621
		} else {
1622
			// Nothing to search for...
1623
			return false;
1624
		}
1625
	}
1626
1627
	public function aggregateValue( $valuedata, $valuename = 'value' ) {
1628
		return $valuename;
1629
	}
1630
1631
	public function bitNot( $field ) {
1632
		return "(~$field)";
1633
	}
1634
1635
	public function bitAnd( $fieldLeft, $fieldRight ) {
1636
		return "($fieldLeft & $fieldRight)";
1637
	}
1638
1639
	public function bitOr( $fieldLeft, $fieldRight ) {
1640
		return "($fieldLeft | $fieldRight)";
1641
	}
1642
1643
	public function buildConcat( $stringList ) {
1644
		return 'CONCAT(' . implode( ',', $stringList ) . ')';
1645
	}
1646
1647 View Code Duplication
	public function buildGroupConcatField(
1648
		$delim, $table, $field, $conds = '', $join_conds = []
1649
	) {
1650
		$fld = "GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) . ')';
1651
1652
		return '(' . $this->selectSQLText( $table, $fld, $conds, null, [], $join_conds ) . ')';
1653
	}
1654
1655
	public function buildStringCast( $field ) {
1656
		return $field;
1657
	}
1658
1659
	public function selectDB( $db ) {
1660
		# Stub. Shouldn't cause serious problems if it's not overridden, but
1661
		# if your database engine supports a concept similar to MySQL's
1662
		# databases you may as well.
1663
		$this->mDBname = $db;
1664
1665
		return true;
1666
	}
1667
1668
	public function getDBname() {
1669
		return $this->mDBname;
1670
	}
1671
1672
	public function getServer() {
1673
		return $this->mServer;
1674
	}
1675
1676
	public function tableName( $name, $format = 'quoted' ) {
1677
		# Skip the entire process when we have a string quoted on both ends.
1678
		# Note that we check the end so that we will still quote any use of
1679
		# use of `database`.table. But won't break things if someone wants
1680
		# to query a database table with a dot in the name.
1681
		if ( $this->isQuotedIdentifier( $name ) ) {
1682
			return $name;
1683
		}
1684
1685
		# Lets test for any bits of text that should never show up in a table
1686
		# name. Basically anything like JOIN or ON which are actually part of
1687
		# SQL queries, but may end up inside of the table value to combine
1688
		# sql. Such as how the API is doing.
1689
		# Note that we use a whitespace test rather than a \b test to avoid
1690
		# any remote case where a word like on may be inside of a table name
1691
		# surrounded by symbols which may be considered word breaks.
1692
		if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
1693
			return $name;
1694
		}
1695
1696
		# Split database and table into proper variables.
1697
		# We reverse the explode so that database.table and table both output
1698
		# the correct table.
1699
		$dbDetails = explode( '.', $name, 3 );
1700
		if ( count( $dbDetails ) == 3 ) {
1701
			list( $database, $schema, $table ) = $dbDetails;
1702
			# We don't want any prefix added in this case
1703
			$prefix = '';
1704
		} elseif ( count( $dbDetails ) == 2 ) {
1705
			list( $database, $table ) = $dbDetails;
1706
			# We don't want any prefix added in this case
1707
			# In dbs that support it, $database may actually be the schema
1708
			# but that doesn't affect any of the functionality here
1709
			$prefix = '';
1710
			$schema = '';
1711
		} else {
1712
			list( $table ) = $dbDetails;
1713
			if ( isset( $this->tableAliases[$table] ) ) {
1714
				$database = $this->tableAliases[$table]['dbname'];
1715
				$schema = is_string( $this->tableAliases[$table]['schema'] )
1716
					? $this->tableAliases[$table]['schema']
1717
					: $this->mSchema;
1718
				$prefix = is_string( $this->tableAliases[$table]['prefix'] )
1719
					? $this->tableAliases[$table]['prefix']
1720
					: $this->mTablePrefix;
1721
			} else {
1722
				$database = '';
1723
				$schema = $this->mSchema; # Default schema
1724
				$prefix = $this->mTablePrefix; # Default prefix
1725
			}
1726
		}
1727
1728
		# Quote $table and apply the prefix if not quoted.
1729
		# $tableName might be empty if this is called from Database::replaceVars()
1730
		$tableName = "{$prefix}{$table}";
1731
		if ( $format == 'quoted'
1732
			&& !$this->isQuotedIdentifier( $tableName ) && $tableName !== ''
1733
		) {
1734
			$tableName = $this->addIdentifierQuotes( $tableName );
1735
		}
1736
1737
		# Quote $schema and merge it with the table name if needed
1738 View Code Duplication
		if ( strlen( $schema ) ) {
1739
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $schema ) ) {
1740
				$schema = $this->addIdentifierQuotes( $schema );
1741
			}
1742
			$tableName = $schema . '.' . $tableName;
1743
		}
1744
1745
		# Quote $database and merge it with the table name if needed
1746 View Code Duplication
		if ( $database !== '' ) {
1747
			if ( $format == 'quoted' && !$this->isQuotedIdentifier( $database ) ) {
1748
				$database = $this->addIdentifierQuotes( $database );
1749
			}
1750
			$tableName = $database . '.' . $tableName;
1751
		}
1752
1753
		return $tableName;
1754
	}
1755
1756 View Code Duplication
	public function tableNames() {
1757
		$inArray = func_get_args();
1758
		$retVal = [];
1759
1760
		foreach ( $inArray as $name ) {
1761
			$retVal[$name] = $this->tableName( $name );
1762
		}
1763
1764
		return $retVal;
1765
	}
1766
1767 View Code Duplication
	public function tableNamesN() {
1768
		$inArray = func_get_args();
1769
		$retVal = [];
1770
1771
		foreach ( $inArray as $name ) {
1772
			$retVal[] = $this->tableName( $name );
1773
		}
1774
1775
		return $retVal;
1776
	}
1777
1778
	/**
1779
	 * Get an aliased table name
1780
	 * e.g. tableName AS newTableName
1781
	 *
1782
	 * @param string $name Table name, see tableName()
1783
	 * @param string|bool $alias Alias (optional)
1784
	 * @return string SQL name for aliased table. Will not alias a table to its own name
1785
	 */
1786
	protected function tableNameWithAlias( $name, $alias = false ) {
1787
		if ( !$alias || $alias == $name ) {
1788
			return $this->tableName( $name );
1789
		} else {
1790
			return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1786 can also be of type boolean; however, Database::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...
1791
		}
1792
	}
1793
1794
	/**
1795
	 * Gets an array of aliased table names
1796
	 *
1797
	 * @param array $tables [ [alias] => table ]
1798
	 * @return string[] See tableNameWithAlias()
1799
	 */
1800
	protected function tableNamesWithAlias( $tables ) {
1801
		$retval = [];
1802
		foreach ( $tables as $alias => $table ) {
1803
			if ( is_numeric( $alias ) ) {
1804
				$alias = $table;
1805
			}
1806
			$retval[] = $this->tableNameWithAlias( $table, $alias );
1807
		}
1808
1809
		return $retval;
1810
	}
1811
1812
	/**
1813
	 * Get an aliased field name
1814
	 * e.g. fieldName AS newFieldName
1815
	 *
1816
	 * @param string $name Field name
1817
	 * @param string|bool $alias Alias (optional)
1818
	 * @return string SQL name for aliased field. Will not alias a field to its own name
1819
	 */
1820
	protected function fieldNameWithAlias( $name, $alias = false ) {
1821
		if ( !$alias || (string)$alias === (string)$name ) {
1822
			return $name;
1823
		} else {
1824
			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 1820 can also be of type boolean; however, Database::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...
1825
		}
1826
	}
1827
1828
	/**
1829
	 * Gets an array of aliased field names
1830
	 *
1831
	 * @param array $fields [ [alias] => field ]
1832
	 * @return string[] See fieldNameWithAlias()
1833
	 */
1834
	protected function fieldNamesWithAlias( $fields ) {
1835
		$retval = [];
1836
		foreach ( $fields as $alias => $field ) {
1837
			if ( is_numeric( $alias ) ) {
1838
				$alias = $field;
1839
			}
1840
			$retval[] = $this->fieldNameWithAlias( $field, $alias );
1841
		}
1842
1843
		return $retval;
1844
	}
1845
1846
	/**
1847
	 * Get the aliased table name clause for a FROM clause
1848
	 * which might have a JOIN and/or USE INDEX or IGNORE INDEX clause
1849
	 *
1850
	 * @param array $tables ( [alias] => table )
1851
	 * @param array $use_index Same as for select()
1852
	 * @param array $ignore_index Same as for select()
1853
	 * @param array $join_conds Same as for select()
1854
	 * @return string
1855
	 */
1856
	protected function tableNamesWithIndexClauseOrJOIN(
1857
		$tables, $use_index = [], $ignore_index = [], $join_conds = []
1858
	) {
1859
		$ret = [];
1860
		$retJOIN = [];
1861
		$use_index = (array)$use_index;
1862
		$ignore_index = (array)$ignore_index;
1863
		$join_conds = (array)$join_conds;
1864
1865
		foreach ( $tables as $alias => $table ) {
1866
			if ( !is_string( $alias ) ) {
1867
				// No alias? Set it equal to the table name
1868
				$alias = $table;
1869
			}
1870
			// Is there a JOIN clause for this table?
1871
			if ( isset( $join_conds[$alias] ) ) {
1872
				list( $joinType, $conds ) = $join_conds[$alias];
1873
				$tableClause = $joinType;
1874
				$tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
1875 View Code Duplication
				if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
1876
					$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
1877
					if ( $use != '' ) {
1878
						$tableClause .= ' ' . $use;
1879
					}
1880
				}
1881 View Code Duplication
				if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
1882
					$ignore = $this->ignoreIndexClause(
1883
						implode( ',', (array)$ignore_index[$alias] ) );
1884
					if ( $ignore != '' ) {
1885
						$tableClause .= ' ' . $ignore;
1886
					}
1887
				}
1888
				$on = $this->makeList( (array)$conds, self::LIST_AND );
1889
				if ( $on != '' ) {
1890
					$tableClause .= ' ON (' . $on . ')';
1891
				}
1892
1893
				$retJOIN[] = $tableClause;
1894
			} elseif ( isset( $use_index[$alias] ) ) {
1895
				// Is there an INDEX clause for this table?
1896
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1897
				$tableClause .= ' ' . $this->useIndexClause(
1898
						implode( ',', (array)$use_index[$alias] )
1899
					);
1900
1901
				$ret[] = $tableClause;
1902
			} elseif ( isset( $ignore_index[$alias] ) ) {
1903
				// Is there an INDEX clause for this table?
1904
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1905
				$tableClause .= ' ' . $this->ignoreIndexClause(
1906
						implode( ',', (array)$ignore_index[$alias] )
1907
					);
1908
1909
				$ret[] = $tableClause;
1910
			} else {
1911
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1912
1913
				$ret[] = $tableClause;
1914
			}
1915
		}
1916
1917
		// We can't separate explicit JOIN clauses with ',', use ' ' for those
1918
		$implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
1919
		$explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
1920
1921
		// Compile our final table clause
1922
		return implode( ' ', [ $implicitJoins, $explicitJoins ] );
1923
	}
1924
1925
	/**
1926
	 * Get the name of an index in a given table.
1927
	 *
1928
	 * @param string $index
1929
	 * @return string
1930
	 */
1931
	protected function indexName( $index ) {
1932
		return $index;
1933
	}
1934
1935
	public function addQuotes( $s ) {
1936
		if ( $s instanceof Blob ) {
1937
			$s = $s->fetch();
1938
		}
1939
		if ( $s === null ) {
1940
			return 'NULL';
1941
		} elseif ( is_bool( $s ) ) {
1942
			return (int)$s;
1943
		} else {
1944
			# This will also quote numeric values. This should be harmless,
1945
			# and protects against weird problems that occur when they really
1946
			# _are_ strings such as article titles and string->number->string
1947
			# conversion is not 1:1.
1948
			return "'" . $this->strencode( $s ) . "'";
1949
		}
1950
	}
1951
1952
	/**
1953
	 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
1954
	 * MySQL uses `backticks` while basically everything else uses double quotes.
1955
	 * Since MySQL is the odd one out here the double quotes are our generic
1956
	 * and we implement backticks in DatabaseMysql.
1957
	 *
1958
	 * @param string $s
1959
	 * @return string
1960
	 */
1961
	public function addIdentifierQuotes( $s ) {
1962
		return '"' . str_replace( '"', '""', $s ) . '"';
1963
	}
1964
1965
	/**
1966
	 * Returns if the given identifier looks quoted or not according to
1967
	 * the database convention for quoting identifiers .
1968
	 *
1969
	 * @note Do not use this to determine if untrusted input is safe.
1970
	 *   A malicious user can trick this function.
1971
	 * @param string $name
1972
	 * @return bool
1973
	 */
1974
	public function isQuotedIdentifier( $name ) {
1975
		return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
1976
	}
1977
1978
	/**
1979
	 * @param string $s
1980
	 * @return string
1981
	 */
1982
	protected function escapeLikeInternal( $s ) {
1983
		return addcslashes( $s, '\%_' );
1984
	}
1985
1986
	public function buildLike() {
1987
		$params = func_get_args();
1988
1989
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
1990
			$params = $params[0];
1991
		}
1992
1993
		$s = '';
1994
1995
		foreach ( $params as $value ) {
1996
			if ( $value instanceof LikeMatch ) {
1997
				$s .= $value->toString();
1998
			} else {
1999
				$s .= $this->escapeLikeInternal( $value );
2000
			}
2001
		}
2002
2003
		return " LIKE {$this->addQuotes( $s )} ";
2004
	}
2005
2006
	public function anyChar() {
2007
		return new LikeMatch( '_' );
2008
	}
2009
2010
	public function anyString() {
2011
		return new LikeMatch( '%' );
2012
	}
2013
2014
	public function nextSequenceValue( $seqName ) {
2015
		return null;
2016
	}
2017
2018
	/**
2019
	 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2020
	 * is only needed because a) MySQL must be as efficient as possible due to
2021
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2022
	 * which index to pick. Anyway, other databases might have different
2023
	 * indexes on a given table. So don't bother overriding this unless you're
2024
	 * MySQL.
2025
	 * @param string $index
2026
	 * @return string
2027
	 */
2028
	public function useIndexClause( $index ) {
2029
		return '';
2030
	}
2031
2032
	/**
2033
	 * IGNORE INDEX clause. Unlikely to be useful for anything but MySQL. This
2034
	 * is only needed because a) MySQL must be as efficient as possible due to
2035
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2036
	 * which index to pick. Anyway, other databases might have different
2037
	 * indexes on a given table. So don't bother overriding this unless you're
2038
	 * MySQL.
2039
	 * @param string $index
2040
	 * @return string
2041
	 */
2042
	public function ignoreIndexClause( $index ) {
2043
		return '';
2044
	}
2045
2046
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2047
		$quotedTable = $this->tableName( $table );
2048
2049
		if ( count( $rows ) == 0 ) {
2050
			return;
2051
		}
2052
2053
		# Single row case
2054
		if ( !is_array( reset( $rows ) ) ) {
2055
			$rows = [ $rows ];
2056
		}
2057
2058
		// @FXIME: this is not atomic, but a trx would break affectedRows()
2059
		foreach ( $rows as $row ) {
2060
			# Delete rows which collide
2061
			if ( $uniqueIndexes ) {
2062
				$sql = "DELETE FROM $quotedTable WHERE ";
2063
				$first = true;
2064
				foreach ( $uniqueIndexes as $index ) {
2065
					if ( $first ) {
2066
						$first = false;
2067
						$sql .= '( ';
2068
					} else {
2069
						$sql .= ' ) OR ( ';
2070
					}
2071
					if ( is_array( $index ) ) {
2072
						$first2 = true;
2073
						foreach ( $index as $col ) {
2074
							if ( $first2 ) {
2075
								$first2 = false;
2076
							} else {
2077
								$sql .= ' AND ';
2078
							}
2079
							$sql .= $col . '=' . $this->addQuotes( $row[$col] );
2080
						}
2081
					} else {
2082
						$sql .= $index . '=' . $this->addQuotes( $row[$index] );
2083
					}
2084
				}
2085
				$sql .= ' )';
2086
				$this->query( $sql, $fname );
2087
			}
2088
2089
			# Now insert the row
2090
			$this->insert( $table, $row, $fname );
2091
		}
2092
	}
2093
2094
	/**
2095
	 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2096
	 * statement.
2097
	 *
2098
	 * @param string $table Table name
2099
	 * @param array|string $rows Row(s) to insert
2100
	 * @param string $fname Caller function name
2101
	 *
2102
	 * @return ResultWrapper
2103
	 */
2104
	protected function nativeReplace( $table, $rows, $fname ) {
2105
		$table = $this->tableName( $table );
2106
2107
		# Single row case
2108
		if ( !is_array( reset( $rows ) ) ) {
2109
			$rows = [ $rows ];
2110
		}
2111
2112
		$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2113
		$first = true;
2114
2115 View Code Duplication
		foreach ( $rows as $row ) {
2116
			if ( $first ) {
2117
				$first = false;
2118
			} else {
2119
				$sql .= ',';
2120
			}
2121
2122
			$sql .= '(' . $this->makeList( $row ) . ')';
2123
		}
2124
2125
		return $this->query( $sql, $fname );
2126
	}
2127
2128
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2129
		$fname = __METHOD__
2130
	) {
2131
		if ( !count( $rows ) ) {
2132
			return true; // nothing to do
2133
		}
2134
2135
		if ( !is_array( reset( $rows ) ) ) {
2136
			$rows = [ $rows ];
2137
		}
2138
2139
		if ( count( $uniqueIndexes ) ) {
2140
			$clauses = []; // list WHERE clauses that each identify a single row
2141
			foreach ( $rows as $row ) {
2142
				foreach ( $uniqueIndexes as $index ) {
2143
					$index = is_array( $index ) ? $index : [ $index ]; // columns
2144
					$rowKey = []; // unique key to this row
2145
					foreach ( $index as $column ) {
2146
						$rowKey[$column] = $row[$column];
2147
					}
2148
					$clauses[] = $this->makeList( $rowKey, self::LIST_AND );
2149
				}
2150
			}
2151
			$where = [ $this->makeList( $clauses, self::LIST_OR ) ];
2152
		} else {
2153
			$where = false;
2154
		}
2155
2156
		$useTrx = !$this->mTrxLevel;
2157
		if ( $useTrx ) {
2158
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2159
		}
2160
		try {
2161
			# Update any existing conflicting row(s)
2162
			if ( $where !== false ) {
2163
				$ok = $this->update( $table, $set, $where, $fname );
2164
			} else {
2165
				$ok = true;
2166
			}
2167
			# Now insert any non-conflicting row(s)
2168
			$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2169
		} catch ( Exception $e ) {
2170
			if ( $useTrx ) {
2171
				$this->rollback( $fname, self::FLUSHING_INTERNAL );
2172
			}
2173
			throw $e;
2174
		}
2175
		if ( $useTrx ) {
2176
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2177
		}
2178
2179
		return $ok;
2180
	}
2181
2182 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2183
		$fname = __METHOD__
2184
	) {
2185
		if ( !$conds ) {
2186
			throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
2187
		}
2188
2189
		$delTable = $this->tableName( $delTable );
2190
		$joinTable = $this->tableName( $joinTable );
2191
		$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2192
		if ( $conds != '*' ) {
2193
			$sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
2194
		}
2195
		$sql .= ')';
2196
2197
		$this->query( $sql, $fname );
2198
	}
2199
2200
	public function textFieldSize( $table, $field ) {
2201
		$table = $this->tableName( $table );
2202
		$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2203
		$res = $this->query( $sql, __METHOD__ );
2204
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql, __METHOD__) on line 2203 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...
2205
2206
		$m = [];
2207
2208
		if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2209
			$size = $m[1];
2210
		} else {
2211
			$size = -1;
2212
		}
2213
2214
		return $size;
2215
	}
2216
2217
	public function delete( $table, $conds, $fname = __METHOD__ ) {
2218
		if ( !$conds ) {
2219
			throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
2220
		}
2221
2222
		$table = $this->tableName( $table );
2223
		$sql = "DELETE FROM $table";
2224
2225 View Code Duplication
		if ( $conds != '*' ) {
2226
			if ( is_array( $conds ) ) {
2227
				$conds = $this->makeList( $conds, self::LIST_AND );
2228
			}
2229
			$sql .= ' WHERE ' . $conds;
2230
		}
2231
2232
		return $this->query( $sql, $fname );
2233
	}
2234
2235
	public function insertSelect(
2236
		$destTable, $srcTable, $varMap, $conds,
2237
		$fname = __METHOD__, $insertOptions = [], $selectOptions = []
2238
	) {
2239
		if ( $this->cliMode ) {
2240
			// For massive migrations with downtime, we don't want to select everything
2241
			// into memory and OOM, so do all this native on the server side if possible.
2242
			return $this->nativeInsertSelect(
2243
				$destTable,
2244
				$srcTable,
2245
				$varMap,
2246
				$conds,
2247
				$fname,
2248
				$insertOptions,
2249
				$selectOptions
2250
			);
2251
		}
2252
2253
		// For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
2254
		// on only the master (without needing row-based-replication). It also makes it easy to
2255
		// know how big the INSERT is going to be.
2256
		$fields = [];
2257
		foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
2258
			$fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
2259
		}
2260
		$selectOptions[] = 'FOR UPDATE';
2261
		$res = $this->select( $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions );
2262
		if ( !$res ) {
2263
			return false;
2264
		}
2265
2266
		$rows = [];
2267
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

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

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

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

Loading history...
2268
			$rows[] = (array)$row;
2269
		}
2270
2271
		return $this->insert( $destTable, $rows, $fname, $insertOptions );
2272
	}
2273
2274
	protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
2275
		$fname = __METHOD__,
2276
		$insertOptions = [], $selectOptions = []
2277
	) {
2278
		$destTable = $this->tableName( $destTable );
2279
2280
		if ( !is_array( $insertOptions ) ) {
2281
			$insertOptions = [ $insertOptions ];
2282
		}
2283
2284
		$insertOptions = $this->makeInsertOptions( $insertOptions );
2285
2286
		if ( !is_array( $selectOptions ) ) {
2287
			$selectOptions = [ $selectOptions ];
2288
		}
2289
2290
		list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions(
2291
			$selectOptions );
2292
2293 View Code Duplication
		if ( is_array( $srcTable ) ) {
2294
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2295
		} else {
2296
			$srcTable = $this->tableName( $srcTable );
2297
		}
2298
2299
		$sql = "INSERT $insertOptions" .
2300
			" INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2301
			" SELECT $startOpts " . implode( ',', $varMap ) .
2302
			" FROM $srcTable $useIndex $ignoreIndex ";
2303
2304 View Code Duplication
		if ( $conds != '*' ) {
2305
			if ( is_array( $conds ) ) {
2306
				$conds = $this->makeList( $conds, self::LIST_AND );
2307
			}
2308
			$sql .= " WHERE $conds";
2309
		}
2310
2311
		$sql .= " $tailOpts";
2312
2313
		return $this->query( $sql, $fname );
2314
	}
2315
2316
	/**
2317
	 * Construct a LIMIT query with optional offset. This is used for query
2318
	 * pages. The SQL should be adjusted so that only the first $limit rows
2319
	 * are returned. If $offset is provided as well, then the first $offset
2320
	 * rows should be discarded, and the next $limit rows should be returned.
2321
	 * If the result of the query is not ordered, then the rows to be returned
2322
	 * are theoretically arbitrary.
2323
	 *
2324
	 * $sql is expected to be a SELECT, if that makes a difference.
2325
	 *
2326
	 * The version provided by default works in MySQL and SQLite. It will very
2327
	 * likely need to be overridden for most other DBMSes.
2328
	 *
2329
	 * @param string $sql SQL query we will append the limit too
2330
	 * @param int $limit The SQL limit
2331
	 * @param int|bool $offset The SQL offset (default false)
2332
	 * @throws DBUnexpectedError
2333
	 * @return string
2334
	 */
2335
	public function limitResult( $sql, $limit, $offset = false ) {
2336
		if ( !is_numeric( $limit ) ) {
2337
			throw new DBUnexpectedError( $this,
2338
				"Invalid non-numeric limit passed to limitResult()\n" );
2339
		}
2340
2341
		return "$sql LIMIT "
2342
		. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2343
		. "{$limit} ";
2344
	}
2345
2346
	public function unionSupportsOrderAndLimit() {
2347
		return true; // True for almost every DB supported
2348
	}
2349
2350
	public function unionQueries( $sqls, $all ) {
2351
		$glue = $all ? ') UNION ALL (' : ') UNION (';
2352
2353
		return '(' . implode( $glue, $sqls ) . ')';
2354
	}
2355
2356
	public function conditional( $cond, $trueVal, $falseVal ) {
2357
		if ( is_array( $cond ) ) {
2358
			$cond = $this->makeList( $cond, self::LIST_AND );
2359
		}
2360
2361
		return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2362
	}
2363
2364
	public function strreplace( $orig, $old, $new ) {
2365
		return "REPLACE({$orig}, {$old}, {$new})";
2366
	}
2367
2368
	public function getServerUptime() {
2369
		return 0;
2370
	}
2371
2372
	public function wasDeadlock() {
2373
		return false;
2374
	}
2375
2376
	public function wasLockTimeout() {
2377
		return false;
2378
	}
2379
2380
	public function wasErrorReissuable() {
2381
		return false;
2382
	}
2383
2384
	public function wasReadOnlyError() {
2385
		return false;
2386
	}
2387
2388
	/**
2389
	 * Do not use this method outside of Database/DBError classes
2390
	 *
2391
	 * @param integer|string $errno
2392
	 * @return bool Whether the given query error was a connection drop
2393
	 */
2394
	public function wasConnectionError( $errno ) {
2395
		return false;
2396
	}
2397
2398
	public function deadlockLoop() {
2399
		$args = func_get_args();
2400
		$function = array_shift( $args );
2401
		$tries = self::DEADLOCK_TRIES;
2402
2403
		$this->begin( __METHOD__ );
2404
2405
		$retVal = null;
2406
		/** @var Exception $e */
2407
		$e = null;
2408
		do {
2409
			try {
2410
				$retVal = call_user_func_array( $function, $args );
2411
				break;
2412
			} catch ( DBQueryError $e ) {
2413
				if ( $this->wasDeadlock() ) {
2414
					// Retry after a randomized delay
2415
					usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2416
				} else {
2417
					// Throw the error back up
2418
					throw $e;
2419
				}
2420
			}
2421
		} while ( --$tries > 0 );
2422
2423
		if ( $tries <= 0 ) {
2424
			// Too many deadlocks; give up
2425
			$this->rollback( __METHOD__ );
2426
			throw $e;
2427
		} else {
2428
			$this->commit( __METHOD__ );
2429
2430
			return $retVal;
2431
		}
2432
	}
2433
2434
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
2435
		# Real waits are implemented in the subclass.
2436
		return 0;
2437
	}
2438
2439
	public function getReplicaPos() {
2440
		# Stub
2441
		return false;
2442
	}
2443
2444
	public function getMasterPos() {
2445
		# Stub
2446
		return false;
2447
	}
2448
2449
	public function serverIsReadOnly() {
2450
		return false;
2451
	}
2452
2453
	final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
2454
		if ( !$this->mTrxLevel ) {
2455
			throw new DBUnexpectedError( $this, "No transaction is active." );
2456
		}
2457
		$this->mTrxEndCallbacks[] = [ $callback, $fname ];
2458
	}
2459
2460
	final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
2461
		$this->mTrxIdleCallbacks[] = [ $callback, $fname ];
2462
		if ( !$this->mTrxLevel ) {
2463
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2464
		}
2465
	}
2466
2467
	final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
2468
		if ( $this->mTrxLevel ) {
2469
			$this->mTrxPreCommitCallbacks[] = [ $callback, $fname ];
2470
		} else {
2471
			// If no transaction is active, then make one for this callback
2472
			$this->startAtomic( __METHOD__ );
2473
			try {
2474
				call_user_func( $callback );
2475
				$this->endAtomic( __METHOD__ );
2476
			} catch ( Exception $e ) {
2477
				$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2478
				throw $e;
2479
			}
2480
		}
2481
	}
2482
2483
	final public function setTransactionListener( $name, callable $callback = null ) {
2484
		if ( $callback ) {
2485
			$this->mTrxRecurringCallbacks[$name] = $callback;
2486
		} else {
2487
			unset( $this->mTrxRecurringCallbacks[$name] );
2488
		}
2489
	}
2490
2491
	/**
2492
	 * Whether to disable running of post-COMMIT/ROLLBACK callbacks
2493
	 *
2494
	 * This method should not be used outside of Database/LoadBalancer
2495
	 *
2496
	 * @param bool $suppress
2497
	 * @since 1.28
2498
	 */
2499
	final public function setTrxEndCallbackSuppression( $suppress ) {
2500
		$this->mTrxEndCallbacksSuppressed = $suppress;
2501
	}
2502
2503
	/**
2504
	 * Actually run and consume any "on transaction idle/resolution" callbacks.
2505
	 *
2506
	 * This method should not be used outside of Database/LoadBalancer
2507
	 *
2508
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2509
	 * @since 1.20
2510
	 * @throws Exception
2511
	 */
2512
	public function runOnTransactionIdleCallbacks( $trigger ) {
2513
		if ( $this->mTrxEndCallbacksSuppressed ) {
2514
			return;
2515
		}
2516
2517
		$autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
2518
		/** @var Exception $e */
2519
		$e = null; // first exception
2520
		do { // callbacks may add callbacks :)
2521
			$callbacks = array_merge(
2522
				$this->mTrxIdleCallbacks,
2523
				$this->mTrxEndCallbacks // include "transaction resolution" callbacks
2524
			);
2525
			$this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2526
			$this->mTrxEndCallbacks = []; // consumed (recursion guard)
2527
			foreach ( $callbacks as $callback ) {
2528
				try {
2529
					list( $phpCallback ) = $callback;
2530
					$this->clearFlag( self::DBO_TRX ); // make each query its own transaction
2531
					call_user_func_array( $phpCallback, [ $trigger ] );
2532
					if ( $autoTrx ) {
2533
						$this->setFlag( self::DBO_TRX ); // restore automatic begin()
2534
					} else {
2535
						$this->clearFlag( self::DBO_TRX ); // restore auto-commit
2536
					}
2537
				} catch ( Exception $ex ) {
2538
					call_user_func( $this->errorLogger, $ex );
2539
					$e = $e ?: $ex;
2540
					// Some callbacks may use startAtomic/endAtomic, so make sure
2541
					// their transactions are ended so other callbacks don't fail
2542
					if ( $this->trxLevel() ) {
2543
						$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2544
					}
2545
				}
2546
			}
2547
		} while ( count( $this->mTrxIdleCallbacks ) );
2548
2549
		if ( $e instanceof Exception ) {
2550
			throw $e; // re-throw any first exception
2551
		}
2552
	}
2553
2554
	/**
2555
	 * Actually run and consume any "on transaction pre-commit" callbacks.
2556
	 *
2557
	 * This method should not be used outside of Database/LoadBalancer
2558
	 *
2559
	 * @since 1.22
2560
	 * @throws Exception
2561
	 */
2562
	public function runOnTransactionPreCommitCallbacks() {
2563
		$e = null; // first exception
2564
		do { // callbacks may add callbacks :)
2565
			$callbacks = $this->mTrxPreCommitCallbacks;
2566
			$this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2567
			foreach ( $callbacks as $callback ) {
2568
				try {
2569
					list( $phpCallback ) = $callback;
2570
					call_user_func( $phpCallback );
2571
				} catch ( Exception $ex ) {
2572
					call_user_func( $this->errorLogger, $ex );
2573
					$e = $e ?: $ex;
2574
				}
2575
			}
2576
		} while ( count( $this->mTrxPreCommitCallbacks ) );
2577
2578
		if ( $e instanceof Exception ) {
2579
			throw $e; // re-throw any first exception
2580
		}
2581
	}
2582
2583
	/**
2584
	 * Actually run any "transaction listener" callbacks.
2585
	 *
2586
	 * This method should not be used outside of Database/LoadBalancer
2587
	 *
2588
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2589
	 * @throws Exception
2590
	 * @since 1.20
2591
	 */
2592
	public function runTransactionListenerCallbacks( $trigger ) {
2593
		if ( $this->mTrxEndCallbacksSuppressed ) {
2594
			return;
2595
		}
2596
2597
		/** @var Exception $e */
2598
		$e = null; // first exception
2599
2600
		foreach ( $this->mTrxRecurringCallbacks as $phpCallback ) {
2601
			try {
2602
				$phpCallback( $trigger, $this );
2603
			} catch ( Exception $ex ) {
2604
				call_user_func( $this->errorLogger, $ex );
2605
				$e = $e ?: $ex;
2606
			}
2607
		}
2608
2609
		if ( $e instanceof Exception ) {
2610
			throw $e; // re-throw any first exception
2611
		}
2612
	}
2613
2614
	final public function startAtomic( $fname = __METHOD__ ) {
2615
		if ( !$this->mTrxLevel ) {
2616
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2617
			// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2618
			// in all changes being in one transaction to keep requests transactional.
2619
			if ( !$this->getFlag( self::DBO_TRX ) ) {
2620
				$this->mTrxAutomaticAtomic = true;
2621
			}
2622
		}
2623
2624
		$this->mTrxAtomicLevels[] = $fname;
2625
	}
2626
2627
	final public function endAtomic( $fname = __METHOD__ ) {
2628
		if ( !$this->mTrxLevel ) {
2629
			throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2630
		}
2631
		if ( !$this->mTrxAtomicLevels ||
2632
			array_pop( $this->mTrxAtomicLevels ) !== $fname
2633
		) {
2634
			throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2635
		}
2636
2637
		if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2638
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2639
		}
2640
	}
2641
2642
	final public function doAtomicSection( $fname, callable $callback ) {
2643
		$this->startAtomic( $fname );
2644
		try {
2645
			$res = call_user_func_array( $callback, [ $this, $fname ] );
2646
		} catch ( Exception $e ) {
2647
			$this->rollback( $fname, self::FLUSHING_INTERNAL );
2648
			throw $e;
2649
		}
2650
		$this->endAtomic( $fname );
2651
2652
		return $res;
2653
	}
2654
2655
	final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2656
		// Protect against mismatched atomic section, transaction nesting, and snapshot loss
2657
		if ( $this->mTrxLevel ) {
2658
			if ( $this->mTrxAtomicLevels ) {
2659
				$levels = implode( ', ', $this->mTrxAtomicLevels );
2660
				$msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2661
				throw new DBUnexpectedError( $this, $msg );
2662
			} elseif ( !$this->mTrxAutomatic ) {
2663
				$msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2664
				throw new DBUnexpectedError( $this, $msg );
2665
			} else {
2666
				// @TODO: make this an exception at some point
2667
				$msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2668
				$this->queryLogger->error( $msg );
2669
				return; // join the main transaction set
2670
			}
2671
		} elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2672
			// @TODO: make this an exception at some point
2673
			$msg = "$fname: Implicit transaction expected (DBO_TRX set).";
2674
			$this->queryLogger->error( $msg );
2675
			return; // let any writes be in the main transaction
2676
		}
2677
2678
		// Avoid fatals if close() was called
2679
		$this->assertOpen();
2680
2681
		$this->doBegin( $fname );
2682
		$this->mTrxTimestamp = microtime( true );
2683
		$this->mTrxFname = $fname;
2684
		$this->mTrxDoneWrites = false;
2685
		$this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
2686
		$this->mTrxAutomaticAtomic = false;
2687
		$this->mTrxAtomicLevels = [];
2688
		$this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
2689
		$this->mTrxWriteDuration = 0.0;
2690
		$this->mTrxWriteQueryCount = 0;
2691
		$this->mTrxWriteAdjDuration = 0.0;
2692
		$this->mTrxWriteAdjQueryCount = 0;
2693
		$this->mTrxWriteCallers = [];
2694
		// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2695
		// Get an estimate of the replica DB lag before then, treating estimate staleness
2696
		// as lag itself just to be safe
2697
		$status = $this->getApproximateLagStatus();
2698
		$this->mTrxReplicaLag = $status['lag'] + ( microtime( true ) - $status['since'] );
0 ignored issues
show
Documentation Bug introduced by
It seems like $status['lag'] + (microt...ue) - $status['since']) can also be of type integer. However, the property $mTrxReplicaLag is declared as type double. Maybe add an additional type check?

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

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

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

class Id
{
    public $id;

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

}

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

$account_id = false;

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

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