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

DatabaseMysqlBase::getReplicaPos()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 22
Code Lines 16

Duplication

Lines 7
Ratio 31.82 %

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 7
nop 0
dl 7
loc 22
rs 8.6737
c 0
b 0
f 0
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 DatabaseBase {
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
		parent::__construct( $params );
76
77
		$this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
78
			? $params['lagDetectionMethod']
79
			: 'Seconds_Behind_Master';
80
		$this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
81
			? $params['lagDetectionOptions']
82
			: [];
83
		$this->useGTIDs = !empty( $params['useGTIDs' ] );
84
		foreach ( [ 'KeyPath', 'CertPath', 'CAPath', 'Ciphers' ] as $name ) {
85
			$var = "ssl{$name}";
86
			if ( isset( $params[$var] ) ) {
87
				$this->$var = $params[$var];
88
			}
89
		}
90
		$this->sqlMode = isset( $params['sqlMode'] ) ? $params['sqlMode'] : '';
91
		$this->utf8Mode = !empty( $params['utf8Mode'] );
92
	}
93
94
	/**
95
	 * @return string
96
	 */
97
	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
	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->queryLogger->error(
133
				"Error connecting to {db_server}: {error}",
134
				$this->getLogContext( [
135
					'method' => __METHOD__,
136
					'error' => $error,
137
				] )
138
			);
139
			$this->queryLogger->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
	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
Bug introduced by
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
	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
Bug introduced by
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
	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
Bug introduced by
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
Bug introduced by
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
	function numFields( $res ) {
366
		if ( $res instanceof ResultWrapper ) {
367
			$res = $res->result;
368
		}
369
370
		return $this->mysqlNumFields( $res );
0 ignored issues
show
Bug introduced by
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
	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
Bug introduced by
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 );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $res->result on line 411 can also be of type array or null; however, DatabaseMysqlBase::mysqlFieldType() 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...
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
	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
Bug introduced by
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
	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
	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
Bug introduced by
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...
Bug introduced by
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
Bug introduced by
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
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...
515
			$rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
516
		}
517
518
		return (int)$rows;
519
	}
520
521
	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
	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
	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
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...
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
	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
	/**
612
	 * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
613
	 *
614
	 * @param string $s
615
	 * @return string
616
	 */
617
	public function addIdentifierQuotes( $s ) {
618
		// Characters in the range \u0001-\uFFFF are valid in a quoted identifier
619
		// Remove NUL bytes and escape backticks by doubling
620
		return '`' . str_replace( [ "\0", '`' ], [ '', '``' ], $s ) . '`';
621
	}
622
623
	/**
624
	 * @param string $name
625
	 * @return bool
626
	 */
627
	public function isQuotedIdentifier( $name ) {
628
		return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
629
	}
630
631
	function getLag() {
632
		if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
633
			return $this->getLagFromPtHeartbeat();
634
		} else {
635
			return $this->getLagFromSlaveStatus();
636
		}
637
	}
638
639
	/**
640
	 * @return string
641
	 */
642
	protected function getLagDetectionMethod() {
643
		return $this->lagDetectionMethod;
644
	}
645
646
	/**
647
	 * @return bool|int
648
	 */
649
	protected function getLagFromSlaveStatus() {
650
		$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
651
		$row = $res ? $res->fetchObject() : false;
652
		if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
653
			return intval( $row->Seconds_Behind_Master );
654
		}
655
656
		return false;
657
	}
658
659
	/**
660
	 * @return bool|float
661
	 */
