Completed
Branch master (9ca75b)
by
unknown
26:06
created

Database::prependDatabaseOrSchema()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 3
dl 0
loc 10
rs 9.2
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 3481.

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
			$prefix = '';
1725
			# In dbs that support it, $database may actually be the schema
1726
			# but that doesn't affect any of the functionality here
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 )
1750
			&& $tableName !== ''
1751
		) {
1752
			$tableName = $this->addIdentifierQuotes( $tableName );
1753
		}
1754
1755
		# Quote $schema and $database and merge them with the table name if needed
1756
		$tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
1757
		$tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
1758
1759
		return $tableName;
1760
	}
1761
1762
	/**
1763
	 * @param string|null $namespace Database or schema
1764
	 * @param string $relation Name of table, view, sequence, etc...
1765
	 * @param string $format One of (raw, quoted)
1766
	 * @return string Relation name with quoted and merged $namespace as needed
1767
	 */
1768
	private function prependDatabaseOrSchema( $namespace, $relation, $format ) {
1769
		if ( strlen( $namespace ) ) {
1770
			if ( $format === 'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
1771
				$namespace = $this->addIdentifierQuotes( $namespace );
1772
			}
1773
			$relation = $namespace . '.' . $relation;
1774
		}
1775
1776
		return $relation;
1777
	}
1778
1779 View Code Duplication
	public function tableNames() {
1780
		$inArray = func_get_args();
1781
		$retVal = [];
1782
1783
		foreach ( $inArray as $name ) {
1784
			$retVal[$name] = $this->tableName( $name );
1785
		}
1786
1787
		return $retVal;
1788
	}
1789
1790 View Code Duplication
	public function tableNamesN() {
1791
		$inArray = func_get_args();
1792
		$retVal = [];
1793
1794
		foreach ( $inArray as $name ) {
1795
			$retVal[] = $this->tableName( $name );
1796
		}
1797
1798
		return $retVal;
1799
	}
1800
1801
	/**
1802
	 * Get an aliased table name
1803
	 * e.g. tableName AS newTableName
1804
	 *
1805
	 * @param string $name Table name, see tableName()
1806
	 * @param string|bool $alias Alias (optional)
1807
	 * @return string SQL name for aliased table. Will not alias a table to its own name
1808
	 */
1809
	protected function tableNameWithAlias( $name, $alias = false ) {
1810
		if ( !$alias || $alias == $name ) {
1811
			return $this->tableName( $name );
1812
		} else {
1813
			return $this->tableName( $name ) . ' ' . $this->addIdentifierQuotes( $alias );
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1809 can also be of type boolean; however, Database::addIdentifierQuotes() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1814
		}
1815
	}
1816
1817
	/**
1818
	 * Gets an array of aliased table names
1819
	 *
1820
	 * @param array $tables [ [alias] => table ]
1821
	 * @return string[] See tableNameWithAlias()
1822
	 */
1823
	protected function tableNamesWithAlias( $tables ) {
1824
		$retval = [];
1825
		foreach ( $tables as $alias => $table ) {
1826
			if ( is_numeric( $alias ) ) {
1827
				$alias = $table;
1828
			}
1829
			$retval[] = $this->tableNameWithAlias( $table, $alias );
1830
		}
1831
1832
		return $retval;
1833
	}
1834
1835
	/**
1836
	 * Get an aliased field name
1837
	 * e.g. fieldName AS newFieldName
1838
	 *
1839
	 * @param string $name Field name
1840
	 * @param string|bool $alias Alias (optional)
1841
	 * @return string SQL name for aliased field. Will not alias a field to its own name
1842
	 */
1843
	protected function fieldNameWithAlias( $name, $alias = false ) {
1844
		if ( !$alias || (string)$alias === (string)$name ) {
1845
			return $name;
1846
		} else {
1847
			return $name . ' AS ' . $this->addIdentifierQuotes( $alias ); // PostgreSQL needs AS
0 ignored issues
show
Bug introduced by
It seems like $alias defined by parameter $alias on line 1843 can also be of type boolean; however, Database::addIdentifierQuotes() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
1848
		}
1849
	}
1850
1851
	/**
1852
	 * Gets an array of aliased field names
1853
	 *
1854
	 * @param array $fields [ [alias] => field ]
1855
	 * @return string[] See fieldNameWithAlias()
1856
	 */
1857
	protected function fieldNamesWithAlias( $fields ) {
1858
		$retval = [];
1859
		foreach ( $fields as $alias => $field ) {
1860
			if ( is_numeric( $alias ) ) {
1861
				$alias = $field;
1862
			}
1863
			$retval[] = $this->fieldNameWithAlias( $field, $alias );
1864
		}
1865
1866
		return $retval;
1867
	}
1868
1869
	/**
1870
	 * Get the aliased table name clause for a FROM clause
1871
	 * which might have a JOIN and/or USE INDEX or IGNORE INDEX clause
1872
	 *
1873
	 * @param array $tables ( [alias] => table )
1874
	 * @param array $use_index Same as for select()
1875
	 * @param array $ignore_index Same as for select()
1876
	 * @param array $join_conds Same as for select()
1877
	 * @return string
1878
	 */
1879
	protected function tableNamesWithIndexClauseOrJOIN(
1880
		$tables, $use_index = [], $ignore_index = [], $join_conds = []
1881
	) {
1882
		$ret = [];
1883
		$retJOIN = [];
1884
		$use_index = (array)$use_index;
1885
		$ignore_index = (array)$ignore_index;
1886
		$join_conds = (array)$join_conds;
1887
1888
		foreach ( $tables as $alias => $table ) {
1889
			if ( !is_string( $alias ) ) {
1890
				// No alias? Set it equal to the table name
1891
				$alias = $table;
1892
			}
1893
			// Is there a JOIN clause for this table?
1894
			if ( isset( $join_conds[$alias] ) ) {
1895
				list( $joinType, $conds ) = $join_conds[$alias];
1896
				$tableClause = $joinType;
1897
				$tableClause .= ' ' . $this->tableNameWithAlias( $table, $alias );
1898 View Code Duplication
				if ( isset( $use_index[$alias] ) ) { // has USE INDEX?
1899
					$use = $this->useIndexClause( implode( ',', (array)$use_index[$alias] ) );
1900
					if ( $use != '' ) {
1901
						$tableClause .= ' ' . $use;
1902
					}
1903
				}
1904 View Code Duplication
				if ( isset( $ignore_index[$alias] ) ) { // has IGNORE INDEX?
1905
					$ignore = $this->ignoreIndexClause(
1906
						implode( ',', (array)$ignore_index[$alias] ) );
1907
					if ( $ignore != '' ) {
1908
						$tableClause .= ' ' . $ignore;
1909
					}
1910
				}
1911
				$on = $this->makeList( (array)$conds, self::LIST_AND );
1912
				if ( $on != '' ) {
1913
					$tableClause .= ' ON (' . $on . ')';
1914
				}
1915
1916
				$retJOIN[] = $tableClause;
1917
			} elseif ( isset( $use_index[$alias] ) ) {
1918
				// Is there an INDEX clause for this table?
1919
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1920
				$tableClause .= ' ' . $this->useIndexClause(
1921
						implode( ',', (array)$use_index[$alias] )
1922
					);
1923
1924
				$ret[] = $tableClause;
1925
			} elseif ( isset( $ignore_index[$alias] ) ) {
1926
				// Is there an INDEX clause for this table?
1927
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1928
				$tableClause .= ' ' . $this->ignoreIndexClause(
1929
						implode( ',', (array)$ignore_index[$alias] )
1930
					);
1931
1932
				$ret[] = $tableClause;
1933
			} else {
1934
				$tableClause = $this->tableNameWithAlias( $table, $alias );
1935
1936
				$ret[] = $tableClause;
1937
			}
1938
		}
1939
1940
		// We can't separate explicit JOIN clauses with ',', use ' ' for those
1941
		$implicitJoins = !empty( $ret ) ? implode( ',', $ret ) : "";
1942
		$explicitJoins = !empty( $retJOIN ) ? implode( ' ', $retJOIN ) : "";
1943
1944
		// Compile our final table clause
1945
		return implode( ' ', [ $implicitJoins, $explicitJoins ] );
1946
	}
