Completed
Branch master (4cbefc)
by
unknown
27:08
created

DatabaseMysqlBase::ignoreIndexClause()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
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 Database {
33
	/** @var MysqlMasterPos */
34
	protected $lastKnownReplicaPos;
35
	/** @var string Method to detect replica DB lag */
36
	protected $lagDetectionMethod;
37
	/** @var array Method to detect replica DB lag */
38
	protected $lagDetectionOptions = [];
39
	/** @var bool bool Whether to use GTID methods */
40
	protected $useGTIDs = false;
41
	/** @var string|null */
42
	protected $sslKeyPath;
43
	/** @var string|null */
44
	protected $sslCertPath;
45
	/** @var string|null */
46
	protected $sslCAPath;
47
	/** @var string[]|null */
48
	protected $sslCiphers;
49
	/** @var string|null */
50
	private $serverVersion = null;
51
52
	/**
53
	 * Additional $params include:
54
	 *   - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
55
	 *       pt-heartbeat assumes the table is at heartbeat.heartbeat
56
	 *       and uses UTC timestamps in the heartbeat.ts column.
57
	 *       (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html)
58
	 *   - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map to change
59
	 *       the default behavior. Normally, the heartbeat row with the server
60
	 *       ID of this server's master will be used. Set the "conds" field to
61
	 *       override the query conditions, e.g. ['shard' => 's1'].
62
	 *   - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible.
63
	 *   - sslKeyPath : path to key file [default: null]
64
	 *   - sslCertPath : path to certificate file [default: null]
65
	 *   - sslCAPath : parth to certificate authority PEM files [default: null]
66
	 *   - sslCiphers : array list of allowable ciphers [default: null]
67
	 * @param array $params
68
	 */
69
	function __construct( array $params ) {
70
		parent::__construct( $params );
71
72
		$this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
73
			? $params['lagDetectionMethod']
74
			: 'Seconds_Behind_Master';
75
		$this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
76
			? $params['lagDetectionOptions']
77
			: [];
78
		$this->useGTIDs = !empty( $params['useGTIDs' ] );
79
		foreach ( [ 'KeyPath', 'CertPath', 'CAPath', 'Ciphers' ] as $name ) {
80
			$var = "ssl{$name}";
81
			if ( isset( $params[$var] ) ) {
82
				$this->$var = $params[$var];
83
			}
84
		}
85
	}
86
87
	/**
88
	 * @return string
89
	 */
90
	function getType() {
91
		return 'mysql';
92
	}
93
94
	/**
95
	 * @param string $server
96
	 * @param string $user
97
	 * @param string $password
98
	 * @param string $dbName
99
	 * @throws Exception|DBConnectionError
100
	 * @return bool
101
	 */
102
	function open( $server, $user, $password, $dbName ) {
103
		global $wgAllDBsAreLocalhost, $wgSQLMode;
104
105
		# Close/unset connection handle
106
		$this->close();
107
108
		# Debugging hack -- fake cluster
109
		$realServer = $wgAllDBsAreLocalhost ? 'localhost' : $server;
110
		$this->mServer = $server;
111
		$this->mUser = $user;
112
		$this->mPassword = $password;
113
		$this->mDBname = $dbName;
114
115
		$this->installErrorHandler();
116
		try {
117
			$this->mConn = $this->mysqlConnect( $realServer );
118
		} catch ( Exception $ex ) {
119
			$this->restoreErrorHandler();
120
			throw $ex;
121
		}
122
		$error = $this->restoreErrorHandler();
123
124
		# Always log connection errors
125
		if ( !$this->mConn ) {
126
			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...
127
				$error = $this->lastError();
128
			}
129
			wfLogDBError(
130
				"Error connecting to {db_server}: {error}",
131
				$this->getLogContext( [
132
					'method' => __METHOD__,
133
					'error' => $error,
134
				] )
135
			);
136
			wfDebug( "DB connection error\n" .
137
				"Server: $server, User: $user, Password: " .
138
				substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
139
140
			$this->reportConnectionError( $error );
141
		}
142
143
		if ( $dbName != '' ) {
144
			MediaWiki\suppressWarnings();
145
			$success = $this->selectDB( $dbName );
146
			MediaWiki\restoreWarnings();
147
			if ( !$success ) {
148
				wfLogDBError(
149
					"Error selecting database {db_name} on server {db_server}",
150
					$this->getLogContext( [
151
						'method' => __METHOD__,
152
					] )
153
				);
154
				wfDebug( "Error selecting database $dbName on server {$this->mServer} " .
155
					"from client host " . wfHostname() . "\n" );
156
157
				$this->reportConnectionError( "Error selecting database $dbName" );
158
			}
159
		}
160
161
		// Tell the server what we're communicating with
162
		if ( !$this->connectInitCharset() ) {
163
			$this->reportConnectionError( "Error setting character set" );
164
		}
165
166
		// Abstract over any insane MySQL defaults
167
		$set = [ 'group_concat_max_len = 262144' ];
168
		// Set SQL mode, default is turning them all off, can be overridden or skipped with null
169
		if ( is_string( $wgSQLMode ) ) {
170
			$set[] = 'sql_mode = ' . $this->addQuotes( $wgSQLMode );
171
		}
172
		// Set any custom settings defined by site config
173
		// (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html)
174
		foreach ( $this->mSessionVars as $var => $val ) {
175
			// Escape strings but not numbers to avoid MySQL complaining
176
			if ( !is_int( $val ) && !is_float( $val ) ) {
177
				$val = $this->addQuotes( $val );
178
			}
179
			$set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val;
180
		}
181
182
		if ( $set ) {
183
			// Use doQuery() to avoid opening implicit transactions (DBO_TRX)
184
			$success = $this->doQuery( 'SET ' . implode( ', ', $set ) );
185
			if ( !$success ) {
186
				wfLogDBError(
187
					'Error setting MySQL variables on server {db_server} (check $wgSQLMode)',
188
					$this->getLogContext( [
189
						'method' => __METHOD__,
190
					] )
191
				);
192
				$this->reportConnectionError(
193
					'Error setting MySQL variables on server {db_server} (check $wgSQLMode)' );
194
			}
195
		}
196
197
		$this->mOpened = true;
198
199
		return true;
200
	}
201
202
	/**
203
	 * Set the character set information right after connection
204
	 * @return bool
205
	 */
206
	protected function connectInitCharset() {
207
		global $wgDBmysql5;
208
209
		if ( $wgDBmysql5 ) {
210
			// Tell the server we're communicating with it in UTF-8.
211
			// This may engage various charset conversions.
212
			return $this->mysqlSetCharset( 'utf8' );
213
		} else {
214
			return $this->mysqlSetCharset( 'binary' );
215
		}
216
	}
217
218
	/**
219
	 * Open a connection to a MySQL server
220
	 *
221
	 * @param string $realServer
222
	 * @return mixed Raw connection
223
	 * @throws DBConnectionError
224
	 */
225
	abstract protected function mysqlConnect( $realServer );
226
227
	/**
228
	 * Set the character set of the MySQL link
229
	 *
230
	 * @param string $charset
231
	 * @return bool
232
	 */
233
	abstract protected function mysqlSetCharset( $charset );
234
235
	/**
236
	 * @param ResultWrapper|resource $res
237
	 * @throws DBUnexpectedError
238
	 */
239 View Code Duplication
	function freeResult( $res ) {
240
		if ( $res instanceof ResultWrapper ) {
241
			$res = $res->result;
242
		}
243
		MediaWiki\suppressWarnings();
244
		$ok = $this->mysqlFreeResult( $res );
245
		MediaWiki\restoreWarnings();
246
		if ( !$ok ) {
247
			throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
248
		}
249
	}
250
251
	/**
252
	 * Free result memory
253
	 *
254
	 * @param resource $res Raw result
255
	 * @return bool
256
	 */
257
	abstract protected function mysqlFreeResult( $res );
258
259
	/**
260
	 * @param ResultWrapper|resource $res
261
	 * @return stdClass|bool
262
	 * @throws DBUnexpectedError
263
	 */
264 View Code Duplication
	function fetchObject( $res ) {
265
		if ( $res instanceof ResultWrapper ) {
266
			$res = $res->result;
267
		}
268
		MediaWiki\suppressWarnings();
269
		$row = $this->mysqlFetchObject( $res );
270
		MediaWiki\restoreWarnings();
271
272
		$errno = $this->lastErrno();
273
		// Unfortunately, mysql_fetch_object does not reset the last errno.
274
		// Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
275
		// these are the only errors mysql_fetch_object can cause.
276
		// See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
277
		if ( $errno == 2000 || $errno == 2013 ) {
278
			throw new DBUnexpectedError(
279
				$this,
280
				'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() )
281
			);
282
		}
283
284
		return $row;
285
	}
286
287
	/**
288
	 * Fetch a result row as an object
289
	 *
290
	 * @param resource $res Raw result
291
	 * @return stdClass
292
	 */
293
	abstract protected function mysqlFetchObject( $res );
294
295
	/**
296
	 * @param ResultWrapper|resource $res
297
	 * @return array|bool
298
	 * @throws DBUnexpectedError
299
	 */
300 View Code Duplication
	function fetchRow( $res ) {
301
		if ( $res instanceof ResultWrapper ) {
302
			$res = $res->result;
303
		}
304
		MediaWiki\suppressWarnings();
305
		$row = $this->mysqlFetchArray( $res );
306
		MediaWiki\restoreWarnings();
307
308
		$errno = $this->lastErrno();
309
		// Unfortunately, mysql_fetch_array does not reset the last errno.
310
		// Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
311
		// these are the only errors mysql_fetch_array can cause.
312
		// See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
313
		if ( $errno == 2000 || $errno == 2013 ) {
314
			throw new DBUnexpectedError(
315
				$this,
316
				'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() )
317
			);
318
		}
319
320
		return $row;
321
	}
322
323
	/**
324
	 * Fetch a result row as an associative and numeric array
325
	 *
326
	 * @param resource $res Raw result
327
	 * @return array
328
	 */
329
	abstract protected function mysqlFetchArray( $res );
330
331
	/**
332
	 * @throws DBUnexpectedError
333
	 * @param ResultWrapper|resource $res
334
	 * @return int
335
	 */
336
	function numRows( $res ) {
337
		if ( $res instanceof ResultWrapper ) {
338
			$res = $res->result;
339
		}
340
		MediaWiki\suppressWarnings();
341
		$n = $this->mysqlNumRows( $res );
342
		MediaWiki\restoreWarnings();
343
344
		// Unfortunately, mysql_num_rows does not reset the last errno.
345
		// We are not checking for any errors here, since
346
		// these are no errors mysql_num_rows can cause.
347
		// See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
348
		// See https://phabricator.wikimedia.org/T44430
349
		return $n;
350
	}
351
352
	/**
353
	 * Get number of rows in result
354
	 *
355
	 * @param resource $res Raw result
356
	 * @return int
357
	 */
358
	abstract protected function mysqlNumRows( $res );
359
360
	/**
361
	 * @param ResultWrapper|resource $res
362
	 * @return int
363
	 */
364
	function numFields( $res ) {
365
		if ( $res instanceof ResultWrapper ) {
366
			$res = $res->result;
367
		}
368
369
		return $this->mysqlNumFields( $res );
370
	}
371
372
	/**
373
	 * Get number of fields in result
374
	 *
375
	 * @param resource $res Raw result
376
	 * @return int
377
	 */
378
	abstract protected function mysqlNumFields( $res );
379
380
	/**
381
	 * @param ResultWrapper|resource $res
382
	 * @param int $n
383
	 * @return string
384
	 */
385
	function fieldName( $res, $n ) {
386
		if ( $res instanceof ResultWrapper ) {
387
			$res = $res->result;
388
		}
389
390
		return $this->mysqlFieldName( $res, $n );
391
	}
392
393
	/**
394
	 * Get the name of the specified field in a result
395
	 *
396
	 * @param ResultWrapper|resource $res
397
	 * @param int $n
398
	 * @return string
399
	 */
400
	abstract protected function mysqlFieldName( $res, $n );
401
402
	/**
403
	 * mysql_field_type() wrapper
404
	 * @param ResultWrapper|resource $res
405
	 * @param int $n
406
	 * @return string
407
	 */
408
	public function fieldType( $res, $n ) {
409
		if ( $res instanceof ResultWrapper ) {
410
			$res = $res->result;
411
		}
412
413
		return $this->mysqlFieldType( $res, $n );
414
	}
415
416
	/**
417
	 * Get the type of the specified field in a result
418
	 *
419
	 * @param ResultWrapper|resource $res
420
	 * @param int $n
421
	 * @return string
422
	 */
423
	abstract protected function mysqlFieldType( $res, $n );
424
425
	/**
426
	 * @param ResultWrapper|resource $res
427
	 * @param int $row
428
	 * @return bool
429
	 */
430
	function dataSeek( $res, $row ) {
431
		if ( $res instanceof ResultWrapper ) {
432
			$res = $res->result;
433
		}
434
435
		return $this->mysqlDataSeek( $res, $row );
436
	}
437
438
	/**
439
	 * Move internal result pointer
440
	 *
441
	 * @param ResultWrapper|resource $res
442
	 * @param int $row
443
	 * @return bool
444
	 */
445
	abstract protected function mysqlDataSeek( $res, $row );
446
447
	/**
448
	 * @return string
449
	 */
450
	function lastError() {
451
		if ( $this->mConn ) {
452
			# Even if it's non-zero, it can still be invalid
453
			MediaWiki\suppressWarnings();
454
			$error = $this->mysqlError( $this->mConn );
455
			if ( !$error ) {
456
				$error = $this->mysqlError();
457
			}
458
			MediaWiki\restoreWarnings();
459
		} else {
460
			$error = $this->mysqlError();
461
		}
462
		if ( $error ) {
463
			$error .= ' (' . $this->mServer . ')';
464
		}
465
466
		return $error;
467
	}
468
469
	/**
470
	 * Returns the text of the error message from previous MySQL operation
471
	 *
472
	 * @param resource $conn Raw connection
473
	 * @return string
474
	 */
475
	abstract protected function mysqlError( $conn = null );
476
477
	/**
478
	 * @param string $table
479
	 * @param array $uniqueIndexes
480
	 * @param array $rows
481
	 * @param string $fname
482
	 * @return ResultWrapper
483
	 */
484
	function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
485
		return $this->nativeReplace( $table, $rows, $fname );
486
	}
