Completed
Branch master (66f57c)
by
unknown
33:51
created

Database::wasReadOnlyError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 36 and the first side effect is on line 3475.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
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
use Wikimedia\ScopedCallback;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, ScopedCallback.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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