1947
1948
	/**
1949
	 * Get the name of an index in a given table.
1950
	 *
1951
	 * @param string $index
1952
	 * @return string
1953
	 */
1954
	protected function indexName( $index ) {
1955
		return $index;
1956
	}
1957
1958
	public function addQuotes( $s ) {
1959
		if ( $s instanceof Blob ) {
1960
			$s = $s->fetch();
1961
		}
1962
		if ( $s === null ) {
1963
			return 'NULL';
1964
		} elseif ( is_bool( $s ) ) {
1965
			return (int)$s;
1966
		} else {
1967
			# This will also quote numeric values. This should be harmless,
1968
			# and protects against weird problems that occur when they really
1969
			# _are_ strings such as article titles and string->number->string
1970
			# conversion is not 1:1.
1971
			return "'" . $this->strencode( $s ) . "'";
1972
		}
1973
	}
1974
1975
	/**
1976
	 * Quotes an identifier using `backticks` or "double quotes" depending on the database type.
1977
	 * MySQL uses `backticks` while basically everything else uses double quotes.
1978
	 * Since MySQL is the odd one out here the double quotes are our generic
1979
	 * and we implement backticks in DatabaseMysql.
1980
	 *
1981
	 * @param string $s
1982
	 * @return string
1983
	 */
1984
	public function addIdentifierQuotes( $s ) {
1985
		return '"' . str_replace( '"', '""', $s ) . '"';
1986
	}
1987
1988
	/**
1989
	 * Returns if the given identifier looks quoted or not according to
1990
	 * the database convention for quoting identifiers .
1991
	 *
1992
	 * @note Do not use this to determine if untrusted input is safe.
1993
	 *   A malicious user can trick this function.
1994
	 * @param string $name
1995
	 * @return bool
1996
	 */
1997
	public function isQuotedIdentifier( $name ) {
1998
		return $name[0] == '"' && substr( $name, -1, 1 ) == '"';
1999
	}
2000
2001
	/**
2002
	 * @param string $s
2003
	 * @return string
2004
	 */
2005
	protected function escapeLikeInternal( $s ) {
2006
		return addcslashes( $s, '\%_' );
2007
	}
2008
2009
	public function buildLike() {
2010
		$params = func_get_args();
2011
2012
		if ( count( $params ) > 0 && is_array( $params[0] ) ) {
2013
			$params = $params[0];
2014
		}
2015
2016
		$s = '';
2017
2018
		foreach ( $params as $value ) {
2019
			if ( $value instanceof LikeMatch ) {
2020
				$s .= $value->toString();
2021
			} else {
2022
				$s .= $this->escapeLikeInternal( $value );
2023
			}
2024
		}
2025
2026
		return " LIKE {$this->addQuotes( $s )} ";
2027
	}
2028
2029
	public function anyChar() {
2030
		return new LikeMatch( '_' );
2031
	}
2032
2033
	public function anyString() {
2034
		return new LikeMatch( '%' );
2035
	}
2036
2037
	public function nextSequenceValue( $seqName ) {
2038
		return null;
2039
	}
2040
2041
	/**
2042
	 * USE INDEX clause. Unlikely to be useful for anything but MySQL. This
2043
	 * is only needed because a) MySQL must be as efficient as possible due to
2044
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2045
	 * which index to pick. Anyway, other databases might have different
2046
	 * indexes on a given table. So don't bother overriding this unless you're
2047
	 * MySQL.
2048
	 * @param string $index
2049
	 * @return string
2050
	 */
2051
	public function useIndexClause( $index ) {
2052
		return '';
2053
	}
2054
2055
	/**
2056
	 * IGNORE INDEX clause. Unlikely to be useful for anything but MySQL. This
2057
	 * is only needed because a) MySQL must be as efficient as possible due to
2058
	 * its use on Wikipedia, and b) MySQL 4.0 is kind of dumb sometimes about
2059
	 * which index to pick. Anyway, other databases might have different
2060
	 * indexes on a given table. So don't bother overriding this unless you're
2061
	 * MySQL.
2062
	 * @param string $index
2063
	 * @return string
2064
	 */
2065
	public function ignoreIndexClause( $index ) {
2066
		return '';
2067
	}
2068
2069
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2070
		$quotedTable = $this->tableName( $table );
2071
2072
		if ( count( $rows ) == 0 ) {
2073
			return;
2074
		}
2075
2076
		# Single row case
2077
		if ( !is_array( reset( $rows ) ) ) {
2078
			$rows = [ $rows ];
2079
		}
2080
2081
		// @FXIME: this is not atomic, but a trx would break affectedRows()
2082
		foreach ( $rows as $row ) {
2083
			# Delete rows which collide
2084
			if ( $uniqueIndexes ) {
2085
				$sql = "DELETE FROM $quotedTable WHERE ";
2086
				$first = true;
2087
				foreach ( $uniqueIndexes as $index ) {
2088
					if ( $first ) {
2089
						$first = false;
2090
						$sql .= '( ';
2091
					} else {
2092
						$sql .= ' ) OR ( ';
2093
					}
2094
					if ( is_array( $index ) ) {
2095
						$first2 = true;
2096
						foreach ( $index as $col ) {
2097
							if ( $first2 ) {
2098
								$first2 = false;
2099
							} else {
2100
								$sql .= ' AND ';
2101
							}
2102
							$sql .= $col . '=' . $this->addQuotes( $row[$col] );
2103
						}
2104
					} else {
2105
						$sql .= $index . '=' . $this->addQuotes( $row[$index] );
2106
					}
2107
				}
2108
				$sql .= ' )';
2109
				$this->query( $sql, $fname );
2110
			}
2111
2112
			# Now insert the row
2113
			$this->insert( $table, $row, $fname );
2114
		}
2115
	}
2116
2117
	/**
2118
	 * REPLACE query wrapper for MySQL and SQLite, which have a native REPLACE
2119
	 * statement.
2120
	 *
2121
	 * @param string $table Table name
2122
	 * @param array|string $rows Row(s) to insert
2123
	 * @param string $fname Caller function name
2124
	 *
2125
	 * @return ResultWrapper
2126
	 */
2127
	protected function nativeReplace( $table, $rows, $fname ) {
2128
		$table = $this->tableName( $table );
2129
2130
		# Single row case
2131
		if ( !is_array( reset( $rows ) ) ) {
2132
			$rows = [ $rows ];
2133
		}
2134
2135
		$sql = "REPLACE INTO $table (" . implode( ',', array_keys( $rows[0] ) ) . ') VALUES ';
2136
		$first = true;
2137
2138 View Code Duplication
		foreach ( $rows as $row ) {
2139
			if ( $first ) {
2140
				$first = false;
2141
			} else {
2142
				$sql .= ',';
2143
			}
2144
2145
			$sql .= '(' . $this->makeList( $row ) . ')';
2146
		}
2147
2148
		return $this->query( $sql, $fname );
2149
	}