487
488
	/**
489
	 * Estimate rows in dataset
490
	 * Returns estimated count, based on EXPLAIN output
491
	 * Takes same arguments as Database::select()
492
	 *
493
	 * @param string|array $table
494
	 * @param string|array $vars
495
	 * @param string|array $conds
496
	 * @param string $fname
497
	 * @param string|array $options
498
	 * @return bool|int
499
	 */
500
	public function estimateRowCount( $table, $vars = '*', $conds = '',
501
		$fname = __METHOD__, $options = []
502
	) {
503
		$options['EXPLAIN'] = true;
504
		$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 500 can also be of type array; however, DatabaseBase::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 501 can also be of type string; however, DatabaseBase::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...
505
		if ( $res === false ) {
506
			return false;
507
		}
508
		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 504 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...
509
			return 0;
510
		}
511
512
		$rows = 1;
513
		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...
514
			$rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
515
		}
516
517
		return (int)$rows;
518
	}
519
520
	/**
521
	 * @param string $table
522
	 * @param string $field
523
	 * @return bool|MySQLField
524
	 */
525
	function fieldInfo( $table, $field ) {
526
		$table = $this->tableName( $table );
527
		$res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
528
		if ( !$res ) {
529
			return false;
530
		}
531
		$n = $this->mysqlNumFields( $res->result );
532
		for ( $i = 0; $i < $n; $i++ ) {
533
			$meta = $this->mysqlFetchField( $res->result, $i );
534
			if ( $field == $meta->name ) {
535
				return new MySQLField( $meta );
536
			}
537
		}
538
539
		return false;
540
	}