662
	protected function getLagFromPtHeartbeat() {
663
		$options = $this->lagDetectionOptions;
664
665
		if ( isset( $options['conds'] ) ) {
666
			// Best method for multi-DC setups: use logical channel names
667
			$data = $this->getHeartbeatData( $options['conds'] );
668
		} else {
669
			// Standard method: use master server ID (works with stock pt-heartbeat)
670
			$masterInfo = $this->getMasterServerInfo();
671
			if ( !$masterInfo ) {
672
				$this->queryLogger->error(
673
					"Unable to query master of {db_server} for server ID",
674
					$this->getLogContext( [
675
						'method' => __METHOD__
676
					] )
677
				);
678
679
				return false; // could not get master server ID
680
			}
681
682
			$conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
683
			$data = $this->getHeartbeatData( $conds );
684
		}
685
686
		list( $time, $nowUnix ) = $data;
687
		if ( $time !== null ) {
688
			// @time is in ISO format like "2015-09-25T16:48:10.000510"
689
			$dateTime = new DateTime( $time, new DateTimeZone( 'UTC' ) );
690
			$timeUnix = (int)$dateTime->format( 'U' ) + $dateTime->format( 'u' ) / 1e6;
691
692
			return max( $nowUnix - $timeUnix, 0.0 );
693
		}
694
695
		$this->queryLogger->error(
696
			"Unable to find pt-heartbeat row for {db_server}",
697
			$this->getLogContext( [
698
				'method' => __METHOD__
699
			] )
700
		);
701
702
		return false;
703
	}
704
705
	protected function getMasterServerInfo() {
706
		$cache = $this->srvCache;
707
		$key = $cache->makeGlobalKey(
708
			'mysql',
709
			'master-info',
710
			// Using one key for all cluster replica DBs is preferable
711
			$this->getLBInfo( 'clusterMasterHost' ) ?: $this->getServer()
712
		);
713
714
		return $cache->getWithSetCallback(
715
			$key,
716
			$cache::TTL_INDEFINITE,
717
			function () use ( $cache, $key ) {
718
				// Get and leave a lock key in place for a short period
719
				if ( !$cache->lock( $key, 0, 10 ) ) {
720
					return false; // avoid master connection spike slams
721
				}
722
723
				$conn = $this->getLazyMasterHandle();
724
				if ( !$conn ) {
725
					return false; // something is misconfigured
726
				}
727
728
				// Connect to and query the master; catch errors to avoid outages
729
				try {
730
					$res = $conn->query( 'SELECT @@server_id AS id', __METHOD__ );
731
					$row = $res ? $res->fetchObject() : false;
732
					$id = $row ? (int)$row->id : 0;
733
				} catch ( DBError $e ) {
734
					$id = 0;
735
				}
736
737
				// Cache the ID if it was retrieved
738
				return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false;
739
			}
740
		);
741
	}
742
743
	/**
744
	 * @param array $conds WHERE clause conditions to find a row
745
	 * @return array (heartbeat `ts` column value or null, UNIX timestamp) for the newest beat
746
	 * @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html
747
	 */
748
	protected function getHeartbeatData( array $conds ) {
749
		$whereSQL = $this->makeList( $conds, self::LIST_AND );
750
		// Use ORDER BY for channel based queries since that field might not be UNIQUE.
751
		// Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
752
		// percision field is not supported in MySQL <= 5.5.
753
		$res = $this->query(
754
			"SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1"
755
		);
756
		$row = $res ? $res->fetchObject() : false;
757
758
		return [ $row ? $row->ts : null, microtime( true ) ];
759
	}
760
761
	public function getApproximateLagStatus() {
762
		if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
763
			// Disable caching since this is fast enough and we don't wan't
764
			// to be *too* pessimistic by having both the cache TTL and the
765
			// pt-heartbeat interval count as lag in getSessionLagStatus()
766
			return parent::getApproximateLagStatus();
767
		}
768
769
		$key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServer() );
770
		$approxLag = $this->srvCache->get( $key );
771
		if ( !$approxLag ) {
772
			$approxLag = parent::getApproximateLagStatus();
773
			$this->srvCache->set( $key, $approxLag, 1 );
774
		}
775
776
		return $approxLag;
777
	}
