Completed
Branch master (425ee1)
by
unknown
27:09
created

MySQLMasterPos::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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