Completed
Branch master (05c729)
by
unknown
20:00
created

includes/libs/rdbms/database/DatabaseMysqlBase.php (24 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * This is the MySQL database abstraction layer.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Database
22
 */
23
24
/**
25
 * Database abstraction object for MySQL.
26
 * Defines methods independent on used MySQL extension.
27
 *
28
 * @ingroup Database
29
 * @since 1.22
30
 * @see Database
31
 */
32
abstract class DatabaseMysqlBase extends Database {
33
	/** @var MysqlMasterPos */
34
	protected $lastKnownReplicaPos;
35
	/** @var string Method to detect replica DB lag */
36
	protected $lagDetectionMethod;
37
	/** @var array Method to detect replica DB lag */
38
	protected $lagDetectionOptions = [];
39
	/** @var bool bool Whether to use GTID methods */
40
	protected $useGTIDs = false;
41
	/** @var string|null */
42
	protected $sslKeyPath;
43
	/** @var string|null */
44
	protected $sslCertPath;
45
	/** @var string|null */
46
	protected $sslCAPath;
47
	/** @var string[]|null */
48
	protected $sslCiphers;
49
	/** @var string sql_mode value to send on connection */
50
	protected $sqlMode;
51
	/** @var bool Use experimental UTF-8 transmission encoding */
52
	protected $utf8Mode;
53
54
	/** @var string|null */
55
	private $serverVersion = null;
56
57
	/**
58
	 * Additional $params include:
59
	 *   - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
60
	 *       pt-heartbeat assumes the table is at heartbeat.heartbeat
61
	 *       and uses UTC timestamps in the heartbeat.ts column.
62
	 *       (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html)
63
	 *   - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map to change
64
	 *       the default behavior. Normally, the heartbeat row with the server
65
	 *       ID of this server's master will be used. Set the "conds" field to
66
	 *       override the query conditions, e.g. ['shard' => 's1'].
67
	 *   - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible.
68
	 *   - sslKeyPath : path to key file [default: null]
69
	 *   - sslCertPath : path to certificate file [default: null]
70
	 *   - sslCAPath : parth to certificate authority PEM files [default: null]
71
	 *   - sslCiphers : array list of allowable ciphers [default: null]
72
	 * @param array $params
73
	 */
74
	function __construct( array $params ) {
75
		$this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
76
			? $params['lagDetectionMethod']
77
			: 'Seconds_Behind_Master';
78
		$this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
79
			? $params['lagDetectionOptions']
80
			: [];
81
		$this->useGTIDs = !empty( $params['useGTIDs' ] );
82
		foreach ( [ 'KeyPath', 'CertPath', 'CAPath', 'Ciphers' ] as $name ) {
83
			$var = "ssl{$name}";
84
			if ( isset( $params[$var] ) ) {
85
				$this->$var = $params[$var];
86
			}
87
		}
88
		$this->sqlMode = isset( $params['sqlMode'] ) ? $params['sqlMode'] : '';
89
		$this->utf8Mode = !empty( $params['utf8Mode'] );
90
91
		parent::__construct( $params );
92
	}
93
94
	/**
95
	 * @return string
96
	 */
97
	public function getType() {
98
		return 'mysql';
99
	}
100
101
	/**
102
	 * @param string $server
103
	 * @param string $user
104
	 * @param string $password
105
	 * @param string $dbName
106
	 * @throws Exception|DBConnectionError
107
	 * @return bool
108
	 */
109
	public function open( $server, $user, $password, $dbName ) {
110
		# Close/unset connection handle
111
		$this->close();
112
113
		$this->mServer = $server;
114
		$this->mUser = $user;
115
		$this->mPassword = $password;
116
		$this->mDBname = $dbName;
117
118
		$this->installErrorHandler();
119
		try {
120
			$this->mConn = $this->mysqlConnect( $this->mServer );
121
		} catch ( Exception $ex ) {
122
			$this->restoreErrorHandler();
123
			throw $ex;
124
		}
125
		$error = $this->restoreErrorHandler();
126
127
		# Always log connection errors
128
		if ( !$this->mConn ) {
129
			if ( !$error ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $error of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
130
				$error = $this->lastError();
131
			}
132
			$this->connLogger->error(
133
				"Error connecting to {db_server}: {error}",
134
				$this->getLogContext( [
135
					'method' => __METHOD__,
136
					'error' => $error,
137
				] )
138
			);
139
			$this->connLogger->debug( "DB connection error\n" .
140
				"Server: $server, User: $user, Password: " .
141
				substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
142
143
			$this->reportConnectionError( $error );
144
		}
145
146
		if ( $dbName != '' ) {
147
			MediaWiki\suppressWarnings();
148
			$success = $this->selectDB( $dbName );
149
			MediaWiki\restoreWarnings();
150
			if ( !$success ) {
151
				$this->queryLogger->error(
152
					"Error selecting database {db_name} on server {db_server}",
153
					$this->getLogContext( [
154
						'method' => __METHOD__,
155
					] )
156
				);
157
				$this->queryLogger->debug(
158
					"Error selecting database $dbName on server {$this->mServer}" );
159
160
				$this->reportConnectionError( "Error selecting database $dbName" );
161
			}
162
		}
163
164
		// Tell the server what we're communicating with
165
		if ( !$this->connectInitCharset() ) {
166
			$this->reportConnectionError( "Error setting character set" );
167
		}
168
169
		// Abstract over any insane MySQL defaults
170
		$set = [ 'group_concat_max_len = 262144' ];
171
		// Set SQL mode, default is turning them all off, can be overridden or skipped with null
172
		if ( is_string( $this->sqlMode ) ) {
173
			$set[] = 'sql_mode = ' . $this->addQuotes( $this->sqlMode );
174
		}
175
		// Set any custom settings defined by site config
176
		// (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html)
177
		foreach ( $this->mSessionVars as $var => $val ) {
178
			// Escape strings but not numbers to avoid MySQL complaining
179
			if ( !is_int( $val ) && !is_float( $val ) ) {
180
				$val = $this->addQuotes( $val );
181
			}
182
			$set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val;
183
		}
184
185
		if ( $set ) {
186
			// Use doQuery() to avoid opening implicit transactions (DBO_TRX)
187
			$success = $this->doQuery( 'SET ' . implode( ', ', $set ) );
188
			if ( !$success ) {
189
				$this->queryLogger->error(
190
					'Error setting MySQL variables on server {db_server} (check $wgSQLMode)',
191
					$this->getLogContext( [
192
						'method' => __METHOD__,
193
					] )
194
				);
195
				$this->reportConnectionError(
196
					'Error setting MySQL variables on server {db_server} (check $wgSQLMode)' );
197
			}
198
		}
199
200
		$this->mOpened = true;
201
202
		return true;
203
	}
204
205
	/**
206
	 * Set the character set information right after connection
207
	 * @return bool
208
	 */
209
	protected function connectInitCharset() {
210
		if ( $this->utf8Mode ) {
211
			// Tell the server we're communicating with it in UTF-8.
212
			// This may engage various charset conversions.
213
			return $this->mysqlSetCharset( 'utf8' );
214
		} else {
215
			return $this->mysqlSetCharset( 'binary' );
216
		}
217
	}
218
219
	/**
220
	 * Open a connection to a MySQL server
221
	 *
222
	 * @param string $realServer
223
	 * @return mixed Raw connection
224
	 * @throws DBConnectionError
225
	 */
226
	abstract protected function mysqlConnect( $realServer );
227
228
	/**
229
	 * Set the character set of the MySQL link
230
	 *
231
	 * @param string $charset
232
	 * @return bool
233
	 */
234
	abstract protected function mysqlSetCharset( $charset );
235
236
	/**
237
	 * @param ResultWrapper|resource $res
238
	 * @throws DBUnexpectedError
239
	 */
240 View Code Duplication
	public function freeResult( $res ) {
241
		if ( $res instanceof ResultWrapper ) {
242
			$res = $res->result;
243
		}
244
		MediaWiki\suppressWarnings();
245
		$ok = $this->mysqlFreeResult( $res );
0 ignored issues
show
It seems like $res can also be of type array or null; however, DatabaseMysqlBase::mysqlFreeResult() does only seem to accept resource, 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...
246
		MediaWiki\restoreWarnings();
247
		if ( !$ok ) {
248
			throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
249
		}
250
	}
251
252
	/**
253
	 * Free result memory
254
	 *
255
	 * @param resource $res Raw result
256
	 * @return bool
257
	 */
258
	abstract protected function mysqlFreeResult( $res );
259
260
	/**
261
	 * @param ResultWrapper|resource $res
262
	 * @return stdClass|bool
263
	 * @throws DBUnexpectedError
264
	 */
265 View Code Duplication
	public function fetchObject( $res ) {
266
		if ( $res instanceof ResultWrapper ) {
267
			$res = $res->result;
268
		}
269
		MediaWiki\suppressWarnings();
270
		$row = $this->mysqlFetchObject( $res );
0 ignored issues
show
It seems like $res can also be of type array or null; however, DatabaseMysqlBase::mysqlFetchObject() does only seem to accept resource, 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...
271
		MediaWiki\restoreWarnings();
272
273
		$errno = $this->lastErrno();
274
		// Unfortunately, mysql_fetch_object does not reset the last errno.
275
		// Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
276
		// these are the only errors mysql_fetch_object can cause.
277
		// See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
278
		if ( $errno == 2000 || $errno == 2013 ) {
279
			throw new DBUnexpectedError(
280
				$this,
281
				'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() )
282
			);
283
		}
284
285
		return $row;
286
	}
287
288
	/**
289
	 * Fetch a result row as an object
290
	 *
291
	 * @param resource $res Raw result
292
	 * @return stdClass
293
	 */
294
	abstract protected function mysqlFetchObject( $res );
295
296
	/**
297
	 * @param ResultWrapper|resource $res
298
	 * @return array|bool
299
	 * @throws DBUnexpectedError
300
	 */
301 View Code Duplication
	public function fetchRow( $res ) {
302
		if ( $res instanceof ResultWrapper ) {
303
			$res = $res->result;
304
		}
305
		MediaWiki\suppressWarnings();
306
		$row = $this->mysqlFetchArray( $res );
0 ignored issues
show
It seems like $res can also be of type array or null; however, DatabaseMysqlBase::mysqlFetchArray() does only seem to accept resource, 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...
307
		MediaWiki\restoreWarnings();
308
309
		$errno = $this->lastErrno();
310
		// Unfortunately, mysql_fetch_array does not reset the last errno.
311
		// Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
312
		// these are the only errors mysql_fetch_array can cause.
313
		// See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
314
		if ( $errno == 2000 || $errno == 2013 ) {
315
			throw new DBUnexpectedError(
316
				$this,
317
				'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() )
318
			);
319
		}
320
321
		return $row;
322
	}
323
324
	/**
325
	 * Fetch a result row as an associative and numeric array
326
	 *
327
	 * @param resource $res Raw result
328
	 * @return array
329
	 */
330
	abstract protected function mysqlFetchArray( $res );
331
332
	/**
333
	 * @throws DBUnexpectedError
334
	 * @param ResultWrapper|resource $res
335
	 * @return int
336
	 */
337
	function numRows( $res ) {
338
		if ( $res instanceof ResultWrapper ) {
339
			$res = $res->result;
340
		}
341
		MediaWiki\suppressWarnings();
342
		$n = $this->mysqlNumRows( $res );
0 ignored issues
show
It seems like $res can also be of type array or null; however, DatabaseMysqlBase::mysqlNumRows() does only seem to accept resource, 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...
343
		MediaWiki\restoreWarnings();
344
345
		// Unfortunately, mysql_num_rows does not reset the last errno.
346
		// We are not checking for any errors here, since
347
		// these are no errors mysql_num_rows can cause.
348
		// See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
349
		// See https://phabricator.wikimedia.org/T44430
350
		return $n;
351
	}
352
353
	/**
354
	 * Get number of rows in result
355
	 *
356
	 * @param resource $res Raw result
357
	 * @return int
358
	 */
359
	abstract protected function mysqlNumRows( $res );
360
361
	/**
362
	 * @param ResultWrapper|resource $res
363
	 * @return int
364
	 */
365
	public function numFields( $res ) {
366
		if ( $res instanceof ResultWrapper ) {
367
			$res = $res->result;
368
		}
369
370
		return $this->mysqlNumFields( $res );
0 ignored issues
show
It seems like $res can also be of type array or null; however, DatabaseMysqlBase::mysqlNumFields() does only seem to accept resource, 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...
371
	}
372
373
	/**
374
	 * Get number of fields in result
375
	 *
376
	 * @param resource $res Raw result
377
	 * @return int
378
	 */
379
	abstract protected function mysqlNumFields( $res );
380
381
	/**
382
	 * @param ResultWrapper|resource $res
383
	 * @param int $n
384
	 * @return string
385
	 */
386
	public function fieldName( $res, $n ) {
387
		if ( $res instanceof ResultWrapper ) {
388
			$res = $res->result;
389
		}
390
391
		return $this->mysqlFieldName( $res, $n );
0 ignored issues
show
It seems like $res defined by $res->result on line 388 can also be of type array or null; however, DatabaseMysqlBase::mysqlFieldName() does only seem to accept object<ResultWrapper>|resource, 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...
392
	}
393
394
	/**
395
	 * Get the name of the specified field in a result
396
	 *
397
	 * @param ResultWrapper|resource $res
398
	 * @param int $n
399
	 * @return string
400
	 */
401
	abstract protected function mysqlFieldName( $res, $n );
402
403
	/**
404
	 * mysql_field_type() wrapper
405
	 * @param ResultWrapper|resource $res
406
	 * @param int $n
407
	 * @return string
408
	 */
409
	public function fieldType( $res, $n ) {
410
		if ( $res instanceof ResultWrapper ) {
411
			$res = $res->result;
412
		}
413
414
		return $this->mysqlFieldType( $res, $n );
415
	}
416
417
	/**
418
	 * Get the type of the specified field in a result
419
	 *
420
	 * @param ResultWrapper|resource $res
421
	 * @param int $n
422
	 * @return string
423
	 */
424
	abstract protected function mysqlFieldType( $res, $n );
425
426
	/**
427
	 * @param ResultWrapper|resource $res
428
	 * @param int $row
429
	 * @return bool
430
	 */
431
	public function dataSeek( $res, $row ) {
432
		if ( $res instanceof ResultWrapper ) {
433
			$res = $res->result;
434
		}
435
436
		return $this->mysqlDataSeek( $res, $row );
0 ignored issues
show
It seems like $res defined by $res->result on line 433 can also be of type array or null; however, DatabaseMysqlBase::mysqlDataSeek() does only seem to accept object<ResultWrapper>|resource, 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...
437
	}
438
439
	/**
440
	 * Move internal result pointer
441
	 *
442
	 * @param ResultWrapper|resource $res
443
	 * @param int $row
444
	 * @return bool
445
	 */
446
	abstract protected function mysqlDataSeek( $res, $row );
447
448
	/**
449
	 * @return string
450
	 */
451
	public function lastError() {
452
		if ( $this->mConn ) {
453
			# Even if it's non-zero, it can still be invalid
454
			MediaWiki\suppressWarnings();
455
			$error = $this->mysqlError( $this->mConn );
456
			if ( !$error ) {
457
				$error = $this->mysqlError();
458
			}
459
			MediaWiki\restoreWarnings();
460
		} else {
461
			$error = $this->mysqlError();
462
		}
463
		if ( $error ) {
464
			$error .= ' (' . $this->mServer . ')';
465
		}
466
467
		return $error;
468
	}
469
470
	/**
471
	 * Returns the text of the error message from previous MySQL operation
472
	 *
473
	 * @param resource $conn Raw connection
474
	 * @return string
475
	 */
476
	abstract protected function mysqlError( $conn = null );
477
478
	/**
479
	 * @param string $table
480
	 * @param array $uniqueIndexes
481
	 * @param array $rows
482
	 * @param string $fname
483
	 * @return ResultWrapper
484
	 */
485
	public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
486
		return $this->nativeReplace( $table, $rows, $fname );
487
	}
488
489
	/**
490
	 * Estimate rows in dataset
491
	 * Returns estimated count, based on EXPLAIN output
492
	 * Takes same arguments as Database::select()
493
	 *
494
	 * @param string|array $table
495
	 * @param string|array $vars
496
	 * @param string|array $conds
497
	 * @param string $fname
498
	 * @param string|array $options
499
	 * @return bool|int
500
	 */
501
	public function estimateRowCount( $table, $vars = '*', $conds = '',
502
		$fname = __METHOD__, $options = []
503
	) {
504
		$options['EXPLAIN'] = true;
505
		$res = $this->select( $table, $vars, $conds, $fname, $options );
0 ignored issues
show
It seems like $conds defined by parameter $conds on line 501 can also be of type array; however, Database::select() 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...
It seems like $options defined by parameter $options on line 502 can also be of type string; however, Database::select() does only seem to accept array, 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...
506
		if ( $res === false ) {
507
			return false;
508
		}
509
		if ( !$this->numRows( $res ) ) {
0 ignored issues
show
It seems like $res defined by $this->select($table, $v...onds, $fname, $options) on line 505 can also be of type boolean; however, DatabaseMysqlBase::numRows() does only seem to accept object<ResultWrapper>|resource, 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...
510
			return 0;
511
		}
512
513
		$rows = 1;
514
		foreach ( $res as $plan ) {
0 ignored issues
show
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...
515
			$rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
516
		}
517
518
		return (int)$rows;
519
	}
520
521
	public function tableExists( $table, $fname = __METHOD__ ) {
522
		$table = $this->tableName( $table, 'raw' );
523
		if ( isset( $this->mSessionTempTables[$table] ) ) {
524
			return true; // already known to exist and won't show in SHOW TABLES anyway
525
		}
526
527
		$encLike = $this->buildLike( $table );
528
529
		return $this->query( "SHOW TABLES $encLike", $fname )->numRows() > 0;
530
	}
531
532
	/**
533
	 * @param string $table
534
	 * @param string $field
535
	 * @return bool|MySQLField
536
	 */
537
	public function fieldInfo( $table, $field ) {
538
		$table = $this->tableName( $table );
539
		$res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
540
		if ( !$res ) {
541
			return false;
542
		}
543
		$n = $this->mysqlNumFields( $res->result );
544
		for ( $i = 0; $i < $n; $i++ ) {
545
			$meta = $this->mysqlFetchField( $res->result, $i );
546
			if ( $field == $meta->name ) {
547
				return new MySQLField( $meta );
548
			}
549
		}
550
551
		return false;
552
	}
553
554
	/**
555
	 * Get column information from a result
556
	 *
557
	 * @param resource $res Raw result
558
	 * @param int $n
559
	 * @return stdClass
560
	 */
561
	abstract protected function mysqlFetchField( $res, $n );
562
563
	/**
564
	 * Get information about an index into an object
565
	 * Returns false if the index does not exist
566
	 *
567
	 * @param string $table
568
	 * @param string $index
569
	 * @param string $fname
570
	 * @return bool|array|null False or null on failure
571
	 */
572
	public function indexInfo( $table, $index, $fname = __METHOD__ ) {
573
		# SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
574
		# SHOW INDEX should work for 3.x and up:
575
		# http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
576
		$table = $this->tableName( $table );
577
		$index = $this->indexName( $index );
578
579
		$sql = 'SHOW INDEX FROM ' . $table;
580
		$res = $this->query( $sql, $fname );
581
582
		if ( !$res ) {
583
			return null;
584
		}
585
586
		$result = [];
587
588
		foreach ( $res as $row ) {
0 ignored issues
show
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...
589
			if ( $row->Key_name == $index ) {
590
				$result[] = $row;
591
			}
592
		}
593
594
		return empty( $result ) ? false : $result;
595
	}
596
597
	/**
598
	 * @param string $s
599
	 * @return string
600
	 */
601
	public function strencode( $s ) {
602
		return $this->mysqlRealEscapeString( $s );
603
	}
604
605
	/**
606
	 * @param string $s
607
	 * @return mixed
608
	 */
609
	abstract protected function mysqlRealEscapeString( $s );
610
611
	public function addQuotes( $s ) {
612
		if ( is_bool( $s ) ) {
613
			// Parent would transform to int, which does not play nice with MySQL type juggling.
614
			// When searching for an int in a string column, the strings are cast to int, which
615
			// means false would match any string not starting with a number.
616
			$s = (string)(int)$s;
617
		}
618
		return parent::addQuotes( $s );
619
	}
620
621
	/**
622
	 * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
623
	 *
624
	 * @param string $s
625
	 * @return string
626
	 */
627
	public function addIdentifierQuotes( $s ) {
628
		// Characters in the range \u0001-\uFFFF are valid in a quoted identifier
629
		// Remove NUL bytes and escape backticks by doubling
630
		return '`' . str_replace( [ "\0", '`' ], [ '', '``' ], $s ) . '`';
631
	}
632
633
	/**
634
	 * @param string $name
635
	 * @return bool
636
	 */
637
	public function isQuotedIdentifier( $name ) {
638
		return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
639
	}
640
641
	public function getLag() {
642
		if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
643
			return $this->getLagFromPtHeartbeat();
644
		} else {
645
			return $this->getLagFromSlaveStatus();
646
		}
647
	}
648
649
	/**
650
	 * @return string
651
	 */
652
	protected function getLagDetectionMethod() {
653
		return $this->lagDetectionMethod;
654
	}
655
656
	/**
657
	 * @return bool|int
658
	 */
659
	protected function getLagFromSlaveStatus() {
660
		$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
661
		$row = $res ? $res->fetchObject() : false;
662
		if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
663
			return intval( $row->Seconds_Behind_Master );
664
		}
665
666
		return false;
667
	}