778
779
	function masterPosWait( DBMasterPos $pos, $timeout ) {
780
		if ( !( $pos instanceof MySQLMasterPos ) ) {
781
			throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" );
782
		}
783
784
		if ( $this->getLBInfo( 'is static' ) === true ) {
785
			return 0; // this is a copy of a read-only dataset with no master DB
786
		} elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) {
787
			return 0; // already reached this point for sure
788
		}
789
790
		// Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
791
		if ( $this->useGTIDs && $pos->gtids ) {
792
			// Wait on the GTID set (MariaDB only)
793
			$gtidArg = $this->addQuotes( implode( ',', $pos->gtids ) );
794
			$res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
795
		} else {
796
			// Wait on the binlog coordinates
797
			$encFile = $this->addQuotes( $pos->file );
798
			$encPos = intval( $pos->pos );
799
			$res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
800
		}
801
802
		$row = $res ? $this->fetchRow( $res ) : false;
0 ignored issues
show
Bug introduced by
It seems like $res can also be of type boolean; however, DatabaseMysqlBase::fetchRow() 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...
803
		if ( !$row ) {
804
			throw new DBExpectedError( $this, "Failed to query MASTER_POS_WAIT()" );
805
		}
806
807
		// Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
808
		$status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
809
		if ( $status === null ) {
810
			// T126436: jobs programmed to wait on master positions might be referencing binlogs
811
			// with an old master hostname. Such calls make MASTER_POS_WAIT() return null. Try
812
			// to detect this and treat the replica DB as having reached the position; a proper master
813
			// switchover already requires that the new master be caught up before the switch.
814
			$replicationPos = $this->getReplicaPos();
815
			if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) {
816
				$this->lastKnownReplicaPos = $replicationPos;
817
				$status = 0;
818
			}
819
		} elseif ( $status >= 0 ) {
820
			// Remember that this position was reached to save queries next time
821
			$this->lastKnownReplicaPos = $pos;
822
		}
823
824
		return $status;
825
	}
826
827
	/**
828
	 * Get the position of the master from SHOW SLAVE STATUS
829
	 *
830
	 * @return MySQLMasterPos|bool
831
	 */
832
	function getReplicaPos() {
833
		$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
834
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query('SHOW SLAVE STATUS', __METHOD__) on line 833 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...
835
836
		if ( $row ) {
837
			$pos = isset( $row->Exec_master_log_pos )
838
				? $row->Exec_master_log_pos
839
				: $row->Exec_Master_Log_Pos;
840
			// Also fetch the last-applied GTID set (MariaDB)
841 View Code Duplication
			if ( $this->useGTIDs ) {
842
				$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_slave_pos'", __METHOD__ );
843
				$gtidRow = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query('SHOW GLOBA...ave_pos\'', __METHOD__) on line 842 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...
844
				$gtidSet = $gtidRow ? $gtidRow->Value : '';
845
			} else {
846
				$gtidSet = '';
847
			}
848
849
			return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos, $gtidSet );
850
		} else {
851
			return false;
852
		}
853
	}
854
855
	/**
856
	 * Get the position of the master from SHOW MASTER STATUS
857
	 *
858
	 * @return MySQLMasterPos|bool
859
	 */
860
	function getMasterPos() {
861
		$res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
862
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query('SHOW MASTER STATUS', __METHOD__) on line 861 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...
863
864
		if ( $row ) {
865
			// Also fetch the last-written GTID set (MariaDB)
866 View Code Duplication
			if ( $this->useGTIDs ) {
867
				$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_binlog_pos'", __METHOD__ );
868
				$gtidRow = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query('SHOW GLOBA...log_pos\'', __METHOD__) on line 867 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...
869
				$gtidSet = $gtidRow ? $gtidRow->Value : '';
870
			} else {
871
				$gtidSet = '';
872
			}
873
874
			return new MySQLMasterPos( $row->File, $row->Position, $gtidSet );
875
		} else {
876
			return false;
877
		}
878
	}
