Completed
Branch master (4de667)
by
unknown
26:16
created

DatabaseMysqlBase::serverIsReadOnly()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 6
rs 9.4285
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 $lastKnownSlavePos;
35
	/** @var string Method to detect slave lag */
36
	protected $lagDetectionMethod;
37
	/** @var array Method to detect slave lag */
38
	protected $lagDetectionOptions = [];
39
	/** @var bool bool Whether to use GTID methods */
40
	protected $useGTIDs = false;
41
42
	/** @var string|null */
43
	private $serverVersion = null;
44
45
	/**
46
	 * Additional $params include:
47
	 *   - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat).
48
	 *       pt-heartbeat assumes the table is at heartbeat.heartbeat
49
	 *       and uses UTC timestamps in the heartbeat.ts column.
50
	 *       (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html)
51
	 *   - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map to change
52
	 *       the default behavior. Normally, the heartbeat row with the server
53
	 *       ID of this server's master will be used. Set the "conds" field to
54
	 *       override the query conditions, e.g. ['shard' => 's1'].
55
	 *   - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible.
56
	 * @param array $params
57
	 */
58
	function __construct( array $params ) {
59
		parent::__construct( $params );
60
61
		$this->lagDetectionMethod = isset( $params['lagDetectionMethod'] )
62
			? $params['lagDetectionMethod']
63
			: 'Seconds_Behind_Master';
64
		$this->lagDetectionOptions = isset( $params['lagDetectionOptions'] )
65
			? $params['lagDetectionOptions']
66
			: [];
67
		$this->useGTIDs = !empty( $params['useGTIDs' ] );
68
	}
69
70
	/**
71
	 * @return string
72
	 */
73
	function getType() {
74
		return 'mysql';
75
	}
76
77
	/**
78
	 * @param string $server
79
	 * @param string $user
80
	 * @param string $password
81
	 * @param string $dbName
82
	 * @throws Exception|DBConnectionError
83
	 * @return bool
84
	 */
85
	function open( $server, $user, $password, $dbName ) {
86
		global $wgAllDBsAreLocalhost, $wgSQLMode;
87
88
		# Close/unset connection handle
89
		$this->close();
90
91
		# Debugging hack -- fake cluster
92
		$realServer = $wgAllDBsAreLocalhost ? 'localhost' : $server;
93
		$this->mServer = $server;
94
		$this->mUser = $user;
95
		$this->mPassword = $password;
96
		$this->mDBname = $dbName;
97
98
		$this->installErrorHandler();
99
		try {
100
			$this->mConn = $this->mysqlConnect( $realServer );
101
		} catch ( Exception $ex ) {
102
			$this->restoreErrorHandler();
103
			throw $ex;
104
		}
105
		$error = $this->restoreErrorHandler();
106
107
		# Always log connection errors
108
		if ( !$this->mConn ) {
109
			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...
110
				$error = $this->lastError();
111
			}
112
			wfLogDBError(
113
				"Error connecting to {db_server}: {error}",
114
				$this->getLogContext( [
115
					'method' => __METHOD__,
116
					'error' => $error,
117
				] )
118
			);
119
			wfDebug( "DB connection error\n" .
120
				"Server: $server, User: $user, Password: " .
121
				substr( $password, 0, 3 ) . "..., error: " . $error . "\n" );
122
123
			$this->reportConnectionError( $error );
124
		}
125
126
		if ( $dbName != '' ) {
127
			MediaWiki\suppressWarnings();
128
			$success = $this->selectDB( $dbName );
129
			MediaWiki\restoreWarnings();
130
			if ( !$success ) {
131
				wfLogDBError(
132
					"Error selecting database {db_name} on server {db_server}",
133
					$this->getLogContext( [
134
						'method' => __METHOD__,
135
					] )
136
				);
137
				wfDebug( "Error selecting database $dbName on server {$this->mServer} " .
138
					"from client host " . wfHostname() . "\n" );
139
140
				$this->reportConnectionError( "Error selecting database $dbName" );
141
			}
142
		}
143
144
		// Tell the server what we're communicating with
145
		if ( !$this->connectInitCharset() ) {
146
			$this->reportConnectionError( "Error setting character set" );
147
		}
148
149
		// Abstract over any insane MySQL defaults
150
		$set = [ 'group_concat_max_len = 262144' ];
151
		// Set SQL mode, default is turning them all off, can be overridden or skipped with null
152
		if ( is_string( $wgSQLMode ) ) {
153
			$set[] = 'sql_mode = ' . $this->addQuotes( $wgSQLMode );
154
		}
155
		// Set any custom settings defined by site config
156
		// (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html)
157
		foreach ( $this->mSessionVars as $var => $val ) {
158
			// Escape strings but not numbers to avoid MySQL complaining
159
			if ( !is_int( $val ) && !is_float( $val ) ) {
160
				$val = $this->addQuotes( $val );
161
			}
162
			$set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val;
163
		}
164
165
		if ( $set ) {
166
			// Use doQuery() to avoid opening implicit transactions (DBO_TRX)
167
			$success = $this->doQuery( 'SET ' . implode( ', ', $set ) );
168
			if ( !$success ) {
169
				wfLogDBError(
170
					'Error setting MySQL variables on server {db_server} (check $wgSQLMode)',
171
					$this->getLogContext( [
172
						'method' => __METHOD__,
173
					] )
174
				);
175
				$this->reportConnectionError(
176
					'Error setting MySQL variables on server {db_server} (check $wgSQLMode)' );
177
			}
178
		}
179
180
		$this->mOpened = true;
181
182
		return true;
183
	}
184
185
	/**
186
	 * Set the character set information right after connection
187
	 * @return bool
188
	 */
189
	protected function connectInitCharset() {
190
		global $wgDBmysql5;
191
192
		if ( $wgDBmysql5 ) {
193
			// Tell the server we're communicating with it in UTF-8.
194
			// This may engage various charset conversions.
195
			return $this->mysqlSetCharset( 'utf8' );
196
		} else {
197
			return $this->mysqlSetCharset( 'binary' );
198
		}
199
	}
200
201
	/**
202
	 * Open a connection to a MySQL server
203
	 *
204
	 * @param string $realServer
205
	 * @return mixed Raw connection
206
	 * @throws DBConnectionError
207
	 */
208
	abstract protected function mysqlConnect( $realServer );
209
210
	/**
211
	 * Set the character set of the MySQL link
212
	 *
213
	 * @param string $charset
214
	 * @return bool
215
	 */
216
	abstract protected function mysqlSetCharset( $charset );
217
218
	/**
219
	 * @param ResultWrapper|resource $res
220
	 * @throws DBUnexpectedError
221
	 */