541
542
	/**
543
	 * Get column information from a result
544
	 *
545
	 * @param resource $res Raw result
546
	 * @param int $n
547
	 * @return stdClass
548
	 */
549
	abstract protected function mysqlFetchField( $res, $n );
550
551
	/**
552
	 * Get information about an index into an object
553
	 * Returns false if the index does not exist
554
	 *
555
	 * @param string $table
556
	 * @param string $index
557
	 * @param string $fname
558
	 * @return bool|array|null False or null on failure
559
	 */
560
	function indexInfo( $table, $index, $fname = __METHOD__ ) {
561
		# SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
562
		# SHOW INDEX should work for 3.x and up:
563
		# http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
564
		$table = $this->tableName( $table );
565
		$index = $this->indexName( $index );
566
567
		$sql = 'SHOW INDEX FROM ' . $table;
568
		$res = $this->query( $sql, $fname );
569
570
		if ( !$res ) {
571
			return null;
572
		}
573
574
		$result = [];
575
576
		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...
577
			if ( $row->Key_name == $index ) {
578
				$result[] = $row;
579
			}
580
		}
581
582
		return empty( $result ) ? false : $result;
583
	}
584
585
	/**
586
	 * @param string $s
587
	 * @return string
588
	 */
589
	function strencode( $s ) {
590
		return $this->mysqlRealEscapeString( $s );
591
	}
592
593
	/**
594
	 * @param string $s
595
	 * @return mixed
596
	 */
597
	abstract protected function mysqlRealEscapeString( $s );
598
599
	/**
600
	 * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
601
	 *
602
	 * @param string $s
603
	 * @return string
604
	 */
605
	public function addIdentifierQuotes( $s ) {
606
		// Characters in the range \u0001-\uFFFF are valid in a quoted identifier
607
		// Remove NUL bytes and escape backticks by doubling
608
		return '`' . str_replace( [ "\0", '`' ], [ '', '``' ], $s ) . '`';
609
	}
610
611
	/**
612
	 * @param string $name
613
	 * @return bool
614
	 */
615
	public function isQuotedIdentifier( $name ) {
616
		return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
617
	}
618
619
	function getLag() {
620
		if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
621
			return $this->getLagFromPtHeartbeat();
622
		} else {
623
			return $this->getLagFromSlaveStatus();
624
		}
625
	}
626
627
	/**
628
	 * @return string
629
	 */
630
	protected function getLagDetectionMethod() {
631
		return $this->lagDetectionMethod;
632
	}
633
634
	/**
635
	 * @return bool|int
636
	 */
637
	protected function getLagFromSlaveStatus() {
638
		$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
639
		$row = $res ? $res->fetchObject() : false;
640
		if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
641
			return intval( $row->Seconds_Behind_Master );
642
		}