668
669
	/**
670
	 * @return bool|float
671
	 */
672
	protected function getLagFromPtHeartbeat() {
673
		$options = $this->lagDetectionOptions;
674
675
		if ( isset( $options['conds'] ) ) {
676
			// Best method for multi-DC setups: use logical channel names
677
			$data = $this->getHeartbeatData( $options['conds'] );
678
		} else {
679
			// Standard method: use master server ID (works with stock pt-heartbeat)
680
			$masterInfo = $this->getMasterServerInfo();
681
			if ( !$masterInfo ) {
682
				$this->queryLogger->error(
683
					"Unable to query master of {db_server} for server ID",
684
					$this->getLogContext( [
685
						'method' => __METHOD__
686
					] )
687
				);
688
689
				return false; // could not get master server ID
690
			}
691
692
			$conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
693
			$data = $this->getHeartbeatData( $conds );
694
		}
695
696
		list( $time, $nowUnix ) = $data;
697
		if ( $time !== null ) {
698
			// @time is in ISO format like "2015-09-25T16:48:10.000510"
699
			$dateTime = new DateTime( $time, new DateTimeZone( 'UTC' ) );
700
			$timeUnix = (int)$dateTime->format( 'U' ) + $dateTime->format( 'u' ) / 1e6;
701
702
			return max( $nowUnix - $timeUnix, 0.0 );
703
		}
704
705
		$this->queryLogger->error(
706
			"Unable to find pt-heartbeat row for {db_server}",
707
			$this->getLogContext( [
708
				'method' => __METHOD__
709
			] )
710
		);
711
712
		return false;
713
	}
