31use InvalidArgumentException;
33use Psr\Log\LoggerAwareInterface;
34use Psr\Log\LoggerInterface;
35use Psr\Log\NullLogger;
38use UnexpectedValueException;
39use Wikimedia\AtEase\AtEase;
40use Wikimedia\ScopedCallback;
41use Wikimedia\Timestamp\ConvertibleTimestamp;
194 public const ATTR_DB_IS_FILE =
'db-is-file';
196 public const ATTR_DB_LEVEL_LOCKING =
'db-level-locking';
198 public const ATTR_SCHEMAS_AS_TABLE_GROUPS =
'supports-schemas';
201 public const NEW_UNCONNECTED = 0;
203 public const NEW_CONNECTED = 1;
206 public const STATUS_TRX_ERROR = 1;
208 public const STATUS_TRX_OK = 2;
210 public const STATUS_TRX_NONE = 3;
259 $this->connectionParams = [
260 'host' => ( isset( $params[
'host'] ) && $params[
'host'] !==
'' )
263 'user' => ( isset( $params[
'user'] ) && $params[
'user'] !==
'' )
266 'dbname' => ( isset( $params[
'dbname'] ) && $params[
'dbname'] !==
'' )
269 'schema' => ( isset( $params[
'schema'] ) && $params[
'schema'] !==
'' )
272 'password' => is_string( $params[
'password'] ) ? $params[
'password'] :
null,
273 'tablePrefix' => (string)$params[
'tablePrefix']
276 $this->lbInfo = $params[
'lbInfo'] ?? [];
277 $this->lazyMasterHandle = $params[
'lazyMasterHandle'] ??
null;
278 $this->connectionVariables = $params[
'variables'] ?? [];
280 $this->flags = (int)$params[
'flags'];
281 $this->cliMode = (bool)$params[
'cliMode'];
282 $this->agent = (string)$params[
'agent'];
283 $this->topologyRole = (string)$params[
'topologyRole'];
284 $this->topologyRootMaster = (string)$params[
'topologicalMaster'];
285 $this->nonNativeInsertSelectBatchSize = $params[
'nonNativeInsertSelectBatchSize'] ?? 10000;
287 $this->srvCache = $params[
'srvCache'];
288 $this->profiler = is_callable( $params[
'profiler'] ) ? $params[
'profiler'] :
null;
289 $this->trxProfiler = $params[
'trxProfiler'];
290 $this->connLogger = $params[
'connLogger'];
291 $this->queryLogger = $params[
'queryLogger'];
292 $this->replLogger = $params[
'replLogger'];
293 $this->errorLogger = $params[
'errorLogger'];
294 $this->deprecationLogger = $params[
'deprecationLogger'];
298 $params[
'dbname'] !=
'' ? $params[
'dbname'] :
null,
299 $params[
'schema'] !=
'' ? $params[
'schema'] :
null,
300 $params[
'tablePrefix']
303 $this->ownerId = $params[
'ownerId'] ??
null;
316 throw new LogicException( __METHOD__ .
': already connected' );
330 $this->connectionParams[
'host'],
331 $this->connectionParams[
'user'],
332 $this->connectionParams[
'password'],
333 $this->connectionParams[
'dbname'],
334 $this->connectionParams[
'schema'],
335 $this->connectionParams[
'tablePrefix']
405 final public static function factory(
$type, $params = [], $connect = self::NEW_CONNECTED ) {
408 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
420 'cliMode' => ( PHP_SAPI ===
'cli' || PHP_SAPI ===
'phpdbg' ),
423 'topologyRole' =>
null,
424 'topologicalMaster' =>
null,
426 'lazyMasterHandle' => $params[
'lazyMasterHandle'] ??
null,
428 'profiler' => $params[
'profiler'] ??
null,
430 'connLogger' => $params[
'connLogger'] ??
new NullLogger(),
431 'queryLogger' => $params[
'queryLogger'] ??
new NullLogger(),
432 'replLogger' => $params[
'replLogger'] ??
new NullLogger(),
433 'errorLogger' => $params[
'errorLogger'] ??
function ( Throwable $e ) {
434 trigger_error( get_class( $e ) .
': ' . $e->getMessage(), E_USER_WARNING );
436 'deprecationLogger' => $params[
'deprecationLogger'] ??
function ( $msg ) {
437 trigger_error( $msg, E_USER_DEPRECATED );
442 $conn =
new $class( $params );
443 if ( $connect === self::NEW_CONNECTED ) {
444 $conn->initConnection();
462 self::ATTR_DB_IS_FILE =>
false,
463 self::ATTR_DB_LEVEL_LOCKING =>
false,
464 self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
469 return call_user_func( [ $class,
'getAttributes' ] ) + $defaults;
478 private static function getClass( $dbType, $driver =
null ) {
485 static $builtinTypes = [
486 'mysql' => [
'mysqli' => DatabaseMysqli::class ],
487 'sqlite' => DatabaseSqlite::class,
488 'postgres' => DatabasePostgres::class,
491 $dbType = strtolower( $dbType );
494 if ( isset( $builtinTypes[$dbType] ) ) {
495 $possibleDrivers = $builtinTypes[$dbType];
496 if ( is_string( $possibleDrivers ) ) {
497 $class = $possibleDrivers;
498 } elseif ( (
string)$driver !==
'' ) {
499 if ( !isset( $possibleDrivers[$driver] ) ) {
500 throw new InvalidArgumentException( __METHOD__ .
501 " type '$dbType' does not support driver '{$driver}'" );
504 $class = $possibleDrivers[$driver];
506 foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
507 if ( extension_loaded( $posDriver ) ) {
508 $class = $possibleClass;
514 $class =
'Database' . ucfirst( $dbType );
517 if ( $class ===
false ) {
518 throw new InvalidArgumentException( __METHOD__ .
519 " no viable database extension found for type '$dbType'" );
542 $this->queryLogger = $logger;
558 return ( $this->trxShortId !=
'' ) ? 1 : 0;
574 $old = $this->currentDomain->getTablePrefix();
576 if ( $prefix !==
null ) {
578 $this->currentDomain->getDatabase(),
579 $this->currentDomain->getSchema(),
588 $old = $this->currentDomain->getSchema();
590 if ( $schema !==
null ) {
591 if ( $schema !==
'' && $this->
getDBname() ===
null ) {
594 "Cannot set schema to '$schema'; no database set"
599 $this->currentDomain->getDatabase(),
601 ( $schema !==
'' ) ? $schema :
null,
602 $this->currentDomain->getTablePrefix()
618 if ( $name ===
null ) {
622 if ( array_key_exists( $name, $this->lbInfo ) ) {
623 return $this->lbInfo[$name];
629 public function setLBInfo( $nameOrArray, $value =
null ) {
630 if ( is_array( $nameOrArray ) ) {
631 $this->lbInfo = $nameOrArray;
632 } elseif ( is_string( $nameOrArray ) ) {
633 if ( $value !==
null ) {
634 $this->lbInfo[$nameOrArray] = $value;
636 unset( $this->lbInfo[$nameOrArray] );
639 throw new InvalidArgumentException(
"Got non-string key" );
666 return $this->lastWriteTime ?:
false;
675 $this->trxDoneWrites ||
676 $this->trxIdleCallbacks ||
677 $this->trxPreCommitCallbacks ||
678 $this->trxEndCallbacks ||
692 if ( $this->
getFlag( self::DBO_TRX ) ) {
693 $id = $this->
getLBInfo( self::LB_TRX_ROUND_ID );
695 return is_string( $id ) ? $id :
null;
704 } elseif ( !$this->trxDoneWrites ) {
709 case self::ESTIMATE_DB_APPLY:
722 $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
723 $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
726 $applyTime += self::$TINY_WRITE_SEC * $omitted;
732 return $this->
trxLevel() ? $this->trxWriteCallers : [];
750 $this->trxIdleCallbacks,
751 $this->trxPreCommitCallbacks,
752 $this->trxEndCallbacks,
753 $this->trxSectionCancelCallbacks
755 foreach ( $callbacks as $callback ) {
756 $fnames[] = $callback[1];
767 return array_reduce( $this->trxAtomicLevels,
function ( $accum, $v ) {
768 return $accum ===
null ? $v[0] :
"$accum, " . $v[0];
776 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
777 if ( $flag & ~static::$DBO_MUTABLE ) {
780 "Got $flag (allowed: " . implode(
', ', static::$MUTABLE_FLAGS ) .
')'
784 if ( $remember === self::REMEMBER_PRIOR ) {
785 array_push( $this->priorFlags, $this->flags );
788 $this->flags |= $flag;
791 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
792 if ( $flag & ~static::$DBO_MUTABLE ) {
795 "Got $flag (allowed: " . implode(
', ', static::$MUTABLE_FLAGS ) .
')'
799 if ( $remember === self::REMEMBER_PRIOR ) {
800 array_push( $this->priorFlags, $this->flags );
803 $this->flags &= ~$flag;
807 if ( !$this->priorFlags ) {
811 if ( $state === self::RESTORE_INITIAL ) {
812 $this->flags = reset( $this->priorFlags );
813 $this->priorFlags = [];
815 $this->flags = array_pop( $this->priorFlags );
820 return ( ( $this->flags & $flag ) === $flag );
824 return $this->currentDomain->getId();
836 abstract public function indexInfo( $table, $index, $fname = __METHOD__ );
851 $this->lastPhpError =
false;
852 $this->htmlErrors = ini_set(
'html_errors',
'0' );
853 set_error_handler( [ $this,
'connectionErrorLogger' ] );
862 restore_error_handler();
863 if ( $this->htmlErrors !==
false ) {
864 ini_set(
'html_errors', $this->htmlErrors );
874 if ( $this->lastPhpError ) {
875 $error = preg_replace(
'!\[<a.*</a>\]!',
'', $this->lastPhpError );
876 $error = preg_replace(
'!^.*?:\s?(.*)$!',
'$1', $error );
892 $this->lastPhpError = $errstr;
904 'db_server' => $this->server,
906 'db_user' => $this->user,
912 final public function close( $fname = __METHOD__, $owner =
null ) {
915 $wasOpen = (bool)$this->conn;
920 if ( $this->trxAtomicLevels ) {
923 $error =
"$fname: atomic sections $levels are still open";
924 } elseif ( $this->trxAutomatic ) {
928 $error =
"$fname: " .
929 "expected mass rollback of all peer transactions (DBO_TRX set)";
934 $error =
"$fname: transaction is still open (from {$this->trxFname})";
937 if ( $this->trxEndCallbacksSuppressed && $error ===
null ) {
938 $error =
"$fname: callbacks are suppressed; cannot properly commit";
942 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
954 if ( $error !==
null ) {
958 if ( $this->ownerId !==
null && $owner === $this->ownerId ) {
959 $this->queryLogger->error( $error );
973 throw new RuntimeException(
974 "Transaction callbacks are still pending: " . implode(
', ', $fnames )
1004 list( $reason,
$source ) = $info;
1091 '/^\s*(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\(SELECT)\b/i',
1101 return preg_match(
'/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) :
null;
1121 [
'BEGIN',
'ROLLBACK',
'COMMIT',
'SET',
'SHOW',
'CREATE',
'ALTER',
'USE',
'SHOW' ],
1138 static $regexes =
null;
1139 if ( $regexes ===
null ) {
1141 $qts =
'((?:\w+|`\w+`|\'\w+\'|"\w+")(?:\s*,\s*(?:\w+|`\w+`|\'\w+\'|"\w+"))*)';
1145 "/^(INSERT|REPLACE)\s+(?:\w+\s+)*?INTO\s+$qts/i",
1146 "/^(UPDATE)(?:\s+OR\s+\w+|\s+IGNORE|\s+ONLY)?\s+$qts/i",
1147 "/^(DELETE)\s+(?:\w+\s+)*?FROM(?:\s+ONLY)?\s+$qts/i",
1149 "/^(CREATE)\s+TEMPORARY\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+$qts/i",
1150 "/^(DROP)\s+(?:TEMPORARY\s+)?TABLE(?:\s+IF\s+EXISTS)?\s+$qts/i",
1151 "/^(TRUNCATE)\s+(?:TEMPORARY\s+)?TABLE\s+$qts/i",
1152 "/^(ALTER)\s+TABLE\s+$qts/i"
1158 foreach ( $regexes as $regex ) {
1159 if ( preg_match( $regex, $sql, $m, PREG_UNMATCHED_AS_NULL ) ) {
1161 $allTables = preg_split(
'/\s*,\s*/', $m[2] );
1162 foreach ( $allTables as $quotedTable ) {
1163 $queryTables[] = trim( $quotedTable,
"\"'`" );
1169 $tempTableChanges = [];
1170 foreach ( $queryTables as $table ) {
1171 if ( $queryVerb ===
'CREATE' ) {
1175 $tableType = $this->sessionTempTables[$table] ??
null;
1178 if ( $tableType !==
null ) {
1179 $tempTableChanges[] = [ $tableType, $queryVerb, $table ];
1183 return $tempTableChanges;
1191 if ( $ret ===
false ) {
1195 foreach ( $changes as list( $tmpTableType, $verb, $table ) ) {
1198 $this->sessionTempTables[$table] = $tmpTableType;
1201 unset( $this->sessionTempTables[$table] );
1202 unset( $this->sessionDirtyTempTables[$table] );
1205 unset( $this->sessionDirtyTempTables[$table] );
1208 $this->sessionDirtyTempTables[$table] = 1;
1222 $rawTable = $this->
tableName( $table,
'raw' );
1225 isset( $this->sessionTempTables[$rawTable] ) &&
1226 !isset( $this->sessionDirtyTempTables[$rawTable] )
1230 public function query( $sql, $fname = __METHOD__,
$flags = self::QUERY_NORMAL ) {
1237 list( $ret, $err, $errno, $unignorable ) = $this->
executeQuery( $sql, $fname,
$flags );
1238 if ( $ret ===
false ) {
1241 $this->
reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
1270 $priorTransaction = $this->
trxLevel();
1278 $isPermWrite = !$tempTableChanges;
1279 foreach ( $tempTableChanges as list( $tmpType ) ) {
1285 if ( $isPermWrite ) {
1295 $isPermWrite =
false;
1297 $tempTableChanges = [];
1302 $encAgent = str_replace(
'/',
'-', $this->agent );
1303 $commentedSql = preg_replace(
'/\s|$/',
" /* $fname $encAgent */ ", $sql, 1 );
1307 list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
1312 if ( $ret ===
false && $recoverableCL && $reconnected && $allowRetry ) {
1314 list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
1321 $corruptedTrx =
false;
1323 if ( $ret ===
false ) {
1324 if ( $priorTransaction ) {
1325 if ( $recoverableSR ) {
1326 # We're ignoring an error that caused just the current query to be aborted.
1327 # But log the cause so we can log a deprecation notice if a caller actually
1329 $this->trxStatusIgnoredCause = [ $err, $errno, $fname ];
1330 } elseif ( !$recoverableCL ) {
1331 # Either the query was aborted or all queries after BEGIN where aborted.
1332 # In the first case, the only options going forward are (a) ROLLBACK, or
1333 # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1334 # option is ROLLBACK, since the snapshots would have been released.
1335 $corruptedTrx =
true;
1336 $this->
trxStatus = self::STATUS_TRX_ERROR;
1338 $this->trxStatusIgnoredCause =
null;
1343 return [ $ret, $err, $errno, $corruptedTrx ];
1367 if ( (
$flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
1372 if ( $isPermWrite ) {
1373 $this->lastWriteTime = microtime(
true );
1374 if ( $this->
trxLevel() && !$this->trxDoneWrites ) {
1375 $this->trxDoneWrites =
true;
1376 $this->trxProfiler->transactionWritingIn(
1377 $this->server, $this->
getDomainID(), $this->trxShortId );
1381 $prefix = $this->topologyRole ?
'query-m: ' :
'query: ';
1382 $generalizedSql =
new GeneralizedSql( $sql, $this->trxShortId, $prefix );
1384 $startTime = microtime(
true );
1385 $ps = $this->profiler
1388 $this->affectedRowCount =
null;
1390 $ret = $this->
doQuery( $commentedSql );
1396 $queryRuntime = max( microtime(
true ) - $startTime, 0.0 );
1398 $recoverableSR =
false;
1399 $recoverableCL =
false;
1400 $reconnected =
false;
1402 if ( $ret !==
false ) {
1403 $this->lastPing = $startTime;
1404 if ( $isPermWrite && $this->
trxLevel() ) {
1406 $this->trxWriteCallers[] = $fname;
1409 # Check if no meaningful session state was lost
1411 # Update session state tracking and try to restore the connection
1414 # Check if only the last query was rolled back
1418 if ( $sql === self::$PING_QUERY ) {
1419 $this->lastRoundTripEstimate = $queryRuntime;
1422 $this->trxProfiler->recordQueryCompletion(
1430 if ( $this->
getFlag( self::DBO_DEBUG ) ) {
1431 $this->queryLogger->debug(
1432 "{method} [{runtime}s] {db_host}: {sql}",
1438 'runtime' => round( $queryRuntime, 3 )
1443 return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
1455 $this->
getFlag( self::DBO_TRX ) &&
1458 $this->
begin( __METHOD__ .
" ($fname)", self::TRANSACTION_INTERNAL );
1459 $this->trxAutomatic =
true;
1477 $indicativeOfReplicaRuntime =
true;
1478 if ( $runtime > self::$SLOW_WRITE_SEC ) {
1481 if ( $verb ===
'INSERT' ) {
1483 } elseif ( $verb ===
'REPLACE' ) {
1484 $indicativeOfReplicaRuntime = $this->
affectedRows() > self::$SMALL_WRITE_ROWS / 2;
1488 $this->trxWriteDuration += $runtime;
1489 $this->trxWriteQueryCount += 1;
1490 $this->trxWriteAffectedRows += $affected;
1491 if ( $indicativeOfReplicaRuntime ) {
1492 $this->trxWriteAdjDuration += $runtime;
1493 $this->trxWriteAdjQueryCount += 1;
1506 if ( $verb ===
'USE' ) {
1507 throw new DBUnexpectedError( $this,
"Got USE query; use selectDomain() instead" );
1510 if ( $verb ===
'ROLLBACK' ) {
1514 if ( $this->
trxStatus < self::STATUS_TRX_OK ) {
1517 "Cannot execute query from $fname while transaction status is ERROR",
1519 $this->trxStatusCause
1521 } elseif ( $this->
trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1523 call_user_func( $this->deprecationLogger,
1524 "Caller from $fname ignored an error originally raised from $iFname: " .
1525 "[$iLastErrno] $iLastError"
1527 $this->trxStatusIgnoredCause =
null;
1535 "Explicit transaction still active. A caller may have caught an error. "
1551 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1552 # Dropped connections also mean that named locks are automatically released.
1553 # Only allow error suppression in autocommit mode or when the lost transaction
1554 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1555 if ( $this->sessionNamedLocks ) {
1557 } elseif ( $this->sessionTempTables ) {
1559 } elseif ( $sql ===
'COMMIT' ) {
1560 return !$priorWritesPending;
1561 } elseif ( $sql ===
'ROLLBACK' ) {
1565 } elseif ( $priorWritesPending ) {
1579 $this->sessionTempTables = [];
1580 $this->sessionDirtyTempTables = [];
1583 $this->sessionNamedLocks = [];
1586 $this->trxAtomicCounter = 0;
1587 $this->trxIdleCallbacks = [];
1588 $this->trxPreCommitCallbacks = [];
1592 if ( $this->trxDoneWrites ) {
1593 $this->trxProfiler->transactionWritingOut(
1598 $this->trxWriteAffectedRows
1619 }
catch ( Throwable $ex ) {
1625 }
catch ( Throwable $ex ) {
1637 $this->trxShortId =
'';
1670 $this->queryLogger->debug(
"SQL ERROR (ignored): $error" );
1686 $this->queryLogger->error(
1687 "Error $errno from $fname, {error} {sql1line} {db_server}",
1689 'method' => __METHOD__,
1692 'sql1line' => mb_substr( str_replace(
"\n",
"\\n", $sql ), 0, 5 * 1024 ),
1694 'exception' =>
new RuntimeException()
1713 return new DBQueryError( $this, $error, $errno, $sql, $fname );
1725 $this->connLogger->error(
1726 "Error connecting to {db_server} as user {db_user}: {error}",
1729 'exception' =>
new RuntimeException()
1751 $table, $var, $cond =
'', $fname = __METHOD__, $options = [], $join_conds = []
1753 if ( $var ===
'*' ) {
1758 $options[
'LIMIT'] = 1;
1760 $res = $this->
select( $table, $var, $cond, $fname, $options, $join_conds );
1761 if (
$res ===
false ) {
1766 if ( $row ===
false ) {
1770 return reset( $row );
1774 $table, $var, $cond =
'', $fname = __METHOD__, $options = [], $join_conds = []
1776 if ( $var ===
'*' ) {
1778 } elseif ( !is_string( $var ) ) {
1783 $res = $this->
select( $table, [
'value' => $var ], $cond, $fname, $options, $join_conds );
1784 if (
$res ===
false ) {
1789 foreach (
$res as $row ) {
1790 $values[] = $row->value;
1808 $preLimitTail = $postLimitTail =
'';
1813 foreach ( $options as $key => $option ) {
1814 if ( is_numeric( $key ) ) {
1815 $noKeyOptions[$option] =
true;
1823 if ( isset( $noKeyOptions[
'FOR UPDATE'] ) ) {
1824 $postLimitTail .=
' FOR UPDATE';
1827 if ( isset( $noKeyOptions[
'LOCK IN SHARE MODE'] ) ) {
1828 $postLimitTail .=
' LOCK IN SHARE MODE';
1831 if ( isset( $noKeyOptions[
'DISTINCT'] ) || isset( $noKeyOptions[
'DISTINCTROW'] ) ) {
1832 $startOpts .=
'DISTINCT';
1835 # Various MySQL extensions
1836 if ( isset( $noKeyOptions[
'STRAIGHT_JOIN'] ) ) {
1837 $startOpts .=
' /*! STRAIGHT_JOIN */';
1840 if ( isset( $noKeyOptions[
'SQL_BIG_RESULT'] ) ) {
1841 $startOpts .=
' SQL_BIG_RESULT';
1844 if ( isset( $noKeyOptions[
'SQL_BUFFER_RESULT'] ) ) {
1845 $startOpts .=
' SQL_BUFFER_RESULT';
1848 if ( isset( $noKeyOptions[
'SQL_SMALL_RESULT'] ) ) {
1849 $startOpts .=
' SQL_SMALL_RESULT';
1852 if ( isset( $noKeyOptions[
'SQL_CALC_FOUND_ROWS'] ) ) {
1853 $startOpts .=
' SQL_CALC_FOUND_ROWS';
1856 if ( isset( $options[
'USE INDEX'] ) && is_string( $options[
'USE INDEX'] ) ) {
1861 if ( isset( $options[
'IGNORE INDEX'] ) && is_string( $options[
'IGNORE INDEX'] ) ) {
1867 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1880 if ( isset( $options[
'GROUP BY'] ) ) {
1881 $gb = is_array( $options[
'GROUP BY'] )
1882 ? implode(
',', $options[
'GROUP BY'] )
1883 : $options[
'GROUP BY'];
1884 $sql .=
' GROUP BY ' . $gb;
1886 if ( isset( $options[
'HAVING'] ) ) {
1887 $having = is_array( $options[
'HAVING'] )
1888 ? $this->
makeList( $options[
'HAVING'], self::LIST_AND )
1889 : $options[
'HAVING'];
1890 $sql .=
' HAVING ' . $having;
1905 if ( isset( $options[
'ORDER BY'] ) ) {
1906 $ob = is_array( $options[
'ORDER BY'] )
1907 ? implode(
',', $options[
'ORDER BY'] )
1908 : $options[
'ORDER BY'];
1910 return ' ORDER BY ' . $ob;
1917 $table, $vars, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
1919 $sql = $this->
selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1921 return $this->
query( $sql, $fname, self::QUERY_CHANGE_NONE );
1929 $options = [], $join_conds = []
1931 if ( is_array( $vars ) ) {
1937 $options = (array)$options;
1938 $useIndexes = ( isset( $options[
'USE INDEX'] ) && is_array( $options[
'USE INDEX'] ) )
1939 ? $options[
'USE INDEX']
1942 isset( $options[
'IGNORE INDEX'] ) &&
1943 is_array( $options[
'IGNORE INDEX'] )
1945 ? $options[
'IGNORE INDEX']
1955 $this->deprecationLogger,
1956 __METHOD__ .
": aggregation used with a locking SELECT ($fname)"
1960 if ( is_array( $table ) ) {
1961 if ( count( $table ) === 0 ) {
1966 $table, $useIndexes, $ignoreIndexes, $join_conds );
1968 } elseif ( $table !=
'' ) {
1971 [ $table ], $useIndexes, $ignoreIndexes, [] );
1976 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1979 if ( is_array( $conds ) ) {
1980 $conds = $this->
makeList( $conds, self::LIST_AND );
1983 if ( $conds ===
null || $conds ===
false ) {
1984 $this->queryLogger->warning(
1988 .
' with incorrect parameters: $conds must be a string or an array'
1993 if ( $conds ===
'' || $conds ===
'*' ) {
1994 $sql =
"SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
1995 } elseif ( is_string( $conds ) ) {
1996 $sql =
"SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
1997 "WHERE $conds $preLimitTail";
1999 throw new DBUnexpectedError( $this, __METHOD__ .
' called with incorrect parameters' );
2002 if ( isset( $options[
'LIMIT'] ) ) {
2003 $sql = $this->
limitResult( $sql, $options[
'LIMIT'],
2004 $options[
'OFFSET'] ??
false );
2006 $sql =
"$sql $postLimitTail";
2008 if ( isset( $options[
'EXPLAIN'] ) ) {
2009 $sql =
'EXPLAIN ' . $sql;
2015 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
2016 $options = [], $join_conds = []
2018 $options = (array)$options;
2019 $options[
'LIMIT'] = 1;
2021 $res = $this->
select( $table, $vars, $conds, $fname, $options, $join_conds );
2022 if (
$res ===
false ) {
2038 $tables, $var =
'*', $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2042 if ( is_string( $column ) && !in_array( $column, [
'*',
'1' ] ) ) {
2043 $conds[] =
"$column IS NOT NULL";
2047 $tables, [
'rowcount' =>
'COUNT(*)' ], $conds, $fname, $options, $join_conds
2051 return isset( $row[
'rowcount'] ) ? (int)$row[
'rowcount'] : 0;
2055 $tables, $var =
'*', $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2059 if ( is_string( $column ) && !in_array( $column, [
'*',
'1' ] ) ) {
2060 $conds[] =
"$column IS NOT NULL";
2074 [
'rowcount' =>
'COUNT(*)' ],
2080 return isset( $row[
'rowcount'] ) ? (int)$row[
'rowcount'] : 0;
2088 $options = (array)$options;
2089 foreach ( [
'FOR UPDATE',
'LOCK IN SHARE MODE' ] as $lock ) {
2090 if ( in_array( $lock, $options,
true ) ) {
2104 foreach ( (array)$options as $key => $value ) {
2105 if ( is_string( $key ) ) {
2106 if ( preg_match(
'/^(?:GROUP BY|HAVING)$/i', $key ) ) {
2109 } elseif ( is_string( $value ) ) {
2110 if ( preg_match(
'/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
2116 $regex =
'/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
2117 foreach ( (array)$fields as $field ) {
2118 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
2132 if ( !$rowOrRows ) {
2134 } elseif ( isset( $rowOrRows[0] ) ) {
2137 $rows = [ $rowOrRows ];
2140 foreach ( $rows as $row ) {
2141 if ( !is_array( $row ) ) {
2143 } elseif ( !$row ) {
2158 if ( $conds ===
null || $conds ===
false ) {
2159 $this->queryLogger->warning(
2163 .
' with incorrect parameters: $conds must be a string or an array'
2166 } elseif ( $conds ===
'' ) {
2170 return is_array( $conds ) ? $conds : [ $conds ];
2179 if ( is_string( $uniqueKeys ) ) {
2180 return [ [ $uniqueKeys ] ];
2183 if ( !is_array( $uniqueKeys ) || !$uniqueKeys ) {
2188 $uniqueColumnSets = [];
2189 foreach ( $uniqueKeys as $i => $uniqueKey ) {
2190 if ( !is_int( $i ) ) {
2192 } elseif ( is_string( $uniqueKey ) ) {
2194 $uniqueColumnSets[] = [ $uniqueKey ];
2195 } elseif ( is_array( $uniqueKey ) && $uniqueKey ) {
2196 $uniqueColumnSets[] = $uniqueKey;
2202 if ( count( $uniqueColumnSets ) > 1 ) {
2206 $this->queryLogger->warning(
2207 __METHOD__ .
" called with multiple unique keys",
2208 [
'exception' =>
new RuntimeException() ]
2215 $this->queryLogger->warning(
2216 __METHOD__ .
" called with deprecated parameter style: " .
2217 "the unique key array should be a string or array of string arrays",
2218 [
'exception' =>
new RuntimeException() ]
2222 return $uniqueColumnSets;
2231 if ( is_array( $options ) ) {
2233 } elseif ( is_string( $options ) ) {
2234 return ( $options ===
'' ) ? [] : [ $options ];
2236 throw new DBUnexpectedError( $this, __METHOD__ .
': expected string or array' );
2247 foreach ( array_keys( $options, $option,
true ) as $k ) {
2248 if ( is_int( $k ) ) {
2261 if ( is_array( $var ) ) {
2264 } elseif ( count( $var ) == 1 ) {
2265 $column = $var[0] ?? reset( $var );
2277 $table, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2282 __METHOD__ .
': no transaction is active nor is DBO_TRX set'
2286 $options = (array)$options;
2287 $options[] =
'FOR UPDATE';
2289 return $this->
selectRowCount( $table,
'*', $conds, $fname, $options, $join_conds );
2293 $info = $this->
fieldInfo( $table, $field );
2303 $info = $this->
indexInfo( $table, $index, $fname );
2304 if ( $info ===
null ) {
2307 return $info !==
false;
2318 $indexInfo = $this->
indexInfo( $table, $index, $fname );
2320 if ( !$indexInfo ) {
2324 return !$indexInfo[0]->Non_unique;
2327 public function insert( $table, $rows, $fname = __METHOD__, $options = [] ) {
2337 $this->
doInsert( $table, $rows, $fname );
2351 protected function doInsert( $table, array $rows, $fname ) {
2355 $sql =
"INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples";
2357 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2373 $sql = rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" );
2375 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2384 return [
'INSERT IGNORE INTO',
'' ];
2398 $firstRow = $rows[0];
2399 if ( !is_array( $firstRow ) || !$firstRow ) {
2403 $tupleColumns = array_keys( $firstRow );
2406 foreach ( $rows as $row ) {
2407 $rowColumns = array_keys( $row );
2409 if ( $rowColumns !== $tupleColumns ) {
2412 'Got row columns (' . implode(
', ', $rowColumns ) .
') ' .
2413 'instead of expected (' . implode(
', ', $tupleColumns ) .
')'
2417 $valueTuples[] =
'(' . $this->
makeList( $row, self::LIST_COMMA ) .
')';
2421 $this->
makeList( $tupleColumns, self::LIST_NAMES ),
2422 implode(
',', $valueTuples )
2438 if ( in_array(
'IGNORE', $options ) ) {
2455 return implode(
' ', $opts );
2458 public function update( $table, $set, $conds, $fname = __METHOD__, $options = [] ) {
2462 $sql =
"UPDATE $opts $table SET " . $this->
makeList( $set, self::LIST_SET );
2464 if ( $conds && $conds !== IDatabase::ALL_ROWS ) {
2465 if ( is_array( $conds ) ) {
2466 $conds = $this->
makeList( $conds, self::LIST_AND );
2468 $sql .=
' WHERE ' . $conds;
2471 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2476 public function makeList( array $a, $mode = self::LIST_COMMA ) {
2480 foreach ( $a as $field => $value ) {
2482 if ( $mode == self::LIST_AND ) {
2484 } elseif ( $mode == self::LIST_OR ) {
2493 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2494 $list .=
"($value)";
2495 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2498 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2501 $includeNull =
false;
2502 foreach ( array_keys( $value,
null,
true ) as $nullKey ) {
2503 $includeNull =
true;
2504 unset( $value[$nullKey] );
2506 if ( count( $value ) == 0 && !$includeNull ) {
2507 throw new InvalidArgumentException(
2508 __METHOD__ .
": empty input for field $field" );
2509 } elseif ( count( $value ) == 0 ) {
2511 $list .=
"$field IS NULL";
2514 if ( $includeNull ) {
2518 if ( count( $value ) == 1 ) {
2522 $value = array_values( $value )[0];
2523 $list .= $field .
" = " . $this->
addQuotes( $value );
2525 $list .= $field .
" IN (" . $this->
makeList( $value ) .
") ";
2528 if ( $includeNull ) {
2529 $list .=
" OR $field IS NULL)";
2532 } elseif ( $value ===
null ) {
2533 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2534 $list .=
"$field IS ";
2535 } elseif ( $mode == self::LIST_SET ) {
2536 $list .=
"$field = ";
2541 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2543 $list .=
"$field = ";
2545 $list .= $mode == self::LIST_NAMES ? $value : $this->
addQuotes( $value );
2555 foreach ( $data as
$base => $sub ) {
2556 if ( count( $sub ) ) {
2558 [ $baseKey =>
$base, $subKey => array_map(
'strval', array_keys( $sub ) ) ],
2564 return $this->
makeList( $conds, self::LIST_OR );
2591 public function bitAnd( $fieldLeft, $fieldRight ) {
2592 return "($fieldLeft & $fieldRight)";
2599 public function bitOr( $fieldLeft, $fieldRight ) {
2600 return "($fieldLeft | $fieldRight)";
2608 return 'CONCAT(' . implode(
',', $stringList ) .
')';
2616 $delim, $table, $field, $conds =
'', $join_conds = []
2618 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->
addQuotes( $delim ) .
')';
2620 return '(' . $this->
selectSQLText( $table, $fld, $conds,
null, [], $join_conds ) .
')';
2656 $fields = is_array( $fields ) ? $fields : [ $fields ];
2657 $values = is_array( $values ) ? $values : [ $values ];
2660 foreach ( $fields as $alias => $field ) {
2661 if ( is_int( $alias ) ) {
2664 $encValues[] = $field;
2667 foreach ( $values as $value ) {
2668 if ( is_int( $value ) || is_float( $value ) ) {
2669 $encValues[] = $value;
2670 } elseif ( is_string( $value ) ) {
2671 $encValues[] = $this->
addQuotes( $value );
2672 } elseif ( $value ===
null ) {
2679 return $sqlfunc .
'(' . implode(
',', $encValues ) .
')';
2688 $functionBody =
"$input FROM $startPosition";
2689 if ( $length !==
null ) {
2690 $functionBody .=
" FOR $length";
2692 return 'SUBSTRING(' . $functionBody .
')';
2708 if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2709 throw new InvalidArgumentException(
2710 '$startPosition must be a positive integer'
2713 if ( !( is_int( $length ) && $length >= 0 || $length ===
null ) ) {
2714 throw new InvalidArgumentException(
2715 '$length must be null or an integer greater than or equal to 0'
2734 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
2735 if ( !$isCondValid ) {
2737 wfDeprecated( $fname .
' called with empty $conds',
'1.35',
false, 3 );
2751 return "CAST( $field AS CHARACTER )";
2759 return 'CAST( ' . $field .
' AS INTEGER )';
2763 $table, $vars, $conds =
'', $fname = __METHOD__,
2764 $options = [], $join_conds = []
2767 $this->
selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2782 $this->currentDomain->getSchema(),
2783 $this->currentDomain->getTablePrefix()
2801 $this->currentDomain = $domain;
2805 return $this->currentDomain->getDatabase();
2820 __METHOD__ .
': got Subquery instance when expecting a string'
2824 # Skip the entire process when we have a string quoted on both ends.
2825 # Note that we check the end so that we will still quote any use of
2826 # use of `database`.table. But won't break things if someone wants
2827 # to query a database table with a dot in the name.
2832 # Lets test for any bits of text that should never show up in a table
2833 # name. Basically anything like JOIN or ON which are actually part of
2834 # SQL queries, but may end up inside of the table value to combine
2835 # sql. Such as how the API is doing.
2836 # Note that we use a whitespace test rather than a \b test to avoid
2837 # any remote case where a word like on may be inside of a table name
2838 # surrounded by symbols which may be considered word breaks.
2839 if ( preg_match(
'/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2840 $this->queryLogger->warning(
2841 __METHOD__ .
": use of subqueries is not supported this way",
2842 [
'exception' =>
new RuntimeException() ]
2848 # Split database and table into proper variables.
2851 # Quote $table and apply the prefix if not quoted.
2852 # $tableName might be empty if this is called from Database::replaceVars()
2853 $tableName =
"{$prefix}{$table}";
2854 if ( $format ===
'quoted'
2856 && $tableName !==
''
2861 # Quote $schema and $database and merge them with the table name if needed
2875 # We reverse the explode so that database.table and table both output the correct table.
2876 $dbDetails = explode(
'.', $name, 3 );
2877 if ( count( $dbDetails ) == 3 ) {
2878 list( $database, $schema, $table ) = $dbDetails;
2879 # We don't want any prefix added in this case
2881 } elseif ( count( $dbDetails ) == 2 ) {
2882 list( $database, $table ) = $dbDetails;
2883 # We don't want any prefix added in this case
2885 # In dbs that support it, $database may actually be the schema
2886 # but that doesn't affect any of the functionality here
2889 list( $table ) = $dbDetails;
2890 if ( isset( $this->tableAliases[$table] ) ) {
2891 $database = $this->tableAliases[$table][
'dbname'];
2892 $schema = is_string( $this->tableAliases[$table][
'schema'] )
2893 ? $this->tableAliases[$table][
'schema']
2895 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
2896 ? $this->tableAliases[$table][
'prefix']
2905 return [ $database, $schema, $prefix, $table ];
2915 if ( $namespace !==
null && $namespace !==
'' ) {
2919 $relation = $namespace .
'.' . $relation;
2928 foreach ( $tables as $name ) {
2929 $retVal[$name] = $this->
tableName( $name );
2938 foreach ( $tables as $name ) {
2957 if ( is_string( $table ) ) {
2958 $quotedTable = $this->
tableName( $table );
2959 } elseif ( $table instanceof
Subquery ) {
2960 $quotedTable = (string)$table;
2962 throw new InvalidArgumentException(
"Table must be a string or Subquery" );
2965 if ( $alias ===
false || $alias === $table ) {
2966 if ( $table instanceof
Subquery ) {
2967 throw new InvalidArgumentException(
"Subquery table missing alias" );
2970 return $quotedTable;
2986 if ( !$alias || (
string)$alias === (
string)$name ) {
3001 foreach ( $fields as $alias => $field ) {
3002 if ( is_numeric( $alias ) ) {
3022 $tables, $use_index = [], $ignore_index = [], $join_conds = []
3026 $use_index = (array)$use_index;
3027 $ignore_index = (array)$ignore_index;
3028 $join_conds = (array)$join_conds;
3030 foreach ( $tables as $alias => $table ) {
3031 if ( !is_string( $alias ) ) {
3036 if ( is_array( $table ) ) {
3038 if ( count( $table ) > 1 ) {
3039 $joinedTable =
'(' .
3041 $table, $use_index, $ignore_index, $join_conds ) .
')';
3044 $innerTable = reset( $table );
3045 $innerAlias = key( $table );
3048 is_string( $innerAlias ) ? $innerAlias : $innerTable
3056 if ( isset( $join_conds[$alias] ) ) {
3057 list( $joinType, $conds ) = $join_conds[$alias];
3058 $tableClause = $joinType;
3059 $tableClause .=
' ' . $joinedTable;
3060 if ( isset( $use_index[$alias] ) ) {
3061 $use = $this->
useIndexClause( implode(
',', (array)$use_index[$alias] ) );
3063 $tableClause .=
' ' . $use;
3066 if ( isset( $ignore_index[$alias] ) ) {
3068 implode(
',', (array)$ignore_index[$alias] ) );
3069 if ( $ignore !=
'' ) {
3070 $tableClause .=
' ' . $ignore;
3073 $on = $this->
makeList( (array)$conds, self::LIST_AND );
3075 $tableClause .=
' ON (' . $on .
')';
3078 $retJOIN[] = $tableClause;
3079 } elseif ( isset( $use_index[$alias] ) ) {
3081 $tableClause = $joinedTable;
3083 implode(
',', (array)$use_index[$alias] )
3086 $ret[] = $tableClause;
3087 } elseif ( isset( $ignore_index[$alias] ) ) {
3089 $tableClause = $joinedTable;
3091 implode(
',', (array)$ignore_index[$alias] )
3094 $ret[] = $tableClause;
3096 $tableClause = $joinedTable;
3098 $ret[] = $tableClause;
3103 $implicitJoins = implode(
',', $ret );
3104 $explicitJoins = implode(
' ', $retJOIN );
3107 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
3117 return $this->indexAliases[$index] ?? $index;
3125 if (
$s instanceof
Blob ) {
3128 if (
$s ===
null ) {
3130 } elseif ( is_bool(
$s ) ) {
3131 return (
string)(int)
$s;
3132 } elseif ( is_int(
$s ) ) {
3144 return '"' . str_replace(
'"',
'""',
$s ) .
'"';
3158 return $name[0] ==
'"' && substr( $name, -1, 1 ) ==
'"';
3168 return str_replace( [ $escapeChar,
'%',
'_' ],
3169 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
3178 if ( is_array( $param ) ) {
3181 $params = func_get_args();
3192 foreach ( $params as $value ) {
3194 $s .= $value->toString();
3246 public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
3252 if ( $uniqueKeys ) {
3254 $this->
doReplace( $table, $uniqueKeys, $rows, $fname );
3256 $this->queryLogger->warning(
3257 __METHOD__ .
" called with no unique keys",
3258 [
'exception' =>
new RuntimeException() ]
3260 $this->
doInsert( $table, $rows, $fname );
3273 protected function doReplace( $table, array $uniqueKeys, array $rows, $fname ) {
3275 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
3277 foreach ( $rows as $row ) {
3280 $this->
delete( $table, [ $sqlCondition ], $fname );
3283 $this->
insert( $table, $row, $fname );
3287 }
catch ( Throwable $e ) {
3302 } elseif ( !$uniqueKey ) {
3306 if ( count( $uniqueKey ) == 1 ) {
3308 $column = reset( $uniqueKey );
3309 $values = array_column( $rows, $column );
3310 if ( count( $values ) !== count( $rows ) ) {
3311 throw new DBUnexpectedError( $this,
"Missing values for unique key ($column)" );
3314 return $this->
makeList( [ $column => $values ], self::LIST_AND );
3318 foreach ( $rows as $row ) {
3319 $rowKeyMap = array_intersect_key( $row, array_flip( $uniqueKey ) );
3320 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
3323 "Missing values for unique key (" . implode(
',', $uniqueKey ) .
")"
3326 $disjunctions[] = $this->
makeList( $rowKeyMap, self::LIST_AND );
3329 return count( $disjunctions ) > 1
3330 ? $this->
makeList( $disjunctions, self::LIST_OR )
3341 if ( !$uniqueKeys ) {
3346 foreach ( $uniqueKeys as $uniqueKey ) {
3350 return count( $disjunctions ) > 1
3351 ? $this->
makeList( $disjunctions, self::LIST_OR )
3355 public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__ ) {
3361 if ( $uniqueKeys ) {
3363 $this->
doUpsert( $table, $rows, $uniqueKeys, $set, $fname );
3365 $this->queryLogger->warning(
3366 __METHOD__ .
" called with no unique keys",
3367 [
'exception' =>
new RuntimeException() ]
3369 $this->
doInsert( $table, $rows, $fname );
3385 protected function doUpsert( $table, array $rows, array $uniqueKeys, array $set, $fname ) {
3387 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
3389 foreach ( $rows as $row ) {
3392 $this->
update( $table, $set, [ $sqlConditions ], $fname );
3395 if ( $rowsUpdated <= 0 ) {
3397 $this->
insert( $table, $row, $fname );
3402 }
catch ( Throwable $e ) {
3413 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
3420 $delTable = $this->
tableName( $delTable );
3421 $joinTable = $this->
tableName( $joinTable );
3422 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
3423 if ( $conds !=
'*' ) {
3424 $sql .=
'WHERE ' . $this->
makeList( $conds, self::LIST_AND );
3428 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3437 $sql =
"SHOW COLUMNS FROM $table LIKE \"$field\"";
3438 $res = $this->
query( $sql, __METHOD__, self::QUERY_CHANGE_NONE );
3443 if ( preg_match(
'/\((.*)\)/', $row->Type, $m ) ) {
3452 public function delete( $table, $conds, $fname = __METHOD__ ) {
3456 $sql =
"DELETE FROM $table";
3458 if ( $conds !== IDatabase::ALL_ROWS ) {
3459 if ( is_array( $conds ) ) {
3460 $conds = $this->
makeList( $conds, self::LIST_AND );
3462 $sql .=
' WHERE ' . $conds;
3465 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3475 $fname = __METHOD__,
3476 $insertOptions = [],
3477 $selectOptions = [],
3478 $selectJoinConds = []
3480 static $hints = [
'NO_AUTO_COLUMNS' ];
3485 if ( $this->cliMode && $this->
isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
3494 array_diff( $insertOptions, $hints ),
3505 array_diff( $insertOptions, $hints ),
3545 array $insertOptions,
3546 array $selectOptions,
3553 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3558 implode(
',', $fields ),
3561 array_merge( $selectOptions, [
'FOR UPDATE' ] ),
3569 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
3572 foreach (
$res as $row ) {
3573 $rows[] = (array)$row;
3576 $rowBatches = array_chunk( $rows, $this->nonNativeInsertSelectBatchSize );
3577 foreach ( $rowBatches as $rows ) {
3578 $this->
insert( $destTable, $rows, $fname, $insertOptions );
3581 }
catch ( Throwable $e ) {
3610 array $insertOptions,
3611 array $selectOptions,
3614 list( $sqlVerb, $sqlOpts ) = $this->
isFlagInOptions(
'IGNORE', $insertOptions )
3616 : [
'INSERT INTO',
'' ];
3617 $encDstTable = $this->
tableName( $destTable );
3618 $sqlDstColumns = implode(
',', array_keys( $varMap ) );
3621 array_values( $varMap ),
3628 $sql = rtrim(
"$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
3630 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3638 if ( !is_numeric( $limit ) ) {
3641 "Invalid non-numeric limit passed to " . __METHOD__
3646 return "$sql LIMIT "
3647 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
3664 $glue = $all ?
') UNION ALL (' :
') UNION (';
3666 return '(' . implode( $glue, $sqls ) .
')';
3670 $table, $vars, array $permute_conds, $extra_conds =
'', $fname = __METHOD__,
3671 $options = [], $join_conds = []
3675 foreach ( $permute_conds as $field => $values ) {
3680 $values = array_unique( $values );
3682 foreach ( $conds as $cond ) {
3683 foreach ( $values as $value ) {
3684 $cond[$field] = $value;
3685 $newConds[] = $cond;
3691 $extra_conds = $extra_conds ===
'' ? [] : (array)$extra_conds;
3695 if ( count( $conds ) === 1 &&
3699 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3708 $limit = $options[
'LIMIT'] ??
null;
3709 $offset = $options[
'OFFSET'] ??
false;
3710 $all = empty( $options[
'NOTALL'] ) && !in_array(
'NOTALL', $options );
3712 unset( $options[
'ORDER BY'], $options[
'LIMIT'], $options[
'OFFSET'] );
3714 if ( array_key_exists(
'INNER ORDER BY', $options ) ) {
3715 $options[
'ORDER BY'] = $options[
'INNER ORDER BY'];
3717 if ( $limit !==
null && is_numeric( $offset ) && $offset != 0 ) {
3721 $options[
'LIMIT'] = $limit + $offset;
3722 unset( $options[
'OFFSET'] );
3727 foreach ( $conds as $cond ) {
3729 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3733 if ( $limit !==
null ) {
3734 $sql = $this->
limitResult( $sql, $limit, $offset );
3745 if ( is_array( $cond ) ) {
3746 $cond = $this->
makeList( $cond, self::LIST_AND );
3749 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3757 return "REPLACE({$orig}, {$old}, {$new})";
3835 $function = array_shift(
$args );
3838 $this->
begin( __METHOD__ );
3845 $retVal = $function( ...
$args );
3850 usleep( mt_rand( self::$DEADLOCK_DELAY_MIN, self::$DEADLOCK_DELAY_MAX ) );
3856 }
while ( --$tries > 0 );
3858 if ( $tries <= 0 ) {
3863 $this->
commit( __METHOD__ );
3874 # Real waits are implemented in the subclass.
3914 $this->
begin( __METHOD__, self::TRANSACTION_INTERNAL );
3915 $this->trxAutomatic =
true;
3931 $this->
begin( __METHOD__, self::TRANSACTION_INTERNAL );
3932 $this->trxAutomatic =
true;
3939 $this->
startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3943 }
catch ( Throwable $e ) {
3951 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
3961 if ( $this->
trxLevel() && $this->trxAtomicLevels ) {
3962 $levelInfo = end( $this->trxAtomicLevels );
3964 return $levelInfo[1];
3979 foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3980 if ( $info[2] === $old ) {
3981 $this->trxPreCommitCallbacks[$key][2] = $new;
3984 foreach ( $this->trxIdleCallbacks as $key => $info ) {
3985 if ( $info[2] === $old ) {
3986 $this->trxIdleCallbacks[$key][2] = $new;
3989 foreach ( $this->trxEndCallbacks as $key => $info ) {
3990 if ( $info[2] === $old ) {
3991 $this->trxEndCallbacks[$key][2] = $new;
3994 foreach ( $this->trxSectionCancelCallbacks as $key => $info ) {
3995 if ( $info[2] === $old ) {
3996 $this->trxSectionCancelCallbacks[$key][2] = $new;
4024 $this->trxIdleCallbacks = array_filter(
4025 $this->trxIdleCallbacks,
4026 function ( $entry ) use ( $sectionIds ) {
4027 return !in_array( $entry[2], $sectionIds,
true );
4030 $this->trxPreCommitCallbacks = array_filter(
4031 $this->trxPreCommitCallbacks,
4032 function ( $entry ) use ( $sectionIds ) {
4033 return !in_array( $entry[2], $sectionIds,
true );
4037 foreach ( $this->trxEndCallbacks as $key => $entry ) {
4038 if ( in_array( $entry[2], $sectionIds,
true ) ) {
4039 $callback = $entry[0];
4040 $this->trxEndCallbacks[$key][0] =
function () use ( $callback ) {
4041 return $callback( self::TRIGGER_ROLLBACK, $this );
4044 $this->trxEndCallbacks[$key][2] =
null;
4048 foreach ( $this->trxSectionCancelCallbacks as $key => $entry ) {
4049 if ( in_array( $entry[2], $sectionIds,
true ) ) {
4050 $this->trxSectionCancelCallbacks[$key][2] = $newSectionId;
4057 $this->trxRecurringCallbacks[$name] = $callback;
4059 unset( $this->trxRecurringCallbacks[$name] );
4072 $this->trxEndCallbacksSuppressed = $suppress;
4087 throw new DBUnexpectedError( $this, __METHOD__ .
': a transaction is still open' );
4090 if ( $this->trxEndCallbacksSuppressed ) {
4095 $autoTrx = $this->
getFlag( self::DBO_TRX );
4099 $callbacks = array_merge(
4100 $this->trxIdleCallbacks,
4101 $this->trxEndCallbacks
4103 $this->trxIdleCallbacks = [];
4104 $this->trxEndCallbacks = [];
4108 if ( $trigger === self::TRIGGER_ROLLBACK ) {
4109 $callbacks = array_merge( $callbacks, $this->trxSectionCancelCallbacks );
4111 $this->trxSectionCancelCallbacks = [];
4113 foreach ( $callbacks as $callback ) {
4115 list( $phpCallback ) = $callback;
4118 call_user_func( $phpCallback, $trigger, $this );
4119 }
catch ( Throwable $ex ) {
4120 call_user_func( $this->errorLogger, $ex );
4125 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
4129 $this->
setFlag( self::DBO_TRX );
4136 }
while ( count( $this->trxIdleCallbacks ) );
4138 if ( $e instanceof Throwable ) {
4160 $this->trxPreCommitCallbacks = [];
4161 foreach ( $callbacks as $callback ) {
4164 list( $phpCallback ) = $callback;
4166 $phpCallback( $this );
4167 }
catch ( Throwable $ex ) {
4173 }
while ( count( $this->trxPreCommitCallbacks ) );
4175 if ( $e instanceof Throwable ) {
4190 $trigger, array $sectionIds =
null
4198 $this->trxSectionCancelCallbacks = [];
4199 foreach ( $callbacks as $entry ) {
4200 if ( $sectionIds ===
null || in_array( $entry[2], $sectionIds,
true ) ) {
4203 $entry[0]( $trigger, $this );
4204 }
catch ( Throwable $ex ) {
4209 $notCancelled[] = $entry;
4213 }
while ( count( $this->trxSectionCancelCallbacks ) );
4214 $this->trxSectionCancelCallbacks = $notCancelled;
4216 if ( $e !==
null ) {
4231 if ( $this->trxEndCallbacksSuppressed ) {
4238 foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
4240 $phpCallback( $trigger, $this );
4241 }
catch ( Throwable $ex ) {
4247 if ( $e instanceof Throwable ) {
4265 $this->
query( $sql, $fname, self::QUERY_CHANGE_TRX );
4281 $this->
query( $sql, $fname, self::QUERY_CHANGE_TRX );
4297 $this->
query( $sql, $fname, self::QUERY_CHANGE_TRX );
4306 if ( strlen( $savepointId ) > 30 ) {
4311 'There have been an excessively large number of atomic sections in a transaction'
4312 .
" started by $this->trxFname (at $fname)"
4316 return $savepointId;
4320 $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
4325 $this->
begin( $fname, self::TRANSACTION_INTERNAL );
4328 if ( $this->
getFlag( self::DBO_TRX ) ) {
4334 $this->trxAutomaticAtomic =
true;
4336 } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
4342 $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
4343 $this->queryLogger->debug(
'startAtomic: entering level ' .
4344 ( count( $this->trxAtomicLevels ) - 1 ) .
" ($fname)" );
4350 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
4355 $pos = count( $this->trxAtomicLevels ) - 1;
4356 list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
4357 $this->queryLogger->debug(
"endAtomic: leaving level $pos ($fname)" );
4359 if ( $savedFname !== $fname ) {
4362 "Invalid atomic section ended (got $fname but expected $savedFname)"
4367 array_pop( $this->trxAtomicLevels );
4369 if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
4370 $this->
commit( $fname, self::FLUSHING_INTERNAL );
4371 } elseif ( $savepointId !==
null && $savepointId !== self::$NOT_APPLICABLE ) {
4378 if ( $currentSectionId ) {
4386 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
4393 $excisedFnames = [];
4394 if ( $sectionId !==
null ) {
4397 foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
4398 if ( $asId === $sectionId ) {
4406 $len = count( $this->trxAtomicLevels );
4407 for ( $i = $pos + 1; $i < $len; ++$i ) {
4408 $excisedFnames[] = $this->trxAtomicLevels[$i][0];
4409 $excisedIds[] = $this->trxAtomicLevels[$i][1];
4411 $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
4416 $pos = count( $this->trxAtomicLevels ) - 1;
4417 list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
4419 if ( $excisedFnames ) {
4420 $this->queryLogger->debug(
"cancelAtomic: canceling level $pos ($savedFname) " .
4421 "and descendants " . implode(
', ', $excisedFnames ) );
4423 $this->queryLogger->debug(
"cancelAtomic: canceling level $pos ($savedFname)" );
4426 if ( $savedFname !== $fname ) {
4429 "Invalid atomic section ended (got $fname but expected $savedFname)"
4434 array_pop( $this->trxAtomicLevels );
4435 $excisedIds[] = $savedSectionId;
4438 if ( $savepointId !==
null ) {
4440 if ( $savepointId === self::$NOT_APPLICABLE ) {
4441 $this->
rollback( $fname, self::FLUSHING_INTERNAL );
4446 $this->trxStatusIgnoredCause =
null;
4451 } elseif ( $this->
trxStatus > self::STATUS_TRX_ERROR ) {
4453 $this->
trxStatus = self::STATUS_TRX_ERROR;
4456 "Uncancelable atomic section canceled (got $fname)"
4465 $this->affectedRowCount = 0;
4469 $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
4471 $sectionId = $this->
startAtomic( $fname, $cancelable );
4473 $res = $callback( $this, $fname );
4474 }
catch ( Throwable $e ) {
4484 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
4485 static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
4486 if ( !in_array( $mode, $modes,
true ) ) {
4492 if ( $this->trxAtomicLevels ) {
4494 $msg =
"$fname: got explicit BEGIN while atomic section(s) $levels are open";
4496 } elseif ( !$this->trxAutomatic ) {
4497 $msg =
"$fname: explicit transaction already active (from {$this->trxFname})";
4500 $msg =
"$fname: implicit transaction already active (from {$this->trxFname})";
4503 } elseif ( $this->
getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
4504 $msg =
"$fname: implicit transaction expected (DBO_TRX set)";
4511 $this->trxShortId = sprintf(
'%06x', mt_rand( 0, 0xffffff ) );
4513 $this->trxStatusIgnoredCause =
null;
4514 $this->trxAtomicCounter = 0;
4516 $this->trxFname = $fname;
4517 $this->trxDoneWrites =
false;
4518 $this->trxAutomaticAtomic =
false;
4519 $this->trxAtomicLevels = [];
4520 $this->trxWriteDuration = 0.0;
4521 $this->trxWriteQueryCount = 0;
4522 $this->trxWriteAffectedRows = 0;
4523 $this->trxWriteAdjDuration = 0.0;
4524 $this->trxWriteAdjQueryCount = 0;
4525 $this->trxWriteCallers = [];
4528 $this->trxReplicaLag =
null;
4533 $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
4545 $this->
query(
'BEGIN', $fname, self::QUERY_CHANGE_TRX );
4548 final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4549 static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
4550 if ( !in_array( $flush, $modes,
true ) ) {
4551 throw new DBUnexpectedError( $this,
"$fname: invalid flush parameter '$flush'" );
4554 if ( $this->
trxLevel() && $this->trxAtomicLevels ) {
4559 "$fname: got COMMIT while atomic sections $levels are still open"
4563 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
4566 } elseif ( !$this->trxAutomatic ) {
4569 "$fname: flushing an explicit transaction, getting out of sync"
4573 $this->queryLogger->error(
4574 "$fname: no transaction to commit, something got out of sync" );
4576 } elseif ( $this->trxAutomatic ) {
4579 "$fname: expected mass commit of all peer transactions (DBO_TRX set)"
4590 $this->
trxStatus = self::STATUS_TRX_NONE;
4592 if ( $this->trxDoneWrites ) {
4593 $this->lastWriteTime = microtime(
true );
4594 $this->trxProfiler->transactionWritingOut(
4599 $this->trxWriteAffectedRows
4604 if ( $flush !== self::FLUSHING_ALL_PEERS ) {
4620 $this->
query(
'COMMIT', $fname, self::QUERY_CHANGE_TRX );
4624 final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4627 if ( $flush !== self::FLUSHING_INTERNAL
4628 && $flush !== self::FLUSHING_ALL_PEERS
4629 && $this->
getFlag( self::DBO_TRX )
4633 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
4642 $this->
trxStatus = self::STATUS_TRX_NONE;
4643 $this->trxAtomicLevels = [];
4647 if ( $this->trxDoneWrites ) {
4648 $this->trxProfiler->transactionWritingOut(
4653 $this->trxWriteAffectedRows
4660 $this->trxIdleCallbacks = [];
4661 $this->trxPreCommitCallbacks = [];
4664 if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
4667 }
catch ( Throwable $e ) {
4672 }
catch ( Throwable $e ) {
4676 $this->affectedRowCount = 0;
4690 # Disconnects cause rollback anyway, so ignore those errors
4691 $this->
query(
'ROLLBACK', $fname, self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_TRX );
4695 public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4700 "$fname: Cannot flush snapshot; " .
4701 "explicit transaction '{$this->trxFname}' is still open"
4708 "$fname: Cannot flush snapshot; " .
4709 "writes from transaction {$this->trxFname} are still pending ($fnames)"
4714 $flush !== self::FLUSHING_INTERNAL &&
4715 $flush !== self::FLUSHING_ALL_PEERS
4717 $this->queryLogger->warning(
4718 "$fname: Expected mass snapshot flush of all peer transactions " .
4719 "in the explicit transactions round '{$this->getTransactionRoundId()}'",
4720 [
'exception' =>
new RuntimeException() ]
4724 $this->
commit( $fname, self::FLUSHING_INTERNAL );
4736 $oldName, $newName, $temporary =
false, $fname = __METHOD__
4738 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4745 public function listTables( $prefix =
null, $fname = __METHOD__ ) {
4746 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4753 public function listViews( $prefix =
null, $fname = __METHOD__ ) {
4754 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4762 $t =
new ConvertibleTimestamp( $ts );
4764 return $t->getTimestamp( TS_MW );
4768 if ( $ts ===
null ) {
4776 return ( $this->affectedRowCount ===
null )
4803 } elseif ( $result ===
true ) {
4810 public function ping( &$rtt =
null ) {
4812 if ( $this->
isOpen() && ( microtime(
true ) - $this->lastPing ) < self::$PING_TTL ) {
4813 if ( !func_num_args() || $this->lastRoundTripEstimate > 0 ) {
4820 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_NONE;
4821 $ok = ( $this->
query( self::$PING_QUERY, __METHOD__,
$flags ) !== false );
4846 $this->currentDomain->getDatabase(),
4847 $this->currentDomain->getSchema(),
4848 $this->tablePrefix()
4850 $this->lastPing = microtime(
true );
4853 $this->connLogger->warning(
4854 $fname .
': lost connection to {dbserver}; reconnected',
4857 'exception' =>
new RuntimeException()
4863 $this->connLogger->error(
4864 $fname .
': lost connection to {dbserver} permanently',
4892 return ( $this->
trxLevel() && $this->trxReplicaLag !==
null )
4908 'lag' => ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) ? $this->
getLag() : 0,
4909 'since' => microtime(
true )
4933 $res = [
'lag' => 0,
'since' => INF,
'pending' => false ];
4934 foreach ( func_get_args() as $db ) {
4936 $status = $db->getSessionLagStatus();
4937 if ( $status[
'lag'] ===
false ) {
4938 $res[
'lag'] =
false;
4939 } elseif (
$res[
'lag'] !==
false ) {
4940 $res[
'lag'] = max(
$res[
'lag'], $status[
'lag'] );
4942 $res[
'since'] = min(
$res[
'since'], $status[
'since'] );
4943 $res[
'pending'] =
$res[
'pending'] ?: $db->writesPending();
4950 if ( $this->topologyRole === self::ROLE_STREAMING_MASTER ) {
4952 } elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
4988 if ( $b instanceof
Blob ) {
5003 callable $lineCallback =
null,
5004 callable $resultCallback =
null,
5006 callable $inputCallback =
null
5008 AtEase::suppressWarnings();
5009 $fp = fopen( $filename,
'r' );
5010 AtEase::restoreWarnings();
5012 if ( $fp ===
false ) {
5013 throw new RuntimeException(
"Could not open \"{$filename}\"" );
5017 $fname = __METHOD__ .
"( $filename )";
5022 $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
5023 }
catch ( Throwable $e ) {
5034 $this->schemaVars = is_array( $vars ) ? $vars :
null;
5039 callable $lineCallback =
null,
5040 callable $resultCallback =
null,
5041 $fname = __METHOD__,
5042 callable $inputCallback =
null
5044 $delimiterReset =
new ScopedCallback(
5052 while ( !feof( $fp ) ) {
5053 if ( $lineCallback ) {
5054 call_user_func( $lineCallback );
5057 $line = trim( fgets( $fp ) );
5059 if (
$line ==
'' ) {
5075 if ( $done || feof( $fp ) ) {
5078 if ( $inputCallback ) {
5079 $callbackResult = $inputCallback( $cmd );
5081 if ( is_string( $callbackResult ) || !$callbackResult ) {
5082 $cmd = $callbackResult;
5089 if ( $resultCallback ) {
5090 $resultCallback(
$res, $this );
5093 if (
$res ===
false ) {
5096 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
5103 ScopedCallback::consume( $delimiterReset );
5116 if ( $this->delimiter ) {
5118 $newLine = preg_replace(
5119 '/' . preg_quote( $this->delimiter,
'/' ) .
'$/',
'', $newLine );
5120 if ( $newLine != $prev ) {
5151 return preg_replace_callback(
5153 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
5154 \'\{\$ (\w+) }\' | # 3. addQuotes
5155 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
5156 /\*\$ (\w+) \*/ # 5. leave unencoded
5158 function ( $m ) use ( $vars ) {
5161 if ( isset( $m[1] ) && $m[1] !==
'' ) {
5162 if ( $m[1] ===
'i' ) {
5167 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3], $vars ) ) {
5168 return $this->
addQuotes( $vars[$m[3]] );
5169 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4], $vars ) ) {
5171 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5], $vars ) ) {
5172 return $vars[$m[5]];
5212 return !isset( $this->sessionNamedLocks[$lockName] );
5219 public function lock( $lockName, $method, $timeout = 5 ) {
5220 $this->sessionNamedLocks[$lockName] = 1;
5229 public function unlock( $lockName, $method ) {
5230 unset( $this->sessionNamedLocks[$lockName] );
5241 "$fname: Cannot flush pre-lock snapshot; " .
5242 "writes from transaction {$this->trxFname} are still pending ($fnames)"
5246 if ( !$this->
lock( $lockKey, $fname, $timeout ) ) {
5250 $unlocker =
new ScopedCallback(
function () use ( $lockKey, $fname ) {
5256 function () use ( $lockKey, $fname ) {
5257 $this->
unlock( $lockKey, $fname );
5262 $this->
unlock( $lockKey, $fname );
5266 $this->
commit( $fname, self::FLUSHING_INTERNAL );
5283 final public function lockTables( array $read, array $write, $method ) {
5285 throw new DBUnexpectedError( $this,
"Transaction writes or callbacks still pending" );
5349 $sql =
"DROP TABLE " . $this->
tableName( $table ) .
" CASCADE";
5350 $this->
query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
5353 public function truncate( $tables, $fname = __METHOD__ ) {
5354 $tables = is_array( $tables ) ? $tables : [ $tables ];
5356 $tablesTruncate = [];
5357 foreach ( $tables as $table ) {
5361 $tablesTruncate[] = $table;
5365 if ( $tablesTruncate ) {
5366 $this->
doTruncate( $tablesTruncate, $fname );
5377 foreach ( $tables as $table ) {
5378 $sql =
"TRUNCATE TABLE " . $this->
tableName( $table );
5379 $this->
query( $sql, $fname, self::QUERY_CHANGE_SCHEMA );
5392 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() )
5398 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() ) {
5402 return ConvertibleTimestamp::convert( $format, $expiry );
5421 if ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) {
5422 return [
'Server is configured as a read-only replica database.',
'role' ];
5423 } elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
5424 return [
'Server is configured as a read-only static clone database.',
'role' ];
5427 $reason = $this->
getLBInfo( self::LB_READ_ONLY_REASON );
5428 if ( is_string( $reason ) ) {
5429 return [ $reason,
'lb' ];
5440 $this->tableAliases = $aliases;
5448 $this->indexAliases = $aliases;
5474 if ( !$this->conn ) {
5477 'DB connection was already closed or the connection dropped'
5486 $id = function_exists(
'spl_object_id' )
5487 ? spl_object_id( $this )
5488 : spl_object_hash( $this );
5490 $description = $this->
getType() .
' object #' . $id;
5492 if ( is_resource( $this->conn ) ) {
5493 $description .=
' (' . (string)$this->conn .
')';
5494 } elseif ( is_object( $this->conn ) ) {
5496 $handleId = function_exists(
'spl_object_id' )
5497 ? spl_object_id( $this->conn )
5498 : spl_object_hash( $this->conn );
5499 $description .=
" (handle id #$handleId)";
5502 return $description;
5510 $this->connLogger->warning(
5511 "Cloning " . static::class .
" is not recommended; forking connection",
5512 [
'exception' =>
new RuntimeException() ]
5518 $this->trxEndCallbacks = [];
5519 $this->trxSectionCancelCallbacks = [];
5525 $this->currentDomain->getDatabase(),
5526 $this->currentDomain->getSchema(),
5527 $this->tablePrefix()
5529 $this->lastPing = microtime(
true );
5539 throw new RuntimeException(
'Database serialization may cause problems, since ' .
5540 'the connection is not restored on wakeup' );
5547 if ( $this->
trxLevel() && $this->trxDoneWrites ) {
5548 trigger_error(
"Uncommitted DB writes (transaction from {$this->trxFname})" );
5552 if ( $danglingWriters ) {
5553 $fnames = implode(
', ', $danglingWriters );
5554 trigger_error(
"DB transaction writes or callbacks still pending ($fnames)" );
5557 if ( $this->conn ) {
5560 AtEase::suppressWarnings();
5562 AtEase::restoreWarnings();
5571class_alias( Database::class,
'DatabaseBase' );
5576class_alias( Database::class,
'Database' );
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
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.
fieldInfo( $table, $field)
mysql_fetch_field() wrapper Returns false if the field doesn't exist