643
644
		return false;
645
	}
646
647
	/**
648
	 * @return bool|float
649
	 */
650
	protected function getLagFromPtHeartbeat() {
651
		$options = $this->lagDetectionOptions;
652
653
		if ( isset( $options['conds'] ) ) {
654
			// Best method for multi-DC setups: use logical channel names
655
			$data = $this->getHeartbeatData( $options['conds'] );
656
		} else {
657
			// Standard method: use master server ID (works with stock pt-heartbeat)
658
			$masterInfo = $this->getMasterServerInfo();
659
			if ( !$masterInfo ) {
660
				wfLogDBError(
661
					"Unable to query master of {db_server} for server ID",
662
					$this->getLogContext( [
663
						'method' => __METHOD__
664
					] )
665
				);
666
667
				return false; // could not get master server ID
668
			}
669
670
			$conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
671
			$data = $this->getHeartbeatData( $conds );
672
		}
673
674
		list( $time, $nowUnix ) = $data;
675
		if ( $time !== null ) {
676
			// @time is in ISO format like "2015-09-25T16:48:10.000510"
677
			$dateTime = new DateTime( $time, new DateTimeZone( 'UTC' ) );
678
			$timeUnix = (int)$dateTime->format( 'U' ) + $dateTime->format( 'u' ) / 1e6;
679
680
			return max( $nowUnix - $timeUnix, 0.0 );
681
		}
682
683
		wfLogDBError(
684
			"Unable to find pt-heartbeat row for {db_server}",
685
			$this->getLogContext( [
686
				'method' => __METHOD__
687
			] )
688
		);
689
690
		return false;
691
	}
692
693
	protected function getMasterServerInfo() {
694
		$cache = $this->srvCache;
695
		$key = $cache->makeGlobalKey(
696
			'mysql',
697
			'master-info',
698
			// Using one key for all cluster replica DBs is preferable
699
			$this->getLBInfo( 'clusterMasterHost' ) ?: $this->getServer()
700
		);
701
702
		return $cache->getWithSetCallback(
703
			$key,
704
			$cache::TTL_INDEFINITE,
705
			function () use ( $cache, $key ) {
706
				// Get and leave a lock key in place for a short period
707
				if ( !$cache->lock( $key, 0, 10 ) ) {
708
					return false; // avoid master connection spike slams
709
				}
710
711
				$conn = $this->getLazyMasterHandle();
712
				if ( !$conn ) {
713
					return false; // something is misconfigured
714
				}
715
716
				// Connect to and query the master; catch errors to avoid outages
717
				try {
718
					$res = $conn->query( 'SELECT @@server_id AS id', __METHOD__ );
719
					$row = $res ? $res->fetchObject() : false;
720
					$id = $row ? (int)$row->id : 0;
721
				} catch ( DBError $e ) {
722
					$id = 0;
723
				}
724
725
				// Cache the ID if it was retrieved
726
				return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false;
727
			}
728
		);
729
	}
730
731
	/**
732
	 * @param array $conds WHERE clause conditions to find a row
733
	 * @return array (heartbeat `ts` column value or null, UNIX timestamp) for the newest beat
734
	 * @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html
735
	 */
736
	protected function getHeartbeatData( array $conds ) {
737
		$whereSQL = $this->makeList( $conds, LIST_AND );
738
		// Use ORDER BY for channel based queries since that field might not be UNIQUE.
739
		// Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
740
		// percision field is not supported in MySQL <= 5.5.
741
		$res = $this->query(
742
			"SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1"
743
		);
744
		$row = $res ? $res->fetchObject() : false;
745
746
		return [ $row ? $row->ts : null, microtime( true ) ];
747
	}
748
749
	public function getApproximateLagStatus() {
750
		if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
751
			// Disable caching since this is fast enough and we don't wan't
752
			// to be *too* pessimistic by having both the cache TTL and the
753
			// pt-heartbeat interval count as lag in getSessionLagStatus()
754
			return parent::getApproximateLagStatus();
755
		}
756
757
		$key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServer() );
758
		$approxLag = $this->srvCache->get( $key );
759
		if ( !$approxLag ) {
760
			$approxLag = parent::getApproximateLagStatus();
761
			$this->srvCache->set( $key, $approxLag, 1 );
762
		}
763
764
		return $approxLag;
765
	}
766
767
	function masterPosWait( DBMasterPos $pos, $timeout ) {
768
		if ( !( $pos instanceof MySQLMasterPos ) ) {
769
			throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" );
770
		}
771
772
		if ( $this->getLBInfo( 'is static' ) === true ) {
773
			return 0; // this is a copy of a read-only dataset with no master DB
774
		} elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) {
775
			return 0; // already reached this point for sure
776
		}
777
778
		// Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
779
		if ( $this->useGTIDs && $pos->gtids ) {
780
			// Wait on the GTID set (MariaDB only)
781
			$gtidArg = $this->addQuotes( implode( ',', $pos->gtids ) );
782
			$res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
783
		} else {
784
			// Wait on the binlog coordinates
785
			$encFile = $this->addQuotes( $pos->file );
786
			$encPos = intval( $pos->pos );
787
			$res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
788
		}
789
790
		$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...
791
		if ( !$row ) {
792
			throw new DBExpectedError( $this, "Failed to query MASTER_POS_WAIT()" );
793
		}
794
795
		// Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
796
		$status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
797
		if ( $status === null ) {
798
			// T126436: jobs programmed to wait on master positions might be referencing binlogs
799
			// with an old master hostname. Such calls make MASTER_POS_WAIT() return null. Try
800
			// to detect this and treat the replica DB as having reached the position; a proper master
801
			// switchover already requires that the new master be caught up before the switch.
802
			$replicationPos = $this->getSlavePos();
803
			if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) {
804
				$this->lastKnownReplicaPos = $replicationPos;
805
				$status = 0;
806
			}
807
		} elseif ( $status >= 0 ) {
808
			// Remember that this position was reached to save queries next time
809
			$this->lastKnownReplicaPos = $pos;
810
		}
811
812
		return $status;
813
	}
814
815
	/**
816
	 * Get the position of the master from SHOW SLAVE STATUS
817
	 *
818
	 * @return MySQLMasterPos|bool
819
	 */
