28use Psr\Log\LoggerAwareInterface;
29use Psr\Log\LoggerInterface;
30use Psr\Log\NullLogger;
31use Wikimedia\ScopedCallback;
32use Wikimedia\Timestamp\ConvertibleTimestamp;
33use Wikimedia\AtEase\AtEase;
37use InvalidArgumentException;
38use 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'];
251 if ( $this->flags & self::DBO_DEFAULT ) {
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 ||
659 if ( $this->
getFlag( self::DBO_TRX ) ) {
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 ) {
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(
1335 if ( $this->
getFlag( self::DBO_DEBUG ) ) {
1336 $this->queryLogger->debug(
1337 "{method} [{runtime}s] {db_host}: {sql}",
1343 'runtime' => round( $queryRuntime, 3 )
1348 return [ $ret, $lastError, $lastErrno, $recoverableSR, $recoverableCL, $reconnected ];
1360 $this->
getFlag( self::DBO_TRX ) &&
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'] )
1774 ? $this->
makeList( $options[
'HAVING'], self::LIST_AND )
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 ) ) {
1858 $conds = $this->
makeList( $conds, self::LIST_AND );
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;
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 = [] ) {
2180 $sql =
"UPDATE $opts $table SET " . $this->
makeList( $values, self::LIST_SET );
2183 if ( $conds !== [] && $conds !==
'*' ) {
2184 $sql .=
" WHERE " . $this->
makeList( $conds, self::LIST_AND );
2187 $this->
query( $sql, $fname );
2192 public function makeList( $a, $mode = self::LIST_COMMA ) {
2193 if ( !is_array( $a ) ) {
2194 throw new DBUnexpectedError( $this, __METHOD__ .
' called with incorrect parameters' );
2200 foreach ( $a as $field => $value ) {
2202 if ( $mode == self::LIST_AND ) {
2204 } elseif ( $mode == self::LIST_OR ) {
2213 if ( ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_numeric( $field ) ) {
2214 $list .=
"($value)";
2215 } elseif ( $mode == self::LIST_SET && is_numeric( $field ) ) {
2218 ( $mode == self::LIST_AND || $mode == self::LIST_OR ) && is_array( $value )
2221 $includeNull =
false;
2222 foreach ( array_keys( $value,
null,
true ) as $nullKey ) {
2223 $includeNull =
true;
2224 unset( $value[$nullKey] );
2226 if ( count( $value ) == 0 && !$includeNull ) {
2227 throw new InvalidArgumentException(
2228 __METHOD__ .
": empty input for field $field" );
2229 } elseif ( count( $value ) == 0 ) {
2231 $list .=
"$field IS NULL";
2234 if ( $includeNull ) {
2238 if ( count( $value ) == 1 ) {
2242 $value = array_values( $value )[0];
2243 $list .= $field .
" = " . $this->
addQuotes( $value );
2245 $list .= $field .
" IN (" . $this->
makeList( $value ) .
") ";
2248 if ( $includeNull ) {
2249 $list .=
" OR $field IS NULL)";
2252 } elseif ( $value ===
null ) {
2253 if ( $mode == self::LIST_AND || $mode == self::LIST_OR ) {
2254 $list .=
"$field IS ";
2255 } elseif ( $mode == self::LIST_SET ) {
2256 $list .=
"$field = ";
2261 $mode == self::LIST_AND || $mode == self::LIST_OR || $mode == self::LIST_SET
2263 $list .=
"$field = ";
2265 $list .= $mode == self::LIST_NAMES ? $value : $this->
addQuotes( $value );
2275 foreach ( $data as
$base => $sub ) {
2276 if ( count( $sub ) ) {
2278 [ $baseKey =>
$base, $subKey => array_keys( $sub ) ],
2284 return $this->
makeList( $conds, self::LIST_OR );
2299 public function bitAnd( $fieldLeft, $fieldRight ) {
2300 return "($fieldLeft & $fieldRight)";
2303 public function bitOr( $fieldLeft, $fieldRight ) {
2304 return "($fieldLeft | $fieldRight)";
2308 return 'CONCAT(' . implode(
',', $stringList ) .
')';
2312 $delim, $table, $field, $conds =
'', $join_conds = []
2314 $fld =
"GROUP_CONCAT($field SEPARATOR " . $this->
addQuotes( $delim ) .
')';
2316 return '(' . $this->
selectSQLText( $table, $fld, $conds,
null, [], $join_conds ) .
')';
2321 $functionBody =
"$input FROM $startPosition";
2322 if ( $length !==
null ) {
2323 $functionBody .=
" FOR $length";
2325 return 'SUBSTRING(' . $functionBody .
')';
2341 if ( !is_int( $startPosition ) || $startPosition <= 0 ) {
2342 throw new InvalidArgumentException(
2343 '$startPosition must be a positive integer'
2346 if ( !( is_int( $length ) && $length >= 0 || $length ===
null ) ) {
2347 throw new InvalidArgumentException(
2348 '$length must be null or an integer greater than or equal to 0'
2356 return "CAST( $field AS CHARACTER )";
2360 return 'CAST( ' . $field .
' AS INTEGER )';
2364 $table, $vars, $conds =
'', $fname = __METHOD__,
2365 $options = [], $join_conds = []
2368 $this->
selectSQLText( $table, $vars, $conds, $fname, $options, $join_conds )
2379 $this->currentDomain->getSchema(),
2380 $this->currentDomain->getTablePrefix()
2397 $this->currentDomain = $domain;
2401 return $this->currentDomain->getDatabase();
2412 __METHOD__ .
': got Subquery instance when expecting a string'
2416 # Skip the entire process when we have a string quoted on both ends.
2417 # Note that we check the end so that we will still quote any use of
2418 # use of `database`.table. But won't break things if someone wants
2419 # to query a database table with a dot in the name.
2424 # Lets test for any bits of text that should never show up in a table
2425 # name. Basically anything like JOIN or ON which are actually part of
2426 # SQL queries, but may end up inside of the table value to combine
2427 # sql. Such as how the API is doing.
2428 # Note that we use a whitespace test rather than a \b test to avoid
2429 # any remote case where a word like on may be inside of a table name
2430 # surrounded by symbols which may be considered word breaks.
2431 if ( preg_match(
'/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
2432 $this->queryLogger->warning(
2433 __METHOD__ .
": use of subqueries is not supported this way",
2434 [
'exception' =>
new RuntimeException() ]
2440 # Split database and table into proper variables.
2443 # Quote $table and apply the prefix if not quoted.
2444 # $tableName might be empty if this is called from Database::replaceVars()
2445 $tableName =
"{$prefix}{$table}";
2446 if ( $format ===
'quoted'
2448 && $tableName !==
''
2453 # Quote $schema and $database and merge them with the table name if needed
2467 # We reverse the explode so that database.table and table both output the correct table.
2468 $dbDetails = explode(
'.', $name, 3 );
2469 if ( count( $dbDetails ) == 3 ) {
2470 list( $database, $schema, $table ) = $dbDetails;
2471 # We don't want any prefix added in this case
2473 } elseif ( count( $dbDetails ) == 2 ) {
2474 list( $database, $table ) = $dbDetails;
2475 # We don't want any prefix added in this case
2477 # In dbs that support it, $database may actually be the schema
2478 # but that doesn't affect any of the functionality here
2481 list( $table ) = $dbDetails;
2482 if ( isset( $this->tableAliases[$table] ) ) {
2483 $database = $this->tableAliases[$table][
'dbname'];
2484 $schema = is_string( $this->tableAliases[$table][
'schema'] )
2485 ? $this->tableAliases[$table][
'schema']
2487 $prefix = is_string( $this->tableAliases[$table][
'prefix'] )
2488 ? $this->tableAliases[$table][
'prefix']
2497 return [ $database, $schema, $prefix, $table ];
2507 if ( strlen( $namespace ) ) {
2511 $relation = $namespace .
'.' . $relation;
2518 $inArray = func_get_args();
2521 foreach ( $inArray as $name ) {
2522 $retVal[$name] = $this->
tableName( $name );
2529 $inArray = func_get_args();
2532 foreach ( $inArray as $name ) {
2551 if ( is_string( $table ) ) {
2552 $quotedTable = $this->
tableName( $table );
2553 } elseif ( $table instanceof
Subquery ) {
2554 $quotedTable = (string)$table;
2556 throw new InvalidArgumentException(
"Table must be a string or Subquery" );
2559 if ( $alias ===
false || $alias === $table ) {
2560 if ( $table instanceof
Subquery ) {
2561 throw new InvalidArgumentException(
"Subquery table missing alias" );
2564 return $quotedTable;
2578 foreach ( $tables as $alias => $table ) {
2579 if ( is_numeric( $alias ) ) {
2597 if ( !$alias || (
string)$alias === (
string)$name ) {
2612 foreach ( $fields as $alias => $field ) {
2613 if ( is_numeric( $alias ) ) {
2633 $tables, $use_index = [], $ignore_index = [], $join_conds = []
2637 $use_index = (array)$use_index;
2638 $ignore_index = (array)$ignore_index;
2639 $join_conds = (array)$join_conds;
2641 foreach ( $tables as $alias => $table ) {
2642 if ( !is_string( $alias ) ) {
2647 if ( is_array( $table ) ) {
2649 if ( count( $table ) > 1 ) {
2650 $joinedTable =
'(' .
2652 $table, $use_index, $ignore_index, $join_conds ) .
')';
2655 $innerTable = reset( $table );
2656 $innerAlias = key( $table );
2659 is_string( $innerAlias ) ? $innerAlias : $innerTable
2667 if ( isset( $join_conds[$alias] ) ) {
2668 list( $joinType, $conds ) = $join_conds[$alias];
2669 $tableClause = $joinType;
2670 $tableClause .=
' ' . $joinedTable;
2671 if ( isset( $use_index[$alias] ) ) {
2672 $use = $this->
useIndexClause( implode(
',', (array)$use_index[$alias] ) );
2674 $tableClause .=
' ' . $use;
2677 if ( isset( $ignore_index[$alias] ) ) {
2679 implode(
',', (array)$ignore_index[$alias] ) );
2680 if ( $ignore !=
'' ) {
2681 $tableClause .=
' ' . $ignore;
2684 $on = $this->
makeList( (array)$conds, self::LIST_AND );
2686 $tableClause .=
' ON (' . $on .
')';
2689 $retJOIN[] = $tableClause;
2690 } elseif ( isset( $use_index[$alias] ) ) {
2692 $tableClause = $joinedTable;
2694 implode(
',', (array)$use_index[$alias] )
2697 $ret[] = $tableClause;
2698 } elseif ( isset( $ignore_index[$alias] ) ) {
2700 $tableClause = $joinedTable;
2702 implode(
',', (array)$ignore_index[$alias] )
2705 $ret[] = $tableClause;
2707 $tableClause = $joinedTable;
2709 $ret[] = $tableClause;
2714 $implicitJoins = implode(
',', $ret );
2715 $explicitJoins = implode(
' ', $retJOIN );
2718 return implode(
' ', [ $implicitJoins, $explicitJoins ] );
2728 return $this->indexAliases[$index] ?? $index;
2732 if (
$s instanceof
Blob ) {
2735 if (
$s ===
null ) {
2737 } elseif ( is_bool(
$s ) ) {
2740 # This will also quote numeric values. This should be harmless,
2741 # and protects against weird problems that occur when they really
2742 # _are_ strings such as article titles and string->number->string
2743 # conversion is not 1:1.
2749 return '"' . str_replace(
'"',
'""',
$s ) .
'"';
2762 return $name[0] ==
'"' && substr( $name, -1, 1 ) ==
'"';
2771 return str_replace( [ $escapeChar,
'%',
'_' ],
2772 [
"{$escapeChar}{$escapeChar}",
"{$escapeChar}%",
"{$escapeChar}_" ],
2777 if ( is_array( $param ) ) {
2780 $params = func_get_args();
2791 foreach ( $params as $value ) {
2793 $s .= $value->toString();
2843 public function replace( $table, $uniqueIndexes, $rows, $fname = __METHOD__ ) {
2844 if ( count( $rows ) == 0 ) {
2848 $uniqueIndexes = (array)$uniqueIndexes;
2850 if ( !is_array( reset( $rows ) ) ) {
2855 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
2857 foreach ( $rows as $row ) {
2859 $indexWhereClauses = [];
2860 foreach ( $uniqueIndexes as $index ) {
2861 $indexColumns = (array)$index;
2862 $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
2863 if ( count( $indexRowValues ) != count( $indexColumns ) ) {
2866 'New record does not provide all values for unique key (' .
2867 implode(
', ', $indexColumns ) .
')'
2869 } elseif ( in_array(
null, $indexRowValues,
true ) ) {
2872 'New record has a null value for unique key (' .
2873 implode(
', ', $indexColumns ) .
')'
2879 if ( $indexWhereClauses ) {
2880 $this->
delete( $table, $this->
makeList( $indexWhereClauses,
LIST_OR ), $fname );
2885 $this->
insert( $table, $row, $fname );
2890 }
catch ( Exception $e ) {
2908 if ( !is_array( reset( $rows ) ) ) {
2912 $sql =
"REPLACE INTO $table (" . implode(
',', array_keys( $rows[0] ) ) .
') VALUES ';
2915 foreach ( $rows as $row ) {
2922 $sql .=
'(' . $this->
makeList( $row ) .
')';
2925 $this->
query( $sql, $fname );
2928 public function upsert( $table, array $rows, $uniqueIndexes, array $set,
2931 if ( $rows === [] ) {
2935 $uniqueIndexes = (array)$uniqueIndexes;
2936 if ( !is_array( reset( $rows ) ) ) {
2940 if ( count( $uniqueIndexes ) ) {
2942 foreach ( $rows as $row ) {
2943 foreach ( $uniqueIndexes as $index ) {
2944 $index = is_array( $index ) ? $index : [ $index ];
2946 foreach ( $index as $column ) {
2947 $rowKey[$column] = $row[$column];
2949 $clauses[] = $this->
makeList( $rowKey, self::LIST_AND );
2952 $where = [ $this->
makeList( $clauses, self::LIST_OR ) ];
2959 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
2960 # Update any existing conflicting row(s)
2961 if ( $where !==
false ) {
2962 $this->
update( $table, $set, $where, $fname );
2965 # Now insert any non-conflicting row(s)
2966 $this->
insert( $table, $rows, $fname, [
'IGNORE' ] );
2970 }
catch ( Exception $e ) {
2978 public function deleteJoin( $delTable, $joinTable, $delVar, $joinVar, $conds,
2985 $delTable = $this->
tableName( $delTable );
2986 $joinTable = $this->
tableName( $joinTable );
2987 $sql =
"DELETE FROM $delTable WHERE $delVar IN (SELECT $joinVar FROM $joinTable ";
2988 if ( $conds !=
'*' ) {
2989 $sql .=
'WHERE ' . $this->
makeList( $conds, self::LIST_AND );
2993 $this->
query( $sql, $fname );
2998 $sql =
"SHOW COLUMNS FROM $table LIKE \"$field\"";
2999 $res = $this->
query( $sql, __METHOD__ );
3004 if ( preg_match(
'/\((.*)\)/', $row->Type, $m ) ) {
3013 public function delete( $table, $conds, $fname = __METHOD__ ) {
3015 throw new DBUnexpectedError( $this, __METHOD__ .
' called with no conditions' );
3019 $sql =
"DELETE FROM $table";
3021 if ( $conds !=
'*' ) {
3022 if ( is_array( $conds ) ) {
3023 $conds = $this->
makeList( $conds, self::LIST_AND );
3025 $sql .=
' WHERE ' . $conds;
3028 $this->
query( $sql, $fname );
3034 $destTable, $srcTable, $varMap, $conds,
3035 $fname = __METHOD__, $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3037 static $hints = [
'NO_AUTO_COLUMNS' ];
3039 $insertOptions = (array)$insertOptions;
3040 $selectOptions = (array)$selectOptions;
3042 if ( $this->cliMode && $this->
isInsertSelectSafe( $insertOptions, $selectOptions ) ) {
3051 array_diff( $insertOptions, $hints ),
3062 array_diff( $insertOptions, $hints ),
3096 $fname = __METHOD__,
3097 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3103 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
3106 $selectOptions[] =
'FOR UPDATE';
3108 $srcTable, implode(
',', $fields ), $conds, $fname, $selectOptions, $selectJoinConds
3116 $this->
startAtomic( $fname, self::ATOMIC_CANCELABLE );
3119 foreach (
$res as $row ) {
3120 $rows[] = (array)$row;
3124 $ok = $this->
insert( $destTable, $rows, $fname, $insertOptions );
3132 if ( $rows && $ok ) {
3133 $ok = $this->
insert( $destTable, $rows, $fname, $insertOptions );
3144 }
catch ( Exception $e ) {
3165 $fname = __METHOD__,
3166 $insertOptions = [], $selectOptions = [], $selectJoinConds = []
3168 $destTable = $this->
tableName( $destTable );
3170 if ( !is_array( $insertOptions ) ) {
3171 $insertOptions = [ $insertOptions ];
3178 array_values( $varMap ),
3185 $sql =
"INSERT $insertOptions" .
3186 " INTO $destTable (" . implode(
',', array_keys( $varMap ) ) .
') ' .
3189 $this->
query( $sql, $fname );
3193 if ( !is_numeric( $limit ) ) {
3196 "Invalid non-numeric limit passed to " . __METHOD__
3201 return "$sql LIMIT "
3202 . ( ( is_numeric( $offset ) && $offset != 0 ) ?
"{$offset}," :
"" )
3211 $glue = $all ?
') UNION ALL (' :
') UNION (';
3213 return '(' . implode( $glue, $sqls ) .
')';
3217 $table, $vars, array $permute_conds, $extra_conds =
'', $fname = __METHOD__,
3218 $options = [], $join_conds = []
3222 foreach ( $permute_conds as $field => $values ) {
3227 $values = array_unique( $values );
3229 foreach ( $conds as $cond ) {
3230 foreach ( $values as $value ) {
3231 $cond[$field] = $value;
3232 $newConds[] = $cond;
3238 $extra_conds = $extra_conds ===
'' ? [] : (array)$extra_conds;
3242 if ( count( $conds ) === 1 &&
3246 $table, $vars, $conds[0] + $extra_conds, $fname, $options, $join_conds
3255 $limit = $options[
'LIMIT'] ??
null;
3256 $offset = $options[
'OFFSET'] ??
false;
3257 $all = empty( $options[
'NOTALL'] ) && !in_array(
'NOTALL', $options );
3259 unset( $options[
'ORDER BY'], $options[
'LIMIT'], $options[
'OFFSET'] );
3261 if ( array_key_exists(
'INNER ORDER BY', $options ) ) {
3262 $options[
'ORDER BY'] = $options[
'INNER ORDER BY'];
3264 if ( $limit !==
null && is_numeric( $offset ) && $offset != 0 ) {
3268 $options[
'LIMIT'] = $limit + $offset;
3269 unset( $options[
'OFFSET'] );
3274 foreach ( $conds as $cond ) {
3276 $table, $vars, $cond + $extra_conds, $fname, $options, $join_conds
3280 if ( $limit !==
null ) {
3281 $sql = $this->
limitResult( $sql, $limit, $offset );
3288 if ( is_array( $cond ) ) {
3289 $cond = $this->
makeList( $cond, self::LIST_AND );
3292 return " (CASE WHEN $cond THEN $trueVal ELSE $falseVal END) ";
3296 return "REPLACE({$orig}, {$old}, {$new})";
3348 $args = func_get_args();
3349 $function = array_shift(
$args );
3352 $this->
begin( __METHOD__ );
3359 $retVal = $function( ...
$args );
3364 usleep( mt_rand( self::$DEADLOCK_DELAY_MIN, self::$DEADLOCK_DELAY_MAX ) );
3370 }
while ( --$tries > 0 );
3372 if ( $tries <= 0 ) {
3377 $this->
commit( __METHOD__ );
3384 # Real waits are implemented in the subclass.
3412 $this->
begin( __METHOD__, self::TRANSACTION_INTERNAL );
3413 $this->trxAutomatic =
true;
3429 $this->
begin( __METHOD__, self::TRANSACTION_INTERNAL );
3430 $this->trxAutomatic =
true;
3437 $this->
startAtomic( __METHOD__, self::ATOMIC_CANCELABLE );
3441 }
catch ( Exception $e ) {
3449 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
3459 if ( $this->
trxLevel() && $this->trxAtomicLevels ) {
3460 $levelInfo = end( $this->trxAtomicLevels );
3462 return $levelInfo[1];
3477 foreach ( $this->trxPreCommitCallbacks as $key => $info ) {
3478 if ( $info[2] === $old ) {
3479 $this->trxPreCommitCallbacks[$key][2] = $new;
3482 foreach ( $this->trxIdleCallbacks as $key => $info ) {
3483 if ( $info[2] === $old ) {
3484 $this->trxIdleCallbacks[$key][2] = $new;
3487 foreach ( $this->trxEndCallbacks as $key => $info ) {
3488 if ( $info[2] === $old ) {
3489 $this->trxEndCallbacks[$key][2] = $new;
3492 foreach ( $this->trxSectionCancelCallbacks as $key => $info ) {
3493 if ( $info[2] === $old ) {
3494 $this->trxSectionCancelCallbacks[$key][2] = $new;
3522 $this->trxIdleCallbacks = array_filter(
3523 $this->trxIdleCallbacks,
3524 function ( $entry ) use ( $sectionIds ) {
3525 return !in_array( $entry[2], $sectionIds,
true );
3528 $this->trxPreCommitCallbacks = array_filter(
3529 $this->trxPreCommitCallbacks,
3530 function ( $entry ) use ( $sectionIds ) {
3531 return !in_array( $entry[2], $sectionIds,
true );
3535 foreach ( $this->trxEndCallbacks as $key => $entry ) {
3536 if ( in_array( $entry[2], $sectionIds,
true ) ) {
3537 $callback = $entry[0];
3538 $this->trxEndCallbacks[$key][0] =
function () use ( $callback ) {
3540 return $callback( self::TRIGGER_ROLLBACK, $this );
3543 $this->trxEndCallbacks[$key][2] =
null;
3547 foreach ( $this->trxSectionCancelCallbacks as $key => $entry ) {
3548 if ( in_array( $entry[2], $sectionIds,
true ) ) {
3549 $this->trxSectionCancelCallbacks[$key][2] = $newSectionId;
3556 $this->trxRecurringCallbacks[$name] = $callback;
3558 unset( $this->trxRecurringCallbacks[$name] );
3571 $this->trxEndCallbacksSuppressed = $suppress;
3586 throw new DBUnexpectedError( $this, __METHOD__ .
': a transaction is still open' );
3589 if ( $this->trxEndCallbacksSuppressed ) {
3594 $autoTrx = $this->
getFlag( self::DBO_TRX );
3598 $callbacks = array_merge(
3599 $this->trxIdleCallbacks,
3600 $this->trxEndCallbacks
3602 $this->trxIdleCallbacks = [];
3603 $this->trxEndCallbacks = [];
3607 if ( $trigger === self::TRIGGER_ROLLBACK ) {
3608 $callbacks = array_merge( $callbacks, $this->trxSectionCancelCallbacks );
3610 $this->trxSectionCancelCallbacks = [];
3612 foreach ( $callbacks as $callback ) {
3614 list( $phpCallback ) = $callback;
3618 call_user_func( $phpCallback, $trigger, $this );
3619 }
catch ( Exception $ex ) {
3620 call_user_func( $this->errorLogger, $ex );
3625 $this->
rollback( __METHOD__, self::FLUSHING_INTERNAL );
3629 $this->
setFlag( self::DBO_TRX );
3635 }
while ( count( $this->trxIdleCallbacks ) );
3637 if ( $e instanceof Exception ) {
3659 $this->trxPreCommitCallbacks = [];
3660 foreach ( $callbacks as $callback ) {
3663 list( $phpCallback ) = $callback;
3665 $phpCallback( $this );
3666 }
catch ( Exception $ex ) {
3671 }
while ( count( $this->trxPreCommitCallbacks ) );
3673 if ( $e instanceof Exception ) {
3688 $trigger, array $sectionIds =
null
3696 $this->trxSectionCancelCallbacks = [];
3697 foreach ( $callbacks as $entry ) {
3698 if ( $sectionIds ===
null || in_array( $entry[2], $sectionIds,
true ) ) {
3701 $entry[0]( $trigger, $this );
3702 }
catch ( Exception $ex ) {
3705 }
catch ( Throwable $ex ) {
3710 $notCancelled[] = $entry;
3713 }
while ( count( $this->trxSectionCancelCallbacks ) );
3714 $this->trxSectionCancelCallbacks = $notCancelled;
3716 if ( $e !==
null ) {
3731 if ( $this->trxEndCallbacksSuppressed ) {
3738 foreach ( $this->trxRecurringCallbacks as $phpCallback ) {
3740 $phpCallback( $trigger, $this );
3741 }
catch ( Exception $ex ) {
3747 if ( $e instanceof Exception ) {
3800 if ( strlen( $savepointId ) > 30 ) {
3805 'There have been an excessively large number of atomic sections in a transaction'
3806 .
" started by $this->trxFname (at $fname)"
3810 return $savepointId;
3814 $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE
3819 $this->
begin( $fname, self::TRANSACTION_INTERNAL );
3822 if ( $this->
getFlag( self::DBO_TRX ) ) {
3828 $this->trxAutomaticAtomic =
true;
3830 } elseif ( $cancelable === self::ATOMIC_CANCELABLE ) {
3836 $this->trxAtomicLevels[] = [ $fname, $sectionId, $savepointId ];
3837 $this->queryLogger->debug(
'startAtomic: entering level ' .
3838 ( count( $this->trxAtomicLevels ) - 1 ) .
" ($fname)" );
3844 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
3849 $pos = count( $this->trxAtomicLevels ) - 1;
3850 list( $savedFname, $sectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3851 $this->queryLogger->debug(
"endAtomic: leaving level $pos ($fname)" );
3853 if ( $savedFname !== $fname ) {
3856 "Invalid atomic section ended (got $fname but expected $savedFname)"
3861 array_pop( $this->trxAtomicLevels );
3863 if ( !$this->trxAtomicLevels && $this->trxAutomaticAtomic ) {
3864 $this->
commit( $fname, self::FLUSHING_INTERNAL );
3865 } elseif ( $savepointId !==
null && $savepointId !== self::$NOT_APPLICABLE ) {
3872 if ( $currentSectionId ) {
3880 if ( !$this->
trxLevel() || !$this->trxAtomicLevels ) {
3887 $excisedFnames = [];
3888 if ( $sectionId !==
null ) {
3891 foreach ( $this->trxAtomicLevels as $i => list( $asFname, $asId, $spId ) ) {
3892 if ( $asId === $sectionId ) {
3900 $len = count( $this->trxAtomicLevels );
3901 for ( $i = $pos + 1; $i < $len; ++$i ) {
3902 $excisedFnames[] = $this->trxAtomicLevels[$i][0];
3903 $excisedIds[] = $this->trxAtomicLevels[$i][1];
3905 $this->trxAtomicLevels = array_slice( $this->trxAtomicLevels, 0, $pos + 1 );
3910 $pos = count( $this->trxAtomicLevels ) - 1;
3911 list( $savedFname, $savedSectionId, $savepointId ) = $this->trxAtomicLevels[$pos];
3913 if ( $excisedFnames ) {
3914 $this->queryLogger->debug(
"cancelAtomic: canceling level $pos ($savedFname) " .
3915 "and descendants " . implode(
', ', $excisedFnames ) );
3917 $this->queryLogger->debug(
"cancelAtomic: canceling level $pos ($savedFname)" );
3920 if ( $savedFname !== $fname ) {
3923 "Invalid atomic section ended (got $fname but expected $savedFname)"
3928 array_pop( $this->trxAtomicLevels );
3929 $excisedIds[] = $savedSectionId;
3932 if ( $savepointId !==
null ) {
3934 if ( $savepointId === self::$NOT_APPLICABLE ) {
3935 $this->
rollback( $fname, self::FLUSHING_INTERNAL );
3940 $this->trxStatusIgnoredCause =
null;
3945 } elseif ( $this->
trxStatus > self::STATUS_TRX_ERROR ) {
3947 $this->
trxStatus = self::STATUS_TRX_ERROR;
3950 "Uncancelable atomic section canceled (got $fname)"
3959 $this->affectedRowCount = 0;
3963 $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
3965 $sectionId = $this->
startAtomic( $fname, $cancelable );
3967 $res = $callback( $this, $fname );
3968 }
catch ( Exception $e ) {
3978 final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
3979 static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
3980 if ( !in_array( $mode, $modes,
true ) ) {
3986 if ( $this->trxAtomicLevels ) {
3988 $msg =
"$fname: got explicit BEGIN while atomic section(s) $levels are open";
3990 } elseif ( !$this->trxAutomatic ) {
3991 $msg =
"$fname: explicit transaction already active (from {$this->trxFname})";
3994 $msg =
"$fname: implicit transaction already active (from {$this->trxFname})";
3997 } elseif ( $this->
getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
3998 $msg =
"$fname: implicit transaction expected (DBO_TRX set)";
4005 $this->trxShortId = sprintf(
'%06x', mt_rand( 0, 0xffffff ) );
4007 $this->trxStatusIgnoredCause =
null;
4008 $this->trxAtomicCounter = 0;
4010 $this->trxFname = $fname;
4011 $this->trxDoneWrites =
false;
4012 $this->trxAutomaticAtomic =
false;
4013 $this->trxAtomicLevels = [];
4014 $this->trxWriteDuration = 0.0;
4015 $this->trxWriteQueryCount = 0;
4016 $this->trxWriteAffectedRows = 0;
4017 $this->trxWriteAdjDuration = 0.0;
4018 $this->trxWriteAdjQueryCount = 0;
4019 $this->trxWriteCallers = [];
4022 $this->trxReplicaLag =
null;
4027 $this->trxAutomatic = ( $mode === self::TRANSACTION_INTERNAL );
4038 $this->
query(
'BEGIN', $fname );
4041 final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4042 static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
4043 if ( !in_array( $flush, $modes,
true ) ) {
4044 throw new DBUnexpectedError( $this,
"$fname: invalid flush parameter '$flush'" );
4047 if ( $this->
trxLevel() && $this->trxAtomicLevels ) {
4052 "$fname: got COMMIT while atomic sections $levels are still open"
4056 if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
4059 } elseif ( !$this->trxAutomatic ) {
4062 "$fname: flushing an explicit transaction, getting out of sync"
4066 $this->queryLogger->error(
4067 "$fname: no transaction to commit, something got out of sync" );
4069 } elseif ( $this->trxAutomatic ) {
4072 "$fname: expected mass commit of all peer transactions (DBO_TRX set)"
4083 $this->
trxStatus = self::STATUS_TRX_NONE;
4085 if ( $this->trxDoneWrites ) {
4086 $this->lastWriteTime = microtime(
true );
4087 $this->trxProfiler->transactionWritingOut(
4092 $this->trxWriteAffectedRows
4097 if ( $flush !== self::FLUSHING_ALL_PEERS ) {
4112 $this->
query(
'COMMIT', $fname );
4116 final public function rollback( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4119 if ( $flush !== self::FLUSHING_INTERNAL
4120 && $flush !== self::FLUSHING_ALL_PEERS
4121 && $this->
getFlag( self::DBO_TRX )
4125 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
4134 $this->
trxStatus = self::STATUS_TRX_NONE;
4135 $this->trxAtomicLevels = [];
4139 if ( $this->trxDoneWrites ) {
4140 $this->trxProfiler->transactionWritingOut(
4145 $this->trxWriteAffectedRows
4152 $this->trxIdleCallbacks = [];
4153 $this->trxPreCommitCallbacks = [];
4156 if ( $trxActive && $flush !== self::FLUSHING_ALL_PEERS ) {
4159 }
catch ( Exception $e ) {
4164 }
catch ( Exception $e ) {
4168 $this->affectedRowCount = 0;
4181 # Disconnects cause rollback anyway, so ignore those errors
4182 $ignoreErrors =
true;
4183 $this->
query(
'ROLLBACK', $fname, $ignoreErrors );
4187 public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
4192 "$fname: Cannot flush snapshot; " .
4193 "explicit transaction '{$this->trxFname}' is still open"
4200 "$fname: Cannot flush snapshot; " .
4201 "writes from transaction {$this->trxFname} are still pending ($fnames)"
4206 $flush !== self::FLUSHING_INTERNAL &&
4207 $flush !== self::FLUSHING_ALL_PEERS
4209 $this->queryLogger->warning(
4210 "$fname: Expected mass snapshot flush of all peer transactions " .
4211 "in the explicit transactions round '{$this->getTransactionRoundId()}'",
4212 [
'exception' =>
new RuntimeException() ]
4216 $this->
commit( $fname, self::FLUSHING_INTERNAL );
4224 $oldName, $newName, $temporary =
false, $fname = __METHOD__
4226 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4229 public function listTables( $prefix =
null, $fname = __METHOD__ ) {
4230 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4233 public function listViews( $prefix =
null, $fname = __METHOD__ ) {
4234 throw new RuntimeException( __METHOD__ .
' is not implemented in descendant class' );
4238 $t =
new ConvertibleTimestamp( $ts );
4240 return $t->getTimestamp( TS_MW );
4244 if ( is_null( $ts ) ) {
4252 return ( $this->affectedRowCount ===
null )
4279 } elseif ( $result ===
true ) {
4286 public function ping( &$rtt =
null ) {
4288 if ( $this->
isOpen() && ( microtime(
true ) - $this->lastPing ) < self::$PING_TTL ) {
4289 if ( !func_num_args() || $this->lastRoundTripEstimate > 0 ) {
4296 $flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_SILENCE_ERRORS;
4297 $ok = ( $this->
query( self::$PING_QUERY, __METHOD__,
$flags ) !== false );
4322 $this->currentDomain->getDatabase(),
4323 $this->currentDomain->getSchema(),
4324 $this->tablePrefix()
4326 $this->lastPing = microtime(
true );
4329 $this->connLogger->warning(
4330 $fname .
': lost connection to {dbserver}; reconnected',
4333 'exception' =>
new RuntimeException()
4339 $this->connLogger->error(
4340 $fname .
': lost connection to {dbserver} permanently',
4368 return ( $this->
trxLevel() && $this->trxReplicaLag !==
null )
4382 'since' => microtime(
true )
4406 $res = [
'lag' => 0,
'since' => INF,
'pending' => false ];
4407 foreach ( func_get_args() as $db ) {
4409 $status = $db->getSessionLagStatus();
4410 if ( $status[
'lag'] ===
false ) {
4411 $res[
'lag'] =
false;
4412 } elseif (
$res[
'lag'] !==
false ) {
4413 $res[
'lag'] = max(
$res[
'lag'], $status[
'lag'] );
4415 $res[
'since'] = min(
$res[
'since'], $status[
'since'] );
4416 $res[
'pending'] =
$res[
'pending'] ?: $db->writesPending();
4425 } elseif ( $this->
getLBInfo(
'is static' ) ) {
4445 if ( $b instanceof
Blob ) {
4456 callable $lineCallback =
null,
4457 callable $resultCallback =
null,
4459 callable $inputCallback =
null
4461 AtEase::suppressWarnings();
4462 $fp = fopen( $filename,
'r' );
4463 AtEase::restoreWarnings();
4465 if ( $fp ===
false ) {
4466 throw new RuntimeException(
"Could not open \"{$filename}\"" );
4470 $fname = __METHOD__ .
"( $filename )";
4475 $fp, $lineCallback, $resultCallback, $fname, $inputCallback );
4476 }
catch ( Exception $e ) {
4487 $this->schemaVars = is_array( $vars ) ? $vars :
null;
4492 callable $lineCallback =
null,
4493 callable $resultCallback =
null,
4494 $fname = __METHOD__,
4495 callable $inputCallback =
null
4497 $delimiterReset =
new ScopedCallback(
4505 while ( !feof( $fp ) ) {
4506 if ( $lineCallback ) {
4507 call_user_func( $lineCallback );
4510 $line = trim( fgets( $fp ) );
4512 if (
$line ==
'' ) {
4528 if ( $done || feof( $fp ) ) {
4531 if ( $inputCallback ) {
4532 $callbackResult = $inputCallback( $cmd );
4534 if ( is_string( $callbackResult ) || !$callbackResult ) {
4535 $cmd = $callbackResult;
4542 if ( $resultCallback ) {
4543 $resultCallback(
$res, $this );
4546 if (
$res ===
false ) {
4549 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
4556 ScopedCallback::consume( $delimiterReset );
4568 if ( $this->delimiter ) {
4570 $newLine = preg_replace(
4571 '/' . preg_quote( $this->delimiter,
'/' ) .
'$/',
'', $newLine );
4572 if ( $newLine != $prev ) {
4602 return preg_replace_callback(
4604 /\* (\$wgDBprefix|[_i]) \*/ (\w*) | # 1-2. tableName, indexName
4605 \'\{\$ (\w+) }\' | # 3. addQuotes
4606 `\{\$ (\w+) }` | # 4. addIdentifierQuotes
4607 /\*\$ (\w+) \*/ # 5. leave unencoded
4609 function ( $m ) use ( $vars ) {
4612 if ( isset( $m[1] ) && $m[1] !==
'' ) {
4613 if ( $m[1] ===
'i' ) {
4618 } elseif ( isset( $m[3] ) && $m[3] !==
'' && array_key_exists( $m[3], $vars ) ) {
4619 return $this->
addQuotes( $vars[$m[3]] );
4620 } elseif ( isset( $m[4] ) && $m[4] !==
'' && array_key_exists( $m[4], $vars ) ) {
4622 } elseif ( isset( $m[5] ) && $m[5] !==
'' && array_key_exists( $m[5], $vars ) ) {
4623 return $vars[$m[5]];
4658 return !isset( $this->sessionNamedLocks[$lockName] );
4661 public function lock( $lockName, $method, $timeout = 5 ) {
4662 $this->sessionNamedLocks[$lockName] = 1;
4667 public function unlock( $lockName, $method ) {
4668 unset( $this->sessionNamedLocks[$lockName] );
4679 "$fname: Cannot flush pre-lock snapshot; " .
4680 "writes from transaction {$this->trxFname} are still pending ($fnames)"
4684 if ( !$this->
lock( $lockKey, $fname, $timeout ) ) {
4688 $unlocker =
new ScopedCallback(
function () use ( $lockKey, $fname ) {
4694 function () use ( $lockKey, $fname ) {
4695 $this->
unlock( $lockKey, $fname );
4700 $this->
unlock( $lockKey, $fname );
4704 $this->
commit( $fname, self::FLUSHING_INTERNAL );
4717 final public function lockTables( array $read, array $write, $method ) {
4719 throw new DBUnexpectedError( $this,
"Transaction writes or callbacks still pending" );
4768 public function dropTable( $tableName, $fName = __METHOD__ ) {
4769 if ( !$this->
tableExists( $tableName, $fName ) ) {
4772 $sql =
"DROP TABLE " . $this->
tableName( $tableName ) .
" CASCADE";
4774 return $this->
query( $sql, $fName );
4782 return ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() )
4788 if ( $expiry ==
'' || $expiry ==
'infinity' || $expiry == $this->
getInfinity() ) {
4792 return ConvertibleTimestamp::convert( $format, $expiry );
4807 $reason = $this->
getLBInfo(
'readOnlyReason' );
4808 if ( is_string( $reason ) ) {
4810 } elseif ( $this->
getLBInfo(
'replica' ) ) {
4811 return "Server is configured in the role of a read-only replica database.";
4818 $this->tableAliases = $aliases;
4822 $this->indexAliases = $aliases;
4847 if ( !$this->conn ) {
4850 'DB connection was already closed or the connection dropped'
4859 $id = function_exists(
'spl_object_id' )
4860 ? spl_object_id( $this )
4861 : spl_object_hash( $this );
4863 $description = $this->
getType() .
' object #' . $id;
4864 if ( is_resource( $this->conn ) ) {
4865 $description .=
' (' . (string)$this->conn .
')';
4866 } elseif ( is_object( $this->conn ) ) {
4868 $handleId = function_exists(
'spl_object_id' )
4869 ? spl_object_id( $this->conn )
4870 : spl_object_hash( $this->conn );
4871 $description .=
" (handle id #$handleId)";
4874 return $description;
4882 $this->connLogger->warning(
4883 "Cloning " . static::class .
" is not recommended; forking connection",
4884 [
'exception' =>
new RuntimeException() ]
4890 $this->trxEndCallbacks = [];
4891 $this->trxSectionCancelCallbacks = [];
4897 $this->currentDomain->getDatabase(),
4898 $this->currentDomain->getSchema(),
4899 $this->tablePrefix()
4901 $this->lastPing = microtime(
true );
4911 throw new RuntimeException(
'Database serialization may cause problems, since ' .
4912 'the connection is not restored on wakeup' );
4919 if ( $this->
trxLevel() && $this->trxDoneWrites ) {
4920 trigger_error(
"Uncommitted DB writes (transaction from {$this->trxFname})" );
4924 if ( $danglingWriters ) {
4925 $fnames = implode(
', ', $danglingWriters );
4926 trigger_error(
"DB transaction writes or callbacks still pending ($fnames)" );
4929 if ( $this->conn ) {
4932 AtEase::suppressWarnings();
4934 AtEase::restoreWarnings();
4943class_alias( Database::class,
'DatabaseBase' );
4948class_alias( Database::class,
'Database' );
Class representing a cache/ephemeral data store.
Simple store for keeping values in an associative array for the current process.
Class to handle database/schema/prefix specifications for IDatabase.
static newFromId( $domain)
Advanced database interface for IDatabase handles that include maintenance methods.
fieldInfo( $table, $field)
mysql_fetch_field() wrapper Returns false if the field doesn't exist