2150
2151
	public function upsert( $table, array $rows, array $uniqueIndexes, array $set,
2152
		$fname = __METHOD__
2153
	) {
2154
		if ( !count( $rows ) ) {
2155
			return true; // nothing to do
2156
		}
2157
2158
		if ( !is_array( reset( $rows ) ) ) {
2159
			$rows = [ $rows ];
2160
		}
2161
2162
		if ( count( $uniqueIndexes ) ) {
2163
			$clauses = []; // list WHERE clauses that each identify a single row
2164
			foreach ( $rows as $row ) {
2165
				foreach ( $uniqueIndexes as $index ) {
2166
					$index = is_array( $index ) ? $index : [ $index ]; // columns
2167
					$rowKey = []; // unique key to this row
2168
					foreach ( $index as $column ) {
2169
						$rowKey[$column] = $row[$column];
2170
					}
2171
					$clauses[] = $this->makeList( $rowKey, self::LIST_AND );
2172
				}
2173
			}
2174
			$where = [ $this->makeList( $clauses, self::LIST_OR ) ];
2175
		} else {
2176
			$where = false;
2177
		}
2178
2179
		$useTrx = !$this->mTrxLevel;
2180
		if ( $useTrx ) {
2181
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2182
		}
2183
		try {
2184
			# Update any existing conflicting row(s)
2185
			if ( $where !== false ) {
2186
				$ok = $this->update( $table, $set, $where, $fname );
2187
			} else {
2188
				$ok = true;
2189
			}
2190
			# Now insert any non-conflicting row(s)
2191
			$ok = $this->insert( $table, $rows, $fname, [ 'IGNORE' ] ) && $ok;
2192
		} catch ( Exception $e ) {
2193
			if ( $useTrx ) {
2194
				$this->rollback( $fname, self::FLUSHING_INTERNAL );
2195
			}
2196
			throw $e;
2197
		}
2198
		if ( $useTrx ) {
2199
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2200
		}
2201
2202
		return $ok;
2203
	}
2204
2205 View Code Duplication
	public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2206
		$fname = __METHOD__
2207
	) {
2208
		if ( !$conds ) {
2209
			throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
2210
		}
2211
2212
		$delTable = $this->tableName( $delTable );
2213
		$joinTable = $this->tableName( $joinTable );
2214
		$sql = "DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2215
		if ( $conds != '*' ) {
2216
			$sql .= 'WHERE ' . $this->makeList( $conds, self::LIST_AND );
2217
		}
2218
		$sql .= ')';
2219
2220
		$this->query( $sql, $fname );
2221
	}
2222
2223
	public function textFieldSize( $table, $field ) {
2224
		$table = $this->tableName( $table );
2225
		$sql = "SHOW COLUMNS FROM $table LIKE \"$field\";";
2226
		$res = $this->query( $sql, __METHOD__ );
2227
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query($sql, __METHOD__) on line 2226 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...
2228
2229
		$m = [];
2230
2231
		if ( preg_match( '/\((.*)\)/', $row->Type, $m ) ) {
2232
			$size = $m[1];
2233
		} else {
2234
			$size = -1;
2235
		}
2236
2237
		return $size;
2238
	}
2239
2240
	public function delete( $table, $conds, $fname = __METHOD__ ) {
2241
		if ( !$conds ) {
2242
			throw new DBUnexpectedError( $this, __METHOD__ . ' called with no conditions' );
2243
		}
2244
2245
		$table = $this->tableName( $table );
2246
		$sql = "DELETE FROM $table";
2247
2248 View Code Duplication
		if ( $conds != '*' ) {
2249
			if ( is_array( $conds ) ) {
2250
				$conds = $this->makeList( $conds, self::LIST_AND );
2251
			}
2252
			$sql .= ' WHERE ' . $conds;
2253
		}
2254
2255
		return $this->query( $sql, $fname );
2256
	}
2257
2258
	public function insertSelect(
2259
		$destTable, $srcTable, $varMap, $conds,
2260
		$fname = __METHOD__, $insertOptions = [], $selectOptions = []
2261
	) {
2262
		if ( $this->cliMode ) {
2263
			// For massive migrations with downtime, we don't want to select everything
2264
			// into memory and OOM, so do all this native on the server side if possible.
2265
			return $this->nativeInsertSelect(
2266
				$destTable,
2267
				$srcTable,
2268
				$varMap,
2269
				$conds,
2270
				$fname,
2271
				$insertOptions,
2272
				$selectOptions
2273
			);
2274
		}
2275
2276
		// For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
2277
		// on only the master (without needing row-based-replication). It also makes it easy to
2278
		// know how big the INSERT is going to be.
2279
		$fields = [];
2280
		foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
2281
			$fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
2282
		}
2283
		$selectOptions[] = 'FOR UPDATE';
2284
		$res = $this->select( $srcTable, implode( ',', $fields ), $conds, $fname, $selectOptions );
2285
		if ( !$res ) {
2286
			return false;
2287
		}
2288
2289
		$rows = [];
2290
		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...
2291
			$rows[] = (array)$row;
2292
		}
2293
2294
		return $this->insert( $destTable, $rows, $fname, $insertOptions );
2295
	}
2296
2297
	protected function nativeInsertSelect( $destTable, $srcTable, $varMap, $conds,
2298
		$fname = __METHOD__,
2299
		$insertOptions = [], $selectOptions = []
2300
	) {
2301
		$destTable = $this->tableName( $destTable );
2302
2303
		if ( !is_array( $insertOptions ) ) {
2304
			$insertOptions = [ $insertOptions ];
2305
		}
2306
2307
		$insertOptions = $this->makeInsertOptions( $insertOptions );
2308
2309
		if ( !is_array( $selectOptions ) ) {
2310
			$selectOptions = [ $selectOptions ];
2311
		}
2312
2313
		list( $startOpts, $useIndex, $tailOpts, $ignoreIndex ) = $this->makeSelectOptions(
2314
			$selectOptions );
2315
2316 View Code Duplication
		if ( is_array( $srcTable ) ) {
2317
			$srcTable = implode( ',', array_map( [ &$this, 'tableName' ], $srcTable ) );
2318
		} else {
2319
			$srcTable = $this->tableName( $srcTable );
2320
		}
2321
2322
		$sql = "INSERT $insertOptions" .
2323
			" INTO $destTable (" . implode( ',', array_keys( $varMap ) ) . ')' .
2324
			" SELECT $startOpts " . implode( ',', $varMap ) .
2325
			" FROM $srcTable $useIndex $ignoreIndex ";
2326
2327 View Code Duplication
		if ( $conds != '*' ) {
2328
			if ( is_array( $conds ) ) {
2329
				$conds = $this->makeList( $conds, self::LIST_AND );
2330
			}
2331
			$sql .= " WHERE $conds";
2332
		}
2333
2334
		$sql .= " $tailOpts";
2335
2336
		return $this->query( $sql, $fname );
2337
	}
2338
2339
	/**
2340
	 * Construct a LIMIT query with optional offset. This is used for query
2341
	 * pages. The SQL should be adjusted so that only the first $limit rows
2342
	 * are returned. If $offset is provided as well, then the first $offset
2343
	 * rows should be discarded, and the next $limit rows should be returned.
2344
	 * If the result of the query is not ordered, then the rows to be returned
2345
	 * are theoretically arbitrary.
2346
	 *
2347
	 * $sql is expected to be a SELECT, if that makes a difference.
2348
	 *
2349
	 * The version provided by default works in MySQL and SQLite. It will very
2350
	 * likely need to be overridden for most other DBMSes.
2351
	 *
2352
	 * @param string $sql SQL query we will append the limit too
2353
	 * @param int $limit The SQL limit
2354
	 * @param int|bool $offset The SQL offset (default false)
2355
	 * @throws DBUnexpectedError
2356
	 * @return string
2357
	 */
2358
	public function limitResult( $sql, $limit, $offset = false ) {
2359
		if ( !is_numeric( $limit ) ) {
2360
			throw new DBUnexpectedError( $this,
2361
				"Invalid non-numeric limit passed to limitResult()\n" );
2362
		}
2363
2364
		return "$sql LIMIT "
2365
		. ( ( is_numeric( $offset ) && $offset != 0 ) ? "{$offset}," : "" )
2366
		. "{$limit} ";
2367
	}
2368
2369
	public function unionSupportsOrderAndLimit() {
2370
		return true; // True for almost every DB supported
2371
	}
2372
2373
	public function unionQueries( $sqls, $all ) {
2374
		$glue = $all ? ') UNION ALL (' : ') UNION (';
2375
2376
		return '(' . implode( $glue, $sqls ) . ')';
2377
	}