820
	function getSlavePos() {
821
		$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
822
		$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 821 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...
823
824
		if ( $row ) {
825
			$pos = isset( $row->Exec_master_log_pos )
826
				? $row->Exec_master_log_pos
827
				: $row->Exec_Master_Log_Pos;
828
			// Also fetch the last-applied GTID set (MariaDB)
829 View Code Duplication
			if ( $this->useGTIDs ) {
830
				$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_slave_pos'", __METHOD__ );
831
				$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 830 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...
832
				$gtidSet = $gtidRow ? $gtidRow->Value : '';
833
			} else {
834
				$gtidSet = '';
835
			}
836
837
			return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos, $gtidSet );
838
		} else {
839
			return false;
840
		}
841
	}
842
843
	/**
844
	 * Get the position of the master from SHOW MASTER STATUS
845
	 *
846
	 * @return MySQLMasterPos|bool
847
	 */
848
	function getMasterPos() {
849
		$res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
850
		$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 849 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...
851
852
		if ( $row ) {
853
			// Also fetch the last-written GTID set (MariaDB)
854 View Code Duplication
			if ( $this->useGTIDs ) {
855
				$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_binlog_pos'", __METHOD__ );
856
				$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 855 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...
857
				$gtidSet = $gtidRow ? $gtidRow->Value : '';
858
			} else {
859
				$gtidSet = '';
860
			}
861
862
			return new MySQLMasterPos( $row->File, $row->Position, $gtidSet );
863
		} else {
864
			return false;
865
		}
866
	}
867
868
	public function serverIsReadOnly() {
869
		$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__ );
870
		$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 869 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...
871
872
		return $row ? ( strtolower( $row->Value ) === 'on' ) : false;
873
	}
874
875
	/**
876
	 * @param string $index
877
	 * @return string
878
	 */
879
	function useIndexClause( $index ) {
880
		return "FORCE INDEX (" . $this->indexName( $index ) . ")";
881
	}
882
883
	/**
884
	 * @param string $index
885
	 * @return string
886
	 */
887
	function ignoreIndexClause( $index ) {
888
		return "IGNORE INDEX (" . $this->indexName( $index ) . ")";
889
	}
890
891
	/**
892
	 * @return string
893
	 */
894
	function lowPriorityOption() {
895
		return 'LOW_PRIORITY';
896
	}
897
898
	/**
899
	 * @return string
900
	 */
901
	public function getSoftwareLink() {
902
		// MariaDB includes its name in its version string; this is how MariaDB's version of
903
		// the mysql command-line client identifies MariaDB servers (see mariadb_connection()
904
		// in libmysql/libmysql.c).
905
		$version = $this->getServerVersion();
906
		if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) {
907
			return '[{{int:version-db-mariadb-url}} MariaDB]';
908
		}
909
910
		// Percona Server's version suffix is not very distinctive, and @@version_comment
911
		// doesn't give the necessary info for source builds, so assume the server is MySQL.
912
		// (Even Percona's version of mysql doesn't try to make the distinction.)
913
		return '[{{int:version-db-mysql-url}} MySQL]';
914
	}
915
916
	/**
917
	 * @return string
918
	 */
919
	public function getServerVersion() {
920
		// Not using mysql_get_server_info() or similar for consistency: in the handshake,
921
		// MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
922
		// it off (see RPL_VERSION_HACK in include/mysql_com.h).
923
		if ( $this->serverVersion === null ) {
924
			$this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ );
925
		}
926
		return $this->serverVersion;
927
	}
928
929
	/**
930
	 * @param array $options
931
	 */
932
	public function setSessionOptions( array $options ) {
933
		if ( isset( $options['connTimeout'] ) ) {
934
			$timeout = (int)$options['connTimeout'];
935
			$this->query( "SET net_read_timeout=$timeout" );
936
			$this->query( "SET net_write_timeout=$timeout" );
937
		}
938
	}
939
940
	/**
941
	 * @param string $sql
942
	 * @param string $newLine
943
	 * @return bool
944
	 */
945
	public function streamStatementEnd( &$sql, &$newLine ) {
946
		if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
947
			preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
948
			$this->delimiter = $m[1];
949
			$newLine = '';
950
		}
951
952
		return parent::streamStatementEnd( $sql, $newLine );
953
	}
954
955
	/**
956
	 * Check to see if a named lock is available. This is non-blocking.
957
	 *
958
	 * @param string $lockName Name of lock to poll
959
	 * @param string $method Name of method calling us
960
	 * @return bool
961
	 * @since 1.20
962
	 */
963 View Code Duplication
	public function lockIsFree( $lockName, $method ) {
964
		$lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
965
		$result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method );
966
		$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 965 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...
967
968
		return ( $row->lockstatus == 1 );
969
	}
970
971
	/**
972
	 * @param string $lockName
973
	 * @param string $method
974
	 * @param int $timeout
975
	 * @return bool
976
	 */
977 View Code Duplication
	public function lock( $lockName, $method, $timeout = 5 ) {
978
		$lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
979
		$result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method );
980
		$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 979 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...
981
982
		if ( $row->lockstatus == 1 ) {
983
			parent::lock( $lockName, $method, $timeout ); // record
984
			return true;
985
		}
986
987
		wfDebug( __METHOD__ . " failed to acquire lock\n" );
988
989
		return false;
990
	}
991
992
	/**
993
	 * FROM MYSQL DOCS:
994
	 * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
995
	 * @param string $lockName
996
	 * @param string $method
997
	 * @return bool
998
	 */
999 View Code Duplication
	public function unlock( $lockName, $method ) {
1000
		$lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
1001
		$result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method );
1002
		$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 1001 can also be of type boolean; however, DatabaseMysqlBase::fetchObject() does only seem to accept object<ResultWrapper>|resource, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1003
1004
		if ( $row->lockstatus == 1 ) {
1005
			parent::unlock( $lockName, $method ); // record
1006
			return true;
1007
		}
1008
1009
		wfDebug( __METHOD__ . " failed to release lock\n" );
1010
1011
		return false;
1012
	}
1013
1014
	private function makeLockName( $lockName ) {
1015
		// http://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1016
		// Newer version enforce a 64 char length limit.
1017
		return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
1018
	}
1019
1020
	public function namedLocksEnqueue() {
1021
		return true;
1022
	}
