Completed
Branch master (174b3a)
by
unknown
26:51
created

Database::select()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

Loading history...
2930
	}
2931
2932
	/**
2933
	 * Lists all the VIEWs in the database
2934
	 *
2935
	 * For caching purposes the list of all views should be stored in
2936
	 * $this->allViews. The process cache can be cleared with clearViewsCache()
2937
	 *
2938
	 * @param string $prefix Only show VIEWs with this prefix, eg. unit_test_
2939
	 * @param string $fname Name of calling function
2940
	 * @throws RuntimeException
2941
	 * @return array
2942
	 * @since 1.22
2943
	 */
2944
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
2945
		throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
2946
	}
2947
2948
	/**
2949
	 * Differentiates between a TABLE and a VIEW
2950
	 *
2951
	 * @param string $name Name of the database-structure to test.
2952
	 * @throws RuntimeException
2953
	 * @return bool
2954
	 * @since 1.22
2955
	 */
2956
	public function isView( $name ) {
2957
		throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
2958
	}
2959
2960
	public function timestamp( $ts = 0 ) {
2961
		$t = new ConvertableTimestamp( $ts );
2962
		// Let errors bubble up to avoid putting garbage in the DB
2963
		return $t->getTimestamp( TS_MW );
2964
	}
2965
2966
	public function timestampOrNull( $ts = null ) {
2967
		if ( is_null( $ts ) ) {
2968
			return null;
2969
		} else {
2970
			return $this->timestamp( $ts );
2971
		}
2972
	}
2973
2974
	/**
2975
	 * Take the result from a query, and wrap it in a ResultWrapper if
2976
	 * necessary. Boolean values are passed through as is, to indicate success
2977
	 * of write queries or failure.
2978
	 *
2979
	 * Once upon a time, DatabaseBase::query() returned a bare MySQL result
2980
	 * resource, and it was necessary to call this function to convert it to
2981
	 * a wrapper. Nowadays, raw database objects are never exposed to external
2982
	 * callers, so this is unnecessary in external code.
2983
	 *
2984
	 * @param bool|ResultWrapper|resource|object $result
2985
	 * @return bool|ResultWrapper
2986
	 */
2987
	protected function resultObject( $result ) {
2988
		if ( !$result ) {
2989
			return false;
2990
		} elseif ( $result instanceof ResultWrapper ) {
2991
			return $result;
2992
		} elseif ( $result === true ) {
2993
			// Successful write query
2994
			return $result;
2995
		} else {
2996
			return new ResultWrapper( $this, $result );
2997
		}
2998
	}
2999
3000
	public function ping( &$rtt = null ) {
3001
		// Avoid hitting the server if it was hit recently
3002
		if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
3003
			if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
3004
				$rtt = $this->mRTTEstimate;
3005
				return true; // don't care about $rtt
3006
			}
3007
		}
3008
3009
		// This will reconnect if possible or return false if not
3010
		$this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
3011
		$ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
3012
		$this->restoreFlags( self::RESTORE_PRIOR );
3013
3014
		if ( $ok ) {
3015
			$rtt = $this->mRTTEstimate;
3016
		}
3017
3018
		return $ok;
3019
	}
3020
3021
	/**
3022
	 * @return bool
3023
	 */
3024
	protected function reconnect() {
3025
		$this->closeConnection();
3026
		$this->mOpened = false;
3027
		$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...
3028
		try {
3029
			$this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
3030
			$this->lastPing = microtime( true );
3031
			$ok = true;
3032
		} catch ( DBConnectionError $e ) {
3033
			$ok = false;
3034
		}
3035
3036
		return $ok;
3037
	}
3038
3039
	public function getSessionLagStatus() {
3040
		return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
3041
	}
3042
3043
	/**
3044
	 * Get the replica DB lag when the current transaction started
3045
	 *
3046
	 * This is useful when transactions might use snapshot isolation
3047
	 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
3048
	 * is this lag plus transaction duration. If they don't, it is still
3049
	 * safe to be pessimistic. This returns null if there is no transaction.
3050
	 *
3051
	 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
3052
	 * @since 1.27
3053
	 */
3054
	public function getTransactionLagStatus() {
3055
		return $this->mTrxLevel
3056
			? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
3057
			: null;
3058
	}
3059
3060
	/**
3061
	 * Get a replica DB lag estimate for this server
3062
	 *
3063
	 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
3064
	 * @since 1.27
3065
	 */
3066
	public function getApproximateLagStatus() {
3067
		return [
3068
			'lag'   => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
3069
			'since' => microtime( true )
3070
		];
3071
	}