714
715
	protected function getMasterServerInfo() {
716
		$cache = $this->srvCache;
717
		$key = $cache->makeGlobalKey(
718
			'mysql',
719
			'master-info',
720
			// Using one key for all cluster replica DBs is preferable
721
			$this->getLBInfo( 'clusterMasterHost' ) ?: $this->getServer()
722
		);
723
724
		return $cache->getWithSetCallback(
725
			$key,
726
			$cache::TTL_INDEFINITE,
727
			function () use ( $cache, $key ) {
728
				// Get and leave a lock key in place for a short period
729
				if ( !$cache->lock( $key, 0, 10 ) ) {
730
					return false; // avoid master connection spike slams
731
				}
732
733
				$conn = $this->getLazyMasterHandle();
734
				if ( !$conn ) {
735
					return false; // something is misconfigured
736
				}
737
738
				// Connect to and query the master; catch errors to avoid outages
739
				try {
740
					$res = $conn->query( 'SELECT @@server_id AS id', __METHOD__ );
741
					$row = $res ? $res->fetchObject() : false;
742
					$id = $row ? (int)$row->id : 0;
743
				} catch ( DBError $e ) {
744
					$id = 0;
745
				}
746
747
				// Cache the ID if it was retrieved
748
				return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false;
749
			}
750
		);
751
	}