2378
2379
	public function conditional( $cond, $trueVal, $falseVal ) {
2380
		if ( is_array( $cond ) ) {
2381
			$cond = $this->makeList( $cond, self::LIST_AND );
2382
		}
2383
2384
		return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
2385
	}
2386
2387
	public function strreplace( $orig, $old, $new ) {
2388
		return "REPLACE({$orig}, {$old}, {$new})";
2389
	}
2390
2391
	public function getServerUptime() {
2392
		return 0;
2393
	}
2394
2395
	public function wasDeadlock() {
2396
		return false;
2397
	}
2398
2399
	public function wasLockTimeout() {
2400
		return false;
2401
	}
2402
2403
	public function wasErrorReissuable() {
2404
		return false;
2405
	}
2406
2407
	public function wasReadOnlyError() {
2408
		return false;
2409
	}
2410
2411
	/**
2412
	 * Do not use this method outside of Database/DBError classes
2413
	 *
2414
	 * @param integer|string $errno
2415
	 * @return bool Whether the given query error was a connection drop
2416
	 */
2417
	public function wasConnectionError( $errno ) {
2418
		return false;
2419
	}
2420
2421
	public function deadlockLoop() {
2422
		$args = func_get_args();
2423
		$function = array_shift( $args );
2424
		$tries = self::DEADLOCK_TRIES;
2425
2426
		$this->begin( __METHOD__ );
2427
2428
		$retVal = null;
2429
		/** @var Exception $e */
2430
		$e = null;
2431
		do {
2432
			try {
2433
				$retVal = call_user_func_array( $function, $args );
2434
				break;
2435
			} catch ( DBQueryError $e ) {
2436
				if ( $this->wasDeadlock() ) {
2437
					// Retry after a randomized delay
2438
					usleep( mt_rand( self::DEADLOCK_DELAY_MIN, self::DEADLOCK_DELAY_MAX ) );
2439
				} else {
2440
					// Throw the error back up
2441
					throw $e;
2442
				}
2443
			}
2444
		} while ( --$tries > 0 );
2445
2446
		if ( $tries <= 0 ) {
2447
			// Too many deadlocks; give up
2448
			$this->rollback( __METHOD__ );
2449
			throw $e;
2450
		} else {
2451
			$this->commit( __METHOD__ );
2452
2453
			return $retVal;
2454
		}
2455
	}
2456
2457
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
2458
		# Real waits are implemented in the subclass.
2459
		return 0;
2460
	}
2461
2462
	public function getReplicaPos() {
2463
		# Stub
2464
		return false;
2465
	}
2466
2467
	public function getMasterPos() {
2468
		# Stub
2469
		return false;
2470
	}
2471
2472
	public function serverIsReadOnly() {
2473
		return false;
2474
	}
2475
2476
	final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
2477
		if ( !$this->mTrxLevel ) {
2478
			throw new DBUnexpectedError( $this, "No transaction is active." );
2479
		}
2480
		$this->mTrxEndCallbacks[] = [ $callback, $fname ];
2481
	}
2482
2483
	final public function onTransactionIdle( callable $callback, $fname = __METHOD__ ) {
2484
		$this->mTrxIdleCallbacks[] = [ $callback, $fname ];
2485
		if ( !$this->mTrxLevel ) {
2486
			$this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE );
2487
		}
2488
	}
2489
2490
	final public function onTransactionPreCommitOrIdle( callable $callback, $fname = __METHOD__ ) {
2491
		if ( $this->mTrxLevel ) {
2492
			$this->mTrxPreCommitCallbacks[] = [ $callback, $fname ];
2493
		} else {
2494
			// If no transaction is active, then make one for this callback
2495
			$this->startAtomic( __METHOD__ );
2496
			try {
2497
				call_user_func( $callback );
2498
				$this->endAtomic( __METHOD__ );
2499
			} catch ( Exception $e ) {
2500
				$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2501
				throw $e;
2502
			}
2503
		}
2504
	}
2505
2506
	final public function setTransactionListener( $name, callable $callback = null ) {
2507
		if ( $callback ) {
2508
			$this->mTrxRecurringCallbacks[$name] = $callback;
2509
		} else {
2510
			unset( $this->mTrxRecurringCallbacks[$name] );
2511
		}
2512
	}
2513
2514
	/**
2515
	 * Whether to disable running of post-COMMIT/ROLLBACK callbacks
2516
	 *
2517
	 * This method should not be used outside of Database/LoadBalancer
2518
	 *
2519
	 * @param bool $suppress
2520
	 * @since 1.28
2521
	 */
2522
	final public function setTrxEndCallbackSuppression( $suppress ) {
2523
		$this->mTrxEndCallbacksSuppressed = $suppress;
2524
	}
2525
2526
	/**
2527
	 * Actually run and consume any "on transaction idle/resolution" callbacks.
2528
	 *
2529
	 * This method should not be used outside of Database/LoadBalancer
2530
	 *
2531
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2532
	 * @since 1.20
2533
	 * @throws Exception
2534
	 */
2535
	public function runOnTransactionIdleCallbacks( $trigger ) {
2536
		if ( $this->mTrxEndCallbacksSuppressed ) {
2537
			return;
2538
		}
2539
2540
		$autoTrx = $this->getFlag( self::DBO_TRX ); // automatic begin() enabled?
2541
		/** @var Exception $e */
2542
		$e = null; // first exception
2543
		do { // callbacks may add callbacks :)
2544
			$callbacks = array_merge(
2545
				$this->mTrxIdleCallbacks,
2546
				$this->mTrxEndCallbacks // include "transaction resolution" callbacks
2547
			);
2548
			$this->mTrxIdleCallbacks = []; // consumed (and recursion guard)
2549
			$this->mTrxEndCallbacks = []; // consumed (recursion guard)
2550
			foreach ( $callbacks as $callback ) {
2551
				try {
2552
					list( $phpCallback ) = $callback;
2553
					$this->clearFlag( self::DBO_TRX ); // make each query its own transaction
2554
					call_user_func_array( $phpCallback, [ $trigger ] );
2555
					if ( $autoTrx ) {
2556
						$this->setFlag( self::DBO_TRX ); // restore automatic begin()
2557
					} else {
2558
						$this->clearFlag( self::DBO_TRX ); // restore auto-commit
2559
					}
2560
				} catch ( Exception $ex ) {
2561
					call_user_func( $this->errorLogger, $ex );
2562
					$e = $e ?: $ex;
2563
					// Some callbacks may use startAtomic/endAtomic, so make sure
2564
					// their transactions are ended so other callbacks don't fail
2565
					if ( $this->trxLevel() ) {
2566
						$this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
2567
					}
2568
				}
2569
			}
2570
		} while ( count( $this->mTrxIdleCallbacks ) );
2571
2572
		if ( $e instanceof Exception ) {
2573
			throw $e; // re-throw any first exception
2574
		}
2575
	}
2576
2577
	/**
2578
	 * Actually run and consume any "on transaction pre-commit" callbacks.
2579
	 *
2580
	 * This method should not be used outside of Database/LoadBalancer
2581
	 *
2582
	 * @since 1.22
2583
	 * @throws Exception
2584
	 */
2585
	public function runOnTransactionPreCommitCallbacks() {
2586
		$e = null; // first exception
2587
		do { // callbacks may add callbacks :)
2588
			$callbacks = $this->mTrxPreCommitCallbacks;
2589
			$this->mTrxPreCommitCallbacks = []; // consumed (and recursion guard)
2590
			foreach ( $callbacks as $callback ) {
2591
				try {
2592
					list( $phpCallback ) = $callback;
2593
					call_user_func( $phpCallback );
2594
				} catch ( Exception $ex ) {
2595
					call_user_func( $this->errorLogger, $ex );
2596
					$e = $e ?: $ex;
2597
				}
2598
			}
2599
		} while ( count( $this->mTrxPreCommitCallbacks ) );
2600
2601
		if ( $e instanceof Exception ) {
2602
			throw $e; // re-throw any first exception
2603
		}
2604
	}
