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

DatabaseMysqlBase::getDefaultSchemaVars()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1271
		$vars = parent::getDefaultSchemaVars();
1272
		$vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
1273
		$vars['wgDBTableOptions'] = str_replace(
1274
			'CHARSET=mysql4',
1275
			'CHARSET=binary',
1276
			$vars['wgDBTableOptions']
1277
		);
1278
1279
		return $vars;
1280
	}
1281
1282
	/**
1283
	 * Get status information from SHOW STATUS in an associative array
1284
	 *
1285
	 * @param string $which
1286
	 * @return array
1287
	 */
1288
	function getMysqlStatus( $which = "%" ) {
1289
		$res = $this->query( "SHOW STATUS LIKE '{$which}'" );
1290
		$status = [];
1291
1292
		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...
1293
			$status[$row->Variable_name] = $row->Value;
1294
		}
1295
1296
		return $status;
1297
	}
1298
1299
	/**
1300
	 * Lists VIEWs in the database
1301
	 *
1302
	 * @param string $prefix Only show VIEWs with this prefix, eg.
1303
	 * unit_test_, or $wgDBprefix. Default: null, would return all views.
1304
	 * @param string $fname Name of calling function
1305
	 * @return array
1306
	 * @since 1.22
1307
	 */
1308
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
1309
1310
		if ( !isset( $this->allViews ) ) {
1311
1312
			// The name of the column containing the name of the VIEW
1313
			$propertyName = 'Tables_in_' . $this->mDBname;
1314
1315
			// Query for the VIEWS
1316
			$result = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
1317
			$this->allViews = [];
1318
			while ( ( $row = $this->fetchRow( $result ) ) !== false ) {
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query('SHOW FULL ...E TABLE_TYPE = "VIEW"') on line 1316 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...
1319
				array_push( $this->allViews, $row[$propertyName] );
1320
			}
1321
		}
1322
1323
		if ( is_null( $prefix ) || $prefix === '' ) {
1324
			return $this->allViews;
1325
		}
1326
1327
		$filteredViews = [];
1328
		foreach ( $this->allViews as $viewName ) {
1329
			// Does the name of this VIEW start with the table-prefix?
1330
			if ( strpos( $viewName, $prefix ) === 0 ) {
1331
				array_push( $filteredViews, $viewName );
1332
			}
1333
		}
1334
1335
		return $filteredViews;
1336
	}
1337
1338
	/**
1339
	 * Differentiates between a TABLE and a VIEW.
1340
	 *
1341
	 * @param string $name Name of the TABLE/VIEW to test
1342
	 * @param string $prefix
1343
	 * @return bool
1344
	 * @since 1.22
1345
	 */
1346
	public function isView( $name, $prefix = null ) {
1347
		return in_array( $name, $this->listViews( $prefix ) );
1348
	}
1349
}
1350
1351