11use Wikimedia\AtEase\AtEase;
52 private $sessionLastAutoRowId;
74 foreach ( [
'KeyPath',
'CertPath',
'CAFile',
'CAPath',
'Ciphers' ] as $name ) {
76 if ( isset( $params[$var] ) ) {
77 $this->$var = $params[$var];
80 $this->utf8Mode = !empty( $params[
'utf8Mode'] );
81 parent::__construct( $params );
89 $params[
'topologyRole'],
92 $params[
'lagDetectionMethod'] ??
'Seconds_Behind_Master',
93 $params[
'lagDetectionOptions'] ?? [],
94 !empty( $params[
'useGTIDs' ] )
106 protected function open( $server, $user, $password, $db, $schema, $tablePrefix ) {
107 $this->
close( __METHOD__ );
109 if ( $schema !==
null ) {
115 $this->conn = $this->mysqlConnect( $server, $user, $password, $db );
116 }
catch ( RuntimeException $e ) {
122 if ( !$this->conn ) {
128 ( $db !==
'' ) ? $db :
null,
135 if ( !$this->
flagsHolder->getFlag( self::DBO_GAUGE ) ) {
137 $set[] =
'group_concat_max_len = 262144';
142 if ( !is_int( $val ) && !is_float( $val ) ) {
145 $set[] = $this->
platform->addIdentifierQuotes( $var ) .
' = ' . $val;
150 $sql =
'SET ' . implode(
', ', $set );
156 if ( $qs->res ===
false ) {
160 }
catch ( RuntimeException $e ) {
170 __CLASS__ .
": domain '{$domain->getId()}' has a schema component"
176 if ( $database ===
null ) {
187 if ( $database !== $this->
getDBname() ) {
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 ) {
199 $this->
platform->setCurrentDomain( $domain );
210 $error = $this->mysqlError( $this->conn );
212 $error = $this->mysqlError();
215 $error = $this->mysqlError() ?: $this->lastConnectError;
223 $row = $this->replicationReporter->getReplicationSafetyInfo( $this, $fname );
225 if ( $row->binlog_format ===
'ROW' ) {
229 if ( isset( $selectOptions[
'LIMIT'] ) ) {
240 in_array(
'NO_AUTO_COLUMNS', $insertOptions ) ||
241 (
int)$row->innodb_autoinc_lock_mode === 0
247 if ( $this->conn && $this->conn->warning_count ) {
249 $warnings = $this->conn->get_warnings();
250 $done = $warnings ===
false;
252 if ( in_array( $warnings->errno, [
264 'Insert returned unacceptable warning: ' . $warnings->message,
270 $done = !$warnings->next();
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";
290 $options[
'EXPLAIN'] =
true;
291 $res = $this->
select( $tables, $var, $conds, $fname, $options, $join_conds );
292 if ( $res ===
false ) {
295 if ( !$res->numRows() ) {
300 foreach ( $res as $plan ) {
301 $rows *= $plan->rows > 0 ? $plan->rows : 1;
309 [ $db, $pt ] = $this->platform->getDatabaseAndTableIdentifier( $table );
310 if ( isset( $this->sessionTempTables[$db][$pt] ) ) {
314 return (
bool)$this->newSelectQueryBuilder()
316 ->from(
'information_schema.tables' )
318 'table_schema' => $db,
332 "SELECT * FROM " . $this->tableName( $table ) .
" LIMIT 1",
333 self::QUERY_SILENCE_ERRORS | self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
336 $res = $this->query( $query, __METHOD__ );
341 '@phan-var MysqliResultWrapper $res';
342 return $res->getInternalFieldInfo( $field );
346 public function indexInfo( $table, $index, $fname = __METHOD__ ) {
349 'SHOW INDEX FROM ' . $this->tableName( $table ),
350 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
353 $res = $this->query( $query, $fname );
355 foreach ( $res as $row ) {
356 if ( $row->Key_name === $index ) {
357 return [
'unique' => !$row->Non_unique ];
367 'SHOW INDEX FROM ' . $this->tableName( $table ),
368 self::QUERY_IGNORE_DBO_TRX | self::QUERY_CHANGE_NONE,
371 $res = $this->query( $query, $fname );
374 foreach ( $res as $row ) {
375 if ( $row->Key_name ===
'PRIMARY' ) {
376 $bySeq[(int)$row->Seq_in_index] = (
string)$row->Column_name;
382 return array_values( $bySeq );
390 return $this->mysqlRealEscapeString( $s );
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();
401 return $row && $row->Value && $row->Value !==
'OFF';
408 [ $variant ] = $this->getMySqlServerVariant();
409 if ( $variant ===
'MariaDB' ) {
410 return '[{{int:version-db-mariadb-url}} MariaDB]';
413 return '[{{int:version-db-mysql-url}} MySQL]';
419 private function getMySqlServerVariant() {
420 $version = $this->getServerVersion();
426 $parts = explode(
'-', $version, 2 );
428 $suffix = $parts[1] ??
'';
429 if ( str_contains( $suffix,
'MariaDB' ) || str_contains( $suffix,
'-maria-' ) ) {
435 return [ $vendor, $number ];
444 $version = $this->conn->server_info;
446 str_starts_with( $version,
'5.5.5-' ) &&
447 ( str_contains( $version,
'MariaDB' ) || str_contains( $version,
'-maria-' ) )
449 $version = substr( $version, strlen(
'5.5.5-' ) );
455 $sqlAssignments = [];
457 if ( isset( $options[
'connTimeout'] ) ) {
458 $encTimeout = (int)$options[
'connTimeout'];
459 $sqlAssignments[] =
"net_read_timeout=$encTimeout";
460 $sqlAssignments[] =
"net_write_timeout=$encTimeout";
462 if ( isset( $options[
'groupConcatMaxLen'] ) ) {
463 $maxLength = (int)$options[
'groupConcatMaxLen'];
464 $sqlAssignments[] =
"group_concat_max_len=$maxLength";
467 if ( $sqlAssignments ) {
469 'SET ' . implode(
', ', $sqlAssignments ),
470 self::QUERY_CHANGE_TRX | self::QUERY_CHANGE_NONE,
473 $this->query( $query, __METHOD__ );
483 if ( preg_match(
'/^DELIMITER\s+(\S+)/i', $newLine, $m ) ) {
484 $this->delimiter = $m[1];
488 return parent::streamStatementEnd( $sql, $newLine );
493 $query =
new Query( $this->platform->lockIsFreeSQLText( $lockName ), self::QUERY_CHANGE_LOCKS,
'SELECT' );
494 $res = $this->query( $query, $method );
495 $row = $res->fetchObject();
497 return ( $row->unlocked == 1 );
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();
506 return ( $row->acquired !==
null ) ? (float)$row->acquired :
null;
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();
515 return ( $row->released == 1 );
522 $releaseLockFields = [];
523 foreach ( $this->sessionNamedLocks as $name => $info ) {
524 $encName = $this->addQuotes( $this->platform->makeLockName( $name ) );
525 $releaseLockFields[] =
"RELEASE_LOCK($encName)";
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 );
539 public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__ ) {
540 $identityKey = $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
544 $this->platform->assertValidUpsertSetArray( $set, $identityKey, $rows );
546 $encTable = $this->tableName( $table );
547 [ $sqlColumns, $sqlTuples ] = $this->platform->makeInsertLists( $rows );
548 $sqlColumnAssignments = $this->makeList( $set, self::LIST_SET );
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 );
560 $this->lastQueryAffectedRows = min( $this->lastQueryAffectedRows, count( $rows ) );
564 public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
565 $this->platform->normalizeUpsertParams( $uniqueKeys, $rows );
569 $encTable = $this->tableName( $table );
570 [ $sqlColumns, $sqlTuples ] = $this->platform->makeInsertLists( $rows );
572 $sql =
"REPLACE INTO $encTable ($sqlColumns) VALUES $sqlTuples";
575 $query =
new Query( $sql, self::QUERY_CHANGE_ROWS,
'REPLACE', $table );
576 $this->query( $query, $fname );
578 $this->lastQueryAffectedRows = min( $this->lastQueryAffectedRows, count( $rows ) );
586 return in_array( $errno, [ 2013, 2006, 2003, 1927, 1053 ],
true );
595 return in_array( $errno, [ 3024, 1969, 1028 ],
true );
604 [ 3024, 1969, 1022, 1062, 1216, 1217, 1137, 1146, 1051, 1054 ],
617 $oldName, $newName, $temporary =
false, $fname = __METHOD__
619 $tmp = $temporary ?
'TEMPORARY ' :
'';
620 $newNameQuoted = $this->addIdentifierQuotes( $newName );
621 $oldNameQuoted = $this->addIdentifierQuotes( $oldName );
624 "CREATE $tmp TABLE $newNameQuoted (LIKE $oldNameQuoted)",
625 self::QUERY_PSEUDO_PERMANENT | self::QUERY_CHANGE_SCHEMA,
626 $temporary ?
'CREATE TEMPORARY' :
'CREATE',
630 return $this->query( $query, $fname );
640 public function listTables( $prefix =
null, $fname = __METHOD__ ) {
641 $qb = $this->newSelectQueryBuilder()
642 ->select(
'table_name' )
643 ->from(
'information_schema.tables' )
645 'table_schema' => $this->currentDomain->getDatabase(),
646 'table_type' =>
'BASE TABLE'
649 if ( $prefix !==
null && $prefix !==
'' ) {
650 $qb->andWhere( $this->expr(
654 return $qb->fetchFieldValues();
666 $sql = parent::selectSQLText( $tables, $vars, $conds, $fname, $options, $join_conds );
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',
'>=' ) ) {
678 "SELECT /*+ MAX_EXECUTION_TIME($timeoutMsec)*/",
688 $conn = $this->getBindingHandle();
691 AtEase::suppressWarnings();
692 $res = $conn->query( $sql );
693 AtEase::restoreWarnings();
695 $insertId = (int)$conn->insert_id;
696 $this->lastQueryInsertId = $insertId;
697 $this->sessionLastAutoRowId = $insertId ?: $this->sessionLastAutoRowId;
701 $conn->affected_rows,
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?"
723 mysqli_report( MYSQLI_REPORT_OFF );
735 $hostAndPort = IPUtils::splitHostAndPort( $server );
736 if ( $hostAndPort ) {
737 $realServer = $hostAndPort[0];
738 if ( $hostAndPort[1] ) {
739 $port = $hostAndPort[1];
741 } elseif ( substr_count( $server,
':/' ) == 1 ) {
744 [ $realServer, $socket ] = explode(
':', $server, 2 );
746 $realServer = $server;
749 $mysqli = mysqli_init();
753 $flags = MYSQLI_CLIENT_FOUND_ROWS;
755 $flags |= MYSQLI_CLIENT_SSL;
764 if ( $this->getFlag( self::DBO_COMPRESS ) ) {
765 $flags |= MYSQLI_CLIENT_COMPRESS;
767 if ( $this->getFlag( self::DBO_PERSISTENT ) ) {
768 $realServer =
'p:' . $realServer;
771 if ( $this->utf8Mode ) {
774 $mysqli->options( MYSQLI_SET_CHARSET_NAME,
'utf8' );
776 $mysqli->options( MYSQLI_SET_CHARSET_NAME,
'binary' );
779 $mysqli->options( MYSQLI_OPT_CONNECT_TIMEOUT, $this->connectTimeout ?: 3 );
780 if ( $this->receiveTimeout ) {
781 $mysqli->options( MYSQLI_OPT_READ_TIMEOUT, $this->receiveTimeout );
785 $ok = $mysqli->real_connect( $realServer, $user, $password, $db, $port, $socket, $flags );
787 return $ok ? $mysqli :
null;
792 return ( $this->conn instanceof mysqli ) ? mysqli_close( $this->conn ) :
true;
797 return $this->sessionLastAutoRowId;
802 $this->sessionLastAutoRowId = 0;
807 if ( $this->lastEmulatedInsertId ===
null ) {
808 $conn = $this->getBindingHandle();
810 $this->lastEmulatedInsertId = (int)$conn->insert_id;
813 return $this->lastEmulatedInsertId;
820 if ( $this->conn instanceof mysqli ) {
821 return $this->conn->errno;
823 return mysqli_connect_errno();
831 private function mysqlError( $conn =
null ) {
832 if ( $conn ===
null ) {
833 return (
string)mysqli_connect_error();
842 private function mysqlRealEscapeString( $s ): string {
843 $conn = $this->getBindingHandle();
845 return $conn->real_escape_string( (
string)$s );
Class to handle database/schema/prefix specifications for IDatabase.