222 View Code Duplication
	function freeResult( $res ) {
223
		if ( $res instanceof ResultWrapper ) {
224
			$res = $res->result;
225
		}
226
		MediaWiki\suppressWarnings();
227
		$ok = $this->mysqlFreeResult( $res );
228
		MediaWiki\restoreWarnings();
229
		if ( !$ok ) {
230
			throw new DBUnexpectedError( $this, "Unable to free MySQL result" );
231
		}
232
	}
233
234
	/**
235
	 * Free result memory
236
	 *
237
	 * @param resource $res Raw result
238
	 * @return bool
239
	 */
240
	abstract protected function mysqlFreeResult( $res );
241
242
	/**
243
	 * @param ResultWrapper|resource $res
244
	 * @return stdClass|bool
245
	 * @throws DBUnexpectedError
246
	 */
247 View Code Duplication
	function fetchObject( $res ) {
248
		if ( $res instanceof ResultWrapper ) {
249
			$res = $res->result;
250
		}
251
		MediaWiki\suppressWarnings();
252
		$row = $this->mysqlFetchObject( $res );
253
		MediaWiki\restoreWarnings();
254
255
		$errno = $this->lastErrno();
256
		// Unfortunately, mysql_fetch_object does not reset the last errno.
257
		// Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
258
		// these are the only errors mysql_fetch_object can cause.
259
		// See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
260
		if ( $errno == 2000 || $errno == 2013 ) {
261
			throw new DBUnexpectedError(
262
				$this,
263
				'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() )
264
			);
265
		}
266
267
		return $row;
268
	}
269
270
	/**
271
	 * Fetch a result row as an object
272
	 *
273
	 * @param resource $res Raw result
274
	 * @return stdClass
275
	 */
276
	abstract protected function mysqlFetchObject( $res );
277
278
	/**
279
	 * @param ResultWrapper|resource $res
280
	 * @return array|bool
281
	 * @throws DBUnexpectedError
282
	 */
283 View Code Duplication
	function fetchRow( $res ) {
284
		if ( $res instanceof ResultWrapper ) {
285
			$res = $res->result;
286
		}
287
		MediaWiki\suppressWarnings();
288
		$row = $this->mysqlFetchArray( $res );
289
		MediaWiki\restoreWarnings();
290
291
		$errno = $this->lastErrno();
292
		// Unfortunately, mysql_fetch_array does not reset the last errno.
293
		// Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as
294
		// these are the only errors mysql_fetch_array can cause.
295
		// See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
296
		if ( $errno == 2000 || $errno == 2013 ) {
297
			throw new DBUnexpectedError(
298
				$this,
299
				'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() )
300
			);
301
		}
302
303
		return $row;
304
	}
305
306
	/**
307
	 * Fetch a result row as an associative and numeric array
308
	 *
309
	 * @param resource $res Raw result
310
	 * @return array
311
	 */
312
	abstract protected function mysqlFetchArray( $res );
313
314
	/**
315
	 * @throws DBUnexpectedError
316
	 * @param ResultWrapper|resource $res
317
	 * @return int
318
	 */
319
	function numRows( $res ) {
320
		if ( $res instanceof ResultWrapper ) {
321
			$res = $res->result;
322
		}
323
		MediaWiki\suppressWarnings();
324
		$n = $this->mysqlNumRows( $res );
325
		MediaWiki\restoreWarnings();
326
327
		// Unfortunately, mysql_num_rows does not reset the last errno.
328
		// We are not checking for any errors here, since
329
		// these are no errors mysql_num_rows can cause.
330
		// See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html.
331
		// See https://phabricator.wikimedia.org/T44430
332
		return $n;
333
	}
334
335
	/**
336
	 * Get number of rows in result
337
	 *
338
	 * @param resource $res Raw result
339
	 * @return int
340
	 */
341
	abstract protected function mysqlNumRows( $res );
342
343
	/**
344
	 * @param ResultWrapper|resource $res
345
	 * @return int
346
	 */
347
	function numFields( $res ) {
348
		if ( $res instanceof ResultWrapper ) {
349
			$res = $res->result;
350
		}
351
352
		return $this->mysqlNumFields( $res );
353
	}
354
355
	/**
356
	 * Get number of fields in result
357
	 *
358
	 * @param resource $res Raw result
359
	 * @return int
360
	 */
361
	abstract protected function mysqlNumFields( $res );
362
363
	/**
364
	 * @param ResultWrapper|resource $res
365
	 * @param int $n
366
	 * @return string
367
	 */
368
	function fieldName( $res, $n ) {
369
		if ( $res instanceof ResultWrapper ) {
370
			$res = $res->result;
371
		}
372
373
		return $this->mysqlFieldName( $res, $n );
374
	}
375
376
	/**
377
	 * Get the name of the specified field in a result
378
	 *
379
	 * @param ResultWrapper|resource $res
380
	 * @param int $n
381
	 * @return string
382
	 */
383
	abstract protected function mysqlFieldName( $res, $n );
384
385
	/**
386
	 * mysql_field_type() wrapper
387
	 * @param ResultWrapper|resource $res
388
	 * @param int $n
389
	 * @return string
390
	 */
391
	public function fieldType( $res, $n ) {
392
		if ( $res instanceof ResultWrapper ) {
393
			$res = $res->result;
394
		}
395
396
		return $this->mysqlFieldType( $res, $n );
397
	}
398
399
	/**
400
	 * Get the type of the specified field in a result
401
	 *
402
	 * @param ResultWrapper|resource $res
403
	 * @param int $n
404
	 * @return string
405
	 */
406
	abstract protected function mysqlFieldType( $res, $n );
407
408
	/**
409
	 * @param ResultWrapper|resource $res
410
	 * @param int $row
411
	 * @return bool
412
	 */
413
	function dataSeek( $res, $row ) {
414
		if ( $res instanceof ResultWrapper ) {
415
			$res = $res->result;
416
		}
417
418
		return $this->mysqlDataSeek( $res, $row );
419
	}
420
421
	/**
422
	 * Move internal result pointer
423
	 *
424
	 * @param ResultWrapper|resource $res
425
	 * @param int $row
426
	 * @return bool
427
	 */
428
	abstract protected function mysqlDataSeek( $res, $row );
429
430
	/**
431
	 * @return string
432
	 */
433
	function lastError() {
434
		if ( $this->mConn ) {
435
			# Even if it's non-zero, it can still be invalid
436
			MediaWiki\suppressWarnings();
437
			$error = $this->mysqlError( $this->mConn );
438
			if ( !$error ) {
439
				$error = $this->mysqlError();
440
			}
441
			MediaWiki\restoreWarnings();
442
		} else {
443
			$error = $this->mysqlError();
444
		}
445
		if ( $error ) {
446
			$error .= ' (' . $this->mServer . ')';
447
		}
448
449
		return $error;
450
	}