752
753
	/**
754
	 * @param array $conds WHERE clause conditions to find a row
755
	 * @return array (heartbeat `ts` column value or null, UNIX timestamp) for the newest beat
756
	 * @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html
757
	 */
758
	protected function getHeartbeatData( array $conds ) {
759
		$whereSQL = $this->makeList( $conds, self::LIST_AND );
760
		// Use ORDER BY for channel based queries since that field might not be UNIQUE.
761
		// Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
762
		// percision field is not supported in MySQL <= 5.5.
763
		$res = $this->query(
764
			"SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1"
765
		);
766
		$row = $res ? $res->fetchObject() : false;
767
768
		return [ $row ? $row->ts : null, microtime( true ) ];
769
	}
770
771
	protected function getApproximateLagStatus() {
772
		if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
773
			// Disable caching since this is fast enough and we don't wan't
774
			// to be *too* pessimistic by having both the cache TTL and the
775
			// pt-heartbeat interval count as lag in getSessionLagStatus()
776
			return parent::getApproximateLagStatus();
777
		}
778
779
		$key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServer() );
780
		$approxLag = $this->srvCache->get( $key );
781
		if ( !$approxLag ) {
782
			$approxLag = parent::getApproximateLagStatus();
783
			$this->srvCache->set( $key, $approxLag, 1 );
784
		}