1023
1024
	/**
1025
	 * @param array $read
1026
	 * @param array $write
1027
	 * @param string $method
1028
	 * @param bool $lowPriority
1029
	 * @return bool
1030
	 */
1031
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
1032
		$items = [];
1033
1034
		foreach ( $write as $table ) {
1035
			$tbl = $this->tableName( $table ) .
1036
				( $lowPriority ? ' LOW_PRIORITY' : '' ) .
1037
				' WRITE';
1038
			$items[] = $tbl;
1039
		}
1040
		foreach ( $read as $table ) {
1041
			$items[] = $this->tableName( $table ) . ' READ';
1042
		}
1043
		$sql = "LOCK TABLES " . implode( ',', $items );
1044
		$this->query( $sql, $method );
1045
1046
		return true;
1047
	}
1048
1049
	/**
1050
	 * @param string $method
1051
	 * @return bool
1052
	 */
1053
	public function unlockTables( $method ) {
1054
		$this->query( "UNLOCK TABLES", $method );
1055
1056
		return true;
1057
	}
1058
1059
	/**
1060
	 * Get search engine class. All subclasses of this
1061
	 * need to implement this if they wish to use searching.
1062
	 *
1063
	 * @return string
1064
	 */
1065
	public function getSearchEngine() {
1066
		return 'SearchMySQL';
1067
	}
1068
1069
	/**
1070
	 * @param bool $value
1071
	 */
1072
	public function setBigSelects( $value = true ) {
1073
		if ( $value === 'default' ) {
1074
			if ( $this->mDefaultBigSelects === null ) {
1075
				# Function hasn't been called before so it must already be set to the default
1076
				return;
1077
			} else {
1078
				$value = $this->mDefaultBigSelects;
1079
			}
1080
		} elseif ( $this->mDefaultBigSelects === null ) {
1081
			$this->mDefaultBigSelects =
1082
				(bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
1083
		}
1084
		$encValue = $value ? '1' : '0';
1085
		$this->query( "SET sql_big_selects=$encValue", __METHOD__ );
1086
	}
1087
1088
	/**
1089
	 * DELETE where the condition is a join. MySql uses multi-table deletes.
1090
	 * @param string $delTable
1091
	 * @param string $joinTable
1092
	 * @param string $delVar
1093
	 * @param string $joinVar
1094
	 * @param array|string $conds
1095
	 * @param bool|string $fname
1096
	 * @throws DBUnexpectedError
1097
	 * @return bool|ResultWrapper
1098
	 */
1099 View Code Duplication
	function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ ) {
1100
		if ( !$conds ) {
1101
			throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' );
1102
		}
1103
1104
		$delTable = $this->tableName( $delTable );
1105
		$joinTable = $this->tableName( $joinTable );
1106
		$sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
1107
1108
		if ( $conds != '*' ) {
1109
			$sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
0 ignored issues
show
Bug introduced by
It seems like $conds defined by parameter $conds on line 1099 can also be of type string; however, DatabaseBase::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...
1110
		}
1111
1112
		return $this->query( $sql, $fname );
0 ignored issues
show
Bug introduced by
It seems like $fname defined by parameter $fname on line 1099 can also be of type boolean; however, DatabaseBase::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...
1113
	}
1114
1115
	/**
1116
	 * @param string $table
1117
	 * @param array $rows
1118
	 * @param array $uniqueIndexes
1119
	 * @param array $set
1120
	 * @param string $fname
1121
	 * @return bool
1122
	 */
1123
	public function upsert( $table, array $rows, array $uniqueIndexes,
1124
		array $set, $fname = __METHOD__
1125
	) {
1126
		if ( !count( $rows ) ) {
1127
			return true; // nothing to do
1128
		}
1129
1130
		if ( !is_array( reset( $rows ) ) ) {
1131
			$rows = [ $rows ];
1132
		}
1133
1134
		$table = $this->tableName( $table );
1135
		$columns = array_keys( $rows[0] );
1136
1137
		$sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES ';
1138
		$rowTuples = [];
1139
		foreach ( $rows as $row ) {
1140
			$rowTuples[] = '(' . $this->makeList( $row ) . ')';
1141
		}
1142
		$sql .= implode( ',', $rowTuples );
1143
		$sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, LIST_SET );
1144
1145
		return (bool)$this->query( $sql, $fname );
1146
	}
1147
1148
	/**
1149
	 * Determines how long the server has been up
1150
	 *
1151
	 * @return int
1152
	 */
1153
	function getServerUptime() {
1154
		$vars = $this->getMysqlStatus( 'Uptime' );
1155
1156
		return (int)$vars['Uptime'];
1157
	}
1158
1159
	/**
1160
	 * Determines if the last failure was due to a deadlock
1161
	 *
1162
	 * @return bool
1163
	 */
1164
	function wasDeadlock() {
1165
		return $this->lastErrno() == 1213;
1166
	}
1167
1168
	/**
1169
	 * Determines if the last failure was due to a lock timeout
1170
	 *
1171
	 * @return bool
1172
	 */
1173
	function wasLockTimeout() {
1174
		return $this->lastErrno() == 1205;
1175
	}
1176
1177
	function wasErrorReissuable() {
1178
		return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
1179
	}
1180
1181
	/**
1182
	 * Determines if the last failure was due to the database being read-only.
1183
	 *
1184
	 * @return bool
1185
	 */
1186
	function wasReadOnlyError() {
1187
		return $this->lastErrno() == 1223 ||
1188
			( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
1189
	}
1190
1191
	function wasConnectionError( $errno ) {
1192
		return $errno == 2013 || $errno == 2006;
1193
	}
1194
1195
	/**
1196
	 * Get the underlying binding handle, mConn
1197
	 *
1198
	 * Makes sure that mConn is set (disconnects and ping() failure can unset it).
1199
	 * This catches broken callers than catch and ignore disconnection exceptions.
1200
	 * Unlike checking isOpen(), this is safe to call inside of open().
1201
	 *
1202
	 * @return resource|object
1203
	 * @throws DBUnexpectedError
1204
	 * @since 1.26
1205
	 */
1206
	protected function getBindingHandle() {
1207
		if ( !$this->mConn ) {
1208
			throw new DBUnexpectedError(
1209
				$this,
1210
				'DB connection was already closed or the connection dropped.'
1211
			);
1212
		}
1213
1214
		return $this->mConn;
1215
	}
1216
1217
	/**
1218
	 * @param string $oldName
1219
	 * @param string $newName
1220
	 * @param bool $temporary
1221
	 * @param string $fname
1222
	 * @return bool
1223
	 */
1224
	function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
1225
		$tmp = $temporary ? 'TEMPORARY ' : '';
1226
		$newName = $this->addIdentifierQuotes( $newName );
1227
		$oldName = $this->addIdentifierQuotes( $oldName );
1228
		$query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
1229
1230
		return $this->query( $query, $fname );
1231
	}
