MediaWiki REL1_41
DatabaseMySQL.php
Go to the documentation of this file.
1<?php
20namespace Wikimedia\Rdbms;
21
22use mysqli;
23use mysqli_result;
24use RuntimeException;
25use Wikimedia\AtEase\AtEase;
26use Wikimedia\IPUtils;
30
40class DatabaseMySQL extends Database {
42 private $sslKeyPath;
44 private $sslCertPath;
46 private $sslCAFile;
48 private $sslCAPath;
54 private $sslCiphers;
56 private $utf8Mode;
57
59 protected $platform;
60
64 private $sessionLastAutoRowId;
65
85 public function __construct( array $params ) {
86 foreach ( [ 'KeyPath', 'CertPath', 'CAFile', 'CAPath', 'Ciphers' ] as $name ) {
87 $var = "ssl{$name}";
88 if ( isset( $params[$var] ) ) {
89 $this->$var = $params[$var];
90 }
91 }
92 $this->utf8Mode = !empty( $params['utf8Mode'] );
93 parent::__construct( $params );
94 $this->platform = new MySQLPlatform(
95 $this,
96 $this->logger,
97 $this->currentDomain,
98 $this->errorLogger
99 );
100 $this->replicationReporter = new MysqlReplicationReporter(
101 $params['topologyRole'],
102 $this->logger,
103 $params['srvCache'],
104 $params['lagDetectionMethod'] ?? 'Seconds_Behind_Master',
105 $params['lagDetectionOptions'] ?? [],
106 !empty( $params['useGTIDs' ] )
107 );
108 }
109
113 public function getType() {
114 return 'mysql';
115 }
116
117 protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
118 $this->close( __METHOD__ );
119
120 if ( $schema !== null ) {
121 throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
122 }
123
124 $this->installErrorHandler();
125 try {
126 $this->conn = $this->mysqlConnect( $server, $user, $password, $db );
127 } catch ( RuntimeException $e ) {
128 $this->restoreErrorHandler();
129 throw $this->newExceptionAfterConnectError( $e->getMessage() );
130 }
131 $error = $this->restoreErrorHandler();
132
133 if ( !$this->conn ) {
134 throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() );
135 }
136
137 try {
138 $this->currentDomain = new DatabaseDomain(
139 $db && strlen( $db ) ? $db : null,
140 null,
141 $tablePrefix
142 );
143 $this->platform->setPrefix( $tablePrefix );
144
145 $set = [];
146 if ( !$this->flagsHolder->getFlag( self::DBO_GAUGE ) ) {
147 // Abstract over any excessive MySQL defaults
148 $set[] = 'group_concat_max_len = 262144';
149 // Set any custom settings defined by site config
150 // https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html
151 foreach ( $this->connectionVariables as $var => $val ) {
152 // Escape strings but not numbers to avoid MySQL complaining
153 if ( !is_int( $val ) && !is_float( $val ) ) {
154 $val = $this->addQuotes( $val );
155 }
156 $set[] = $this->platform->addIdentifierQuotes( $var ) . ' = ' . $val;
157 }
158 }
159
160 if ( $set ) {
161 $sql = 'SET ' . implode( ', ', $set );
162 $flags = self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX;
163 $query = new Query( $sql, $flags, 'SET' );
164 // Avoid using query() so that replaceLostConnection() does not throw
165 // errors if the transaction status is STATUS_TRX_ERROR
166 $qs = $this->executeQuery( $query, __METHOD__, $flags );
167 if ( $qs->res === false ) {
168 $this->reportQueryError( $qs->message, $qs->code, $sql, __METHOD__ );
169 }
170 }
171 } catch ( RuntimeException $e ) {
172 throw $this->newExceptionAfterConnectError( $e->getMessage() );
173 }
174 }
175
176 protected function doSelectDomain( DatabaseDomain $domain ) {
177 if ( $domain->getSchema() !== null ) {
178 throw new DBExpectedError(
179 $this,
180 __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
181 );
182 }
183
184 $database = $domain->getDatabase();
185 // A null database means "don't care" so leave it as is and update the table prefix
186 if ( $database === null ) {
187 $this->currentDomain = new DatabaseDomain(
188 $this->currentDomain->getDatabase(),
189 null,
190 $domain->getTablePrefix()
191 );
192 $this->platform->setPrefix( $domain->getTablePrefix() );
193
194 return true;
195 }
196
197 if ( $database !== $this->getDBname() ) {
198 $sql = 'USE ' . $this->addIdentifierQuotes( $database );
199 $query = new Query( $sql, self::QUERY_CHANGE_TRX, 'USE' );
200 $qs = $this->executeQuery( $query, __METHOD__, self::QUERY_CHANGE_TRX );
201 if ( $qs->res === false ) {
202 $this->reportQueryError( $qs->message, $qs->code, $sql, __METHOD__ );
203 return false; // unreachable
204 }
205 }
206
207 // Update that domain fields on success (no exception thrown)
208 $this->currentDomain = $domain;
209 $this->platform->setPrefix( $domain->getTablePrefix() );
210
211 return true;
212 }
213
217 public function lastError() {
218 if ( $this->conn ) {
219 // Even if it's non-zero, it can still be invalid
220 $error = $this->mysqlError( $this->conn );
221 if ( !$error ) {
222 $error = $this->mysqlError();
223 }
224 } else {
225 $error = $this->mysqlError() ?: $this->lastConnectError;
226 }
227
228 return $error;
229 }
230
231 protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
232 $row = $this->replicationReporter->getReplicationSafetyInfo( $this );
233 // For row-based-replication, the resulting changes will be relayed, not the query
234 if ( $row->binlog_format === 'ROW' ) {
235 return true;
236 }
237 // LIMIT requires ORDER BY on a unique key or it is non-deterministic
238 if ( isset( $selectOptions['LIMIT'] ) ) {
239 return false;
240 }
241 // In MySQL, an INSERT SELECT is only replication safe with row-based
242 // replication or if innodb_autoinc_lock_mode is 0. When those
243 // conditions aren't met, use non-native mode.
244 // While we could try to determine if the insert is safe anyway by
245 // checking if the target table has an auto-increment column that
246 // isn't set in $varMap, that seems unlikely to be worth the extra
247 // complexity.
248 return (
249 in_array( 'NO_AUTO_COLUMNS', $insertOptions ) ||
250 (int)$row->innodb_autoinc_lock_mode === 0
251 );
252 }
253
267 public function estimateRowCount(
268 $tables,
269 $var = '*',
270 $conds = '',
271 $fname = __METHOD__,
272 $options = [],
273 $join_conds = []
274 ) {
275 $conds = $this->platform->normalizeConditions( $conds, $fname );
276 $column = $this->platform->extractSingleFieldFromList( $var );
277 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
278 $conds[] = "$column IS NOT NULL";
279 }
280
281 $options['EXPLAIN'] = true;
282 $res = $this->select( $tables, $var, $conds, $fname, $options, $join_conds );
283 if ( $res === false ) {
284 return false;
285 }
286 if ( !$res->numRows() ) {
287 return 0;
288 }
289
290 $rows = 1;
291 foreach ( $res as $plan ) {
292 $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
293 }
294
295 return (int)$rows;
296 }
297
298 public function tableExists( $table, $fname = __METHOD__ ) {
299 // Split database and table into proper variables as Database::tableName() returns
300 // shared tables prefixed with their database, which do not work in SHOW TABLES statements
301 [ $database, , $prefix, $table ] = $this->platform->qualifiedTableComponents( $table );
302 $tableName = "{$prefix}{$table}";
303
304 if ( isset( $this->sessionTempTables[$tableName] ) ) {
305 return true; // already known to exist and won't show in SHOW TABLES anyway
306 }
307
308 // We can't use buildLike() here, because it specifies an escape character
309 // other than the backslash, which is the only one supported by SHOW TABLES
310 // TODO: Avoid using platform's internal methods
311 $encLike = $this->platform->escapeLikeInternal( $tableName, '\\' );
312
313 // If the database has been specified (such as for shared tables), use "FROM"
314 if ( $database !== '' ) {
315 $encDatabase = $this->platform->addIdentifierQuotes( $database );
316 $sql = "SHOW TABLES FROM $encDatabase LIKE '$encLike'";
317 } else {
318 $sql = "SHOW TABLES LIKE '$encLike'";
319 }
320
321 $query = new Query( $sql, self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, 'SHOW', $table );
322 $res = $this->query( $query, $fname );
323
324 return $res->numRows() > 0;
325 }
326
332 public function fieldInfo( $table, $field ) {
333 $query = new Query(
334 "SELECT * FROM " . $this->tableName( $table ) . " LIMIT 1",
335 self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
336 'SELECT',
337 $table
338 );
339 $res = $this->query( $query, __METHOD__ );
340 if ( !$res ) {
341 return false;
342 }
344 '@phan-var MysqliResultWrapper $res';
345 return $res->getInternalFieldInfo( $field );
346 }
347
357 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
358 # https://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
359 $index = $this->platform->indexName( $index );
360 $query = new Query(
361 'SHOW INDEX FROM ' . $this->tableName( $table ),
362 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
363 'SHOW',
364 $table
365 );
366 $res = $this->query( $query, $fname );
367
368 if ( !$res ) {
369 return null;
370 }
371
372 $result = [];
373
374 foreach ( $res as $row ) {
375 if ( $row->Key_name == $index ) {
376 $result[] = $row;
377 }
378 }
379
380 return $result ?: false;
381 }
382
387 public function strencode( $s ) {
388 return $this->mysqlRealEscapeString( $s );
389 }
390
391 public function serverIsReadOnly() {
392 // Avoid SHOW to avoid internal temporary tables
393 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
394 $query = new Query( "SELECT @@GLOBAL.read_only AS Value", $flags, 'SELECT' );
395 $res = $this->query( $query, __METHOD__ );
396 $row = $res->fetchObject();
397
398 return $row && (bool)$row->Value;
399 }
400
404 public function getSoftwareLink() {
405 [ $variant ] = $this->getMySqlServerVariant();
406 if ( $variant === 'MariaDB' ) {
407 return '[{{int:version-db-mariadb-url}} MariaDB]';
408 }
409
410 return '[{{int:version-db-mysql-url}} MySQL]';
411 }
412
416 private function getMySqlServerVariant() {
417 $version = $this->getServerVersion();
418
419 // MariaDB includes its name in its version string; this is how MariaDB's version of
420 // the mysql command-line client identifies MariaDB servers.
421 // https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_version
422 // https://mariadb.com/kb/en/version/
423 $parts = explode( '-', $version, 2 );
424 $number = $parts[0];
425 $suffix = $parts[1] ?? '';
426 if ( strpos( $suffix, 'MariaDB' ) !== false || strpos( $suffix, '-maria-' ) !== false ) {
427 $vendor = 'MariaDB';
428 } else {
429 $vendor = 'MySQL';
430 }
431
432 return [ $vendor, $number ];
433 }
434
438 public function getServerVersion() {
439 // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
440 // it off (see RPL_VERSION_HACK in include/mysql_com.h).
441 $version = $this->conn->server_info;
442 if (
443 str_starts_with( $version, '5.5.5-' ) &&
444 ( str_contains( $version, 'MariaDB' ) || str_contains( $version, '-maria-' ) )
445 ) {
446 $version = substr( $version, strlen( '5.5.5-' ) );
447 }
448 return $version;
449 }
450
454 public function setSessionOptions( array $options ) {
455 $sqlAssignments = [];
456
457 if ( isset( $options['connTimeout'] ) ) {
458 $encTimeout = (int)$options['connTimeout'];
459 $sqlAssignments[] = "net_read_timeout=$encTimeout";
460 $sqlAssignments[] = "net_write_timeout=$encTimeout";
461 }
462 if ( isset( $options['groupConcatMaxLen'] ) ) {
463 $maxLength = (int)$options['groupConcatMaxLen'];
464 $sqlAssignments[] = "group_concat_max_len=$maxLength";
465 }
466
467 if ( $sqlAssignments ) {
468 $query = new Query(
469 'SET ' . implode( ', ', $sqlAssignments ),
470 self::QUERY_CHANGE_TRX | self::QUERY_CHANGE_NONE,
471 'SET'
472 );
473 $this->query( $query, __METHOD__ );
474 }
475 }
476
482 public function streamStatementEnd( &$sql, &$newLine ) {
483 if ( preg_match( '/^DELIMITER\s+(\S+)/i', $newLine, $m ) ) {
484 $this->delimiter = $m[1];
485 $newLine = '';
486 }
487
488 return parent::streamStatementEnd( $sql, $newLine );
489 }
490
491 public function doLockIsFree( string $lockName, string $method ) {
492 $query = new Query( $this->platform->lockIsFreeSQLText( $lockName ), self::QUERY_CHANGE_LOCKS, 'SELECT' );
493 $res = $this->query( $query, $method );
494 $row = $res->fetchObject();
495
496 return ( $row->unlocked == 1 );
497 }
498
499 public function doLock( string $lockName, string $method, int $timeout ) {
500 $query = new Query( $this->platform->lockSQLText( $lockName, $timeout ), self::QUERY_CHANGE_LOCKS, 'SELECT' );
501 $res = $this->query( $query, $method );
502 $row = $res->fetchObject();
503
504 return ( $row->acquired !== null ) ? (float)$row->acquired : null;
505 }
506
507 public function doUnlock( string $lockName, string $method ) {
508 $query = new Query( $this->platform->unlockSQLText( $lockName ), self::QUERY_CHANGE_LOCKS, 'SELECT' );
509 $res = $this->query( $query, $method );
510 $row = $res->fetchObject();
511
512 return ( $row->released == 1 );
513 }
514
515 public function namedLocksEnqueue() {
516 return true;
517 }
518
519 protected function doFlushSession( $fname ) {
520 // Note that RELEASE_ALL_LOCKS() is not supported well enough to use here.
521 // https://mariadb.com/kb/en/release_all_locks/
522 $releaseLockFields = [];
523 foreach ( $this->sessionNamedLocks as $name => $info ) {
524 $encName = $this->addQuotes( $this->platform->makeLockName( $name ) );
525 $releaseLockFields[] = "RELEASE_LOCK($encName)";
526 }
527 if ( $releaseLockFields ) {
528 $sql = 'SELECT ' . implode( ',', $releaseLockFields );
529 $flags = self::QUERY_CHANGE_LOCKS | self::QUERY_NO_RETRY;
530 $query = new Query( $sql, $flags, 'SELECT' );
531 $qs = $this->executeQuery( $query, __METHOD__, $flags );
532 if ( $qs->res === false ) {
533 $this->reportQueryError( $qs->message, $qs->code, $sql, $fname, true );
534 }
535 }
536 }
537
538 public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__ ) {
539 $identityKey = $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
540 if ( !$rows ) {
541 return true;
542 }
543 $this->platform->assertValidUpsertSetArray( $set, $identityKey, $rows );
544
545 $encTable = $this->tableName( $table );
546 [ $sqlColumns, $sqlTuples ] = $this->platform->makeInsertLists( $rows );
547 $sqlColumnAssignments = $this->makeList( $set, self::LIST_SET );
548 // No need to expose __NEW.* since buildExcludedValue() uses VALUES(column)
549
550 // https://mariadb.com/kb/en/insert-on-duplicate-key-update/
551 // https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
552 $sql =
553 "INSERT INTO $encTable " .
554 "($sqlColumns) VALUES $sqlTuples " .
555 "ON DUPLICATE KEY UPDATE $sqlColumnAssignments";
556 $query = new Query( $sql, self::QUERY_CHANGE_ROWS, 'INSERT', $table );
557 $this->query( $query, $fname );
558 // Count updates of conflicting rows and row inserts equally toward the change count
559 $this->lastQueryAffectedRows = min( $this->lastQueryAffectedRows, count( $rows ) );
560 return true;
561 }
562
563 public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
564 $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
565 if ( !$rows ) {
566 return;
567 }
568 $encTable = $this->tableName( $table );
569 [ $sqlColumns, $sqlTuples ] = $this->platform->makeInsertLists( $rows );
570 // https://dev.mysql.com/doc/refman/8.0/en/replace.html
571 $sql = "REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples";
572 // Note that any auto-increment columns on conflicting rows will be reassigned
573 // due to combined DELETE+INSERT semantics. This will be reflected in insertId().
574 $query = new Query( $sql, self::QUERY_CHANGE_ROWS, 'REPLACE', $table );
575 $this->query( $query, $fname );
576 // Do not count deletions of conflicting rows toward the change count
577 $this->lastQueryAffectedRows = min( $this->lastQueryAffectedRows, count( $rows ) );
578 }
579
585 public function wasDeadlock() {
586 return $this->lastErrno() == 1213;
587 }
588
594 public function wasReadOnlyError() {
595 return $this->lastErrno() == 1223 ||
596 ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
597 }
598
599 protected function isConnectionError( $errno ) {
600 // https://mariadb.com/kb/en/mariadb-error-codes/
601 // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
602 // https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html
603 return in_array( $errno, [ 2013, 2006, 2003, 1927, 1053 ], true );
604 }
605
606 protected function isQueryTimeoutError( $errno ) {
607 // https://mariadb.com/kb/en/mariadb-error-codes/
608 // https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html
609 // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
610 return in_array( $errno, [ 3024, 2062, 1969, 1028 ], true );
611 }
612
613 protected function isKnownStatementRollbackError( $errno ) {
614 // https://mariadb.com/kb/en/mariadb-error-codes/
615 // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
616 return in_array(
617 $errno,
618 [ 3024, 1969, 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ],
619 true
620 );
621 }
622
630 public function duplicateTableStructure(
631 $oldName, $newName, $temporary = false, $fname = __METHOD__
632 ) {
633 $tmp = $temporary ? 'TEMPORARY ' : '';
634 $newNameQuoted = $this->addIdentifierQuotes( $newName );
635 $oldNameQuoted = $this->addIdentifierQuotes( $oldName );
636
637 $query = new Query(
638 "CREATE $tmp TABLE $newNameQuoted (LIKE $oldNameQuoted)",
639 self::QUERY_PSEUDO_PERMANENT | self::QUERY_CHANGE_SCHEMA,
640 'CREATE',
641 [ $oldName, $newName ]
642 );
643 return $this->query( $query, $fname );
644 }
645
653 public function listTables( $prefix = null, $fname = __METHOD__ ) {
654 $query = new Query( "SHOW TABLES", self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE, 'SHOW' );
655 $result = $this->query( $query, $fname );
656
657 $endArray = [];
658
659 foreach ( $result as $table ) {
660 $vars = get_object_vars( $table );
661 $table = array_pop( $vars );
662
663 if ( !$prefix || strpos( $table, $prefix ) === 0 ) {
664 $endArray[] = $table;
665 }
666 }
667
668 return $endArray;
669 }
670
680 public function listViews( $prefix = null, $fname = __METHOD__ ) {
681 // The name of the column containing the name of the VIEW
682 $propertyName = 'Tables_in_' . $this->getDBname();
683 $query = new Query(
684 'SHOW FULL TABLES WHERE TABLE_TYPE = "VIEW"',
685 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
686 'SHOW'
687 );
688 // Query for the VIEWS
689 $res = $this->query( $query, $fname );
690
691 $allViews = [];
692 foreach ( $res as $row ) {
693 $allViews[] = $row->$propertyName;
694 }
695
696 if ( $prefix === null || $prefix === '' ) {
697 return $allViews;
698 }
699
700 $filteredViews = [];
701 foreach ( $allViews as $viewName ) {
702 // Does the name of this VIEW start with the table-prefix?
703 if ( strpos( $viewName, $prefix ) === 0 ) {
704 $filteredViews[] = $viewName;
705 }
706 }
707
708 return $filteredViews;
709 }
710
711 public function selectSQLText(
712 $table,
713 $vars,
714 $conds = '',
715 $fname = __METHOD__,
716 $options = [],
717 $join_conds = []
718 ) {
719 $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
720 // https://dev.mysql.com/doc/refman/5.7/en/optimizer-hints.html
721 // https://mariadb.com/kb/en/library/aborting-statements/
722 $timeoutMsec = intval( $options['MAX_EXECUTION_TIME'] ?? 0 );
723 if ( $timeoutMsec > 0 ) {
724 [ $vendor, $number ] = $this->getMySqlServerVariant();
725 if ( $vendor === 'MariaDB' && version_compare( $number, '10.1.2', '>=' ) ) {
726 $timeoutSec = $timeoutMsec / 1000;
727 $sql = "SET STATEMENT max_statement_time=$timeoutSec FOR $sql";
728 } elseif ( $vendor === 'MySQL' && version_compare( $number, '5.7.0', '>=' ) ) {
729 $sql = preg_replace(
730 '/^SELECT(?=\s)/',
731 "SELECT /*+ MAX_EXECUTION_TIME($timeoutMsec)*/",
732 $sql
733 );
734 }
735 }
736
737 return $sql;
738 }
739
740 protected function doSingleStatementQuery( string $sql ): QueryStatus {
741 $conn = $this->getBindingHandle();
742
743 // Hide packet warnings caused by things like dropped connections
744 AtEase::suppressWarnings();
745 $res = $conn->query( $sql );
746 AtEase::restoreWarnings();
747 // Note that mysqli::insert_id only reflects the last query statement
748 $insertId = (int)$conn->insert_id;
749 $this->lastQueryInsertId = $insertId;
750 $this->sessionLastAutoRowId = $insertId ?: $this->sessionLastAutoRowId;
751
752 return new QueryStatus(
753 $res instanceof mysqli_result ? new MysqliResultWrapper( $this, $res ) : $res,
754 $conn->affected_rows,
755 $conn->error,
756 $conn->errno
757 );
758 }
759
768 private function mysqlConnect( $server, $user, $password, $db ) {
769 if ( !function_exists( 'mysqli_init' ) ) {
770 throw $this->newExceptionAfterConnectError(
771 "MySQLi functions missing, have you compiled PHP with the --with-mysqli option?"
772 );
773 }
774
775 // PHP 8.1.0+ throws exceptions by default. Turn that off for consistency.
776 mysqli_report( MYSQLI_REPORT_OFF );
777
778 // Other than mysql_connect, mysqli_real_connect expects an explicit port number
779 // e.g. "localhost:1234" or "127.0.0.1:1234"
780 // or Unix domain socket path
781 // e.g. "localhost:/socket_path" or "localhost:/foo/bar:bar:bar"
782 // colons are known to be used by Google AppEngine,
783 // see <https://cloud.google.com/sql/docs/mysql/connect-app-engine>
784 //
785 // We need to parse the port or socket path out of $realServer
786 $port = null;
787 $socket = null;
788 $hostAndPort = IPUtils::splitHostAndPort( $server );
789 if ( $hostAndPort ) {
790 $realServer = $hostAndPort[0];
791 if ( $hostAndPort[1] ) {
792 $port = $hostAndPort[1];
793 }
794 } elseif ( substr_count( $server, ':/' ) == 1 ) {
795 // If we have a colon slash instead of a colon and a port number
796 // after the ip or hostname, assume it's the Unix domain socket path
797 [ $realServer, $socket ] = explode( ':', $server, 2 );
798 } else {
799 $realServer = $server;
800 }
801
802 $mysqli = mysqli_init();
803 // Make affectedRows() for UPDATE reflect the number of matching rows, regardless
804 // of whether any column values changed. This is what callers want to know and is
805 // consistent with what Postgres and SQLite return.
806 $flags = MYSQLI_CLIENT_FOUND_ROWS;
807 if ( $this->ssl ) {
808 $flags |= MYSQLI_CLIENT_SSL;
809 $mysqli->ssl_set(
810 $this->sslKeyPath,
811 $this->sslCertPath,
812 $this->sslCAFile,
813 $this->sslCAPath,
814 $this->sslCiphers
815 );
816 }
817 if ( $this->getFlag( self::DBO_COMPRESS ) ) {
818 $flags |= MYSQLI_CLIENT_COMPRESS;
819 }
820 if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
821 $realServer = 'p:' . $realServer;
822 }
823
824 if ( $this->utf8Mode ) {
825 // Tell the server we're communicating with it in UTF-8.
826 // This may engage various charset conversions.
827 $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'utf8' );
828 } else {
829 $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'binary' );
830 }
831
832 $mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, $this->connectTimeout ?: 3 );
833 if ( $this->receiveTimeout ) {
834 $mysqli->options( MYSQLI_OPT_READ_TIMEOUT, $this->receiveTimeout );
835 }
836
837 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal socket seems set when used
838 $ok = $mysqli->real_connect( $realServer, $user, $password, $db, $port, $socket, $flags );
839
840 return $ok ? $mysqli : null;
841 }
842
843 protected function closeConnection() {
844 return ( $this->conn instanceof mysqli ) ? mysqli_close( $this->conn ) : true;
845 }
846
847 protected function lastInsertId() {
848 return $this->sessionLastAutoRowId;
849 }
850
851 protected function doHandleSessionLossPreconnect() {
852 // https://mariadb.com/kb/en/last_insert_id/
853 $this->sessionLastAutoRowId = 0;
854 }
855
856 public function insertId() {
857 if ( $this->lastEmulatedInsertId === null ) {
858 $conn = $this->getBindingHandle();
859 // Note that mysqli::insert_id only reflects the last query statement
860 $this->lastEmulatedInsertId = (int)$conn->insert_id;
861 }
862
863 return $this->lastEmulatedInsertId;
864 }
865
869 public function lastErrno() {
870 if ( $this->conn instanceof mysqli ) {
871 return $this->conn->errno;
872 } else {
873 return mysqli_connect_errno();
874 }
875 }
876
881 private function mysqlError( $conn = null ) {
882 if ( $conn === null ) {
883 return (string)mysqli_connect_error();
884 } else {
885 return $conn->error;
886 }
887 }
888
889 private function mysqlRealEscapeString( $s ) {
890 $conn = $this->getBindingHandle();
891
892 return $conn->real_escape_string( (string)$s );
893 }
894}
895
899class_alias( DatabaseMySQL::class, 'DatabaseMysqlBase' );
900
904class_alias( DatabaseMySQL::class, 'DatabaseMysqli' );
Base class for the more common types of database errors.
Class to handle database/schema/prefix specifications for IDatabase.
MySQL database abstraction layer.
doLockIsFree(string $lockName, string $method)
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.
upsert( $table, array $rows, $uniqueKeys, array $set, $fname=__METHOD__)
Upsert row(s) into a table, in the provided order, while updating conflicting rows.
isQueryTimeoutError( $errno)
Checks whether the cause of the error is detected to be a timeout.
listViews( $prefix=null, $fname=__METHOD__)
Lists VIEWs in the database.
isInsertSelectSafe(array $insertOptions, array $selectOptions)
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
insertId()
Get the sequence-based ID assigned by the last query method call.
isConnectionError( $errno)
Do not use this method outside of Database/DBError classes.
namedLocksEnqueue()
Check to see if a named lock used by lock() use blocking queues.bool 1.26to override
wasDeadlock()
Determines if the last failure was due to a deadlock.
indexInfo( $table, $index, $fname=__METHOD__)
Get information about an index into an object Returns false if the index does not exist.
doFlushSession( $fname)
Reset the server-side session state for named locks and table locks.
MysqlReplicationReporter $replicationReporter
doUnlock(string $lockName, string $method)
__construct(array $params)
Additional $params include:
streamStatementEnd(&$sql, &$newLine)
doHandleSessionLossPreconnect()
Reset any additional subclass trx* and session* fields.
serverIsReadOnly()
bool Whether this DB server is running in server-side read-only mode query} 1.28to override
doSelectDomain(DatabaseDomain $domain)
estimateRowCount( $tables, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Estimate rows in dataset Returns estimated count, based on EXPLAIN output Takes same arguments as Dat...
lastInsertId()
Get a row ID from the last insert statement to implicitly assign one within the session.
selectSQLText( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Take the same arguments as IDatabase::select() and return the SQL it would use.
replace( $table, $uniqueKeys, $rows, $fname=__METHOD__)
Insert row(s) into a table, in the provided order, while deleting conflicting rows.
duplicateTableStructure( $oldName, $newName, $temporary=false, $fname=__METHOD__)
closeConnection()
Closes underlying database connection.
doSingleStatementQuery(string $sql)
Run a query and return a QueryStatus instance with the query result information.
wasReadOnlyError()
Determines if the last failure was due to the database being read-only.
open( $server, $user, $password, $db, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)
doLock(string $lockName, string $method, int $timeout)
Relational database abstraction object.
Definition Database.php:44
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition Database.php:442
object resource null $conn
Database connection.
Definition Database.php:65
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.string -taint none
newExceptionAfterConnectError( $error)
string $lastConnectError
Last error during connection; empty string if none.
Definition Database.php:110
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition Database.php:431
tableName( $name, $format='quoted')
Format a table name ready for use in constructing an SQL query.
addIdentifierQuotes( $s)
Escape a SQL identifier (e.g.
executeQuery( $sql, $fname, $flags)
Execute a query without enforcing public (non-Database) caller restrictions.
Definition Database.php:701
close( $fname=__METHOD__)
Close the database connection.
Definition Database.php:494
reportQueryError( $error, $errno, $sql, $fname, $ignore=false)
Report a query error.
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
query( $sql, $fname=__METHOD__, $flags=0)
Run an SQL query statement and return the result.
Definition Database.php:658
getBindingHandle()
Get the underlying binding connection handle.
getDBname()
Get the current database name; null if there isn't one.
Holds information on Query to be executed.
Definition Query.php:31