785
786
		return $approxLag;
787
	}
788
789
	public function masterPosWait( DBMasterPos $pos, $timeout ) {
790
		if ( !( $pos instanceof MySQLMasterPos ) ) {
791
			throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" );
792
		}
793
794
		if ( $this->getLBInfo( 'is static' ) === true ) {
795
			return 0; // this is a copy of a read-only dataset with no master DB
796
		} elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) {
797
			return 0; // already reached this point for sure
798
		}
799
800
		// Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
801
		if ( $this->useGTIDs && $pos->gtids ) {
802
			// Wait on the GTID set (MariaDB only)
803
			$gtidArg = $this->addQuotes( implode( ',', $pos->gtids ) );
804
			$res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
805
		} else {
806
			// Wait on the binlog coordinates
807
			$encFile = $this->addQuotes( $pos->file );
808
			$encPos = intval( $pos->pos );
809
			$res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
810
		}
811
812
		$row = $res ? $this->fetchRow( $res ) : false;
813
		if ( !$row ) {
814
			throw new DBExpectedError( $this, "Failed to query MASTER_POS_WAIT()" );
815
		}
816
817
		// Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
818
		$status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
819
		if ( $status === null ) {
820
			// T126436: jobs programmed to wait on master positions might be referencing binlogs
821
			// with an old master hostname. Such calls make MASTER_POS_WAIT() return null. Try
822
			// to detect this and treat the replica DB as having reached the position; a proper master
823
			// switchover already requires that the new master be caught up before the switch.
824
			$replicationPos = $this->getReplicaPos();
825
			if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) {
826
				$this->lastKnownReplicaPos = $replicationPos;
827
				$status = 0;
828
			}
829
		} elseif ( $status >= 0 ) {
830
			// Remember that this position was reached to save queries next time
831
			$this->lastKnownReplicaPos = $pos;
832
		}
833
834
		return $status;
835
	}
836
837
	/**
838
	 * Get the position of the master from SHOW SLAVE STATUS
839
	 *
840
	 * @return MySQLMasterPos|bool
841
	 */