1232
1233
	/**
1234
	 * List all tables on the database
1235
	 *
1236
	 * @param string $prefix Only show tables with this prefix, e.g. mw_
1237
	 * @param string $fname Calling function name
1238
	 * @return array
1239
	 */
1240
	function listTables( $prefix = null, $fname = __METHOD__ ) {
1241
		$result = $this->query( "SHOW TABLES", $fname );
1242
1243
		$endArray = [];
1244
1245 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...
1246
			$vars = get_object_vars( $table );
1247
			$table = array_pop( $vars );
1248
1249
			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...
1250
				$endArray[] = $table;
1251
			}
1252
		}
1253
1254
		return $endArray;
1255
	}
1256
1257
	/**
1258
	 * @param string $tableName
1259
	 * @param string $fName
1260
	 * @return bool|ResultWrapper
1261
	 */
1262 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
1263
		if ( !$this->tableExists( $tableName, $fName ) ) {
1264
			return false;
1265
		}
1266
1267
		return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
1268
	}
1269
1270
	/**
1271
	 * @return array
1272
	 */
1273
	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...
1274
		$vars = parent::getDefaultSchemaVars();
1275
		$vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
1276
		$vars['wgDBTableOptions'] = str_replace(
1277
			'CHARSET=mysql4',
1278
			'CHARSET=binary',
1279
			$vars['wgDBTableOptions']
1280
		);
1281
1282
		return $vars;
1283
	}
1284
1285
	/**
1286
	 * Get status information from SHOW STATUS in an associative array
1287
	 *
1288
	 * @param string $which
1289
	 * @return array
1290
	 */
1291
	function getMysqlStatus( $which = "%" ) {
1292
		$res = $this->query( "SHOW STATUS LIKE '{$which}'" );
1293
		$status = [];
1294
1295
		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...
1296
			$status[$row->Variable_name] = $row->Value;
1297
		}
1298
1299
		return $status;
1300
	}
1301
1302
	/**
1303
	 * Lists VIEWs in the database
1304
	 *
1305
	 * @param string $prefix Only show VIEWs with this prefix, eg.
1306
	 * unit_test_, or $wgDBprefix. Default: null, would return all views.
1307
	 * @param string $fname Name of calling function
1308
	 * @return array
1309
	 * @since 1.22
1310
	 */
1311
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
1312
1313
		if ( !isset( $this->allViews ) ) {
1314
1315
			// The name of the column containing the name of the VIEW
1316
			$propertyName = 'Tables_in_' . $this->mDBname;
1317
1318
			// Query for the VIEWS
1319
			$result = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
1320
			$this->allViews = [];
1321
			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 1319 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...
1322
				array_push( $this->allViews, $row[$propertyName] );
1323
			}
1324
		}
1325
1326
		if ( is_null( $prefix ) || $prefix === '' ) {
1327
			return $this->allViews;
1328
		}
1329
1330
		$filteredViews = [];
1331
		foreach ( $this->allViews as $viewName ) {
1332
			// Does the name of this VIEW start with the table-prefix?
1333
			if ( strpos( $viewName, $prefix ) === 0 ) {
1334
				array_push( $filteredViews, $viewName );
1335
			}
1336
		}
1337
1338
		return $filteredViews;
1339
	}
1340
1341
	/**
1342
	 * Differentiates between a TABLE and a VIEW.
1343
	 *
1344
	 * @param string $name Name of the TABLE/VIEW to test
1345
	 * @param string $prefix
1346
	 * @return bool
1347
	 * @since 1.22
1348
	 */
1349
	public function isView( $name, $prefix = null ) {
1350
		return in_array( $name, $this->listViews( $prefix ) );
1351
	}
1352
}
1353
1354
/**
1355
 * Utility class.
1356
 * @ingroup Database
1357
 */
1358
class MySQLField implements Field {
1359
	private $name, $tablename, $default, $max_length, $nullable,
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
1360
		$is_pk, $is_unique, $is_multiple, $is_key, $type, $binary,
1361
		$is_numeric, $is_blob, $is_unsigned, $is_zerofill;
1362
1363
	function __construct( $info ) {
1364
		$this->name = $info->name;
1365
		$this->tablename = $info->table;
1366
		$this->default = $info->def;
1367
		$this->max_length = $info->max_length;
1368
		$this->nullable = !$info->not_null;
1369
		$this->is_pk = $info->primary_key;
1370
		$this->is_unique = $info->unique_key;
1371
		$this->is_multiple = $info->multiple_key;
1372
		$this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple );
1373
		$this->type = $info->type;
1374
		$this->binary = isset( $info->binary ) ? $info->binary : false;
1375
		$this->is_numeric = isset( $info->numeric ) ? $info->numeric : false;
1376
		$this->is_blob = isset( $info->blob ) ? $info->blob : false;
1377
		$this->is_unsigned = isset( $info->unsigned ) ? $info->unsigned : false;
1378
		$this->is_zerofill = isset( $info->zerofill ) ? $info->zerofill : false;
1379
	}
1380
1381
	/**
1382
	 * @return string
1383
	 */
1384
	function name() {
1385
		return $this->name;
1386
	}
1387
1388
	/**
1389
	 * @return string
1390
	 */
1391
	function tableName() {
1392
		return $this->tablename;
1393
	}
1394
1395
	/**
1396
	 * @return string
1397
	 */
1398
	function type() {
1399
		return $this->type;
1400
	}
1401
1402
	/**
1403
	 * @return bool
1404
	 */
1405
	function isNullable() {
1406
		return $this->nullable;
1407
	}
1408
1409
	function defaultValue() {
1410
		return $this->default;
1411
	}
1412
1413
	/**
1414
	 * @return bool
1415
	 */
