31 use InvalidArgumentException;
33 use Psr\Log\LoggerAwareInterface;
34 use Psr\Log\LoggerInterface;
35 use Psr\Log\NullLogger;
38 use UnexpectedValueException;
39 use Wikimedia\AtEase\AtEase;
40 use Wikimedia\ScopedCallback;
41 use Wikimedia\Timestamp\ConvertibleTimestamp;
193 public const ATTR_DB_IS_FILE =
'db-is-file';
195 public const ATTR_DB_LEVEL_LOCKING =
'db-level-locking';
197 public const ATTR_SCHEMAS_AS_TABLE_GROUPS =
'supports-schemas';
200 public const NEW_UNCONNECTED = 0;
202 public const NEW_CONNECTED = 1;
205 public const STATUS_TRX_ERROR = 1;
207 public const STATUS_TRX_OK = 2;
209 public const STATUS_TRX_NONE = 3;
258 $this->connectionParams = [
259 'host' => strlen( $params[
'host'] ) ? $params[
'host'] :
null,
260 'user' => strlen( $params[
'user'] ) ? $params[
'user'] :
null,
261 'dbname' => strlen( $params[
'dbname'] ) ? $params[
'dbname'] :
null,
262 'schema' => strlen( $params[
'schema'] ) ? $params[
'schema'] :
null,
263 'password' => is_string( $params[
'password'] ) ? $params[
'password'] :
null,
264 'tablePrefix' => (string)$params[
'tablePrefix']
267 $this->lbInfo = $params[
'lbInfo'] ?? [];
268 $this->lazyMasterHandle = $params[
'lazyMasterHandle'] ??
null;
269 $this->connectionVariables = $params[
'variables'] ?? [];
271 $this->flags = (int)$params[
'flags'];
272 $this->cliMode = (bool)$params[
'cliMode'];
273 $this->agent = (string)$params[
'agent'];
274 $this->topologyRole = (string)$params[
'topologyRole'];
275 $this->topologyRootMaster = (string)$params[
'topologicalMaster'];
276 $this->nonNativeInsertSelectBatchSize = $params[
'nonNativeInsertSelectBatchSize'] ?? 10000;
278 $this->srvCache = $params[
'srvCache'];
279 $this->profiler = is_callable( $params[
'profiler'] ) ? $params[
'profiler'] :
null;
280 $this->trxProfiler = $params[
'trxProfiler'];
281 $this->connLogger = $params[
'connLogger'];
282 $this->queryLogger = $params[
'queryLogger'];
283 $this->replLogger = $params[
'replLogger'];
284 $this->errorLogger = $params[
'errorLogger'];
285 $this->deprecationLogger = $params[
'deprecationLogger'];
289 $params[
'dbname'] !=
'' ? $params[
'dbname'] :
null,
290 $params[
'schema'] !=
'' ? $params[
'schema'] :
null,
291 $params[
'tablePrefix']
294 $this->ownerId = $params[
'ownerId'] ??
null;
307 throw new LogicException( __METHOD__ .
': already connected' );
321 $this->connectionParams[
'host'],
322 $this->connectionParams[
'user'],
323 $this->connectionParams[
'password'],
324 $this->connectionParams[
'dbname'],
325 $this->connectionParams[
'schema'],
326 $this->connectionParams[
'tablePrefix']
396 final public static function factory(
$type, $params = [], $connect = self::NEW_CONNECTED ) {
399 if ( class_exists( $class ) && is_subclass_of( $class, IDatabase::class ) ) {
411 'cliMode' => ( PHP_SAPI ===
'cli' || PHP_SAPI ===
'phpdbg' ),
414 'topologyRole' =>
null,
415 'topologicalMaster' =>
null,
417 'lazyMasterHandle' => $params[
'lazyMasterHandle'] ??
null,
419 'profiler' => $params[
'profiler'] ??
null,
421 'connLogger' => $params[
'connLogger'] ??
new NullLogger(),
422 'queryLogger' => $params[
'queryLogger'] ??
new NullLogger(),
423 'replLogger' => $params[
'replLogger'] ??
new NullLogger(),
424 'errorLogger' => $params[
'errorLogger'] ??
function ( Throwable $e ) {
425 trigger_error( get_class( $e ) .
': ' . $e->getMessage(), E_USER_WARNING );
427 'deprecationLogger' => $params[
'deprecationLogger'] ??
function ( $msg ) {
428 trigger_error( $msg, E_USER_DEPRECATED );
433 $conn =
new $class( $params );
434 if ( $connect === self::NEW_CONNECTED ) {
435 $conn->initConnection();
453 self::ATTR_DB_IS_FILE =>
false,
454 self::ATTR_DB_LEVEL_LOCKING =>
false,
455 self::ATTR_SCHEMAS_AS_TABLE_GROUPS => false
460 return call_user_func( [ $class,
'getAttributes' ] ) + $defaults;
469 private static function getClass( $dbType, $driver =
null ) {
476 static $builtinTypes = [
477 'mysql' => [
'mysqli' => DatabaseMysqli::class ],
478 'sqlite' => DatabaseSqlite::class,
479 'postgres' => DatabasePostgres::class,
482 $dbType = strtolower( $dbType );
484 if ( !isset( $builtinTypes[$dbType] ) ) {
486 return 'Database' . ucfirst( $dbType );
490 $possibleDrivers = $builtinTypes[$dbType];
491 if ( is_string( $possibleDrivers ) ) {
492 $class = $possibleDrivers;
493 } elseif ( (
string)$driver !==
'' ) {
494 if ( !isset( $possibleDrivers[$driver] ) ) {
495 throw new InvalidArgumentException( __METHOD__ .
496 " type '$dbType' does not support driver '{$driver}'" );
499 $class = $possibleDrivers[$driver];
501 foreach ( $possibleDrivers as $posDriver => $possibleClass ) {
502 if ( extension_loaded( $posDriver ) ) {
503 $class = $possibleClass;
509 if ( $class ===
false ) {
510 throw new InvalidArgumentException( __METHOD__ .
511 " no viable database extension found for type '$dbType'" );
534 $this->queryLogger = $logger;
550 return ( $this->trxShortId !=
'' ) ? 1 : 0;
566 $old = $this->currentDomain->getTablePrefix();
567 if ( $prefix !==
null ) {
569 $this->currentDomain->getDatabase(),
570 $this->currentDomain->getSchema(),
579 if ( strlen( $schema ) && $this->
getDBname() ===
null ) {
580 throw new DBUnexpectedError( $this,
"Cannot set schema to '$schema'; no database set" );
583 $old = $this->currentDomain->getSchema();
584 if ( $schema !==
null ) {
586 $this->currentDomain->getDatabase(),
588 strlen( $schema ) ? $schema :
null,
589 $this->currentDomain->getTablePrefix()
605 if ( $name ===
null ) {
609 if ( array_key_exists( $name, $this->lbInfo ) ) {
610 return $this->lbInfo[$name];
616 public function setLBInfo( $nameOrArray, $value =
null ) {
617 if ( is_array( $nameOrArray ) ) {
618 $this->lbInfo = $nameOrArray;
619 } elseif ( is_string( $nameOrArray ) ) {
620 if ( $value !==
null ) {
621 $this->lbInfo[$nameOrArray] = $value;
623 unset( $this->lbInfo[$nameOrArray] );
626 throw new InvalidArgumentException(
"Got non-string key" );
653 return $this->lastWriteTime ?:
false;
662 $this->trxDoneWrites ||
663 $this->trxPostCommitOrIdleCallbacks ||
664 $this->trxPreCommitOrIdleCallbacks ||
665 $this->trxEndCallbacks ||
680 $id = $this->
getLBInfo( self::LB_TRX_ROUND_ID );
682 return is_string( $id ) ? $id :
null;
691 } elseif ( !$this->trxDoneWrites ) {
696 case self::ESTIMATE_DB_APPLY:
711 $rttAdjTotal = $this->trxWriteAdjQueryCount * $rtt;
712 $applyTime = max( $this->trxWriteAdjDuration - $rttAdjTotal, 0 );
715 $applyTime += self::$TINY_WRITE_SEC * $omitted;
721 return $this->
trxLevel() ? $this->trxWriteCallers : [];
739 $this->trxPostCommitOrIdleCallbacks,
740 $this->trxPreCommitOrIdleCallbacks,
741 $this->trxEndCallbacks,
742 $this->trxSectionCancelCallbacks
744 foreach ( $callbacks as $callback ) {
745 $fnames[] = $callback[1];
756 return array_reduce( $this->trxAtomicLevels,
function ( $accum, $v ) {
757 return $accum ===
null ? $v[0] :
"$accum, " . $v[0];
765 public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
766 if ( $flag & ~static::$DBO_MUTABLE ) {
769 "Got $flag (allowed: " . implode(
', ', static::$MUTABLE_FLAGS ) .
')'
773 if ( $remember === self::REMEMBER_PRIOR ) {
777 $this->flags |= $flag;
780 public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
781 if ( $flag & ~static::$DBO_MUTABLE ) {
784 "Got $flag (allowed: " . implode(
', ', static::$MUTABLE_FLAGS ) .
')'
788 if ( $remember === self::REMEMBER_PRIOR ) {
792 $this->flags &= ~$flag;
796 if ( !$this->priorFlags ) {
800 if ( $state === self::RESTORE_INITIAL ) {
801 $this->flags = reset( $this->priorFlags );
802 $this->priorFlags = [];
804 $this->flags = array_pop( $this->priorFlags );
809 return ( ( $this->flags & $flag ) === $flag );
813 return $this->currentDomain->getId();
825 abstract public function indexInfo( $table, $index, $fname = __METHOD__ );
840 $this->lastPhpError =
false;
841 $this->htmlErrors = ini_set(
'html_errors',
'0' );
842 set_error_handler( [ $this,
'connectionErrorLogger' ] );
851 restore_error_handler();
852 if ( $this->htmlErrors !==
false ) {
853 ini_set(
'html_errors', $this->htmlErrors );
863 if ( $this->lastPhpError ) {
864 $error = preg_replace(
'!\[<a.*</a>\]!',
'', $this->lastPhpError );
865 $error = preg_replace(
'!^.*?:\s?(.*)$!',
'$1', $error );
881 $this->lastPhpError = $errstr;
893 'db_server' => $this->server,
895 'db_user' => $this->user,
901 final public function close( $fname = __METHOD__, $owner =
null ) {
904 $wasOpen = (bool)$this->conn;
909 if ( $this->trxAtomicLevels ) {
912 $error =
"$fname: atomic sections $levels are still open";
913 } elseif ( $this->trxAutomatic ) {
917 $error =
"$fname: " .
918 "expected mass rollback of all peer transactions (DBO_TRX set)";
923 $error =
"$fname: transaction is still open (from {$this->trxFname})";
926 if ( $this->trxEndCallbacksSuppressed && $error ===
null ) {
927 $error =
"$fname: callbacks are suppressed; cannot properly commit";
931 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
943 if ( $error !==
null ) {
947 if ( $this->ownerId !==
null && $owner === $this->ownerId ) {
948 $this->queryLogger->error( $error );
962 throw new RuntimeException(
963 "Transaction callbacks are still pending: " . implode(
', ', $fnames )
993 list( $reason,
$source ) = $info;
1080 '/^\s*(?:SELECT|BEGIN|ROLLBACK|COMMIT|SAVEPOINT|RELEASE|SET|SHOW|EXPLAIN|USE|\(SELECT)\b/i',
1090 return preg_match(
'/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) :
null;
1110 [
'BEGIN',
'ROLLBACK',
'COMMIT',
'SET',
'SHOW',
'CREATE',
'ALTER',
'USE',
'SHOW' ],
1127 static $regexes =
null;
1128 if ( $regexes ===
null ) {
1130 $qts =
'((?:\w+|`\w+`|\'\w+\'|"\w+")(?:\s*,\s*(?:\w+|`\w+`|\'\w+\'|"\w+"))*)';
1134 "/^(INSERT|REPLACE)\s+(?:\w+\s+)*?INTO\s+$qts/i",
1135 "/^(UPDATE)(?:\s+OR\s+\w+|\s+IGNORE|\s+ONLY)?\s+$qts/i",
1136 "/^(DELETE)\s+(?:\w+\s+)*?FROM(?:\s+ONLY)?\s+$qts/i",
1138 "/^(CREATE)\s+TEMPORARY\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+$qts/i",
1139 "/^(DROP)\s+(?:TEMPORARY\s+)?TABLE(?:\s+IF\s+EXISTS)?\s+$qts/i",
1140 "/^(TRUNCATE)\s+(?:TEMPORARY\s+)?TABLE\s+$qts/i",
1141 "/^(ALTER)\s+TABLE\s+$qts/i"
1147 foreach ( $regexes as $regex ) {
1148 if ( preg_match( $regex, $sql, $m, PREG_UNMATCHED_AS_NULL ) ) {
1150 $allTables = preg_split(
'/\s*,\s*/', $m[2] );
1151 foreach ( $allTables as $quotedTable ) {
1152 $queryTables[] = trim( $quotedTable,
"\"'`" );
1158 $tempTableChanges = [];
1159 foreach ( $queryTables as $table ) {
1160 if ( $queryVerb ===
'CREATE' ) {
1164 $tableType = $this->sessionTempTables[$table] ??
null;
1167 if ( $tableType !==
null ) {
1168 $tempTableChanges[] = [ $tableType, $queryVerb, $table ];
1172 return $tempTableChanges;
1180 if ( $ret ===
false ) {
1184 foreach ( $changes as list( $tmpTableType, $verb, $table ) ) {
1187 $this->sessionTempTables[$table] = $tmpTableType;
1190 unset( $this->sessionTempTables[$table] );
1191 unset( $this->sessionDirtyTempTables[$table] );
1194 unset( $this->sessionDirtyTempTables[$table] );
1197 $this->sessionDirtyTempTables[$table] = 1;
1211 $rawTable = $this->
tableName( $table,
'raw' );
1214 isset( $this->sessionTempTables[$rawTable] ) &&
1215 !isset( $this->sessionDirtyTempTables[$rawTable] )
1219 public function query( $sql, $fname = __METHOD__,
$flags = self::QUERY_NORMAL ) {
1226 list( $ret, $err, $errno, $unignorable ) = $this->
executeQuery( $sql, $fname,
$flags );
1227 if ( $ret ===
false ) {
1230 $this->
reportQueryError( $err, $errno, $sql, $fname, $ignoreErrors && !$unignorable );
1259 $priorTransaction = $this->
trxLevel();
1267 $isPermWrite = !$tempTableChanges;
1268 foreach ( $tempTableChanges as list( $tmpType ) ) {
1274 if ( $isPermWrite ) {
1284 $isPermWrite =
false;
1286 $tempTableChanges = [];
1291 $encAgent = str_replace(
'/',
'-', $this->agent );
1292 $commentedSql = preg_replace(
'/\s|$/',
" /* $fname $encAgent */ ", $sql, 1 );
1296 list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
1301 if ( $ret ===
false && $recoverableCL && $reconnected && $allowRetry ) {
1303 list( $ret, $err, $errno, $recoverableSR, $recoverableCL ) =
1310 $corruptedTrx =
false;
1312 if ( $ret ===
false ) {
1313 if ( $priorTransaction ) {
1314 if ( $recoverableSR ) {
1315 # We're ignoring an error that caused just the current query to be aborted.
1316 # But log the cause so we can log a deprecation notice if a caller actually
1318 $this->trxStatusIgnoredCause = [ $err, $errno, $fname ];
1319 } elseif ( !$recoverableCL ) {
1320 # Either the query was aborted or all queries after BEGIN where aborted.
1321 # In the first case, the only options going forward are (a) ROLLBACK, or
1322 # (b) ROLLBACK TO SAVEPOINT (if one was set). If the later case, the only
1323 # option is ROLLBACK, since the snapshots would have been released.
1324 $corruptedTrx =
true;
1325 $this->
trxStatus = self::STATUS_TRX_ERROR;
1327 $this->trxStatusIgnoredCause =
null;
1332 return [ $ret, $err, $errno, $corruptedTrx ];
1356 if ( (
$flags & self::QUERY_IGNORE_DBO_TRX ) == 0 ) {
1361 if ( $isPermWrite ) {
1362 $this->lastWriteTime = microtime(
true );
1363 if ( $this->
trxLevel() && !$this->trxDoneWrites ) {
1364 $this->trxDoneWrites =
true;
1365 $this->trxProfiler->transactionWritingIn(
1366 $this->server, $this->
getDomainID(), $this->trxShortId );
1370 $prefix = $this->topologyRole ?
'query-m: ' :
'query: ';
1371 $generalizedSql =
new GeneralizedSql( $sql, $this->trxShortId, $prefix );
1373 $startTime = microtime(
true );
1374 $ps = $this->profiler
1377 $this->affectedRowCount =
null;
1379 $ret = $this->
doQuery( $commentedSql );
1385 $queryRuntime = max( microtime(
true ) - $startTime, 0.0 );
1387 $recoverableSR =
false;
1388 $recoverableCL =
false;
1389 $reconnected =
false;
1391 if ( $ret !==
false ) {
1392 $this->lastPing = $startTime;
1393 if ( $isPermWrite && $this->
trxLevel() ) {
1395 $this->trxWriteCallers[] = $fname;
1398 # Check if no meaningful session state was lost
1400 # Update session state tracking and try to restore the connection
1403 # Check if only the last query was rolled back
1407 if ( $sql === self::$PING_QUERY ) {
1408 $this->lastRoundTripEstimate = $queryRuntime;
1411 $this->trxProfiler->recordQueryCompletion(
1420 $this->queryLogger->debug(
1421 "{method} [{runtime}s] {db_host}: {sql}",
1427 'runtime' => round( $queryRuntime, 3 )
1432 return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
1447 $this->
begin( __METHOD__ .
" ($fname)", self::TRANSACTION_INTERNAL );
1448 $this->trxAutomatic =
true;
1466 $indicativeOfReplicaRuntime =
true;
1467 if ( $runtime > self::$SLOW_WRITE_SEC ) {
1470 if ( $verb ===
'INSERT' ) {
1472 } elseif ( $verb ===
'REPLACE' ) {
1473 $indicativeOfReplicaRuntime = $this->
affectedRows() > self::$SMALL_WRITE_ROWS / 2;
1477 $this->trxWriteDuration += $runtime;
1478 $this->trxWriteQueryCount += 1;
1479 $this->trxWriteAffectedRows += $affected;
1480 if ( $indicativeOfReplicaRuntime ) {
1481 $this->trxWriteAdjDuration += $runtime;
1482 $this->trxWriteAdjQueryCount += 1;
1495 if ( $verb ===
'USE' ) {
1496 throw new DBUnexpectedError( $this,
"Got USE query; use selectDomain() instead" );
1499 if ( $verb ===
'ROLLBACK' ) {
1503 if ( $this->
trxStatus < self::STATUS_TRX_OK ) {
1506 "Cannot execute query from $fname while transaction status is ERROR",
1508 $this->trxStatusCause
1510 } elseif ( $this->
trxStatus === self::STATUS_TRX_OK && $this->trxStatusIgnoredCause ) {
1512 call_user_func( $this->deprecationLogger,
1513 "Caller from $fname ignored an error originally raised from $iFname: " .
1514 "[$iLastErrno] $iLastError"
1516 $this->trxStatusIgnoredCause =
null;
1524 "Explicit transaction still active. A caller may have caught an error. "
1540 # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
1541 # Dropped connections also mean that named locks are automatically released.
1542 # Only allow error suppression in autocommit mode or when the lost transaction
1543 # didn't matter anyway (aside from DBO_TRX snapshot loss).
1544 if ( $this->sessionNamedLocks ) {
1546 } elseif ( $this->sessionTempTables ) {
1548 } elseif ( $sql ===
'COMMIT' ) {
1549 return !$priorWritesPending;
1550 } elseif ( $sql ===
'ROLLBACK' ) {
1554 } elseif ( $priorWritesPending ) {
1568 $this->sessionTempTables = [];
1569 $this->sessionDirtyTempTables = [];
1572 $this->sessionNamedLocks = [];
1575 $this->trxAtomicCounter = 0;
1576 $this->trxPostCommitOrIdleCallbacks = [];
1577 $this->trxPreCommitOrIdleCallbacks = [];
1581 if ( $this->trxDoneWrites ) {
1582 $this->trxProfiler->transactionWritingOut(
1587 $this->trxWriteAffectedRows
1608 }
catch ( Throwable $ex ) {
1614 }
catch ( Throwable $ex ) {
1626 $this->trxShortId =
'';
1659 $this->queryLogger->debug(
"SQL ERROR (ignored): $error" );
1675 $this->queryLogger->error(
1676 "Error $errno from $fname, {error} {sql1line} {db_server}",
1678 'method' => __METHOD__,
1681 'sql1line' => mb_substr( str_replace(
"\n",
"\\n", $sql ), 0, 5 * 1024 ),
1683 'exception' =>
new RuntimeException()
1702 return new DBQueryError( $this, $error, $errno, $sql, $fname );
1714 $this->connLogger->error(
1715 "Error connecting to {db_server} as user {db_user}: {error}",
1718 'exception' =>
new RuntimeException()
1740 $table, $var, $cond =
'', $fname = __METHOD__, $options = [], $join_conds = []
1742 if ( $var ===
'*' ) {
1747 $options[
'LIMIT'] = 1;
1749 $res = $this->
select( $table, $var, $cond, $fname, $options, $join_conds );
1750 if (
$res ===
false ) {
1755 if ( $row ===
false ) {
1759 return reset( $row );
1763 $table, $var, $cond =
'', $fname = __METHOD__, $options = [], $join_conds = []
1765 if ( $var ===
'*' ) {
1767 } elseif ( !is_string( $var ) ) {
1772 $res = $this->
select( $table, [
'value' => $var ], $cond, $fname, $options, $join_conds );
1773 if (
$res ===
false ) {
1778 foreach (
$res as $row ) {
1779 $values[] = $row->value;
1797 $preLimitTail = $postLimitTail =
'';
1802 foreach ( $options as $key => $option ) {
1803 if ( is_numeric( $key ) ) {
1804 $noKeyOptions[$option] =
true;
1812 if ( isset( $noKeyOptions[
'FOR UPDATE'] ) ) {
1813 $postLimitTail .=
' FOR UPDATE';
1816 if ( isset( $noKeyOptions[
'LOCK IN SHARE MODE'] ) ) {
1817 $postLimitTail .=
' LOCK IN SHARE MODE';
1820 if ( isset( $noKeyOptions[
'DISTINCT'] ) || isset( $noKeyOptions[
'DISTINCTROW'] ) ) {
1821 $startOpts .=
'DISTINCT';
1824 # Various MySQL extensions
1825 if ( isset( $noKeyOptions[
'STRAIGHT_JOIN'] ) ) {
1826 $startOpts .=
' /*! STRAIGHT_JOIN */';
1829 if ( isset( $noKeyOptions[
'SQL_BIG_RESULT'] ) ) {
1830 $startOpts .=
' SQL_BIG_RESULT';
1833 if ( isset( $noKeyOptions[
'SQL_BUFFER_RESULT'] ) ) {
1834 $startOpts .=
' SQL_BUFFER_RESULT';
1837 if ( isset( $noKeyOptions[
'SQL_SMALL_RESULT'] ) ) {
1838 $startOpts .=
' SQL_SMALL_RESULT';
1841 if ( isset( $noKeyOptions[
'SQL_CALC_FOUND_ROWS'] ) ) {
1842 $startOpts .=
' SQL_CALC_FOUND_ROWS';
1845 if ( isset( $options[
'USE INDEX'] ) && is_string( $options[
'USE INDEX'] ) ) {
1850 if ( isset( $options[
'IGNORE INDEX'] ) && is_string( $options[
'IGNORE INDEX'] ) ) {
1856 return [ $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ];
1869 if ( isset( $options[
'GROUP BY'] ) ) {
1870 $gb = is_array( $options[
'GROUP BY'] )
1871 ? implode(
',', $options[
'GROUP BY'] )
1872 : $options[
'GROUP BY'];
1873 $sql .=
' GROUP BY ' . $gb;
1875 if ( isset( $options[
'HAVING'] ) ) {
1876 $having = is_array( $options[
'HAVING'] )
1878 : $options[
'HAVING'];
1879 $sql .=
' HAVING ' . $having;
1894 if ( isset( $options[
'ORDER BY'] ) ) {
1895 $ob = is_array( $options[
'ORDER BY'] )
1896 ? implode(
',', $options[
'ORDER BY'] )
1897 : $options[
'ORDER BY'];
1899 return ' ORDER BY ' . $ob;
1906 $table, $vars, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
1908 $sql = $this->
selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds );
1910 return $this->
query( $sql, $fname, self::QUERY_CHANGE_NONE );
1918 $options = [], $join_conds = []
1920 if ( is_array( $vars ) ) {
1926 $options = (array)$options;
1927 $useIndexes = ( isset( $options[
'USE INDEX'] ) && is_array( $options[
'USE INDEX'] ) )
1928 ? $options[
'USE INDEX']
1931 isset( $options[
'IGNORE INDEX'] ) &&
1932 is_array( $options[
'IGNORE INDEX'] )
1934 ? $options[
'IGNORE INDEX']
1944 $this->deprecationLogger,
1945 __METHOD__ .
": aggregation used with a locking SELECT ($fname)"
1949 if ( is_array( $table ) ) {
1950 if ( count( $table ) === 0 ) {
1955 $table, $useIndexes, $ignoreIndexes, $join_conds );
1957 } elseif ( $table !=
'' ) {
1960 [ $table ], $useIndexes, $ignoreIndexes, [] );
1965 list( $startOpts, $useIndex, $preLimitTail, $postLimitTail, $ignoreIndex ) =
1968 if ( is_array( $conds ) ) {
1972 if ( $conds ===
null || $conds ===
false ) {
1973 $this->queryLogger->warning(
1977 .
' with incorrect parameters: $conds must be a string or an array'
1982 if ( $conds ===
'' || $conds ===
'*' ) {
1983 $sql =
"SELECT $startOpts $fields $from $useIndex $ignoreIndex $preLimitTail";
1984 } elseif ( is_string( $conds ) ) {
1985 $sql =
"SELECT $startOpts $fields $from $useIndex $ignoreIndex " .
1986 "WHERE $conds $preLimitTail";
1988 throw new DBUnexpectedError( $this, __METHOD__ .
' called with incorrect parameters' );
1991 if ( isset( $options[
'LIMIT'] ) ) {
1992 $sql = $this->
limitResult( $sql, $options[
'LIMIT'],
1993 $options[
'OFFSET'] ??
false );
1995 $sql =
"$sql $postLimitTail";
1997 if ( isset( $options[
'EXPLAIN'] ) ) {
1998 $sql =
'EXPLAIN ' . $sql;
2004 public function selectRow( $table, $vars, $conds, $fname = __METHOD__,
2005 $options = [], $join_conds = []
2007 $options = (array)$options;
2008 $options[
'LIMIT'] = 1;
2010 $res = $this->
select( $table, $vars, $conds, $fname, $options, $join_conds );
2011 if (
$res ===
false ) {
2027 $tables, $var =
'*', $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2031 if ( is_string( $column ) && !in_array( $column, [
'*',
'1' ] ) ) {
2032 $conds[] =
"$column IS NOT NULL";
2036 $tables, [
'rowcount' =>
'COUNT(*)' ], $conds, $fname, $options, $join_conds
2040 return isset( $row[
'rowcount'] ) ? (int)$row[
'rowcount'] : 0;
2044 $tables, $var =
'*', $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2048 if ( is_string( $column ) && !in_array( $column, [
'*',
'1' ] ) ) {
2049 $conds[] =
"$column IS NOT NULL";
2063 [
'rowcount' =>
'COUNT(*)' ],
2069 return isset( $row[
'rowcount'] ) ? (int)$row[
'rowcount'] : 0;
2077 $options = (array)$options;
2078 foreach ( [
'FOR UPDATE',
'LOCK IN SHARE MODE' ] as $lock ) {
2079 if ( in_array( $lock, $options,
true ) ) {
2093 foreach ( (array)$options as $key => $value ) {
2094 if ( is_string( $key ) ) {
2095 if ( preg_match(
'/^(?:GROUP BY|HAVING)$/i', $key ) ) {
2098 } elseif ( is_string( $value ) ) {
2099 if ( preg_match(
'/^(?:DISTINCT|DISTINCTROW)$/i', $value ) ) {
2105 $regex =
'/^(?:COUNT|MIN|MAX|SUM|GROUP_CONCAT|LISTAGG|ARRAY_AGG)\s*\\(/i';
2106 foreach ( (array)$fields as $field ) {
2107 if ( is_string( $field ) && preg_match( $regex, $field ) ) {
2121 if ( !$rowOrRows ) {
2123 } elseif ( isset( $rowOrRows[0] ) ) {
2126 $rows = [ $rowOrRows ];
2129 foreach ( $rows as $row ) {
2130 if ( !is_array( $row ) ) {
2132 } elseif ( !$row ) {
2147 if ( $conds ===
null || $conds ===
false ) {
2148 $this->queryLogger->warning(
2152 .
' with incorrect parameters: $conds must be a string or an array'
2155 } elseif ( $conds ===
'' ) {
2159 return is_array( $conds ) ? $conds : [ $conds ];
2168 if ( is_string( $uniqueKeys ) ) {
2169 return [ [ $uniqueKeys ] ];
2172 if ( !is_array( $uniqueKeys ) || !$uniqueKeys ) {
2177 $uniqueColumnSets = [];
2178 foreach ( $uniqueKeys as $i => $uniqueKey ) {
2179 if ( !is_int( $i ) ) {
2181 } elseif ( is_string( $uniqueKey ) ) {
2183 $uniqueColumnSets[] = [ $uniqueKey ];
2184 } elseif ( is_array( $uniqueKey ) && $uniqueKey ) {
2185 $uniqueColumnSets[] = $uniqueKey;
2191 if ( count( $uniqueColumnSets ) > 1 ) {
2195 $this->queryLogger->warning(
2196 __METHOD__ .
" called with multiple unique keys",
2197 [
'exception' =>
new RuntimeException() ]
2204 $this->queryLogger->warning(
2205 __METHOD__ .
" called with deprecated parameter style: " .
2206 "the unique key array should be a string or array of string arrays",
2207 [
'exception' =>
new RuntimeException() ]
2211 return $uniqueColumnSets;
2220 if ( is_array( $options ) ) {
2222 } elseif ( is_string( $options ) ) {
2223 return ( $options ===
'' ) ? [] : [ $options ];
2225 throw new DBUnexpectedError( $this, __METHOD__ .
': expected string or array' );
2236 foreach ( array_keys( $options, $option,
true ) as $k ) {
2237 if ( is_int( $k ) ) {
2250 if ( is_array( $var ) ) {
2253 } elseif ( count( $var ) == 1 ) {
2254 $column = $var[0] ?? reset( $var );
2266 $table, $conds =
'', $fname = __METHOD__, $options = [], $join_conds = []
2271 __METHOD__ .
': no transaction is active nor is DBO_TRX set'
2275 $options = (array)$options;
2276 $options[] =
'FOR UPDATE';
2278 return $this->
selectRowCount( $table,
'*', $conds, $fname, $options, $join_conds );
2282 $info = $this->
fieldInfo( $table, $field );
2292 $info = $this->
indexInfo( $table, $index, $fname );
2293 if ( $info ===
null ) {
2296 return $info !==
false;
2307 $indexInfo = $this->
indexInfo( $table, $index, $fname );
2309 if ( !$indexInfo ) {
2313 return !$indexInfo[0]->Non_unique;
2316 public function insert( $table, $rows, $fname = __METHOD__, $options = [] ) {
2326 $this->
doInsert( $table, $rows, $fname );
2340 protected function doInsert( $table, array $rows, $fname ) {
2344 $sql =
"INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples";
2346 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2362 $sql = rtrim(
"$sqlVerb $encTable ($sqlColumns) VALUES $sqlTuples $sqlOpts" );
2364 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2373 return [
'INSERT IGNORE INTO',
'' ];
2387 $firstRow = $rows[0];
2388 if ( !is_array( $firstRow ) || !$firstRow ) {
2392 $tupleColumns = array_keys( $firstRow );
2395 foreach ( $rows as $row ) {
2396 $rowColumns = array_keys( $row );
2398 if ( $rowColumns !== $tupleColumns ) {
2401 'Got row columns (' . implode(
', ', $rowColumns ) .
') ' .
2402 'instead of expected (' . implode(
', ', $tupleColumns ) .
')'
2411 implode(
',', $valueTuples )
2427 if ( in_array(
'IGNORE', $options ) ) {
2444 return implode(
' ', $opts );
2447 public function update( $table, $set, $conds, $fname = __METHOD__, $options = [] ) {
2453 if ( $conds && $conds !== IDatabase::ALL_ROWS ) {
2454 if ( is_array( $conds ) ) {
2457 $sql .=
' WHERE ' . $conds;
2460 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
2469 foreach ( $a as $field => $value ) {
2483 $list .=
"($value)";
2490 $includeNull =
false;
2491 foreach ( array_keys( $value,
null,
true ) as $nullKey ) {
2492 $includeNull =
true;
2493 unset( $value[$nullKey] );
2495 if ( count( $value ) == 0 && !$includeNull ) {
2496 throw new InvalidArgumentException(
2497 __METHOD__ .
": empty input for field $field" );
2498 } elseif ( count( $value ) == 0 ) {
2500 $list .=
"$field IS NULL";
2503 if ( $includeNull ) {
2507 if ( count( $value ) == 1 ) {
2511 $value = array_values( $value )[0];
2512 $list .= $field .
" = " . $this->
addQuotes( $value );
2514 $list .= $field .
" IN (" . $this->
makeList( $value ) .
") ";
2517 if ( $includeNull ) {
2518 $list .=
" OR $field IS NULL)";
2521 } elseif ( $value ===
null ) {
2523 $list .=
"$field IS ";
2525 $list .=
"$field = ";
2532 $list .=
"$field = ";
2544 foreach ( $data as
$base => $sub ) {
2545 if ( count( $sub ) ) {
2547 [ $baseKey =>
$base, $subKey => array_map(
'strval', array_keys( $sub ) ) ],
2581 public function bitAnd( $fieldLeft, $fieldRight ) {
2582 return "($fieldLeft & $fieldRight)";
2589 public function bitOr( $fieldLeft, $fieldRight ) {
2590 return "($fieldLeft | $fieldRight)";
2598 return 'CONCAT(' . implode(
',', $stringList ) .
')';
2606 $delim, $table, $field, $conds =
'', $join_conds = []
2608 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->
addQuotes( $delim ) .
')';
2610 return '(' . $this->
selectSQLText( $table, $fld, $conds,
null, [], $join_conds ) .
')';
2646 $fields = is_array( $fields ) ? $fields : [ $fields ];
2647 $values = is_array( $values ) ? $values : [ $values ];
2650 foreach ( $fields as $alias => $field ) {
2651 if ( is_int( $alias ) ) {
2654 $encValues[] = $field;
2657 foreach ( $values as $value ) {
2658 if ( is_int( $value ) || is_float( $value ) ) {
2659 $encValues[] = $value;
2660 } elseif ( is_string( $value ) ) {
2661 $encValues[] = $this->
addQuotes( $value );
2662 } elseif ( $value ===
null ) {
2669 return $sqlfunc .
'(' . implode(
',', $encValues ) .
')';
2678 $functionBody =
"$input FROM $startPosition";
2679 if ( $length !==
null ) {
2680 $functionBody .=
" FOR $length";
2682 return 'SUBSTRING(' . $functionBody .
')';
2698 if ( $startPosition === 0 ) {
2700 throw new InvalidArgumentException(
'Use 1 as $startPosition for the beginning of the string' );
2702 if ( !is_int( $startPosition ) || $startPosition < 0 ) {
2703 throw new InvalidArgumentException(
2704 '$startPosition must be a positive integer'
2707 if ( !( is_int( $length ) && $length >= 0 || $length ===
null ) ) {
2708 throw new InvalidArgumentException(
2709 '$length must be null or an integer greater than or equal to 0'
2728 $isCondValid = ( is_string( $conds ) || is_array( $conds ) ) && $conds;
2729 if ( !$isCondValid ) {
2731 wfDeprecated( $fname .
' called with empty $conds',
'1.35',
false, 3 );
2745 return "CAST( $field AS CHARACTER )";
2753 return 'CAST( ' . $field .
' AS INTEGER )';
2757 $table, $vars, $conds =
'', $fname = __METHOD__,
2758 $options = [], $join_conds = []
2761 $this->
selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2776 $this->currentDomain->getSchema(),
2777 $this->currentDomain->getTablePrefix()
2795 $this->currentDomain = $domain;
2799 return $this->currentDomain->getDatabase();
2814 __METHOD__ .
': got Subquery instance when expecting a string'
2818 # Skip the entire process when we have a string quoted on both ends.
2819 # Note that we check the end so that we will still quote any use of
2820 # use of `database`.table. But won't break things if someone wants
2821 # to query a database table with a dot in the name.
2826 # Lets test for any bits of text that should never show up in a table
2827 # name. Basically anything like JOIN or ON which are actually part of
2828 # SQL queries, but may end up inside of the table value to combine
2829 # sql. Such as how the API is doing.
2830 # Note that we use a whitespace test rather than a \b test to avoid
2831 # any remote case where a word like on may be inside of a table name
2832 # surrounded by symbols which may be considered word breaks.
2833 if ( preg_match(
'/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2834 $this->queryLogger->warning(
2835 __METHOD__ .
": use of subqueries is not supported this way",
2836 [
'exception' =>
new RuntimeException() ]
2842 # Split database and table into proper variables.
2845 # Quote $table and apply the prefix if not quoted.
2846 # $tableName might be empty if this is called from Database::replaceVars()
2847 $tableName =
"{$prefix}{$table}";
2848 if ( $format ===
'quoted'
2850 && $tableName !==
''
2855 # Quote $schema and $database and merge them with the table name if needed
2869 # We reverse the explode so that database.table and table both output the correct table.
2870 $dbDetails = explode(
'.', $name, 3 );
2871 if ( count( $dbDetails ) == 3 ) {
2872 list( $database, $schema, $table ) = $dbDetails;
2873 # We don't want any prefix added in this case
2875 } elseif ( count( $dbDetails ) == 2 ) {
2876 list( $database, $table ) = $dbDetails;
2877 # We don't want any prefix added in this case
2879 # In dbs that support it, $database may actually be the schema
2880 # but that doesn't affect any of the functionality here
2883 list( $table ) = $dbDetails;
2884 if ( isset( $this->tableAliases[$table] ) ) {
2885 $database = $this->tableAliases[$table][
'dbname'];
2886 $schema = is_string( $this->tableAliases[$table][
'schema'] )
2887 ? $this->tableAliases[$table][
'schema']
2889 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
2890 ? $this->tableAliases[$table][
'prefix']
2899 return [ $database, $schema, $prefix, $table ];
2909 if ( strlen( $namespace ) ) {
2913 $relation = $namespace .
'.' . $relation;
2922 foreach ( $tables as $name ) {
2923 $retVal[$name] = $this->
tableName( $name );
2932 foreach ( $tables as $name ) {
2951 if ( is_string( $table ) ) {
2952 $quotedTable = $this->
tableName( $table );
2953 } elseif ( $table instanceof
Subquery ) {
2954 $quotedTable = (string)$table;
2956 throw new InvalidArgumentException(
"Table must be a string or Subquery" );
2959 if ( $alias ===
false || $alias === $table ) {
2960 if ( $table instanceof
Subquery ) {
2961 throw new InvalidArgumentException(
"Subquery table missing alias" );
2964 return $quotedTable;
2980 if ( !$alias || (
string)$alias === (
string)$name ) {
2995 foreach ( $fields as $alias => $field ) {
2996 if ( is_numeric( $alias ) ) {
3023 $use_index = (array)$use_index;
3024 $ignore_index = (array)$ignore_index;
3025 $join_conds = (array)$join_conds;
3027 foreach ( $tables as $alias => $table ) {
3028 if ( !is_string( $alias ) ) {
3033 if ( is_array( $table ) ) {
3035 if ( count( $table ) > 1 ) {
3036 $joinedTable =
'(' .
3038 $table, $use_index, $ignore_index, $join_conds ) .
')';
3041 $innerTable = reset( $table );
3042 $innerAlias = key( $table );
3045 is_string( $innerAlias ) ? $innerAlias : $innerTable
3053 if ( isset( $join_conds[$alias] ) ) {
3054 list( $joinType, $conds ) = $join_conds[$alias];
3055 $tableClause = $joinType;
3056 $tableClause .=
' ' . $joinedTable;
3057 if ( isset( $use_index[$alias] ) ) {
3058 $use = $this->
useIndexClause( implode(
',', (array)$use_index[$alias] ) );
3060 $tableClause .=
' ' . $use;
3063 if ( isset( $ignore_index[$alias] ) ) {
3065 implode(
',', (array)$ignore_index[$alias] ) );
3066 if ( $ignore !=
'' ) {
3067 $tableClause .=
' ' . $ignore;
3072 $tableClause .=
' ON (' . $on .
')';
3075 $retJOIN[] = $tableClause;
3076 } elseif ( isset( $use_index[$alias] ) ) {
3078 $tableClause = $joinedTable;
3080 implode(
',', (array)$use_index[$alias] )
3083 $ret[] = $tableClause;
3084 } elseif ( isset( $ignore_index[$alias] ) ) {
3086 $tableClause = $joinedTable;
3088 implode(
',', (array)$ignore_index[$alias] )
3091 $ret[] = $tableClause;
3093 $tableClause = $joinedTable;
3095 $ret[] = $tableClause;
3100 $implicitJoins = implode(
',', $ret );
3101 $explicitJoins = implode(
' ', $retJOIN );
3104 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
3114 return $this->indexAliases[$index] ?? $index;
3122 if (
$s instanceof
Blob ) {
3125 if (
$s ===
null ) {
3127 } elseif ( is_bool(
$s ) ) {
3128 return (
string)(int)
$s;
3129 } elseif ( is_int(
$s ) ) {
3141 return '"' . str_replace(
'"',
'""',
$s ) .
'"';
3155 return $name[0] ==
'"' && substr( $name, -1, 1 ) ==
'"';
3166 [ $escapeChar,
'%',
'_' ],
3167 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
3177 if ( is_array( $param ) ) {
3180 $params = func_get_args();
3191 foreach ( $params as $value ) {
3193 $s .= $value->toString();
3247 public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__ ) {
3253 if ( $uniqueKeys ) {
3255 $this->
doReplace( $table, $uniqueKeys, $rows, $fname );
3257 $this->queryLogger->warning(
3258 __METHOD__ .
" called with no unique keys",
3259 [
'exception' =>
new RuntimeException() ]
3261 $this->
doInsert( $table, $rows, $fname );
3274 protected function doReplace( $table, array $uniqueKeys, array $rows, $fname ) {
3276 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
3278 foreach ( $rows as $row ) {
3281 $this->
delete( $table, [ $sqlCondition ], $fname );
3284 $this->
insert( $table, $row, $fname );
3288 }
catch ( Throwable $e ) {
3303 } elseif ( !$uniqueKey ) {
3307 if ( count( $uniqueKey ) == 1 ) {
3309 $column = reset( $uniqueKey );
3310 $values = array_column( $rows, $column );
3311 if ( count( $values ) !== count( $rows ) ) {
3312 throw new DBUnexpectedError( $this,
"Missing values for unique key ($column)" );
3319 foreach ( $rows as $row ) {
3320 $rowKeyMap = array_intersect_key( $row, array_flip( $uniqueKey ) );
3321 if ( count( $rowKeyMap ) != count( $uniqueKey ) ) {
3324 "Missing values for unique key (" . implode(
',', $uniqueKey ) .
")"
3330 return count( $disjunctions ) > 1
3342 if ( !$uniqueKeys ) {
3347 foreach ( $uniqueKeys as $uniqueKey ) {
3351 return count( $disjunctions ) > 1
3356 public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__ ) {
3362 if ( $uniqueKeys ) {
3364 $this->
doUpsert( $table, $rows, $uniqueKeys, $set, $fname );
3366 $this->queryLogger->warning(
3367 __METHOD__ .
" called with no unique keys",
3368 [
'exception' =>
new RuntimeException() ]
3370 $this->
doInsert( $table, $rows, $fname );
3386 protected function doUpsert( $table, array $rows, array $uniqueKeys, array $set, $fname ) {
3388 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
3390 foreach ( $rows as $row ) {
3393 $this->
update( $table, $set, [ $sqlConditions ], $fname );
3396 if ( $rowsUpdated <= 0 ) {
3398 $this->
insert( $table, $row, $fname );
3403 }
catch ( Throwable $e ) {
3426 $delTable = $this->
tableName( $delTable );
3427 $joinTable = $this->
tableName( $joinTable );
3428 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
3429 if ( $conds !=
'*' ) {
3434 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3443 $sql =
"SHOW COLUMNS FROM $table LIKE \"$field\"";
3444 $res = $this->
query( $sql, __METHOD__, self::QUERY_CHANGE_NONE );
3449 if ( preg_match(
'/\((.*)\)/', $row->Type, $m ) ) {
3458 public function delete( $table, $conds, $fname = __METHOD__ ) {
3462 $sql =
"DELETE FROM $table";
3464 if ( $conds !== IDatabase::ALL_ROWS ) {
3465 if ( is_array( $conds ) ) {
3468 $sql .=
' WHERE ' . $conds;
3471 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3481 $fname = __METHOD__,
3482 $insertOptions = [],
3483 $selectOptions = [],
3484 $selectJoinConds = []
3486 static $hints = [
'NO_AUTO_COLUMNS' ];
3491 if ( $this->cliMode && $this->
isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
3500 array_diff( $insertOptions, $hints ),
3511 array_diff( $insertOptions, $hints ),
3551 array $insertOptions,
3552 array $selectOptions,
3559 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3564 implode(
',', $fields ),
3567 array_merge( $selectOptions, [
'FOR UPDATE' ] ),
3575 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
3578 foreach (
$res as $row ) {
3579 $rows[] = (array)$row;
3582 $rowBatches = array_chunk( $rows, $this->nonNativeInsertSelectBatchSize );
3583 foreach ( $rowBatches as $rows ) {
3584 $this->
insert( $destTable, $rows, $fname, $insertOptions );
3587 }
catch ( Throwable $e ) {
3616 array $insertOptions,
3617 array $selectOptions,
3620 list( $sqlVerb, $sqlOpts ) = $this->
isFlagInOptions(
'IGNORE', $insertOptions )
3622 : [
'INSERT INTO',
'' ];
3623 $encDstTable = $this->
tableName( $destTable );
3624 $sqlDstColumns = implode(
',', array_keys( $varMap ) );
3627 array_values( $varMap ),
3634 $sql = rtrim(
"$sqlVerb $encDstTable ($sqlDstColumns) $selectSql $sqlOpts" );
3636 $this->
query( $sql, $fname, self::QUERY_CHANGE_ROWS );
3644 if ( !is_numeric( $limit ) ) {
3647 "Invalid non-numeric limit passed to " . __METHOD__
3652 return "$sql LIMIT "
3653 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
3670 $glue = $all ?
') UNION ALL (' :
') UNION (';
3672 return '(' . implode( $glue, $sqls ) .
')';
3678 array $permute_conds,
3680 $fname = __METHOD__,
3686 foreach ( $permute_conds as $field => $values ) {
3691 $values = array_unique( $values );
3693 foreach ( $conds as $cond ) {
3694 foreach ( $values as $value ) {
3695 $cond[$field] = $value;
3696 $newConds[] = $cond;
3702 $extra_conds = $extra_conds ===
'' ? [] : (array)$extra_conds;
3706 if ( count( $conds ) === 1 &&
3710 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3719 $limit = $options[
'LIMIT'] ??
null;
3720 $offset = $options[
'OFFSET'] ??
false;
3721 $all = empty( $options[
'NOTALL'] ) && !in_array(
'NOTALL', $options );
3723 unset( $options[
'ORDER BY'], $options[
'LIMIT'], $options[
'OFFSET'] );
3725 if ( array_key_exists(
'INNER ORDER BY', $options ) ) {
3726 $options[
'ORDER BY'] = $options[
'INNER ORDER BY'];
3728 if ( $limit !==
null && is_numeric( $offset ) && $offset != 0 ) {
3732 $options[
'LIMIT'] = $limit + $offset;
3733 unset( $options[
'OFFSET'] );
3738 foreach ( $conds as $cond ) {
3740 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3744 if ( $limit !==
null ) {
3745 $sql = $this->
limitResult( $sql, $limit, $offset );
3756 if ( is_array( $cond ) ) {
3760 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3768 return "REPLACE({$orig}, {$old}, {$new})";
3846 $function = array_shift(
$args );
3849 $this->
begin( __METHOD__ );
3856 $retVal = $function( ...
$args );
3861 usleep( mt_rand( self::$DEADLOCK_DELAY_MIN, self::$DEADLOCK_DELAY_MAX ) );
3867 }
while ( --$tries > 0 );
3869 if ( $tries <= 0 ) {
3874 $this->
commit( __METHOD__ );
3885 # Real waits are implemented in the subclass.
3925 $this->
begin( __METHOD__, self::TRANSACTION_INTERNAL );
3926 $this->trxAutomatic =
true;
3929 $this->trxPostCommitOrIdleCallbacks[] = [
3947 $this->
begin( __METHOD__, self::TRANSACTION_INTERNAL );
3948 $this->trxAutomatic =
true;
3952 $this->trxPreCommitOrIdleCallbacks[] = [
3959 $this->
startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3963 }
catch ( Throwable $e ) {
3971 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
3981 if ( $this->
trxLevel() && $this->trxAtomicLevels ) {
3982 $levelInfo = end( $this->trxAtomicLevels );
3984 return $levelInfo[1];
4000 foreach ( $this->trxPreCommitOrIdleCallbacks as $key => $info ) {
4001 if ( $info[2] === $old ) {
4002 $this->trxPreCommitOrIdleCallbacks[$key][2] = $new;
4005 foreach ( $this->trxPostCommitOrIdleCallbacks as $key => $info ) {
4006 if ( $info[2] === $old ) {
4007 $this->trxPostCommitOrIdleCallbacks[$key][2] = $new;
4010 foreach ( $this->trxEndCallbacks as $key => $info ) {
4011 if ( $info[2] === $old ) {
4012 $this->trxEndCallbacks[$key][2] = $new;
4015 foreach ( $this->trxSectionCancelCallbacks as $key => $info ) {
4016 if ( $info[2] === $old ) {
4017 $this->trxSectionCancelCallbacks[$key][2] = $new;
4046 $this->trxPostCommitOrIdleCallbacks = array_filter(
4047 $this->trxPostCommitOrIdleCallbacks,
4048 function ( $entry ) use ( $sectionIds ) {
4049 return !in_array( $entry[2], $sectionIds,
true );
4052 $this->trxPreCommitOrIdleCallbacks = array_filter(
4053 $this->trxPreCommitOrIdleCallbacks,
4054 function ( $entry ) use ( $sectionIds ) {
4055 return !in_array( $entry[2], $sectionIds,
true );
4059 foreach ( $this->trxEndCallbacks as $key => $entry ) {
4060 if ( in_array( $entry[2], $sectionIds,
true ) ) {
4061 $callback = $entry[0];
4062 $this->trxEndCallbacks[$key][0] =
function () use ( $callback ) {
4063 return $callback( self::TRIGGER_ROLLBACK, $this );
4066 $this->trxEndCallbacks[$key][2] =
null;
4070 foreach ( $this->trxSectionCancelCallbacks as $key => $entry ) {
4071 if ( in_array( $entry[2], $sectionIds,
true ) ) {
4072 $this->trxSectionCancelCallbacks[$key][2] = $newSectionId;
4079 $this->trxRecurringCallbacks[$name] = $callback;
4081 unset( $this->trxRecurringCallbacks[$name] );
4094 $this->trxEndCallbacksSuppressed = $suppress;
4109 throw new DBUnexpectedError( $this, __METHOD__ .
': a transaction is still open' );
4112 if ( $this->trxEndCallbacksSuppressed ) {
4121 $callbacks = array_merge(
4122 $this->trxPostCommitOrIdleCallbacks,
4123 $this->trxEndCallbacks
4125 $this->trxPostCommitOrIdleCallbacks = [];
4126 $this->trxEndCallbacks = [];
4130 if ( $trigger === self::TRIGGER_ROLLBACK ) {
4131 $callbacks = array_merge( $callbacks, $this->trxSectionCancelCallbacks );
4133 $this->trxSectionCancelCallbacks = [];
4135 foreach ( $callbacks as $callback ) {
4137 list( $phpCallback ) = $callback;
4140 call_user_func( $phpCallback, $trigger, $this );
4141 }
catch ( Throwable $ex ) {
4142 call_user_func( $this->errorLogger, $ex );
4147 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
4158 }
while ( count( $this->trxPostCommitOrIdleCallbacks ) );
4160 if ( $e instanceof Throwable ) {
4182 $this->trxPreCommitOrIdleCallbacks = [];
4183 foreach ( $callbacks as $callback ) {
4186 list( $phpCallback ) = $callback;
4188 $phpCallback( $this );
4189 }
catch ( Throwable $ex ) {
4195 }
while ( count( $this->trxPreCommitOrIdleCallbacks ) );
4197 if ( $e instanceof Throwable ) {
4212 $trigger, array $sectionIds =
null
4220 $this->trxSectionCancelCallbacks = [];
4221 foreach ( $callbacks as $entry ) {
4222 if ( $sectionIds ===
null || in_array( $entry[2], $sectionIds,
true ) ) {
4225 $entry[0]( $trigger, $this );
4226 }
catch ( Throwable $ex ) {
4231 $notCancelled[] = $entry;
4235 }
while ( count( $this->trxSectionCancelCallbacks ) );
4236 $this->trxSectionCancelCallbacks = $notCancelled;
4238 if ( $e !==
null ) {
4253 if ( $this->trxEndCallbacksSuppressed ) {
4260 foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
4262 $phpCallback( $trigger, $this );
4263 }
catch ( Throwable $ex ) {
4269 if ( $e instanceof Throwable ) {
4287 $this->
query( $sql, $fname, self::QUERY_CHANGE_TRX );
4303 $this->
query( $sql, $fname, self::QUERY_CHANGE_TRX );
4319 $this->
query( $sql, $fname, self::QUERY_CHANGE_TRX );
4328 if ( strlen( $savepointId ) > 30 ) {
4333 'There have been an excessively large number of atomic sections in a transaction'
4334 .
" started by $this->trxFname (at $fname)"
4338 return $savepointId;
4342 $fname = __METHOD__,
4343 $cancelable = self::ATOMIC_NOT_CANCELABLE
4348 $this->
begin( $fname, self::TRANSACTION_INTERNAL );
4357 $this->trxAutomaticAtomic =
true;
4359 } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
4365 $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
4366 $this->queryLogger->debug(
'startAtomic: entering level ' .
4367 ( count( $this->trxAtomicLevels ) - 1 ) .
" ($fname)" );
4373 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
4378 $pos = count( $this->trxAtomicLevels ) - 1;
4379 list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
4380 $this->queryLogger->debug(
"endAtomic: leaving level $pos ($fname)" );
4382 if ( $savedFname !== $fname ) {
4385 "Invalid atomic section ended (got $fname but expected $savedFname)"
4390 array_pop( $this->trxAtomicLevels );
4392 if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
4393 $this->
commit( $fname, self::FLUSHING_INTERNAL );
4394 } elseif ( $savepointId !==
null && $savepointId !== self::$NOT_APPLICABLE ) {
4401 if ( $currentSectionId ) {
4407 $fname = __METHOD__,
4410 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
4417 $excisedFnames = [];
4418 if ( $sectionId !==
null ) {
4421 foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
4422 if ( $asId === $sectionId ) {
4430 $len = count( $this->trxAtomicLevels );
4431 for ( $i = $pos + 1; $i < $len; ++$i ) {
4432 $excisedFnames[] = $this->trxAtomicLevels[$i][0];
4433 $excisedIds[] = $this->trxAtomicLevels[$i][1];
4435 $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
4440 $pos = count( $this->trxAtomicLevels ) - 1;
4441 list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
4443 if ( $excisedFnames ) {
4444 $this->queryLogger->debug(
"cancelAtomic: canceling level $pos ($savedFname) " .
4445 "and descendants " . implode(
', ', $excisedFnames ) );
4447 $this->queryLogger->debug(
"cancelAtomic: canceling level $pos ($savedFname)" );
4450 if ( $savedFname !== $fname ) {
4453 "Invalid atomic section ended (got $fname but expected $savedFname)"
4458 array_pop( $this->trxAtomicLevels );
4459 $excisedIds[] = $savedSectionId;
4462 if ( $savepointId !==
null ) {
4464 if ( $savepointId === self::$NOT_APPLICABLE ) {
4465 $this->
rollback( $fname, self::FLUSHING_INTERNAL );
4470 $this->trxStatusIgnoredCause =
null;
4475 } elseif ( $this->
trxStatus > self::STATUS_TRX_ERROR ) {
4477 $this->
trxStatus = self::STATUS_TRX_ERROR;
4480 "Uncancelable atomic section canceled (got $fname)"
4489 $this->affectedRowCount = 0;
4495 $cancelable = self::ATOMIC_NOT_CANCELABLE
4497 $sectionId = $this->
startAtomic( $fname, $cancelable );
4499 $res = $callback( $this, $fname );
4500 }
catch ( Throwable $e ) {
4510 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
4511 static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
4512 if ( !in_array( $mode, $modes,
true ) ) {
4518 if ( $this->trxAtomicLevels ) {
4520 $msg =
"$fname: got explicit BEGIN while atomic section(s) $levels are open";
4522 } elseif ( !$this->trxAutomatic ) {
4523 $msg =
"$fname: explicit transaction already active (from {$this->trxFname})";
4526 $msg =
"$fname: implicit transaction already active (from {$this->trxFname})";
4530 $msg =
"$fname: implicit transaction expected (DBO_TRX set)";
4537 $this->trxShortId = sprintf(
'%06x', mt_rand( 0, 0xffffff ) );
4539 $this->trxStatusIgnoredCause =
null;
4540 $this->trxAtomicCounter = 0;
4542 $this->trxFname = $fname;
4543 $this->trxDoneWrites =
false;
4544 $this->trxAutomaticAtomic =
false;
4545 $this->trxAtomicLevels = [];
4546 $this->trxWriteDuration = 0.0;
4547 $this->trxWriteQueryCount = 0;
4548 $this->trxWriteAffectedRows = 0;
4549 $this->trxWriteAdjDuration = 0.0;
4550 $this->trxWriteAdjQueryCount = 0;
4551 $this->trxWriteCallers = [];
4554 $this->trxReplicaLag =
null;
4559 $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
4571 $this->
query(
'BEGIN', $fname, self::QUERY_CHANGE_TRX );
4574 final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4575 static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
4576 if ( !in_array( $flush, $modes,
true ) ) {
4577 throw new DBUnexpectedError( $this,
"$fname: invalid flush parameter '$flush'" );
4580 if ( $this->
trxLevel() && $this->trxAtomicLevels ) {
4585 "$fname: got COMMIT while atomic sections $levels are still open"
4589 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
4592 } elseif ( !$this->trxAutomatic ) {
4595 "$fname: flushing an explicit transaction, getting out of sync"
4599 $this->queryLogger->error(
4600 "$fname: no transaction to commit, something got out of sync",
4601 [
'exception' =>
new RuntimeException() ]
4605 } elseif ( $this->trxAutomatic ) {
4608 "$fname: expected mass commit of all peer transactions (DBO_TRX set)"
4619 $this->
trxStatus = self::STATUS_TRX_NONE;
4621 if ( $this->trxDoneWrites ) {
4622 $this->lastWriteTime = microtime(
true );
4623 $this->trxProfiler->transactionWritingOut(
4628 $this->trxWriteAffectedRows
4633 if ( $flush !== self::FLUSHING_ALL_PEERS ) {
4649 $this->
query(
'COMMIT', $fname, self::QUERY_CHANGE_TRX );
4653 final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4656 if ( $flush !== self::FLUSHING_INTERNAL
4657 && $flush !== self::FLUSHING_ALL_PEERS
4662 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
4671 $this->
trxStatus = self::STATUS_TRX_NONE;
4672 $this->trxAtomicLevels = [];
4676 if ( $this->trxDoneWrites ) {
4677 $this->trxProfiler->transactionWritingOut(
4682 $this->trxWriteAffectedRows
4689 $this->trxPostCommitOrIdleCallbacks = [];
4690 $this->trxPreCommitOrIdleCallbacks = [];
4693 if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
4696 }
catch ( Throwable $e ) {
4701 }
catch ( Throwable $e ) {
4705 $this->affectedRowCount = 0;
4719 # Disconnects cause rollback anyway, so ignore those errors
4720 $this->
query(
'ROLLBACK', $fname, self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_TRX );
4724 public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4729 "$fname: Cannot flush snapshot; " .
4730 "explicit transaction '{$this->trxFname}' is still open"
4737 "$fname: Cannot flush snapshot; " .
4738 "writes from transaction {$this->trxFname} are still pending ($fnames)"
4743 $flush !== self::FLUSHING_INTERNAL &&
4744 $flush !== self::FLUSHING_ALL_PEERS
4746 $this->queryLogger->warning(
4747 "$fname: Expected mass snapshot flush of all peer transactions " .
4748 "in the explicit transactions round '{$this->getTransactionRoundId()}'",
4749 [
'exception' =>
new RuntimeException() ]
4753 $this->
commit( $fname, self::FLUSHING_INTERNAL );
4770 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4777 public function listTables( $prefix =
null, $fname = __METHOD__ ) {
4778 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4785 public function listViews( $prefix =
null, $fname = __METHOD__ ) {
4786 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4794 $t =
new ConvertibleTimestamp( $ts );
4796 return $t->getTimestamp( TS_MW );
4800 if ( $ts ===
null ) {
4808 return ( $this->affectedRowCount ===
null )
4835 } elseif ( $result ===
true ) {
4842 public function ping( &$rtt =
null ) {
4844 if ( $this->
isOpen() && ( microtime(
true ) - $this->lastPing ) < self::$PING_TTL ) {
4845 if ( !func_num_args() || $this->lastRoundTripEstimate > 0 ) {
4852 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS | self::QUERY_CHANGE_NONE;
4853 $ok = ( $this->
query( self::$PING_QUERY, __METHOD__,
$flags ) !== false );
4878 $this->currentDomain->getDatabase(),
4879 $this->currentDomain->getSchema(),
4880 $this->tablePrefix()
4882 $this->lastPing = microtime(
true );
4885 $this->connLogger->warning(
4886 $fname .
': lost connection to {dbserver}; reconnected',
4889 'exception' =>
new RuntimeException()
4895 $this->connLogger->error(
4896 $fname .
': lost connection to {dbserver} permanently',
4924 return ( $this->
trxLevel() && $this->trxReplicaLag !==
null )
4940 'lag' => ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) ? $this->
getLag() : 0,
4941 'since' => microtime(
true )
4968 $res = [
'lag' => 0,
'since' => INF,
'pending' => false ];
4970 foreach ( func_get_args() as $db ) {
4972 $status = $db->getSessionLagStatus();
4974 if ( $status[
'lag'] ===
false ) {
4975 $res[
'lag'] =
false;
4976 } elseif (
$res[
'lag'] !==
false ) {
4977 $res[
'lag'] = max(
$res[
'lag'], $status[
'lag'] );
4979 $res[
'since'] = min(
$res[
'since'], $status[
'since'] );
4980 $res[
'pending'] =
$res[
'pending'] ?: $db->writesPending();
4988 if ( $this->topologyRole === self::ROLE_STREAMING_MASTER ) {
4990 } elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
5026 if ( $b instanceof
Blob ) {
5041 callable $lineCallback =
null,
5042 callable $resultCallback =
null,
5044 callable $inputCallback =
null
5046 AtEase::suppressWarnings();
5047 $fp = fopen( $filename,
'r' );
5048 AtEase::restoreWarnings();
5050 if ( $fp ===
false ) {
5051 throw new RuntimeException(
"Could not open \"{$filename}\"" );
5055 $fname = __METHOD__ .
"( $filename )";
5060 $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
5061 }
catch ( Throwable $e ) {
5072 $this->schemaVars = is_array( $vars ) ? $vars :
null;
5077 callable $lineCallback =
null,
5078 callable $resultCallback =
null,
5079 $fname = __METHOD__,
5080 callable $inputCallback =
null
5082 $delimiterReset =
new ScopedCallback(
5090 while ( !feof( $fp ) ) {
5091 if ( $lineCallback ) {
5092 call_user_func( $lineCallback );
5095 $line = trim( fgets( $fp ) );
5097 if (
$line ==
'' ) {
5113 if ( $done || feof( $fp ) ) {
5116 if ( $inputCallback ) {
5117 $callbackResult = $inputCallback( $cmd );
5119 if ( is_string( $callbackResult ) || !$callbackResult ) {
5120 $cmd = $callbackResult;
5127 if ( $resultCallback ) {
5128 $resultCallback(
$res, $this );
5131 if (
$res ===
false ) {
5134 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
5141 ScopedCallback::consume( $delimiterReset );
5154 if ( $this->delimiter ) {
5156 $newLine = preg_replace(
5157 '/' . preg_quote( $this->delimiter,
'/' ) .
'$/',
5161 if ( $newLine != $prev ) {
5192 return preg_replace_callback(
5194 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
5195 \'\{\$ (\w+) }\' | # 3. addQuotes
5196 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
5197 /\*\$ (\w+) \*/ # 5. leave unencoded
5199 function ( $m ) use ( $vars ) {
5202 if ( isset( $m[1] ) && $m[1] !==
'' ) {
5203 if ( $m[1] ===
'i' ) {
5208 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3], $vars ) ) {
5209 return $this->
addQuotes( $vars[$m[3]] );
5210 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4], $vars ) ) {
5212 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5], $vars ) ) {
5213 return $vars[$m[5]];
5253 return !isset( $this->sessionNamedLocks[$lockName] );
5260 public function lock( $lockName, $method, $timeout = 5 ) {
5261 $this->sessionNamedLocks[$lockName] = 1;
5270 public function unlock( $lockName, $method ) {
5271 unset( $this->sessionNamedLocks[$lockName] );
5282 "$fname: Cannot flush pre-lock snapshot; " .
5283 "writes from transaction {$this->trxFname} are still pending ($fnames)"
5287 if ( !$this->
lock( $lockKey, $fname, $timeout ) ) {
5291 $unlocker =
new ScopedCallback(
function () use ( $lockKey, $fname ) {
5297 function () use ( $lockKey, $fname ) {
5298 $this->
unlock( $lockKey, $fname );
5303 $this->
unlock( $lockKey, $fname );
5307 $this->
commit( $fname, self::FLUSHING_INTERNAL );
5324 final public function lockTables( array $read, array $write, $method ) {
5326 throw new DBUnexpectedError( $this,
"Transaction writes or callbacks still pending" );
5390 $sql =
"DROP TABLE " . $this->
tableName( $table ) .
" CASCADE";
5391 $this->
query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
5394 public function truncate( $tables, $fname = __METHOD__ ) {
5395 $tables = is_array( $tables ) ? $tables : [ $tables ];
5397 $tablesTruncate = [];
5398 foreach ( $tables as $table ) {
5402 $tablesTruncate[] = $table;
5406 if ( $tablesTruncate ) {
5407 $this->
doTruncate( $tablesTruncate, $fname );
5418 foreach ( $tables as $table ) {
5419 $sql =
"TRUNCATE TABLE " . $this->
tableName( $table );
5420 $this->
query( $sql, $fname, self::QUERY_CHANGE_SCHEMA );
5433 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() )
5439 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() ) {
5443 return ConvertibleTimestamp::convert( $format, $expiry );
5462 if ( $this->topologyRole === self::ROLE_STREAMING_REPLICA ) {
5463 return [
'Server is configured as a read-only replica database.',
'role' ];
5464 } elseif ( $this->topologyRole === self::ROLE_STATIC_CLONE ) {
5465 return [
'Server is configured as a read-only static clone database.',
'role' ];
5468 $reason = $this->
getLBInfo( self::LB_READ_ONLY_REASON );
5469 if ( is_string( $reason ) ) {
5470 return [ $reason,
'lb' ];
5481 $this->tableAliases = $aliases;
5489 $this->indexAliases = $aliases;
5499 return ( (
$flags & $bit ) === $bit );
5515 if ( !$this->conn ) {
5518 'DB connection was already closed or the connection dropped'
5527 $id = function_exists(
'spl_object_id' )
5528 ? spl_object_id( $this )
5529 : spl_object_hash( $this );
5531 $description = $this->
getType() .
' object #' . $id;
5532 if ( is_resource( $this->conn ) ) {
5533 $description .=
' (' . (string)$this->conn .
')';
5534 } elseif ( is_object( $this->conn ) ) {
5536 $handleId = function_exists(
'spl_object_id' )
5537 ? spl_object_id( $this->conn )
5538 : spl_object_hash( $this->conn );
5539 $description .=
" (handle id #$handleId)";
5542 return $description;
5550 $this->connLogger->warning(
5551 "Cloning " . static::class .
" is not recommended; forking connection",
5552 [
'exception' =>
new RuntimeException() ]
5558 $this->trxEndCallbacks = [];
5559 $this->trxSectionCancelCallbacks = [];
5565 $this->currentDomain->getDatabase(),
5566 $this->currentDomain->getSchema(),
5567 $this->tablePrefix()
5569 $this->lastPing = microtime(
true );
5579 throw new RuntimeException(
'Database serialization may cause problems, since ' .
5580 'the connection is not restored on wakeup' );
5587 if ( $this->
trxLevel() && $this->trxDoneWrites ) {
5588 trigger_error(
"Uncommitted DB writes (transaction from {$this->trxFname})" );
5592 if ( $danglingWriters ) {
5593 $fnames = implode(
', ', $danglingWriters );
5594 trigger_error(
"DB transaction writes or callbacks still pending ($fnames)" );
5597 if ( $this->conn ) {
5600 AtEase::suppressWarnings();
5602 AtEase::restoreWarnings();
5611 class_alias( Database::class,
'DatabaseBase' );
5616 class_alias( Database::class,
'Database' );