2605
2606
	/**
2607
	 * Actually run any "transaction listener" callbacks.
2608
	 *
2609
	 * This method should not be used outside of Database/LoadBalancer
2610
	 *
2611
	 * @param integer $trigger IDatabase::TRIGGER_* constant
2612
	 * @throws Exception
2613
	 * @since 1.20
2614
	 */
2615
	public function runTransactionListenerCallbacks( $trigger ) {
2616
		if ( $this->mTrxEndCallbacksSuppressed ) {
2617
			return;
2618
		}
2619
2620
		/** @var Exception $e */
2621
		$e = null; // first exception
2622
2623
		foreach ( $this->mTrxRecurringCallbacks as $phpCallback ) {
2624
			try {
2625
				$phpCallback( $trigger, $this );
2626
			} catch ( Exception $ex ) {
2627
				call_user_func( $this->errorLogger, $ex );
2628
				$e = $e ?: $ex;
2629
			}
2630
		}
2631
2632
		if ( $e instanceof Exception ) {
2633
			throw $e; // re-throw any first exception
2634
		}
2635
	}
2636
2637
	final public function startAtomic( $fname = __METHOD__ ) {
2638
		if ( !$this->mTrxLevel ) {
2639
			$this->begin( $fname, self::TRANSACTION_INTERNAL );
2640
			// If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
2641
			// in all changes being in one transaction to keep requests transactional.
2642
			if ( !$this->getFlag( self::DBO_TRX ) ) {
2643
				$this->mTrxAutomaticAtomic = true;
2644
			}
2645
		}
2646
2647
		$this->mTrxAtomicLevels[] = $fname;
2648
	}
2649
2650
	final public function endAtomic( $fname = __METHOD__ ) {
2651
		if ( !$this->mTrxLevel ) {
2652
			throw new DBUnexpectedError( $this, "No atomic transaction is open (got $fname)." );
2653
		}
2654
		if ( !$this->mTrxAtomicLevels ||
2655
			array_pop( $this->mTrxAtomicLevels ) !== $fname
2656
		) {
2657
			throw new DBUnexpectedError( $this, "Invalid atomic section ended (got $fname)." );
2658
		}
2659
2660
		if ( !$this->mTrxAtomicLevels && $this->mTrxAutomaticAtomic ) {
2661
			$this->commit( $fname, self::FLUSHING_INTERNAL );
2662
		}
2663
	}
2664
2665
	final public function doAtomicSection( $fname, callable $callback ) {
2666
		$this->startAtomic( $fname );
2667
		try {
2668
			$res = call_user_func_array( $callback, [ $this, $fname ] );
2669
		} catch ( Exception $e ) {
2670
			$this->rollback( $fname, self::FLUSHING_INTERNAL );
2671
			throw $e;
2672
		}
2673
		$this->endAtomic( $fname );
2674
2675
		return $res;
2676
	}
2677
2678
	final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
2679
		// Protect against mismatched atomic section, transaction nesting, and snapshot loss
2680
		if ( $this->mTrxLevel ) {
2681
			if ( $this->mTrxAtomicLevels ) {
2682
				$levels = implode( ', ', $this->mTrxAtomicLevels );
2683
				$msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
2684
				throw new DBUnexpectedError( $this, $msg );
2685
			} elseif ( !$this->mTrxAutomatic ) {
2686
				$msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
2687
				throw new DBUnexpectedError( $this, $msg );
2688
			} else {
2689
				// @TODO: make this an exception at some point
2690
				$msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
2691
				$this->queryLogger->error( $msg );
2692
				return; // join the main transaction set
2693
			}
2694
		} elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
2695
			// @TODO: make this an exception at some point
2696
			$msg = "$fname: Implicit transaction expected (DBO_TRX set).";
2697
			$this->queryLogger->error( $msg );
2698
			return; // let any writes be in the main transaction
2699
		}
2700
2701
		// Avoid fatals if close() was called
2702
		$this->assertOpen();
2703
2704
		$this->doBegin( $fname );
2705
		$this->mTrxTimestamp = microtime( true );
2706
		$this->mTrxFname = $fname;
2707
		$this->mTrxDoneWrites = false;
2708
		$this->mTrxAutomaticAtomic = false;
2709
		$this->mTrxAtomicLevels = [];
2710
		$this->mTrxShortId = sprintf( '%06x', mt_rand( 0, 0xffffff ) );
2711
		$this->mTrxWriteDuration = 0.0;
2712
		$this->mTrxWriteQueryCount = 0;
2713
		$this->mTrxWriteAdjDuration = 0.0;
2714
		$this->mTrxWriteAdjQueryCount = 0;
2715
		$this->mTrxWriteCallers = [];
2716
		// First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
2717
		// Get an estimate of the replica DB lag before then, treating estimate staleness
2718
		// as lag itself just to be safe
2719
		$status = $this->getApproximateLagStatus();
2720
		$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...
2721
		// T147697: make explicitTrxActive() return true until begin() finishes. This way, no
2722
		// caller will think its OK to muck around with the transaction just because startAtomic()
2723
		// has not yet completed (e.g. setting mTrxAtomicLevels).
2724
		$this->mTrxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
2725
	}
2726
2727
	/**
2728
	 * Issues the BEGIN command to the database server.
2729
	 *
2730
	 * @see Database::begin()
2731
	 * @param string $fname
2732
	 */
2733
	protected function doBegin( $fname ) {
2734
		$this->query( 'BEGIN', $fname );
2735
		$this->mTrxLevel = 1;
2736
	}
2737
2738
	final public function commit( $fname = __METHOD__, $flush = '' ) {
2739
		if ( $this->mTrxLevel && $this->mTrxAtomicLevels ) {
2740
			// There are still atomic sections open. This cannot be ignored
2741
			$levels = implode( ', ', $this->mTrxAtomicLevels );
2742
			throw new DBUnexpectedError(
2743
				$this,
2744
				"$fname: Got COMMIT while atomic sections $levels are still open."
2745
			);
2746
		}
2747
2748
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2749
			if ( !$this->mTrxLevel ) {
2750
				return; // nothing to do
2751
			} elseif ( !$this->mTrxAutomatic ) {
2752
				throw new DBUnexpectedError(
2753
					$this,
2754
					"$fname: Flushing an explicit transaction, getting out of sync."
2755
				);
2756
			}
2757
		} else {
2758
			if ( !$this->mTrxLevel ) {
2759
				$this->queryLogger->error(
2760
					"$fname: No transaction to commit, something got out of sync." );
2761
				return; // nothing to do
2762
			} elseif ( $this->mTrxAutomatic ) {
2763
				// @TODO: make this an exception at some point
2764
				$msg = "$fname: Explicit commit of implicit transaction.";
2765
				$this->queryLogger->error( $msg );
2766
				return; // wait for the main transaction set commit round
2767
			}
2768
		}
2769
2770
		// Avoid fatals if close() was called
2771
		$this->assertOpen();
2772
2773
		$this->runOnTransactionPreCommitCallbacks();
2774
		$writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
2775
		$this->doCommit( $fname );
2776
		if ( $this->mTrxDoneWrites ) {
2777
			$this->mLastWriteTime = microtime( true );
2778
			$this->trxProfiler->transactionWritingOut(
2779
				$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 2774 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...
2780
		}
2781
2782
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
2783
		$this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
2784
	}
2785
2786
	/**
2787
	 * Issues the COMMIT command to the database server.
2788
	 *
2789
	 * @see Database::commit()
2790
	 * @param string $fname
2791
	 */
2792
	protected function doCommit( $fname ) {
2793
		if ( $this->mTrxLevel ) {
2794
			$this->query( 'COMMIT', $fname );
2795
			$this->mTrxLevel = 0;
2796
		}
2797
	}