1416
	function isKey() {
1417
		return $this->is_key;
1418
	}
1419
1420
	/**
1421
	 * @return bool
1422
	 */
1423
	function isMultipleKey() {
1424
		return $this->is_multiple;
1425
	}
1426
1427
	/**
1428
	 * @return bool
1429
	 */
1430
	function isBinary() {
1431
		return $this->binary;
1432
	}
1433
1434
	/**
1435
	 * @return bool
1436
	 */
1437
	function isNumeric() {
1438
		return $this->is_numeric;
1439
	}
1440
1441
	/**
1442
	 * @return bool
1443
	 */
1444
	function isBlob() {
1445
		return $this->is_blob;
1446
	}
1447
1448
	/**
1449
	 * @return bool
1450
	 */
1451
	function isUnsigned() {
1452
		return $this->is_unsigned;
1453
	}
1454
1455
	/**
1456
	 * @return bool
1457
	 */
1458
	function isZerofill() {
1459
		return $this->is_zerofill;
1460
	}
1461
}
1462
1463
/**
1464
 * DBMasterPos class for MySQL/MariaDB
1465
 *
1466
 * Note that master positions and sync logic here make some assumptions:
1467
 *  - Binlog-based usage assumes single-source replication and non-hierarchical replication.
1468
 *  - GTID-based usage allows getting/syncing with multi-source replication. It is assumed
1469
 *    that GTID sets are complete (e.g. include all domains on the server).
1470
 */
1471
class MySQLMasterPos implements DBMasterPos {
1472
	/** @var string Binlog file */
1473
	public $file;
1474
	/** @var int Binglog file position */
1475
	public $pos;
1476
	/** @var string[] GTID list */
1477
	public $gtids = [];
1478
	/** @var float UNIX timestamp */
1479
	public $asOfTime = 0.0;
1480
1481
	/**
1482
	 * @param string $file Binlog file name
1483
	 * @param integer $pos Binlog position
1484
	 * @param string $gtid Comma separated GTID set [optional]
1485
	 */
1486
	function __construct( $file, $pos, $gtid = '' ) {
1487
		$this->file = $file;
1488
		$this->pos = $pos;
1489
		$this->gtids = array_map( 'trim', explode( ',', $gtid ) );
1490
		$this->asOfTime = microtime( true );
1491
	}
1492
1493
	/**
1494
	 * @return string <binlog file>/<position>, e.g db1034-bin.000976/843431247
1495
	 */
1496
	function __toString() {
1497
		return "{$this->file}/{$this->pos}";
1498
	}
1499
1500
	function asOfTime() {
1501
		return $this->asOfTime;
1502
	}
1503
1504
	function hasReached( DBMasterPos $pos ) {
1505
		if ( !( $pos instanceof self ) ) {
1506
			throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
1507
		}
1508
1509
		// Prefer GTID comparisons, which work with multi-tier replication
1510
		$thisPosByDomain = $this->getGtidCoordinates();
1511
		$thatPosByDomain = $pos->getGtidCoordinates();
1512
		if ( $thisPosByDomain && $thatPosByDomain ) {
1513
			$reached = true;
1514
			// Check that this has positions GTE all of those in $pos for all domains in $pos
1515
			foreach ( $thatPosByDomain as $domain => $thatPos ) {
1516
				$thisPos = isset( $thisPosByDomain[$domain] ) ? $thisPosByDomain[$domain] : -1;
1517
				$reached = $reached && ( $thatPos <= $thisPos );
1518
			}
1519
1520
			return $reached;
1521
		}
1522
1523
		// Fallback to the binlog file comparisons
1524
		$thisBinPos = $this->getBinlogCoordinates();
1525
		$thatBinPos = $pos->getBinlogCoordinates();
1526
		if ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] ) {
1527
			return ( $thisBinPos['pos'] >= $thatBinPos['pos'] );
1528
		}
1529
1530
		// Comparing totally different binlogs does not make sense
1531
		return false;
1532
	}
1533
1534
	function channelsMatch( DBMasterPos $pos ) {
1535
		if ( !( $pos instanceof self ) ) {
1536
			throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
1537
		}
1538
1539
		// Prefer GTID comparisons, which work with multi-tier replication
1540
		$thisPosDomains = array_keys( $this->getGtidCoordinates() );
1541
		$thatPosDomains = array_keys( $pos->getGtidCoordinates() );
1542
		if ( $thisPosDomains && $thatPosDomains ) {
1543
			// Check that this has GTIDs for all domains in $pos
1544
			return !array_diff( $thatPosDomains, $thisPosDomains );
1545
		}
1546
1547
		// Fallback to the binlog file comparisons
1548
		$thisBinPos = $this->getBinlogCoordinates();
1549
		$thatBinPos = $pos->getBinlogCoordinates();
1550
1551
		return ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] );
1552
	}
1553
1554
	/**
1555
	 * @note: this returns false for multi-source replication GTID sets
1556
	 * @see https://mariadb.com/kb/en/mariadb/gtid
1557
	 * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
1558
	 * @return array Map of (domain => integer position) or false
1559
	 */
1560
	protected function getGtidCoordinates() {
1561
		$gtidInfos = [];
1562
		foreach ( $this->gtids as $gtid ) {
1563
			$m = [];
1564
			// MariaDB style: <domain>-<server id>-<sequence number>
1565
			if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) {
1566
				$gtidInfos[(int)$m[1]] = (int)$m[2];
1567
			// MySQL style: <UUID domain>:<sequence number>
1568
			} elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) {
1569
				$gtidInfos[$m[1]] = (int)$m[2];
1570
			} else {
1571
				$gtidInfos = [];
1572
				break; // unrecognized GTID
1573
			}
1574
1575
		}
1576
1577
		return $gtidInfos;
1578
	}
1579
1580
	/**
1581
	 * @see http://dev.mysql.com/doc/refman/5.7/en/show-master-status.html
1582
	 * @see http://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html
1583
	 * @return array|bool (binlog, (integer file number, integer position)) or false
1584
	 */
1585
	protected function getBinlogCoordinates() {
1586
		$m = [];
1587
		if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', (string)$this, $m ) ) {
1588
			return [ 'binlog' => $m[1], 'pos' => [ (int)$m[2], (int)$m[3] ] ];
1589
		}
1590
1591
		return false;
1592
	}
1593
}
1594