842
	public function getReplicaPos() {
843
		$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
844
		$row = $this->fetchObject( $res );
0 ignored issues
show
It seems like $res defined by $this->query('SHOW SLAVE STATUS', __METHOD__) on line 843 can also be of type boolean; however, DatabaseMysqlBase::fetchObject() does only seem to accept object<ResultWrapper>|resource, 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...
845
846
		if ( $row ) {
847
			$pos = isset( $row->Exec_master_log_pos )
848
				? $row->Exec_master_log_pos
849
				: $row->Exec_Master_Log_Pos;
850
			// Also fetch the last-applied GTID set (MariaDB)
851 View Code Duplication
			if ( $this->useGTIDs ) {
852
				$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_slave_pos'", __METHOD__ );
853
				$gtidRow = $this->fetchObject( $res );
854
				$gtidSet = $gtidRow ? $gtidRow->Value : '';
855
			} else {
856
				$gtidSet = '';
857
			}
858
859
			return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos, $gtidSet );
860
		} else {
861
			return false;
862
		}
863
	}
864
865
	/**
866
	 * Get the position of the master from SHOW MASTER STATUS
867
	 *
868
	 * @return MySQLMasterPos|bool
869
	 */
870
	public function getMasterPos() {
871
		$res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
872
		$row = $this->fetchObject( $res );
0 ignored issues
show
It seems like $res defined by $this->query('SHOW MASTER STATUS', __METHOD__) on line 871 can also be of type boolean; however, DatabaseMysqlBase::fetchObject() does only seem to accept object<ResultWrapper>|resource, 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...
873
874
		if ( $row ) {
875
			// Also fetch the last-written GTID set (MariaDB)
876 View Code Duplication
			if ( $this->useGTIDs ) {
877
				$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_binlog_pos'", __METHOD__ );
878
				$gtidRow = $this->fetchObject( $res );
879
				$gtidSet = $gtidRow ? $gtidRow->Value : '';
880
			} else {
881
				$gtidSet = '';
882
			}
883
884
			return new MySQLMasterPos( $row->File, $row->Position, $gtidSet );
885
		} else {
886
			return false;
887
		}
888
	}
889
890
	public function serverIsReadOnly() {
891
		$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__ );
892
		$row = $this->fetchObject( $res );
893
894
		return $row ? ( strtolower( $row->Value ) === 'on' ) : false;
895
	}
896
897
	/**
898
	 * @param string $index
899
	 * @return string
900
	 */
901
	function useIndexClause( $index ) {
902
		return "FORCE INDEX (" . $this->indexName( $index ) . ")";
903
	}
904
905
	/**
906
	 * @param string $index
907
	 * @return string
908
	 */
909
	function ignoreIndexClause( $index ) {
910
		return "IGNORE INDEX (" . $this->indexName( $index ) . ")";
911
	}
912
913
	/**
914
	 * @return string
915
	 */
916
	function lowPriorityOption() {
917
		return 'LOW_PRIORITY';
918
	}
919
920
	/**
921
	 * @return string
922
	 */
923
	public function getSoftwareLink() {
924
		// MariaDB includes its name in its version string; this is how MariaDB's version of
925
		// the mysql command-line client identifies MariaDB servers (see mariadb_connection()
926
		// in libmysql/libmysql.c).
927
		$version = $this->getServerVersion();
928
		if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) {
929
			return '[{{int:version-db-mariadb-url}} MariaDB]';
930
		}
931
932
		// Percona Server's version suffix is not very distinctive, and @@version_comment
933
		// doesn't give the necessary info for source builds, so assume the server is MySQL.
934
		// (Even Percona's version of mysql doesn't try to make the distinction.)
935
		return '[{{int:version-db-mysql-url}} MySQL]';
936
	}
937
938
	/**
939
	 * @return string
940
	 */
941
	public function getServerVersion() {
942
		// Not using mysql_get_server_info() or similar for consistency: in the handshake,
943
		// MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
944
		// it off (see RPL_VERSION_HACK in include/mysql_com.h).
945
		if ( $this->serverVersion === null ) {
946
			$this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ );
947
		}
948
		return $this->serverVersion;
949
	}
950
951
	/**
952
	 * @param array $options
953
	 */
954
	public function setSessionOptions( array $options ) {
955
		if ( isset( $options['connTimeout'] ) ) {
956
			$timeout = (int)$options['connTimeout'];
957
			$this->query( "SET net_read_timeout=$timeout" );
958
			$this->query( "SET net_write_timeout=$timeout" );
959
		}
960
	}
961
962
	/**
963
	 * @param string $sql
964
	 * @param string $newLine
965
	 * @return bool
966
	 */
967
	public function streamStatementEnd( &$sql, &$newLine ) {
968
		if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
969
			preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
970
			$this->delimiter = $m[1];
971
			$newLine = '';
972
		}
973
974
		return parent::streamStatementEnd( $sql, $newLine );
975
	}
976
977
	/**
978
	 * Check to see if a named lock is available. This is non-blocking.
979
	 *
980
	 * @param string $lockName Name of lock to poll
981
	 * @param string $method Name of method calling us
982
	 * @return bool
983
	 * @since 1.20
984
	 */
985 View Code Duplication
	public function lockIsFree( $lockName, $method ) {
986
		$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
987
		$result = $this->query( "SELECT IS_FREE_LOCK($encName) AS lockstatus", $method );
988
		$row = $this->fetchObject( $result );
0 ignored issues
show
It seems like $result defined by $this->query("SELECT IS_...S lockstatus", $method) on line 987 can also be of type boolean; however, DatabaseMysqlBase::fetchObject() does only seem to accept object<ResultWrapper>|resource, 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...
989
990
		return ( $row->lockstatus == 1 );
991
	}
992
993
	/**
994
	 * @param string $lockName
995
	 * @param string $method
996
	 * @param int $timeout
997
	 * @return bool
998
	 */