2798
2799
	final public function rollback( $fname = __METHOD__, $flush = '' ) {
2800
		if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
2801
			if ( !$this->mTrxLevel ) {
2802
				return; // nothing to do
2803
			}
2804
		} else {
2805
			if ( !$this->mTrxLevel ) {
2806
				$this->queryLogger->error(
2807
					"$fname: No transaction to rollback, something got out of sync." );
2808
				return; // nothing to do
2809
			} elseif ( $this->getFlag( self::DBO_TRX ) ) {
2810
				throw new DBUnexpectedError(
2811
					$this,
2812
					"$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
2813
				);
2814
			}
2815
		}
2816
2817
		// Avoid fatals if close() was called
2818
		$this->assertOpen();
2819
2820
		$this->doRollback( $fname );
2821
		$this->mTrxAtomicLevels = [];
2822
		if ( $this->mTrxDoneWrites ) {
2823
			$this->trxProfiler->transactionWritingOut(
2824
				$this->mServer, $this->mDBname, $this->mTrxShortId );
2825
		}
2826
2827
		$this->mTrxIdleCallbacks = []; // clear
2828
		$this->mTrxPreCommitCallbacks = []; // clear
2829
		$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
2830
		$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
2831
	}
2832
2833
	/**
2834
	 * Issues the ROLLBACK command to the database server.
2835
	 *
2836
	 * @see Database::rollback()
2837
	 * @param string $fname
2838
	 */
2839
	protected function doRollback( $fname ) {
2840
		if ( $this->mTrxLevel ) {
2841
			# Disconnects cause rollback anyway, so ignore those errors
2842
			$ignoreErrors = true;
2843
			$this->query( 'ROLLBACK', $fname, $ignoreErrors );
2844
			$this->mTrxLevel = 0;
2845
		}
2846
	}
2847
2848
	public function flushSnapshot( $fname = __METHOD__ ) {
2849
		if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
2850
			// This only flushes transactions to clear snapshots, not to write data
2851
			$fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
2852
			throw new DBUnexpectedError(
2853
				$this,
2854
				"$fname: Cannot flush snapshot because writes are pending ($fnames)."
2855
			);
2856
		}
2857
2858
		$this->commit( $fname, self::FLUSHING_INTERNAL );
2859
	}
2860
2861
	public function explicitTrxActive() {
2862
		return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
2863
	}
2864
2865
	/**
2866
	 * Creates a new table with structure copied from existing table
2867
	 * Note that unlike most database abstraction functions, this function does not
2868
	 * automatically append database prefix, because it works at a lower
2869
	 * abstraction level.
2870
	 * The table names passed to this function shall not be quoted (this
2871
	 * function calls addIdentifierQuotes when needed).
2872
	 *
2873
	 * @param string $oldName Name of table whose structure should be copied
2874
	 * @param string $newName Name of table to be created
2875
	 * @param bool $temporary Whether the new table should be temporary
2876
	 * @param string $fname Calling function name
2877
	 * @throws RuntimeException
2878
	 * @return bool True if operation was successful
2879
	 */
2880
	public function duplicateTableStructure( $oldName, $newName, $temporary = false,
2881
		$fname = __METHOD__
2882
	) {
2883
		throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
2884
	}
2885
2886
	public function listTables( $prefix = null, $fname = __METHOD__ ) {
2887
		throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
2888
	}
2889
2890
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
2891
		throw new RuntimeException( __METHOD__ . ' is not implemented in descendant class' );
2892
	}
2893
2894
	public function timestamp( $ts = 0 ) {
2895
		$t = new ConvertibleTimestamp( $ts );
2896
		// Let errors bubble up to avoid putting garbage in the DB
2897
		return $t->getTimestamp( TS_MW );
2898
	}
2899
2900
	public function timestampOrNull( $ts = null ) {
2901
		if ( is_null( $ts ) ) {
2902
			return null;
2903
		} else {
2904
			return $this->timestamp( $ts );
2905
		}
2906
	}
2907
2908
	/**
2909
	 * Take the result from a query, and wrap it in a ResultWrapper if
2910
	 * necessary. Boolean values are passed through as is, to indicate success
2911
	 * of write queries or failure.
2912
	 *
2913
	 * Once upon a time, Database::query() returned a bare MySQL result
2914
	 * resource, and it was necessary to call this function to convert it to
2915
	 * a wrapper. Nowadays, raw database objects are never exposed to external
2916
	 * callers, so this is unnecessary in external code.
2917
	 *
2918
	 * @param bool|ResultWrapper|resource|object $result
2919
	 * @return bool|ResultWrapper
2920
	 */
2921
	protected function resultObject( $result ) {
2922
		if ( !$result ) {
2923
			return false;
2924
		} elseif ( $result instanceof ResultWrapper ) {
2925
			return $result;
2926
		} elseif ( $result === true ) {
2927
			// Successful write query
2928
			return $result;
2929
		} else {
2930
			return new ResultWrapper( $this, $result );
2931
		}
2932
	}
2933
2934
	public function ping( &$rtt = null ) {
2935
		// Avoid hitting the server if it was hit recently
2936
		if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
2937
			if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
2938
				$rtt = $this->mRTTEstimate;
2939
				return true; // don't care about $rtt
2940
			}
2941
		}
2942
2943
		// This will reconnect if possible or return false if not
2944
		$this->clearFlag( self::DBO_TRX, self::REMEMBER_PRIOR );
2945
		$ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
2946
		$this->restoreFlags( self::RESTORE_PRIOR );
2947
2948
		if ( $ok ) {
2949
			$rtt = $this->mRTTEstimate;
2950
		}
2951
2952
		return $ok;
2953
	}
2954
2955
	/**
2956
	 * @return bool
2957
	 */
2958
	protected function reconnect() {
2959
		$this->closeConnection();
2960
		$this->mOpened = false;
2961
		$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...
2962
		try {
2963
			$this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
2964
			$this->lastPing = microtime( true );
2965
			$ok = true;
2966
		} catch ( DBConnectionError $e ) {
2967
			$ok = false;
2968
		}
2969
2970
		return $ok;
2971
	}
2972
2973
	public function getSessionLagStatus() {
2974
		return $this->getTransactionLagStatus() ?: $this->getApproximateLagStatus();
2975
	}
2976
2977
	/**
2978
	 * Get the replica DB lag when the current transaction started
2979
	 *
2980
	 * This is useful when transactions might use snapshot isolation
2981
	 * (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
2982
	 * is this lag plus transaction duration. If they don't, it is still
2983
	 * safe to be pessimistic. This returns null if there is no transaction.
2984
	 *
2985
	 * @return array|null ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
2986
	 * @since 1.27
2987
	 */
2988
	protected function getTransactionLagStatus() {
2989
		return $this->mTrxLevel
2990
			? [ 'lag' => $this->mTrxReplicaLag, 'since' => $this->trxTimestamp() ]
2991
			: null;
2992
	}
2993
2994
	/**
2995
	 * Get a replica DB lag estimate for this server
2996
	 *
2997
	 * @return array ('lag': seconds or false on error, 'since': UNIX timestamp of estimate)
2998
	 * @since 1.27
2999
	 */
3000
	protected function getApproximateLagStatus() {
3001
		return [
3002
			'lag'   => $this->getLBInfo( 'replica' ) ? $this->getLag() : 0,
3003
			'since' => microtime( true )
3004
		];
3005
	}
3006
3007
	/**
3008
	 * Merge the result of getSessionLagStatus() for several DBs
3009
	 * using the most pessimistic values to estimate the lag of
3010
	 * any data derived from them in combination
3011
	 *
3012
	 * This is information is useful for caching modules
3013
	 *
3014
	 * @see WANObjectCache::set()
3015
	 * @see WANObjectCache::getWithSetCallback()
3016
	 *
3017
	 * @param IDatabase $db1
3018
	 * @param IDatabase ...
3019
	 * @return array Map of values:
3020
	 *   - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
3021
	 *   - since: oldest UNIX timestamp of any of the DB lag estimates
3022
	 *   - pending: whether any of the DBs have uncommitted changes
3023
	 * @since 1.27
3024
	 */
