MediaWiki master
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
42class DatabaseMySQL extends Database {
44 private $sslKeyPath;
46 private $sslCertPath;
48 private $sslCAFile;
50 private $sslCAPath;
56 private $sslCiphers;
58 private $utf8Mode;
59
61 protected $platform;
62
66 private $sessionLastAutoRowId;
67
87 public function __construct( array $params ) {
88 foreach ( [ 'KeyPath', 'CertPath', 'CAFile', 'CAPath', 'Ciphers' ] as $name ) {
89 $var = "ssl{$name}";
90 if ( isset( $params[$var] ) ) {
91 $this->$var = $params[$var];
92 }
93 }
94 $this->utf8Mode = !empty( $params['utf8Mode'] );
95 parent::__construct( $params );
96 $this->platform = new MySQLPlatform(
97 $this,
98 $this->logger,
99 $this->currentDomain,
100 $this->errorLogger
101 );
102 $this->replicationReporter = new MysqlReplicationReporter(
103 $params['topologyRole'],
104 $this->logger,
105 $params['srvCache'],
106 $params['lagDetectionMethod'] ?? 'Seconds_Behind_Master',
107 $params['lagDetectionOptions'] ?? [],
108 !empty( $params['useGTIDs' ] )
109 );
110 }
111
115 public function getType() {
116 return 'mysql';
117 }
118
119 protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
120 $this->close( __METHOD__ );
121
122 if ( $schema !== null ) {
123 throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
124 }
125
126 $this->installErrorHandler();
127 try {
128 $this->conn = $this->mysqlConnect( $server, $user, $password, $db );
129 } catch ( RuntimeException $e ) {
130 $this->restoreErrorHandler();
131 throw $this->newExceptionAfterConnectError( $e->getMessage() );
132 }
133 $error = $this->restoreErrorHandler();
134
135 if ( !$this->conn ) {
136 throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() );
137 }
138
139 try {
140 $this->currentDomain = new DatabaseDomain(
141 $db && strlen( $db ) ? $db : null,
142 null,
143 $tablePrefix
144 );
145 $this->platform->setPrefix( $tablePrefix );
146
147 $set = [];
148 if ( !$this->flagsHolder->getFlag( self::DBO_GAUGE ) ) {
149 // Abstract over any excessive MySQL defaults
150 $set[] = 'group_concat_max_len = 262144';
151 // Set any custom settings defined by site config
152 // https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html
153 foreach ( $this->connectionVariables as $var => $val ) {
154 // Escape strings but not numbers to avoid MySQL complaining
155 if ( !is_int( $val ) && !is_float( $val ) ) {
156 $val = $this->addQuotes( $val );
157 }
158 $set[] = $this->platform->addIdentifierQuotes( $var ) . ' = ' . $val;
159 }
160 }
161
162 if ( $set ) {
163 $sql = 'SET ' . implode( ', ', $set );
164 $flags = self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX;
165 $query = new Query( $sql, $flags, 'SET' );
166 // Avoid using query() so that replaceLostConnection() does not throw
167 // errors if the transaction status is STATUS_TRX_ERROR
168 $qs = $this->executeQuery( $query, __METHOD__, $flags );
169 if ( $qs->res === false ) {
170 $this->reportQueryError( $qs->message, $qs->code, $sql, __METHOD__ );
171 }
172 }
173 } catch ( RuntimeException $e ) {
174 throw $this->newExceptionAfterConnectError( $e->getMessage() );
175 }
176 }
177
178 protected function doSelectDomain( DatabaseDomain $domain ) {
179 if ( $domain->getSchema() !== null ) {
180 throw new DBExpectedError(
181 $this,
182 __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
183 );
184 }
185
186 $database = $domain->getDatabase();
187 // A null database means "don't care" so leave it as is and update the table prefix
188 if ( $database === null ) {
189 $this->currentDomain = new DatabaseDomain(
190 $this->currentDomain->getDatabase(),
191 null,
192 $domain->getTablePrefix()
193 );
194 $this->platform->setPrefix( $domain->getTablePrefix() );
195
196 return true;
197 }
198
199 if ( $database !== $this->getDBname() ) {
200 $sql = 'USE ' . $this->addIdentifierQuotes( $database );
201 $query = new Query( $sql, self::QUERY_CHANGE_TRX, 'USE' );
202 $qs = $this->executeQuery( $query, __METHOD__, self::QUERY_CHANGE_TRX );
203 if ( $qs->res === false ) {
204 $this->reportQueryError( $qs->message, $qs->code, $sql, __METHOD__ );
205 return false; // unreachable
206 }
207 }
208
209 // Update that domain fields on success (no exception thrown)
210 $this->currentDomain = $domain;
211 $this->platform->setPrefix( $domain->getTablePrefix() );
212
213 return true;
214 }
215
219 public function lastError() {
220 if ( $this->conn ) {
221 // Even if it's non-zero, it can still be invalid
222 $error = $this->mysqlError( $this->conn );
223 if ( !$error ) {
224 $error = $this->mysqlError();
225 }
226 } else {
227 $error = $this->mysqlError() ?: $this->lastConnectError;
228 }
229
230 return $error;
231 }
232
233 protected function isInsertSelectSafe( array $insertOptions, array $selectOptions ) {
234 $row = $this->replicationReporter->getReplicationSafetyInfo( $this );
235 // For row-based-replication, the resulting changes will be relayed, not the query
236 if ( $row->binlog_format === 'ROW' ) {
237 return true;
238 }
239 // LIMIT requires ORDER BY on a unique key or it is non-deterministic
240 if ( isset( $selectOptions['LIMIT'] ) ) {
241 return false;
242 }
243 // In MySQL, an INSERT SELECT is only replication safe with row-based
244 // replication or if innodb_autoinc_lock_mode is 0. When those
245 // conditions aren't met, use non-native mode.
246 // While we could try to determine if the insert is safe anyway by
247 // checking if the target table has an auto-increment column that
248 // isn't set in $varMap, that seems unlikely to be worth the extra
249 // complexity.
250 return (
251 in_array( 'NO_AUTO_COLUMNS', $insertOptions ) ||
252 (int)$row->innodb_autoinc_lock_mode === 0
253 );
254 }
255
256 protected function checkInsertWarnings( Query $query, $fname ) {
257 if ( $this->conn && $this->conn->warning_count ) {
258 // Yeah it's weird. It's not iterable.
259 $warnings = $this->conn->get_warnings();
260 $done = $warnings === false;
261 while ( !$done ) {
262 if ( in_array( $warnings->errno, [
263 // List based on https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#ignore-effect-on-execution
264 1048, /* ER_BAD_NULL_ERROR */
265 1526, /* ER_NO_PARTITION_FOR_GIVEN_VALUE */
266 1748, /* ER_ROW_DOES_NOT_MATCH_GIVEN_PARTITION_SET */
267 1242, /* ER_SUBQUERY_NO_1_ROW */
268 1369, /* ER_VIEW_CHECK_FAILED */
269 // Truncation and overflow per T108255
270 1264, /* ER_WARN_DATA_OUT_OF_RANGE */
271 1265, /* WARN_DATA_TRUNCATED */
272 ] ) ) {
273 $this->reportQueryError(
274 'Insert returned unacceptable warning: ' . $warnings->message,
275 $warnings->errno,
276 $query->getSQL(),
277 $fname
278 );
279 }
280 $done = !$warnings->next();
281 }
282 }
283 }
284
298 public function estimateRowCount(
299 $tables,
300 $var = '*',
301 $conds = '',
302 $fname = __METHOD__,
303 $options = [],
304 $join_conds = []
305 ): int {
306 $conds = $this->platform->normalizeConditions( $conds, $fname );
307 $column = $this->platform->extractSingleFieldFromList( $var );
308 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
309 $conds[] = "$column IS NOT NULL";
310 }
311
312 $options['EXPLAIN'] = true;
313 $res = $this->select( $tables, $var, $conds, $fname, $options, $join_conds );
314 if ( $res === false ) {
315 return -1;
316 }
317 if ( !$res->numRows() ) {
318 return 0;
319 }
320
321 $rows = 1;
322 foreach ( $res as $plan ) {
323 $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
324 }
325
326 return (int)$rows;
327 }
328
329 public function tableExists( $table, $fname = __METHOD__ ) {
330 $components = $this->platform->qualifiedTableComponents( $table );
331 if ( count( $components ) === 1 ) {
332 $db = $this->currentDomain->getDatabase();
333 $tableName = $components[0];
334 } elseif ( count( $components ) === 2 ) {
335 [ $db, $tableName ] = $components;
336 } else {
337 throw new DBLanguageError( 'Too many table components' );
338 }
339
340 if ( isset( $this->sessionTempTables[$tableName] ) ) {
341 return true; // already known to exist and won't be found in the query anyway
342 }
343 return (bool)$this->newSelectQueryBuilder()
344 ->select( '1' )
345 ->from( 'information_schema.tables' )
346 ->where( [
347 'table_schema' => $db,
348 'table_name' => $tableName,
349 ] )
350 ->caller( $fname )
351 ->fetchField();
352 }
353
359 public function fieldInfo( $table, $field ) {
360 $query = new Query(
361 "SELECT * FROM " . $this->tableName( $table ) . " LIMIT 1",
362 self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
363 'SELECT'
364 );
365 $res = $this->query( $query, __METHOD__ );
366 if ( !$res ) {
367 return false;
368 }
370 '@phan-var MysqliResultWrapper $res';
371 return $res->getInternalFieldInfo( $field );
372 }
373
383 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
384 # https://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
385 $index = $this->platform->indexName( $index );
386 $query = new Query(
387 'SHOW INDEX FROM ' . $this->tableName( $table ),
388 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
389 'SHOW'
390 );
391 $res = $this->query( $query, $fname );
392
393 if ( !$res ) {
394 return null;
395 }
396
397 $result = [];
398
399 foreach ( $res as $row ) {
400 if ( $row->Key_name == $index ) {
401 $result[] = $row;
402 }
403 }
404
405 return $result ?: false;
406 }
407
412 public function strencode( $s ) {
413 return $this->mysqlRealEscapeString( $s );
414 }
415
416 public function serverIsReadOnly() {
417 // Avoid SHOW to avoid internal temporary tables
418 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
419 $query = new Query( "SELECT @@GLOBAL.read_only AS Value", $flags, 'SELECT' );
420 $res = $this->query( $query, __METHOD__ );
421 $row = $res->fetchObject();
422
423 return $row && (bool)$row->Value;
424 }
425
429 public function getSoftwareLink() {
430 [ $variant ] = $this->getMySqlServerVariant();
431 if ( $variant === 'MariaDB' ) {
432 return '[{{int:version-db-mariadb-url}} MariaDB]';
433 }
434
435 return '[{{int:version-db-mysql-url}} MySQL]';
436 }
437
441 private function getMySqlServerVariant() {
442 $version = $this->getServerVersion();
443
444 // MariaDB includes its name in its version string; this is how MariaDB's version of
445 // the mysql command-line client identifies MariaDB servers.
446 // https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_version
447 // https://mariadb.com/kb/en/version/
448 $parts = explode( '-', $version, 2 );
449 $number = $parts[0];
450 $suffix = $parts[1] ?? '';
451 if ( strpos( $suffix, 'MariaDB' ) !== false || strpos( $suffix, '-maria-' ) !== false ) {
452 $vendor = 'MariaDB';
453 } else {
454 $vendor = 'MySQL';
455 }
456
457 return [ $vendor, $number ];
458 }
459
463 public function getServerVersion() {
464 // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
465 // it off (see RPL_VERSION_HACK in include/mysql_com.h).
466 $version = $this->conn->server_info;
467 if (
468 str_starts_with( $version, '5.5.5-' ) &&
469 ( str_contains( $version, 'MariaDB' ) || str_contains( $version, '-maria-' ) )
470 ) {
471 $version = substr( $version, strlen( '5.5.5-' ) );
472 }
473 return $version;
474 }
475
479 public function setSessionOptions( array $options ) {
480 $sqlAssignments = [];
481
482 if ( isset( $options['connTimeout'] ) ) {
483 $encTimeout = (int)$options['connTimeout'];
484 $sqlAssignments[] = "net_read_timeout=$encTimeout";
485 $sqlAssignments[] = "net_write_timeout=$encTimeout";
486 }
487 if ( isset( $options['groupConcatMaxLen'] ) ) {
488 $maxLength = (int)$options['groupConcatMaxLen'];
489 $sqlAssignments[] = "group_concat_max_len=$maxLength";
490 }
491
492 if ( $sqlAssignments ) {
493 $query = new Query(
494 'SET ' . implode( ', ', $sqlAssignments ),
495 self::QUERY_CHANGE_TRX | self::QUERY_CHANGE_NONE,
496 'SET'
497 );
498 $this->query( $query, __METHOD__ );
499 }
500 }
501
507 public function streamStatementEnd( &$sql, &$newLine ) {
508 if ( preg_match( '/^DELIMITER\s+(\S+)/i', $newLine, $m ) ) {
509 $this->delimiter = $m[1];
510 $newLine = '';
511 }
512
513 return parent::streamStatementEnd( $sql, $newLine );
514 }
515
516 public function doLockIsFree( string $lockName, string $method ) {
517 $query = new Query( $this->platform->lockIsFreeSQLText( $lockName ), self::QUERY_CHANGE_LOCKS, 'SELECT' );
518 $res = $this->query( $query, $method );
519 $row = $res->fetchObject();
520
521 return ( $row->unlocked == 1 );
522 }
523
524 public function doLock( string $lockName, string $method, int $timeout ) {
525 $query = new Query( $this->platform->lockSQLText( $lockName, $timeout ), self::QUERY_CHANGE_LOCKS, 'SELECT' );
526 $res = $this->query( $query, $method );
527 $row = $res->fetchObject();
528
529 return ( $row->acquired !== null ) ? (float)$row->acquired : null;
530 }
531
532 public function doUnlock( string $lockName, string $method ) {
533 $query = new Query( $this->platform->unlockSQLText( $lockName ), self::QUERY_CHANGE_LOCKS, 'SELECT' );
534 $res = $this->query( $query, $method );
535 $row = $res->fetchObject();
536
537 return ( $row->released == 1 );
538 }
539
540 public function namedLocksEnqueue() {
541 return true;
542 }
543
544 protected function doFlushSession( $fname ) {
545 // Note that RELEASE_ALL_LOCKS() is not supported well enough to use here.
546 // https://mariadb.com/kb/en/release_all_locks/
547 $releaseLockFields = [];
548 foreach ( $this->sessionNamedLocks as $name => $info ) {
549 $encName = $this->addQuotes( $this->platform->makeLockName( $name ) );
550 $releaseLockFields[] = "RELEASE_LOCK($encName)";
551 }
552 if ( $releaseLockFields ) {
553 $sql = 'SELECT ' . implode( ',', $releaseLockFields );
554 $flags = self::QUERY_CHANGE_LOCKS | self::QUERY_NO_RETRY;
555 $query = new Query( $sql, $flags, 'SELECT' );
556 $qs = $this->executeQuery( $query, __METHOD__, $flags );
557 if ( $qs->res === false ) {
558 $this->reportQueryError( $qs->message, $qs->code, $sql, $fname, true );
559 }
560 }
561 }
562
563 public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__ ) {
564 $identityKey = $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
565 if ( !$rows ) {
566 return;
567 }
568 $this->platform->assertValidUpsertSetArray( $set, $identityKey, $rows );
569
570 $encTable = $this->tableName( $table );
571 [ $sqlColumns, $sqlTuples ] = $this->platform->makeInsertLists( $rows );
572 $sqlColumnAssignments = $this->makeList( $set, self::LIST_SET );
573 // No need to expose __NEW.* since buildExcludedValue() uses VALUES(column)
574
575 // https://mariadb.com/kb/en/insert-on-duplicate-key-update/
576 // https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
577 $sql =
578 "INSERT INTO $encTable " .
579 "($sqlColumns) VALUES $sqlTuples " .
580 "ON DUPLICATE KEY UPDATE $sqlColumnAssignments";
581 $query = new Query( $sql, self::QUERY_CHANGE_ROWS, 'INSERT', $table );
582 $this->query( $query, $fname );
583 // Count updates of conflicting rows and row inserts equally toward the change count
584 $this->lastQueryAffectedRows = min( $this->lastQueryAffectedRows, count( $rows ) );
585 }
586
587 public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
588 $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
589 if ( !$rows ) {
590 return;
591 }
592 $encTable = $this->tableName( $table );
593 [ $sqlColumns, $sqlTuples ] = $this->platform->makeInsertLists( $rows );
594 // https://dev.mysql.com/doc/refman/8.0/en/replace.html
595 $sql = "REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples";
596 // Note that any auto-increment columns on conflicting rows will be reassigned
597 // due to combined DELETE+INSERT semantics. This will be reflected in insertId().
598 $query = new Query( $sql, self::QUERY_CHANGE_ROWS, 'REPLACE', $table );
599 $this->query( $query, $fname );
600 // Do not count deletions of conflicting rows toward the change count
601 $this->lastQueryAffectedRows = min( $this->lastQueryAffectedRows, count( $rows ) );
602 }
603
609 public function wasDeadlock() {
610 return $this->lastErrno() == 1213;
611 }
612
618 public function wasReadOnlyError() {
619 return $this->lastErrno() == 1223 ||
620 ( $this->lastErrno() == 1290 && strpos( $this->lastError(), '--read-only' ) !== false );
621 }
622
623 protected function isConnectionError( $errno ) {
624 // https://mariadb.com/kb/en/mariadb-error-codes/
625 // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
626 // https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html
627 return in_array( $errno, [ 2013, 2006, 2003, 1927, 1053 ], true );
628 }
629
630 protected function isQueryTimeoutError( $errno ) {
631 // https://mariadb.com/kb/en/mariadb-error-codes/
632 // https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html
633 // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
634 return in_array( $errno, [ 3024, 2062, 1969, 1028 ], true );
635 }
636
637 protected function isKnownStatementRollbackError( $errno ) {
638 // https://mariadb.com/kb/en/mariadb-error-codes/
639 // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
640 return in_array(
641 $errno,
642 [ 3024, 1969, 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ],
643 true
644 );
645 }
646
654 public function duplicateTableStructure(
655 $oldName, $newName, $temporary = false, $fname = __METHOD__
656 ) {
657 $tmp = $temporary ? 'TEMPORARY ' : '';
658 $newNameQuoted = $this->addIdentifierQuotes( $newName );
659 $oldNameQuoted = $this->addIdentifierQuotes( $oldName );
660
661 $query = new Query(
662 "CREATE $tmp TABLE $newNameQuoted (LIKE $oldNameQuoted)",
663 self::QUERY_PSEUDO_PERMANENT | self::QUERY_CHANGE_SCHEMA,
664 $temporary ? 'CREATE TEMPORARY' : 'CREATE',
665 // Use a dot to avoid double-prefixing in Database::getTempTableWrites()
666 '.' . $newName
667 );
668 return $this->query( $query, $fname );
669 }
670
678 public function listTables( $prefix = null, $fname = __METHOD__ ) {
679 $qb = $this->newSelectQueryBuilder()
680 ->select( 'table_name' )
681 ->from( 'information_schema.tables' )
682 ->where( [
683 'table_schema' => $this->currentDomain->getDatabase(),
684 'table_type' => 'BASE TABLE'
685 ] )
686 ->caller( $fname );
687 if ( $prefix !== null && $prefix !== '' ) {
688 $qb->andWhere( $this->expr(
689 'table_name', IExpression::LIKE, new LikeValue( $prefix, $this->anyString() )
690 ) );
691 }
692 return $qb->fetchFieldValues();
693 }
694
706 public function listViews( $prefix = null, $fname = __METHOD__ ) {
707 wfDeprecated( __METHOD__, '1.42' );
708 $qb = $this->newSelectQueryBuilder()
709 ->select( 'table_name' )
710 ->from( 'information_schema.views' )
711 ->where( [ 'table_schema' => $this->currentDomain->getDatabase() ] )
712 ->caller( $fname );
713
714 if ( $prefix !== null && $prefix !== '' ) {
715 $qb->andWhere( $this->expr(
716 'table_name', IExpression::LIKE, new LikeValue( $prefix, $this->anyString() )
717 ) );
718 }
719 return $qb->fetchFieldValues();
720 }
721
722 public function selectSQLText(
723 $table,
724 $vars,
725 $conds = '',
726 $fname = __METHOD__,
727 $options = [],
728 $join_conds = []
729 ) {
730 $sql = parent::selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
731 // https://dev.mysql.com/doc/refman/5.7/en/optimizer-hints.html
732 // https://mariadb.com/kb/en/library/aborting-statements/
733 $timeoutMsec = intval( $options['MAX_EXECUTION_TIME'] ?? 0 );
734 if ( $timeoutMsec > 0 ) {
735 [ $vendor, $number ] = $this->getMySqlServerVariant();
736 if ( $vendor === 'MariaDB' && version_compare( $number, '10.1.2', '>=' ) ) {
737 $timeoutSec = $timeoutMsec / 1000;
738 $sql = "SET STATEMENT max_statement_time=$timeoutSec FOR $sql";
739 } elseif ( $vendor === 'MySQL' && version_compare( $number, '5.7.0', '>=' ) ) {
740 $sql = preg_replace(
741 '/^SELECT(?=\s)/',
742 "SELECT /*+ MAX_EXECUTION_TIME($timeoutMsec)*/",
743 $sql
744 );
745 }
746 }
747
748 return $sql;
749 }
750
751 protected function doSingleStatementQuery( string $sql ): QueryStatus {
752 $conn = $this->getBindingHandle();
753
754 // Hide packet warnings caused by things like dropped connections
755 AtEase::suppressWarnings();
756 $res = $conn->query( $sql );
757 AtEase::restoreWarnings();
758 // Note that mysqli::insert_id only reflects the last query statement
759 $insertId = (int)$conn->insert_id;
760 $this->lastQueryInsertId = $insertId;
761 $this->sessionLastAutoRowId = $insertId ?: $this->sessionLastAutoRowId;
762
763 return new QueryStatus(
764 $res instanceof mysqli_result ? new MysqliResultWrapper( $this, $res ) : $res,
765 $conn->affected_rows,
766 $conn->error,
767 $conn->errno
768 );
769 }
770
779 private function mysqlConnect( $server, $user, $password, $db ) {
780 if ( !function_exists( 'mysqli_init' ) ) {
781 throw $this->newExceptionAfterConnectError(
782 "MySQLi functions missing, have you compiled PHP with the --with-mysqli option?"
783 );
784 }
785
786 // PHP 8.1.0+ throws exceptions by default. Turn that off for consistency.
787 mysqli_report( MYSQLI_REPORT_OFF );
788
789 // Other than mysql_connect, mysqli_real_connect expects an explicit port number
790 // e.g. "localhost:1234" or "127.0.0.1:1234"
791 // or Unix domain socket path
792 // e.g. "localhost:/socket_path" or "localhost:/foo/bar:bar:bar"
793 // colons are known to be used by Google AppEngine,
794 // see <https://cloud.google.com/sql/docs/mysql/connect-app-engine>
795 //
796 // We need to parse the port or socket path out of $realServer
797 $port = null;
798 $socket = null;
799 $hostAndPort = IPUtils::splitHostAndPort( $server );
800 if ( $hostAndPort ) {
801 $realServer = $hostAndPort[0];
802 if ( $hostAndPort[1] ) {
803 $port = $hostAndPort[1];
804 }
805 } elseif ( substr_count( $server, ':/' ) == 1 ) {
806 // If we have a colon slash instead of a colon and a port number
807 // after the ip or hostname, assume it's the Unix domain socket path
808 [ $realServer, $socket ] = explode( ':', $server, 2 );
809 } else {
810 $realServer = $server;
811 }
812
813 $mysqli = mysqli_init();
814 // Make affectedRows() for UPDATE reflect the number of matching rows, regardless
815 // of whether any column values changed. This is what callers want to know and is
816 // consistent with what Postgres and SQLite return.
817 $flags = MYSQLI_CLIENT_FOUND_ROWS;
818 if ( $this->ssl ) {
819 $flags |= MYSQLI_CLIENT_SSL;
820 $mysqli->ssl_set(
821 $this->sslKeyPath,
822 $this->sslCertPath,
823 $this->sslCAFile,
824 $this->sslCAPath,
825 $this->sslCiphers
826 );
827 }
828 if ( $this->getFlag( self::DBO_COMPRESS ) ) {
829 $flags |= MYSQLI_CLIENT_COMPRESS;
830 }
831 if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
832 $realServer = 'p:' . $realServer;
833 }
834
835 if ( $this->utf8Mode ) {
836 // Tell the server we're communicating with it in UTF-8.
837 // This may engage various charset conversions.
838 $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'utf8' );
839 } else {
840 $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'binary' );
841 }
842
843 $mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, $this->connectTimeout ?: 3 );
844 if ( $this->receiveTimeout ) {
845 $mysqli->options( MYSQLI_OPT_READ_TIMEOUT, $this->receiveTimeout );
846 }
847
848 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal socket seems set when used
849 $ok = $mysqli->real_connect( $realServer, $user, $password, $db, $port, $socket, $flags );
850
851 return $ok ? $mysqli : null;
852 }
853
854 protected function closeConnection() {
855 return ( $this->conn instanceof mysqli ) ? mysqli_close( $this->conn ) : true;
856 }
857
858 protected function lastInsertId() {
859 return $this->sessionLastAutoRowId;
860 }
861
862 protected function doHandleSessionLossPreconnect() {
863 // https://mariadb.com/kb/en/last_insert_id/
864 $this->sessionLastAutoRowId = 0;
865 }
866
867 public function insertId() {
868 if ( $this->lastEmulatedInsertId === null ) {
869 $conn = $this->getBindingHandle();
870 // Note that mysqli::insert_id only reflects the last query statement
871 $this->lastEmulatedInsertId = (int)$conn->insert_id;
872 }
873
874 return $this->lastEmulatedInsertId;
875 }
876
880 public function lastErrno() {
881 if ( $this->conn instanceof mysqli ) {
882 return $this->conn->errno;
883 } else {
884 return mysqli_connect_errno();
885 }
886 }
887
892 private function mysqlError( $conn = null ) {
893 if ( $conn === null ) {
894 return (string)mysqli_connect_error();
895 } else {
896 return $conn->error;
897 }
898 }
899
900 private function mysqlRealEscapeString( $s ) {
901 $conn = $this->getBindingHandle();
902
903 return $conn->real_escape_string( (string)$s );
904 }
905}
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
array $params
The job parameters.
Base class for the more common types of database errors.
Class to handle database/schema/prefix specifications for IDatabase.
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.
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
checkInsertWarnings(Query $query, $fname)
Check for warnings after performing an INSERT query, and throw exceptions if necessary.
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.28
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:43
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition Database.php:435
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
newExceptionAfterConnectError( $error)
string $lastConnectError
Last error during connection; empty string if none.
Definition Database.php:111
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:424
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:674
close( $fname=__METHOD__)
Close the database connection.
Definition Database.php:487
reportQueryError( $error, $errno, $sql, $fname, $ignore=false)
Report a query error.
getDBname()
Get the current database name; null if there isn't one.
Content of like value.
Definition LikeValue.php:14
Holds information on Query to be executed.
Definition Query.php:31