These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * This is the MySQL database abstraction layer. |
||
4 | * |
||
5 | * This program is free software; you can redistribute it and/or modify |
||
6 | * it under the terms of the GNU General Public License as published by |
||
7 | * the Free Software Foundation; either version 2 of the License, or |
||
8 | * (at your option) any later version. |
||
9 | * |
||
10 | * This program is distributed in the hope that it will be useful, |
||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | * GNU General Public License for more details. |
||
14 | * |
||
15 | * You should have received a copy of the GNU General Public License along |
||
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
18 | * http://www.gnu.org/copyleft/gpl.html |
||
19 | * |
||
20 | * @file |
||
21 | * @ingroup Database |
||
22 | */ |
||
23 | |||
24 | /** |
||
25 | * Database abstraction object for MySQL. |
||
26 | * Defines methods independent on used MySQL extension. |
||
27 | * |
||
28 | * @ingroup Database |
||
29 | * @since 1.22 |
||
30 | * @see Database |
||
31 | */ |
||
32 | abstract class DatabaseMysqlBase extends Database { |
||
33 | /** @var MysqlMasterPos */ |
||
34 | protected $lastKnownReplicaPos; |
||
35 | /** @var string Method to detect replica DB lag */ |
||
36 | protected $lagDetectionMethod; |
||
37 | /** @var array Method to detect replica DB lag */ |
||
38 | protected $lagDetectionOptions = []; |
||
39 | /** @var bool bool Whether to use GTID methods */ |
||
40 | protected $useGTIDs = false; |
||
41 | /** @var string|null */ |
||
42 | protected $sslKeyPath; |
||
43 | /** @var string|null */ |
||
44 | protected $sslCertPath; |
||
45 | /** @var string|null */ |
||
46 | protected $sslCAPath; |
||
47 | /** @var string[]|null */ |
||
48 | protected $sslCiphers; |
||
49 | /** @var string sql_mode value to send on connection */ |
||
50 | protected $sqlMode; |
||
51 | /** @var bool Use experimental UTF-8 transmission encoding */ |
||
52 | protected $utf8Mode; |
||
53 | |||
54 | /** @var string|null */ |
||
55 | private $serverVersion = null; |
||
56 | |||
57 | /** |
||
58 | * Additional $params include: |
||
59 | * - lagDetectionMethod : set to one of (Seconds_Behind_Master,pt-heartbeat). |
||
60 | * pt-heartbeat assumes the table is at heartbeat.heartbeat |
||
61 | * and uses UTC timestamps in the heartbeat.ts column. |
||
62 | * (https://www.percona.com/doc/percona-toolkit/2.2/pt-heartbeat.html) |
||
63 | * - lagDetectionOptions : if using pt-heartbeat, this can be set to an array map to change |
||
64 | * the default behavior. Normally, the heartbeat row with the server |
||
65 | * ID of this server's master will be used. Set the "conds" field to |
||
66 | * override the query conditions, e.g. ['shard' => 's1']. |
||
67 | * - useGTIDs : use GTID methods like MASTER_GTID_WAIT() when possible. |
||
68 | * - sslKeyPath : path to key file [default: null] |
||
69 | * - sslCertPath : path to certificate file [default: null] |
||
70 | * - sslCAPath : parth to certificate authority PEM files [default: null] |
||
71 | * - sslCiphers : array list of allowable ciphers [default: null] |
||
72 | * @param array $params |
||
73 | */ |
||
74 | function __construct( array $params ) { |
||
75 | $this->lagDetectionMethod = isset( $params['lagDetectionMethod'] ) |
||
76 | ? $params['lagDetectionMethod'] |
||
77 | : 'Seconds_Behind_Master'; |
||
78 | $this->lagDetectionOptions = isset( $params['lagDetectionOptions'] ) |
||
79 | ? $params['lagDetectionOptions'] |
||
80 | : []; |
||
81 | $this->useGTIDs = !empty( $params['useGTIDs' ] ); |
||
82 | foreach ( [ 'KeyPath', 'CertPath', 'CAPath', 'Ciphers' ] as $name ) { |
||
83 | $var = "ssl{$name}"; |
||
84 | if ( isset( $params[$var] ) ) { |
||
85 | $this->$var = $params[$var]; |
||
86 | } |
||
87 | } |
||
88 | $this->sqlMode = isset( $params['sqlMode'] ) ? $params['sqlMode'] : ''; |
||
89 | $this->utf8Mode = !empty( $params['utf8Mode'] ); |
||
90 | |||
91 | parent::__construct( $params ); |
||
92 | } |
||
93 | |||
94 | /** |
||
95 | * @return string |
||
96 | */ |
||
97 | function getType() { |
||
98 | return 'mysql'; |
||
99 | } |
||
100 | |||
101 | /** |
||
102 | * @param string $server |
||
103 | * @param string $user |
||
104 | * @param string $password |
||
105 | * @param string $dbName |
||
106 | * @throws Exception|DBConnectionError |
||
107 | * @return bool |
||
108 | */ |
||
109 | function open( $server, $user, $password, $dbName ) { |
||
110 | # Close/unset connection handle |
||
111 | $this->close(); |
||
112 | |||
113 | $this->mServer = $server; |
||
114 | $this->mUser = $user; |
||
115 | $this->mPassword = $password; |
||
116 | $this->mDBname = $dbName; |
||
117 | |||
118 | $this->installErrorHandler(); |
||
119 | try { |
||
120 | $this->mConn = $this->mysqlConnect( $this->mServer ); |
||
121 | } catch ( Exception $ex ) { |
||
122 | $this->restoreErrorHandler(); |
||
123 | throw $ex; |
||
124 | } |
||
125 | $error = $this->restoreErrorHandler(); |
||
126 | |||
127 | # Always log connection errors |
||
128 | if ( !$this->mConn ) { |
||
129 | if ( !$error ) { |
||
130 | $error = $this->lastError(); |
||
131 | } |
||
132 | $this->connLogger->error( |
||
133 | "Error connecting to {db_server}: {error}", |
||
134 | $this->getLogContext( [ |
||
135 | 'method' => __METHOD__, |
||
136 | 'error' => $error, |
||
137 | ] ) |
||
138 | ); |
||
139 | $this->connLogger->debug( "DB connection error\n" . |
||
140 | "Server: $server, User: $user, Password: " . |
||
141 | substr( $password, 0, 3 ) . "..., error: " . $error . "\n" ); |
||
142 | |||
143 | $this->reportConnectionError( $error ); |
||
144 | } |
||
145 | |||
146 | if ( $dbName != '' ) { |
||
147 | MediaWiki\suppressWarnings(); |
||
148 | $success = $this->selectDB( $dbName ); |
||
149 | MediaWiki\restoreWarnings(); |
||
150 | if ( !$success ) { |
||
151 | $this->queryLogger->error( |
||
152 | "Error selecting database {db_name} on server {db_server}", |
||
153 | $this->getLogContext( [ |
||
154 | 'method' => __METHOD__, |
||
155 | ] ) |
||
156 | ); |
||
157 | $this->queryLogger->debug( |
||
158 | "Error selecting database $dbName on server {$this->mServer}" ); |
||
159 | |||
160 | $this->reportConnectionError( "Error selecting database $dbName" ); |
||
161 | } |
||
162 | } |
||
163 | |||
164 | // Tell the server what we're communicating with |
||
165 | if ( !$this->connectInitCharset() ) { |
||
166 | $this->reportConnectionError( "Error setting character set" ); |
||
167 | } |
||
168 | |||
169 | // Abstract over any insane MySQL defaults |
||
170 | $set = [ 'group_concat_max_len = 262144' ]; |
||
171 | // Set SQL mode, default is turning them all off, can be overridden or skipped with null |
||
172 | if ( is_string( $this->sqlMode ) ) { |
||
173 | $set[] = 'sql_mode = ' . $this->addQuotes( $this->sqlMode ); |
||
174 | } |
||
175 | // Set any custom settings defined by site config |
||
176 | // (e.g. https://dev.mysql.com/doc/refman/4.1/en/innodb-parameters.html) |
||
177 | foreach ( $this->mSessionVars as $var => $val ) { |
||
178 | // Escape strings but not numbers to avoid MySQL complaining |
||
179 | if ( !is_int( $val ) && !is_float( $val ) ) { |
||
180 | $val = $this->addQuotes( $val ); |
||
181 | } |
||
182 | $set[] = $this->addIdentifierQuotes( $var ) . ' = ' . $val; |
||
183 | } |
||
184 | |||
185 | if ( $set ) { |
||
186 | // Use doQuery() to avoid opening implicit transactions (DBO_TRX) |
||
187 | $success = $this->doQuery( 'SET ' . implode( ', ', $set ) ); |
||
188 | if ( !$success ) { |
||
189 | $this->queryLogger->error( |
||
190 | 'Error setting MySQL variables on server {db_server} (check $wgSQLMode)', |
||
191 | $this->getLogContext( [ |
||
192 | 'method' => __METHOD__, |
||
193 | ] ) |
||
194 | ); |
||
195 | $this->reportConnectionError( |
||
196 | 'Error setting MySQL variables on server {db_server} (check $wgSQLMode)' ); |
||
197 | } |
||
198 | } |
||
199 | |||
200 | $this->mOpened = true; |
||
201 | |||
202 | return true; |
||
203 | } |
||
204 | |||
205 | /** |
||
206 | * Set the character set information right after connection |
||
207 | * @return bool |
||
208 | */ |
||
209 | protected function connectInitCharset() { |
||
210 | if ( $this->utf8Mode ) { |
||
211 | // Tell the server we're communicating with it in UTF-8. |
||
212 | // This may engage various charset conversions. |
||
213 | return $this->mysqlSetCharset( 'utf8' ); |
||
214 | } else { |
||
215 | return $this->mysqlSetCharset( 'binary' ); |
||
216 | } |
||
217 | } |
||
218 | |||
219 | /** |
||
220 | * Open a connection to a MySQL server |
||
221 | * |
||
222 | * @param string $realServer |
||
223 | * @return mixed Raw connection |
||
224 | * @throws DBConnectionError |
||
225 | */ |
||
226 | abstract protected function mysqlConnect( $realServer ); |
||
227 | |||
228 | /** |
||
229 | * Set the character set of the MySQL link |
||
230 | * |
||
231 | * @param string $charset |
||
232 | * @return bool |
||
233 | */ |
||
234 | abstract protected function mysqlSetCharset( $charset ); |
||
235 | |||
236 | /** |
||
237 | * @param ResultWrapper|resource $res |
||
238 | * @throws DBUnexpectedError |
||
239 | */ |
||
240 | View Code Duplication | function freeResult( $res ) { |
|
241 | if ( $res instanceof ResultWrapper ) { |
||
242 | $res = $res->result; |
||
243 | } |
||
244 | MediaWiki\suppressWarnings(); |
||
245 | $ok = $this->mysqlFreeResult( $res ); |
||
246 | MediaWiki\restoreWarnings(); |
||
247 | if ( !$ok ) { |
||
248 | throw new DBUnexpectedError( $this, "Unable to free MySQL result" ); |
||
249 | } |
||
250 | } |
||
251 | |||
252 | /** |
||
253 | * Free result memory |
||
254 | * |
||
255 | * @param resource $res Raw result |
||
256 | * @return bool |
||
257 | */ |
||
258 | abstract protected function mysqlFreeResult( $res ); |
||
259 | |||
260 | /** |
||
261 | * @param ResultWrapper|resource $res |
||
262 | * @return stdClass|bool |
||
263 | * @throws DBUnexpectedError |
||
264 | */ |
||
265 | View Code Duplication | function fetchObject( $res ) { |
|
266 | if ( $res instanceof ResultWrapper ) { |
||
267 | $res = $res->result; |
||
268 | } |
||
269 | MediaWiki\suppressWarnings(); |
||
270 | $row = $this->mysqlFetchObject( $res ); |
||
271 | MediaWiki\restoreWarnings(); |
||
272 | |||
273 | $errno = $this->lastErrno(); |
||
274 | // Unfortunately, mysql_fetch_object does not reset the last errno. |
||
275 | // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as |
||
276 | // these are the only errors mysql_fetch_object can cause. |
||
277 | // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html. |
||
278 | if ( $errno == 2000 || $errno == 2013 ) { |
||
279 | throw new DBUnexpectedError( |
||
280 | $this, |
||
281 | 'Error in fetchObject(): ' . htmlspecialchars( $this->lastError() ) |
||
282 | ); |
||
283 | } |
||
284 | |||
285 | return $row; |
||
286 | } |
||
287 | |||
288 | /** |
||
289 | * Fetch a result row as an object |
||
290 | * |
||
291 | * @param resource $res Raw result |
||
292 | * @return stdClass |
||
293 | */ |
||
294 | abstract protected function mysqlFetchObject( $res ); |
||
295 | |||
296 | /** |
||
297 | * @param ResultWrapper|resource $res |
||
298 | * @return array|bool |
||
299 | * @throws DBUnexpectedError |
||
300 | */ |
||
301 | View Code Duplication | function fetchRow( $res ) { |
|
302 | if ( $res instanceof ResultWrapper ) { |
||
303 | $res = $res->result; |
||
304 | } |
||
305 | MediaWiki\suppressWarnings(); |
||
306 | $row = $this->mysqlFetchArray( $res ); |
||
307 | MediaWiki\restoreWarnings(); |
||
308 | |||
309 | $errno = $this->lastErrno(); |
||
310 | // Unfortunately, mysql_fetch_array does not reset the last errno. |
||
311 | // Only check for CR_SERVER_LOST and CR_UNKNOWN_ERROR, as |
||
312 | // these are the only errors mysql_fetch_array can cause. |
||
313 | // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html. |
||
314 | if ( $errno == 2000 || $errno == 2013 ) { |
||
315 | throw new DBUnexpectedError( |
||
316 | $this, |
||
317 | 'Error in fetchRow(): ' . htmlspecialchars( $this->lastError() ) |
||
318 | ); |
||
319 | } |
||
320 | |||
321 | return $row; |
||
322 | } |
||
323 | |||
324 | /** |
||
325 | * Fetch a result row as an associative and numeric array |
||
326 | * |
||
327 | * @param resource $res Raw result |
||
328 | * @return array |
||
329 | */ |
||
330 | abstract protected function mysqlFetchArray( $res ); |
||
331 | |||
332 | /** |
||
333 | * @throws DBUnexpectedError |
||
334 | * @param ResultWrapper|resource $res |
||
335 | * @return int |
||
336 | */ |
||
337 | function numRows( $res ) { |
||
338 | if ( $res instanceof ResultWrapper ) { |
||
339 | $res = $res->result; |
||
340 | } |
||
341 | MediaWiki\suppressWarnings(); |
||
342 | $n = $this->mysqlNumRows( $res ); |
||
343 | MediaWiki\restoreWarnings(); |
||
344 | |||
345 | // Unfortunately, mysql_num_rows does not reset the last errno. |
||
346 | // We are not checking for any errors here, since |
||
347 | // these are no errors mysql_num_rows can cause. |
||
348 | // See http://dev.mysql.com/doc/refman/5.0/en/mysql-fetch-row.html. |
||
349 | // See https://phabricator.wikimedia.org/T44430 |
||
350 | return $n; |
||
351 | } |
||
352 | |||
353 | /** |
||
354 | * Get number of rows in result |
||
355 | * |
||
356 | * @param resource $res Raw result |
||
357 | * @return int |
||
358 | */ |
||
359 | abstract protected function mysqlNumRows( $res ); |
||
360 | |||
361 | /** |
||
362 | * @param ResultWrapper|resource $res |
||
363 | * @return int |
||
364 | */ |
||
365 | function numFields( $res ) { |
||
366 | if ( $res instanceof ResultWrapper ) { |
||
367 | $res = $res->result; |
||
368 | } |
||
369 | |||
370 | return $this->mysqlNumFields( $res ); |
||
371 | } |
||
372 | |||
373 | /** |
||
374 | * Get number of fields in result |
||
375 | * |
||
376 | * @param resource $res Raw result |
||
377 | * @return int |
||
378 | */ |
||
379 | abstract protected function mysqlNumFields( $res ); |
||
380 | |||
381 | /** |
||
382 | * @param ResultWrapper|resource $res |
||
383 | * @param int $n |
||
384 | * @return string |
||
385 | */ |
||
386 | function fieldName( $res, $n ) { |
||
387 | if ( $res instanceof ResultWrapper ) { |
||
388 | $res = $res->result; |
||
389 | } |
||
390 | |||
391 | return $this->mysqlFieldName( $res, $n ); |
||
392 | } |
||
393 | |||
394 | /** |
||
395 | * Get the name of the specified field in a result |
||
396 | * |
||
397 | * @param ResultWrapper|resource $res |
||
398 | * @param int $n |
||
399 | * @return string |
||
400 | */ |
||
401 | abstract protected function mysqlFieldName( $res, $n ); |
||
402 | |||
403 | /** |
||
404 | * mysql_field_type() wrapper |
||
405 | * @param ResultWrapper|resource $res |
||
406 | * @param int $n |
||
407 | * @return string |
||
408 | */ |
||
409 | public function fieldType( $res, $n ) { |
||
410 | if ( $res instanceof ResultWrapper ) { |
||
411 | $res = $res->result; |
||
412 | } |
||
413 | |||
414 | return $this->mysqlFieldType( $res, $n ); |
||
415 | } |
||
416 | |||
417 | /** |
||
418 | * Get the type of the specified field in a result |
||
419 | * |
||
420 | * @param ResultWrapper|resource $res |
||
421 | * @param int $n |
||
422 | * @return string |
||
423 | */ |
||
424 | abstract protected function mysqlFieldType( $res, $n ); |
||
425 | |||
426 | /** |
||
427 | * @param ResultWrapper|resource $res |
||
428 | * @param int $row |
||
429 | * @return bool |
||
430 | */ |
||
431 | function dataSeek( $res, $row ) { |
||
432 | if ( $res instanceof ResultWrapper ) { |
||
433 | $res = $res->result; |
||
434 | } |
||
435 | |||
436 | return $this->mysqlDataSeek( $res, $row ); |
||
437 | } |
||
438 | |||
439 | /** |
||
440 | * Move internal result pointer |
||
441 | * |
||
442 | * @param ResultWrapper|resource $res |
||
443 | * @param int $row |
||
444 | * @return bool |
||
445 | */ |
||
446 | abstract protected function mysqlDataSeek( $res, $row ); |
||
447 | |||
448 | /** |
||
449 | * @return string |
||
450 | */ |
||
451 | function lastError() { |
||
452 | if ( $this->mConn ) { |
||
453 | # Even if it's non-zero, it can still be invalid |
||
454 | MediaWiki\suppressWarnings(); |
||
455 | $error = $this->mysqlError( $this->mConn ); |
||
456 | if ( !$error ) { |
||
457 | $error = $this->mysqlError(); |
||
458 | } |
||
459 | MediaWiki\restoreWarnings(); |
||
460 | } else { |
||
461 | $error = $this->mysqlError(); |
||
462 | } |
||
463 | if ( $error ) { |
||
464 | $error .= ' (' . $this->mServer . ')'; |
||
465 | } |
||
466 | |||
467 | return $error; |
||
468 | } |
||
469 | |||
470 | /** |
||
471 | * Returns the text of the error message from previous MySQL operation |
||
472 | * |
||
473 | * @param resource $conn Raw connection |
||
474 | * @return string |
||
475 | */ |
||
476 | abstract protected function mysqlError( $conn = null ); |
||
477 | |||
478 | /** |
||
479 | * @param string $table |
||
480 | * @param array $uniqueIndexes |
||
481 | * @param array $rows |
||
482 | * @param string $fname |
||
483 | * @return ResultWrapper |
||
484 | */ |
||
485 | function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) { |
||
486 | return $this->nativeReplace( $table, $rows, $fname ); |
||
487 | } |
||
488 | |||
489 | /** |
||
490 | * Estimate rows in dataset |
||
491 | * Returns estimated count, based on EXPLAIN output |
||
492 | * Takes same arguments as Database::select() |
||
493 | * |
||
494 | * @param string|array $table |
||
495 | * @param string|array $vars |
||
496 | * @param string|array $conds |
||
497 | * @param string $fname |
||
498 | * @param string|array $options |
||
499 | * @return bool|int |
||
500 | */ |
||
501 | public function estimateRowCount( $table, $vars = '*', $conds = '', |
||
502 | $fname = __METHOD__, $options = [] |
||
503 | ) { |
||
504 | $options['EXPLAIN'] = true; |
||
505 | $res = $this->select( $table, $vars, $conds, $fname, $options ); |
||
506 | if ( $res === false ) { |
||
507 | return false; |
||
508 | } |
||
509 | if ( !$this->numRows( $res ) ) { |
||
510 | return 0; |
||
511 | } |
||
512 | |||
513 | $rows = 1; |
||
514 | foreach ( $res as $plan ) { |
||
515 | $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero |
||
516 | } |
||
517 | |||
518 | return (int)$rows; |
||
519 | } |
||
520 | |||
521 | function tableExists( $table, $fname = __METHOD__ ) { |
||
522 | $table = $this->tableName( $table, 'raw' ); |
||
523 | if ( isset( $this->mSessionTempTables[$table] ) ) { |
||
524 | return true; // already known to exist and won't show in SHOW TABLES anyway |
||
525 | } |
||
526 | |||
527 | $encLike = $this->buildLike( $table ); |
||
528 | |||
529 | return $this->query( "SHOW TABLES $encLike", $fname )->numRows() > 0; |
||
530 | } |
||
531 | |||
532 | /** |
||
533 | * @param string $table |
||
534 | * @param string $field |
||
535 | * @return bool|MySQLField |
||
536 | */ |
||
537 | function fieldInfo( $table, $field ) { |
||
538 | $table = $this->tableName( $table ); |
||
539 | $res = $this->query( "SELECT * FROM $table LIMIT 1", __METHOD__, true ); |
||
540 | if ( !$res ) { |
||
541 | return false; |
||
542 | } |
||
543 | $n = $this->mysqlNumFields( $res->result ); |
||
544 | for ( $i = 0; $i < $n; $i++ ) { |
||
545 | $meta = $this->mysqlFetchField( $res->result, $i ); |
||
546 | if ( $field == $meta->name ) { |
||
547 | return new MySQLField( $meta ); |
||
548 | } |
||
549 | } |
||
550 | |||
551 | return false; |
||
552 | } |
||
553 | |||
554 | /** |
||
555 | * Get column information from a result |
||
556 | * |
||
557 | * @param resource $res Raw result |
||
558 | * @param int $n |
||
559 | * @return stdClass |
||
560 | */ |
||
561 | abstract protected function mysqlFetchField( $res, $n ); |
||
562 | |||
563 | /** |
||
564 | * Get information about an index into an object |
||
565 | * Returns false if the index does not exist |
||
566 | * |
||
567 | * @param string $table |
||
568 | * @param string $index |
||
569 | * @param string $fname |
||
570 | * @return bool|array|null False or null on failure |
||
571 | */ |
||
572 | function indexInfo( $table, $index, $fname = __METHOD__ ) { |
||
573 | # SHOW INDEX works in MySQL 3.23.58, but SHOW INDEXES does not. |
||
574 | # SHOW INDEX should work for 3.x and up: |
||
575 | # http://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html |
||
576 | $table = $this->tableName( $table ); |
||
577 | $index = $this->indexName( $index ); |
||
578 | |||
579 | $sql = 'SHOW INDEX FROM ' . $table; |
||
580 | $res = $this->query( $sql, $fname ); |
||
581 | |||
582 | if ( !$res ) { |
||
583 | return null; |
||
584 | } |
||
585 | |||
586 | $result = []; |
||
587 | |||
588 | foreach ( $res as $row ) { |
||
589 | if ( $row->Key_name == $index ) { |
||
590 | $result[] = $row; |
||
591 | } |
||
592 | } |
||
593 | |||
594 | return empty( $result ) ? false : $result; |
||
595 | } |
||
596 | |||
597 | /** |
||
598 | * @param string $s |
||
599 | * @return string |
||
600 | */ |
||
601 | function strencode( $s ) { |
||
602 | return $this->mysqlRealEscapeString( $s ); |
||
603 | } |
||
604 | |||
605 | /** |
||
606 | * @param string $s |
||
607 | * @return mixed |
||
608 | */ |
||
609 | abstract protected function mysqlRealEscapeString( $s ); |
||
610 | |||
611 | public function addQuotes( $s ) { |
||
612 | if ( is_bool( $s ) ) { |
||
613 | // Parent would transform to int, which does not play nice with MySQL type juggling. |
||
614 | // When searching for an int in a string column, the strings are cast to int, which |
||
615 | // means false would match any string not starting with a number. |
||
616 | $s = (string)(int)$s; |
||
617 | } |
||
618 | return parent::addQuotes( $s ); |
||
619 | } |
||
620 | |||
621 | /** |
||
622 | * MySQL uses `backticks` for identifier quoting instead of the sql standard "double quotes". |
||
623 | * |
||
624 | * @param string $s |
||
625 | * @return string |
||
626 | */ |
||
627 | public function addIdentifierQuotes( $s ) { |
||
628 | // Characters in the range \u0001-\uFFFF are valid in a quoted identifier |
||
629 | // Remove NUL bytes and escape backticks by doubling |
||
630 | return '`' . str_replace( [ "\0", '`' ], [ '', '``' ], $s ) . '`'; |
||
631 | } |
||
632 | |||
633 | /** |
||
634 | * @param string $name |
||
635 | * @return bool |
||
636 | */ |
||
637 | public function isQuotedIdentifier( $name ) { |
||
638 | return strlen( $name ) && $name[0] == '`' && substr( $name, -1, 1 ) == '`'; |
||
639 | } |
||
640 | |||
641 | function getLag() { |
||
642 | if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) { |
||
643 | return $this->getLagFromPtHeartbeat(); |
||
644 | } else { |
||
645 | return $this->getLagFromSlaveStatus(); |
||
646 | } |
||
647 | } |
||
648 | |||
649 | /** |
||
650 | * @return string |
||
651 | */ |
||
652 | protected function getLagDetectionMethod() { |
||
653 | return $this->lagDetectionMethod; |
||
654 | } |
||
655 | |||
656 | /** |
||
657 | * @return bool|int |
||
658 | */ |
||
659 | protected function getLagFromSlaveStatus() { |
||
660 | $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ ); |
||
661 | $row = $res ? $res->fetchObject() : false; |
||
662 | if ( $row && strval( $row->Seconds_Behind_Master ) !== '' ) { |
||
663 | return intval( $row->Seconds_Behind_Master ); |
||
664 | } |
||
665 | |||
666 | return false; |
||
667 | } |
||
668 | |||
669 | /** |
||
670 | * @return bool|float |
||
671 | */ |
||
672 | protected function getLagFromPtHeartbeat() { |
||
673 | $options = $this->lagDetectionOptions; |
||
674 | |||
675 | if ( isset( $options['conds'] ) ) { |
||
676 | // Best method for multi-DC setups: use logical channel names |
||
677 | $data = $this->getHeartbeatData( $options['conds'] ); |
||
678 | } else { |
||
679 | // Standard method: use master server ID (works with stock pt-heartbeat) |
||
680 | $masterInfo = $this->getMasterServerInfo(); |
||
681 | if ( !$masterInfo ) { |
||
682 | $this->queryLogger->error( |
||
683 | "Unable to query master of {db_server} for server ID", |
||
684 | $this->getLogContext( [ |
||
685 | 'method' => __METHOD__ |
||
686 | ] ) |
||
687 | ); |
||
688 | |||
689 | return false; // could not get master server ID |
||
690 | } |
||
691 | |||
692 | $conds = [ 'server_id' => intval( $masterInfo['serverId'] ) ]; |
||
693 | $data = $this->getHeartbeatData( $conds ); |
||
694 | } |
||
695 | |||
696 | list( $time, $nowUnix ) = $data; |
||
697 | if ( $time !== null ) { |
||
698 | // @time is in ISO format like "2015-09-25T16:48:10.000510" |
||
699 | $dateTime = new DateTime( $time, new DateTimeZone( 'UTC' ) ); |
||
700 | $timeUnix = (int)$dateTime->format( 'U' ) + $dateTime->format( 'u' ) / 1e6; |
||
701 | |||
702 | return max( $nowUnix - $timeUnix, 0.0 ); |
||
703 | } |
||
704 | |||
705 | $this->queryLogger->error( |
||
706 | "Unable to find pt-heartbeat row for {db_server}", |
||
707 | $this->getLogContext( [ |
||
708 | 'method' => __METHOD__ |
||
709 | ] ) |
||
710 | ); |
||
711 | |||
712 | return false; |
||
713 | } |
||
714 | |||
715 | protected function getMasterServerInfo() { |
||
716 | $cache = $this->srvCache; |
||
717 | $key = $cache->makeGlobalKey( |
||
718 | 'mysql', |
||
719 | 'master-info', |
||
720 | // Using one key for all cluster replica DBs is preferable |
||
721 | $this->getLBInfo( 'clusterMasterHost' ) ?: $this->getServer() |
||
722 | ); |
||
723 | |||
724 | return $cache->getWithSetCallback( |
||
725 | $key, |
||
726 | $cache::TTL_INDEFINITE, |
||
727 | function () use ( $cache, $key ) { |
||
728 | // Get and leave a lock key in place for a short period |
||
729 | if ( !$cache->lock( $key, 0, 10 ) ) { |
||
730 | return false; // avoid master connection spike slams |
||
731 | } |
||
732 | |||
733 | $conn = $this->getLazyMasterHandle(); |
||
734 | if ( !$conn ) { |
||
735 | return false; // something is misconfigured |
||
736 | } |
||
737 | |||
738 | // Connect to and query the master; catch errors to avoid outages |
||
739 | try { |
||
740 | $res = $conn->query( 'SELECT @@server_id AS id', __METHOD__ ); |
||
741 | $row = $res ? $res->fetchObject() : false; |
||
742 | $id = $row ? (int)$row->id : 0; |
||
743 | } catch ( DBError $e ) { |
||
744 | $id = 0; |
||
745 | } |
||
746 | |||
747 | // Cache the ID if it was retrieved |
||
748 | return $id ? [ 'serverId' => $id, 'asOf' => time() ] : false; |
||
749 | } |
||
750 | ); |
||
751 | } |
||
752 | |||
753 | /** |
||
754 | * @param array $conds WHERE clause conditions to find a row |
||
755 | * @return array (heartbeat `ts` column value or null, UNIX timestamp) for the newest beat |
||
756 | * @see https://www.percona.com/doc/percona-toolkit/2.1/pt-heartbeat.html |
||
757 | */ |
||
758 | protected function getHeartbeatData( array $conds ) { |
||
759 | $whereSQL = $this->makeList( $conds, self::LIST_AND ); |
||
760 | // Use ORDER BY for channel based queries since that field might not be UNIQUE. |
||
761 | // Note: this would use "TIMESTAMPDIFF(MICROSECOND,ts,UTC_TIMESTAMP(6))" but the |
||
762 | // percision field is not supported in MySQL <= 5.5. |
||
763 | $res = $this->query( |
||
764 | "SELECT ts FROM heartbeat.heartbeat WHERE $whereSQL ORDER BY ts DESC LIMIT 1" |
||
765 | ); |
||
766 | $row = $res ? $res->fetchObject() : false; |
||
767 | |||
768 | return [ $row ? $row->ts : null, microtime( true ) ]; |
||
769 | } |
||
770 | |||
771 | public function getApproximateLagStatus() { |
||
772 | if ( $this->getLagDetectionMethod() === 'pt-heartbeat' ) { |
||
773 | // Disable caching since this is fast enough and we don't wan't |
||
774 | // to be *too* pessimistic by having both the cache TTL and the |
||
775 | // pt-heartbeat interval count as lag in getSessionLagStatus() |
||
776 | return parent::getApproximateLagStatus(); |
||
777 | } |
||
778 | |||
779 | $key = $this->srvCache->makeGlobalKey( 'mysql-lag', $this->getServer() ); |
||
780 | $approxLag = $this->srvCache->get( $key ); |
||
781 | if ( !$approxLag ) { |
||
782 | $approxLag = parent::getApproximateLagStatus(); |
||
783 | $this->srvCache->set( $key, $approxLag, 1 ); |
||
784 | } |
||
785 | |||
786 | return $approxLag; |
||
787 | } |
||
788 | |||
789 | function masterPosWait( DBMasterPos $pos, $timeout ) { |
||
790 | if ( !( $pos instanceof MySQLMasterPos ) ) { |
||
791 | throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" ); |
||
792 | } |
||
793 | |||
794 | if ( $this->getLBInfo( 'is static' ) === true ) { |
||
795 | return 0; // this is a copy of a read-only dataset with no master DB |
||
796 | } elseif ( $this->lastKnownReplicaPos && $this->lastKnownReplicaPos->hasReached( $pos ) ) { |
||
797 | return 0; // already reached this point for sure |
||
798 | } |
||
799 | |||
800 | // Call doQuery() directly, to avoid opening a transaction if DBO_TRX is set |
||
801 | if ( $this->useGTIDs && $pos->gtids ) { |
||
802 | // Wait on the GTID set (MariaDB only) |
||
803 | $gtidArg = $this->addQuotes( implode( ',', $pos->gtids ) ); |
||
804 | $res = $this->doQuery( "SELECT MASTER_GTID_WAIT($gtidArg, $timeout)" ); |
||
805 | } else { |
||
806 | // Wait on the binlog coordinates |
||
807 | $encFile = $this->addQuotes( $pos->file ); |
||
808 | $encPos = intval( $pos->pos ); |
||
809 | $res = $this->doQuery( "SELECT MASTER_POS_WAIT($encFile, $encPos, $timeout)" ); |
||
810 | } |
||
811 | |||
812 | $row = $res ? $this->fetchRow( $res ) : false; |
||
813 | if ( !$row ) { |
||
814 | throw new DBExpectedError( $this, "Failed to query MASTER_POS_WAIT()" ); |
||
815 | } |
||
816 | |||
817 | // Result can be NULL (error), -1 (timeout), or 0+ per the MySQL manual |
||
818 | $status = ( $row[0] !== null ) ? intval( $row[0] ) : null; |
||
819 | if ( $status === null ) { |
||
820 | // T126436: jobs programmed to wait on master positions might be referencing binlogs |
||
821 | // with an old master hostname. Such calls make MASTER_POS_WAIT() return null. Try |
||
822 | // to detect this and treat the replica DB as having reached the position; a proper master |
||
823 | // switchover already requires that the new master be caught up before the switch. |
||
824 | $replicationPos = $this->getReplicaPos(); |
||
825 | if ( $replicationPos && !$replicationPos->channelsMatch( $pos ) ) { |
||
826 | $this->lastKnownReplicaPos = $replicationPos; |
||
827 | $status = 0; |
||
828 | } |
||
829 | } elseif ( $status >= 0 ) { |
||
830 | // Remember that this position was reached to save queries next time |
||
831 | $this->lastKnownReplicaPos = $pos; |
||
832 | } |
||
833 | |||
834 | return $status; |
||
835 | } |
||
836 | |||
837 | /** |
||
838 | * Get the position of the master from SHOW SLAVE STATUS |
||
839 | * |
||
840 | * @return MySQLMasterPos|bool |
||
841 | */ |
||
842 | function getReplicaPos() { |
||
843 | $res = $this->query( 'SHOW SLAVE STATUS', __METHOD__ ); |
||
844 | $row = $this->fetchObject( $res ); |
||
845 | |||
846 | if ( $row ) { |
||
847 | $pos = isset( $row->Exec_master_log_pos ) |
||
848 | ? $row->Exec_master_log_pos |
||
849 | : $row->Exec_Master_Log_Pos; |
||
850 | // Also fetch the last-applied GTID set (MariaDB) |
||
851 | View Code Duplication | if ( $this->useGTIDs ) { |
|
852 | $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_slave_pos'", __METHOD__ ); |
||
853 | $gtidRow = $this->fetchObject( $res ); |
||
854 | $gtidSet = $gtidRow ? $gtidRow->Value : ''; |
||
855 | } else { |
||
856 | $gtidSet = ''; |
||
857 | } |
||
858 | |||
859 | return new MySQLMasterPos( $row->Relay_Master_Log_File, $pos, $gtidSet ); |
||
860 | } else { |
||
861 | return false; |
||
862 | } |
||
863 | } |
||
864 | |||
865 | /** |
||
866 | * Get the position of the master from SHOW MASTER STATUS |
||
867 | * |
||
868 | * @return MySQLMasterPos|bool |
||
869 | */ |
||
870 | function getMasterPos() { |
||
871 | $res = $this->query( 'SHOW MASTER STATUS', __METHOD__ ); |
||
872 | $row = $this->fetchObject( $res ); |
||
873 | |||
874 | if ( $row ) { |
||
875 | // Also fetch the last-written GTID set (MariaDB) |
||
876 | View Code Duplication | if ( $this->useGTIDs ) { |
|
877 | $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'gtid_binlog_pos'", __METHOD__ ); |
||
878 | $gtidRow = $this->fetchObject( $res ); |
||
879 | $gtidSet = $gtidRow ? $gtidRow->Value : ''; |
||
880 | } else { |
||
881 | $gtidSet = ''; |
||
882 | } |
||
883 | |||
884 | return new MySQLMasterPos( $row->File, $row->Position, $gtidSet ); |
||
885 | } else { |
||
886 | return false; |
||
887 | } |
||
888 | } |
||
889 | |||
890 | public function serverIsReadOnly() { |
||
891 | $res = $this->query( "SHOW GLOBAL VARIABLES LIKE 'read_only'", __METHOD__ ); |
||
892 | $row = $this->fetchObject( $res ); |
||
893 | |||
894 | return $row ? ( strtolower( $row->Value ) === 'on' ) : false; |
||
895 | } |
||
896 | |||
897 | /** |
||
898 | * @param string $index |
||
899 | * @return string |
||
900 | */ |
||
901 | function useIndexClause( $index ) { |
||
902 | return "FORCE INDEX (" . $this->indexName( $index ) . ")"; |
||
903 | } |
||
904 | |||
905 | /** |
||
906 | * @param string $index |
||
907 | * @return string |
||
908 | */ |
||
909 | function ignoreIndexClause( $index ) { |
||
910 | return "IGNORE INDEX (" . $this->indexName( $index ) . ")"; |
||
911 | } |
||
912 | |||
913 | /** |
||
914 | * @return string |
||
915 | */ |
||
916 | function lowPriorityOption() { |
||
917 | return 'LOW_PRIORITY'; |
||
918 | } |
||
919 | |||
920 | /** |
||
921 | * @return string |
||
922 | */ |
||
923 | public function getSoftwareLink() { |
||
924 | // MariaDB includes its name in its version string; this is how MariaDB's version of |
||
925 | // the mysql command-line client identifies MariaDB servers (see mariadb_connection() |
||
926 | // in libmysql/libmysql.c). |
||
927 | $version = $this->getServerVersion(); |
||
928 | if ( strpos( $version, 'MariaDB' ) !== false || strpos( $version, '-maria-' ) !== false ) { |
||
929 | return '[{{int:version-db-mariadb-url}} MariaDB]'; |
||
930 | } |
||
931 | |||
932 | // Percona Server's version suffix is not very distinctive, and @@version_comment |
||
933 | // doesn't give the necessary info for source builds, so assume the server is MySQL. |
||
934 | // (Even Percona's version of mysql doesn't try to make the distinction.) |
||
935 | return '[{{int:version-db-mysql-url}} MySQL]'; |
||
936 | } |
||
937 | |||
938 | /** |
||
939 | * @return string |
||
940 | */ |
||
941 | public function getServerVersion() { |
||
942 | // Not using mysql_get_server_info() or similar for consistency: in the handshake, |
||
943 | // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip |
||
944 | // it off (see RPL_VERSION_HACK in include/mysql_com.h). |
||
945 | if ( $this->serverVersion === null ) { |
||
946 | $this->serverVersion = $this->selectField( '', 'VERSION()', '', __METHOD__ ); |
||
947 | } |
||
948 | return $this->serverVersion; |
||
949 | } |
||
950 | |||
951 | /** |
||
952 | * @param array $options |
||
953 | */ |
||
954 | public function setSessionOptions( array $options ) { |
||
955 | if ( isset( $options['connTimeout'] ) ) { |
||
956 | $timeout = (int)$options['connTimeout']; |
||
957 | $this->query( "SET net_read_timeout=$timeout" ); |
||
958 | $this->query( "SET net_write_timeout=$timeout" ); |
||
959 | } |
||
960 | } |
||
961 | |||
962 | /** |
||
963 | * @param string $sql |
||
964 | * @param string $newLine |
||
965 | * @return bool |
||
966 | */ |
||
967 | public function streamStatementEnd( &$sql, &$newLine ) { |
||
968 | if ( strtoupper( substr( $newLine, 0, 9 ) ) == 'DELIMITER' ) { |
||
969 | preg_match( '/^DELIMITER\s+(\S+)/', $newLine, $m ); |
||
970 | $this->delimiter = $m[1]; |
||
971 | $newLine = ''; |
||
972 | } |
||
973 | |||
974 | return parent::streamStatementEnd( $sql, $newLine ); |
||
975 | } |
||
976 | |||
977 | /** |
||
978 | * Check to see if a named lock is available. This is non-blocking. |
||
979 | * |
||
980 | * @param string $lockName Name of lock to poll |
||
981 | * @param string $method Name of method calling us |
||
982 | * @return bool |
||
983 | * @since 1.20 |
||
984 | */ |
||
985 | View Code Duplication | public function lockIsFree( $lockName, $method ) { |
|
986 | $encName = $this->addQuotes( $this->makeLockName( $lockName ) ); |
||
987 | $result = $this->query( "SELECT IS_FREE_LOCK($encName) AS lockstatus", $method ); |
||
988 | $row = $this->fetchObject( $result ); |
||
989 | |||
990 | return ( $row->lockstatus == 1 ); |
||
991 | } |
||
992 | |||
993 | /** |
||
994 | * @param string $lockName |
||
995 | * @param string $method |
||
996 | * @param int $timeout |
||
997 | * @return bool |
||
998 | */ |
||
999 | View Code Duplication | public function lock( $lockName, $method, $timeout = 5 ) { |
|
1000 | $encName = $this->addQuotes( $this->makeLockName( $lockName ) ); |
||
1001 | $result = $this->query( "SELECT GET_LOCK($encName, $timeout) AS lockstatus", $method ); |
||
1002 | $row = $this->fetchObject( $result ); |
||
1003 | |||
1004 | if ( $row->lockstatus == 1 ) { |
||
1005 | parent::lock( $lockName, $method, $timeout ); // record |
||
1006 | return true; |
||
1007 | } |
||
1008 | |||
1009 | $this->queryLogger->warning( __METHOD__ . " failed to acquire lock '$lockName'\n" ); |
||
1010 | |||
1011 | return false; |
||
1012 | } |
||
1013 | |||
1014 | /** |
||
1015 | * FROM MYSQL DOCS: |
||
1016 | * http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock |
||
1017 | * @param string $lockName |
||
1018 | * @param string $method |
||
1019 | * @return bool |
||
1020 | */ |
||
1021 | View Code Duplication | public function unlock( $lockName, $method ) { |
|
1022 | $encName = $this->addQuotes( $this->makeLockName( $lockName ) ); |
||
1023 | $result = $this->query( "SELECT RELEASE_LOCK($encName) as lockstatus", $method ); |
||
1024 | $row = $this->fetchObject( $result ); |
||
1025 | |||
1026 | if ( $row->lockstatus == 1 ) { |
||
1027 | parent::unlock( $lockName, $method ); // record |
||
1028 | return true; |
||
1029 | } |
||
1030 | |||
1031 | $this->queryLogger->warning( __METHOD__ . " failed to release lock '$lockName'\n" ); |
||
1032 | |||
1033 | return false; |
||
1034 | } |
||
1035 | |||
1036 | private function makeLockName( $lockName ) { |
||
1037 | // http://dev.mysql.com/doc/refman/5.7/en/miscellaneous-functions.html#function_get-lock |
||
1038 | // Newer version enforce a 64 char length limit. |
||
1039 | return ( strlen( $lockName ) > 64 ) ? sha1( $lockName ) : $lockName; |
||
1040 | } |
||
1041 | |||
1042 | public function namedLocksEnqueue() { |
||
1043 | return true; |
||
1044 | } |
||
1045 | |||
1046 | /** |
||
1047 | * @param array $read |
||
1048 | * @param array $write |
||
1049 | * @param string $method |
||
1050 | * @param bool $lowPriority |
||
1051 | * @return bool |
||
1052 | */ |
||
1053 | public function lockTables( $read, $write, $method, $lowPriority = true ) { |
||
1054 | $items = []; |
||
1055 | |||
1056 | foreach ( $write as $table ) { |
||
1057 | $tbl = $this->tableName( $table ) . |
||
1058 | ( $lowPriority ? ' LOW_PRIORITY' : '' ) . |
||
1059 | ' WRITE'; |
||
1060 | $items[] = $tbl; |
||
1061 | } |
||
1062 | foreach ( $read as $table ) { |
||
1063 | $items[] = $this->tableName( $table ) . ' READ'; |
||
1064 | } |
||
1065 | $sql = "LOCK TABLES " . implode( ',', $items ); |
||
1066 | $this->query( $sql, $method ); |
||
1067 | |||
1068 | return true; |
||
1069 | } |
||
1070 | |||
1071 | /** |
||
1072 | * @param string $method |
||
1073 | * @return bool |
||
1074 | */ |
||
1075 | public function unlockTables( $method ) { |
||
1076 | $this->query( "UNLOCK TABLES", $method ); |
||
1077 | |||
1078 | return true; |
||
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, __METHOD__ . ' 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, self::LIST_AND ); |
||
1122 | } |
||
1123 | |||
1124 | return $this->query( $sql, $fname ); |
||
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, self::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 | function wasErrorReissuable() { |
||
1190 | return $this->lastErrno() == 2013 || $this->lastErrno() == 2006; |
||
1191 | } |
||
1192 | |||
1193 | /** |
||
1194 | * Determines if the last failure was due to the database being read-only. |
||
1195 | * |
||
1196 | * @return bool |
||
1197 | */ |
||
1198 | function wasReadOnlyError() { |
||
1199 | return $this->lastErrno() == 1223 || |
||
1200 | ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false ); |
||
1201 | } |
||
1202 | |||
1203 | function wasConnectionError( $errno ) { |
||
1204 | return $errno == 2013 || $errno == 2006; |
||
1205 | } |
||
1206 | |||
1207 | /** |
||
1208 | * @param string $oldName |
||
1209 | * @param string $newName |
||
1210 | * @param bool $temporary |
||
1211 | * @param string $fname |
||
1212 | * @return bool |
||
1213 | */ |
||
1214 | function duplicateTableStructure( $oldName, $newName, $temporary = false, $fname = __METHOD__ ) { |
||
1215 | $tmp = $temporary ? 'TEMPORARY ' : ''; |
||
1216 | $newName = $this->addIdentifierQuotes( $newName ); |
||
1217 | $oldName = $this->addIdentifierQuotes( $oldName ); |
||
1218 | $query = "CREATE $tmp TABLE $newName (LIKE $oldName)"; |
||
1219 | |||
1220 | return $this->query( $query, $fname ); |
||
1221 | } |
||
1222 | |||
1223 | /** |
||
1224 | * List all tables on the database |
||
1225 | * |
||
1226 | * @param string $prefix Only show tables with this prefix, e.g. mw_ |
||
1227 | * @param string $fname Calling function name |
||
1228 | * @return array |
||
1229 | */ |
||
1230 | function listTables( $prefix = null, $fname = __METHOD__ ) { |
||
1231 | $result = $this->query( "SHOW TABLES", $fname ); |
||
1232 | |||
1233 | $endArray = []; |
||
1234 | |||
1235 | View Code Duplication | foreach ( $result as $table ) { |
|
1236 | $vars = get_object_vars( $table ); |
||
1237 | $table = array_pop( $vars ); |
||
1238 | |||
1239 | if ( !$prefix || strpos( $table, $prefix ) === 0 ) { |
||
1240 | $endArray[] = $table; |
||
1241 | } |
||
1242 | } |
||
1243 | |||
1244 | return $endArray; |
||
1245 | } |
||
1246 | |||
1247 | /** |
||
1248 | * @param string $tableName |
||
1249 | * @param string $fName |
||
1250 | * @return bool|ResultWrapper |
||
1251 | */ |
||
1252 | View Code Duplication | public function dropTable( $tableName, $fName = __METHOD__ ) { |
|
1253 | if ( !$this->tableExists( $tableName, $fName ) ) { |
||
1254 | return false; |
||
1255 | } |
||
1256 | |||
1257 | return $this->query( "DROP TABLE IF EXISTS " . $this->tableName( $tableName ), $fName ); |
||
1258 | } |
||
1259 | |||
1260 | /** |
||
1261 | * Get status information from SHOW STATUS in an associative array |
||
1262 | * |
||
1263 | * @param string $which |
||
1264 | * @return array |
||
1265 | */ |
||
1266 | function getMysqlStatus( $which = "%" ) { |
||
1267 | $res = $this->query( "SHOW STATUS LIKE '{$which}'" ); |
||
1268 | $status = []; |
||
1269 | |||
1270 | foreach ( $res as $row ) { |
||
0 ignored issues
–
show
|
|||
1271 | $status[$row->Variable_name] = $row->Value; |
||
1272 | } |
||
1273 | |||
1274 | return $status; |
||
1275 | } |
||
1276 | |||
1277 | /** |
||
1278 | * Lists VIEWs in the database |
||
1279 | * |
||
1280 | * @param string $prefix Only show VIEWs with this prefix, eg. |
||
1281 | * unit_test_, or $wgDBprefix. Default: null, would return all views. |
||
1282 | * @param string $fname Name of calling function |
||
1283 | * @return array |
||
1284 | * @since 1.22 |
||
1285 | */ |
||
1286 | public function listViews( $prefix = null, $fname = __METHOD__ ) { |
||
1287 | // The name of the column containing the name of the VIEW |
||
1288 | $propertyName = 'Tables_in_' . $this->mDBname; |
||
1289 | |||
1290 | // Query for the VIEWS |
||
1291 | $res = $this->query( 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"' ); |
||
1292 | $allViews = []; |
||
1293 | foreach ( $res as $row ) { |
||
1294 | array_push( $allViews, $row->$propertyName ); |
||
1295 | } |
||
1296 | |||
1297 | if ( is_null( $prefix ) || $prefix === '' ) { |
||
1298 | return $allViews; |
||
1299 | } |
||
1300 | |||
1301 | $filteredViews = []; |
||
1302 | foreach ( $allViews as $viewName ) { |
||
1303 | // Does the name of this VIEW start with the table-prefix? |
||
1304 | if ( strpos( $viewName, $prefix ) === 0 ) { |
||
1305 | array_push( $filteredViews, $viewName ); |
||
1306 | } |
||
1307 | } |
||
1308 | |||
1309 | return $filteredViews; |
||
1310 | } |
||
1311 | |||
1312 | /** |
||
1313 | * Differentiates between a TABLE and a VIEW. |
||
1314 | * |
||
1315 | * @param string $name Name of the TABLE/VIEW to test |
||
1316 | * @param string $prefix |
||
1317 | * @return bool |
||
1318 | * @since 1.22 |
||
1319 | */ |
||
1320 | public function isView( $name, $prefix = null ) { |
||
1321 | return in_array( $name, $this->listViews( $prefix ) ); |
||
1322 | } |
||
1323 | } |
||
1324 | |||
1325 |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
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:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.