3025
	public static function getCacheSetOptions( IDatabase $db1 ) {
3026
		$res = [ 'lag' => 0, 'since' => INF, 'pending' => false ];
3027
		foreach ( func_get_args() as $db ) {
3028
			/** @var IDatabase $db */
3029
			$status = $db->getSessionLagStatus();
3030
			if ( $status['lag'] === false ) {
3031
				$res['lag'] = false;
3032
			} elseif ( $res['lag'] !== false ) {
3033
				$res['lag'] = max( $res['lag'], $status['lag'] );
3034
			}
3035
			$res['since'] = min( $res['since'], $status['since'] );
3036
			$res['pending'] = $res['pending'] ?: $db->writesPending();
3037
		}
3038
3039
		return $res;
3040
	}
3041
3042
	public function getLag() {
3043
		return 0;
3044
	}
3045
3046
	public function maxListLen() {
3047
		return 0;
3048
	}
3049
3050
	public function encodeBlob( $b ) {
3051
		return $b;
3052
	}
3053
3054
	public function decodeBlob( $b ) {
3055
		if ( $b instanceof Blob ) {
3056
			$b = $b->fetch();
3057
		}
3058
		return $b;
3059
	}
3060
3061
	public function setSessionOptions( array $options ) {
3062
	}
3063
3064
	public function sourceFile(
3065
		$filename,
3066
		callable $lineCallback = null,
3067
		callable $resultCallback = null,
3068
		$fname = false,
3069
		callable $inputCallback = null
3070
	) {
3071
		MediaWiki\suppressWarnings();
3072
		$fp = fopen( $filename, 'r' );
3073
		MediaWiki\restoreWarnings();
3074
3075
		if ( false === $fp ) {
3076
			throw new RuntimeException( "Could not open \"{$filename}\".\n" );
3077
		}
3078
3079
		if ( !$fname ) {
3080
			$fname = __METHOD__ . "( $filename )";
3081
		}
3082
3083
		try {
3084
			$error = $this->sourceStream(
3085
				$fp, $lineCallback, $resultCallback, $fname, $inputCallback );
0 ignored issues
show
Bug introduced by
It seems like $fname defined by parameter $fname on line 3068 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...
3086
		} catch ( Exception $e ) {
3087
			fclose( $fp );
3088
			throw $e;
3089
		}
3090
3091
		fclose( $fp );
3092
3093
		return $error;
3094
	}
3095
3096
	public function setSchemaVars( $vars ) {
3097
		$this->mSchemaVars = $vars;
3098
	}
3099
3100
	public function sourceStream(
3101
		$fp,
3102
		callable $lineCallback = null,
3103
		callable $resultCallback = null,
3104
		$fname = __METHOD__,
3105
		callable $inputCallback = null
3106
	) {
3107
		$cmd = '';
3108
3109
		while ( !feof( $fp ) ) {
3110
			if ( $lineCallback ) {
3111
				call_user_func( $lineCallback );
3112
			}
3113
3114
			$line = trim( fgets( $fp ) );
3115
3116
			if ( $line == '' ) {
3117
				continue;
3118
			}
3119
3120
			if ( '-' == $line[0] && '-' == $line[1] ) {
3121
				continue;
3122
			}
3123
3124
			if ( $cmd != '' ) {
3125
				$cmd .= ' ';
3126
			}
3127
3128
			$done = $this->streamStatementEnd( $cmd, $line );
3129
3130
			$cmd .= "$line\n";
3131
3132
			if ( $done || feof( $fp ) ) {
3133
				$cmd = $this->replaceVars( $cmd );
3134
3135
				if ( !$inputCallback || call_user_func( $inputCallback, $cmd ) ) {
3136
					$res = $this->query( $cmd, $fname );
3137
3138
					if ( $resultCallback ) {
3139
						call_user_func( $resultCallback, $res, $this );
3140
					}
3141
3142
					if ( false === $res ) {
3143
						$err = $this->lastError();
3144
3145
						return "Query \"{$cmd}\" failed with error code \"$err\".\n";
3146
					}
3147
				}
3148
				$cmd = '';
3149
			}
3150
		}
3151
3152
		return true;
3153
	}
3154
3155
	/**
3156
	 * Called by sourceStream() to check if we've reached a statement end
3157
	 *
3158
	 * @param string &$sql SQL assembled so far
3159
	 * @param string &$newLine New line about to be added to $sql
3160
	 * @return bool Whether $newLine contains end of the statement
3161
	 */
3162
	public function streamStatementEnd( &$sql, &$newLine ) {
3163
		if ( $this->delimiter ) {
3164
			$prev = $newLine;
3165
			$newLine = preg_replace(
3166
				'/' . preg_quote( $this->delimiter, '/' ) . '$/', '', $newLine );
3167
			if ( $newLine != $prev ) {
3168
				return true;
3169
			}
3170
		}
3171
3172
		return false;
3173
	}
3174
3175
	/**
3176
	 * Database independent variable replacement. Replaces a set of variables
3177
	 * in an SQL statement with their contents as given by $this->getSchemaVars().
3178
	 *
3179
	 * Supports '{$var}' `{$var}` and / *$var* / (without the spaces) style variables.
3180
	 *
3181
	 * - '{$var}' should be used for text and is passed through the database's
3182
	 *   addQuotes method.
3183
	 * - `{$var}` should be used for identifiers (e.g. table and database names).
3184
	 *   It is passed through the database's addIdentifierQuotes method which
3185
	 *   can be overridden if the database uses something other than backticks.
3186
	 * - / *_* / or / *$wgDBprefix* / passes the name that follows through the
3187
	 *   database's tableName method.
3188
	 * - / *i* / passes the name that follows through the database's indexName method.
3189
	 * - In all other cases, / *$var* / is left unencoded. Except for table options,
3190
	 *   its use should be avoided. In 1.24 and older, string encoding was applied.
3191
	 *
3192
	 * @param string $ins SQL statement to replace variables in
3193
	 * @return string The new SQL statement with variables replaced
3194
	 */
3195
	protected function replaceVars( $ins ) {
3196
		$vars = $this->getSchemaVars();
3197
		return preg_replace_callback(
3198
			'!
3199
				/\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
3200
				\'\{\$ (\w+) }\'                  | # 3. addQuotes
3201
				`\{\$ (\w+) }`                    | # 4. addIdentifierQuotes
3202
				/\*\$ (\w+) \*/                     # 5. leave unencoded
3203
			!x',
3204
			function ( $m ) use ( $vars ) {
3205
				// Note: Because of <https://bugs.php.net/bug.php?id=51881>,
3206
				// check for both nonexistent keys *and* the empty string.
3207
				if ( isset( $m[1] ) && $m[1] !== '' ) {
3208
					if ( $m[1] === 'i' ) {
3209
						return $this->indexName( $m[2] );
3210
					} else {
3211
						return $this->tableName( $m[2] );
3212
					}
3213 View Code Duplication
				} elseif ( isset( $m[3] ) && $m[3] !== '' && array_key_exists( $m[3], $vars ) ) {
3214
					return $this->addQuotes( $vars[$m[3]] );
3215
				} elseif ( isset( $m[4] ) && $m[4] !== '' && array_key_exists( $m[4], $vars ) ) {
3216
					return $this->addIdentifierQuotes( $vars[$m[4]] );
3217 View Code Duplication
				} elseif ( isset( $m[5] ) && $m[5] !== '' && array_key_exists( $m[5], $vars ) ) {
3218
					return $vars[$m[5]];
3219
				} else {
3220
					return $m[0];
3221
				}
3222
			},
3223
			$ins
3224
		);
3225
	}
3226
3227
	/**
3228
	 * Get schema variables. If none have been set via setSchemaVars(), then
3229
	 * use some defaults from the current object.
3230
	 *
3231
	 * @return array
3232
	 */
3233
	protected function getSchemaVars() {
3234
		if ( $this->mSchemaVars ) {
3235
			return $this->mSchemaVars;
3236
		} else {
3237
			return $this->getDefaultSchemaVars();
3238
		}
3239
	}
