30use InvalidArgumentException;
32use Psr\Log\LoggerAwareInterface;
33use Psr\Log\LoggerInterface;
34use Psr\Log\NullLogger;
37use UnexpectedValueException;
38use Wikimedia\Assert\Assert;
39use Wikimedia\AtEase\AtEase;
40use Wikimedia\RequestTimeout\CriticalSectionProvider;
41use Wikimedia\RequestTimeout\CriticalSectionScope;
42use Wikimedia\ScopedCallback;
43use Wikimedia\Timestamp\ConvertibleTimestamp;
207 public const ATTR_DB_IS_FILE =
'db-is-file';
209 public const ATTR_DB_LEVEL_LOCKING =
'db-level-locking';
211 public const ATTR_SCHEMAS_AS_TABLE_GROUPS =
'supports-schemas';
214 public const NEW_UNCONNECTED = 0;
216 public const NEW_CONNECTED = 1;
219 public const STATUS_TRX_ERROR = 1;
221 public const STATUS_TRX_OK = 2;
223 public const STATUS_TRX_NONE = 3;
285 $this->connectionParams = [
286 self::CONN_HOST => ( isset( $params[
'host'] ) && $params[
'host'] !==
'' )
289 self::CONN_USER => ( isset( $params[
'user'] ) && $params[
'user'] !==
'' )
292 self::CONN_INITIAL_DB => ( isset( $params[
'dbname'] ) && $params[
'dbname'] !==
'' )
295 self::CONN_INITIAL_SCHEMA => ( isset( $params[
'schema'] ) && $params[
'schema'] !==
'' )
298 self::CONN_PASSWORD => is_string( $params[
'password'] ) ? $params[
'password'] :
null,
299 self::CONN_INITIAL_TABLE_PREFIX => (string)$params[
'tablePrefix']
302 $this->lbInfo = $params[
'lbInfo'] ?? [];
303 $this->lazyMasterHandle = $params[
'lazyMasterHandle'] ??
null;
304 $this->connectionVariables = $params[
'variables'] ?? [];
306 $this->flags = (int)$params[
'flags'];
307 $this->cliMode = (bool)$params[
'cliMode'];
308 $this->agent = (string)$params[
'agent'];
309 $this->serverName = $params[
'serverName'];
310 $this->topologyRole = $params[
'topologyRole'];
311 $this->topologyRootMaster = $params[
'topologicalMaster'];
312 $this->nonNativeInsertSelectBatchSize = $params[
'nonNativeInsertSelectBatchSize'] ?? 10000;
314 $this->srvCache = $params[
'srvCache'];
315 $this->profiler = is_callable( $params[
'profiler'] ) ? $params[
'profiler'] :
null;
316 $this->trxProfiler = $params[
'trxProfiler'];
317 $this->connLogger = $params[
'connLogger'];
318 $this->queryLogger = $params[
'queryLogger'];
319 $this->replLogger = $params[
'replLogger'];
320 $this->errorLogger = $params[
'errorLogger'];
321 $this->deprecationLogger = $params[
'deprecationLogger'];
323 $this->csProvider = $params[
'criticalSectionProvider'] ??
null;
327 $params[
'dbname'] !=
'' ? $params[
'dbname'] :
null,
328 $params[
'schema'] !=
'' ? $params[
'schema'] :
null,
329 $params[
'tablePrefix']
332 $this->ownerId = $params[
'ownerId'] ??
null;
345 throw new LogicException( __METHOD__ .
': already connected' );
359 $this->connectionParams[self::CONN_HOST],
360 $this->connectionParams[self::CONN_USER],
361 $this->connectionParams[self::CONN_PASSWORD],
362 $this->connectionParams[self::CONN_INITIAL_DB],
363 $this->connectionParams[self::CONN_INITIAL_SCHEMA],
364 $this->connectionParams[self::CONN_INITIAL_TABLE_PREFIX]
436 final public static function factory(
$type, $params = [], $connect = self::NEW_CONNECTED ) {
439 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
451 'cliMode' => ( PHP_SAPI ===
'cli' || PHP_SAPI ===
'phpdbg' ),
454 'serverName' =>
null,
455 'topologyRole' =>
null,
456 'topologicalMaster' =>
null,
458 'lazyMasterHandle' => $params[
'lazyMasterHandle'] ??
null,
460 'profiler' => $params[
'profiler'] ??
null,
462 'connLogger' => $params[
'connLogger'] ??
new NullLogger(),
463 'queryLogger' => $params[
'queryLogger'] ??
new NullLogger(),
464 'replLogger' => $params[
'replLogger'] ??
new NullLogger(),
465 'errorLogger' => $params[
'errorLogger'] ??
static function ( Throwable $e ) {
466 trigger_error( get_class( $e ) .
': ' . $e->getMessage(), E_USER_WARNING );
468 'deprecationLogger' => $params[
'deprecationLogger'] ??
static function ( $msg ) {
469 trigger_error( $msg, E_USER_DEPRECATED );
474 $conn =
new $class( $params );
475 if ( $connect === self::NEW_CONNECTED ) {
476 $conn->initConnection();
494 self::ATTR_DB_IS_FILE =>
false,
495 self::ATTR_DB_LEVEL_LOCKING =>
false,
496 self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
501 return call_user_func( [ $class,
'getAttributes' ] ) + $defaults;
510 private static function getClass( $dbType, $driver =
null ) {
517 static $builtinTypes = [
518 'mysql' => [
'mysqli' => DatabaseMysqli::class ],
519 'sqlite' => DatabaseSqlite::class,
520 'postgres' => DatabasePostgres::class,
523 $dbType = strtolower( $dbType );
525 if ( !isset( $builtinTypes[$dbType] ) ) {
527 return 'Database' . ucfirst( $dbType );
531 $possibleDrivers = $builtinTypes[$dbType];
532 if ( is_string( $possibleDrivers ) ) {
533 $class = $possibleDrivers;
534 } elseif ( (
string)$driver !==
'' ) {
535 if ( !isset( $possibleDrivers[$driver] ) ) {
536 throw new InvalidArgumentException( __METHOD__ .
537 " type '$dbType' does not support driver '{$driver}'" );
540 $class = $possibleDrivers[$driver];
542 foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
543 if ( extension_loaded( $posDriver ) ) {
544 $class = $possibleClass;
550 if ( $class ===
false ) {
551 throw new InvalidArgumentException( __METHOD__ .
552 " no viable database extension found for type '$dbType'" );
575 $this->queryLogger = $logger;
600 return ( $this->trxShortId !=
'' ) ? 1 : 0;
616 $old = $this->currentDomain->getTablePrefix();
618 if ( $prefix !==
null ) {
620 $this->currentDomain->getDatabase(),
621 $this->currentDomain->getSchema(),
630 $old = $this->currentDomain->getSchema();
632 if ( $schema !==
null ) {
633 if ( $schema !==
'' && $this->
getDBname() ===
null ) {
636 "Cannot set schema to '$schema'; no database set"
641 $this->currentDomain->getDatabase(),
643 ( $schema !==
'' ) ? $schema :
null,
644 $this->currentDomain->getTablePrefix()
660 if ( $name ===
null ) {
664 if ( array_key_exists( $name, $this->lbInfo ) ) {
665 return $this->lbInfo[$name];
671 public function setLBInfo( $nameOrArray, $value =
null ) {
672 if ( is_array( $nameOrArray ) ) {
673 $this->lbInfo = $nameOrArray;
674 } elseif ( is_string( $nameOrArray ) ) {
675 if ( $value !==
null ) {
676 $this->lbInfo[$nameOrArray] = $value;
678 unset( $this->lbInfo[$nameOrArray] );
681 throw new InvalidArgumentException(
"Got non-string key" );
708 return $this->lastWriteTime ?:
false;
717 $this->trxDoneWrites ||
718 $this->trxPostCommitOrIdleCallbacks ||
719 $this->trxPreCommitOrIdleCallbacks ||
720 $this->trxEndCallbacks ||
733 if ( $this->
getFlag( self::DBO_TRX ) ) {
736 $id = $this->
getLBInfo( self::LB_TRX_ROUND_ID );
738 return is_string( $id ) ? $id :
null;
747 } elseif ( !$this->trxDoneWrites ) {
752 case self::ESTIMATE_DB_APPLY:
767 $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
768 $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
771 $applyTime += self::$TINY_WRITE_SEC * $omitted;
777 return $this->
trxLevel() ? $this->trxWriteCallers : [];
795 $this->trxPostCommitOrIdleCallbacks,
796 $this->trxPreCommitOrIdleCallbacks,
797 $this->trxEndCallbacks,
798 $this->trxSectionCancelCallbacks
800 foreach ( $callbacks as $callback ) {
801 $fnames[] = $callback[1];
812 return array_reduce( $this->trxAtomicLevels,
static function ( $accum, $v ) {
813 return $accum ===
null ? $v[0] :
"$accum, " . $v[0];
821 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
822 if ( $flag & ~static::$DBO_MUTABLE ) {
825 "Got $flag (allowed: " . implode(
', ', static::$MUTABLE_FLAGS ) .
')'
829 if ( $remember === self::REMEMBER_PRIOR ) {
833 $this->flags |= $flag;
836 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
837 if ( $flag & ~static::$DBO_MUTABLE ) {
840 "Got $flag (allowed: " . implode(
', ', static::$MUTABLE_FLAGS ) .
')'
844 if ( $remember === self::REMEMBER_PRIOR ) {
848 $this->flags &= ~$flag;
852 if ( !$this->priorFlags ) {
856 if ( $state === self::RESTORE_INITIAL ) {
857 $this->flags = reset( $this->priorFlags );
858 $this->priorFlags = [];
860 $this->flags = array_pop( $this->priorFlags );
865 return ( ( $this->flags & $flag ) === $flag );
869 return $this->currentDomain->getId();
873 return $res->fetchObject();
877 return $res->fetchRow();
881 if ( is_bool(
$res ) ) {
884 return $res->numRows();
889 return count(
$res->getFieldNames() );
893 return $res->getFieldNames()[$n];
913 abstract public function indexInfo( $table, $index, $fname = __METHOD__ );
928 $this->lastPhpError =
false;
929 $this->htmlErrors = ini_set(
'html_errors',
'0' );
930 set_error_handler( [ $this,
'connectionErrorLogger' ] );
939 restore_error_handler();
940 if ( $this->htmlErrors !==
false ) {
941 ini_set(
'html_errors', $this->htmlErrors );
951 if ( $this->lastPhpError ) {
952 $error = preg_replace(
'!\[<a.*</a>\]!',
'', $this->lastPhpError );
953 $error = preg_replace(
'!^.*?:\s?(.*)$!',
'$1', $error );
970 $this->lastPhpError = $errstr;
984 'db_user' => $this->connectionParams[self::CONN_USER],
990 final public function close( $fname = __METHOD__, $owner =
null ) {
993 $wasOpen = (bool)$this->conn;
998 if ( $this->trxAtomicLevels ) {
1001 $error =
"$fname: atomic sections $levels are still open";
1002 } elseif ( $this->trxAutomatic ) {
1006 $error =
"$fname: " .
1007 "expected mass rollback of all peer transactions (DBO_TRX set)";
1012 $error =
"$fname: transaction is still open (from {$this->trxFname})";
1015 if ( $this->trxEndCallbacksSuppressed && $error ===
null ) {
1016 $error =
"$fname: callbacks are suppressed; cannot properly commit";
1020 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
1033 if ( $error !==
null ) {
1037 if ( $this->ownerId !==
null && $owner === $this->ownerId ) {
1038 $this->queryLogger->error( $error );
1052 throw new RuntimeException(
1053 "Transaction callbacks are still pending: " . implode(
', ', $fnames )
1070 if ( !$this->
isOpen() ) {
1084 list( $reason,
$source ) = $info;
1177 '/^\s*(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\(SELECT)\b/i',
1187 return preg_match(
'/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) :
null;
1207 [
'BEGIN',
'ROLLBACK',
'COMMIT',
'SET',
'SHOW',
'CREATE',
'ALTER',
'USE',
'SHOW' ],
1224 static $regexes =
null;
1225 if ( $regexes ===
null ) {
1227 $qts =
'((?:\w+|`\w+`|\'\w+\'|"\w+")(?:\s*,\s*(?:\w+|`\w+`|\'\w+\'|"\w+"))*)';
1231 "/^(INSERT|REPLACE)\s+(?:\w+\s+)*?INTO\s+$qts/i",
1232 "/^(UPDATE)(?:\s+OR\s+\w+|\s+IGNORE|\s+ONLY)?\s+$qts/i",
1233 "/^(DELETE)\s+(?:\w+\s+)*?FROM(?:\s+ONLY)?\s+$qts/i",
1235 "/^(CREATE)\s+TEMPORARY\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+$qts/i",
1236 "/^(DROP)\s+(?:TEMPORARY\s+)?TABLE(?:\s+IF\s+EXISTS)?\s+$qts/i",
1237 "/^(TRUNCATE)\s+(?:TEMPORARY\s+)?TABLE\s+$qts/i",
1238 "/^(ALTER)\s+TABLE\s+$qts/i"
1244 foreach ( $regexes as $regex ) {
1245 if ( preg_match( $regex, $sql, $m, PREG_UNMATCHED_AS_NULL ) ) {
1247 $allTables = preg_split(
'/\s*,\s*/', $m[2] );
1248 foreach ( $allTables as $quotedTable ) {
1249 $queryTables[] = trim( $quotedTable,
"\"'`" );
1255 $tempTableChanges = [];
1256 foreach ( $queryTables as $table ) {
1257 if ( $queryVerb ===
'CREATE' ) {
1261 $tableType = $this->sessionTempTables[$table] ??
null;
1264 if ( $tableType !==
null ) {
1265 $tempTableChanges[] = [ $tableType, $queryVerb, $table ];
1269 return $tempTableChanges;
1277 if ( $ret ===
false ) {
1281 foreach ( $changes as list( $tmpTableType, $verb, $table ) ) {
1284 $this->sessionTempTables[$table] = $tmpTableType;
1287 unset( $this->sessionTempTables[$table] );
1288 unset( $this->sessionDirtyTempTables[$table] );
1291 unset( $this->sessionDirtyTempTables[$table] );
1294 $this->sessionDirtyTempTables[$table] = 1;
1308 $rawTable = $this->
tableName( $table,
'raw' );
1311 isset( $this->sessionTempTables[$rawTable] ) &&
1312 !isset( $this->sessionDirtyTempTables[$rawTable] )
1316 public function query( $sql, $fname = __METHOD__,
$flags = self::QUERY_NORMAL ) {
1323 list( $ret, $err, $errno, $unignorable ) = $this->
executeQuery( $sql, $fname,
$flags );
1324 if ( $ret ===
false ) {
1327 $this->
reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
1356 $priorTransaction = $this->
trxLevel();
1364 $isPermWrite = !$tempTableChanges;
1365 foreach ( $tempTableChanges as list( $tmpType ) ) {
1371 if ( $isPermWrite ) {
1381 $isPermWrite =
false;
1383 $tempTableChanges = [];
1388 $encName = preg_replace(
'/[\x00-\x1F\/]/',
'-',
"$fname {$this->agent}" );
1389 $commentedSql = preg_replace(
'/\s|$/',
" /* $encName */ ", $sql, 1 );
1391 $corruptedTrx =
false;
1397 list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
1402 if ( $ret ===
false && $recoverableCL && $reconnected && $allowRetry ) {
1404 list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
1411 if ( $ret ===
false && $priorTransaction ) {
1412 if ( $recoverableSR ) {
1413 # We're ignoring an error that caused just the current query to be aborted.
1414 # But log the cause so we can log a deprecation notice if a caller actually
1416 $this->trxStatusIgnoredCause = [ $err, $errno, $fname ];
1417 } elseif ( !$recoverableCL ) {
1418 # Either the query was aborted or all queries after BEGIN where aborted.
1419 # In the first case, the only options going forward are (a) ROLLBACK, or
1420 # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1421 # option is ROLLBACK, since the snapshots would have been released.
1422 $corruptedTrx =
true;
1430 return [ $ret, $err, $errno, $corruptedTrx ];
1454 if ( (
$flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
1459 if ( $isPermWrite ) {
1460 $this->lastWriteTime = microtime(
true );
1461 if ( $this->
trxLevel() && !$this->trxDoneWrites ) {
1462 $this->trxDoneWrites =
true;
1463 $this->trxProfiler->transactionWritingIn(
1471 $prefix = $this->topologyRole ?
'query-m: ' :
'query: ';
1472 $generalizedSql =
new GeneralizedSql( $sql, $this->trxShortId, $prefix );
1474 $startTime = microtime(
true );
1475 $ps = $this->profiler
1478 $this->affectedRowCount =
null;
1480 $ret = $this->
doQuery( $commentedSql );
1486 $queryRuntime = max( microtime(
true ) - $startTime, 0.0 );
1488 $recoverableSR =
false;
1489 $recoverableCL =
false;
1490 $reconnected =
false;
1492 if ( $ret !==
false ) {
1493 $this->lastPing = $startTime;
1494 if ( $isPermWrite && $this->
trxLevel() ) {
1496 $this->trxWriteCallers[] = $fname;
1499 # Check if no meaningful session state was lost
1501 # Update session state tracking and try to restore the connection
1504 # Check if only the last query was rolled back
1508 if ( $sql === self::$PING_QUERY ) {
1509 $this->lastRoundTripEstimate = $queryRuntime;
1512 $this->trxProfiler->recordQueryCompletion(
1520 if ( $this->
getFlag( self::DBO_DEBUG ) ) {
1521 $this->queryLogger->debug(
1522 "{method} [{runtime}s] {db_server}: {sql}",
1527 'runtime' => round( $queryRuntime, 3 )
1532 if ( !is_bool( $ret ) && $ret !==
null && !( $ret instanceof
IResultWrapper ) ) {
1534 static::class .
'::doQuery() should return an IResultWrapper' );
1537 return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
1549 $this->
getFlag( self::DBO_TRX ) &&
1552 $this->
begin( __METHOD__ .
" ($fname)", self::TRANSACTION_INTERNAL );
1553 $this->trxAutomatic =
true;
1571 $indicativeOfReplicaRuntime =
true;
1572 if ( $runtime > self::$SLOW_WRITE_SEC ) {
1575 if ( $verb ===
'INSERT' ) {
1577 } elseif ( $verb ===
'REPLACE' ) {
1578 $indicativeOfReplicaRuntime = $this->
affectedRows() > self::$SMALL_WRITE_ROWS / 2;
1582 $this->trxWriteDuration += $runtime;
1583 $this->trxWriteQueryCount += 1;
1584 $this->trxWriteAffectedRows += $affected;
1585 if ( $indicativeOfReplicaRuntime ) {
1586 $this->trxWriteAdjDuration += $runtime;
1587 $this->trxWriteAdjQueryCount += 1;
1601 if ( $verb ===
'USE' ) {
1602 throw new DBUnexpectedError( $this,
"Got USE query; use selectDomain() instead" );
1605 if ( $verb ===
'ROLLBACK' ) {
1609 if ( $this->csmError ) {
1612 "Cannot execute query from $fname while session state is out of sync.\n\n" .
1613 $this->csmError->getMessage() .
"\n" .
1614 $this->csmError->getTraceAsString()
1618 if ( $this->
trxStatus < self::STATUS_TRX_OK ) {
1621 "Cannot execute query from $fname while transaction status is ERROR",
1623 $this->trxStatusCause
1625 } elseif ( $this->
trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1627 call_user_func( $this->deprecationLogger,
1628 "Caller from $fname ignored an error originally raised from $iFname: " .
1629 "[$iLastErrno] $iLastError"
1631 $this->trxStatusIgnoredCause =
null;
1639 "Explicit transaction still active. A caller may have caught an error. "
1655 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1656 # Dropped connections also mean that named locks are automatically released.
1657 # Only allow error suppression in autocommit mode or when the lost transaction
1658 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1659 if ( $this->sessionNamedLocks ) {
1661 } elseif ( $this->sessionTempTables ) {
1663 } elseif ( $sql ===
'COMMIT' ) {
1664 return !$priorWritesPending;
1665 } elseif ( $sql ===
'ROLLBACK' ) {
1669 } elseif ( $priorWritesPending ) {
1683 $this->sessionTempTables = [];
1684 $this->sessionDirtyTempTables = [];
1687 $this->sessionNamedLocks = [];
1690 $this->trxAtomicCounter = 0;
1691 $this->trxPostCommitOrIdleCallbacks = [];
1692 $this->trxPreCommitOrIdleCallbacks = [];
1696 if ( $this->trxDoneWrites ) {
1697 $this->trxProfiler->transactionWritingOut(
1702 $this->trxWriteAffectedRows
1733 $this->trxShortId =
'';
1766 $this->queryLogger->debug(
"SQL ERROR (ignored): $error" );
1782 $this->queryLogger->error(
1783 "Error $errno from $fname, {error} {sql1line} {db_server}",
1785 'method' => __METHOD__,
1788 'sql1line' => mb_substr( str_replace(
"\n",
"\\n", $sql ), 0, 5 * 1024 ),
1790 'exception' =>
new RuntimeException()
1809 return new DBQueryError( $this, $error, $errno, $sql, $fname );
1821 $this->connLogger->error(
1822 "Error connecting to {db_server} as user {db_user}: {error}",
1825 'exception' =>
new RuntimeException()
1840 $table, $var, $cond =
'', $fname = __METHOD__, $options = [], $join_conds = []
1842 if ( $var ===
'*' ) {
1844 } elseif ( is_array( $var ) && count( $var ) !== 1 ) {
1849 $options[
'LIMIT'] = 1;
1851 $res = $this->
select( $table, $var, $cond, $fname, $options, $join_conds );
1852 if (
$res ===
false ) {
1857 if ( $row ===
false ) {
1861 return reset( $row );
1865 $table, $var, $cond =
'', $fname = __METHOD__, $options = [], $join_conds = []
1867 if ( $var ===
'*' ) {
1869 } elseif ( !is_string( $var ) ) {
1874 $res = $this->
select( $table, [
'value' => $var ], $cond, $fname, $options, $join_conds );
1875 if (
$res ===
false ) {
1876 throw new DBUnexpectedError( $this,
"Got false from select()" );
1880 foreach (
$res as $row ) {
1881 $values[] = $row->value;
1899 $preLimitTail = $postLimitTail =
'';
1904 foreach ( $options as $key => $option ) {
1905 if ( is_numeric( $key ) ) {
1906 $noKeyOptions[$option] =
true;
1910 $preLimitTail .= $this->makeGroupByWithHaving( $options );
1912 $preLimitTail .= $this->makeOrderBy( $options );
1914 if ( isset( $noKeyOptions[
'FOR UPDATE'] ) ) {
1915 $postLimitTail .=
' FOR UPDATE';
1918 if ( isset( $noKeyOptions[
'LOCK IN SHARE MODE'] ) ) {
1919 $postLimitTail .=
' LOCK IN SHARE MODE';
1922 if ( isset( $noKeyOptions[
'DISTINCT'] ) || isset( $noKeyOptions[
'DISTINCTROW'] ) ) {
1923 $startOpts .=
'DISTINCT';
1926 # Various MySQL extensions
1927 if ( isset( $noKeyOptions[
'STRAIGHT_JOIN'] ) ) {
1928 $startOpts .=
' /*! STRAIGHT_JOIN */';
1931 if ( isset( $noKeyOptions[
'SQL_BIG_RESULT'] ) ) {
1932 $startOpts .=
' SQL_BIG_RESULT';
1935 if ( isset( $noKeyOptions[
'SQL_BUFFER_RESULT'] ) ) {
1936 $startOpts .=
' SQL_BUFFER_RESULT';
1939 if ( isset( $noKeyOptions[
'SQL_SMALL_RESULT'] ) ) {
1940 $startOpts .=
' SQL_SMALL_RESULT';
1943 if ( isset( $noKeyOptions[
'SQL_CALC_FOUND_ROWS'] ) ) {
1944 $startOpts .=
' SQL_CALC_FOUND_ROWS';
1947 if ( isset( $options[
'USE INDEX'] ) && is_string( $options[
'USE INDEX'] ) ) {
1948 $useIndex = $this->useIndexClause( $options[
'USE INDEX'] );
1952 if ( isset( $options[
'IGNORE INDEX'] ) && is_string( $options[
'IGNORE INDEX'] ) ) {
1953 $ignoreIndex = $this->ignoreIndexClause( $options[
'IGNORE INDEX'] );
1958 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1971 if ( isset( $options[
'GROUP BY'] ) ) {
1972 $gb = is_array( $options[
'GROUP BY'] )
1973 ? implode(
',', $options[
'GROUP BY'] )
1974 : $options[
'GROUP BY'];
1975 $sql .=
' GROUP BY ' . $gb;
1977 if ( isset( $options[
'HAVING'] ) ) {
1978 $having = is_array( $options[
'HAVING'] )
1979 ? $this->makeList( $options[
'HAVING'], self::LIST_AND )
1980 : $options[
'HAVING'];
1981 $sql .=
' HAVING ' . $having;
1996 if ( isset( $options[
'ORDER BY'] ) ) {
1997 $ob = is_array( $options[
'ORDER BY'] )
1998 ? implode(
',', $options[
'ORDER BY'] )
1999 : $options[
'ORDER BY'];
2001 return ' ORDER BY ' . $ob;
2008 $table, $vars, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2010 $sql = $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
2012 return $this->query( $sql, $fname, self::QUERY_CHANGE_NONE );
2020 $options = [], $join_conds = []
2022 if ( is_array( $vars ) ) {
2023 $fields = implode(
',', $this->fieldNamesWithAlias( $vars ) );
2028 $options = (array)$options;
2029 $useIndexes = ( isset( $options[
'USE INDEX'] ) && is_array( $options[
'USE INDEX'] ) )
2030 ? $options[
'USE INDEX']
2033 isset( $options[
'IGNORE INDEX'] ) &&
2034 is_array( $options[
'IGNORE INDEX'] )
2036 ? $options[
'IGNORE INDEX']
2040 $this->selectOptionsIncludeLocking( $options ) &&
2041 $this->selectFieldsOrOptionsAggregate( $vars, $options )
2046 $this->deprecationLogger,
2047 __METHOD__ .
": aggregation used with a locking SELECT ($fname)"
2051 if ( is_array( $table ) ) {
2052 if ( count( $table ) === 0 ) {
2056 $this->tableNamesWithIndexClauseOrJOIN(
2057 $table, $useIndexes, $ignoreIndexes, $join_conds );
2059 } elseif ( $table !=
'' ) {
2061 $this->tableNamesWithIndexClauseOrJOIN(
2062 [ $table ], $useIndexes, $ignoreIndexes, [] );
2067 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
2068 $this->makeSelectOptions( $options );
2070 if ( is_array( $conds ) ) {
2071 $conds = $this->makeList( $conds, self::LIST_AND );
2074 if ( $conds ===
null || $conds ===
false ) {
2075 $this->queryLogger->warning(
2079 .
' with incorrect parameters: $conds must be a string or an array'
2084 if ( $conds ===
'' || $conds ===
'*' ) {
2085 $sql =
"SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
2086 } elseif ( is_string( $conds ) ) {
2087 $sql =
"SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
2088 "WHERE $conds $preLimitTail";
2090 throw new DBUnexpectedError( $this, __METHOD__ .
' called with incorrect parameters' );
2093 if ( isset( $options[
'LIMIT'] ) ) {
2094 $sql = $this->limitResult( $sql, $options[
'LIMIT'],
2095 $options[
'OFFSET'] ??
false );
2097 $sql =
"$sql $postLimitTail";
2099 if ( isset( $options[
'EXPLAIN'] ) ) {
2100 $sql =
'EXPLAIN ' . $sql;
2106 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
2107 $options = [], $join_conds = []
2109 $options = (array)$options;
2110 $options[
'LIMIT'] = 1;
2112 $res = $this->select( $table, $vars, $conds, $fname, $options, $join_conds );
2113 if (
$res ===
false ) {
2117 if ( !$this->numRows(
$res ) ) {
2121 return $this->fetchObject(
$res );
2129 $tables, $var =
'*', $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2131 $conds = $this->normalizeConditions( $conds, $fname );
2132 $column = $this->extractSingleFieldFromList( $var );
2133 if ( is_string( $column ) && !in_array( $column, [
'*',
'1' ] ) ) {
2134 $conds[] =
"$column IS NOT NULL";
2137 $res = $this->select(
2138 $tables, [
'rowcount' =>
'COUNT(*)' ], $conds, $fname, $options, $join_conds
2140 $row =
$res ? $this->fetchRow(
$res ) : [];
2142 return isset( $row[
'rowcount'] ) ? (int)$row[
'rowcount'] : 0;
2146 $tables, $var =
'*', $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2148 $conds = $this->normalizeConditions( $conds, $fname );
2149 $column = $this->extractSingleFieldFromList( $var );
2150 if ( is_string( $column ) && !in_array( $column, [
'*',
'1' ] ) ) {
2151 $conds[] =
"$column IS NOT NULL";
2154 $res = $this->select(
2156 'tmp_count' => $this->buildSelectSubquery(
2165 [
'rowcount' =>
'COUNT(*)' ],
2169 $row =
$res ? $this->fetchRow(
$res ) : [];
2171 return isset( $row[
'rowcount'] ) ? (int)$row[
'rowcount'] : 0;
2179 $options = (array)$options;
2180 foreach ( [
'FOR UPDATE',
'LOCK IN SHARE MODE' ] as $lock ) {
2181 if ( in_array( $lock, $options,
true ) ) {
2195 foreach ( (array)$options as $key => $value ) {
2196 if ( is_string( $key ) ) {
2197 if ( preg_match(
'/^(?:GROUP BY|HAVING)$/i', $key ) ) {
2200 } elseif ( is_string( $value ) ) {
2201 if ( preg_match(
'/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
2207 $regex =
'/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
2208 foreach ( (array)$fields as $field ) {
2209 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
2223 if ( !$rowOrRows ) {
2225 } elseif ( isset( $rowOrRows[0] ) ) {
2228 $rows = [ $rowOrRows ];
2231 foreach ( $rows as $row ) {
2232 if ( !is_array( $row ) ) {
2234 } elseif ( !$row ) {
2249 if ( $conds ===
null || $conds ===
false ) {
2250 $this->queryLogger->warning(
2254 .
' with incorrect parameters: $conds must be a string or an array'
2257 } elseif ( $conds ===
'' ) {
2261 return is_array( $conds ) ? $conds : [ $conds ];
2274 $rows = $this->normalizeRowArray( $rows );
2278 if ( !$uniqueKeys ) {
2280 $this->queryLogger->warning(
2281 "upsert/replace called with no unique key",
2282 [
'exception' =>
new RuntimeException() ]
2286 $identityKey = $this->normalizeUpsertKeys( $uniqueKeys );
2287 if ( $identityKey ) {
2288 $allDefaultKeyValues = $this->assertValidUpsertRowArray( $rows, $identityKey );
2289 if ( $allDefaultKeyValues ) {
2292 $this->queryLogger->warning(
2293 "upsert/replace called with all-null values for unique key",
2294 [
'exception' =>
new RuntimeException() ]
2299 return $identityKey;
2309 if ( is_string( $uniqueKeys ) ) {
2310 return [ $uniqueKeys ];
2311 } elseif ( !is_array( $uniqueKeys ) ) {
2314 if ( count( $uniqueKeys ) !== 1 || !isset( $uniqueKeys[0] ) ) {
2316 "The unique key array should contain a single unique index" );
2319 $uniqueKey = $uniqueKeys[0];
2320 if ( is_string( $uniqueKey ) ) {
2323 $this->queryLogger->warning( __METHOD__ .
2324 " called with deprecated parameter style: " .
2325 "the unique key array should be a string or array of string arrays",
2326 [
'exception' =>
new RuntimeException() ] );
2328 } elseif ( is_array( $uniqueKey ) ) {
2342 if ( is_array( $options ) ) {
2344 } elseif ( is_string( $options ) ) {
2345 return ( $options ===
'' ) ? [] : [ $options ];
2347 throw new DBUnexpectedError( $this, __METHOD__ .
': expected string or array' );
2359 foreach ( $rows as $row ) {
2360 foreach ( $identityKey as $column ) {
2361 $numNulls += ( isset( $row[$column] ) ? 0 : 1 );
2367 $numNulls !== ( count( $rows ) * count( $identityKey ) )
2371 "NULL/absent values for unique key (" . implode(
',', $identityKey ) .
")"
2375 return (
bool)$numNulls;
2391 $soleRow = ( count( $rows ) == 1 ) ? reset( $rows ) :
null;
2395 foreach ( $set as $k => $v ) {
2396 if ( is_string( $k ) ) {
2398 if ( in_array( $k, $identityKey,
true ) ) {
2399 if ( $soleRow && array_key_exists( $k, $soleRow ) && $soleRow[$k] === $v ) {
2400 $this->queryLogger->warning(
2401 __METHOD__ .
" called with redundant assignment to column '$k'",
2402 [
'exception' =>
new RuntimeException() ]
2407 "Cannot reassign column '$k' since it belongs to identity key"
2411 } elseif ( preg_match(
'/^([a-zA-Z0-9_]+)\s*=/', $v, $m ) ) {
2413 if ( in_array( $m[1], $identityKey,
true ) ) {
2416 "Cannot reassign column '{$m[1]}' since it belongs to identity key"
2430 foreach ( array_keys( $options, $option,
true ) as $k ) {
2431 if ( is_int( $k ) ) {
2444 if ( is_array( $var ) ) {
2447 } elseif ( count( $var ) == 1 ) {
2448 $column = $var[0] ?? reset( $var );
2460 $table, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2462 if ( !$this->trxLevel() && !$this->getFlag( self::DBO_TRX ) ) {
2465 __METHOD__ .
': no transaction is active nor is DBO_TRX set'
2469 $options = (array)$options;
2470 $options[] =
'FOR UPDATE';
2472 return $this->selectRowCount( $table,
'*', $conds, $fname, $options, $join_conds );
2476 $info = $this->fieldInfo( $table, $field );
2482 if ( !$this->tableExists( $table, $fname ) ) {
2486 $info = $this->indexInfo( $table, $index, $fname );
2487 if ( $info ===
null ) {
2490 return $info !==
false;
2501 $indexInfo = $this->indexInfo( $table, $index, $fname );
2503 if ( !$indexInfo ) {
2507 return !$indexInfo[0]->Non_unique;
2510 public function insert( $table, $rows, $fname = __METHOD__, $options = [] ) {
2511 $rows = $this->normalizeRowArray( $rows );
2516 $options = $this->normalizeOptions( $options );
2517 if ( $this->isFlagInOptions(
'IGNORE', $options ) ) {
2518 $this->doInsertNonConflicting( $table, $rows, $fname );
2520 $this->doInsert( $table, $rows, $fname );
2534 protected function doInsert( $table, array $rows, $fname ) {
2535 $encTable = $this->tableName( $table );
2536 list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
2538 $sql =
"INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples";
2540 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2552 $encTable = $this->tableName( $table );
2553 list( $sqlColumns, $sqlTuples ) = $this->makeInsertLists( $rows );
2554 list( $sqlVerb, $sqlOpts ) = $this->makeInsertNonConflictingVerbAndOptions();
2556 $sql = rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" );
2558 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2567 return [
'INSERT IGNORE INTO',
'' ];
2581 $firstRow = $rows[0];
2582 if ( !is_array( $firstRow ) || !$firstRow ) {
2586 $tupleColumns = array_keys( $firstRow );
2589 foreach ( $rows as $row ) {
2590 $rowColumns = array_keys( $row );
2592 if ( $rowColumns !== $tupleColumns ) {
2595 'Got row columns (' . implode(
', ', $rowColumns ) .
') ' .
2596 'instead of expected (' . implode(
', ', $tupleColumns ) .
')'
2600 $valueTuples[] =
'(' . $this->makeList( $row, self::LIST_COMMA ) .
')';
2604 $this->makeList( $tupleColumns, self::LIST_NAMES ),
2605 implode(
',', $valueTuples )
2617 $options = $this->normalizeOptions( $options );
2621 if ( in_array(
'IGNORE', $options ) ) {
2636 $opts = $this->makeUpdateOptionsArray( $options );
2638 return implode(
' ', $opts );
2641 public function update( $table, $set, $conds, $fname = __METHOD__, $options = [] ) {
2642 $this->assertConditionIsNotEmpty( $conds, __METHOD__,
true );
2643 $table = $this->tableName( $table );
2644 $opts = $this->makeUpdateOptions( $options );
2645 $sql =
"UPDATE $opts $table SET " . $this->makeList( $set, self::LIST_SET );
2647 if ( $conds && $conds !== IDatabase::ALL_ROWS ) {
2648 if ( is_array( $conds ) ) {
2649 $conds = $this->makeList( $conds, self::LIST_AND );
2651 $sql .=
' WHERE ' . $conds;
2654 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2659 public function makeList( array $a, $mode = self::LIST_COMMA ) {
2663 foreach ( $a as $field => $value ) {
2667 if ( $mode == self::LIST_AND ) {
2669 } elseif ( $mode == self::LIST_OR ) {
2676 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2677 $list .=
"($value)";
2678 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2681 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2684 $includeNull =
false;
2685 foreach ( array_keys( $value,
null,
true ) as $nullKey ) {
2686 $includeNull =
true;
2687 unset( $value[$nullKey] );
2689 if ( count( $value ) == 0 && !$includeNull ) {
2690 throw new InvalidArgumentException(
2691 __METHOD__ .
": empty input for field $field" );
2692 } elseif ( count( $value ) == 0 ) {
2694 $list .=
"$field IS NULL";
2697 if ( $includeNull ) {
2701 if ( count( $value ) == 1 ) {
2705 $value = array_values( $value )[0];
2706 $list .= $field .
" = " . $this->addQuotes( $value );
2708 $list .= $field .
" IN (" . $this->makeList( $value ) .
") ";
2711 if ( $includeNull ) {
2712 $list .=
" OR $field IS NULL)";
2715 } elseif ( $value ===
null ) {
2716 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2717 $list .=
"$field IS ";
2718 } elseif ( $mode == self::LIST_SET ) {
2719 $list .=
"$field = ";
2724 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2726 $list .=
"$field = ";
2728 $list .= $mode == self::LIST_NAMES ? $value : $this->addQuotes( $value );
2738 foreach ( $data as
$base => $sub ) {
2739 if ( count( $sub ) ) {
2740 $conds[] = $this->makeList(
2741 [ $baseKey =>
$base, $subKey => array_map(
'strval', array_keys( $sub ) ) ],
2748 return $this->makeList( $conds, self::LIST_OR );
2775 public function bitAnd( $fieldLeft, $fieldRight ) {
2776 return "($fieldLeft & $fieldRight)";
2783 public function bitOr( $fieldLeft, $fieldRight ) {
2784 return "($fieldLeft | $fieldRight)";
2792 return 'CONCAT(' . implode(
',', $stringList ) .
')';
2800 $delim, $table, $field, $conds =
'', $join_conds = []
2802 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->addQuotes( $delim ) .
')';
2804 return '(' . $this->selectSQLText( $table, $fld, $conds,
null, [], $join_conds ) .
')';
2812 return $this->buildSuperlative(
'GREATEST', $fields, $values );
2820 return $this->buildSuperlative(
'LEAST', $fields, $values );
2840 $fields = is_array( $fields ) ? $fields : [ $fields ];
2841 $values = is_array( $values ) ? $values : [ $values ];
2844 foreach ( $fields as $alias => $field ) {
2845 if ( is_int( $alias ) ) {
2846 $encValues[] = $this->addIdentifierQuotes( $field );
2848 $encValues[] = $field;
2851 foreach ( $values as $value ) {
2852 if ( is_int( $value ) || is_float( $value ) ) {
2853 $encValues[] = $value;
2854 } elseif ( is_string( $value ) ) {
2855 $encValues[] = $this->addQuotes( $value );
2856 } elseif ( $value ===
null ) {
2863 return $sqlfunc .
'(' . implode(
',', $encValues ) .
')';
2871 $this->assertBuildSubstringParams( $startPosition, $length );
2872 $functionBody =
"$input FROM $startPosition";
2873 if ( $length !==
null ) {
2874 $functionBody .=
" FOR $length";
2876 return 'SUBSTRING(' . $functionBody .
')';
2892 if ( $startPosition === 0 ) {
2894 throw new InvalidArgumentException(
'Use 1 as $startPosition for the beginning of the string' );
2896 if ( !is_int( $startPosition ) || $startPosition < 0 ) {
2897 throw new InvalidArgumentException(
2898 '$startPosition must be a positive integer'
2901 if ( !( is_int( $length ) && $length >= 0 || $length ===
null ) ) {
2902 throw new InvalidArgumentException(
2903 '$length must be null or an integer greater than or equal to 0'
2922 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
2923 if ( !$isCondValid ) {
2925 wfDeprecated( $fname .
' called with empty $conds',
'1.35',
false, 3 );
2939 return "CAST( $field AS CHARACTER )";
2947 return 'CAST( ' . $field .
' AS INTEGER )';
2951 $table, $vars, $conds =
'', $fname = __METHOD__,
2952 $options = [], $join_conds = []
2955 $this->selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2970 $this->currentDomain->getSchema(),
2971 $this->currentDomain->getTablePrefix()
2978 $cs = $this->commenceCriticalSection( __METHOD__ );
2983 $this->completeCriticalSection( __METHOD__, $cs );
2987 $this->completeCriticalSection( __METHOD__, $cs );
2998 $this->currentDomain = $domain;
3002 return $this->currentDomain->getDatabase();
3006 return $this->connectionParams[self::CONN_HOST] ??
null;
3010 return $this->serverName ?? $this->getServer();
3021 __METHOD__ .
': got Subquery instance when expecting a string'
3025 # Skip the entire process when we have a string quoted on both ends.
3026 # Note that we check the end so that we will still quote any use of
3027 # use of `database`.table. But won't break things if someone wants
3028 # to query a database table with a dot in the name.
3029 if ( $this->isQuotedIdentifier( $name ) ) {
3033 # Lets test for any bits of text that should never show up in a table
3034 # name. Basically anything like JOIN or ON which are actually part of
3035 # SQL queries, but may end up inside of the table value to combine
3036 # sql. Such as how the API is doing.
3037 # Note that we use a whitespace test rather than a \b test to avoid
3038 # any remote case where a word like on may be inside of a table name
3039 # surrounded by symbols which may be considered word breaks.
3040 if ( preg_match(
'/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
3041 $this->queryLogger->warning(
3042 __METHOD__ .
": use of subqueries is not supported this way",
3043 [
'exception' =>
new RuntimeException() ]
3049 # Split database and table into proper variables.
3050 list( $database, $schema, $prefix, $table ) = $this->qualifiedTableComponents( $name );
3052 # Quote $table and apply the prefix if not quoted.
3053 # $tableName might be empty if this is called from Database::replaceVars()
3054 $tableName =
"{$prefix}{$table}";
3055 if ( $format ===
'quoted'
3056 && !$this->isQuotedIdentifier( $tableName )
3057 && $tableName !==
''
3059 $tableName = $this->addIdentifierQuotes( $tableName );
3062 # Quote $schema and $database and merge them with the table name if needed
3063 $tableName = $this->prependDatabaseOrSchema( $schema, $tableName, $format );
3064 $tableName = $this->prependDatabaseOrSchema( $database, $tableName, $format );
3076 # We reverse the explode so that database.table and table both output the correct table.
3077 $dbDetails = explode(
'.', $name, 3 );
3078 if ( count( $dbDetails ) == 3 ) {
3079 list( $database, $schema, $table ) = $dbDetails;
3080 # We don't want any prefix added in this case
3082 } elseif ( count( $dbDetails ) == 2 ) {
3083 list( $database, $table ) = $dbDetails;
3084 # We don't want any prefix added in this case
3086 # In dbs that support it, $database may actually be the schema
3087 # but that doesn't affect any of the functionality here
3090 list( $table ) = $dbDetails;
3091 if ( isset( $this->tableAliases[$table] ) ) {
3092 $database = $this->tableAliases[$table][
'dbname'];
3093 $schema = is_string( $this->tableAliases[$table][
'schema'] )
3094 ? $this->tableAliases[$table][
'schema']
3095 : $this->relationSchemaQualifier();
3096 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
3097 ? $this->tableAliases[$table][
'prefix']
3098 : $this->tablePrefix();
3101 $schema = $this->relationSchemaQualifier(); # Default schema
3102 $prefix = $this->tablePrefix(); # Default prefix
3106 return [ $database, $schema, $prefix, $table ];
3116 if ( $namespace !==
null && $namespace !==
'' ) {
3117 if ( $format ===
'quoted' && !$this->isQuotedIdentifier( $namespace ) ) {
3118 $namespace = $this->addIdentifierQuotes( $namespace );
3120 $relation = $namespace .
'.' . $relation;
3129 foreach ( $tables as $name ) {
3130 $retVal[$name] = $this->tableName( $name );
3139 foreach ( $tables as $name ) {
3140 $retVal[] = $this->tableName( $name );
3158 if ( is_string( $table ) ) {
3159 $quotedTable = $this->tableName( $table );
3160 } elseif ( $table instanceof
Subquery ) {
3161 $quotedTable = (string)$table;
3163 throw new InvalidArgumentException(
"Table must be a string or Subquery" );
3166 if ( $alias ===
false || $alias === $table ) {
3167 if ( $table instanceof
Subquery ) {
3168 throw new InvalidArgumentException(
"Subquery table missing alias" );
3171 return $quotedTable;
3173 return $quotedTable .
' ' . $this->addIdentifierQuotes( $alias );
3187 if ( !$alias || (
string)$alias === (
string)$name ) {
3190 return $name .
' AS ' . $this->addIdentifierQuotes( $alias );
3202 foreach ( $fields as $alias => $field ) {
3203 if ( is_numeric( $alias ) ) {
3206 $retval[] = $this->fieldNameWithAlias( $field, $alias );
3230 $use_index = (array)$use_index;
3231 $ignore_index = (array)$ignore_index;
3232 $join_conds = (array)$join_conds;
3234 foreach ( $tables as $alias => $table ) {
3235 if ( !is_string( $alias ) ) {
3240 if ( is_array( $table ) ) {
3242 if ( count( $table ) > 1 ) {
3243 $joinedTable =
'(' .
3244 $this->tableNamesWithIndexClauseOrJOIN(
3245 $table, $use_index, $ignore_index, $join_conds ) .
')';
3248 $innerTable = reset( $table );
3249 $innerAlias = key( $table );
3250 $joinedTable = $this->tableNameWithAlias(
3252 is_string( $innerAlias ) ? $innerAlias : $innerTable
3256 $joinedTable = $this->tableNameWithAlias( $table, $alias );
3260 if ( isset( $join_conds[$alias] ) ) {
3261 Assert::parameterType(
'array', $join_conds[$alias],
"join_conds[$alias]" );
3262 list( $joinType, $conds ) = $join_conds[$alias];
3263 $tableClause = $joinType;
3264 $tableClause .=
' ' . $joinedTable;
3265 if ( isset( $use_index[$alias] ) ) {
3266 $use = $this->useIndexClause( implode(
',', (array)$use_index[$alias] ) );
3268 $tableClause .=
' ' . $use;
3271 if ( isset( $ignore_index[$alias] ) ) {
3272 $ignore = $this->ignoreIndexClause(
3273 implode(
',', (array)$ignore_index[$alias] ) );
3274 if ( $ignore !=
'' ) {
3275 $tableClause .=
' ' . $ignore;
3278 $on = $this->makeList( (array)$conds, self::LIST_AND );
3280 $tableClause .=
' ON (' . $on .
')';
3283 $retJOIN[] = $tableClause;
3284 } elseif ( isset( $use_index[$alias] ) ) {
3286 $tableClause = $joinedTable;
3287 $tableClause .=
' ' . $this->useIndexClause(
3288 implode(
',', (array)$use_index[$alias] )
3291 $ret[] = $tableClause;
3292 } elseif ( isset( $ignore_index[$alias] ) ) {
3294 $tableClause = $joinedTable;
3295 $tableClause .=
' ' . $this->ignoreIndexClause(
3296 implode(
',', (array)$ignore_index[$alias] )
3299 $ret[] = $tableClause;
3301 $tableClause = $joinedTable;
3303 $ret[] = $tableClause;
3308 $implicitJoins = implode(
',', $ret );
3309 $explicitJoins = implode(
' ', $retJOIN );
3312 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
3322 return $this->indexAliases[$index] ?? $index;
3330 if (
$s instanceof
Blob ) {
3333 if (
$s ===
null ) {
3335 } elseif ( is_bool(
$s ) ) {
3336 return (
string)(int)
$s;
3337 } elseif ( is_int(
$s ) ) {
3340 return "'" . $this->strencode(
$s ) .
"'";
3349 return '"' . str_replace(
'"',
'""',
$s ) .
'"';
3363 return $name[0] ==
'"' && substr( $name, -1, 1 ) ==
'"';
3374 [ $escapeChar,
'%',
'_' ],
3375 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
3385 if ( is_array( $param ) ) {
3388 $params = func_get_args();
3399 foreach ( $params as $value ) {
3401 $s .= $value->toString();
3403 $s .= $this->escapeLikeInternal( $value, $escapeChar );
3408 $this->addQuotes(
$s ) .
' ESCAPE ' . $this->addQuotes( $escapeChar ) .
' ';
3453 public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
3454 $identityKey = $this->normalizeUpsertParams( $uniqueKeys, $rows );
3458 if ( $identityKey ) {
3459 $this->doReplace( $table, $identityKey, $rows, $fname );
3461 $this->doInsert( $table, $rows, $fname );
3474 protected function doReplace( $table, array $identityKey, array $rows, $fname ) {
3475 $affectedRowCount = 0;
3476 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3478 foreach ( $rows as $row ) {
3480 $sqlCondition = $this->makeKeyCollisionCondition( [ $row ], $identityKey );
3481 $this->
delete( $table, [ $sqlCondition ], $fname );
3482 $affectedRowCount += $this->affectedRows();
3484 $this->insert( $table, $row, $fname );
3485 $affectedRowCount += $this->affectedRows();
3487 $this->endAtomic( $fname );
3489 $this->cancelAtomic( $fname );
3492 $this->affectedRowCount = $affectedRowCount;
3505 } elseif ( !$uniqueKey ) {
3509 if ( count( $uniqueKey ) == 1 ) {
3511 $column = reset( $uniqueKey );
3512 $values = array_column( $rows, $column );
3513 if ( count( $values ) !== count( $rows ) ) {
3514 throw new DBUnexpectedError( $this,
"Missing values for unique key ($column)" );
3517 return $this->makeList( [ $column => $values ], self::LIST_AND );
3520 $nullByUniqueKeyColumn = array_fill_keys( $uniqueKey,
null );
3523 foreach ( $rows as $row ) {
3524 $rowKeyMap = array_intersect_key( $row, $nullByUniqueKeyColumn );
3525 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
3528 "Missing values for unique key (" . implode(
',', $uniqueKey ) .
")"
3531 $orConds[] = $this->makeList( $rowKeyMap, self::LIST_AND );
3534 return count( $orConds ) > 1
3535 ? $this->makeList( $orConds, self::LIST_OR )
3539 public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__ ) {
3540 $identityKey = $this->normalizeUpsertParams( $uniqueKeys, $rows );
3544 if ( $identityKey ) {
3545 $this->assertValidUpsertSetArray( $set, $identityKey, $rows );
3546 $this->doUpsert( $table, $rows, $identityKey, $set, $fname );
3548 $this->doInsert( $table, $rows, $fname );
3571 $affectedRowCount = 0;
3572 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3574 foreach ( $rows as $row ) {
3576 $sqlConditions = $this->makeKeyCollisionCondition( [ $row ], $identityKey );
3577 $this->update( $table, $set, [ $sqlConditions ], $fname );
3578 $rowsUpdated = $this->affectedRows();
3579 $affectedRowCount += $rowsUpdated;
3580 if ( $rowsUpdated <= 0 ) {
3582 $this->insert( $table, $row, $fname );
3583 $affectedRowCount += $this->affectedRows();
3586 $this->endAtomic( $fname );
3588 $this->cancelAtomic( $fname );
3591 $this->affectedRowCount = $affectedRowCount;
3610 $delTable = $this->tableName( $delTable );
3611 $joinTable = $this->tableName( $joinTable );
3612 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
3613 if ( $conds !=
'*' ) {
3614 $sql .=
'WHERE ' . $this->makeList( $conds, self::LIST_AND );
3618 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3626 $table = $this->tableName( $table );
3627 $sql =
"SHOW COLUMNS FROM $table LIKE \"$field\"";
3628 $res = $this->query( $sql, __METHOD__, self::QUERY_CHANGE_NONE );
3629 $row = $this->fetchObject(
$res );
3633 if ( preg_match(
'/\((.*)\)/', $row->Type, $m ) ) {
3642 public function delete( $table, $conds, $fname = __METHOD__ ) {
3643 $this->assertConditionIsNotEmpty( $conds, __METHOD__,
false );
3645 $table = $this->tableName( $table );
3646 $sql =
"DELETE FROM $table";
3648 if ( $conds !== IDatabase::ALL_ROWS ) {
3649 if ( is_array( $conds ) ) {
3650 $conds = $this->makeList( $conds, self::LIST_AND );
3652 $sql .=
' WHERE ' . $conds;
3655 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3665 $fname = __METHOD__,
3666 $insertOptions = [],
3667 $selectOptions = [],
3668 $selectJoinConds = []
3670 static $hints = [
'NO_AUTO_COLUMNS' ];
3672 $insertOptions = $this->normalizeOptions( $insertOptions );
3673 $selectOptions = $this->normalizeOptions( $selectOptions );
3675 if ( $this->cliMode && $this->isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
3678 $this->doInsertSelectNative(
3684 array_diff( $insertOptions, $hints ),
3689 $this->doInsertSelectGeneric(
3695 array_diff( $insertOptions, $hints ),
3735 array $insertOptions,
3736 array $selectOptions,
3743 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3744 $fields[] = $this->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
3746 $res = $this->select(
3748 implode(
',', $fields ),
3751 array_merge( $selectOptions, [
'FOR UPDATE' ] ),
3758 $affectedRowCount = 0;
3759 $this->startAtomic( $fname, self::ATOMIC_CANCELABLE );
3762 foreach (
$res as $row ) {
3763 $rows[] = (array)$row;
3766 $rowBatches = array_chunk( $rows, $this->nonNativeInsertSelectBatchSize );
3767 foreach ( $rowBatches as $rows ) {
3768 $this->insert( $destTable, $rows, $fname, $insertOptions );
3769 $affectedRowCount += $this->affectedRows();
3772 $this->cancelAtomic( $fname );
3775 $this->endAtomic( $fname );
3776 $this->affectedRowCount = $affectedRowCount;
3800 array $insertOptions,
3801 array $selectOptions,
3804 list( $sqlVerb, $sqlOpts ) = $this->isFlagInOptions(
'IGNORE', $insertOptions )
3805 ? $this->makeInsertNonConflictingVerbAndOptions()
3806 : [
'INSERT INTO',
'' ];
3807 $encDstTable = $this->tableName( $destTable );
3808 $sqlDstColumns = implode(
',', array_keys( $varMap ) );
3809 $selectSql = $this->selectSQLText(
3811 array_values( $varMap ),
3818 $sql = rtrim(
"$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
3820 $this->query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3828 if ( !is_numeric( $limit ) ) {
3831 "Invalid non-numeric limit passed to " . __METHOD__
3836 return "$sql LIMIT "
3837 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
3854 $glue = $all ?
') UNION ALL (' :
') UNION (';
3856 return '(' . implode( $glue, $sqls ) .
')';
3862 array $permute_conds,
3864 $fname = __METHOD__,
3870 foreach ( $permute_conds as $field => $values ) {
3875 $values = array_unique( $values );
3877 foreach ( $conds as $cond ) {
3878 foreach ( $values as $value ) {
3879 $cond[$field] = $value;
3880 $newConds[] = $cond;
3886 $extra_conds = $extra_conds ===
'' ? [] : (array)$extra_conds;
3890 if ( count( $conds ) === 1 &&
3891 ( !isset( $options[
'INNER ORDER BY'] ) || !$this->unionSupportsOrderAndLimit() )
3893 return $this->selectSQLText(
3894 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3902 $orderBy = $this->makeOrderBy( $options );
3903 $limit = $options[
'LIMIT'] ??
null;
3904 $offset = $options[
'OFFSET'] ??
false;
3905 $all = empty( $options[
'NOTALL'] ) && !in_array(
'NOTALL', $options );
3906 if ( !$this->unionSupportsOrderAndLimit() ) {
3907 unset( $options[
'ORDER BY'], $options[
'LIMIT'], $options[
'OFFSET'] );
3909 if ( array_key_exists(
'INNER ORDER BY', $options ) ) {
3910 $options[
'ORDER BY'] = $options[
'INNER ORDER BY'];
3912 if ( $limit !==
null && is_numeric( $offset ) && $offset != 0 ) {
3916 $options[
'LIMIT'] = $limit + $offset;
3917 unset( $options[
'OFFSET'] );
3922 foreach ( $conds as $cond ) {
3923 $sqls[] = $this->selectSQLText(
3924 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3927 $sql = $this->unionQueries( $sqls, $all ) . $orderBy;
3928 if ( $limit !==
null ) {
3929 $sql = $this->limitResult( $sql, $limit, $offset );
3939 public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
3940 if ( is_array( $cond ) ) {
3941 $cond = $this->makeList( $cond, self::LIST_AND );
3944 return "(CASE WHEN $cond THEN $caseTrueExpression ELSE $caseFalseExpression END)";
3952 return "REPLACE({$orig}, {$old}, {$new})";
3984 return $this->wasConnectionError( $this->lastErrno() );
3997 $this->wasDeadlock() ||
3998 $this->wasLockTimeout() ||
3999 $this->wasConnectionLoss()
4030 $function = array_shift(
$args );
4031 $tries = self::$DEADLOCK_TRIES;
4033 $this->begin( __METHOD__ );
4040 $retVal = $function( ...
$args );
4043 if ( $this->wasDeadlock() ) {
4045 usleep( mt_rand( self::$DEADLOCK_DELAY_MIN, self::$DEADLOCK_DELAY_MAX ) );
4051 }
while ( --$tries > 0 );
4053 if ( $tries <= 0 ) {
4055 $this->rollback( __METHOD__ );
4058 $this->commit( __METHOD__ );
4070 # Real waits are implemented in the subclass.
4079 return $this->primaryPosWait( $pos, $timeout );
4102 return $this->getPrimaryPos();
4114 if ( !$this->trxLevel() ) {
4117 $this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
4121 if ( !$this->trxLevel() && $this->getTransactionRoundId() ) {
4125 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
4128 $this->trxPostCommitOrIdleCallbacks[] = [
4131 $this->currentAtomicSectionId()
4134 if ( !$this->trxLevel() ) {
4136 $this->runOnTransactionIdleCallbacks( self::TRIGGER_IDLE, $dbErrors );
4144 $this->onTransactionCommitOrIdle( $callback, $fname );
4148 if ( !$this->trxLevel() && $this->getTransactionRoundId() ) {
4152 $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
4155 if ( $this->trxLevel() ) {
4156 $this->trxPreCommitOrIdleCallbacks[] = [
4159 $this->currentAtomicSectionId()
4163 $this->startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
4166 }
catch ( Throwable $e ) {
4168 if ( !$this->csmError ) {
4169 $this->cancelAtomic( __METHOD__ );
4173 $this->endAtomic( __METHOD__ );
4178 if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
4181 $this->trxSectionCancelCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
4188 if ( $this->trxLevel() && $this->trxAtomicLevels ) {
4189 $levelInfo = end( $this->trxAtomicLevels );
4191 return $levelInfo[1];
4207 foreach ( $this->trxPreCommitOrIdleCallbacks as $key => $info ) {
4208 if ( $info[2] === $old ) {
4209 $this->trxPreCommitOrIdleCallbacks[$key][2] = $new;
4212 foreach ( $this->trxPostCommitOrIdleCallbacks as $key => $info ) {
4213 if ( $info[2] === $old ) {
4214 $this->trxPostCommitOrIdleCallbacks[$key][2] = $new;
4217 foreach ( $this->trxEndCallbacks as $key => $info ) {
4218 if ( $info[2] === $old ) {
4219 $this->trxEndCallbacks[$key][2] = $new;
4222 foreach ( $this->trxSectionCancelCallbacks as $key => $info ) {
4223 if ( $info[2] === $old ) {
4224 $this->trxSectionCancelCallbacks[$key][2] = $new;
4253 $this->trxPostCommitOrIdleCallbacks = array_filter(
4254 $this->trxPostCommitOrIdleCallbacks,
4255 static function ( $entry ) use ( $sectionIds ) {
4256 return !in_array( $entry[2], $sectionIds,
true );
4259 $this->trxPreCommitOrIdleCallbacks = array_filter(
4260 $this->trxPreCommitOrIdleCallbacks,
4261 static function ( $entry ) use ( $sectionIds ) {
4262 return !in_array( $entry[2], $sectionIds,
true );
4266 foreach ( $this->trxEndCallbacks as $key => $entry ) {
4267 if ( in_array( $entry[2], $sectionIds,
true ) ) {
4268 $callback = $entry[0];
4269 $this->trxEndCallbacks[$key][0] =
function () use ( $callback ) {
4270 return $callback( self::TRIGGER_ROLLBACK, $this );
4273 $this->trxEndCallbacks[$key][2] =
null;
4277 foreach ( $this->trxSectionCancelCallbacks as $key => $entry ) {
4278 if ( in_array( $entry[2], $sectionIds,
true ) ) {
4279 $this->trxSectionCancelCallbacks[$key][2] = $newSectionId;
4286 $this->trxRecurringCallbacks[$name] = $callback;
4288 unset( $this->trxRecurringCallbacks[$name] );
4301 $this->trxEndCallbacksSuppressed = $suppress;
4317 if ( $this->trxLevel() ) {
4318 throw new DBUnexpectedError( $this, __METHOD__ .
': a transaction is still open' );
4321 if ( $this->trxEndCallbacksSuppressed ) {
4326 $cs = $this->commenceCriticalSection( __METHOD__ );
4329 $autoTrx = $this->getFlag( self::DBO_TRX );
4332 $callbackEntries = array_merge(
4333 $this->trxPostCommitOrIdleCallbacks,
4334 $this->trxEndCallbacks,
4335 ( $trigger === self::TRIGGER_ROLLBACK )
4336 ? $this->trxSectionCancelCallbacks
4339 $this->trxPostCommitOrIdleCallbacks = [];
4340 $this->trxEndCallbacks = [];
4341 $this->trxSectionCancelCallbacks = [];
4343 $count += count( $callbackEntries );
4344 foreach ( $callbackEntries as $entry ) {
4345 $this->clearFlag( self::DBO_TRX );
4347 $entry[0]( $trigger, $this );
4349 call_user_func( $this->errorLogger, $ex );
4353 if ( $this->trxLevel() ) {
4354 $this->rollback( __METHOD__, self::FLUSHING_INTERNAL );
4358 $this->setFlag( self::DBO_TRX );
4360 $this->clearFlag( self::DBO_TRX );
4365 }
while ( count( $this->trxPostCommitOrIdleCallbacks ) );
4367 $this->completeCriticalSection( __METHOD__, $cs );
4386 $callbackEntries = $this->trxPreCommitOrIdleCallbacks;
4387 $this->trxPreCommitOrIdleCallbacks = [];
4388 $count += count( $callbackEntries );
4389 foreach ( $callbackEntries as $entry ) {
4393 }
catch ( Throwable $trxError ) {
4394 $this->setTransactionError( $trxError );
4399 }
while ( $this->trxPreCommitOrIdleCallbacks );
4413 $unrelatedCallbackEntries = [];
4415 $callbackEntries = $this->trxSectionCancelCallbacks;
4416 $this->trxSectionCancelCallbacks = [];
4417 foreach ( $callbackEntries as $entry ) {
4418 if ( in_array( $entry[2], $sectionIds,
true ) ) {
4421 $entry[0]( $trigger, $this );
4422 }
catch ( Throwable $trxError ) {
4423 $this->setTransactionError( $trxError );
4427 $unrelatedCallbackEntries[] = $entry;
4431 }
while ( $this->trxSectionCancelCallbacks );
4433 $this->trxSectionCancelCallbacks = $unrelatedCallbackEntries;
4447 if ( $this->trxEndCallbacksSuppressed ) {
4453 foreach ( $this->trxRecurringCallbacks as $callback ) {
4455 $callback( $trigger, $this );
4457 ( $this->errorLogger )( $ex );
4471 $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT, $dbErrors );
4472 $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT, $dbErrors );
4473 $this->affectedRowCount = 0;
4487 $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
4488 $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
4489 $this->affectedRowCount = 0;
4504 $sql =
'SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
4505 $this->query( $sql, $fname, self::QUERY_CHANGE_TRX );
4520 $sql =
'RELEASE SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
4521 $this->query( $sql, $fname, self::QUERY_CHANGE_TRX );
4536 $sql =
'ROLLBACK TO SAVEPOINT ' . $this->addIdentifierQuotes( $identifier );
4537 $this->query( $sql, $fname, self::QUERY_CHANGE_TRX );
4545 $savepointId = self::$SAVEPOINT_PREFIX . ++$this->trxAtomicCounter;
4546 if ( strlen( $savepointId ) > 30 ) {
4551 'There have been an excessively large number of atomic sections in a transaction'
4552 .
" started by $this->trxFname (at $fname)"
4556 return $savepointId;
4560 $fname = __METHOD__,
4561 $cancelable = self::ATOMIC_NOT_CANCELABLE
4563 $cs = $this->commenceCriticalSection( __METHOD__ );
4565 if ( $this->trxLevel() ) {
4567 $sectionOwnsTrx =
false;
4571 $this->begin( $fname, self::TRANSACTION_INTERNAL );
4573 $this->completeCriticalSection( __METHOD__, $cs );
4576 if ( $this->getFlag( self::DBO_TRX ) ) {
4581 $sectionOwnsTrx =
false;
4585 $sectionOwnsTrx =
true;
4587 $this->trxAutomaticAtomic = $sectionOwnsTrx;
4590 if ( $cancelable === self::ATOMIC_CANCELABLE ) {
4591 if ( $sectionOwnsTrx ) {
4594 $savepointId = self::$NOT_APPLICABLE;
4600 $savepointId = $this->nextSavepointId( $fname );
4601 $this->doSavepoint( $savepointId, $fname );
4603 $this->completeCriticalSection( __METHOD__, $cs, $e );
4608 $savepointId =
null;
4612 $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
4613 $this->queryLogger->debug(
'startAtomic: entering level ' .
4614 ( count( $this->trxAtomicLevels ) - 1 ) .
" ($fname)" );
4616 $this->completeCriticalSection( __METHOD__, $cs );
4622 if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
4627 $pos = count( $this->trxAtomicLevels ) - 1;
4628 list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
4629 $this->queryLogger->debug(
"endAtomic: leaving level $pos ($fname)" );
4631 if ( $savedFname !== $fname ) {
4634 "Invalid atomic section ended (got $fname but expected $savedFname)"
4638 $runPostCommitCallbacks =
false;
4640 $cs = $this->commenceCriticalSection( __METHOD__ );
4643 array_pop( $this->trxAtomicLevels );
4646 if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
4647 $this->commit( $fname, self::FLUSHING_INTERNAL );
4648 $runPostCommitCallbacks =
true;
4649 } elseif ( $savepointId !==
null && $savepointId !== self::$NOT_APPLICABLE ) {
4650 $this->doReleaseSavepoint( $savepointId, $fname );
4653 $this->completeCriticalSection( __METHOD__, $cs, $e );
4659 $currentSectionId = $this->currentAtomicSectionId();
4660 if ( $currentSectionId ) {
4661 $this->reassignCallbacksForSection( $sectionId, $currentSectionId );
4664 $this->completeCriticalSection( __METHOD__, $cs );
4666 if ( $runPostCommitCallbacks ) {
4667 $this->runTransactionPostCommitCallbacks();
4672 $fname = __METHOD__,
4675 if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
4679 if ( $sectionId !==
null ) {
4682 foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
4683 if ( $asId === $sectionId ) {
4694 $cs = $this->commenceCriticalSection( __METHOD__ );
4697 $excisedFnames = [];
4698 $newTopSection = $this->currentAtomicSectionId();
4699 if ( $pos !==
null ) {
4701 $len = count( $this->trxAtomicLevels );
4702 for ( $i = $pos + 1; $i < $len; ++$i ) {
4703 $excisedFnames[] = $this->trxAtomicLevels[$i][0];
4704 $excisedIds[] = $this->trxAtomicLevels[$i][1];
4706 $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
4707 $newTopSection = $this->currentAtomicSectionId();
4710 $runPostRollbackCallbacks =
false;
4713 $pos = count( $this->trxAtomicLevels ) - 1;
4714 list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
4716 if ( $excisedFnames ) {
4717 $this->queryLogger->debug(
"cancelAtomic: canceling level $pos ($savedFname) " .
4718 "and descendants " . implode(
', ', $excisedFnames ) );
4720 $this->queryLogger->debug(
"cancelAtomic: canceling level $pos ($savedFname)" );
4723 if ( $savedFname !== $fname ) {
4726 "Invalid atomic section ended (got $fname but expected $savedFname)"
4728 $this->completeCriticalSection( __METHOD__, $cs, $e );
4733 array_pop( $this->trxAtomicLevels );
4734 $excisedIds[] = $savedSectionId;
4735 $newTopSection = $this->currentAtomicSectionId();
4737 if ( $savepointId !==
null ) {
4739 if ( $savepointId === self::$NOT_APPLICABLE ) {
4742 $this->rollback( $fname, self::FLUSHING_INTERNAL );
4743 $runPostRollbackCallbacks =
true;
4747 $this->doRollbackToSavepoint( $savepointId, $fname );
4748 $this->trxStatus = self::STATUS_TRX_OK;
4749 $this->trxStatusIgnoredCause =
null;
4750 $this->runOnAtomicSectionCancelCallbacks( self::TRIGGER_CANCEL, $excisedIds );
4752 } elseif ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
4756 "Uncancelable atomic section canceled (got $fname)"
4758 $this->setTransactionError( $trxError );
4763 $this->modifyCallbacksForCancel( $excisedIds, $newTopSection );
4766 $this->affectedRowCount = 0;
4768 $this->completeCriticalSection( __METHOD__, $cs );
4770 if ( $runPostRollbackCallbacks ) {
4771 $this->runTransactionPostRollbackCallbacks();
4778 $cancelable = self::ATOMIC_NOT_CANCELABLE
4780 $sectionId = $this->startAtomic( $fname, $cancelable );
4782 $res = $callback( $this, $fname );
4783 }
catch ( Throwable $e ) {
4785 if ( !$this->csmError ) {
4786 $this->cancelAtomic( $fname, $sectionId );
4791 $this->endAtomic( $fname );
4796 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
4797 static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
4798 if ( !in_array( $mode, $modes,
true ) ) {
4803 if ( $this->trxLevel() ) {
4804 if ( $this->trxAtomicLevels ) {
4805 $levels = $this->flatAtomicSectionList();
4806 $msg =
"$fname: got explicit BEGIN while atomic section(s) $levels are open";
4808 } elseif ( !$this->trxAutomatic ) {
4809 $msg =
"$fname: explicit transaction already active (from {$this->trxFname})";
4812 $msg =
"$fname: implicit transaction already active (from {$this->trxFname})";
4815 } elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
4816 $msg =
"$fname: implicit transaction expected (DBO_TRX set)";
4820 $this->assertHasConnectionHandle();
4822 $cs = $this->commenceCriticalSection( __METHOD__ );
4824 $this->doBegin( $fname );
4826 $this->completeCriticalSection( __METHOD__, $cs );
4829 $this->trxShortId = sprintf(
'%06x', mt_rand( 0, 0xffffff ) );
4830 $this->trxStatus = self::STATUS_TRX_OK;
4831 $this->trxStatusIgnoredCause =
null;
4832 $this->trxAtomicCounter = 0;
4833 $this->trxTimestamp = microtime(
true );
4834 $this->trxFname = $fname;
4835 $this->trxDoneWrites =
false;
4836 $this->trxAutomaticAtomic =
false;
4837 $this->trxAtomicLevels = [];
4838 $this->trxWriteDuration = 0.0;
4839 $this->trxWriteQueryCount = 0;
4840 $this->trxWriteAffectedRows = 0;
4841 $this->trxWriteAdjDuration = 0.0;
4842 $this->trxWriteAdjQueryCount = 0;
4843 $this->trxWriteCallers = [];
4848 $this->trxReplicaLagStatus =
null;
4849 $this->trxReplicaLagStatus = $this->getApproximateLagStatus();
4854 $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
4855 $this->completeCriticalSection( __METHOD__, $cs );
4867 $this->query(
'BEGIN', $fname, self::QUERY_CHANGE_TRX );
4870 final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4871 static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
4872 if ( !in_array( $flush, $modes,
true ) ) {
4873 throw new DBUnexpectedError( $this,
"$fname: invalid flush parameter '$flush'" );
4876 if ( $this->trxLevel() && $this->trxAtomicLevels ) {
4878 $levels = $this->flatAtomicSectionList();
4881 "$fname: got COMMIT while atomic sections $levels are still open"
4885 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
4886 if ( !$this->trxLevel() ) {
4888 } elseif ( !$this->trxAutomatic ) {
4891 "$fname: flushing an explicit transaction, getting out of sync"
4894 } elseif ( !$this->trxLevel() ) {
4895 $this->queryLogger->error(
4896 "$fname: no transaction to commit, something got out of sync",
4897 [
'exception' =>
new RuntimeException() ]
4901 } elseif ( $this->trxAutomatic ) {
4904 "$fname: expected mass commit of all peer transactions (DBO_TRX set)"
4908 $this->assertHasConnectionHandle();
4910 $this->runOnTransactionPreCommitCallbacks();
4911 $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY );
4913 $cs = $this->commenceCriticalSection( __METHOD__ );
4915 $this->doCommit( $fname );
4917 $this->completeCriticalSection( __METHOD__, $cs );
4920 $oldTrxShortId = $this->consumeTrxShortId();
4921 $this->trxStatus = self::STATUS_TRX_NONE;
4922 if ( $this->trxDoneWrites ) {
4923 $this->lastWriteTime = microtime(
true );
4924 $this->trxProfiler->transactionWritingOut(
4925 $this->getServerName(),
4926 $this->getDomainID(),
4929 $this->trxWriteAffectedRows
4935 if ( $flush === self::FLUSHING_ONE ) {
4936 $this->runTransactionPostCommitCallbacks();
4938 $this->completeCriticalSection( __METHOD__, $cs );
4950 if ( $this->trxLevel() ) {
4951 $this->query(
'COMMIT', $fname, self::QUERY_CHANGE_TRX );
4955 final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4957 $flush !== self::FLUSHING_INTERNAL &&
4958 $flush !== self::FLUSHING_ALL_PEERS &&
4959 $this->getFlag( self::DBO_TRX )
4963 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
4967 if ( !$this->trxLevel() ) {
4969 $this->trxPostCommitOrIdleCallbacks = [];
4970 $this->trxPreCommitOrIdleCallbacks = [];
4975 $this->assertHasConnectionHandle();
4977 $cs = $this->commenceCriticalSection( __METHOD__ );
4978 $this->doRollback( $fname );
4979 $oldTrxShortId = $this->consumeTrxShortId();
4980 $this->trxStatus = self::STATUS_TRX_NONE;
4981 $this->trxAtomicLevels = [];
4983 $this->trxPostCommitOrIdleCallbacks = [];
4984 $this->trxPreCommitOrIdleCallbacks = [];
4986 $writeTime = $this->pingAndCalculateLastTrxApplyTime();
4987 if ( $this->trxDoneWrites ) {
4988 $this->trxProfiler->transactionWritingOut(
4989 $this->getServerName(),
4990 $this->getDomainID(),
4993 $this->trxWriteAffectedRows
4999 if ( $flush === self::FLUSHING_ONE ) {
5000 $this->runTransactionPostRollbackCallbacks();
5002 $this->completeCriticalSection( __METHOD__, $cs );
5014 if ( $this->trxLevel() ) {
5015 # Disconnects cause rollback anyway, so ignore those errors
5016 $this->query(
'ROLLBACK', $fname, self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_TRX );
5020 public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
5021 if ( $this->explicitTrxActive() ) {
5025 "$fname: Cannot flush snapshot; " .
5026 "explicit transaction '{$this->trxFname}' is still open"
5028 } elseif ( $this->writesOrCallbacksPending() ) {
5030 $fnames = implode(
', ', $this->pendingWriteAndCallbackCallers() );
5033 "$fname: Cannot flush snapshot; " .
5034 "writes from transaction {$this->trxFname} are still pending ($fnames)"
5037 $this->trxLevel() &&
5038 $this->getTransactionRoundId() &&
5039 $flush !== self::FLUSHING_INTERNAL &&
5040 $flush !== self::FLUSHING_ALL_PEERS
5042 $this->queryLogger->warning(
5043 "$fname: Expected mass snapshot flush of all peer transactions " .
5044 "in the explicit transactions round '{$this->getTransactionRoundId()}'",
5045 [
'exception' =>
new RuntimeException() ]
5049 $this->commit( $fname, self::FLUSHING_INTERNAL );
5053 return $this->trxLevel() && ( $this->trxAtomicLevels || !$this->trxAutomatic );
5066 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
5073 public function listTables( $prefix =
null, $fname = __METHOD__ ) {
5074 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
5081 public function listViews( $prefix =
null, $fname = __METHOD__ ) {
5082 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
5090 $t =
new ConvertibleTimestamp( $ts );
5092 return $t->getTimestamp( TS_MW );
5096 if ( $ts ===
null ) {
5099 return $this->timestamp( $ts );
5104 return $this->affectedRowCount ?? $this->fetchAffectedRowCount();
5112 public function ping( &$rtt =
null ) {
5114 if ( $this->isOpen() && ( microtime(
true ) - $this->lastPing ) < self::$PING_TTL ) {
5115 if ( !func_num_args() || $this->lastRoundTripEstimate > 0 ) {
5116 $rtt = $this->lastRoundTripEstimate;
5122 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_NONE;
5123 $ok = ( $this->query( self::$PING_QUERY, __METHOD__, $flags ) !== false );
5125 $rtt = $this->lastRoundTripEstimate;
5138 $this->closeConnection();
5141 $this->handleSessionLossPreconnect();
5145 $this->connectionParams[self::CONN_HOST],
5146 $this->connectionParams[self::CONN_USER],
5147 $this->connectionParams[self::CONN_PASSWORD],
5148 $this->currentDomain->getDatabase(),
5149 $this->currentDomain->getSchema(),
5150 $this->tablePrefix()
5152 $this->lastPing = microtime(
true );
5155 $this->connLogger->warning(
5156 $fname .
': lost connection to {db_server}; reconnected',
5157 $this->getLogContext( [
'exception' =>
new RuntimeException() ] )
5162 $this->connLogger->error(
5163 $fname .
': lost connection to {db_server} permanently',
5164 $this->getLogContext( [
'exception' =>
new RuntimeException() ] )
5168 $this->handleSessionLossPostconnect();
5174 return $this->getRecordedTransactionLagStatus() ?: $this->getApproximateLagStatus();
5191 return $this->trxLevel() ? $this->trxReplicaLagStatus :
null;
5204 if ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) {
5207 $lag = $this->getLag();
5215 return [
'lag' => $lag,
'since' => microtime(
true ) ];
5241 $res = [
'lag' => 0,
'since' => INF,
'pending' => false ];
5243 foreach ( func_get_args() as $db ) {
5245 $status = $db->getSessionLagStatus();
5247 if ( $status[
'lag'] ===
false ) {
5248 $res[
'lag'] =
false;
5249 } elseif (
$res[
'lag'] !==
false ) {
5250 $res[
'lag'] = max(
$res[
'lag'], $status[
'lag'] );
5252 $res[
'since'] = min(
$res[
'since'], $status[
'since'] );
5253 $res[
'pending'] =
$res[
'pending'] ?: $db->writesPending();
5261 if ( $this->topologyRole === self::ROLE_STREAMING_MASTER ) {
5263 } elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
5267 return $this->doGetLag();
5306 if ( $b instanceof
Blob ) {
5321 callable $lineCallback =
null,
5322 callable $resultCallback =
null,
5324 callable $inputCallback =
null
5326 AtEase::suppressWarnings();
5327 $fp = fopen( $filename,
'r' );
5328 AtEase::restoreWarnings();
5330 if ( $fp ===
false ) {
5331 throw new RuntimeException(
"Could not open \"{$filename}\"" );
5335 $fname = __METHOD__ .
"( $filename )";
5339 $error = $this->sourceStream(
5354 $this->schemaVars = is_array( $vars ) ? $vars :
null;
5359 callable $lineCallback =
null,
5360 callable $resultCallback =
null,
5361 $fname = __METHOD__,
5362 callable $inputCallback =
null
5364 $delimiterReset =
new ScopedCallback(
5365 function ( $delimiter ) {
5366 $this->delimiter = $delimiter;
5368 [ $this->delimiter ]
5372 while ( !feof( $fp ) ) {
5373 if ( $lineCallback ) {
5374 call_user_func( $lineCallback );
5377 $line = trim( fgets( $fp ) );
5379 if (
$line ==
'' ) {
5391 $done = $this->streamStatementEnd( $cmd,
$line );
5395 if ( $done || feof( $fp ) ) {
5396 $cmd = $this->replaceVars( $cmd );
5398 if ( $inputCallback ) {
5399 $callbackResult = $inputCallback( $cmd );
5401 if ( is_string( $callbackResult ) || !$callbackResult ) {
5402 $cmd = $callbackResult;
5407 $res = $this->query( $cmd, $fname );
5409 if ( $resultCallback ) {
5410 $resultCallback(
$res, $this );
5413 if (
$res ===
false ) {
5414 $err = $this->lastError();
5416 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
5423 ScopedCallback::consume( $delimiterReset );
5436 if ( $this->delimiter ) {
5438 $newLine = preg_replace(
5439 '/' . preg_quote( $this->delimiter,
'/' ) .
'$/',
5443 if ( $newLine != $prev ) {
5473 $vars = $this->getSchemaVars();
5474 return preg_replace_callback(
5476 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
5477 \'\{\$ (\w+) }\' | # 3. addQuotes
5478 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
5479 /\*\$ (\w+) \*/ # 5. leave unencoded
5481 function ( $m ) use ( $vars ) {
5484 if ( isset( $m[1] ) && $m[1] !==
'' ) {
5485 if ( $m[1] ===
'i' ) {
5486 return $this->indexName( $m[2] );
5488 return $this->tableName( $m[2] );
5490 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3], $vars ) ) {
5491 return $this->addQuotes( $vars[$m[3]] );
5492 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4], $vars ) ) {
5493 return $this->addIdentifierQuotes( $vars[$m[4]] );
5494 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5], $vars ) ) {
5495 return $vars[$m[5]];
5511 return $this->schemaVars ?? $this->getDefaultSchemaVars();
5534 if ( isset( $this->sessionNamedLocks[$lockName] ) ) {
5535 $lockIsFree =
false;
5537 $lockIsFree = $this->doLockIsFree( $lockName, $method );
5559 public function lock( $lockName, $method, $timeout = 5, $flags = 0 ) {
5560 $lockTsUnix = $this->doLock( $lockName, $method, $timeout );
5561 if ( $lockTsUnix !==
null ) {
5563 $this->sessionNamedLocks[$lockName] = $lockTsUnix;
5566 $this->queryLogger->info( __METHOD__ .
" failed to acquire lock '{lockname}'",
5567 [
'lockname' => $lockName ] );
5570 if ( $this->fieldHasBit( $flags, self::LOCK_TIMESTAMP ) ) {
5587 protected function doLock(
string $lockName,
string $method,
int $timeout ) {
5588 return microtime(
true );
5594 public function unlock( $lockName, $method ) {
5595 $released = $this->doUnlock( $lockName, $method );
5597 unset( $this->sessionNamedLocks[$lockName] );
5599 $this->queryLogger->warning( __METHOD__ .
" failed to release lock '$lockName'\n" );
5614 protected function doUnlock(
string $lockName,
string $method ) {
5619 if ( $this->writesOrCallbacksPending() ) {
5621 $fnames = implode(
', ', $this->pendingWriteAndCallbackCallers() );
5624 "$fname: Cannot flush pre-lock snapshot; " .
5625 "writes from transaction {$this->trxFname} are still pending ($fnames)"
5629 if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
5633 $unlocker =
new ScopedCallback(
function () use ( $lockKey, $fname ) {
5634 if ( $this->trxLevel() ) {
5638 $this->onTransactionResolution(
5639 function () use ( $lockKey, $fname ) {
5640 $this->unlock( $lockKey, $fname );
5645 $this->unlock( $lockKey, $fname );
5649 $this->commit( $fname, self::FLUSHING_INTERNAL );
5666 final public function lockTables( array $read, array $write, $method ) {
5667 if ( $this->writesOrCallbacksPending() ) {
5668 throw new DBUnexpectedError( $this,
"Transaction writes or callbacks still pending" );
5671 if ( $this->tableLocksHaveTransactionScope() ) {
5672 $this->startAtomic( $method );
5675 return $this->doLockTables( $read, $write, $method );
5692 if ( $this->tableLocksHaveTransactionScope() ) {
5693 $this->endAtomic( $method );
5698 return $this->doUnlockTables( $method );
5713 if ( !$this->tableExists( $table, $fname ) ) {
5717 $this->doDropTable( $table, $fname );
5732 $sql =
"DROP TABLE " . $this->tableName( $table ) .
" CASCADE";
5733 $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
5736 public function truncate( $tables, $fname = __METHOD__ ) {
5737 $tables = is_array( $tables ) ? $tables : [ $tables ];
5739 $tablesTruncate = [];
5740 foreach ( $tables as $table ) {
5743 if ( !$this->isPristineTemporaryTable( $table ) ) {
5744 $tablesTruncate[] = $table;
5748 if ( $tablesTruncate ) {
5749 $this->doTruncate( $tablesTruncate, $fname );
5760 foreach ( $tables as $table ) {
5761 $sql =
"TRUNCATE TABLE " . $this->tableName( $table );
5762 $this->query( $sql, $fname, self::QUERY_CHANGE_SCHEMA );
5775 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->getInfinity() )
5776 ? $this->getInfinity()
5777 : $this->timestamp( $expiry );
5781 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->getInfinity() ) {
5785 return ConvertibleTimestamp::convert( $format, $expiry );
5797 return ( $this->getReadOnlyReason() !==
false );
5804 if ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) {
5805 return [
'Server is configured as a read-only replica database.',
'role' ];
5806 } elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
5807 return [
'Server is configured as a read-only static clone database.',
'role' ];
5810 $reason = $this->getLBInfo( self::LB_READ_ONLY_REASON );
5811 if ( is_string( $reason ) ) {
5812 return [ $reason,
'lb' ];
5823 $this->tableAliases = $aliases;
5831 $this->indexAliases = $aliases;
5841 return ( ( $flags & $bit ) === $bit );
5857 if ( !$this->conn ) {
5860 'DB connection was already closed or the connection dropped'
5873 if ( $this->trxStatus > self::STATUS_TRX_ERROR ) {
5874 $this->trxStatus = self::STATUS_TRX_ERROR;
5875 $this->trxStatusCause = $trxError;
5920 if ( $this->csmError ) {
5923 "Cannot execute $fname critical section while session state is out of sync.\n\n" .
5924 $this->csmError->getMessage() .
"\n" .
5925 $this->csmError->getTraceAsString()
5929 if ( $this->csmId ) {
5931 } elseif ( $this->csProvider ) {
5932 $csm = $this->csProvider->scopedEnter(
5936 function () use ( $fname ) {
5938 $e =
new RuntimeException(
"A critical section from {$fname} has failed" );
5939 $this->csmError = $e;
5940 $this->csmId =
null;
5943 $this->csmId = $csm->getId();
5944 $this->csmFname = $fname;
5964 ?CriticalSectionScope $csm,
5965 Throwable $trxError =
null
5967 if ( $csm !==
null ) {
5968 if ( $this->csmId ===
null ) {
5969 throw new LogicException(
"$fname critical section is not active" );
5970 } elseif ( $csm->getId() !== $this->csmId ) {
5971 throw new LogicException(
5972 "$fname critical section is not the active ({$this->csmFname}) one"
5977 $this->csmId =
null;
5981 $this->setTransactionError( $trxError );
5987 $id = function_exists(
'spl_object_id' )
5988 ? spl_object_id( $this )
5989 : spl_object_hash( $this );
5991 $description = $this->getType() .
' object #' . $id;
5993 if ( is_resource( $this->conn ) ) {
5994 $description .=
' (' . (string)$this->conn .
')';
5995 } elseif ( is_object( $this->conn ) ) {
5997 $handleId = function_exists(
'spl_object_id' )
5998 ? spl_object_id( $this->conn )
5999 : spl_object_hash( $this->conn );
6000 $description .=
" (handle id #$handleId)";
6003 return $description;
6011 $this->connLogger->warning(
6012 "Cloning " . static::class .
" is not recommended; forking connection",
6013 [
'exception' =>
new RuntimeException() ]
6016 if ( $this->isOpen() ) {
6019 $this->trxEndCallbacks = [];
6020 $this->trxSectionCancelCallbacks = [];
6021 $this->handleSessionLossPreconnect();
6023 $this->connectionParams[self::CONN_HOST],
6024 $this->connectionParams[self::CONN_USER],
6025 $this->connectionParams[self::CONN_PASSWORD],
6026 $this->currentDomain->getDatabase(),
6027 $this->currentDomain->getSchema(),
6028 $this->tablePrefix()
6030 $this->lastPing = microtime(
true );
6041 throw new RuntimeException(
'Database serialization may cause problems, since ' .
6042 'the connection is not restored on wakeup' );
6049 if ( $this->trxLevel() && $this->trxDoneWrites ) {
6050 trigger_error(
"Uncommitted DB writes (transaction from {$this->trxFname})" );
6053 $danglingWriters = $this->pendingWriteAndCallbackCallers();
6054 if ( $danglingWriters ) {
6055 $fnames = implode(
', ', $danglingWriters );
6056 trigger_error(
"DB transaction writes or callbacks still pending ($fnames)" );
6059 if ( $this->conn ) {
6062 AtEase::suppressWarnings();
6063 $this->closeConnection();
6064 AtEase::restoreWarnings();
6073class_alias( Database::class,
'DatabaseBase' );
6078class_alias( Database::class,
'Database' );
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Class representing a cache/ephemeral data store.
Simple store for keeping values in an associative array for the current process.
Class to handle database/schema/prefix specifications for IDatabase.
static newFromId( $domain)
Advanced database interface for IDatabase handles that include maintenance methods.
foreach( $mmfl['setupFiles'] as $fileName) if($queue) if(empty( $mmfl['quiet'])) $s