3072
3073
	/**
3074
	 * Merge the result of getSessionLagStatus() for several DBs
3075
	 * using the most pessimistic values to estimate the lag of
3076
	 * any data derived from them in combination
3077
	 *
3078
	 * This is information is useful for caching modules
3079
	 *
3080
	 * @see WANObjectCache::set()
3081
	 * @see WANObjectCache::getWithSetCallback()
3082
	 *
3083
	 * @param IDatabase $db1
3084
	 * @param IDatabase ...
3085
	 * @return array Map of values:
3086
	 *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
3087
	 *   - since: oldest UNIX timestamp of any of the DB lag estimates
3088
	 *   - pending: whether any of the DBs have uncommitted changes
3089
	 * @since 1.27
3090
	 */
3091
	public static function getCacheSetOptions( IDatabase $db1 ) {
3092
		$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3093
		foreach ( func_get_args() as $db ) {
3094
			/** @var IDatabase $db */
3095
			$status = $db->getSessionLagStatus();
3096
			if ( $status['lag'] === false ) {
3097
				$res['lag'] = false;
3098
			} elseif ( $res['lag'] !== false ) {
3099
				$res['lag'] = max( $res['lag'], $status['lag'] );
3100
			}
3101
			$res['since'] = min( $res['since'], $status['since'] );
3102
			$res['pending'] = $res['pending'] ?: $db->writesPending();
3103
		}
3104
3105
		return $res;
3106
	}
3107
3108
	public function getLag() {
3109
		return 0;
3110
	}
3111
3112
	function maxListLen() {
3113
		return 0;
3114
	}
3115
3116
	public function encodeBlob( $b ) {
3117
		return $b;
3118
	}
3119
3120
	public function decodeBlob( $b ) {
3121
		if ( $b instanceof Blob ) {
3122
			$b = $b->fetch();
3123
		}
3124
		return $b;
3125
	}
3126
3127
	public function setSessionOptions( array $options ) {
3128
	}
3129
3130
	/**
3131
	 * Read and execute SQL commands from a file.
3132
	 *
3133
	 * Returns true on success, error string or exception on failure (depending
3134
	 * on object's error ignore settings).
3135
	 *
3136
	 * @param string $filename File name to open
3137
	 * @param bool|callable $lineCallback Optional function called before reading each line
3138
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3139
	 * @param bool|string $fname Calling function name or false if name should be
3140
	 *   generated dynamically using $filename
3141
	 * @param bool|callable $inputCallback Optional function called for each
3142
	 *   complete line sent
3143
	 * @return bool|string
3144
	 * @throws Exception
3145
	 */