879
880
	public function serverIsReadOnly() {
881
		$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__ );
882
		$row = $this->fetchObject( $res );
0 ignored issues
show
Bug introduced by
It seems like $res defined by $this->query('SHOW GLOBA...ad_only\'', __METHOD__) on line 881 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...
883
884
		return $row ? ( strtolower( $row->Value ) === 'on' ) : false;
885
	}
886
887
	/**
888
	 * @param string $index
889
	 * @return string
890
	 */
891
	function useIndexClause( $index ) {
892
		return "FORCE INDEX (" . $this->indexName( $index ) . ")";
893
	}
894
895
	/**
896
	 * @param string $index
897
	 * @return string
898
	 */
899
	function ignoreIndexClause( $index ) {
900
		return "IGNORE INDEX (" . $this->indexName( $index ) . ")";
901
	}
902
903
	/**
904
	 * @return string
905
	 */
906
	function lowPriorityOption() {
907
		return 'LOW_PRIORITY';
908
	}
909
910
	/**
911
	 * @return string
912
	 */
913
	public function getSoftwareLink() {
914
		// MariaDB includes its name in its version string; this is how MariaDB's version of
915
		// the mysql command-line client identifies MariaDB servers (see mariadb_connection()
916
		// in libmysql/libmysql.c).
917
		$version = $this->getServerVersion();
918
		if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) {
919
			return '[{{int:version-db-mariadb-url}} MariaDB]';
920
		}
921
922
		// Percona Server's version suffix is not very distinctive, and @@version_comment
923
		// doesn't give the necessary info for source builds, so assume the server is MySQL.
924
		// (Even Percona's version of mysql doesn't try to make the distinction.)
925
		return '[{{int:version-db-mysql-url}} MySQL]';
926
	}
927
928
	/**
929
	 * @return string
930
	 */
931
	public function getServerVersion() {
932
		// Not using mysql_get_server_info() or similar for consistency: in the handshake,
933
		// MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
934
		// it off (see RPL_VERSION_HACK in include/mysql_com.h).
935
		if ( $this->serverVersion === null ) {
936
			$this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ );
937
		}
938
		return $this->serverVersion;
939
	}
940
941
	/**
942
	 * @param array $options
943
	 */
944
	public function setSessionOptions( array $options ) {
945
		if ( isset( $options['connTimeout'] ) ) {
946
			$timeout = (int)$options['connTimeout'];
947
			$this->query( "SET net_read_timeout=$timeout" );
948
			$this->query( "SET net_write_timeout=$timeout" );
949
		}
950
	}
951
952
	/**
953
	 * @param string $sql
954
	 * @param string $newLine
955
	 * @return bool
956
	 */
957
	public function streamStatementEnd( &$sql, &$newLine ) {
958
		if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
959
			preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
960
			$this->delimiter = $m[1];
961
			$newLine = '';
962
		}
963
964
		return parent::streamStatementEnd( $sql, $newLine );
965
	}
966
967
	/**
968
	 * Check to see if a named lock is available. This is non-blocking.
969
	 *
970
	 * @param string $lockName Name of lock to poll
971
	 * @param string $method Name of method calling us
972
	 * @return bool
973
	 * @since 1.20
974
	 */
975 View Code Duplication
	public function lockIsFree( $lockName, $method ) {
976
		$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
977
		$result = $this->query( "SELECT IS_FREE_LOCK($encName) AS lockstatus", $method );
978
		$row = $this->fetchObject( $result );
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query("SELECT IS_...S lockstatus", $method) on line 977 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...
979
980
		return ( $row->lockstatus == 1 );
981
	}
982
983
	/**
984
	 * @param string $lockName
985
	 * @param string $method
986
	 * @param int $timeout
987
	 * @return bool
988
	 */