451
452
	/**
453
	 * Returns the text of the error message from previous MySQL operation
454
	 *
455
	 * @param resource $conn Raw connection
456
	 * @return string
457
	 */
458
	abstract protected function mysqlError( $conn = null );
459
460
	/**
461
	 * @param string $table
462
	 * @param array $uniqueIndexes
463
	 * @param array $rows
464
	 * @param string $fname
465
	 * @return ResultWrapper
466
	 */
467
	function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
468
		return $this->nativeReplace( $table, $rows, $fname );
469
	}
470
471
	/**
472
	 * Estimate rows in dataset
473
	 * Returns estimated count, based on EXPLAIN output
474
	 * Takes same arguments as Database::select()
475
	 *
476
	 * @param string|array $table
477
	 * @param string|array $vars
478
	 * @param string|array $conds
479
	 * @param string $fname
480
	 * @param string|array $options
481
	 * @return bool|int
482
	 */
483
	public function estimateRowCount( $table, $vars = '*', $conds = '',
484
		$fname = __METHOD__, $options = []
485
	) {
486
		$options['EXPLAIN'] = true;
487
		$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 483 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 484 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...
488
		if ( $res === false ) {
489
			return false;
490
		}
491
		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 487 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...
492
			return 0;
493
		}
494
495
		$rows = 1;
496
		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...
497
			$rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
498
		}
499
500
		return (int)$rows;
501
	}
502
503
	/**
504
	 * @param string $table
505
	 * @param string $field
506
	 * @return bool|MySQLField
507
	 */
508
	function fieldInfo( $table, $field ) {
509
		$table = $this->tableName( $table );
510
		$res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true );
511
		if ( !$res ) {
512
			return false;
513
		}
514
		$n = $this->mysqlNumFields( $res->result );
515
		for ( $i = 0; $i < $n; $i++ ) {
516
			$meta = $this->mysqlFetchField( $res->result, $i );
517
			if ( $field == $meta->name ) {
518
				return new MySQLField( $meta );
519
			}
520
		}
521
522
		return false;
523
	}
524
525
	/**
526
	 * Get column information from a result
527
	 *
528
	 * @param resource $res Raw result
529
	 * @param int $n
530
	 * @return stdClass
531
	 */
532
	abstract protected function mysqlFetchField( $res, $n );
533
534
	/**
535
	 * Get information about an index into an object
536
	 * Returns false if the index does not exist
537
	 *
538
	 * @param string $table
539
	 * @param string $index
540
	 * @param string $fname
541
	 * @return bool|array|null False or null on failure
542
	 */
543
	function indexInfo( $table, $index, $fname = __METHOD__ ) {
544
		# SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not.
545
		# SHOW INDEX should work for 3.x and up:
546
		# http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
547
		$table = $this->tableName( $table );
548
		$index = $this->indexName( $index );
549
550
		$sql = 'SHOW INDEX FROM ' . $table;
551
		$res = $this->query( $sql, $fname );
552
553
		if ( !$res ) {
554
			return null;
555
		}
556
557
		$result = [];
558
559
		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...
560
			if ( $row->Key_name == $index ) {
561
				$result[] = $row;
562
			}
563
		}
564
565
		return empty( $result ) ? false : $result;
566
	}
567
568
	/**
569
	 * @param string $s
570
	 * @return string
571
	 */
572
	function strencode( $s ) {
573
		$sQuoted = $this->mysqlRealEscapeString( $s );
574
575
		if ( $sQuoted === false ) {
576
			$this->ping();
577
			$sQuoted = $this->mysqlRealEscapeString( $s );
578
		}
579
580
		return $sQuoted;
581
	}
582
583
	/**
584
	 * @param string $s
585
	 * @return mixed
586
	 */
587
	abstract protected function mysqlRealEscapeString( $s );
588
589
	/**
590
	 * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes".
591
	 *
592
	 * @param string $s
593
	 * @return string
594
	 */
595
	public function addIdentifierQuotes( $s ) {
596
		// Characters in the range \u0001-\uFFFF are valid in a quoted identifier
597
		// Remove NUL bytes and escape backticks by doubling
598
		return '`' . str_replace( [ "\0", '`' ], [ '', '``' ], $s ) . '`';
599
	}
600
601
	/**
602
	 * @param string $name
603
	 * @return bool
604
	 */
