28 use Psr\Log\LoggerAwareInterface;
29 use Psr\Log\LoggerInterface;
30 use Psr\Log\NullLogger;
31 use Wikimedia\ScopedCallback;
32 use Wikimedia\Timestamp\ConvertibleTimestamp;
33 use Wikimedia\AtEase\AtEase;
37 use InvalidArgumentException;
38 use UnexpectedValueException;
181 const ATTR_DB_LEVEL_LOCKING =
'db-level-locking';
183 const ATTR_SCHEMAS_AS_TABLE_GROUPS =
'supports-schemas';
186 const NEW_UNCONNECTED = 0;
188 const NEW_CONNECTED = 1;
191 const STATUS_TRX_ERROR = 1;
193 const STATUS_TRX_OK = 2;
195 const STATUS_TRX_NONE = 3;
243 $this->connectionParams = [];
244 foreach ( [
'host',
'user',
'password',
'dbname',
'schema',
'tablePrefix' ] as $name ) {
245 $this->connectionParams[$name] = $params[$name];
247 $this->connectionVariables = $params[
'variables'] ?? [];
248 $this->cliMode = $params[
'cliMode'];
249 $this->agent = $params[
'agent'];
250 $this->flags = $params[
'flags'];
252 if ( $this->cliMode ) {
258 $this->nonNativeInsertSelectBatchSize = $params[
'nonNativeInsertSelectBatchSize'] ?? 10000;
260 $this->srvCache = $params[
'srvCache'] ??
new HashBagOStuff();
261 $this->profiler = is_callable( $params[
'profiler'] ) ? $params[
'profiler'] :
null;
262 $this->trxProfiler = $params[
'trxProfiler'];
263 $this->connLogger = $params[
'connLogger'];
264 $this->queryLogger = $params[
'queryLogger'];
265 $this->errorLogger = $params[
'errorLogger'];
266 $this->deprecationLogger = $params[
'deprecationLogger'];
270 $params[
'dbname'] !=
'' ? $params[
'dbname'] :
null,
271 $params[
'schema'] !=
'' ? $params[
'schema'] :
null,
272 $params[
'tablePrefix']
275 $this->ownerId = $params[
'ownerId'] ??
null;
288 throw new LogicException( __METHOD__ .
': already connected' );
302 $this->connectionParams[
'host'],
303 $this->connectionParams[
'user'],
304 $this->connectionParams[
'password'],
305 $this->connectionParams[
'dbname'],
306 $this->connectionParams[
'schema'],
307 $this->connectionParams[
'tablePrefix']
370 final public static function factory(
$type, $params = [], $connect = self::NEW_CONNECTED ) {
373 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
383 'cliMode' => ( PHP_SAPI ===
'cli' || PHP_SAPI ===
'phpdbg' ),
384 'agent' => basename( $_SERVER[
'SCRIPT_NAME'] ) .
'@' . gethostname(),
388 $normalizedParams = [
390 'host' => strlen( $params[
'host'] ) ? $params[
'host'] :
null,
391 'user' => strlen( $params[
'user'] ) ? $params[
'user'] :
null,
392 'password' => is_string( $params[
'password'] ) ? $params[
'password'] :
null,
393 'dbname' => strlen( $params[
'dbname'] ) ? $params[
'dbname'] :
null,
394 'schema' => strlen( $params[
'schema'] ) ? $params[
'schema'] :
null,
395 'tablePrefix' => (string)$params[
'tablePrefix'],
396 'flags' => (
int)$params[
'flags'],
397 'variables' => $params[
'variables'],
398 'cliMode' => (bool)$params[
'cliMode'],
399 'agent' => (
string)$params[
'agent'],
402 'profiler' => $params[
'profiler'] ??
null,
404 'connLogger' => $params[
'connLogger'] ??
new NullLogger(),
405 'queryLogger' => $params[
'queryLogger'] ??
new NullLogger(),
406 'errorLogger' => $params[
'errorLogger'] ??
function ( Exception $e ) {
407 trigger_error( get_class( $e ) .
': ' . $e->getMessage(), E_USER_WARNING );
409 'deprecationLogger' => $params[
'deprecationLogger'] ??
function ( $msg ) {
410 trigger_error( $msg, E_USER_DEPRECATED );
415 $conn =
new $class( $normalizedParams );
416 if ( $connect === self::NEW_CONNECTED ) {
417 $conn->initConnection();
435 self::ATTR_DB_LEVEL_LOCKING =>
false,
436 self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
441 return call_user_func( [ $class,
'getAttributes' ] ) + $defaults;
450 private static function getClass( $dbType, $driver =
null ) {
457 static $builtinTypes = [
458 'mysql' => [
'mysqli' => DatabaseMysqli::class ],
459 'sqlite' => DatabaseSqlite::class,
460 'postgres' => DatabasePostgres::class,
463 $dbType = strtolower( $dbType );
466 if ( isset( $builtinTypes[$dbType] ) ) {
467 $possibleDrivers = $builtinTypes[$dbType];
468 if ( is_string( $possibleDrivers ) ) {
469 $class = $possibleDrivers;
470 } elseif ( (
string)$driver !==
'' ) {
471 if ( !isset( $possibleDrivers[$driver] ) ) {
472 throw new InvalidArgumentException( __METHOD__ .
473 " type '$dbType' does not support driver '{$driver}'" );
476 $class = $possibleDrivers[$driver];
478 foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
479 if ( extension_loaded( $posDriver ) ) {
480 $class = $possibleClass;
486 $class =
'Database' . ucfirst( $dbType );
489 if ( $class ===
false ) {
490 throw new InvalidArgumentException( __METHOD__ .
491 " no viable database extension found for type '$dbType'" );
513 $this->queryLogger = $logger;
532 return ( $this->trxShortId !=
'' ) ? 1 : 0;
548 $old = $this->currentDomain->getTablePrefix();
549 if ( $prefix !==
null ) {
551 $this->currentDomain->getDatabase(),
552 $this->currentDomain->getSchema(),
561 if ( strlen( $schema ) && $this->
getDBname() ===
null ) {
562 throw new DBUnexpectedError( $this,
"Cannot set schema to '$schema'; no database set" );
565 $old = $this->currentDomain->getSchema();
566 if ( $schema !==
null ) {
568 $this->currentDomain->getDatabase(),
570 strlen( $schema ) ? $schema :
null,
571 $this->currentDomain->getTablePrefix()
586 if ( is_null( $name ) ) {
590 if ( array_key_exists( $name, $this->lbInfo ) ) {
591 return $this->lbInfo[$name];
597 public function setLBInfo( $nameOrArray, $value =
null ) {
598 if ( is_array( $nameOrArray ) ) {
599 $this->lbInfo = $nameOrArray;
600 } elseif ( is_string( $nameOrArray ) ) {
601 if ( $value !==
null ) {
602 $this->lbInfo[$nameOrArray] = $value;
604 unset( $this->lbInfo[$nameOrArray] );
607 throw new InvalidArgumentException(
"Got non-string key" );
612 $this->lazyMasterHandle =
$conn;
633 return $this->lastWriteTime ?:
false;
642 $this->trxDoneWrites ||
643 $this->trxIdleCallbacks ||
644 $this->trxPreCommitCallbacks ||
645 $this->trxEndCallbacks ||
662 return is_string( $id ) ? $id :
null;
671 } elseif ( !$this->trxDoneWrites ) {
676 case self::ESTIMATE_DB_APPLY:
689 $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
690 $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
693 $applyTime += self::$TINY_WRITE_SEC * $omitted;
699 return $this->
trxLevel() ? $this->trxWriteCallers : [];
717 $this->trxIdleCallbacks,
718 $this->trxPreCommitCallbacks,
719 $this->trxEndCallbacks,
720 $this->trxSectionCancelCallbacks
722 foreach ( $callbacks as $callback ) {
723 $fnames[] = $callback[1];
734 return array_reduce( $this->trxAtomicLevels,
function ( $accum, $v ) {
735 return $accum ===
null ? $v[0] :
"$accum, " . $v[0];
743 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
744 if ( $flag & ~static::$DBO_MUTABLE ) {
747 "Got $flag (allowed: " . implode(
', ', static::$MUTABLE_FLAGS ) .
')'
751 if ( $remember === self::REMEMBER_PRIOR ) {
752 array_push( $this->priorFlags, $this->flags );
755 $this->flags |= $flag;
758 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
759 if ( $flag & ~static::$DBO_MUTABLE ) {
762 "Got $flag (allowed: " . implode(
', ', static::$MUTABLE_FLAGS ) .
')'
766 if ( $remember === self::REMEMBER_PRIOR ) {
767 array_push( $this->priorFlags, $this->flags );
770 $this->flags &= ~$flag;
774 if ( !$this->priorFlags ) {
778 if ( $state === self::RESTORE_INITIAL ) {
779 $this->flags = reset( $this->priorFlags );
780 $this->priorFlags = [];
782 $this->flags = array_pop( $this->priorFlags );
787 return ( ( $this->flags & $flag ) === $flag );
791 return $this->currentDomain->getId();
801 abstract function indexInfo( $table, $index, $fname = __METHOD__ );
815 $this->lastPhpError =
false;
816 $this->htmlErrors = ini_set(
'html_errors',
'0' );
817 set_error_handler( [ $this,
'connectionErrorLogger' ] );
826 restore_error_handler();
827 if ( $this->htmlErrors !==
false ) {
828 ini_set(
'html_errors', $this->htmlErrors );
838 if ( $this->lastPhpError ) {
839 $error = preg_replace(
'!\[<a.*</a>\]!',
'', $this->lastPhpError );
840 $error = preg_replace(
'!^.*?:\s?(.*)$!',
'$1', $error );
856 $this->lastPhpError = $errstr;
868 'db_server' => $this->server,
870 'db_user' => $this->user,
876 final public function close( $fname = __METHOD__, $owner =
null ) {
879 $wasOpen = (bool)$this->conn;
884 if ( $this->trxAtomicLevels ) {
887 $error =
"$fname: atomic sections $levels are still open";
888 } elseif ( $this->trxAutomatic ) {
892 $error =
"$fname: " .
893 "expected mass rollback of all peer transactions (DBO_TRX set)";
898 $error =
"$fname: transaction is still open (from {$this->trxFname})";
901 if ( $this->trxEndCallbacksSuppressed && $error ===
null ) {
902 $error =
"$fname: callbacks are suppressed; cannot properly commit";
906 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
918 if ( $error !==
null ) {
922 if ( $this->ownerId !==
null && $owner === $this->ownerId ) {
923 $this->queryLogger->error( $error );
937 throw new RuntimeException(
938 "Transaction callbacks are still pending: " . implode(
', ', $fnames )
970 'Write operations are not allowed on replica database connections'
974 if ( $reason !==
false ) {
1015 abstract protected function doQuery( $sql );
1048 '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\(SELECT)\b/i',
1058 return preg_match(
'/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) :
null;
1077 [
'BEGIN',
'ROLLBACK',
'COMMIT',
'SET',
'SHOW',
'CREATE',
'ALTER',
'USE',
'SHOW' ],
1091 static $qt =
'[`"\']?(\w+)[`"\']?';
1094 '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?' . $qt .
'/i',
1101 } elseif ( preg_match(
1102 '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt .
'/i',
1106 return [ $this->sessionTempTables[
$matches[1]] ??
null,
null,
$matches[1] ];
1107 } elseif ( preg_match(
1108 '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt .
'/i',
1112 return [ $this->sessionTempTables[
$matches[1]] ??
null,
null, null ];
1113 } elseif ( preg_match(
1114 '/^(?:(?:INSERT|REPLACE)\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+' . $qt .
'/i',
1118 return [ $this->sessionTempTables[
$matches[1]] ??
null,
null, null ];
1121 return [
null,
null, null ];
1131 if ( $ret !==
false ) {
1132 if ( $tmpNew !==
null ) {
1133 $this->sessionTempTables[$tmpNew] = $tmpType;
1135 if ( $tmpDel !==
null ) {
1136 unset( $this->sessionTempTables[$tmpDel] );
1148 list( $ret, $err, $errno, $unignorable ) = $this->
executeQuery( $sql, $fname,
$flags );
1149 if ( $ret ===
false ) {
1152 $this->
reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
1181 $priorTransaction = $this->
trxLevel();
1191 list( $tmpType, $tmpNew, $tmpDel ) = $this->
getTempWrites( $sql, $pseudoPermanent );
1199 $isPermWrite =
false;
1201 list( $tmpType, $tmpNew, $tmpDel ) = [
null,
null, null ];
1206 $encAgent = str_replace(
'/',
'-', $this->agent );
1207 $commentedSql = preg_replace(
'/\s|$/',
" /* $fname $encAgent */ ", $sql, 1 );
1211 list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
1216 if ( $ret ===
false && $recoverableCL && $reconnected && $allowRetry ) {
1218 list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
1225 $corruptedTrx =
false;
1227 if ( $ret ===
false ) {
1228 if ( $priorTransaction ) {
1229 if ( $recoverableSR ) {
1230 # We're ignoring an error that caused just the current query to be aborted.
1231 # But log the cause so we can log a deprecation notice if a caller actually
1233 $this->trxStatusIgnoredCause = [ $err, $errno, $fname ];
1234 } elseif ( !$recoverableCL ) {
1235 # Either the query was aborted or all queries after BEGIN where aborted.
1236 # In the first case, the only options going forward are (a) ROLLBACK, or
1237 # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1238 # option is ROLLBACK, since the snapshots would have been released.
1239 $corruptedTrx =
true;
1240 $this->
trxStatus = self::STATUS_TRX_ERROR;
1241 $this->trxStatusCause =
1243 $this->trxStatusIgnoredCause =
null;
1248 return [ $ret, $err, $errno, $corruptedTrx ];
1272 if ( (
$flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
1277 if ( $isPermWrite ) {
1278 $this->lastWriteTime = microtime(
true );
1279 if ( $this->
trxLevel() && !$this->trxDoneWrites ) {
1280 $this->trxDoneWrites =
true;
1281 $this->trxProfiler->transactionWritingIn(
1282 $this->server, $this->
getDomainID(), $this->trxShortId );
1286 $prefix = $this->
getLBInfo(
'master' ) ?
'query-m: ' :
'query: ';
1287 $generalizedSql =
new GeneralizedSql( $sql, $this->trxShortId, $prefix );
1289 $startTime = microtime(
true );
1290 $ps = $this->profiler
1293 $this->affectedRowCount =
null;
1295 $ret = $this->
doQuery( $commentedSql );
1301 $queryRuntime = max( microtime(
true ) - $startTime, 0.0 );
1303 $recoverableSR =
false;
1304 $recoverableCL =
false;
1305 $reconnected =
false;
1307 if ( $ret !==
false ) {
1308 $this->lastPing = $startTime;
1309 if ( $isPermWrite && $this->
trxLevel() ) {
1311 $this->trxWriteCallers[] = $fname;
1314 # Check if no meaningful session state was lost
1316 # Update session state tracking and try to restore the connection
1319 # Check if only the last query was rolled back
1323 if ( $sql === self::$PING_QUERY ) {
1324 $this->lastRoundTripEstimate = $queryRuntime;
1327 $this->trxProfiler->recordQueryCompletion(
1336 $this->queryLogger->debug(
1337 "{method} [{runtime}s] {db_host}: {sql}",
1343 'runtime' => round( $queryRuntime, 3 )
1348 return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
1363 $this->
begin( __METHOD__ .
" ($fname)", self::TRANSACTION_INTERNAL );
1364 $this->trxAutomatic =
true;
1382 $indicativeOfReplicaRuntime =
true;
1383 if ( $runtime > self::$SLOW_WRITE_SEC ) {
1386 if ( $verb ===
'INSERT' ) {
1388 } elseif ( $verb ===
'REPLACE' ) {
1389 $indicativeOfReplicaRuntime = $this->
affectedRows() > self::$SMALL_WRITE_ROWS / 2;
1393 $this->trxWriteDuration += $runtime;
1394 $this->trxWriteQueryCount += 1;
1395 $this->trxWriteAffectedRows += $affected;
1396 if ( $indicativeOfReplicaRuntime ) {
1397 $this->trxWriteAdjDuration += $runtime;
1398 $this->trxWriteAdjQueryCount += 1;
1411 if ( $verb ===
'USE' ) {
1412 throw new DBUnexpectedError( $this,
"Got USE query; use selectDomain() instead" );
1415 if ( $verb ===
'ROLLBACK' ) {
1419 if ( $this->
trxStatus < self::STATUS_TRX_OK ) {
1422 "Cannot execute query from $fname while transaction status is ERROR",
1424 $this->trxStatusCause
1426 } elseif ( $this->
trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1428 call_user_func( $this->deprecationLogger,
1429 "Caller from $fname ignored an error originally raised from $iFname: " .
1430 "[$iLastErrno] $iLastError"
1432 $this->trxStatusIgnoredCause =
null;
1440 "Explicit transaction still active. A caller may have caught an error. "
1456 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1457 # Dropped connections also mean that named locks are automatically released.
1458 # Only allow error suppression in autocommit mode or when the lost transaction
1459 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1460 if ( $this->sessionNamedLocks ) {
1462 } elseif ( $this->sessionTempTables ) {
1464 } elseif ( $sql ===
'COMMIT' ) {
1465 return !$priorWritesPending;
1466 } elseif ( $sql ===
'ROLLBACK' ) {
1470 } elseif ( $priorWritesPending ) {
1484 $this->sessionTempTables = [];
1487 $this->sessionNamedLocks = [];
1490 $this->trxAtomicCounter = 0;
1491 $this->trxIdleCallbacks = [];
1492 $this->trxPreCommitCallbacks = [];
1496 if ( $this->trxDoneWrites ) {
1497 $this->trxProfiler->transactionWritingOut(
1502 $this->trxWriteAffectedRows
1522 }
catch ( Exception $ex ) {
1528 }
catch ( Exception $ex ) {
1540 $this->trxShortId =
'';
1572 $this->queryLogger->debug(
"SQL ERROR (ignored): $error" );
1586 $this->queryLogger->error(
1587 "{fname}\t{db_server}\t{errno}\t{error}\t{sql1line}",
1589 'method' => __METHOD__,
1592 'sql1line' => mb_substr( str_replace(
"\n",
"\\n", $sql ), 0, 5 * 1024 ),
1594 'exception' =>
new RuntimeException()
1603 $e =
new DBQueryError( $this, $error, $errno, $sql, $fname );
1617 $this->connLogger->error(
1618 "Error connecting to {db_server} as user {db_user}: {error}",
1621 'exception' =>
new RuntimeException()
1632 $table, $var, $cond =
'', $fname = __METHOD__, $options = [], $join_conds = []
1634 if ( $var ===
'*' ) {
1638 if ( !is_array( $options ) ) {
1639 $options = [ $options ];
1642 $options[
'LIMIT'] = 1;
1644 $res = $this->
select( $table, $var, $cond, $fname, $options, $join_conds );
1645 if (
$res ===
false ) {
1650 if ( $row ===
false ) {
1654 return reset( $row );
1658 $table, $var, $cond =
'', $fname = __METHOD__, $options = [], $join_conds = []
1660 if ( $var ===
'*' ) {
1662 } elseif ( !is_string( $var ) ) {
1666 if ( !is_array( $options ) ) {
1667 $options = [ $options ];
1670 $res = $this->
select( $table, [
'value' => $var ], $cond, $fname, $options, $join_conds );
1671 if (
$res ===
false ) {
1676 foreach (
$res as $row ) {
1677 $values[] = $row->value;
1694 $preLimitTail = $postLimitTail =
'';
1699 foreach ( $options as $key => $option ) {
1700 if ( is_numeric( $key ) ) {
1701 $noKeyOptions[$option] =
true;
1709 if ( isset( $noKeyOptions[
'FOR UPDATE'] ) ) {
1710 $postLimitTail .=
' FOR UPDATE';
1713 if ( isset( $noKeyOptions[
'LOCK IN SHARE MODE'] ) ) {
1714 $postLimitTail .=
' LOCK IN SHARE MODE';
1717 if ( isset( $noKeyOptions[
'DISTINCT'] ) || isset( $noKeyOptions[
'DISTINCTROW'] ) ) {
1718 $startOpts .=
'DISTINCT';
1721 # Various MySQL extensions
1722 if ( isset( $noKeyOptions[
'STRAIGHT_JOIN'] ) ) {
1723 $startOpts .=
' /*! STRAIGHT_JOIN */';
1726 if ( isset( $noKeyOptions[
'SQL_BIG_RESULT'] ) ) {
1727 $startOpts .=
' SQL_BIG_RESULT';
1730 if ( isset( $noKeyOptions[
'SQL_BUFFER_RESULT'] ) ) {
1731 $startOpts .=
' SQL_BUFFER_RESULT';
1734 if ( isset( $noKeyOptions[
'SQL_SMALL_RESULT'] ) ) {
1735 $startOpts .=
' SQL_SMALL_RESULT';
1738 if ( isset( $noKeyOptions[
'SQL_CALC_FOUND_ROWS'] ) ) {
1739 $startOpts .=
' SQL_CALC_FOUND_ROWS';
1742 if ( isset( $options[
'USE INDEX'] ) && is_string( $options[
'USE INDEX'] ) ) {
1747 if ( isset( $options[
'IGNORE INDEX'] ) && is_string( $options[
'IGNORE INDEX'] ) ) {
1753 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1766 if ( isset( $options[
'GROUP BY'] ) ) {
1767 $gb = is_array( $options[
'GROUP BY'] )
1768 ? implode(
',', $options[
'GROUP BY'] )
1769 : $options[
'GROUP BY'];
1770 $sql .=
' GROUP BY ' . $gb;
1772 if ( isset( $options[
'HAVING'] ) ) {
1773 $having = is_array( $options[
'HAVING'] )
1775 : $options[
'HAVING'];
1776 $sql .=
' HAVING ' . $having;
1791 if ( isset( $options[
'ORDER BY'] ) ) {
1792 $ob = is_array( $options[
'ORDER BY'] )
1793 ? implode(
',', $options[
'ORDER BY'] )
1794 : $options[
'ORDER BY'];
1796 return ' ORDER BY ' . $ob;
1803 $table, $vars, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
1805 $sql = $this->
selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1807 return $this->
query( $sql, $fname );
1811 $options = [], $join_conds = []
1813 if ( is_array( $vars ) ) {
1819 $options = (array)$options;
1820 $useIndexes = ( isset( $options[
'USE INDEX'] ) && is_array( $options[
'USE INDEX'] ) )
1821 ? $options[
'USE INDEX']
1824 isset( $options[
'IGNORE INDEX'] ) &&
1825 is_array( $options[
'IGNORE INDEX'] )
1827 ? $options[
'IGNORE INDEX']
1837 $this->deprecationLogger,
1838 __METHOD__ .
": aggregation used with a locking SELECT ($fname)"
1842 if ( is_array( $table ) ) {
1845 $table, $useIndexes, $ignoreIndexes, $join_conds );
1846 } elseif ( $table !=
'' ) {
1849 [ $table ], $useIndexes, $ignoreIndexes, [] );
1854 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1857 if ( is_array( $conds ) ) {
1861 if ( $conds ===
null || $conds ===
false ) {
1862 $this->queryLogger->warning(
1866 .
' with incorrect parameters: $conds must be a string or an array'
1871 if ( $conds ===
'' || $conds ===
'*' ) {
1872 $sql =
"SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
1873 } elseif ( is_string( $conds ) ) {
1874 $sql =
"SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
1875 "WHERE $conds $preLimitTail";
1877 throw new DBUnexpectedError( $this, __METHOD__ .
' called with incorrect parameters' );
1880 if ( isset( $options[
'LIMIT'] ) ) {
1881 $sql = $this->
limitResult( $sql, $options[
'LIMIT'],
1882 $options[
'OFFSET'] ??
false );
1884 $sql =
"$sql $postLimitTail";
1886 if ( isset( $options[
'EXPLAIN'] ) ) {
1887 $sql =
'EXPLAIN ' . $sql;
1893 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
1894 $options = [], $join_conds = []
1896 $options = (array)$options;
1897 $options[
'LIMIT'] = 1;
1899 $res = $this->
select( $table, $vars, $conds, $fname, $options, $join_conds );
1900 if (
$res ===
false ) {
1912 $table, $var =
'*', $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
1916 if ( is_string( $column ) && !in_array( $column, [
'*',
'1' ] ) ) {
1917 $conds[] =
"$column IS NOT NULL";
1921 $table, [
'rowcount' =>
'COUNT(*)' ], $conds, $fname, $options, $join_conds
1925 return isset( $row[
'rowcount'] ) ? (int)$row[
'rowcount'] : 0;
1929 $tables, $var =
'*', $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
1933 if ( is_string( $column ) && !in_array( $column, [
'*',
'1' ] ) ) {
1934 $conds[] =
"$column IS NOT NULL";
1948 [
'rowcount' =>
'COUNT(*)' ],
1954 return isset( $row[
'rowcount'] ) ? (int)$row[
'rowcount'] : 0;
1962 $options = (array)$options;
1963 foreach ( [
'FOR UPDATE',
'LOCK IN SHARE MODE' ] as $lock ) {
1964 if ( in_array( $lock, $options,
true ) ) {
1978 foreach ( (array)$options as $key => $value ) {
1979 if ( is_string( $key ) ) {
1980 if ( preg_match(
'/^(?:GROUP BY|HAVING)$/i', $key ) ) {
1983 } elseif ( is_string( $value ) ) {
1984 if ( preg_match(
'/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
1990 $regex =
'/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
1991 foreach ( (array)$fields as $field ) {
1992 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
2006 if ( $conds ===
null || $conds ===
false ) {
2007 $this->queryLogger->warning(
2011 .
' with incorrect parameters: $conds must be a string or an array'
2016 if ( !is_array( $conds ) ) {
2017 $conds = ( $conds ===
'' ) ? [] : [ $conds ];
2029 if ( is_array( $var ) ) {
2032 } elseif ( count( $var ) == 1 ) {
2033 $column = $var[0] ?? reset( $var );
2045 $table, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2050 __METHOD__ .
': no transaction is active nor is DBO_TRX set'
2054 $options = (array)$options;
2055 $options[] =
'FOR UPDATE';
2057 return $this->
selectRowCount( $table,
'*', $conds, $fname, $options, $join_conds );
2061 $info = $this->
fieldInfo( $table, $field );
2071 $info = $this->
indexInfo( $table, $index, $fname );
2072 if ( is_null( $info ) ) {
2075 return $info !==
false;
2079 abstract public function tableExists( $table, $fname = __METHOD__ );
2082 $indexInfo = $this->
indexInfo( $table, $index );
2084 if ( !$indexInfo ) {
2088 return !$indexInfo[0]->Non_unique;
2098 return implode(
' ', $options );
2101 public function insert( $table, $a, $fname = __METHOD__, $options = [] ) {
2102 # No rows to insert, easy just return now
2103 if ( !count( $a ) ) {
2109 if ( !is_array( $options ) ) {
2110 $options = [ $options ];
2115 if ( isset( $a[0] ) && is_array( $a[0] ) ) {
2117 $keys = array_keys( $a[0] );
2120 $keys = array_keys( $a );
2123 $sql =
'INSERT ' . $options .
2124 " INTO $table (" . implode(
',',
$keys ) .
') VALUES ';
2128 foreach ( $a as $row ) {
2134 $sql .=
'(' . $this->
makeList( $row ) .
')';
2137 $sql .=
'(' . $this->
makeList( $a ) .
')';
2140 $this->
query( $sql, $fname );
2152 if ( !is_array( $options ) ) {
2153 $options = [ $options ];
2158 if ( in_array(
'IGNORE', $options ) ) {
2174 return implode(
' ', $opts );
2177 public function update( $table, $values, $conds, $fname = __METHOD__, $options = [] ) {
2182 if ( $conds !== [] && $conds !==
'*' ) {
2186 $this->
query( $sql, $fname );
2192 if ( !is_array( $a ) ) {
2193 throw new DBUnexpectedError( $this, __METHOD__ .
' called with incorrect parameters' );
2199 foreach ( $a as $field => $value ) {
2213 $list .=
"($value)";
2220 $includeNull =
false;
2221 foreach ( array_keys( $value,
null,
true ) as $nullKey ) {
2222 $includeNull =
true;
2223 unset( $value[$nullKey] );
2225 if ( count( $value ) == 0 && !$includeNull ) {
2226 throw new InvalidArgumentException(
2227 __METHOD__ .
": empty input for field $field" );
2228 } elseif ( count( $value ) == 0 ) {
2230 $list .=
"$field IS NULL";
2233 if ( $includeNull ) {
2237 if ( count( $value ) == 1 ) {
2241 $value = array_values( $value )[0];
2242 $list .= $field .
" = " . $this->
addQuotes( $value );
2244 $list .= $field .
" IN (" . $this->
makeList( $value ) .
") ";
2247 if ( $includeNull ) {
2248 $list .=
" OR $field IS NULL)";
2251 } elseif ( $value ===
null ) {
2253 $list .=
"$field IS ";
2255 $list .=
"$field = ";
2262 $list .=
"$field = ";
2274 foreach ( $data as
$base => $sub ) {
2275 if ( count( $sub ) ) {
2277 [ $baseKey =>
$base, $subKey => array_keys( $sub ) ],
2298 public function bitAnd( $fieldLeft, $fieldRight ) {
2299 return "($fieldLeft & $fieldRight)";
2302 public function bitOr( $fieldLeft, $fieldRight ) {
2303 return "($fieldLeft | $fieldRight)";
2307 return 'CONCAT(' . implode(
',', $stringList ) .
')';
2311 $delim, $table, $field, $conds =
'', $join_conds = []
2313 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->
addQuotes( $delim ) .
')';
2315 return '(' . $this->
selectSQLText( $table, $fld, $conds,
null, [], $join_conds ) .
')';
2320 $functionBody =
"$input FROM $startPosition";
2321 if ( $length !==
null ) {
2322 $functionBody .=
" FOR $length";
2324 return 'SUBSTRING(' . $functionBody .
')';
2340 if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2341 throw new InvalidArgumentException(
2342 '$startPosition must be a positive integer'
2345 if ( !( is_int( $length ) && $length >= 0 || $length ===
null ) ) {
2346 throw new InvalidArgumentException(
2347 '$length must be null or an integer greater than or equal to 0'
2355 return "CAST( $field AS CHARACTER )";
2359 return 'CAST( ' . $field .
' AS INTEGER )';
2363 $table, $vars, $conds =
'', $fname = __METHOD__,
2364 $options = [], $join_conds = []
2367 $this->
selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2378 $this->currentDomain->getSchema(),
2379 $this->currentDomain->getTablePrefix()
2396 $this->currentDomain = $domain;
2400 return $this->currentDomain->getDatabase();
2411 __METHOD__ .
': got Subquery instance when expecting a string'
2415 # Skip the entire process when we have a string quoted on both ends.
2416 # Note that we check the end so that we will still quote any use of
2417 # use of `database`.table. But won't break things if someone wants
2418 # to query a database table with a dot in the name.
2423 # Lets test for any bits of text that should never show up in a table
2424 # name. Basically anything like JOIN or ON which are actually part of
2425 # SQL queries, but may end up inside of the table value to combine
2426 # sql. Such as how the API is doing.
2427 # Note that we use a whitespace test rather than a \b test to avoid
2428 # any remote case where a word like on may be inside of a table name
2429 # surrounded by symbols which may be considered word breaks.
2430 if ( preg_match(
'/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2431 $this->queryLogger->warning(
2432 __METHOD__ .
": use of subqueries is not supported this way",
2433 [
'exception' =>
new RuntimeException() ]
2439 # Split database and table into proper variables.
2442 # Quote $table and apply the prefix if not quoted.
2443 # $tableName might be empty if this is called from Database::replaceVars()
2444 $tableName =
"{$prefix}{$table}";
2445 if ( $format ===
'quoted'
2447 && $tableName !==
''
2452 # Quote $schema and $database and merge them with the table name if needed
2466 # We reverse the explode so that database.table and table both output the correct table.
2467 $dbDetails = explode(
'.', $name, 3 );
2468 if ( count( $dbDetails ) == 3 ) {
2469 list( $database, $schema, $table ) = $dbDetails;
2470 # We don't want any prefix added in this case
2472 } elseif ( count( $dbDetails ) == 2 ) {
2473 list( $database, $table ) = $dbDetails;
2474 # We don't want any prefix added in this case
2476 # In dbs that support it, $database may actually be the schema
2477 # but that doesn't affect any of the functionality here
2480 list( $table ) = $dbDetails;
2481 if ( isset( $this->tableAliases[$table] ) ) {
2482 $database = $this->tableAliases[$table][
'dbname'];
2483 $schema = is_string( $this->tableAliases[$table][
'schema'] )
2484 ? $this->tableAliases[$table][
'schema']
2486 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
2487 ? $this->tableAliases[$table][
'prefix']
2496 return [ $database, $schema, $prefix, $table ];
2506 if ( strlen( $namespace ) ) {
2510 $relation = $namespace .
'.' . $relation;
2517 $inArray = func_get_args();
2520 foreach ( $inArray as $name ) {
2521 $retVal[$name] = $this->
tableName( $name );
2528 $inArray = func_get_args();
2531 foreach ( $inArray as $name ) {
2550 if ( is_string( $table ) ) {
2551 $quotedTable = $this->
tableName( $table );
2552 } elseif ( $table instanceof
Subquery ) {
2553 $quotedTable = (string)$table;
2555 throw new InvalidArgumentException(
"Table must be a string or Subquery" );
2558 if ( $alias ===
false || $alias === $table ) {
2559 if ( $table instanceof
Subquery ) {
2560 throw new InvalidArgumentException(
"Subquery table missing alias" );
2563 return $quotedTable;
2577 foreach ( $tables as $alias => $table ) {
2578 if ( is_numeric( $alias ) ) {
2596 if ( !$alias || (
string)$alias === (
string)$name ) {
2611 foreach ( $fields as $alias => $field ) {
2612 if ( is_numeric( $alias ) ) {
2632 $tables, $use_index = [], $ignore_index = [], $join_conds = []
2636 $use_index = (array)$use_index;
2637 $ignore_index = (array)$ignore_index;
2638 $join_conds = (array)$join_conds;
2640 foreach ( $tables as $alias => $table ) {
2641 if ( !is_string( $alias ) ) {
2646 if ( is_array( $table ) ) {
2648 if ( count( $table ) > 1 ) {
2649 $joinedTable =
'(' .
2651 $table, $use_index, $ignore_index, $join_conds ) .
')';
2654 $innerTable = reset( $table );
2655 $innerAlias = key( $table );
2658 is_string( $innerAlias ) ? $innerAlias : $innerTable
2666 if ( isset( $join_conds[$alias] ) ) {
2667 list( $joinType, $conds ) = $join_conds[$alias];
2668 $tableClause = $joinType;
2669 $tableClause .=
' ' . $joinedTable;
2670 if ( isset( $use_index[$alias] ) ) {
2671 $use = $this->
useIndexClause( implode(
',', (array)$use_index[$alias] ) );
2673 $tableClause .=
' ' . $use;
2676 if ( isset( $ignore_index[$alias] ) ) {
2678 implode(
',', (array)$ignore_index[$alias] ) );
2679 if ( $ignore !=
'' ) {
2680 $tableClause .=
' ' . $ignore;
2685 $tableClause .=
' ON (' . $on .
')';
2688 $retJOIN[] = $tableClause;
2689 } elseif ( isset( $use_index[$alias] ) ) {
2691 $tableClause = $joinedTable;
2693 implode(
',', (array)$use_index[$alias] )
2696 $ret[] = $tableClause;
2697 } elseif ( isset( $ignore_index[$alias] ) ) {
2699 $tableClause = $joinedTable;
2701 implode(
',', (array)$ignore_index[$alias] )
2704 $ret[] = $tableClause;
2706 $tableClause = $joinedTable;
2708 $ret[] = $tableClause;
2713 $implicitJoins = implode(
',', $ret );
2714 $explicitJoins = implode(
' ', $retJOIN );
2717 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
2727 return $this->indexAliases[$index] ?? $index;
2731 if (
$s instanceof
Blob ) {
2734 if (
$s ===
null ) {
2736 } elseif ( is_bool(
$s ) ) {
2739 # This will also quote numeric values. This should be harmless,
2740 # and protects against weird problems that occur when they really
2741 # _are_ strings such as article titles and string->number->string
2742 # conversion is not 1:1.
2748 return '"' . str_replace(
'"',
'""',
$s ) .
'"';
2761 return $name[0] ==
'"' && substr( $name, -1, 1 ) ==
'"';
2770 return str_replace( [ $escapeChar,
'%',
'_' ],
2771 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
2776 if ( is_array( $param ) ) {
2779 $params = func_get_args();
2790 foreach ( $params as $value ) {
2792 $s .= $value->toString();
2842 public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2843 if ( count( $rows ) == 0 ) {
2847 $uniqueIndexes = (array)$uniqueIndexes;
2849 if ( !is_array( reset( $rows ) ) ) {
2854 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
2856 foreach ( $rows as $row ) {
2858 $indexWhereClauses = [];
2859 foreach ( $uniqueIndexes as $index ) {
2860 $indexColumns = (array)$index;
2861 $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
2862 if ( count( $indexRowValues ) != count( $indexColumns ) ) {
2865 'New record does not provide all values for unique key (' .
2866 implode(
', ', $indexColumns ) .
')'
2868 } elseif ( in_array(
null, $indexRowValues,
true ) ) {
2871 'New record has a null value for unique key (' .
2872 implode(
', ', $indexColumns ) .
')'
2878 if ( $indexWhereClauses ) {
2879 $this->
delete( $table, $this->
makeList( $indexWhereClauses,
LIST_OR ), $fname );
2884 $this->
insert( $table, $row, $fname );
2889 }
catch ( Exception $e ) {
2907 if ( !is_array( reset( $rows ) ) ) {
2911 $sql =
"REPLACE INTO $table (" . implode(
',', array_keys( $rows[0] ) ) .
') VALUES ';
2914 foreach ( $rows as $row ) {
2921 $sql .=
'(' . $this->
makeList( $row ) .
')';
2924 $this->
query( $sql, $fname );
2927 public function upsert( $table, array $rows, $uniqueIndexes, array $set,
2930 if ( $rows === [] ) {
2934 $uniqueIndexes = (array)$uniqueIndexes;
2935 if ( !is_array( reset( $rows ) ) ) {
2939 if ( count( $uniqueIndexes ) ) {
2941 foreach ( $rows as $row ) {
2942 foreach ( $uniqueIndexes as $index ) {
2943 $index = is_array( $index ) ? $index : [ $index ];
2945 foreach ( $index as $column ) {
2946 $rowKey[$column] = $row[$column];
2958 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
2959 # Update any existing conflicting row(s)
2960 if ( $where !==
false ) {
2961 $this->
update( $table, $set, $where, $fname );
2964 # Now insert any non-conflicting row(s)
2965 $this->
insert( $table, $rows, $fname, [
'IGNORE' ] );
2969 }
catch ( Exception $e ) {
2977 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2984 $delTable = $this->
tableName( $delTable );
2985 $joinTable = $this->
tableName( $joinTable );
2986 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2987 if ( $conds !=
'*' ) {
2992 $this->
query( $sql, $fname );
2997 $sql =
"SHOW COLUMNS FROM $table LIKE \"$field\"";
2998 $res = $this->
query( $sql, __METHOD__ );
3003 if ( preg_match(
'/\((.*)\)/', $row->Type, $m ) ) {
3012 public function delete( $table, $conds, $fname = __METHOD__ ) {
3014 throw new DBUnexpectedError( $this, __METHOD__ .
' called with no conditions' );
3018 $sql =
"DELETE FROM $table";
3020 if ( $conds !=
'*' ) {
3021 if ( is_array( $conds ) ) {
3024 $sql .=
' WHERE ' . $conds;
3027 $this->
query( $sql, $fname );
3033 $destTable, $srcTable, $varMap, $conds,
3034 $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3036 static $hints = [
'NO_AUTO_COLUMNS' ];
3038 $insertOptions = (array)$insertOptions;
3039 $selectOptions = (array)$selectOptions;
3041 if ( $this->cliMode && $this->
isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
3050 array_diff( $insertOptions, $hints ),
3061 array_diff( $insertOptions, $hints ),
3095 $fname = __METHOD__,
3096 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3102 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3105 $selectOptions[] =
'FOR UPDATE';
3107 $srcTable, implode(
',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds
3115 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
3118 foreach (
$res as $row ) {
3119 $rows[] = (array)$row;
3123 $ok = $this->
insert( $destTable, $rows, $fname, $insertOptions );
3131 if ( $rows && $ok ) {
3132 $ok = $this->
insert( $destTable, $rows, $fname, $insertOptions );
3143 }
catch ( Exception $e ) {
3164 $fname = __METHOD__,
3165 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3167 $destTable = $this->
tableName( $destTable );
3169 if ( !is_array( $insertOptions ) ) {
3170 $insertOptions = [ $insertOptions ];
3177 array_values( $varMap ),
3184 $sql =
"INSERT $insertOptions" .
3185 " INTO $destTable (" . implode(
',', array_keys( $varMap ) ) .
') ' .
3188 $this->
query( $sql, $fname );
3192 if ( !is_numeric( $limit ) ) {
3195 "Invalid non-numeric limit passed to " . __METHOD__
3200 return "$sql LIMIT "
3201 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
3210 $glue = $all ?
') UNION ALL (' :
') UNION (';
3212 return '(' . implode( $glue, $sqls ) .
')';
3216 $table, $vars, array $permute_conds, $extra_conds =
'', $fname = __METHOD__,
3217 $options = [], $join_conds = []
3221 foreach ( $permute_conds as $field => $values ) {
3226 $values = array_unique( $values );
3228 foreach ( $conds as $cond ) {
3229 foreach ( $values as $value ) {
3230 $cond[$field] = $value;
3231 $newConds[] = $cond;
3237 $extra_conds = $extra_conds ===
'' ? [] : (array)$extra_conds;
3241 if ( count( $conds ) === 1 &&
3245 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3254 $limit = $options[
'LIMIT'] ??
null;
3255 $offset = $options[
'OFFSET'] ??
false;
3256 $all = empty( $options[
'NOTALL'] ) && !in_array(
'NOTALL', $options );
3258 unset( $options[
'ORDER BY'], $options[
'LIMIT'], $options[
'OFFSET'] );
3260 if ( array_key_exists(
'INNER ORDER BY', $options ) ) {
3261 $options[
'ORDER BY'] = $options[
'INNER ORDER BY'];
3263 if ( $limit !==
null && is_numeric( $offset ) && $offset != 0 ) {
3267 $options[
'LIMIT'] = $limit + $offset;
3268 unset( $options[
'OFFSET'] );
3273 foreach ( $conds as $cond ) {
3275 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3279 if ( $limit !==
null ) {
3280 $sql = $this->
limitResult( $sql, $limit, $offset );
3287 if ( is_array( $cond ) ) {
3291 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3295 return "REPLACE({$orig}, {$old}, {$new})";
3347 $args = func_get_args();
3348 $function = array_shift(
$args );
3351 $this->
begin( __METHOD__ );
3358 $retVal = $function( ...
$args );
3363 usleep( mt_rand( self::$DEADLOCK_DELAY_MIN, self::$DEADLOCK_DELAY_MAX ) );
3369 }
while ( --$tries > 0 );
3371 if ( $tries <= 0 ) {
3376 $this->
commit( __METHOD__ );
3383 # Real waits are implemented in the subclass.
3411 $this->
begin( __METHOD__, self::TRANSACTION_INTERNAL );
3412 $this->trxAutomatic =
true;
3428 $this->
begin( __METHOD__, self::TRANSACTION_INTERNAL );
3429 $this->trxAutomatic =
true;
3436 $this->
startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3440 }
catch ( Exception $e ) {
3448 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
3458 if ( $this->
trxLevel() && $this->trxAtomicLevels ) {
3459 $levelInfo = end( $this->trxAtomicLevels );
3461 return $levelInfo[1];
3476 foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3477 if ( $info[2] === $old ) {
3478 $this->trxPreCommitCallbacks[$key][2] = $new;
3481 foreach ( $this->trxIdleCallbacks as $key => $info ) {
3482 if ( $info[2] === $old ) {
3483 $this->trxIdleCallbacks[$key][2] = $new;
3486 foreach ( $this->trxEndCallbacks as $key => $info ) {
3487 if ( $info[2] === $old ) {
3488 $this->trxEndCallbacks[$key][2] = $new;
3491 foreach ( $this->trxSectionCancelCallbacks as $key => $info ) {
3492 if ( $info[2] === $old ) {
3493 $this->trxSectionCancelCallbacks[$key][2] = $new;
3521 $this->trxIdleCallbacks = array_filter(
3522 $this->trxIdleCallbacks,
3523 function ( $entry ) use ( $sectionIds ) {
3524 return !in_array( $entry[2], $sectionIds,
true );
3527 $this->trxPreCommitCallbacks = array_filter(
3528 $this->trxPreCommitCallbacks,
3529 function ( $entry ) use ( $sectionIds ) {
3530 return !in_array( $entry[2], $sectionIds,
true );
3534 foreach ( $this->trxEndCallbacks as $key => $entry ) {
3535 if ( in_array( $entry[2], $sectionIds,
true ) ) {
3536 $callback = $entry[0];
3537 $this->trxEndCallbacks[$key][0] =
function () use ( $callback ) {
3539 return $callback( self::TRIGGER_ROLLBACK, $this );
3542 $this->trxEndCallbacks[$key][2] =
null;
3546 foreach ( $this->trxSectionCancelCallbacks as $key => $entry ) {
3547 if ( in_array( $entry[2], $sectionIds,
true ) ) {
3548 $this->trxSectionCancelCallbacks[$key][2] = $newSectionId;
3555 $this->trxRecurringCallbacks[$name] = $callback;
3557 unset( $this->trxRecurringCallbacks[$name] );
3570 $this->trxEndCallbacksSuppressed = $suppress;
3585 throw new DBUnexpectedError( $this, __METHOD__ .
': a transaction is still open' );
3588 if ( $this->trxEndCallbacksSuppressed ) {
3597 $callbacks = array_merge(
3598 $this->trxIdleCallbacks,
3599 $this->trxEndCallbacks
3601 $this->trxIdleCallbacks = [];
3602 $this->trxEndCallbacks = [];
3606 if ( $trigger === self::TRIGGER_ROLLBACK ) {
3607 $callbacks = array_merge( $callbacks, $this->trxSectionCancelCallbacks );
3609 $this->trxSectionCancelCallbacks = [];
3611 foreach ( $callbacks as $callback ) {
3613 list( $phpCallback ) = $callback;
3617 call_user_func( $phpCallback, $trigger, $this );
3618 }
catch ( Exception $ex ) {
3619 call_user_func( $this->errorLogger, $ex );
3624 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
3634 }
while ( count( $this->trxIdleCallbacks ) );
3636 if ( $e instanceof Exception ) {
3658 $this->trxPreCommitCallbacks = [];
3659 foreach ( $callbacks as $callback ) {
3662 list( $phpCallback ) = $callback;
3664 $phpCallback( $this );
3665 }
catch ( Exception $ex ) {
3670 }
while ( count( $this->trxPreCommitCallbacks ) );
3672 if ( $e instanceof Exception ) {
3687 $trigger, array $sectionIds =
null
3695 $this->trxSectionCancelCallbacks = [];
3696 foreach ( $callbacks as $entry ) {
3697 if ( $sectionIds ===
null || in_array( $entry[2], $sectionIds,
true ) ) {
3700 $entry[0]( $trigger, $this );
3701 }
catch ( Exception $ex ) {
3704 }
catch ( Throwable $ex ) {
3709 $notCancelled[] = $entry;
3712 }
while ( count( $this->trxSectionCancelCallbacks ) );
3713 $this->trxSectionCancelCallbacks = $notCancelled;
3715 if ( $e !==
null ) {
3730 if ( $this->trxEndCallbacksSuppressed ) {
3737 foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
3739 $phpCallback( $trigger, $this );
3740 }
catch ( Exception $ex ) {
3746 if ( $e instanceof Exception ) {
3799 if ( strlen( $savepointId ) > 30 ) {
3804 'There have been an excessively large number of atomic sections in a transaction'
3805 .
" started by $this->trxFname (at $fname)"
3809 return $savepointId;
3813 $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
3818 $this->
begin( $fname, self::TRANSACTION_INTERNAL );
3827 $this->trxAutomaticAtomic =
true;
3829 } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
3835 $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
3836 $this->queryLogger->debug(
'startAtomic: entering level ' .
3837 ( count( $this->trxAtomicLevels ) - 1 ) .
" ($fname)" );
3843 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
3848 $pos = count( $this->trxAtomicLevels ) - 1;
3849 list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3850 $this->queryLogger->debug(
"endAtomic: leaving level $pos ($fname)" );
3852 if ( $savedFname !== $fname ) {
3855 "Invalid atomic section ended (got $fname but expected $savedFname)"
3860 array_pop( $this->trxAtomicLevels );
3862 if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
3863 $this->
commit( $fname, self::FLUSHING_INTERNAL );
3864 } elseif ( $savepointId !==
null && $savepointId !== self::$NOT_APPLICABLE ) {
3871 if ( $currentSectionId ) {
3879 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
3886 $excisedFnames = [];
3887 if ( $sectionId !==
null ) {
3890 foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
3891 if ( $asId === $sectionId ) {
3899 $len = count( $this->trxAtomicLevels );
3900 for ( $i = $pos + 1; $i < $len; ++$i ) {
3901 $excisedFnames[] = $this->trxAtomicLevels[$i][0];
3902 $excisedIds[] = $this->trxAtomicLevels[$i][1];
3904 $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
3909 $pos = count( $this->trxAtomicLevels ) - 1;
3910 list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3912 if ( $excisedFnames ) {
3913 $this->queryLogger->debug(
"cancelAtomic: canceling level $pos ($savedFname) " .
3914 "and descendants " . implode(
', ', $excisedFnames ) );
3916 $this->queryLogger->debug(
"cancelAtomic: canceling level $pos ($savedFname)" );
3919 if ( $savedFname !== $fname ) {
3922 "Invalid atomic section ended (got $fname but expected $savedFname)"
3927 array_pop( $this->trxAtomicLevels );
3928 $excisedIds[] = $savedSectionId;
3931 if ( $savepointId !==
null ) {
3933 if ( $savepointId === self::$NOT_APPLICABLE ) {
3934 $this->
rollback( $fname, self::FLUSHING_INTERNAL );
3939 $this->trxStatusIgnoredCause =
null;
3944 } elseif ( $this->
trxStatus > self::STATUS_TRX_ERROR ) {
3946 $this->
trxStatus = self::STATUS_TRX_ERROR;
3949 "Uncancelable atomic section canceled (got $fname)"
3958 $this->affectedRowCount = 0;
3962 $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
3964 $sectionId = $this->
startAtomic( $fname, $cancelable );
3966 $res = $callback( $this, $fname );
3967 }
catch ( Exception $e ) {
3977 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
3978 static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
3979 if ( !in_array( $mode, $modes,
true ) ) {
3985 if ( $this->trxAtomicLevels ) {
3987 $msg =
"$fname: got explicit BEGIN while atomic section(s) $levels are open";
3989 } elseif ( !$this->trxAutomatic ) {
3990 $msg =
"$fname: explicit transaction already active (from {$this->trxFname})";
3993 $msg =
"$fname: implicit transaction already active (from {$this->trxFname})";
3997 $msg =
"$fname: implicit transaction expected (DBO_TRX set)";
4004 $this->trxShortId = sprintf(
'%06x', mt_rand( 0, 0xffffff ) );
4006 $this->trxStatusIgnoredCause =
null;
4007 $this->trxAtomicCounter = 0;
4009 $this->trxFname = $fname;
4010 $this->trxDoneWrites =
false;
4011 $this->trxAutomaticAtomic =
false;
4012 $this->trxAtomicLevels = [];
4013 $this->trxWriteDuration = 0.0;
4014 $this->trxWriteQueryCount = 0;
4015 $this->trxWriteAffectedRows = 0;
4016 $this->trxWriteAdjDuration = 0.0;
4017 $this->trxWriteAdjQueryCount = 0;
4018 $this->trxWriteCallers = [];
4021 $this->trxReplicaLag =
null;
4026 $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
4037 $this->
query(
'BEGIN', $fname );
4040 final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4041 static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
4042 if ( !in_array( $flush, $modes,
true ) ) {
4043 throw new DBUnexpectedError( $this,
"$fname: invalid flush parameter '$flush'" );
4046 if ( $this->
trxLevel() && $this->trxAtomicLevels ) {
4051 "$fname: got COMMIT while atomic sections $levels are still open"
4055 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
4058 } elseif ( !$this->trxAutomatic ) {
4061 "$fname: flushing an explicit transaction, getting out of sync"
4065 $this->queryLogger->error(
4066 "$fname: no transaction to commit, something got out of sync" );
4068 } elseif ( $this->trxAutomatic ) {
4071 "$fname: expected mass commit of all peer transactions (DBO_TRX set)"
4082 $this->
trxStatus = self::STATUS_TRX_NONE;
4084 if ( $this->trxDoneWrites ) {
4085 $this->lastWriteTime = microtime(
true );
4086 $this->trxProfiler->transactionWritingOut(
4091 $this->trxWriteAffectedRows
4096 if ( $flush !== self::FLUSHING_ALL_PEERS ) {
4111 $this->
query(
'COMMIT', $fname );
4115 final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4118 if ( $flush !== self::FLUSHING_INTERNAL
4119 && $flush !== self::FLUSHING_ALL_PEERS
4124 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
4133 $this->
trxStatus = self::STATUS_TRX_NONE;
4134 $this->trxAtomicLevels = [];
4138 if ( $this->trxDoneWrites ) {
4139 $this->trxProfiler->transactionWritingOut(
4144 $this->trxWriteAffectedRows
4151 $this->trxIdleCallbacks = [];
4152 $this->trxPreCommitCallbacks = [];
4155 if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
4158 }
catch ( Exception $e ) {
4163 }
catch ( Exception $e ) {
4167 $this->affectedRowCount = 0;
4180 # Disconnects cause rollback anyway, so ignore those errors
4181 $ignoreErrors =
true;
4182 $this->
query(
'ROLLBACK', $fname, $ignoreErrors );
4186 public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4191 "$fname: Cannot flush snapshot; " .
4192 "explicit transaction '{$this->trxFname}' is still open"
4199 "$fname: Cannot flush snapshot; " .
4200 "writes from transaction {$this->trxFname} are still pending ($fnames)"
4205 $flush !== self::FLUSHING_INTERNAL &&
4206 $flush !== self::FLUSHING_ALL_PEERS
4208 $this->queryLogger->warning(
4209 "$fname: Expected mass snapshot flush of all peer transactions " .
4210 "in the explicit transactions round '{$this->getTransactionRoundId()}'",
4211 [
'exception' =>
new RuntimeException() ]
4215 $this->
commit( $fname, self::FLUSHING_INTERNAL );
4223 $oldName, $newName, $temporary =
false, $fname = __METHOD__
4225 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4228 public function listTables( $prefix =
null, $fname = __METHOD__ ) {
4229 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4232 public function listViews( $prefix =
null, $fname = __METHOD__ ) {
4233 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4237 $t =
new ConvertibleTimestamp( $ts );
4239 return $t->getTimestamp( TS_MW );
4243 if ( is_null( $ts ) ) {
4251 return ( $this->affectedRowCount ===
null )
4278 } elseif ( $result ===
true ) {
4285 public function ping( &$rtt =
null ) {
4287 if ( $this->
isOpen() && ( microtime(
true ) - $this->lastPing ) < self::$PING_TTL ) {
4288 if ( !func_num_args() || $this->lastRoundTripEstimate > 0 ) {
4295 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS;
4296 $ok = ( $this->
query( self::$PING_QUERY, __METHOD__,
$flags ) !== false );
4321 $this->currentDomain->getDatabase(),
4322 $this->currentDomain->getSchema(),
4325 $this->lastPing = microtime(
true );
4328 $this->connLogger->warning(
4329 $fname .
': lost connection to {dbserver}; reconnected',
4332 'exception' =>
new RuntimeException()
4338 $this->connLogger->error(
4339 $fname .
': lost connection to {dbserver} permanently',
4367 return ( $this->
trxLevel() && $this->trxReplicaLag !==
null )
4381 'since' => microtime(
true )
4405 $res = [
'lag' => 0,
'since' => INF,
'pending' => false ];
4406 foreach ( func_get_args() as $db ) {
4408 $status = $db->getSessionLagStatus();
4409 if (
$status[
'lag'] ===
false ) {
4410 $res[
'lag'] =
false;
4411 } elseif (
$res[
'lag'] !==
false ) {
4415 $res[
'pending'] =
$res[
'pending'] ?: $db->writesPending();
4424 } elseif ( $this->
getLBInfo(
'is static' ) ) {
4444 if ( $b instanceof
Blob ) {
4455 callable $lineCallback =
null,
4456 callable $resultCallback =
null,
4458 callable $inputCallback =
null
4460 AtEase::suppressWarnings();
4461 $fp = fopen( $filename,
'r' );
4462 AtEase::restoreWarnings();
4464 if ( $fp ===
false ) {
4465 throw new RuntimeException(
"Could not open \"{$filename}\"" );
4469 $fname = __METHOD__ .
"( $filename )";
4474 $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
4475 }
catch ( Exception $e ) {
4486 $this->schemaVars = is_array( $vars ) ? $vars :
null;
4491 callable $lineCallback =
null,
4492 callable $resultCallback =
null,
4493 $fname = __METHOD__,
4494 callable $inputCallback =
null
4496 $delimiterReset =
new ScopedCallback(
4504 while ( !feof( $fp ) ) {
4505 if ( $lineCallback ) {
4506 call_user_func( $lineCallback );
4509 $line = trim( fgets( $fp ) );
4511 if (
$line ==
'' ) {
4527 if ( $done || feof( $fp ) ) {
4530 if ( $inputCallback ) {
4531 $callbackResult = $inputCallback( $cmd );
4533 if ( is_string( $callbackResult ) || !$callbackResult ) {
4534 $cmd = $callbackResult;
4541 if ( $resultCallback ) {
4542 $resultCallback(
$res, $this );
4545 if (
$res ===
false ) {
4548 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
4555 ScopedCallback::consume( $delimiterReset );
4567 if ( $this->delimiter ) {
4569 $newLine = preg_replace(
4570 '/' . preg_quote( $this->delimiter,
'/' ) .
'$/',
'', $newLine );
4571 if ( $newLine != $prev ) {
4601 return preg_replace_callback(
4603 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
4604 \'\{\$ (\w+) }\' | # 3. addQuotes
4605 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
4606 /\*\$ (\w+) \*/ # 5. leave unencoded
4608 function ( $m ) use ( $vars ) {
4611 if ( isset( $m[1] ) && $m[1] !==
'' ) {
4612 if ( $m[1] ===
'i' ) {
4617 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3], $vars ) ) {
4618 return $this->
addQuotes( $vars[$m[3]] );
4619 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4], $vars ) ) {
4621 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5], $vars ) ) {
4622 return $vars[$m[5]];
4657 return !isset( $this->sessionNamedLocks[$lockName] );
4660 public function lock( $lockName, $method, $timeout = 5 ) {
4661 $this->sessionNamedLocks[$lockName] = 1;
4666 public function unlock( $lockName, $method ) {
4667 unset( $this->sessionNamedLocks[$lockName] );
4678 "$fname: Cannot flush pre-lock snapshot; " .
4679 "writes from transaction {$this->trxFname} are still pending ($fnames)"
4683 if ( !$this->
lock( $lockKey, $fname, $timeout ) ) {
4687 $unlocker =
new ScopedCallback(
function () use ( $lockKey, $fname ) {
4693 function () use ( $lockKey, $fname ) {
4694 $this->
unlock( $lockKey, $fname );
4699 $this->
unlock( $lockKey, $fname );
4703 $this->
commit( $fname, self::FLUSHING_INTERNAL );
4716 final public function lockTables( array $read, array $write, $method ) {
4718 throw new DBUnexpectedError( $this,
"Transaction writes or callbacks still pending" );
4767 public function dropTable( $tableName, $fName = __METHOD__ ) {
4768 if ( !$this->
tableExists( $tableName, $fName ) ) {
4771 $sql =
"DROP TABLE " . $this->
tableName( $tableName ) .
" CASCADE";
4773 return $this->
query( $sql, $fName );
4781 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() )
4787 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() ) {
4791 return ConvertibleTimestamp::convert( $format, $expiry );
4806 $reason = $this->
getLBInfo(
'readOnlyReason' );
4807 if ( is_string( $reason ) ) {
4809 } elseif ( $this->
getLBInfo(
'replica' ) ) {
4810 return "Server is configured in the role of a read-only replica database.";
4817 $this->tableAliases = $aliases;
4821 $this->indexAliases = $aliases;
4846 if ( !$this->conn ) {
4849 'DB connection was already closed or the connection dropped'
4858 $id = function_exists(
'spl_object_id' )
4859 ? spl_object_id( $this )
4860 : spl_object_hash( $this );
4862 $description = $this->
getType() .
' object #' . $id;
4863 if ( is_resource( $this->conn ) ) {
4864 $description .=
' (' . (string)$this->conn .
')';
4865 } elseif ( is_object( $this->conn ) ) {
4867 $handleId = function_exists(
'spl_object_id' )
4868 ? spl_object_id( $this->conn )
4869 : spl_object_hash( $this->conn );
4870 $description .=
" (handle id #$handleId)";
4873 return $description;
4881 $this->connLogger->warning(
4882 "Cloning " . static::class .
" is not recommended; forking connection",
4883 [
'exception' =>
new RuntimeException() ]
4889 $this->trxEndCallbacks = [];
4890 $this->trxSectionCancelCallbacks = [];
4896 $this->currentDomain->getDatabase(),
4897 $this->currentDomain->getSchema(),
4900 $this->lastPing = microtime(
true );
4910 throw new RuntimeException(
'Database serialization may cause problems, since ' .
4911 'the connection is not restored on wakeup' );
4918 if ( $this->
trxLevel() && $this->trxDoneWrites ) {
4919 trigger_error(
"Uncommitted DB writes (transaction from {$this->trxFname})" );
4923 if ( $danglingWriters ) {
4924 $fnames = implode(
', ', $danglingWriters );
4925 trigger_error(
"DB transaction writes or callbacks still pending ($fnames)" );
4928 if ( $this->conn ) {
4931 AtEase::suppressWarnings();
4933 AtEase::restoreWarnings();
4942 class_alias( Database::class,
'DatabaseBase' );
4947 class_alias( Database::class,
'Database' );