989 View Code Duplication
	public function lock( $lockName, $method, $timeout = 5 ) {
990
		$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
991
		$result = $this->query( "SELECT GET_LOCK($encName, $timeout) AS lockstatus", $method );
992
		$row = $this->fetchObject( $result );
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query("SELECT GET...S lockstatus", $method) on line 991 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...
993
994
		if ( $row->lockstatus == 1 ) {
995
			parent::lock( $lockName, $method, $timeout ); // record
996
			return true;
997
		}
998
999
		$this->queryLogger->warning( __METHOD__ . " failed to acquire lock '$lockName'\n" );
1000
1001
		return false;
1002
	}
1003
1004
	/**
1005
	 * FROM MYSQL DOCS:
1006
	 * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
1007
	 * @param string $lockName
1008
	 * @param string $method
1009
	 * @return bool
1010
	 */
1011 View Code Duplication
	public function unlock( $lockName, $method ) {
1012
		$encName = $this->addQuotes( $this->makeLockName( $lockName ) );
1013
		$result = $this->query( "SELECT RELEASE_LOCK($encName) as lockstatus", $method );
1014
		$row = $this->fetchObject( $result );
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query("SELECT REL...s lockstatus", $method) on line 1013 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...
1015
1016
		if ( $row->lockstatus == 1 ) {
1017
			parent::unlock( $lockName, $method ); // record
1018
			return true;
1019
		}
1020
1021
		$this->queryLogger->warning( __METHOD__ . " failed to release lock '$lockName'\n" );
1022
1023
		return false;
1024
	}
1025
1026
	private function makeLockName( $lockName ) {
1027
		// http://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1028
		// Newer version enforce a 64 char length limit.
1029
		return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
1030
	}
1031
1032
	public function namedLocksEnqueue() {
1033
		return true;
1034
	}
1035
1036
	/**
1037
	 * @param array $read
1038
	 * @param array $write
1039
	 * @param string $method
1040
	 * @param bool $lowPriority
1041
	 * @return bool
1042
	 */
1043
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
1044
		$items = [];
1045
1046
		foreach ( $write as $table ) {
1047
			$tbl = $this->tableName( $table ) .
1048
				( $lowPriority ? ' LOW_PRIORITY' : '' ) .
1049
				' WRITE';
1050
			$items[] = $tbl;
1051
		}
1052
		foreach ( $read as $table ) {
1053
			$items[] = $this->tableName( $table ) . ' READ';
1054
		}
1055
		$sql = "LOCK TABLES " . implode( ',', $items );
1056
		$this->query( $sql, $method );
1057
1058
		return true;
1059
	}
1060
1061
	/**
1062
	 * @param string $method
1063
	 * @return bool
1064
	 */
1065
	public function unlockTables( $method ) {
1066
		$this->query( "UNLOCK TABLES", $method );
1067
1068
		return true;
1069
	}
1070
1071
	/**
1072
	 * @param bool $value
1073
	 */
1074
	public function setBigSelects( $value = true ) {
1075
		if ( $value === 'default' ) {
1076
			if ( $this->mDefaultBigSelects === null ) {
1077
				# Function hasn't been called before so it must already be set to the default
1078
				return;
1079
			} else {
1080
				$value = $this->mDefaultBigSelects;
1081
			}
1082
		} elseif ( $this->mDefaultBigSelects === null ) {
1083
			$this->mDefaultBigSelects =
1084
				(bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
1085
		}
1086
		$encValue = $value ? '1' : '0';
1087
		$this->query( "SET sql_big_selects=$encValue", __METHOD__ );
1088
	}
1089
1090
	/**
1091
	 * DELETE where the condition is a join. MySql uses multi-table deletes.
1092
	 * @param string $delTable
1093
	 * @param string $joinTable
1094
	 * @param string $delVar
1095
	 * @param string $joinVar
1096
	 * @param array|string $conds
1097
	 * @param bool|string $fname
1098
	 * @throws DBUnexpectedError
1099
	 * @return bool|ResultWrapper
1100
	 */
1101 View Code Duplication
	function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ ) {
1102
		if ( !$conds ) {
1103
			throw new DBUnexpectedError( $this, __METHOD__ . ' called with empty $conds' );
1104
		}
1105
1106
		$delTable = $this->tableName( $delTable );
1107
		$joinTable = $this->tableName( $joinTable );
1108
		$sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
1109
1110
		if ( $conds != '*' ) {
1111
			$sql .= ' AND ' . $this->makeList( $conds, self::LIST_AND );
0 ignored issues
show
Bug introduced by
It seems like $conds defined by parameter $conds on line 1101 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...
1112
		}
1113
1114
		return $this->query( $sql, $fname );
0 ignored issues
show
Bug introduced by
It seems like $fname defined by parameter $fname on line 1101 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...
1115
	}