605
	public function isQuotedIdentifier( $name ) {
606
		return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`';
607
	}
608
609
	/**
610
	 * @return bool
611
	 */
612
	function ping() {
613
		$ping = $this->mysqlPing();
614
		if ( $ping ) {
615
			// Connection was good or lost but reconnected...
616
			// @note: mysqlnd (php 5.6+) does not support this (PHP bug 52561)
617
			return true;
618
		}
619
620
		// Try a full disconnect/reconnect cycle if ping() failed
621
		$this->closeConnection();
622
		$this->mOpened = false;
623
		$this->mConn = false;
0 ignored issues
show
Documentation Bug introduced by
It seems like false of type false is incompatible with the declared type resource of property $mConn.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
624
		$this->open( $this->mServer, $this->mUser, $this->mPassword, $this->mDBname );
625
626
		return true;
627
	}
628
629
	/**
630
	 * Ping a server connection or reconnect if there is no connection
631
	 *
632
	 * @return bool
633
	 */
634
	abstract protected function mysqlPing();
635
636
	function getLag() {
637
		if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
638
			return $this->getLagFromPtHeartbeat();
639
		} else {
640
			return $this->getLagFromSlaveStatus();
641
		}
642
	}
643
644
	/**
645
	 * @return string
646
	 */
647
	protected function getLagDetectionMethod() {
648
		return $this->lagDetectionMethod;
649
	}
650
651
	/**
652
	 * @return bool|int
653
	 */
654
	protected function getLagFromSlaveStatus() {
655
		$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
656
		$row = $res ? $res->fetchObject() : false;
657
		if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) {
658
			return intval( $row->Seconds_Behind_Master );
659
		}
660
661
		return false;
662
	}
663
664
	/**
665
	 * @return bool|float
666
	 */
667
	protected function getLagFromPtHeartbeat() {
668
		$options = $this->lagDetectionOptions;
669
670
		if ( isset( $options['conds'] ) ) {
671
			// Best method for multi-DC setups: use logical channel names
672
			$data = $this->getHeartbeatData( $options['conds'] );
673
		} else {
674
			// Standard method: use master server ID (works with stock pt-heartbeat)
675
			$masterInfo = $this->getMasterServerInfo();
676
			if ( !$masterInfo ) {
677
				wfLogDBError(
678
					"Unable to query master of {db_server} for server ID",
679
					$this->getLogContext( [
680
						'method' => __METHOD__
681
					] )
682
				);
683
684
				return false; // could not get master server ID
685
			}
686
687
			$conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ];
688
			$data = $this->getHeartbeatData( $conds );
689
		}
690
691
		list( $time, $nowUnix ) = $data;
692
		if ( $time !== null ) {
693
			// @time is in ISO format like "2015-09-25T16:48:10.000510"
694
			$dateTime = new DateTime( $time, new DateTimeZone( 'UTC' ) );
695
			$timeUnix = (int)$dateTime->format( 'U' ) + $dateTime->format( 'u' ) / 1e6;
696
697
			return max( $nowUnix - $timeUnix, 0.0 );
698
		}
699
700
		wfLogDBError(
701
			"Unable to find pt-heartbeat row for {db_server}",
702
			$this->getLogContext( [
703
				'method' => __METHOD__
704
			] )
705
		);
706
707
		return false;
708
	}
709
710
	protected function getMasterServerInfo() {
711
		$cache = $this->srvCache;
712
		$key = $cache->makeGlobalKey(
713
			'mysql',
714
			'master-info',
715
			// Using one key for all cluster slaves is preferable
716
			$this->getLBInfo( 'clusterMasterHost' ) ?: $this->getServer()
717
		);
718
719
		return $cache->getWithSetCallback(
720
			$key,
721
			$cache::TTL_INDEFINITE,
722
			function () use ( $cache, $key ) {
723
				// Get and leave a lock key in place for a short period
724
				if ( !$cache->lock( $key, 0, 10 ) ) {
725
					return false; // avoid master connection spike slams
726
				}
727
728
				$conn = $this->getLazyMasterHandle();
729
				if ( !$conn ) {
730
					return false; // something is misconfigured
731
				}
732
733
				// Connect to and query the master; catch errors to avoid outages
734
				try {
735
					$res = $conn->query( 'SELECT @@server_id AS id', __METHOD__ );
736
					$row = $res ? $res->fetchObject() : false;
737
					$id = $row ? (int)$row->id : 0;
738
				} catch ( DBError $e ) {
739
					$id = 0;
740
				}
741
742
				// Cache the ID if it was retrieved
743
				return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false;
744
			}
745
		);
746
	}
747
748
	/**
749
	 * @param array $conds WHERE clause conditions to find a row
750
	 * @return array (heartbeat `ts` column value or null, UNIX timestamp) for the newest beat
751
	 * @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html
752
	 */
753
	protected function getHeartbeatData( array $conds ) {
754
		$whereSQL = $this->makeList( $conds, LIST_AND );
755
		// Use ORDER BY for channel based queries since that field might not be UNIQUE.
756
		// Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the
757
		// percision field is not supported in MySQL <= 5.5.
758
		$res = $this->query(
759
			"SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1"
760
		);
761
		$row = $res ? $res->fetchObject() : false;
762
763
		return [ $row ? $row->ts : null, microtime( true ) ];
764
	}
765
766
	public function getApproximateLagStatus() {
767
		if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) {
768
			// Disable caching since this is fast enough and we don't wan't
769
			// to be *too* pessimistic by having both the cache TTL and the
770
			// pt-heartbeat interval count as lag in getSessionLagStatus()
771
			return parent::getApproximateLagStatus();
772
		}
773
774
		$key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServer() );
775
		$approxLag = $this->srvCache->get( $key );
776
		if ( !$approxLag ) {
777
			$approxLag = parent::getApproximateLagStatus();
778
			$this->srvCache->set( $key, $approxLag, 1 );
779
		}
780
781
		return $approxLag;
782
	}
783
784
	function masterPosWait( DBMasterPos $pos, $timeout ) {
785
		if ( !( $pos instanceof MySQLMasterPos ) ) {
786
			throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" );
787
		}
788
789
		if ( $this->getLBInfo( 'is static' ) === true ) {
790
			return 0; // this is a copy of a read-only dataset with no master DB
791
		} elseif ( $this->lastKnownSlavePos && $this->lastKnownSlavePos->hasReached( $pos ) ) {
792
			return 0; // already reached this point for sure
793
		}
794
795
		// Commit any open transactions
796
		$this->commit( __METHOD__, 'flush' );
797
798
		// Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set
799
		if ( $this->useGTIDs && $pos->gtids ) {
800
			// Wait on the GTID set (MariaDB only)
801
			$gtidArg = implode( ',', $pos->gtids );
802
			$res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" );
803
		} else {
804
			// Wait on the binlog coordinates
805
			$encFile = $this->addQuotes( $pos->file );
806
			$encPos = intval( $pos->pos );
807
			$res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" );
808
		}
809
810
		$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...
811
		if ( !$row ) {
812
			throw new DBExpectedError( $this, "Failed to query MASTER_POS_WAIT()" );
813
		}
814
815
		// Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual
816
		$status = ( $row[0] !== null ) ? intval( $row[0] ) : null;
817
		if ( $status === null ) {
818
			// T126436: jobs programmed to wait on master positions might be referencing binlogs
819
			// with an old master hostname. Such calls make MASTER_POS_WAIT() return null. Try
820
			// to detect this and treat the slave as having reached the position; a proper master
821
			// switchover already requires that the new master be caught up before the switch.
822
			$slavePos = $this->getSlavePos();
823
			if ( $slavePos && !$slavePos->channelsMatch( $pos ) ) {
824
				$this->lastKnownSlavePos = $slavePos;
825
				$status = 0;
826
			}
827
		} elseif ( $status >= 0 ) {
828
			// Remember that this position was reached to save queries next time
829
			$this->lastKnownSlavePos = $pos;
830
		}
831
832
		return $status;
833
	}
834
835
	/**
836
	 * Get the position of the master from SHOW SLAVE STATUS
837
	 *
838
	 * @return MySQLMasterPos|bool
839
	 */
840
	function getSlavePos() {
841
		$res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ );
842
		$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 841 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...
843
844
		if ( $row ) {
845
			$pos = isset( $row->Exec_master_log_pos )
846
				? $row->Exec_master_log_pos
847
				: $row->Exec_Master_Log_Pos;
848
			// Also fetch the last-applied GTID set (MariaDB)
849 View Code Duplication
			if ( $this->useGTIDs ) {
850
				$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_slave_pos'", __METHOD__ );
851
				$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 850 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...
852
				$gtidSet = $gtidRow ? $gtidRow->Value : '';
853
			} else {
854
				$gtidSet = '';
855
			}
856
857
			return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos, $gtidSet );
858
		} else {
859
			return false;
860
		}
861
	}
862
863
	/**
864
	 * Get the position of the master from SHOW MASTER STATUS
865
	 *
866
	 * @return MySQLMasterPos|bool
867
	 */
868
	function getMasterPos() {
869
		$res = $this->query( 'SHOW MASTER STATUS', __METHOD__ );
870
		$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 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
		if ( $row ) {
873
			// Also fetch the last-written GTID set (MariaDB)
874 View Code Duplication
			if ( $this->useGTIDs ) {
875
				$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_binlog_pos'", __METHOD__ );
876
				$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 875 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...
877
				$gtidSet = $gtidRow ? $gtidRow->Value : '';
878
			} else {
879
				$gtidSet = '';
880
			}
881
882
			return new MySQLMasterPos( $row->File, $row->Position, $gtidSet );
883
		} else {
884
			return false;
885
		}
886
	}
887
888
	public function serverIsReadOnly() {
889
		$res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__ );
890
		$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 889 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...
891
892
		return $row ? ( strtolower( $row->Value ) === 'on' ) : false;
893
	}
894
895
	/**
896
	 * @param string $index
897
	 * @return string
898
	 */
899
	function useIndexClause( $index ) {
900
		return "FORCE INDEX (" . $this->indexName( $index ) . ")";
901
	}
902
903
	/**
904
	 * @return string
905
	 */
906
	function lowPriorityOption() {
907
		return 'LOW_PRIORITY';
908
	}
909
910
	/**
911
	 * @return string
912
	 */
913
	public function getSoftwareLink() {
914
		// MariaDB includes its name in its version string; this is how MariaDB's version of
915
		// the mysql command-line client identifies MariaDB servers (see mariadb_connection()
916
		// in libmysql/libmysql.c).
917
		$version = $this->getServerVersion();
918
		if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) {
919
			return '[{{int:version-db-mariadb-url}} MariaDB]';
920
		}
921
922
		// Percona Server's version suffix is not very distinctive, and @@version_comment
923
		// doesn't give the necessary info for source builds, so assume the server is MySQL.
924
		// (Even Percona's version of mysql doesn't try to make the distinction.)
925
		return '[{{int:version-db-mysql-url}} MySQL]';
926
	}
927
928
	/**
929
	 * @return string
930
	 */
931
	public function getServerVersion() {
932
		// Not using mysql_get_server_info() or similar for consistency: in the handshake,
933
		// MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
934
		// it off (see RPL_VERSION_HACK in include/mysql_com.h).
935
		if ( $this->serverVersion === null ) {
936
			$this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ );
937
		}
938
		return $this->serverVersion;
939
	}
940
941
	/**
942
	 * @param array $options
943
	 */
944
	public function setSessionOptions( array $options ) {
945
		if ( isset( $options['connTimeout'] ) ) {
946
			$timeout = (int)$options['connTimeout'];
947
			$this->query( "SET net_read_timeout=$timeout" );
948
			$this->query( "SET net_write_timeout=$timeout" );
949
		}
950
	}
951
952
	/**
953
	 * @param string $sql
954
	 * @param string $newLine
955
	 * @return bool
956
	 */
957
	public function streamStatementEnd( &$sql, &$newLine ) {
958
		if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) {
959
			preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m );
960
			$this->delimiter = $m[1];
961
			$newLine = '';
962
		}
963
964
		return parent::streamStatementEnd( $sql, $newLine );
965
	}
966
967
	/**
968
	 * Check to see if a named lock is available. This is non-blocking.
969
	 *
970
	 * @param string $lockName Name of lock to poll
971
	 * @param string $method Name of method calling us
972
	 * @return bool
973
	 * @since 1.20
974
	 */
975 View Code Duplication
	public function lockIsFree( $lockName, $method ) {
976
		$lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
977
		$result = $this->query( "SELECT IS_FREE_LOCK($lockName) AS lockstatus", $method );
978
		$row = $this->fetchObject( $result );
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query("SELECT IS_...S lockstatus", $method) on line 977 can also be of type boolean; however, DatabaseMysqlBase::fetchObject() does only seem to accept object<ResultWrapper>|resource, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
979
980
		return ( $row->lockstatus == 1 );
981
	}
982
983
	/**
984
	 * @param string $lockName
985
	 * @param string $method
986
	 * @param int $timeout
987
	 * @return bool
988
	 */
989 View Code Duplication
	public function lock( $lockName, $method, $timeout = 5 ) {
990
		$lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
991
		$result = $this->query( "SELECT GET_LOCK($lockName, $timeout) AS lockstatus", $method );
992
		$row = $this->fetchObject( $result );
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query("SELECT GET...S lockstatus", $method) on line 991 can also be of type boolean; however, DatabaseMysqlBase::fetchObject() does only seem to accept object<ResultWrapper>|resource, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
993
994
		if ( $row->lockstatus == 1 ) {
995
			parent::lock( $lockName, $method, $timeout ); // record
996
			return true;
997
		}
998
999
		wfDebug( __METHOD__ . " failed to acquire lock\n" );
1000
1001
		return false;
1002
	}
1003
1004
	/**
1005
	 * FROM MYSQL DOCS:
1006
	 * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock
1007
	 * @param string $lockName
1008
	 * @param string $method
1009
	 * @return bool
1010
	 */
1011 View Code Duplication
	public function unlock( $lockName, $method ) {
1012
		$lockName = $this->addQuotes( $this->makeLockName( $lockName ) );
1013
		$result = $this->query( "SELECT RELEASE_LOCK($lockName) as lockstatus", $method );
1014
		$row = $this->fetchObject( $result );
0 ignored issues
show
Bug introduced by
It seems like $result defined by $this->query("SELECT REL...s lockstatus", $method) on line 1013 can also be of type boolean; however, DatabaseMysqlBase::fetchObject() does only seem to accept object<ResultWrapper>|resource, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1015
1016
		if ( $row->lockstatus == 1 ) {
1017
			parent::unlock( $lockName, $method ); // record
1018
			return true;
1019
		}
1020
1021
		wfDebug( __METHOD__ . " failed to release lock\n" );
1022
1023
		return false;
1024
	}
1025
1026
	private function makeLockName( $lockName ) {
1027
		// http://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock
1028
		// Newer version enforce a 64 char length limit.
1029
		return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName;
1030
	}
1031
1032
	public function namedLocksEnqueue() {
1033
		return true;
1034
	}
1035
1036
	/**
1037
	 * @param array $read
1038
	 * @param array $write
1039
	 * @param string $method
1040
	 * @param bool $lowPriority
1041
	 * @return bool
1042
	 */
1043
	public function lockTables( $read, $write, $method, $lowPriority = true ) {
1044
		$items = [];
1045
1046
		foreach ( $write as $table ) {
1047
			$tbl = $this->tableName( $table ) .
1048
				( $lowPriority ? ' LOW_PRIORITY' : '' ) .
1049
				' WRITE';
1050
			$items[] = $tbl;
1051
		}
1052
		foreach ( $read as $table ) {
1053
			$items[] = $this->tableName( $table ) . ' READ';
1054
		}
1055
		$sql = "LOCK TABLES " . implode( ',', $items );
1056
		$this->query( $sql, $method );
1057
1058
		return true;
1059
	}
1060
1061
	/**
1062
	 * @param string $method
1063
	 * @return bool
1064
	 */
1065
	public function unlockTables( $method ) {
1066
		$this->query( "UNLOCK TABLES", $method );
1067
1068
		return true;
1069
	}
1070
1071
	/**
1072
	 * Get search engine class. All subclasses of this
1073
	 * need to implement this if they wish to use searching.
1074
	 *
1075
	 * @return string
1076
	 */
1077
	public function getSearchEngine() {
1078
		return 'SearchMySQL';
1079
	}
1080
1081
	/**
1082
	 * @param bool $value
1083
	 */
1084
	public function setBigSelects( $value = true ) {
1085
		if ( $value === 'default' ) {
1086
			if ( $this->mDefaultBigSelects === null ) {
1087
				# Function hasn't been called before so it must already be set to the default
1088
				return;
1089
			} else {
1090
				$value = $this->mDefaultBigSelects;
1091
			}
1092
		} elseif ( $this->mDefaultBigSelects === null ) {
1093
			$this->mDefaultBigSelects =
1094
				(bool)$this->selectField( false, '@@sql_big_selects', '', __METHOD__ );
1095
		}
1096
		$encValue = $value ? '1' : '0';
1097
		$this->query( "SET sql_big_selects=$encValue", __METHOD__ );
1098
	}
1099
1100
	/**
1101
	 * DELETE where the condition is a join. MySql uses multi-table deletes.
1102
	 * @param string $delTable
1103
	 * @param string $joinTable
1104
	 * @param string $delVar
1105
	 * @param string $joinVar
1106
	 * @param array|string $conds
1107
	 * @param bool|string $fname
1108
	 * @throws DBUnexpectedError
1109
	 * @return bool|ResultWrapper
1110
	 */
1111 View Code Duplication
	function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds, $fname = __METHOD__ ) {
1112
		if ( !$conds ) {
1113
			throw new DBUnexpectedError( $this, 'DatabaseBase::deleteJoin() called with empty $conds' );
1114
		}
1115
1116
		$delTable = $this->tableName( $delTable );
1117
		$joinTable = $this->tableName( $joinTable );
1118
		$sql = "DELETE $delTable FROM $delTable, $joinTable WHERE $delVar=$joinVar ";
1119
1120
		if ( $conds != '*' ) {
1121
			$sql .= ' AND ' . $this->makeList( $conds, LIST_AND );
0 ignored issues
show
Bug introduced by
It seems like $conds defined by parameter $conds on line 1111 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...
1122
		}
1123
1124
		return $this->query( $sql, $fname );
0 ignored issues
show
Bug introduced by
It seems like $fname defined by parameter $fname on line 1111 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...
1125
	}
1126
1127
	/**
1128
	 * @param string $table
1129
	 * @param array $rows
1130
	 * @param array $uniqueIndexes
1131
	 * @param array $set
1132
	 * @param string $fname
1133
	 * @return bool
1134
	 */
1135
	public function upsert( $table, array $rows, array $uniqueIndexes,
1136
		array $set, $fname = __METHOD__
1137
	) {
1138
		if ( !count( $rows ) ) {
1139
			return true; // nothing to do
1140
		}
1141
1142
		if ( !is_array( reset( $rows ) ) ) {
1143
			$rows = [ $rows ];
1144
		}
1145
1146
		$table = $this->tableName( $table );
1147
		$columns = array_keys( $rows[0] );
1148
1149
		$sql = "INSERT INTO $table (" . implode( ',', $columns ) . ') VALUES ';
1150
		$rowTuples = [];
1151
		foreach ( $rows as $row ) {
1152
			$rowTuples[] = '(' . $this->makeList( $row ) . ')';
1153
		}
1154
		$sql .= implode( ',', $rowTuples );
1155
		$sql .= " ON DUPLICATE KEY UPDATE " . $this->makeList( $set, LIST_SET );
1156
1157
		return (bool)$this->query( $sql, $fname );
1158
	}
1159
1160
	/**
1161
	 * Determines how long the server has been up
1162
	 *
1163
	 * @return int
1164
	 */
1165
	function getServerUptime() {
1166
		$vars = $this->getMysqlStatus( 'Uptime' );
1167
1168
		return (int)$vars['Uptime'];
1169
	}
1170
1171
	/**
1172
	 * Determines if the last failure was due to a deadlock
1173
	 *
1174
	 * @return bool
1175
	 */
1176
	function wasDeadlock() {
1177
		return $this->lastErrno() == 1213;
1178
	}
1179
1180
	/**
1181
	 * Determines if the last failure was due to a lock timeout
1182
	 *
1183
	 * @return bool
1184
	 */
1185
	function wasLockTimeout() {
1186
		return $this->lastErrno() == 1205;
1187
	}
1188
1189
	/**
1190
	 * Determines if the last query error was something that should be dealt
1191
	 * with by pinging the connection and reissuing the query
1192
	 *
1193
	 * @return bool
1194
	 */
1195
	function wasErrorReissuable() {
1196
		return $this->lastErrno() == 2013 || $this->lastErrno() == 2006;
1197
	}
1198
1199
	/**
1200
	 * Determines if the last failure was due to the database being read-only.
1201
	 *
1202
	 * @return bool
1203
	 */
1204
	function wasReadOnlyError() {
1205
		return $this->lastErrno() == 1223 ||
1206
			( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
1207
	}
1208
1209
	function wasConnectionError( $errno ) {
1210
		return $errno == 2013 || $errno == 2006;
1211
	}
1212
1213
	/**
1214
	 * Get the underlying binding handle, mConn
1215
	 *
1216
	 * Makes sure that mConn is set (disconnects and ping() failure can unset it).
1217
	 * This catches broken callers than catch and ignore disconnection exceptions.
1218
	 * Unlike checking isOpen(), this is safe to call inside of open().
1219
	 *
1220
	 * @return resource|object
1221
	 * @throws DBUnexpectedError
1222
	 * @since 1.26
1223
	 */
1224
	protected function getBindingHandle() {
1225
		if ( !$this->mConn ) {
1226
			throw new DBUnexpectedError(
1227
				$this,
1228
				'DB connection was already closed or the connection dropped.'
1229
			);
1230
		}
1231
1232
		return $this->mConn;
1233
	}
1234
1235
	/**
1236
	 * @param string $oldName
1237
	 * @param string $newName
1238
	 * @param bool $temporary
1239
	 * @param string $fname
1240
	 * @return bool
1241
	 */
1242
	function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) {
1243
		$tmp = $temporary ? 'TEMPORARY ' : '';
1244
		$newName = $this->addIdentifierQuotes( $newName );
1245
		$oldName = $this->addIdentifierQuotes( $oldName );
1246
		$query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
1247
1248
		return $this->query( $query, $fname );
1249
	}
1250
1251
	/**
1252
	 * List all tables on the database
1253
	 *
1254
	 * @param string $prefix Only show tables with this prefix, e.g. mw_
1255
	 * @param string $fname Calling function name
1256
	 * @return array
1257
	 */
1258
	function listTables( $prefix = null, $fname = __METHOD__ ) {
1259
		$result = $this->query( "SHOW TABLES", $fname );
1260
1261
		$endArray = [];
1262
1263 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...
1264
			$vars = get_object_vars( $table );
1265
			$table = array_pop( $vars );
1266
1267
			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...
1268
				$endArray[] = $table;
1269
			}
1270
		}
1271
1272
		return $endArray;
1273
	}
1274
1275
	/**
1276
	 * @param string $tableName
1277
	 * @param string $fName
1278
	 * @return bool|ResultWrapper
1279
	 */
1280 View Code Duplication
	public function dropTable( $tableName, $fName = __METHOD__ ) {
1281
		if ( !$this->tableExists( $tableName, $fName ) ) {
1282
			return false;
1283
		}
1284
1285
		return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName );
1286
	}
1287
1288
	/**
1289
	 * @return array
1290
	 */
1291
	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...
1292
		$vars = parent::getDefaultSchemaVars();
1293
		$vars['wgDBTableOptions'] = str_replace( 'TYPE', 'ENGINE', $GLOBALS['wgDBTableOptions'] );
1294
		$vars['wgDBTableOptions'] = str_replace(
1295
			'CHARSET=mysql4',
1296
			'CHARSET=binary',
1297
			$vars['wgDBTableOptions']
1298
		);
1299
1300
		return $vars;
1301
	}
1302
1303
	/**
1304
	 * Get status information from SHOW STATUS in an associative array
1305
	 *
1306
	 * @param string $which
1307
	 * @return array
1308
	 */
1309
	function getMysqlStatus( $which = "%" ) {
1310
		$res = $this->query( "SHOW STATUS LIKE '{$which}'" );
1311
		$status = [];
1312
1313
		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...
1314
			$status[$row->Variable_name] = $row->Value;
1315
		}
1316
1317
		return $status;
1318
	}
1319
1320
	/**
1321
	 * Lists VIEWs in the database
1322
	 *
1323
	 * @param string $prefix Only show VIEWs with this prefix, eg.
1324
	 * unit_test_, or $wgDBprefix. Default: null, would return all views.
1325
	 * @param string $fname Name of calling function
1326
	 * @return array
1327
	 * @since 1.22
1328
	 */
1329
	public function listViews( $prefix = null, $fname = __METHOD__ ) {
1330
1331
		if ( !isset( $this->allViews ) ) {
1332
1333
			// The name of the column containing the name of the VIEW
1334
			$propertyName = 'Tables_in_' . $this->mDBname;
1335
1336
			// Query for the VIEWS
1337
			$result = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' );
1338
			$this->allViews = [];
1339
			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 1337 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...
1340
				array_push( $this->allViews, $row[$propertyName] );
1341
			}
1342
		}
1343
1344
		if ( is_null( $prefix ) || $prefix === '' ) {
1345
			return $this->allViews;
1346
		}
1347
1348
		$filteredViews = [];
1349
		foreach ( $this->allViews as $viewName ) {
1350
			// Does the name of this VIEW start with the table-prefix?
1351
			if ( strpos( $viewName, $prefix ) === 0 ) {
1352
				array_push( $filteredViews, $viewName );
1353
			}
1354
		}
1355
1356
		return $filteredViews;
1357
	}
1358
1359
	/**
1360
	 * Differentiates between a TABLE and a VIEW.
1361
	 *
1362
	 * @param string $name Name of the TABLE/VIEW to test
1363
	 * @param string $prefix
1364
	 * @return bool
1365
	 * @since 1.22
1366
	 */
1367
	public function isView( $name, $prefix = null ) {
1368
		return in_array( $name, $this->listViews( $prefix ) );
1369
	}
1370
}
1371
1372
/**
1373
 * Utility class.
1374
 * @ingroup Database
1375
 */
1376
class MySQLField implements Field {
1377
	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...
1378
		$is_pk, $is_unique, $is_multiple, $is_key, $type, $binary,
1379
		$is_numeric, $is_blob, $is_unsigned, $is_zerofill;
1380
1381
	function __construct( $info ) {
1382
		$this->name = $info->name;
1383
		$this->tablename = $info->table;
1384
		$this->default = $info->def;
1385
		$this->max_length = $info->max_length;
1386
		$this->nullable = !$info->not_null;
1387
		$this->is_pk = $info->primary_key;
1388
		$this->is_unique = $info->unique_key;
1389
		$this->is_multiple = $info->multiple_key;
1390
		$this->is_key = ( $this->is_pk || $this->is_unique || $this->is_multiple );
1391
		$this->type = $info->type;
1392
		$this->binary = isset( $info->binary ) ? $info->binary : false;
1393
		$this->is_numeric = isset( $info->numeric ) ? $info->numeric : false;
1394
		$this->is_blob = isset( $info->blob ) ? $info->blob : false;
1395
		$this->is_unsigned = isset( $info->unsigned ) ? $info->unsigned : false;
1396
		$this->is_zerofill = isset( $info->zerofill ) ? $info->zerofill : false;
1397
	}
1398
1399
	/**
1400
	 * @return string
1401
	 */
1402
	function name() {
1403
		return $this->name;
1404
	}
1405
1406
	/**
1407
	 * @return string
1408
	 */
1409
	function tableName() {
1410
		return $this->tablename;
1411
	}
1412
1413
	/**
1414
	 * @return string
1415
	 */
1416
	function type() {
1417
		return $this->type;
1418
	}
1419
1420
	/**
1421
	 * @return bool
1422
	 */
1423
	function isNullable() {
1424
		return $this->nullable;
1425
	}
1426
1427
	function defaultValue() {
1428
		return $this->default;
1429
	}
1430
1431
	/**
1432
	 * @return bool
1433
	 */
1434
	function isKey() {
1435
		return $this->is_key;
1436
	}
1437
1438
	/**
1439
	 * @return bool
1440
	 */
1441
	function isMultipleKey() {
1442
		return $this->is_multiple;
1443
	}
1444
1445
	/**
1446
	 * @return bool
1447
	 */
1448
	function isBinary() {
1449
		return $this->binary;
1450
	}
1451
1452
	/**
1453
	 * @return bool
1454
	 */
1455
	function isNumeric() {
1456
		return $this->is_numeric;
1457
	}
1458
1459
	/**
1460
	 * @return bool
1461
	 */
1462
	function isBlob() {
1463
		return $this->is_blob;
1464
	}
1465
1466
	/**
1467
	 * @return bool
1468
	 */
1469
	function isUnsigned() {
1470
		return $this->is_unsigned;
1471
	}
1472
1473
	/**
1474
	 * @return bool
1475
	 */
1476
	function isZerofill() {
1477
		return $this->is_zerofill;
1478
	}
1479
}
1480
1481
/**
1482
 * DBMasterPos class for MySQL/MariaDB
1483
 *
1484
 * Note that master positions and sync logic here make some assumptions:
1485
 *  - Binlog-based usage assumes single-source replication and non-hierarchical replication.
1486
 *  - GTID-based usage allows getting/syncing with multi-source replication. It is assumed
1487
 *    that GTID sets are complete (e.g. include all domains on the server).
1488
 */
1489
class MySQLMasterPos implements DBMasterPos {
1490
	/** @var string Binlog file */
1491
	public $file;
1492
	/** @var int Binglog file position */
1493
	public $pos;
1494
	/** @var string[] GTID list */
1495
	public $gtids = [];
1496
	/** @var float UNIX timestamp */
1497
	public $asOfTime = 0.0;
1498
1499
	/**
1500
	 * @param string $file Binlog file name
1501
	 * @param integer $pos Binlog position
1502
	 * @param string $gtid Comma separated GTID set [optional]
1503
	 */
1504
	function __construct( $file, $pos, $gtid = '' ) {
1505
		$this->file = $file;
1506
		$this->pos = $pos;
1507
		$this->gtids = array_map( 'trim', explode( ',', $gtid ) );
1508
		$this->asOfTime = microtime( true );
1509
	}
1510
1511
	/**
1512
	 * @return string <binlog file>/<position>, e.g db1034-bin.000976/843431247
1513
	 */
1514
	function __toString() {
1515
		return "{$this->file}/{$this->pos}";
1516
	}
1517
1518
	function asOfTime() {
1519
		return $this->asOfTime;
1520
	}
1521
1522
	function hasReached( DBMasterPos $pos ) {
1523
		if ( !( $pos instanceof self ) ) {
1524
			throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
1525
		}
1526
1527
		// Prefer GTID comparisons, which work with multi-tier replication
1528
		$thisPosByDomain = $this->getGtidCoordinates();
1529
		$thatPosByDomain = $pos->getGtidCoordinates();
1530
		if ( $thisPosByDomain && $thatPosByDomain ) {
1531
			$reached = true;
1532
			// Check that this has positions GTE all of those in $pos for all domains in $pos
1533
			foreach ( $thatPosByDomain as $domain => $thatPos ) {
1534
				$thisPos = isset( $thisPosByDomain[$domain] ) ? $thisPosByDomain[$domain] : -1;
1535
				$reached = $reached && ( $thatPos <= $thisPos );
1536
			}
1537
1538
			return $reached;
1539
		}
1540
1541
		// Fallback to the binlog file comparisons
1542
		$thisBinPos = $this->getBinlogCoordinates();
1543
		$thatBinPos = $pos->getBinlogCoordinates();
1544
		if ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] ) {
1545
			return ( $thisBinPos['pos'] >= $thatBinPos['pos'] );
1546
		}
1547
1548
		// Comparing totally different binlogs does not make sense
1549
		return false;
1550
	}
1551
1552
	function channelsMatch( DBMasterPos $pos ) {
1553
		if ( !( $pos instanceof self ) ) {
1554
			throw new InvalidArgumentException( "Position not an instance of " . __CLASS__ );
1555
		}
1556
1557
		// Prefer GTID comparisons, which work with multi-tier replication
1558
		$thisPosDomains = array_keys( $this->getGtidCoordinates() );
1559
		$thatPosDomains = array_keys( $pos->getGtidCoordinates() );
1560
		if ( $thisPosDomains && $thatPosDomains ) {
1561
			// Check that this has GTIDs for all domains in $pos
1562
			return !array_diff( $thatPosDomains, $thisPosDomains );
1563
		}
1564
1565
		// Fallback to the binlog file comparisons
1566
		$thisBinPos = $this->getBinlogCoordinates();
1567
		$thatBinPos = $pos->getBinlogCoordinates();
1568
1569
		return ( $thisBinPos && $thatBinPos && $thisBinPos['binlog'] === $thatBinPos['binlog'] );
1570
	}
1571
1572
	/**
1573
	 * @note: this returns false for multi-source replication GTID sets
1574
	 * @see https://mariadb.com/kb/en/mariadb/gtid
1575
	 * @see https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
1576
	 * @return array Map of (domain => integer position) or false
1577
	 */
1578
	protected function getGtidCoordinates() {
1579
		$gtidInfos = [];
1580
		foreach ( $this->gtids as $gtid ) {
1581
			$m = [];
1582
			// MariaDB style: <domain>-<server id>-<sequence number>
1583
			if ( preg_match( '!^(\d+)-\d+-(\d+)$!', $gtid, $m ) ) {
1584
				$gtidInfos[(int)$m[1]] = (int)$m[2];
1585
			// MySQL style: <UUID domain>:<sequence number>
1586
			} elseif ( preg_match( '!^(\w{8}-\w{4}-\w{4}-\w{4}-\w{12}):(\d+)$!', $gtid, $m ) ) {
1587
				$gtidInfos[$m[1]] = (int)$m[2];
1588
			} else {
1589
				$gtidInfos = [];
1590
				break; // unrecognized GTID
1591
			}
1592
1593
		}
1594
1595
		return $gtidInfos;
1596
	}
1597
1598
	/**
1599
	 * @see http://dev.mysql.com/doc/refman/5.7/en/show-master-status.html
1600
	 * @see http://dev.mysql.com/doc/refman/5.7/en/show-slave-status.html
1601
	 * @return array|bool (binlog, (integer file number, integer position)) or false
1602
	 */
1603
	protected function getBinlogCoordinates() {
1604
		$m = [];
1605
		if ( preg_match( '!^(.+)\.(\d+)/(\d+)$!', (string)$this, $m ) ) {
1606
			return [ 'binlog' => $m[1], 'pos' => [ (int)$m[2], (int)$m[3] ] ];
1607
		}
1608
1609
		return false;
1610
	}
1611
}
1612