999 View Code Duplication
	public function lock( $lockName, $method, $timeout = 5 ) {
1000
		$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
1001
		$result = $this->query( "SELECT GET_LOCK($encName, $timeout) AS lockstatus", $method );
1002
		$row = $this->fetchObject( $result );
0 ignored issues
show
It seems like $result defined by $this->query("SELECT GET...S lockstatus", $method) on line 1001 can also be of type boolean; however, DatabaseMysqlBase::fetchObject() does only seem to accept object<ResultWrapper>|resource, 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...
1003
1004
		if ( $row->lockstatus == 1 ) {
1005
			parent::lock( $lockName, $method, $timeout ); // record
1006
			return true;
1007
		}
1008
1009
		$this->queryLogger->warning( __METHOD__ . " failed to acquire lock '$lockName'\n" );
1010
1011
		return false;
1012
	}
1013
1014
	/**
1015
	 * FROM MYSQL DOCS:
1016
	 * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
1017
	 * @param string $lockName
1018
	 * @param string $method
1019
	 * @return bool
1020
	 */
1021 View Code Duplication
	public function unlock( $lockName, $method ) {
1022
		$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
1023
		$result = $this->query( "SELECT RELEASE_LOCK($encName) as lockstatus", $method );
1024
		$row = $this->fetchObject( $result );
0 ignored issues
show
It seems like $result defined by $this->query("SELECT REL...s lockstatus", $method) on line 1023 can also be of type boolean; however, DatabaseMysqlBase::fetchObject() does only seem to accept object<ResultWrapper>|resource, 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...
1025
1026
		if ( $row->lockstatus == 1 ) {
1027
			parent::unlock( $lockName, $method ); // record
1028
			return true;
1029
		}
1030
1031
		$this->queryLogger->warning( __METHOD__ . " failed to release lock '$lockName'\n" );
1032
1033
		return false;
1034
	}
1035
1036
	private function makeLockName( $lockName ) {
1037
		// http://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1038
		// Newer version enforce a 64 char length limit.
1039
		return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
1040
	}
1041
1042
	public function namedLocksEnqueue() {
1043
		return true;
1044
	}
1045
1046
	/**
1047
	 * @param array $read
1048
	 * @param array $write
1049
	 * @param string $method
1050
	 * @param bool $lowPriority
1051
	 * @return bool
1052
	 */
1053
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
1054
		$items = [];
1055
1056
		foreach ( $write as $table ) {
1057
			$tbl = $this->tableName( $table ) .
1058
				( $lowPriority ? ' LOW_PRIORITY' : '' ) .
1059
				' WRITE';
1060
			$items[] = $tbl;
1061
		}
1062
		foreach ( $read as $table ) {
1063
			$items[] = $this->tableName( $table ) . ' READ';
1064
		}
1065
		$sql = "LOCK TABLES " . implode( ',', $items );
1066
		$this->query( $sql, $method );
1067
1068
		return true;
1069
	}
1070
1071
	/**
1072
	 * @param string $method
1073
	 * @return bool
1074
	 */
1075
	public function unlockTables( $method ) {
1076
		$this->query( "UNLOCK TABLES", $method );
1077
1078
		return true;
1079
	}
1080
1081
	/**
1082
	 * @param bool $value
1083
	 */
1084
	public function setBigSelects( $value = true ) {
1085
		if ( $value === 'default' ) {
1086
			if ( $this->mDefaultBigSelects === null ) {
1087
				# Function hasn't been called before so it must already be set to the default
1088
				return;
1089
			} else {
1090
				$value = $this->mDefaultBigSelects;
1091
			}
1092
		} elseif ( $this->mDefaultBigSelects === null ) {
1093
			$this->mDefaultBigSelects =
1094
				(bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
1095
		}
1096
		$encValue = $value ? '1' : '0';
1097
		$this->query( "SET sql_big_selects=$encValue", __METHOD__ );
1098
	}
1099
1100
	/**
1101
	 * DELETE where the condition is a join. MySql uses multi-table deletes.
1102
	 * @param string $delTable
1103
	 * @param string $joinTable
1104
	 * @param string $delVar
1105
	 * @param string $joinVar
1106
	 * @param array|string $conds
1107
	 * @param bool|string $fname
1108
	 * @throws DBUnexpectedError
1109
	 * @return bool|ResultWrapper
1110
	 */
1111 View Code Duplication
	public function deleteJoin(
1112
		$delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__
1113
	) {
1114
		if ( !$conds ) {
1115
			throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
1116
		}
1117
1118
		$delTable = $this->tableName( $delTable );
1119
		$joinTable = $this->tableName( $joinTable );
1120
		$sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
1121
1122
		if ( $conds != '*' ) {
1123
			$sql .= ' AND ' . $this->makeList( $conds, self::LIST_AND );
0 ignored issues
show
It seems like $conds defined by parameter $conds on line 1112 can also be of type string; however, Database::makeList() does only seem to accept array, 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...
1124
		}
1125
1126
		return $this->query( $sql, $fname );
0 ignored issues
show
It seems like $fname defined by parameter $fname on line 1112 can also be of type boolean; however, Database::query() 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...
1127
	}
1128
1129
	/**
1130
	 * @param string $table
1131
	 * @param array $rows
1132
	 * @param array $uniqueIndexes
1133
	 * @param array $set
1134
	 * @param string $fname
1135
	 * @return bool
1136
	 */
1137
	public function upsert( $table, array $rows, array $uniqueIndexes,
1138
		array $set, $fname = __METHOD__
1139
	) {
1140
		if ( !count( $rows ) ) {
1141
			return true; // nothing to do
1142
		}
1143
1144
		if ( !is_array( reset( $rows ) ) ) {
1145
			$rows = [ $rows ];
1146
		}
1147
1148
		$table = $this->tableName( $table );
1149
		$columns = array_keys( $rows[0] );
1150
1151
		$sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES ';
1152
		$rowTuples = [];
1153
		foreach ( $rows as $row ) {
1154
			$rowTuples[] = '(' . $this->makeList( $row ) . ')';
1155
		}
1156
		$sql .= implode( ',', $rowTuples );
1157
		$sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, self::LIST_SET );
1158
1159
		return (bool)$this->query( $sql, $fname );
1160
	}