3146
	public function sourceFile(
3147
		$filename, $lineCallback = false, $resultCallback = false, $fname = false, $inputCallback = false
3148
	) {
3149
		MediaWiki\suppressWarnings();
3150
		$fp = fopen( $filename, 'r' );
3151
		MediaWiki\restoreWarnings();
3152
3153
		if ( false === $fp ) {
3154
			throw new RuntimeException( "Could not open \"{$filename}\".\n" );
3155
		}
3156
3157
		if ( !$fname ) {
3158
			$fname = __METHOD__ . "( $filename )";
3159
		}
3160
3161
		try {
3162
			$error = $this->sourceStream( $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
0 ignored issues
show
Bug introduced by
It seems like $fname defined by parameter $fname on line 3147 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...
3163
		} catch ( Exception $e ) {
3164
			fclose( $fp );
3165
			throw $e;
3166
		}
3167
3168
		fclose( $fp );
3169
3170
		return $error;
3171
	}
3172
3173
	public function setSchemaVars( $vars ) {
3174
		$this->mSchemaVars = $vars;
3175
	}
3176
3177
	/**
3178
	 * Read and execute commands from an open file handle.
3179
	 *
3180
	 * Returns true on success, error string or exception on failure (depending
3181
	 * on object's error ignore settings).
3182
	 *
3183
	 * @param resource $fp File handle
3184
	 * @param bool|callable $lineCallback Optional function called before reading each query
3185
	 * @param bool|callable $resultCallback Optional function called for each MySQL result
3186
	 * @param string $fname Calling function name
3187
	 * @param bool|callable $inputCallback Optional function called for each complete query sent
3188
	 * @return bool|string
3189
	 */
3190
	public function sourceStream( $fp, $lineCallback = false, $resultCallback = false,
3191
		$fname = __METHOD__, $inputCallback = false
3192
	) {
3193
		$cmd = '';
3194
3195
		while ( !feof( $fp ) ) {
3196
			if ( $lineCallback ) {
3197
				call_user_func( $lineCallback );
3198
			}
3199
3200
			$line = trim( fgets( $fp ) );
3201
3202
			if ( $line == '' ) {
3203
				continue;
3204
			}
3205
3206
			if ( '-' == $line[0] && '-' == $line[1] ) {
3207
				continue;
3208
			}
3209
3210
			if ( $cmd != '' ) {
3211
				$cmd .= ' ';
3212
			}
3213
3214
			$done = $this->streamStatementEnd( $cmd, $line );
3215
3216
			$cmd .= "$line\n";
3217
3218
			if ( $done || feof( $fp ) ) {
3219
				$cmd = $this->replaceVars( $cmd );
3220
3221
				if ( ( $inputCallback && call_user_func( $inputCallback, $cmd ) ) || !$inputCallback ) {
3222
					$res = $this->query( $cmd, $fname );
3223
3224
					if ( $resultCallback ) {
3225
						call_user_func( $resultCallback, $res, $this );
3226
					}
3227
3228
					if ( false === $res ) {
3229
						$err = $this->lastError();
3230
3231
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3232
					}
3233
				}
3234
				$cmd = '';
3235
			}
3236
		}
3237
3238
		return true;
3239
	}
3240
3241
	/**
3242
	 * Called by sourceStream() to check if we've reached a statement end
3243
	 *
3244
	 * @param string $sql SQL assembled so far
3245
	 * @param string $newLine New line about to be added to $sql
3246
	 * @return bool Whether $newLine contains end of the statement
3247
	 */
3248
	public function streamStatementEnd( &$sql, &$newLine ) {
3249
		if ( $this->delimiter ) {
3250
			$prev = $newLine;
3251
			$newLine = preg_replace( '/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3252
			if ( $newLine != $prev ) {
3253
				return true;
3254
			}
3255
		}
3256
3257
		return false;
3258
	}
3259
3260
	/**
3261
	 * Database independent variable replacement. Replaces a set of variables
3262
	 * in an SQL statement with their contents as given by $this->getSchemaVars().
3263
	 *
3264
	 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3265
	 *
3266
	 * - '{$var}' should be used for text and is passed through the database's
3267
	 *   addQuotes method.
3268
	 * - `{$var}` should be used for identifiers (e.g. table and database names).
3269
	 *   It is passed through the database's addIdentifierQuotes method which
3270
	 *   can be overridden if the database uses something other than backticks.
3271
	 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3272
	 *   database's tableName method.
3273
	 * - / *i* / passes the name that follows through the database's indexName method.
3274
	 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3275
	 *   its use should be avoided. In 1.24 and older, string encoding was applied.
3276
	 *
3277
	 * @param string $ins SQL statement to replace variables in
3278
	 * @return string The new SQL statement with variables replaced
3279
	 */
3280
	protected function replaceVars( $ins ) {
3281
		$vars = $this->getSchemaVars();
3282
		return preg_replace_callback(
3283
			'!
3284
				/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3285
				\'\{\$ (\w+) }\'                  | # 3. addQuotes
3286
				`\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
3287
				/\*\$ (\w+) \*/                     # 5. leave unencoded
3288
			!x',
3289
			function ( $m ) use ( $vars ) {
3290
				// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3291
				// check for both nonexistent keys *and* the empty string.
3292
				if ( isset( $m[1] ) && $m[1] !== '' ) {
3293
					if ( $m[1] === 'i' ) {
3294
						return $this->indexName( $m[2] );
3295
					} else {
3296
						return $this->tableName( $m[2] );
3297
					}
3298 View Code Duplication
				} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3299
					return $this->addQuotes( $vars[$m[3]] );
3300
				} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3301
					return $this->addIdentifierQuotes( $vars[$m[4]] );
3302 View Code Duplication
				} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3303
					return $vars[$m[5]];
3304
				} else {
3305
					return $m[0];
3306
				}
3307
			},
3308
			$ins
3309
		);
3310
	}
3311
3312
	/**
3313
	 * Get schema variables. If none have been set via setSchemaVars(), then
3314
	 * use some defaults from the current object.
3315
	 *
3316
	 * @return array
3317
	 */
3318
	protected function getSchemaVars() {
3319
		if ( $this->mSchemaVars ) {
3320
			return $this->mSchemaVars;
3321
		} else {
3322
			return $this->getDefaultSchemaVars();
3323
		}
3324
	}
3325
3326
	/**
3327
	 * Get schema variables to use if none have been set via setSchemaVars().
3328
	 *
3329
	 * Override this in derived classes to provide variables for tables.sql
3330
	 * and SQL patch files.
3331
	 *
3332
	 * @return array
3333
	 */
3334
	protected function getDefaultSchemaVars() {
3335
		return [];
3336
	}
3337
3338
	public function lockIsFree( $lockName, $method ) {
3339
		return true;
3340
	}
3341
3342
	public function lock( $lockName, $method, $timeout = 5 ) {
3343
		$this->mNamedLocksHeld[$lockName] = 1;
3344
3345
		return true;
3346
	}
3347
3348
	public function unlock( $lockName, $method ) {
3349
		unset( $this->mNamedLocksHeld[$lockName] );
3350
3351
		return true;
3352
	}
3353
3354
	public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3355
		if ( $this->writesOrCallbacksPending() ) {
3356
			// This only flushes transactions to clear snapshots, not to write data
3357
			$fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
3358
			throw new DBUnexpectedError(
3359
				$this,
3360
				"$fname: Cannot COMMIT to clear snapshot because writes are pending ($fnames)."
3361
			);
3362
		}
3363
3364
		if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3365
			return null;
3366
		}
3367
3368
		$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3369
			if ( $this->trxLevel() ) {
3370
				// There is a good chance an exception was thrown, causing any early return
3371
				// from the caller. Let any error handler get a chance to issue rollback().
3372
				// If there isn't one, let the error bubble up and trigger server-side rollback.
3373
				$this->onTransactionResolution(
3374
					function () use ( $lockKey, $fname ) {
3375
						$this->unlock( $lockKey, $fname );
3376
					},
3377
					$fname
3378
				);
3379
			} else {
3380
				$this->unlock( $lockKey, $fname );
3381
			}
3382
		} );
3383
3384
		$this->commit( $fname, self::FLUSHING_INTERNAL );
3385
3386
		return $unlocker;
3387
	}
3388
3389
	public function namedLocksEnqueue() {
3390
		return false;
3391
	}
3392
3393
	/**
3394
	 * Lock specific tables
3395
	 *
3396
	 * @param array $read Array of tables to lock for read access
3397
	 * @param array $write Array of tables to lock for write access
3398
	 * @param string $method Name of caller
3399
	 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3400
	 * @return bool
3401
	 */
3402
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
3403
		return true;
3404
	}
3405
3406
	/**
3407
	 * Unlock specific tables
3408
	 *
3409
	 * @param string $method The caller
3410
	 * @return bool
3411
	 */
3412
	public function unlockTables( $method ) {
3413
		return true;
3414
	}
3415
3416
	/**
3417
	 * Delete a table
3418
	 * @param string $tableName
3419
	 * @param string $fName
3420
	 * @return bool|ResultWrapper
3421
	 * @since 1.18
3422
	 */
3423 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
3424
		if ( !$this->tableExists( $tableName, $fName ) ) {
3425
			return false;
3426
		}
3427
		$sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
3428
3429
		return $this->query( $sql, $fName );
3430
	}
3431
3432
	public function getInfinity() {
3433
		return 'infinity';
3434
	}
3435
3436
	public function encodeExpiry( $expiry ) {
3437
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3438
			? $this->getInfinity()
3439
			: $this->timestamp( $expiry );
3440
	}
3441
3442
	public function decodeExpiry( $expiry, $format = TS_MW ) {
3443
		if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
3444
			return 'infinity';
3445
		}
3446
3447
		try {
3448
			$t = new ConvertableTimestamp( $expiry );
3449
3450
			return $t->getTimestamp( $format );
3451
		} catch ( TimestampException $e ) {
3452
			return false;
3453
		}
3454
	}
3455
3456
	public function setBigSelects( $value = true ) {
3457
		// no-op
3458
	}
3459
3460
	public function isReadOnly() {
3461
		return ( $this->getReadOnlyReason() !== false );
3462
	}
3463
3464
	/**
3465
	 * @return string|bool Reason this DB is read-only or false if it is not
3466
	 */
3467
	protected function getReadOnlyReason() {
3468
		$reason = $this->getLBInfo( 'readOnlyReason' );
3469
3470
		return is_string( $reason ) ? $reason : false;
3471
	}
3472
3473
	public function setTableAliases( array $aliases ) {
3474
		$this->tableAliases = $aliases;
3475
	}
3476
3477
	/**
3478
	 * @return bool Whether a DB user is required to access the DB
3479
	 * @since 1.28
3480
	 */
3481
	protected function requiresDatabaseUser() {
3482
		return true;
3483
	}
3484
3485
	/**
3486
	 * @since 1.19
3487
	 * @return string
3488
	 */
3489
	public function __toString() {
3490
		return (string)$this->mConn;
3491
	}
3492
3493
	/**
3494
	 * Called by serialize. Throw an exception when DB connection is serialized.
3495
	 * This causes problems on some database engines because the connection is
3496
	 * not restored on unserialize.
3497
	 */
3498
	public function __sleep() {
3499
		throw new RuntimeException( 'Database serialization may cause problems, since ' .
3500
			'the connection is not restored on wakeup.' );
3501
	}
3502
3503
	/**
3504
	 * Run a few simple sanity checks
3505
	 */
3506
	public function __destruct() {
3507
		if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3508
			trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3509
		}
3510
3511
		$danglingWriters = $this->pendingWriteAndCallbackCallers();
3512
		if ( $danglingWriters ) {
3513
			$fnames = implode( ', ', $danglingWriters );
3514
			trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
3515
		}
3516
	}
3517
}
3518