3240
3241
	/**
3242
	 * Get schema variables to use if none have been set via setSchemaVars().
3243
	 *
3244
	 * Override this in derived classes to provide variables for tables.sql
3245
	 * and SQL patch files.
3246
	 *
3247
	 * @return array
3248
	 */
3249
	protected function getDefaultSchemaVars() {
3250
		return [];
3251
	}
3252
3253
	public function lockIsFree( $lockName, $method ) {
3254
		return true;
3255
	}
3256
3257
	public function lock( $lockName, $method, $timeout = 5 ) {
3258
		$this->mNamedLocksHeld[$lockName] = 1;
3259
3260
		return true;
3261
	}
3262
3263
	public function unlock( $lockName, $method ) {
3264
		unset( $this->mNamedLocksHeld[$lockName] );
3265
3266
		return true;
3267
	}
3268
3269
	public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
3270
		if ( $this->writesOrCallbacksPending() ) {
3271
			// This only flushes transactions to clear snapshots, not to write data
3272
			$fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
3273
			throw new DBUnexpectedError(
3274
				$this,
3275
				"$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
3276
			);
3277
		}
3278
3279
		if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
3280
			return null;
3281
		}
3282
3283
		$unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
3284
			if ( $this->trxLevel() ) {
3285
				// There is a good chance an exception was thrown, causing any early return
3286
				// from the caller. Let any error handler get a chance to issue rollback().
3287
				// If there isn't one, let the error bubble up and trigger server-side rollback.
3288
				$this->onTransactionResolution(
3289
					function () use ( $lockKey, $fname ) {
3290
						$this->unlock( $lockKey, $fname );
3291
					},
3292
					$fname
3293
				);
3294
			} else {
3295
				$this->unlock( $lockKey, $fname );
3296
			}
3297
		} );
3298
3299
		$this->commit( $fname, self::FLUSHING_INTERNAL );
3300
3301
		return $unlocker;
3302
	}
3303
3304
	public function namedLocksEnqueue() {
3305
		return false;
3306
	}
3307
3308
	/**
3309
	 * Lock specific tables
3310
	 *
3311
	 * @param array $read Array of tables to lock for read access
3312
	 * @param array $write Array of tables to lock for write access
3313
	 * @param string $method Name of caller
3314
	 * @param bool $lowPriority Whether to indicate writes to be LOW PRIORITY
3315
	 * @return bool
3316
	 */
3317
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
3318
		return true;
3319
	}
3320
3321
	/**
3322
	 * Unlock specific tables
3323
	 *
3324
	 * @param string $method The caller
3325
	 * @return bool
3326
	 */
3327
	public function unlockTables( $method ) {
3328
		return true;
3329
	}
3330
3331
	/**
3332
	 * Delete a table
3333
	 * @param string $tableName
3334
	 * @param string $fName
3335
	 * @return bool|ResultWrapper
3336
	 * @since 1.18
3337
	 */
3338 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
3339
		if ( !$this->tableExists( $tableName, $fName ) ) {
3340
			return false;
3341
		}
3342
		$sql = "DROP TABLE " . $this->tableName( $tableName ) . " CASCADE";
3343
3344
		return $this->query( $sql, $fName );
3345
	}
3346
3347
	public function getInfinity() {
3348
		return 'infinity';
3349
	}
3350
3351
	public function encodeExpiry( $expiry ) {
3352
		return ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() )
3353
			? $this->getInfinity()
3354
			: $this->timestamp( $expiry );
3355
	}
3356
3357
	public function decodeExpiry( $expiry, $format = TS_MW ) {
3358
		if ( $expiry == '' || $expiry == 'infinity' || $expiry == $this->getInfinity() ) {
3359
			return 'infinity';
3360
		}
3361
3362
		return ConvertibleTimestamp::convert( $format, $expiry );
3363
	}
3364
3365
	public function setBigSelects( $value = true ) {
3366
		// no-op
3367
	}
3368
3369
	public function isReadOnly() {
3370
		return ( $this->getReadOnlyReason() !== false );
3371
	}
3372
3373
	/**
3374
	 * @return string|bool Reason this DB is read-only or false if it is not
3375
	 */
3376
	protected function getReadOnlyReason() {
3377
		$reason = $this->getLBInfo( 'readOnlyReason' );
3378
3379
		return is_string( $reason ) ? $reason : false;
3380
	}
3381
3382
	public function setTableAliases( array $aliases ) {
3383
		$this->tableAliases = $aliases;
3384
	}
3385
3386
	/**
3387
	 * @return bool Whether a DB user is required to access the DB
3388
	 * @since 1.28
3389
	 */
3390
	protected function requiresDatabaseUser() {
3391
		return true;
3392
	}
3393
3394
	/**
3395
	 * Get the underlying binding handle, mConn
3396
	 *
3397
	 * Makes sure that mConn is set (disconnects and ping() failure can unset it).
3398
	 * This catches broken callers than catch and ignore disconnection exceptions.
3399
	 * Unlike checking isOpen(), this is safe to call inside of open().
3400
	 *
3401
	 * @return resource|object
3402
	 * @throws DBUnexpectedError
3403
	 * @since 1.26
3404
	 */
3405
	protected function getBindingHandle() {
3406
		if ( !$this->mConn ) {
3407
			throw new DBUnexpectedError(
3408
				$this,
3409
				'DB connection was already closed or the connection dropped.'
3410
			);
3411
		}
3412
3413
		return $this->mConn;
3414
	}
3415
3416
	/**
3417
	 * @since 1.19
3418
	 * @return string
3419
	 */
3420
	public function __toString() {
3421
		return (string)$this->mConn;
3422
	}
3423
3424
	/**
3425
	 * Make sure that copies do not share the same client binding handle
3426
	 * @throws DBConnectionError
3427
	 */
3428
	public function __clone() {
3429
		$this->connLogger->warning(
3430
			"Cloning " . get_class( $this ) . " is not recomended; forking connection:\n" .
3431
			( new RuntimeException() )->getTraceAsString()
3432
		);
3433
3434
		if ( $this->isOpen() ) {
3435
			// Open a new connection resource without messing with the old one
3436
			$this->mOpened = false;
3437
			$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...
3438
			$this->mTrxEndCallbacks = []; // don't copy
3439
			$this->handleSessionLoss(); // no trx or locks anymore
3440
			$this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
3441
			$this->lastPing = microtime( true );
3442
		}
3443
	}
3444
3445
	/**
3446
	 * Called by serialize. Throw an exception when DB connection is serialized.
3447
	 * This causes problems on some database engines because the connection is
3448
	 * not restored on unserialize.
3449
	 */
3450
	public function __sleep() {
3451
		throw new RuntimeException( 'Database serialization may cause problems, since ' .
3452
			'the connection is not restored on wakeup.' );
3453
	}
3454
3455
	/**
3456
	 * Run a few simple sanity checks and close dangling connections
3457
	 */
3458
	public function __destruct() {
3459
		if ( $this->mTrxLevel && $this->mTrxDoneWrites ) {
3460
			trigger_error( "Uncommitted DB writes (transaction from {$this->mTrxFname})." );
3461
		}
3462
3463
		$danglingWriters = $this->pendingWriteAndCallbackCallers();
3464
		if ( $danglingWriters ) {
3465
			$fnames = implode( ', ', $danglingWriters );
3466
			trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
3467
		}
3468
3469
		if ( $this->mConn ) {
3470
			// Avoid connection leaks for sanity. Normally, resources close at script completion.
3471
			// The connection might already be closed in zend/hhvm by now, so suppress warnings.
3472
			\MediaWiki\suppressWarnings();
3473
			$this->closeConnection();
3474
			\MediaWiki\restoreWarnings();
3475
			$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...
3476
			$this->mOpened = false;
3477
		}
3478
	}
3479
}
3480
3481
class_alias( 'Database', 'DatabaseBase' );
3482