1161
1162
	/**
1163
	 * Determines how long the server has been up
1164
	 *
1165
	 * @return int
1166
	 */
1167
	public function getServerUptime() {
1168
		$vars = $this->getMysqlStatus( 'Uptime' );
1169
1170
		return (int)$vars['Uptime'];
1171
	}
1172
1173
	/**
1174
	 * Determines if the last failure was due to a deadlock
1175
	 *
1176
	 * @return bool
1177
	 */
1178
	public function wasDeadlock() {
1179
		return $this->lastErrno() == 1213;
1180
	}
1181
1182
	/**
1183
	 * Determines if the last failure was due to a lock timeout
1184
	 *
1185
	 * @return bool
1186
	 */
1187
	public function wasLockTimeout() {
1188
		return $this->lastErrno() == 1205;
1189
	}
1190
1191
	public function wasErrorReissuable() {
1192
		return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
1193
	}
1194
1195
	/**
1196
	 * Determines if the last failure was due to the database being read-only.
1197
	 *
1198
	 * @return bool
1199
	 */
1200
	public function wasReadOnlyError() {
1201
		return $this->lastErrno() == 1223 ||
1202
			( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
1203
	}
1204
1205
	public function wasConnectionError( $errno ) {
1206
		return $errno == 2013 || $errno == 2006;
1207
	}
1208
1209
	/**
1210
	 * @param string $oldName
1211
	 * @param string $newName
1212
	 * @param bool $temporary
1213
	 * @param string $fname
1214
	 * @return bool
1215
	 */
1216
	public function duplicateTableStructure(
1217
		$oldName, $newName, $temporary = false, $fname = __METHOD__
1218
	) {
1219
		$tmp = $temporary ? 'TEMPORARY ' : '';
1220
		$newName = $this->addIdentifierQuotes( $newName );
1221
		$oldName = $this->addIdentifierQuotes( $oldName );
1222
		$query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
1223
1224
		return $this->query( $query, $fname );
1225
	}
1226
1227
	/**
1228
	 * List all tables on the database
1229
	 *
1230
	 * @param string $prefix Only show tables with this prefix, e.g. mw_
1231
	 * @param string $fname Calling function name
1232
	 * @return array
1233
	 */
1234
	public function listTables( $prefix = null, $fname = __METHOD__ ) {
1235
		$result = $this->query( "SHOW TABLES", $fname );
1236
1237
		$endArray = [];
1238
1239 View Code Duplication
		foreach ( $result as $table ) {
0 ignored issues
show
The expression $result 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...
1240
			$vars = get_object_vars( $table );
1241
			$table = array_pop( $vars );
1242
1243
			if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $prefix of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1244
				$endArray[] = $table;
1245
			}
1246
		}
1247
1248
		return $endArray;
1249
	}
1250
1251
	/**
1252
	 * @param string $tableName
1253
	 * @param string $fName
1254
	 * @return bool|ResultWrapper
1255
	 */
1256 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
1257
		if ( !$this->tableExists( $tableName, $fName ) ) {
1258
			return false;
1259
		}
1260
1261
		return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
1262
	}
1263
1264
	/**
1265
	 * Get status information from SHOW STATUS in an associative array
1266
	 *
1267
	 * @param string $which
1268
	 * @return array
1269
	 */
1270
	private function getMysqlStatus( $which = "%" ) {
1271
		$res = $this->query( "SHOW STATUS LIKE '{$which}'" );
1272
		$status = [];
1273
1274
		foreach ( $res as $row ) {
0 ignored issues
show
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...
1275
			$status[$row->Variable_name] = $row->Value;
1276
		}
1277
1278
		return $status;
1279
	}
1280
1281
	/**
1282
	 * Lists VIEWs in the database
1283
	 *
1284
	 * @param string $prefix Only show VIEWs with this prefix, eg.
1285
	 * unit_test_, or $wgDBprefix. Default: null, would return all views.
1286
	 * @param string $fname Name of calling function
1287
	 * @return array
1288
	 * @since 1.22
1289
	 */
1290
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
1291
		// The name of the column containing the name of the VIEW
1292
		$propertyName = 'Tables_in_' . $this->mDBname;
1293
1294
		// Query for the VIEWS
1295
		$res = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
1296
		$allViews = [];
1297
		foreach ( $res as $row ) {
0 ignored issues
show
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...
1298
			array_push( $allViews, $row->$propertyName );
1299
		}
1300
1301
		if ( is_null( $prefix ) || $prefix === '' ) {
1302
			return $allViews;
1303
		}
1304
1305
		$filteredViews = [];
1306
		foreach ( $allViews as $viewName ) {
1307
			// Does the name of this VIEW start with the table-prefix?
1308
			if ( strpos( $viewName, $prefix ) === 0 ) {
1309
				array_push( $filteredViews, $viewName );
1310
			}
1311
		}
1312
1313
		return $filteredViews;
1314
	}
1315
1316
	/**
1317
	 * Differentiates between a TABLE and a VIEW.
1318
	 *
1319
	 * @param string $name Name of the TABLE/VIEW to test
1320
	 * @param string $prefix
1321
	 * @return bool
1322
	 * @since 1.22
1323
	 */
1324
	public function isView( $name, $prefix = null ) {
1325
		return in_array( $name, $this->listViews( $prefix ) );
1326
	}
1327
}
1328
1329