MediaWiki master
DatabaseMySQL.php
Go to the documentation of this file.
1<?php
6namespace Wikimedia\Rdbms;
7
8use mysqli;
9use mysqli_result;
10use RuntimeException;
11use Wikimedia\AtEase\AtEase;
12use Wikimedia\IPUtils;
16
28class DatabaseMySQL extends Database {
30 private $sslKeyPath;
32 private $sslCertPath;
34 private $sslCAFile;
36 private $sslCAPath;
42 private $sslCiphers;
44 private $utf8Mode;
45
47 protected $platform;
48
52 private $sessionLastAutoRowId;
53
73 public function __construct( array $params ) {
74 foreach ( [ 'KeyPath', 'CertPath', 'CAFile', 'CAPath', 'Ciphers' ] as $name ) {
75 $var = "ssl{$name}";
76 if ( isset( $params[$var] ) ) {
77 $this->$var = $params[$var];
78 }
79 }
80 $this->utf8Mode = !empty( $params['utf8Mode'] );
81 parent::__construct( $params );
82 $this->platform = new MySQLPlatform(
83 $this,
84 $this->logger,
85 $this->currentDomain,
86 $this->errorLogger
87 );
88 $this->replicationReporter = new MysqlReplicationReporter(
89 $params['topologyRole'],
90 $this->logger,
91 $params['srvCache'],
92 $params['lagDetectionMethod'] ?? 'Seconds_Behind_Master',
93 $params['lagDetectionOptions'] ?? [],
94 !empty( $params['useGTIDs' ] )
95 );
96 }
97
101 public function getType() {
102 return 'mysql';
103 }
104
106 protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
107 $this->close( __METHOD__ );
108
109 if ( $schema !== null ) {
110 throw $this->newExceptionAfterConnectError( "Got schema '$schema'; not supported." );
111 }
112
113 $this->installErrorHandler();
114 try {
115 $this->conn = $this->mysqlConnect( $server, $user, $password, $db );
116 } catch ( RuntimeException $e ) {
117 $this->restoreErrorHandler();
118 throw $this->newExceptionAfterConnectError( $e->getMessage() );
119 }
120 $error = $this->restoreErrorHandler();
121
122 if ( !$this->conn ) {
123 throw $this->newExceptionAfterConnectError( $error ?: $this->lastError() );
124 }
125
126 try {
127 $this->currentDomain = new DatabaseDomain(
128 ( $db !== '' ) ? $db : null,
129 null,
130 $tablePrefix
131 );
132 $this->platform->setCurrentDomain( $this->currentDomain );
133
134 $set = [];
135 if ( !$this->flagsHolder->getFlag( self::DBO_GAUGE ) ) {
136 // Abstract over any excessive MySQL defaults
137 $set[] = 'group_concat_max_len = 262144';
138 // Set any custom settings defined by site config
139 // https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html
140 foreach ( $this->connectionVariables as $var => $val ) {
141 // Escape strings but not numbers to avoid MySQL complaining
142 if ( !is_int( $val ) && !is_float( $val ) ) {
143 $val = $this->addQuotes( $val );
144 }
145 $set[] = $this->platform->addIdentifierQuotes( $var ) . ' = ' . $val;
146 }
147 }
148
149 if ( $set ) {
150 $sql = 'SET ' . implode( ', ', $set );
151 $flags = self::QUERY_NO_RETRY | self::QUERY_CHANGE_TRX;
152 $query = new Query( $sql, $flags, 'SET' );
153 // Avoid using query() so that replaceLostConnection() does not throw
154 // errors if the transaction status is STATUS_TRX_ERROR
155 $qs = $this->executeQuery( $query, __METHOD__, $flags );
156 if ( $qs->res === false ) {
157 $this->reportQueryError( $qs->message, $qs->code, $sql, __METHOD__ );
158 }
159 }
160 } catch ( RuntimeException $e ) {
161 throw $this->newExceptionAfterConnectError( $e->getMessage() );
162 }
163 }
164
166 protected function doSelectDomain( DatabaseDomain $domain ) {
167 if ( $domain->getSchema() !== null ) {
168 throw new DBExpectedError(
169 $this,
170 __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
171 );
172 }
173
174 $database = $domain->getDatabase();
175 // A null database means "don't care" so leave it as is and update the table prefix
176 if ( $database === null ) {
177 $this->currentDomain = new DatabaseDomain(
178 $this->currentDomain->getDatabase(),
179 null,
180 $domain->getTablePrefix()
181 );
182 $this->platform->setCurrentDomain( $this->currentDomain );
183
184 return true;
185 }
186
187 if ( $database !== $this->getDBname() ) {
188 $sql = 'USE ' . $this->addIdentifierQuotes( $database );
189 $query = new Query( $sql, self::QUERY_CHANGE_TRX, 'USE' );
190 $qs = $this->executeQuery( $query, __METHOD__, self::QUERY_CHANGE_TRX );
191 if ( $qs->res === false ) {
192 $this->reportQueryError( $qs->message, $qs->code, $sql, __METHOD__ );
193 return false; // unreachable
194 }
195 }
196
197 // Update that domain fields on success (no exception thrown)
198 $this->currentDomain = $domain;
199 $this->platform->setCurrentDomain( $domain );
200
201 return true;
202 }
203
207 public function lastError() {
208 if ( $this->conn ) {
209 // Even if it's non-zero, it can still be invalid
210 $error = $this->mysqlError( $this->conn );
211 if ( !$error ) {
212 $error = $this->mysqlError();
213 }
214 } else {
215 $error = $this->mysqlError() ?: $this->lastConnectError;
216 }
217
218 return $error;
219 }
220
222 protected function isInsertSelectSafe( array $insertOptions, array $selectOptions, $fname ) {
223 $row = $this->replicationReporter->getReplicationSafetyInfo( $this, $fname );
224 // For row-based-replication, the resulting changes will be relayed, not the query
225 if ( $row->binlog_format === 'ROW' ) {
226 return true;
227 }
228 // LIMIT requires ORDER BY on a unique key or it is non-deterministic
229 if ( isset( $selectOptions['LIMIT'] ) ) {
230 return false;
231 }
232 // In MySQL, an INSERT SELECT is only replication safe with row-based
233 // replication or if innodb_autoinc_lock_mode is 0. When those
234 // conditions aren't met, use non-native mode.
235 // While we could try to determine if the insert is safe anyway by
236 // checking if the target table has an auto-increment column that
237 // isn't set in $varMap, that seems unlikely to be worth the extra
238 // complexity.
239 return (
240 in_array( 'NO_AUTO_COLUMNS', $insertOptions ) ||
241 (int)$row->innodb_autoinc_lock_mode === 0
242 );
243 }
244
246 protected function checkInsertWarnings( Query $query, $fname ) {
247 if ( $this->conn && $this->conn->warning_count ) {
248 // Yeah it's weird. It's not iterable.
249 $warnings = $this->conn->get_warnings();
250 $done = $warnings === false;
251 while ( !$done ) {
252 if ( in_array( $warnings->errno, [
253 // List based on https://dev.mysql.com/doc/refman/8.0/en/sql-mode.html#ignore-effect-on-execution
254 1048, /* ER_BAD_NULL_ERROR */
255 1526, /* ER_NO_PARTITION_FOR_GIVEN_VALUE */
256 1748, /* ER_ROW_DOES_NOT_MATCH_GIVEN_PARTITION_SET */
257 1242, /* ER_SUBQUERY_NO_1_ROW */
258 1369, /* ER_VIEW_CHECK_FAILED */
259 // Truncation and overflow per T108255
260 1264, /* ER_WARN_DATA_OUT_OF_RANGE */
261 1265, /* WARN_DATA_TRUNCATED */
262 ] ) ) {
263 $this->reportQueryError(
264 'Insert returned unacceptable warning: ' . $warnings->message,
265 $warnings->errno,
266 $query->getSQL(),
267 $fname
268 );
269 }
270 $done = !$warnings->next();
271 }
272 }
273 }
274
276 public function estimateRowCount(
277 $tables,
278 $var = '*',
279 $conds = '',
280 $fname = __METHOD__,
281 $options = [],
282 $join_conds = []
283 ): int {
284 $conds = $this->platform->normalizeConditions( $conds, $fname );
285 $column = $this->platform->extractSingleFieldFromList( $var );
286 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
287 $conds[] = "$column IS NOT NULL";
288 }
289
290 $options['EXPLAIN'] = true;
291 $res = $this->select( $tables, $var, $conds, $fname, $options, $join_conds );
292 if ( $res === false ) {
293 return -1;
294 }
295 if ( !$res->numRows() ) {
296 return 0;
297 }
298
299 $rows = 1;
300 foreach ( $res as $plan ) {
301 $rows *= $plan->rows > 0 ? $plan->rows : 1; // avoid resetting to zero
302 }
303
304 return (int)$rows;
305 }
306
308 public function tableExists( $table, $fname = __METHOD__ ) {
309 [ $db, $pt ] = $this->platform->getDatabaseAndTableIdentifier( $table );
310 if ( isset( $this->sessionTempTables[$db][$pt] ) ) {
311 return true; // already known to exist and won't be found in the query anyway
312 }
313
314 return (bool)$this->newSelectQueryBuilder()
315 ->select( '1' )
316 ->from( 'information_schema.tables' )
317 ->where( [
318 'table_schema' => $db,
319 'table_name' => $pt,
320 ] )
321 ->caller( $fname )
322 ->fetchField();
323 }
324
330 public function fieldInfo( $table, $field ) {
331 $query = new Query(
332 "SELECT * FROM " . $this->tableName( $table ) . " LIMIT 1",
333 self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
334 'SELECT'
335 );
336 $res = $this->query( $query, __METHOD__ );
337 if ( !$res ) {
338 return false;
339 }
341 '@phan-var MysqliResultWrapper $res';
342 return $res->getInternalFieldInfo( $field );
343 }
344
346 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
347 # https://dev.mysql.com/doc/mysql/en/SHOW_INDEX.html
348 $query = new Query(
349 'SHOW INDEX FROM ' . $this->tableName( $table ),
350 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
351 'SHOW'
352 );
353 $res = $this->query( $query, $fname );
354
355 foreach ( $res as $row ) {
356 if ( $row->Key_name === $index ) {
357 return [ 'unique' => !$row->Non_unique ];
358 }
359 }
360
361 return false;
362 }
363
365 public function getPrimaryKeyColumns( $table, $fname = __METHOD__ ) {
366 $query = new Query(
367 'SHOW INDEX FROM ' . $this->tableName( $table ),
368 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
369 'SHOW'
370 );
371 $res = $this->query( $query, $fname );
372
373 $bySeq = [];
374 foreach ( $res as $row ) {
375 if ( $row->Key_name === 'PRIMARY' ) {
376 $bySeq[(int)$row->Seq_in_index] = (string)$row->Column_name;
377 }
378 }
379
380 ksort( $bySeq );
381
382 return array_values( $bySeq );
383 }
384
389 public function strencode( $s ) {
390 return $this->mysqlRealEscapeString( $s );
391 }
392
394 public function serverIsReadOnly() {
395 // Avoid SHOW to avoid internal temporary tables
396 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE;
397 $query = new Query( "SELECT @@GLOBAL.read_only AS Value", $flags, 'SELECT' );
398 $res = $this->query( $query, __METHOD__ );
399 $row = $res->fetchObject();
400
401 return $row && $row->Value && $row->Value !== 'OFF';
402 }
403
407 public function getSoftwareLink() {
408 [ $variant ] = $this->getMySqlServerVariant();
409 if ( $variant === 'MariaDB' ) {
410 return '[{{int:version-db-mariadb-url}} MariaDB]';
411 }
412
413 return '[{{int:version-db-mysql-url}} MySQL]';
414 }
415
419 private function getMySqlServerVariant() {
420 $version = $this->getServerVersion();
421
422 // MariaDB includes its name in its version string; this is how MariaDB's version of
423 // the mysql command-line client identifies MariaDB servers.
424 // https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_version
425 // https://mariadb.com/kb/en/version/
426 $parts = explode( '-', $version, 2 );
427 $number = $parts[0];
428 $suffix = $parts[1] ?? '';
429 if ( str_contains( $suffix, 'MariaDB' ) || str_contains( $suffix, '-maria-' ) ) {
430 $vendor = 'MariaDB';
431 } else {
432 $vendor = 'MySQL';
433 }
434
435 return [ $vendor, $number ];
436 }
437
441 public function getServerVersion() {
442 // MariaDB 10 adds the prefix "5.5.5-", and only some newer client libraries strip
443 // it off (see RPL_VERSION_HACK in include/mysql_com.h).
444 $version = $this->conn->server_info;
445 if (
446 str_starts_with( $version, '5.5.5-' ) &&
447 ( str_contains( $version, 'MariaDB' ) || str_contains( $version, '-maria-' ) )
448 ) {
449 $version = substr( $version, strlen( '5.5.5-' ) );
450 }
451 return $version;
452 }
453
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
492 public function doLockIsFree( string $lockName, string $method ) {
493 $query = new Query( $this->platform->lockIsFreeSQLText( $lockName ), self::QUERY_CHANGE_LOCKS, 'SELECT' );
494 $res = $this->query( $query, $method );
495 $row = $res->fetchObject();
496
497 return ( $row->unlocked == 1 );
498 }
499
501 public function doLock( string $lockName, string $method, int $timeout ) {
502 $query = new Query( $this->platform->lockSQLText( $lockName, $timeout ), self::QUERY_CHANGE_LOCKS, 'SELECT' );
503 $res = $this->query( $query, $method );
504 $row = $res->fetchObject();
505
506 return ( $row->acquired !== null ) ? (float)$row->acquired : null;
507 }
508
510 public function doUnlock( string $lockName, string $method ) {
511 $query = new Query( $this->platform->unlockSQLText( $lockName ), self::QUERY_CHANGE_LOCKS, 'SELECT' );
512 $res = $this->query( $query, $method );
513 $row = $res->fetchObject();
514
515 return ( $row->released == 1 );
516 }
517
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
539 public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__ ) {
540 $identityKey = $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
541 if ( !$rows ) {
542 return;
543 }
544 $this->platform->assertValidUpsertSetArray( $set, $identityKey, $rows );
545
546 $encTable = $this->tableName( $table );
547 [ $sqlColumns, $sqlTuples ] = $this->platform->makeInsertLists( $rows );
548 $sqlColumnAssignments = $this->makeList( $set, self::LIST_SET );
549 // No need to expose __NEW.* since buildExcludedValue() uses VALUES(column)
550
551 // https://mariadb.com/kb/en/insert-on-duplicate-key-update/
552 // https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html
553 $sql =
554 "INSERT INTO $encTable " .
555 "($sqlColumns) VALUES $sqlTuples " .
556 "ON DUPLICATE KEY UPDATE $sqlColumnAssignments";
557 $query = new Query( $sql, self::QUERY_CHANGE_ROWS, 'INSERT', $table );
558 $this->query( $query, $fname );
559 // Count updates of conflicting rows and row inserts equally toward the change count
560 $this->lastQueryAffectedRows = min( $this->lastQueryAffectedRows, count( $rows ) );
561 }
562
564 public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
565 $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
566 if ( !$rows ) {
567 return;
568 }
569 $encTable = $this->tableName( $table );
570 [ $sqlColumns, $sqlTuples ] = $this->platform->makeInsertLists( $rows );
571 // https://dev.mysql.com/doc/refman/8.0/en/replace.html
572 $sql = "REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples";
573 // Note that any auto-increment columns on conflicting rows will be reassigned
574 // due to combined DELETE+INSERT semantics. This will be reflected in insertId().
575 $query = new Query( $sql, self::QUERY_CHANGE_ROWS, 'REPLACE', $table );
576 $this->query( $query, $fname );
577 // Do not count deletions of conflicting rows toward the change count
578 $this->lastQueryAffectedRows = min( $this->lastQueryAffectedRows, count( $rows ) );
579 }
580
582 protected function isConnectionError( $errno ) {
583 // https://mariadb.com/kb/en/mariadb-error-codes/
584 // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
585 // https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html
586 return in_array( $errno, [ 2013, 2006, 2003, 1927, 1053 ], true );
587 }
588
590 protected function isQueryTimeoutError( $errno ) {
591 // https://mariadb.com/kb/en/mariadb-error-codes/
592 // https://dev.mysql.com/doc/refman/8.0/en/client-error-reference.html
593 // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
594 // Note that 1969 is MariaDB specific and unused in MySQL.
595 return in_array( $errno, [ 3024, 1969, 1028 ], true );
596 }
597
599 protected function isKnownStatementRollbackError( $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 return in_array(
603 $errno,
604 [ 3024, 1969, 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ],
605 true
606 );
607 }
608
616 public function duplicateTableStructure(
617 $oldName, $newName, $temporary = false, $fname = __METHOD__
618 ) {
619 $tmp = $temporary ? 'TEMPORARY ' : '';
620 $newNameQuoted = $this->addIdentifierQuotes( $newName );
621 $oldNameQuoted = $this->addIdentifierQuotes( $oldName );
622
623 $query = new Query(
624 "CREATE $tmp TABLE $newNameQuoted (LIKE $oldNameQuoted)",
625 self::QUERY_PSEUDO_PERMANENT | self::QUERY_CHANGE_SCHEMA,
626 $temporary ? 'CREATE TEMPORARY' : 'CREATE',
627 // Use a dot to avoid double-prefixing in Database::getTempTableWrites()
628 '.' . $newName
629 );
630 return $this->query( $query, $fname );
631 }
632
640 public function listTables( $prefix = null, $fname = __METHOD__ ) {
641 $qb = $this->newSelectQueryBuilder()
642 ->select( 'table_name' )
643 ->from( 'information_schema.tables' )
644 ->where( [
645 'table_schema' => $this->currentDomain->getDatabase(),
646 'table_type' => 'BASE TABLE'
647 ] )
648 ->caller( $fname );
649 if ( $prefix !== null && $prefix !== '' ) {
650 $qb->andWhere( $this->expr(
651 'table_name', IExpression::LIKE, new LikeValue( $prefix, $this->anyString() )
652 ) );
653 }
654 return $qb->fetchFieldValues();
655 }
656
658 public function selectSQLText(
659 $tables,
660 $vars,
661 $conds = '',
662 $fname = __METHOD__,
663 $options = [],
664 $join_conds = []
665 ) {
666 $sql = parent::selectSQLText( $tables, $vars, $conds, $fname, $options, $join_conds );
667 // https://dev.mysql.com/doc/refman/5.7/en/optimizer-hints.html
668 // https://mariadb.com/kb/en/library/aborting-statements/
669 $timeoutMsec = intval( $options['MAX_EXECUTION_TIME'] ?? 0 );
670 if ( $timeoutMsec > 0 ) {
671 [ $vendor, $number ] = $this->getMySqlServerVariant();
672 if ( $vendor === 'MariaDB' && version_compare( $number, '10.1.2', '>=' ) ) {
673 $timeoutSec = $timeoutMsec / 1000;
674 $sql = "SET STATEMENT max_statement_time=$timeoutSec FOR $sql";
675 } elseif ( $vendor === 'MySQL' && version_compare( $number, '5.7.0', '>=' ) ) {
676 $sql = preg_replace(
677 '/^SELECT(?=\s)/',
678 "SELECT /*+ MAX_EXECUTION_TIME($timeoutMsec)*/",
679 $sql
680 );
681 }
682 }
683
684 return $sql;
685 }
686
687 protected function doSingleStatementQuery( string $sql ): QueryStatus {
688 $conn = $this->getBindingHandle();
689
690 // Hide packet warnings caused by things like dropped connections
691 AtEase::suppressWarnings();
692 $res = $conn->query( $sql );
693 AtEase::restoreWarnings();
694 // Note that mysqli::insert_id only reflects the last query statement
695 $insertId = (int)$conn->insert_id;
696 $this->lastQueryInsertId = $insertId;
697 $this->sessionLastAutoRowId = $insertId ?: $this->sessionLastAutoRowId;
698
699 return new QueryStatus(
700 $res instanceof mysqli_result ? new MysqliResultWrapper( $this, $res ) : $res,
701 $conn->affected_rows,
702 $conn->error,
703 $conn->errno
704 );
705 }
706
715 private function mysqlConnect( $server, $user, $password, $db ) {
716 if ( !function_exists( 'mysqli_init' ) ) {
717 throw $this->newExceptionAfterConnectError(
718 "MySQLi functions missing, have you compiled PHP with the --with-mysqli option?"
719 );
720 }
721
722 // PHP 8.1.0+ throws exceptions by default. Turn that off for consistency.
723 mysqli_report( MYSQLI_REPORT_OFF );
724
725 // Other than mysql_connect, mysqli_real_connect expects an explicit port number
726 // e.g. "localhost:1234" or "127.0.0.1:1234"
727 // or Unix domain socket path
728 // e.g. "localhost:/socket_path" or "localhost:/foo/bar:bar:bar"
729 // colons are known to be used by Google AppEngine,
730 // see <https://cloud.google.com/sql/docs/mysql/connect-app-engine>
731 //
732 // We need to parse the port or socket path out of $realServer
733 $port = null;
734 $socket = null;
735 $hostAndPort = IPUtils::splitHostAndPort( $server );
736 if ( $hostAndPort ) {
737 $realServer = $hostAndPort[0];
738 if ( $hostAndPort[1] ) {
739 $port = $hostAndPort[1];
740 }
741 } elseif ( substr_count( $server, ':/' ) == 1 ) {
742 // If we have a colon slash instead of a colon and a port number
743 // after the ip or hostname, assume it's the Unix domain socket path
744 [ $realServer, $socket ] = explode( ':', $server, 2 );
745 } else {
746 $realServer = $server;
747 }
748
749 $mysqli = mysqli_init();
750 // Make affectedRows() for UPDATE reflect the number of matching rows, regardless
751 // of whether any column values changed. This is what callers want to know and is
752 // consistent with what Postgres and SQLite return.
753 $flags = MYSQLI_CLIENT_FOUND_ROWS;
754 if ( $this->ssl ) {
755 $flags |= MYSQLI_CLIENT_SSL;
756 $mysqli->ssl_set(
757 $this->sslKeyPath,
758 $this->sslCertPath,
759 $this->sslCAFile,
760 $this->sslCAPath,
761 $this->sslCiphers
762 );
763 }
764 if ( $this->getFlag( self::DBO_COMPRESS ) ) {
765 $flags |= MYSQLI_CLIENT_COMPRESS;
766 }
767 if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
768 $realServer = 'p:' . $realServer;
769 }
770
771 if ( $this->utf8Mode ) {
772 // Tell the server we're communicating with it in UTF-8.
773 // This may engage various charset conversions.
774 $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'utf8' );
775 } else {
776 $mysqli->options( MYSQLI_SET_CHARSET_NAME, 'binary' );
777 }
778
779 $mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, $this->connectTimeout ?: 3 );
780 if ( $this->receiveTimeout ) {
781 $mysqli->options( MYSQLI_OPT_READ_TIMEOUT, $this->receiveTimeout );
782 }
783
784 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal socket seems set when used
785 $ok = $mysqli->real_connect( $realServer, $user, $password, $db, $port, $socket, $flags );
786
787 return $ok ? $mysqli : null;
788 }
789
791 protected function closeConnection() {
792 return ( $this->conn instanceof mysqli ) ? mysqli_close( $this->conn ) : true;
793 }
794
796 protected function lastInsertId() {
797 return $this->sessionLastAutoRowId;
798 }
799
800 protected function doHandleSessionLossPreconnect() {
801 // https://mariadb.com/kb/en/last_insert_id/
802 $this->sessionLastAutoRowId = 0;
803 }
804
806 public function insertId() {
807 if ( $this->lastEmulatedInsertId === null ) {
808 $conn = $this->getBindingHandle();
809 // Note that mysqli::insert_id only reflects the last query statement
810 $this->lastEmulatedInsertId = (int)$conn->insert_id;
811 }
812
813 return $this->lastEmulatedInsertId;
814 }
815
819 public function lastErrno() {
820 if ( $this->conn instanceof mysqli ) {
821 return $this->conn->errno;
822 } else {
823 return mysqli_connect_errno();
824 }
825 }
826
831 private function mysqlError( $conn = null ) {
832 if ( $conn === null ) {
833 return (string)mysqli_connect_error();
834 } else {
835 return $conn->error;
836 }
837 }
838
842 private function mysqlRealEscapeString( $s ): string {
843 $conn = $this->getBindingHandle();
844
845 return $conn->real_escape_string( (string)$s );
846 }
847}
Base class for the more common types of database errors.
Class to handle database/schema/prefix specifications for IDatabase.
doLockIsFree(string $lockName, string $method)
lockIsFree()bool Success
tableExists( $table, $fname=__METHOD__)
Query whether a given table exists.bool query}
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.It returns false by default,...
getPrimaryKeyColumns( $table, $fname=__METHOD__)
Get the primary key columns of a table.to be used by updater onlystring[] query}
listTables( $prefix=null, $fname=__METHOD__)
List all tables on the database.
isKnownStatementRollbackError( $errno)
bool Whether it is known that the last query error only caused statement rollback This is for backwar...
insertId()
Get the sequence-based ID assigned by the last query method call.This method should only be called wh...
isConnectionError( $errno)
Do not use this method outside of Database/DBError classes.bool Whether the given query error was a c...
indexInfo( $table, $index, $fname=__METHOD__)
Get information about an index into an object.array<string,mixed>|false Index info map; false if it d...
isInsertSelectSafe(array $insertOptions, array $selectOptions, $fname)
bool Whether an INSERT SELECT with these options will be replication safe 1.31
doFlushSession( $fname)
Reset the server-side session state for named locks and table locks.Connection and query errors will ...
MysqlReplicationReporter $replicationReporter
checkInsertWarnings(Query $query, $fname)
Check for warnings after performing an INSERT query, and throw exceptions if necessary....
setSessionOptions(array $options)
Override database's default behavior.
doUnlock(string $lockName, string $method)
unlock()bool Success
__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)
1.32
estimateRowCount( $tables, $var=' *', $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Estimate the number of rows in dataset.MySQL allows you to estimate the number of rows that would be ...
lastInsertId()
Get a row ID from the last insert statement to implicitly assign one within the session....
selectSQLText( $tables, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Take the same arguments as IDatabase::select() and return the SQL it would use.This can be useful for...
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.bool Whether connection was closed successfully 1....
doSingleStatementQuery(string $sql)
Run a query and return a QueryStatus instance with the query result information.
open( $server, $user, $password, $db, $schema, $tablePrefix)
Open a new connection to the database (closing any existing one)connectionParams} connectionParams} c...
doLock(string $lockName, string $method, int $timeout)
lock()float|null UNIX timestamp of lock acquisition; null on failure
A single concrete connection to a relational database.
Definition Database.php:37
restoreErrorHandler()
Restore the previous error handler and return the last PHP error for this DB.
Definition Database.php:426
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.string
newExceptionAfterConnectError( $error)
installErrorHandler()
Set a custom error handler for logging errors during database connection.
Definition Database.php:415
select( $tables, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.New callers should use newSe...
addIdentifierQuotes( $s)
Escape a SQL identifier (e.g.table, column, database) for use in a SQL queryDepending on the database...
executeQuery( $sql, $fname, $flags)
Execute a query without enforcing public (non-Database) caller restrictions.
Definition Database.php:662
close( $fname=__METHOD__)
Close the database connection.This should only be called after any transactions have been resolved,...
Definition Database.php:476
reportQueryError( $error, $errno, $sql, $fname, $ignore=false)
Report a query error.
if(is_string( $params['sqlMode'] ?? null)) $flags
Definition Database.php:209
getDBname()
Get the current database name; null if there isn't one.string|null
Content of like value.
Definition LikeValue.php:14
Holds information on Query to be executed.
Definition Query.php:17
const QUERY_CHANGE_TRX
Query is a Transaction Control Language command (BEGIN, USE, SET, ...)