1116
1117
	/**
1118
	 * @param string $table
1119
	 * @param array $rows
1120
	 * @param array $uniqueIndexes
1121
	 * @param array $set
1122
	 * @param string $fname
1123
	 * @return bool
1124
	 */
1125
	public function upsert( $table, array $rows, array $uniqueIndexes,
1126
		array $set, $fname = __METHOD__
1127
	) {
1128
		if ( !count( $rows ) ) {
1129
			return true; // nothing to do
1130
		}
1131
1132
		if ( !is_array( reset( $rows ) ) ) {
1133
			$rows = [ $rows ];
1134
		}
1135
1136
		$table = $this->tableName( $table );
1137
		$columns = array_keys( $rows[0] );
1138
1139
		$sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES ';
1140
		$rowTuples = [];
1141
		foreach ( $rows as $row ) {
1142
			$rowTuples[] = '(' . $this->makeList( $row ) . ')';
1143
		}
1144
		$sql .= implode( ',', $rowTuples );
1145
		$sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, self::LIST_SET );
1146
1147
		return (bool)$this->query( $sql, $fname );
1148
	}
1149
1150
	/**
1151
	 * Determines how long the server has been up
1152
	 *
1153
	 * @return int
1154
	 */
1155
	function getServerUptime() {
1156
		$vars = $this->getMysqlStatus( 'Uptime' );
1157
1158
		return (int)$vars['Uptime'];
1159
	}
1160
1161
	/**
1162
	 * Determines if the last failure was due to a deadlock
1163
	 *
1164
	 * @return bool
1165
	 */
1166
	function wasDeadlock() {
1167
		return $this->lastErrno() == 1213;
1168
	}
1169
1170
	/**
1171
	 * Determines if the last failure was due to a lock timeout
1172
	 *
1173
	 * @return bool
1174
	 */
1175
	function wasLockTimeout() {
1176
		return $this->lastErrno() == 1205;
1177
	}
1178
1179
	function wasErrorReissuable() {
1180
		return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
1181
	}
1182
1183
	/**
1184
	 * Determines if the last failure was due to the database being read-only.
1185
	 *
1186
	 * @return bool
1187
	 */
1188
	function wasReadOnlyError() {
1189
		return $this->lastErrno() == 1223 ||
1190
			( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
1191
	}
1192
1193
	function wasConnectionError( $errno ) {
1194
		return $errno == 2013 || $errno == 2006;
1195
	}
1196
1197
	/**
1198
	 * Get the underlying binding handle, mConn
1199
	 *
1200
	 * Makes sure that mConn is set (disconnects and ping() failure can unset it).
1201
	 * This catches broken callers than catch and ignore disconnection exceptions.
1202
	 * Unlike checking isOpen(), this is safe to call inside of open().
1203
	 *
1204
	 * @return resource|object
1205
	 * @throws DBUnexpectedError
1206
	 * @since 1.26
1207
	 */
1208
	protected function getBindingHandle() {
1209
		if ( !$this->mConn ) {
1210
			throw new DBUnexpectedError(
1211
				$this,
1212
				'DB connection was already closed or the connection dropped.'
1213
			);
1214
		}
1215
1216
		return $this->mConn;
1217
	}
1218
1219
	/**
1220
	 * @param string $oldName
1221
	 * @param string $newName
1222
	 * @param bool $temporary
1223
	 * @param string $fname
1224
	 * @return bool
1225
	 */
1226
	function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
1227
		$tmp = $temporary ? 'TEMPORARY ' : '';
1228
		$newName = $this->addIdentifierQuotes( $newName );
1229
		$oldName = $this->addIdentifierQuotes( $oldName );
1230
		$query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
1231
1232
		return $this->query( $query, $fname );
1233
	}
1234
1235
	/**
1236
	 * List all tables on the database
1237
	 *
1238
	 * @param string $prefix Only show tables with this prefix, e.g. mw_
1239
	 * @param string $fname Calling function name
1240
	 * @return array
1241
	 */
1242
	function listTables( $prefix = null, $fname = __METHOD__ ) {
1243
		$result = $this->query( "SHOW TABLES", $fname );
1244
1245
		$endArray = [];
1246
1247 View Code Duplication
		foreach ( $result as $table ) {
0 ignored issues
show
Bug introduced by
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...
1248
			$vars = get_object_vars( $table );
1249
			$table = array_pop( $vars );
1250
1251
			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...
1252
				$endArray[] = $table;
1253
			}
1254
		}
1255
1256
		return $endArray;
1257
	}
1258
1259
	/**
1260
	 * @param string $tableName
1261
	 * @param string $fName
1262
	 * @return bool|ResultWrapper
1263
	 */
1264 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
1265
		if ( !$this->tableExists( $tableName, $fName ) ) {
1266
			return false;
1267
		}
1268
1269
		return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
1270
	}
1271
1272
	/**
1273
	 * Get status information from SHOW STATUS in an associative array
1274
	 *
1275
	 * @param string $which
1276
	 * @return array
1277
	 */
1278
	function getMysqlStatus( $which = "%" ) {
1279
		$res = $this->query( "SHOW STATUS LIKE '{$which}'" );
1280
		$status = [];
1281
1282
		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...
1283
			$status[$row->Variable_name] = $row->Value;
1284
		}
1285
1286
		return $status;
1287
	}
1288
1289
	/**
1290
	 * Lists VIEWs in the database
1291
	 *
1292
	 * @param string $prefix Only show VIEWs with this prefix, eg.
1293
	 * unit_test_, or $wgDBprefix. Default: null, would return all views.
1294
	 * @param string $fname Name of calling function
1295
	 * @return array
1296
	 * @since 1.22
1297
	 */
1298
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
1299
		// The name of the column containing the name of the VIEW
1300
		$propertyName = 'Tables_in_' . $this->mDBname;
1301
1302
		// Query for the VIEWS
1303
		$res = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
1304
		$allViews = [];
1305
		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...
1306
			array_push( $allViews, $row->$propertyName );
1307
		}
1308
1309
		if ( is_null( $prefix ) || $prefix === '' ) {
1310
			return $allViews;
1311
		}
1312
1313
		$filteredViews = [];
1314
		foreach ( $allViews as $viewName ) {
1315
			// Does the name of this VIEW start with the table-prefix?
1316
			if ( strpos( $viewName, $prefix ) === 0 ) {
1317
				array_push( $filteredViews, $viewName );
1318
			}
1319
		}
1320
1321
		return $filteredViews;
1322
	}
1323
1324
	/**
1325
	 * Differentiates between a TABLE and a VIEW.
1326
	 *
1327
	 * @param string $name Name of the TABLE/VIEW to test
1328
	 * @param string $prefix
1329
	 * @return bool
1330
	 * @since 1.22
1331
	 */
1332
	public function isView( $name, $prefix = null ) {
1333
		return in_array( $name, $this->listViews( $prefix ) );
1334
	}
1335
}
1336
1337