3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
20 namespace Wikimedia\Rdbms
;
22 use InvalidArgumentException
;
24 use Psr\Log\LoggerAwareInterface
;
25 use Psr\Log\LoggerInterface
;
26 use Psr\Log\NullLogger
;
30 use Wikimedia\AtEase\AtEase
;
31 use Wikimedia\Rdbms\Database\DatabaseFlags
;
32 use Wikimedia\Rdbms\Platform\SQLPlatform
;
33 use Wikimedia\Rdbms\Replication\ReplicationReporter
;
34 use Wikimedia\RequestTimeout\CriticalSectionProvider
;
35 use Wikimedia\RequestTimeout\CriticalSectionScope
;
36 use Wikimedia\ScopedCallback
;
37 use Wikimedia\Telemetry\NoopTracer
;
38 use Wikimedia\Telemetry\SpanInterface
;
39 use Wikimedia\Telemetry\TracerInterface
;
42 * A single concrete connection to a relational database.
44 * This is the base class for all connection-specific relational database handles.
45 * No two instances of this class should share the same underlying network connection.
51 abstract class Database
implements Stringable
, IDatabaseForOwner
, IMaintainableDatabase
, LoggerAwareInterface
{
52 /** @var CriticalSectionProvider|null */
53 protected $csProvider;
54 /** @var LoggerInterface */
56 /** @var callable Error logging callback */
57 protected $errorLogger;
58 /** @var callable Deprecation logging callback */
59 protected $deprecationLogger;
60 /** @var callable|null */
62 /** @var TracerInterface */
64 /** @var TransactionManager */
65 private $transactionManager;
67 /** @var DatabaseDomain */
68 protected $currentDomain;
69 /** @var DatabaseFlags */
70 protected $flagsHolder;
72 // phpcs:ignore MediaWiki.Commenting.PropertyDocumentation.ObjectTypeHintVar
73 /** @var object|resource|null Database connection */
76 /** @var string|null Readable name or host/IP of the database server */
77 protected $serverName;
78 /** @var bool Whether this PHP instance is for a CLI script */
80 /** @var int|null Maximum seconds to wait on connection attempts */
81 protected $connectTimeout;
82 /** @var int|null Maximum seconds to wait on receiving query results */
83 protected $receiveTimeout;
84 /** @var string Agent name for query profiling */
86 /** @var array<string,mixed> Connection parameters used by initConnection() and open() */
87 protected $connectionParams;
88 /** @var string[]|int[]|float[] SQL variables values to use for all new connections */
89 protected $connectionVariables;
90 /** @var int Row batch size to use for emulated INSERT SELECT queries */
91 protected $nonNativeInsertSelectBatchSize;
93 /** @var bool Whether to use SSL connections */
95 /** @var bool Whether to check for warnings */
96 protected $strictWarnings;
97 /** @var array Current LoadBalancer tracking information */
98 protected $lbInfo = [];
99 /** @var string|false Current SQL query delimiter */
100 protected $delimiter = ';';
102 /** @var string|bool|null Stashed value of html_errors INI setting */
105 /** @var array<string,array> Map of (lock name => (UNIX time,trx ID)) */
106 protected $sessionNamedLocks = [];
107 /** @var array<string,array<string, TempTableInfo>> Map of (DB name => table name => info) */
108 protected $sessionTempTables = [];
110 /** @var int Affected row count for the last statement to query() */
111 protected $lastQueryAffectedRows = 0;
112 /** @var int|null Insert (row) ID for the last statement to query() (null if not supported) */
113 protected $lastQueryInsertId;
115 /** @var int|null Affected row count for the last query method call; null if unspecified */
116 protected $lastEmulatedAffectedRows;
117 /** @var int|null Insert (row) ID for the last query method call; null if unspecified */
118 protected $lastEmulatedInsertId;
120 /** @var string Last error during connection; empty string if none */
121 protected $lastConnectError = '';
123 /** @var float UNIX timestamp of the last server response */
124 private $lastPing = 0.0;
125 /** @var float|null UNIX timestamp of the last committed write */
126 private $lastWriteTime;
127 /** @var string|false The last PHP error from a query or connection attempt */
128 private $lastPhpError = false;
130 /** @var int|null Current critical section numeric ID */
132 /** @var string|null Last critical section caller name */
134 /** @var DBUnexpectedError|null Last unresolved critical section error */
137 /** Whether the database is a file on disk */
138 public const ATTR_DB_IS_FILE
= 'db-is-file';
139 /** Lock granularity is on the level of the entire database */
140 public const ATTR_DB_LEVEL_LOCKING
= 'db-level-locking';
141 /** The SCHEMA keyword refers to a grouping of tables in a database */
142 public const ATTR_SCHEMAS_AS_TABLE_GROUPS
= 'supports-schemas';
144 /** New Database instance will not be connected yet when returned */
145 public const NEW_UNCONNECTED
= 0;
146 /** New Database instance will already be connected when returned */
147 public const NEW_CONNECTED
= 1;
149 /** No errors occurred during the query */
150 protected const ERR_NONE
= 0;
151 /** Retry query due to a connection loss detected while sending the query (session intact) */
152 protected const ERR_RETRY_QUERY
= 1;
153 /** Abort query (no retries) due to a statement rollback (session/transaction intact) */
154 protected const ERR_ABORT_QUERY
= 2;
155 /** Abort any current transaction, by rolling it back, due to an error during the query */
156 protected const ERR_ABORT_TRX
= 4;
157 /** Abort and reset session due to server-side session-level state loss (locks, temp tables) */
158 protected const ERR_ABORT_SESSION
= 8;
160 /** Assume that queries taking this long to yield connection loss errors are at fault */
161 protected const DROPPED_CONN_BLAME_THRESHOLD_SEC
= 3.0;
163 /** @var string Idiom used when a cancelable atomic section started the transaction */
164 private const NOT_APPLICABLE
= 'n/a';
166 /** How long before it is worth doing a dummy query to test the connection */
167 private const PING_TTL
= 1.0;
168 /** Dummy SQL query */
169 private const PING_QUERY
= 'SELECT 1 AS ping';
171 /** Hostname or IP address to use on all connections */
172 protected const CONN_HOST
= 'host';
173 /** Database server username to use on all connections */
174 protected const CONN_USER
= 'user';
175 /** Database server password to use on all connections */
176 protected const CONN_PASSWORD
= 'password';
177 /** Database name to use on initial connection */
178 protected const CONN_INITIAL_DB
= 'dbname';
179 /** Schema name to use on initial connection */
180 protected const CONN_INITIAL_SCHEMA
= 'schema';
181 /** Table prefix to use on initial connection */
182 protected const CONN_INITIAL_TABLE_PREFIX
= 'tablePrefix';
184 /** @var SQLPlatform */
187 /** @var ReplicationReporter */
188 protected $replicationReporter;
191 * @note exceptions for missing libraries/drivers should be thrown in initConnection()
192 * @param array $params Parameters passed from Database::factory()
194 public function __construct( array $params ) {
195 $this->logger
= $params['logger'] ??
new NullLogger();
196 $this->transactionManager
= new TransactionManager(
198 $params['trxProfiler']
200 $this->connectionParams
= [
201 self
::CONN_HOST
=> ( isset( $params['host'] ) && $params['host'] !== '' )
204 self
::CONN_USER
=> ( isset( $params['user'] ) && $params['user'] !== '' )
207 self
::CONN_INITIAL_DB
=> ( isset( $params['dbname'] ) && $params['dbname'] !== '' )
210 self
::CONN_INITIAL_SCHEMA
=> ( isset( $params['schema'] ) && $params['schema'] !== '' )
213 self
::CONN_PASSWORD
=> is_string( $params['password'] ) ?
$params['password'] : null,
214 self
::CONN_INITIAL_TABLE_PREFIX
=> (string)$params['tablePrefix']
217 $this->lbInfo
= $params['lbInfo'] ??
[];
218 $this->connectionVariables
= $params['variables'] ??
[];
219 // Set SQL mode, default is turning them all off, can be overridden or skipped with null
220 if ( is_string( $params['sqlMode'] ??
null ) ) {
221 $this->connectionVariables
['sql_mode'] = $params['sqlMode'];
223 $flags = (int)$params['flags'];
224 $this->flagsHolder
= new DatabaseFlags( $flags );
225 $this->ssl
= $params['ssl'] ??
(bool)( $flags & self
::DBO_SSL
);
226 $this->connectTimeout
= $params['connectTimeout'] ??
null;
227 $this->receiveTimeout
= $params['receiveTimeout'] ??
null;
228 $this->cliMode
= (bool)$params['cliMode'];
229 $this->agent
= (string)$params['agent'];
230 $this->serverName
= $params['serverName'];
231 $this->nonNativeInsertSelectBatchSize
= $params['nonNativeInsertSelectBatchSize'] ??
10000;
232 $this->strictWarnings
= !empty( $params['strictWarnings'] );
234 $this->profiler
= is_callable( $params['profiler'] ) ?
$params['profiler'] : null;
235 $this->errorLogger
= $params['errorLogger'];
236 $this->deprecationLogger
= $params['deprecationLogger'];
238 $this->csProvider
= $params['criticalSectionProvider'] ??
null;
240 // Set initial dummy domain until open() sets the final DB/prefix
241 $this->currentDomain
= new DatabaseDomain(
242 $params['dbname'] != '' ?
$params['dbname'] : null,
243 $params['schema'] != '' ?
$params['schema'] : null,
244 $params['tablePrefix']
246 $this->platform
= new SQLPlatform(
249 $this->currentDomain
,
252 $this->tracer
= $params['tracer'] ??
new NoopTracer();
253 // Children classes must set $this->replicationReporter.
257 * Initialize the connection to the database over the wire (or to local files)
259 * @throws LogicException
260 * @throws InvalidArgumentException
261 * @throws DBConnectionError
264 final public function initConnection() {
265 if ( $this->isOpen() ) {
266 throw new LogicException( __METHOD__
. ': already connected' );
268 // Establish the connection
270 $this->connectionParams
[self
::CONN_HOST
],
271 $this->connectionParams
[self
::CONN_USER
],
272 $this->connectionParams
[self
::CONN_PASSWORD
],
273 $this->connectionParams
[self
::CONN_INITIAL_DB
],
274 $this->connectionParams
[self
::CONN_INITIAL_SCHEMA
],
275 $this->connectionParams
[self
::CONN_INITIAL_TABLE_PREFIX
]
277 $this->lastPing
= microtime( true );
281 * Open a new connection to the database (closing any existing one)
283 * @param string|null $server Server host/address and optional port {@see connectionParams}
284 * @param string|null $user User name {@see connectionParams}
285 * @param string|null $password User password {@see connectionParams}
286 * @param string|null $db Database name
287 * @param string|null $schema Database schema name
288 * @param string $tablePrefix
289 * @throws DBConnectionError
291 abstract protected function open( $server, $user, $password, $db, $schema, $tablePrefix );
294 * @return array Map of (Database::ATTR_* constant => value)
297 public static function getAttributes() {
302 * Set the PSR-3 logger interface to use.
304 * @param LoggerInterface $logger
306 public function setLogger( LoggerInterface
$logger ) {
307 $this->logger
= $logger;
310 public function getServerInfo() {
311 return $this->getServerVersion();
314 public function tablePrefix( $prefix = null ) {
315 $old = $this->currentDomain
->getTablePrefix();
317 if ( $prefix !== null ) {
318 $this->currentDomain
= new DatabaseDomain(
319 $this->currentDomain
->getDatabase(),
320 $this->currentDomain
->getSchema(),
323 $this->platform
->setCurrentDomain( $this->currentDomain
);
329 public function dbSchema( $schema = null ) {
330 $old = $this->currentDomain
->getSchema();
332 if ( $schema !== null ) {
333 if ( $schema !== '' && $this->getDBname() === null ) {
334 throw new DBUnexpectedError(
336 "Cannot set schema to '$schema'; no database set"
340 $this->currentDomain
= new DatabaseDomain(
341 $this->currentDomain
->getDatabase(),
342 // DatabaseDomain uses null for unspecified schemas
343 ( $schema !== '' ) ?
$schema : null,
344 $this->currentDomain
->getTablePrefix()
346 $this->platform
->setCurrentDomain( $this->currentDomain
);
352 public function getLBInfo( $name = null ) {
353 if ( $name === null ) {
354 return $this->lbInfo
;
357 if ( array_key_exists( $name, $this->lbInfo
) ) {
358 return $this->lbInfo
[$name];
364 public function setLBInfo( $nameOrArray, $value = null ) {
365 if ( is_array( $nameOrArray ) ) {
366 $this->lbInfo
= $nameOrArray;
367 } elseif ( is_string( $nameOrArray ) ) {
368 if ( $value !== null ) {
369 $this->lbInfo
[$nameOrArray] = $value;
371 unset( $this->lbInfo
[$nameOrArray] );
374 throw new InvalidArgumentException( "Got non-string key" );
378 public function lastDoneWrites() {
379 return $this->lastWriteTime
;
385 * @internal For use by Database/LoadBalancer only
387 public function sessionLocksPending() {
388 return (bool)$this->sessionNamedLocks
;
392 * @return ?string Owner name of explicit transaction round being participating in; null if none
394 final protected function getTransactionRoundFname() {
395 if ( $this->flagsHolder
->hasImplicitTrxFlag() ) {
396 // LoadBalancer transaction round participation is enabled for this DB handle;
397 // get the owner of the active explicit transaction round (if any)
398 return $this->getLBInfo( self
::LB_TRX_ROUND_FNAME
);
404 public function isOpen() {
405 return (bool)$this->conn
;
408 public function getDomainID() {
409 return $this->currentDomain
->getId();
413 * Wrapper for addslashes()
415 * @param string $s String to be slashed.
416 * @return string Slashed string.
418 abstract public function strencode( $s );
421 * Set a custom error handler for logging errors during database connection
423 protected function installErrorHandler() {
424 $this->lastPhpError
= false;
425 $this->htmlErrors
= ini_set( 'html_errors', '0' );
426 set_error_handler( [ $this, 'connectionErrorLogger' ] );
430 * Restore the previous error handler and return the last PHP error for this DB
432 * @return string|false
434 protected function restoreErrorHandler() {
435 restore_error_handler();
436 if ( $this->htmlErrors
!== false ) {
437 ini_set( 'html_errors', $this->htmlErrors
);
440 return $this->getLastPHPError();
444 * @return string|false Last PHP error for this DB (typically connection errors)
446 protected function getLastPHPError() {
447 if ( $this->lastPhpError
) {
448 $error = preg_replace( '!\[<a.*</a>\]!', '', $this->lastPhpError
);
449 $error = preg_replace( '!^.*?:\s?(.*)$!', '$1', $error );
458 * Error handler for logging errors during database connection
460 * @internal This method should not be used outside of Database classes
462 * @param int|string $errno
463 * @param string $errstr
465 public function connectionErrorLogger( $errno, $errstr ) {
466 $this->lastPhpError
= $errstr;
470 * Create a log context to pass to PSR-3 logger functions.
472 * @param array $extras Additional data to add to context
475 protected function getLogContext( array $extras = [] ) {
478 'db_server' => $this->getServerName(),
479 'db_name' => $this->getDBname(),
480 'db_user' => $this->connectionParams
[self
::CONN_USER
] ??
null,
486 final public function close( $fname = __METHOD__
) {
487 $error = null; // error to throw after disconnecting
489 $wasOpen = (bool)$this->conn
;
490 // This should mostly do nothing if the connection is already closed
492 // Roll back any dangling transaction first
493 if ( $this->trxLevel() ) {
494 $error = $this->transactionManager
->trxCheckBeforeClose( $this, $fname );
495 // Rollback the changes and run any callbacks as needed
496 $this->rollback( __METHOD__
, self
::FLUSHING_INTERNAL
);
497 $this->runTransactionPostRollbackCallbacks();
500 // Close the actual connection in the binding handle
501 $closed = $this->closeConnection();
503 $closed = true; // already closed; nothing to do
508 // Log any unexpected errors after having disconnected
509 if ( $error !== null ) {
510 // T217819, T231443: this is probably just LoadBalancer trying to recover from
511 // errors and shutdown. Log any problems and move on since the request has to
512 // end one way or another. Throwing errors is not very useful at some point.
513 $this->logger
->error( $error, [ 'db_log_category' => 'query' ] );
516 // Note that various subclasses call close() at the start of open(), which itself is
517 // called by replaceLostConnection(). In that case, just because onTransactionResolution()
518 // callbacks are pending does not mean that an exception should be thrown. Rather, they
519 // will be executed after the reconnection step.
521 // Double check that no callbacks are dangling
522 $fnames = $this->pendingWriteAndCallbackCallers();
524 throw new RuntimeException(
525 "Transaction callbacks are still pending: " . implode( ', ', $fnames )
534 * Make sure there is an open connection handle (alive or not)
536 * This guards against fatal errors to the binding handle not being defined in cases
537 * where open() was never called or close() was already called.
539 * @throws DBUnexpectedError
541 final protected function assertHasConnectionHandle() {
542 if ( !$this->isOpen() ) {
543 throw new DBUnexpectedError( $this, "DB connection was already closed" );
548 * Closes underlying database connection
549 * @return bool Whether connection was closed successfully
552 abstract protected function closeConnection();
555 * Run a query and return a QueryStatus instance with the query result information
557 * This is meant to handle the basic command of actually sending a query to the
558 * server via the driver. No implicit transaction, reconnection, nor retry logic
559 * should happen here. The higher level query() method is designed to handle those
560 * sorts of concerns. This method should not trigger such higher level methods.
562 * The lastError() and lastErrno() methods should meaningfully reflect what error,
563 * if any, occurred during the last call to this method. Methods like executeQuery(),
564 * query(), select(), insert(), update(), delete(), and upsert() implement their calls
565 * to doQuery() such that an immediately subsequent call to lastError()/lastErrno()
566 * meaningfully reflects any error that occurred during that public query method call.
568 * For SELECT queries, the result field contains either:
569 * - a) A driver-specific IResultWrapper describing the query results
570 * - b) False, on any query failure
572 * For non-SELECT queries, the result field contains either:
573 * - a) A driver-specific IResultWrapper, only on success
574 * - b) True, only on success (e.g. no meaningful result other than "OK")
575 * - c) False, on any query failure
577 * @param string $sql Single-statement SQL query
578 * @return QueryStatus
581 abstract protected function doSingleStatementQuery( string $sql ): QueryStatus
;
584 * Determine whether a write query affects a permanent table.
585 * This includes pseudo-permanent tables.
587 * @param Query $query
590 private function hasPermanentTable( Query
$query ) {
591 if ( $query->getVerb() === 'CREATE TEMPORARY' ) {
592 // Temporary table creation is allowed
595 $table = $query->getWriteTable();
596 if ( $table === null ) {
597 // Parse error? Assume permanent.
600 [ $db, $pt ] = $this->platform
->getDatabaseAndTableIdentifier( $table );
601 $tempInfo = $this->sessionTempTables
[$db][$pt] ??
null;
602 return !$tempInfo ||
$tempInfo->pseudoPermanent
;
606 * Register creation and dropping of temporary tables
608 * @param Query $query
610 protected function registerTempTables( Query
$query ) {
611 $table = $query->getWriteTable();
612 if ( $table === null ) {
615 switch ( $query->getVerb() ) {
616 case 'CREATE TEMPORARY':
617 [ $db, $pt ] = $this->platform
->getDatabaseAndTableIdentifier( $table );
618 $this->sessionTempTables
[$db][$pt] = new TempTableInfo(
619 $this->transactionManager
->getTrxId(),
620 (bool)( $query->getFlags() & self
::QUERY_PSEUDO_PERMANENT
)
625 [ $db, $pt ] = $this->platform
->getDatabaseAndTableIdentifier( $table );
626 unset( $this->sessionTempTables
[$db][$pt] );
630 public function query( $sql, $fname = __METHOD__
, $flags = 0 ) {
631 if ( !( $sql instanceof Query
) ) {
632 $flags = (int)$flags; // b/c; this field used to be a bool
633 $sql = QueryBuilderFromRawSql
::buildQuery( $sql, $flags, $this->currentDomain
->getTablePrefix() );
635 $flags = $sql->getFlags();
638 // Make sure that this caller is allowed to issue this query statement
639 $this->assertQueryIsCurrentlyAllowed( $sql->getVerb(), $fname );
641 // Send the query to the server and fetch any corresponding errors
642 $status = $this->executeQuery( $sql, $fname, $flags );
643 if ( $status->res
=== false ) {
644 // An error occurred; log, and, if needed, report an exception.
645 // Errors that corrupt the transaction/session state cannot be silenced.
647 $this->flagsHolder
::contains( $flags, self
::QUERY_SILENCE_ERRORS
) &&
648 !$this->flagsHolder
::contains( $status->flags
, self
::ERR_ABORT_SESSION
) &&
649 !$this->flagsHolder
::contains( $status->flags
, self
::ERR_ABORT_TRX
)
651 $this->reportQueryError( $status->message
, $status->code
, $sql->getSQL(), $fname, $ignore );
658 * Execute a query without enforcing public (non-Database) caller restrictions.
660 * Retry it if there is a recoverable connection loss (e.g. no important state lost).
662 * This does not precheck for transaction/session state errors or critical section errors.
664 * @see Database::query()
666 * @param Query $sql SQL statement
667 * @param string $fname Name of the calling function
668 * @param int $flags Bit field of ISQLPlatform::QUERY_* constants
669 * @return QueryStatus
670 * @throws DBUnexpectedError
673 final protected function executeQuery( $sql, $fname, $flags ) {
674 $this->assertHasConnectionHandle();
676 $isPermWrite = false;
677 $isWrite = $sql->isWriteQuery();
679 ChangedTablesTracker
::recordQuery( $this->currentDomain
, $sql );
680 // Permit temporary table writes on replica connections, but require a writable
681 // master connection for writes to persistent tables.
682 if ( $this->hasPermanentTable( $sql ) ) {
684 $info = $this->getReadOnlyReason();
686 [ $reason, $source ] = $info;
687 if ( $source === 'role' ) {
688 throw new DBReadOnlyRoleError( $this, "Database is read-only: $reason" );
690 throw new DBReadOnlyError( $this, "Database is read-only: $reason" );
693 // DBConnRef uses QUERY_REPLICA_ROLE to enforce replica roles during query()
694 if ( $this->flagsHolder
::contains( $sql->getFlags(), self
::QUERY_REPLICA_ROLE
) ) {
695 throw new DBReadOnlyRoleError(
697 "Cannot write; target role is DB_REPLICA"
703 // Whether a silent retry attempt is left for recoverable connection loss errors
704 $retryLeft = !$this->flagsHolder
::contains( $flags, self
::QUERY_NO_RETRY
);
706 $cs = $this->commenceCriticalSection( __METHOD__
);
709 // Start a DBO_TRX wrapper transaction as needed (throw an error on failure)
710 if ( $this->beginIfImplied( $sql, $fname, $flags ) ) {
711 // Since begin() was called, any connection loss was already handled
714 // Send the query statement to the server and fetch any results.
715 $status = $this->attemptQuery( $sql, $fname, $isPermWrite );
717 // An error occurred that can be recovered from via query retry
718 $this->flagsHolder
::contains( $status->flags
, self
::ERR_RETRY_QUERY
) &&
719 // The retry has not been exhausted (consume it now)
720 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
721 $retryLeft && !( $retryLeft = false )
724 // Register creation and dropping of temporary tables
725 if ( $status->res
) {
726 $this->registerTempTables( $sql );
728 $this->completeCriticalSection( __METHOD__
, $cs );
734 * Query method wrapper handling profiling, logging, affected row count tracking, and
735 * automatic reconnections (without retry) on query failure due to connection loss
737 * Note that this does not handle DBO_TRX logic.
739 * This method handles profiling, debug logging, reconnection and the tracking of:
742 * - affected row count of the last write
743 * - whether writes occurred in a transaction
744 * - last successful query time (confirming that the connection was not dropped)
746 * @see doSingleStatementQuery()
748 * @param Query $sql SQL statement
749 * @param string $fname Name of the calling function
750 * @param bool $isPermWrite Whether it's a query writing to permanent tables
751 * @return QueryStatus statement result
752 * @throws DBUnexpectedError
754 private function attemptQuery(
759 // Transaction attributes before issuing this query
760 $priorSessInfo = new CriticalSessionInfo(
761 $this->transactionManager
->getTrxId(),
762 $this->transactionManager
->explicitTrxActive(),
763 $this->transactionManager
->pendingWriteCallers(),
764 $this->transactionManager
->pendingPreCommitCallbackCallers(),
765 $this->sessionNamedLocks
,
766 $this->sessionTempTables
768 // Get the transaction-aware SQL string used for profiling
769 $generalizedSql = GeneralizedSql
::newFromQuery(
771 ( $this->replicationReporter
->getTopologyRole() === self
::ROLE_STREAMING_MASTER
)
775 // Add agent and calling method comments to the SQL
776 $cStatement = $this->makeCommentedSql( $sql->getSQL(), $fname );
777 // Start profile section
778 $ps = $this->profiler ?
( $this->profiler
)( $generalizedSql->stringify() ) : null;
779 $startTime = microtime( true );
781 // Clear any overrides from a prior "query method". Note that this does not affect
782 // any such methods that are currently invoking query() itself since those query
783 // methods set these fields before returning.
784 $this->lastEmulatedAffectedRows
= null;
785 $this->lastEmulatedInsertId
= null;
787 // Record an OTEL span for this query.
788 $writeTableName = $sql->getWriteTable();
789 $spanName = $writeTableName ?
790 "Database {$sql->getVerb()} {$this->getDBname()}.{$writeTableName}" :
791 "Database {$sql->getVerb()} {$this->getDBname()}";
792 $span = $this->tracer
->createSpan( $spanName )
793 ->setSpanKind( SpanInterface
::SPAN_KIND_CLIENT
)
795 if ( $span->getContext()->isSampled() ) {
796 $span->setAttributes( [
797 'code.function' => $fname,
798 'db.namespace' => $this->getDBname(),
799 'db.operation.name' => $sql->getVerb(),
800 'db.query.text' => $generalizedSql->stringify(),
801 'db.system' => $this->getType(),
802 'server.address' => $this->getServerName(),
803 'db.collection.name' => $writeTableName, # nulls filtered out
807 $status = $this->doSingleStatementQuery( $cStatement );
809 // End profile section
810 $endTime = microtime( true );
811 $queryRuntime = max( $endTime - $startTime, 0.0 );
815 if ( $status->res
!== false ) {
816 $this->lastPing
= $endTime;
817 $span->setSpanStatus( SpanInterface
::SPAN_STATUS_OK
);
819 $span->setSpanStatus( SpanInterface
::SPAN_STATUS_ERROR
)
821 'db.response.status_code' => $status->code
,
822 'exception.message' => $status->message
,
826 $affectedRowCount = $status->rowsAffected
;
827 $returnedRowCount = $status->rowsReturned
;
828 $this->lastQueryAffectedRows
= $affectedRowCount;
830 if ( $span->getContext()->isSampled() ) {
831 $span->setAttributes( [
832 'db.response.affected_rows' => $affectedRowCount,
833 'db.response.returned_rows' => $returnedRowCount,
837 if ( $status->res
!== false ) {
838 if ( $isPermWrite ) {
839 if ( $this->trxLevel() ) {
840 $this->transactionManager
->transactionWritingIn(
841 $this->getServerName(),
842 $this->getDomainID(),
845 $this->transactionManager
->updateTrxWriteQueryReport(
852 $this->lastWriteTime
= $endTime;
857 $this->transactionManager
->recordQueryCompletion(
861 $isPermWrite ?
$affectedRowCount : $returnedRowCount,
862 $this->getServerName()
865 // Check if the query failed...
866 $status->flags
= $this->handleErroredQuery( $status, $sql, $fname, $queryRuntime, $priorSessInfo );
867 // Avoid the overhead of logging calls unless debug mode is enabled
868 if ( $this->flagsHolder
->getFlag( self
::DBO_DEBUG
) ) {
869 $this->logger
->debug(
870 "{method} [{runtime_ms}ms] {db_server}: {sql}",
871 $this->getLogContext( [
873 'sql' => $sql->getSQL(),
874 'domain' => $this->getDomainID(),
875 'runtime_ms' => round( $queryRuntime * 1000, 3 ),
876 'db_log_category' => 'query'
884 private function handleErroredQuery( QueryStatus
$status, $sql, $fname, $queryRuntime, $priorSessInfo ) {
885 $errflags = self
::ERR_NONE
;
886 $error = $status->message
;
887 $errno = $status->code
;
888 if ( $status->res
!== false ) {
889 // Statement succeeded
892 if ( $this->isConnectionError( $errno ) ) {
893 // Connection lost before or during the query...
894 // Determine how to proceed given the lost session state
895 $connLossFlag = $this->assessConnectionLoss(
900 // Update session state tracking and try to reestablish a connection
901 $reconnected = $this->replaceLostConnection( $errno, __METHOD__
);
902 // Check if important server-side session-level state was lost
903 if ( $connLossFlag >= self
::ERR_ABORT_SESSION
) {
904 $ex = $this->getQueryException( $error, $errno, $sql->getSQL(), $fname );
905 $this->transactionManager
->setSessionError( $ex );
907 // Check if important server-side transaction-level state was lost
908 if ( $connLossFlag >= self
::ERR_ABORT_TRX
) {
909 $ex = $this->getQueryException( $error, $errno, $sql->getSQL(), $fname );
910 $this->transactionManager
->setTransactionError( $ex );
912 // Check if the query should be retried (having made the reconnection attempt)
913 if ( $connLossFlag === self
::ERR_RETRY_QUERY
) {
914 $errflags |
= ( $reconnected ? self
::ERR_RETRY_QUERY
: self
::ERR_ABORT_QUERY
);
916 $errflags |
= $connLossFlag;
918 } elseif ( $this->isKnownStatementRollbackError( $errno ) ) {
919 // Query error triggered a server-side statement-only rollback...
920 $errflags |
= self
::ERR_ABORT_QUERY
;
921 if ( $this->trxLevel() ) {
922 // Allow legacy callers to ignore such errors via QUERY_IGNORE_DBO_TRX and
923 // try/catch. However, a deprecation notice will be logged on the next query.
924 $cause = [ $error, $errno, $fname ];
925 $this->transactionManager
->setTrxStatusIgnoredCause( $cause );
927 } elseif ( $this->trxLevel() ) {
928 // Some other error occurred during the query, within a transaction...
929 // Server-side handling of errors during transactions varies widely depending on
930 // the RDBMS type and configuration. There are several possible results: (a) the
931 // whole transaction is rolled back, (b) only the queries after BEGIN are rolled
932 // back, (c) the transaction is marked as "aborted" and a ROLLBACK is required
933 // before other queries are permitted. For compatibility reasons, pessimistically
934 // require a ROLLBACK query (not using SAVEPOINT) before allowing other queries.
935 $ex = $this->getQueryException( $error, $errno, $sql->getSQL(), $fname );
936 $this->transactionManager
->setTransactionError( $ex );
937 $errflags |
= self
::ERR_ABORT_TRX
;
939 // Some other error occurred during the query, without a transaction...
940 $errflags |
= self
::ERR_ABORT_QUERY
;
948 * @param string $fname
951 private function makeCommentedSql( $sql, $fname ): string {
952 // Add trace comment to the begin of the sql string, right after the operator.
953 // Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598).
954 // NOTE: Don't add varying ids such as request id or session id to the comment.
955 // It would break aggregation of similar queries in analysis tools (see T193050#7512149)
956 $encName = preg_replace( '/[\x00-\x1F\/]/', '-', "$fname {$this->agent}" );
957 return preg_replace( '/\s|$/', " /* $encName */ ", $sql, 1 );
961 * Start an implicit transaction if DBO_TRX is enabled and no transaction is active
963 * @param Query $sql SQL statement
964 * @param string $fname
965 * @param int $flags Bit field of ISQLPlatform::QUERY_* constants
966 * @return bool Whether an implicit transaction was started
969 private function beginIfImplied( $sql, $fname, $flags ) {
970 if ( !$this->trxLevel() && $this->flagsHolder
->hasApplicableImplicitTrxFlag( $flags ) ) {
971 if ( $this->platform
->isTransactableQuery( $sql ) ) {
972 $this->begin( __METHOD__
. " ($fname)", self
::TRANSACTION_INTERNAL
);
973 $this->transactionManager
->turnOnAutomatic();
983 * Check if callers outside of Database can run the given query given the session state
985 * In order to keep the DB handle's session state tracking in sync, certain queries
986 * like "USE", "BEGIN", "COMMIT", and "ROLLBACK" must not be issued directly from
987 * outside callers. Such commands should only be issued through dedicated methods
988 * like selectDomain(), begin(), commit(), and rollback(), respectively.
990 * This also checks if the session state tracking was corrupted by a prior exception.
992 * @param string $verb
993 * @param string $fname
994 * @throws DBUnexpectedError
995 * @throws DBTransactionStateError
997 private function assertQueryIsCurrentlyAllowed( string $verb, string $fname ) {
998 if ( $verb === 'USE' ) {
999 throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead" );
1002 if ( $verb === 'ROLLBACK' ) {
1003 // Whole transaction rollback is used for recovery
1004 // @TODO: T269161; prevent "BEGIN"/"COMMIT"/"ROLLBACK" from outside callers
1008 if ( $this->csmError
) {
1009 throw new DBTransactionStateError(
1011 "Cannot execute query from $fname while session state is out of sync",
1017 $this->transactionManager
->assertSessionStatus( $this, $fname );
1019 if ( $verb !== 'ROLLBACK TO SAVEPOINT' ) {
1020 $this->transactionManager
->assertTransactionStatus(
1022 $this->deprecationLogger
,
1029 * Determine how to handle a connection lost discovered during a query attempt
1031 * This checks if explicit transactions, pending transaction writes, and important
1032 * session-level state (locks, temp tables) was lost. Point-in-time read snapshot loss
1033 * is considered acceptable for DBO_TRX logic.
1035 * If state was lost, but that loss was discovered during a ROLLBACK that would have
1036 * destroyed that state anyway, treat the error as recoverable.
1038 * @param string $verb SQL query verb
1039 * @param float $walltime How many seconds passes while attempting the query
1040 * @param CriticalSessionInfo $priorSessInfo Session state just before the query
1041 * @return int Recovery approach. One of the following ERR_* class constants:
1042 * - Database::ERR_RETRY_QUERY: reconnect silently, retry query
1043 * - Database::ERR_ABORT_QUERY: reconnect silently, do not retry query
1044 * - Database::ERR_ABORT_TRX: reconnect, throw error, enforce transaction rollback
1045 * - Database::ERR_ABORT_SESSION: reconnect, throw error, enforce session rollback
1047 private function assessConnectionLoss(
1050 CriticalSessionInfo
$priorSessInfo
1052 if ( $walltime < self
::DROPPED_CONN_BLAME_THRESHOLD_SEC
) {
1053 // Query failed quickly; the connection was probably lost before the query was sent
1054 $res = self
::ERR_RETRY_QUERY
;
1056 // Query took a long time; the connection was probably lost during query execution
1057 $res = self
::ERR_ABORT_QUERY
;
1060 // List of problems causing session/transaction state corruption
1062 // Loss of named locks breaks future callers relying on those locks for critical sections
1063 foreach ( $priorSessInfo->namedLocks
as $lockName => $lockInfo ) {
1064 if ( $lockInfo['trxId'] && $lockInfo['trxId'] === $priorSessInfo->trxId
) {
1065 // Treat lost locks acquired during the lost transaction as a transaction state
1066 // problem. Connection loss on ROLLBACK (non-SAVEPOINT) is tolerable since
1067 // rollback automatically triggered server-side.
1068 if ( $verb !== 'ROLLBACK' ) {
1069 $res = max( $res, self
::ERR_ABORT_TRX
);
1070 $blockers[] = "named lock '$lockName'";
1073 // Treat lost locks acquired either during prior transactions or during no
1074 // transaction as a session state problem.
1075 $res = max( $res, self
::ERR_ABORT_SESSION
);
1076 $blockers[] = "named lock '$lockName'";
1079 // Loss of temp tables breaks future callers relying on those tables for queries
1080 foreach ( $priorSessInfo->tempTables
as $domainTempTables ) {
1081 foreach ( $domainTempTables as $tableName => $tableInfo ) {
1082 if ( $tableInfo->trxId
&& $tableInfo->trxId
=== $priorSessInfo->trxId
) {
1083 // Treat lost temp tables created during the lost transaction as a
1084 // transaction state problem. Connection loss on ROLLBACK (non-SAVEPOINT)
1085 // is tolerable since rollback automatically triggered server-side.
1086 if ( $verb !== 'ROLLBACK' ) {
1087 $res = max( $res, self
::ERR_ABORT_TRX
);
1088 $blockers[] = "temp table '$tableName'";
1091 // Treat lost temp tables created either during prior transactions or during
1092 // no transaction as a session state problem.
1093 $res = max( $res, self
::ERR_ABORT_SESSION
);
1094 $blockers[] = "temp table '$tableName'";
1098 // Loss of transaction writes breaks future callers and DBO_TRX logic relying on those
1099 // writes to be atomic and still pending. Connection loss on ROLLBACK (non-SAVEPOINT) is
1100 // tolerable since rollback automatically triggered server-side.
1101 if ( $priorSessInfo->trxWriteCallers
&& $verb !== 'ROLLBACK' ) {
1102 $res = max( $res, self
::ERR_ABORT_TRX
);
1103 $blockers[] = 'uncommitted writes';
1105 if ( $priorSessInfo->trxPreCommitCbCallers
&& $verb !== 'ROLLBACK' ) {
1106 $res = max( $res, self
::ERR_ABORT_TRX
);
1107 $blockers[] = 'pre-commit callbacks';
1109 if ( $priorSessInfo->trxExplicit
&& $verb !== 'ROLLBACK' && $verb !== 'COMMIT' ) {
1110 // Transaction automatically rolled back, breaking the expectations of callers
1111 // relying on the continued existence of that transaction for things like atomic
1112 // writes, serializability, or reads from the same point-in-time snapshot. If the
1113 // connection loss occured on ROLLBACK (non-SAVEPOINT) or COMMIT, then we do not
1114 // need to mark the transaction state as corrupt, since no transaction would still
1115 // be open even if the query did succeed (T127428).
1116 $res = max( $res, self
::ERR_ABORT_TRX
);
1117 $blockers[] = 'explicit transaction';
1121 $this->logger
->warning(
1122 "cannot reconnect to {db_server} silently: {error}",
1123 $this->getLogContext( [
1124 'error' => 'session state loss (' . implode( ', ', $blockers ) . ')',
1125 'exception' => new RuntimeException(),
1126 'db_log_category' => 'connection'
1135 * Clean things up after session (and thus transaction) loss before reconnect
1137 private function handleSessionLossPreconnect() {
1138 // Clean up tracking of session-level things...
1139 // https://mariadb.com/kb/en/create-table/#create-temporary-table
1140 // https://www.postgresql.org/docs/9.2/static/sql-createtable.html (ignoring ON COMMIT)
1141 $this->sessionTempTables
= [];
1142 // https://mariadb.com/kb/en/get_lock/
1143 // https://www.postgresql.org/docs/9.4/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS
1144 $this->sessionNamedLocks
= [];
1145 // Session loss implies transaction loss (T67263)
1146 $this->transactionManager
->onSessionLoss( $this );
1147 // Clear additional subclass fields
1148 $this->doHandleSessionLossPreconnect();
1152 * Reset any additional subclass trx* and session* fields
1154 protected function doHandleSessionLossPreconnect() {
1159 * Checks whether the cause of the error is detected to be a timeout.
1161 * It returns false by default, and not all engines support detecting this yet.
1162 * If this returns false, it will be treated as a generic query error.
1164 * @param int|string $errno Error number
1168 protected function isQueryTimeoutError( $errno ) {
1173 * Report a query error
1175 * If $ignore is set, emit a DEBUG level log entry and continue,
1176 * otherwise, emit an ERROR level log entry and throw an exception.
1178 * @param string $error
1179 * @param int|string $errno
1180 * @param string $sql
1181 * @param string $fname
1182 * @param bool $ignore Whether to just log an error rather than throw an exception
1183 * @throws DBQueryError
1185 public function reportQueryError( $error, $errno, $sql, $fname, $ignore = false ) {
1187 $this->logger
->debug(
1188 "SQL ERROR (ignored): $error",
1189 [ 'db_log_category' => 'query' ]
1192 throw $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
1197 * @param string $error
1198 * @param string|int $errno
1199 * @param string $sql
1200 * @param string $fname
1203 private function getQueryExceptionAndLog( $error, $errno, $sql, $fname ) {
1204 // Information that instances of the same problem have in common should
1205 // not be normalized (T255202).
1206 $this->logger
->error(
1207 "Error $errno from $fname, {error} {sql1line} {db_server}",
1208 $this->getLogContext( [
1209 'method' => __METHOD__
,
1212 'sql1line' => mb_substr( str_replace( "\n", "\\n", $sql ), 0, 5 * 1024 ),
1214 'db_log_category' => 'query',
1215 'exception' => new RuntimeException()
1218 return $this->getQueryException( $error, $errno, $sql, $fname );
1222 * @param string $error
1223 * @param string|int $errno
1224 * @param string $sql
1225 * @param string $fname
1228 private function getQueryException( $error, $errno, $sql, $fname ) {
1229 if ( $this->isQueryTimeoutError( $errno ) ) {
1230 return new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
1231 } elseif ( $this->isConnectionError( $errno ) ) {
1232 return new DBQueryDisconnectedError( $this, $error, $errno, $sql, $fname );
1234 return new DBQueryError( $this, $error, $errno, $sql, $fname );
1239 * @param string $error
1240 * @return DBConnectionError
1242 final protected function newExceptionAfterConnectError( $error ) {
1243 // Connection was not fully initialized and is not safe for use.
1244 // Stash any error associated with the handle before destroying it.
1245 $this->lastConnectError
= $error;
1248 $this->logger
->error(
1249 "Error connecting to {db_server} as user {db_user}: {error}",
1250 $this->getLogContext( [
1252 'exception' => new RuntimeException(),
1253 'db_log_category' => 'connection',
1257 return new DBConnectionError( $this, $error );
1261 * Get a SelectQueryBuilder bound to this connection. This is overridden by
1264 * @return SelectQueryBuilder
1266 public function newSelectQueryBuilder(): SelectQueryBuilder
{
1267 return new SelectQueryBuilder( $this );
1271 * Get a UnionQueryBuilder bound to this connection. This is overridden by
1274 * @return UnionQueryBuilder
1276 public function newUnionQueryBuilder(): UnionQueryBuilder
{
1277 return new UnionQueryBuilder( $this );
1281 * Get an UpdateQueryBuilder bound to this connection. This is overridden by
1284 * @return UpdateQueryBuilder
1286 public function newUpdateQueryBuilder(): UpdateQueryBuilder
{
1287 return new UpdateQueryBuilder( $this );
1291 * Get a DeleteQueryBuilder bound to this connection. This is overridden by
1294 * @return DeleteQueryBuilder
1296 public function newDeleteQueryBuilder(): DeleteQueryBuilder
{
1297 return new DeleteQueryBuilder( $this );
1301 * Get a InsertQueryBuilder bound to this connection. This is overridden by
1304 * @return InsertQueryBuilder
1306 public function newInsertQueryBuilder(): InsertQueryBuilder
{
1307 return new InsertQueryBuilder( $this );
1311 * Get a ReplaceQueryBuilder bound to this connection. This is overridden by
1314 * @return ReplaceQueryBuilder
1316 public function newReplaceQueryBuilder(): ReplaceQueryBuilder
{
1317 return new ReplaceQueryBuilder( $this );
1320 public function selectField(
1321 $tables, $var, $cond = '', $fname = __METHOD__
, $options = [], $join_conds = []
1323 if ( $var === '*' ) {
1324 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1325 } elseif ( is_array( $var ) && count( $var ) !== 1 ) {
1326 throw new DBUnexpectedError( $this, 'Cannot use more than one field' );
1329 $options = $this->platform
->normalizeOptions( $options );
1330 $options['LIMIT'] = 1;
1332 $res = $this->select( $tables, $var, $cond, $fname, $options, $join_conds );
1333 if ( $res === false ) {
1334 throw new DBUnexpectedError( $this, "Got false from select()" );
1337 $row = $res->fetchRow();
1338 if ( $row === false ) {
1342 return reset( $row );
1345 public function selectFieldValues(
1346 $tables, $var, $cond = '', $fname = __METHOD__
, $options = [], $join_conds = []
1348 if ( $var === '*' ) {
1349 throw new DBUnexpectedError( $this, "Cannot use a * field" );
1350 } elseif ( !is_string( $var ) ) {
1351 throw new DBUnexpectedError( $this, "Cannot use an array of fields" );
1354 $options = $this->platform
->normalizeOptions( $options );
1355 $res = $this->select( $tables, [ 'value' => $var ], $cond, $fname, $options, $join_conds );
1356 if ( $res === false ) {
1357 throw new DBUnexpectedError( $this, "Got false from select()" );
1361 foreach ( $res as $row ) {
1362 $values[] = $row->value
;
1368 public function select(
1369 $tables, $vars, $conds = '', $fname = __METHOD__
, $options = [], $join_conds = []
1371 $options = (array)$options;
1372 // Don't turn this into using platform directly, DatabaseMySQL overrides this.
1373 $sql = $this->selectSQLText( $tables, $vars, $conds, $fname, $options, $join_conds );
1374 // Treat SELECT queries with FOR UPDATE as writes. This matches
1375 // how MySQL enforces read_only (FOR SHARE and LOCK IN SHADE MODE are allowed).
1376 $flags = in_array( 'FOR UPDATE', $options, true )
1377 ? self
::QUERY_CHANGE_ROWS
1378 : self
::QUERY_CHANGE_NONE
;
1380 $query = new Query( $sql, $flags, 'SELECT' );
1381 return $this->query( $query, $fname );
1384 public function selectRow( $tables, $vars, $conds, $fname = __METHOD__
,
1385 $options = [], $join_conds = []
1387 $options = (array)$options;
1388 $options['LIMIT'] = 1;
1390 $res = $this->select( $tables, $vars, $conds, $fname, $options, $join_conds );
1391 if ( $res === false ) {
1392 throw new DBUnexpectedError( $this, "Got false from select()" );
1395 if ( !$res->numRows() ) {
1399 return $res->fetchObject();
1405 public function estimateRowCount(
1406 $tables, $var = '*', $conds = '', $fname = __METHOD__
, $options = [], $join_conds = []
1408 $conds = $this->platform
->normalizeConditions( $conds, $fname );
1409 $column = $this->platform
->extractSingleFieldFromList( $var );
1410 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1411 $conds[] = "$column IS NOT NULL";
1414 $res = $this->select(
1415 $tables, [ 'rowcount' => 'COUNT(*)' ], $conds, $fname, $options, $join_conds
1417 $row = $res ?
$res->fetchRow() : [];
1419 return isset( $row['rowcount'] ) ?
(int)$row['rowcount'] : 0;
1422 public function selectRowCount(
1423 $tables, $var = '*', $conds = '', $fname = __METHOD__
, $options = [], $join_conds = []
1425 $conds = $this->platform
->normalizeConditions( $conds, $fname );
1426 $column = $this->platform
->extractSingleFieldFromList( $var );
1427 if ( is_string( $column ) && !in_array( $column, [ '*', '1' ] ) ) {
1428 $conds[] = "$column IS NOT NULL";
1430 if ( in_array( 'DISTINCT', (array)$options ) ) {
1431 if ( $column === null ) {
1432 throw new DBUnexpectedError( $this,
1433 '$var cannot be empty when the DISTINCT option is given' );
1435 $innerVar = $column;
1440 $res = $this->select(
1442 'tmp_count' => $this->platform
->buildSelectSubquery(
1451 [ 'rowcount' => 'COUNT(*)' ],
1455 $row = $res ?
$res->fetchRow() : [];
1457 return isset( $row['rowcount'] ) ?
(int)$row['rowcount'] : 0;
1460 public function lockForUpdate(
1461 $table, $conds = '', $fname = __METHOD__
, $options = [], $join_conds = []
1463 if ( !$this->trxLevel() && !$this->flagsHolder
->hasImplicitTrxFlag() ) {
1464 throw new DBUnexpectedError(
1466 __METHOD__
. ': no transaction is active nor is DBO_TRX set'
1470 $options = (array)$options;
1471 $options[] = 'FOR UPDATE';
1473 return $this->selectRowCount( $table, '*', $conds, $fname, $options, $join_conds );
1476 public function fieldExists( $table, $field, $fname = __METHOD__
) {
1477 $info = $this->fieldInfo( $table, $field );
1482 abstract public function tableExists( $table, $fname = __METHOD__
);
1484 public function indexExists( $table, $index, $fname = __METHOD__
) {
1485 $info = $this->indexInfo( $table, $index, $fname );
1490 public function indexUnique( $table, $index, $fname = __METHOD__
) {
1491 $info = $this->indexInfo( $table, $index, $fname );
1493 return $info ?
$info['unique'] : null;
1497 * Get information about an index into an object
1499 * @param string $table The unqualified name of a table
1500 * @param string $index Index name
1501 * @param string $fname Calling function name
1502 * @return array<string,mixed>|false Index info map; false if it does not exist
1503 * @phan-return array{unique:bool}|false
1505 abstract public function indexInfo( $table, $index, $fname = __METHOD__
);
1507 public function insert( $table, $rows, $fname = __METHOD__
, $options = [] ) {
1508 $query = $this->platform
->dispatchingInsertSqlText( $table, $rows, $options );
1512 $this->query( $query, $fname );
1513 if ( $this->strictWarnings
) {
1514 $this->checkInsertWarnings( $query, $fname );
1520 * Check for warnings after performing an INSERT query, and throw exceptions
1523 * @param Query $query
1524 * @param string $fname
1527 protected function checkInsertWarnings( Query
$query, $fname ) {
1530 public function update( $table, $set, $conds, $fname = __METHOD__
, $options = [] ) {
1531 $query = $this->platform
->updateSqlText( $table, $set, $conds, $options );
1532 $this->query( $query, $fname );
1537 public function databasesAreIndependent() {
1541 final public function selectDomain( $domain ) {
1542 $cs = $this->commenceCriticalSection( __METHOD__
);
1545 $this->doSelectDomain( DatabaseDomain
::newFromId( $domain ) );
1546 } catch ( DBError
$e ) {
1547 $this->completeCriticalSection( __METHOD__
, $cs );
1551 $this->completeCriticalSection( __METHOD__
, $cs );
1555 * @param DatabaseDomain $domain
1556 * @throws DBConnectionError
1560 protected function doSelectDomain( DatabaseDomain
$domain ) {
1561 $this->currentDomain
= $domain;
1562 $this->platform
->setCurrentDomain( $this->currentDomain
);
1565 public function getDBname() {
1566 return $this->currentDomain
->getDatabase();
1569 public function getServer() {
1570 return $this->connectionParams
[self
::CONN_HOST
] ??
null;
1573 public function getServerName() {
1574 return $this->serverName ??
$this->getServer() ??
'unknown';
1577 public function addQuotes( $s ) {
1578 if ( $s instanceof RawSQLValue
) {
1581 if ( $s instanceof Blob
) {
1584 if ( $s === null ) {
1586 } elseif ( is_bool( $s ) ) {
1587 return (string)(int)$s;
1588 } elseif ( is_int( $s ) ) {
1591 return "'" . $this->strencode( $s ) . "'";
1595 public function expr( string $field, string $op, $value ): Expression
{
1596 return new Expression( $field, $op, $value );
1599 public function andExpr( array $conds ): AndExpressionGroup
{
1600 return AndExpressionGroup
::newFromArray( $conds );
1603 public function orExpr( array $conds ): OrExpressionGroup
{
1604 return OrExpressionGroup
::newFromArray( $conds );
1607 public function replace( $table, $uniqueKeys, $rows, $fname = __METHOD__
) {
1608 $uniqueKey = $this->platform
->normalizeUpsertParams( $uniqueKeys, $rows );
1612 $affectedRowCount = 0;
1614 $this->startAtomic( $fname, self
::ATOMIC_CANCELABLE
);
1616 foreach ( $rows as $row ) {
1617 // Delete any conflicting rows (including ones inserted from $rows)
1618 $query = $this->platform
->deleteSqlText(
1620 [ $this->platform
->makeKeyCollisionCondition( [ $row ], $uniqueKey ) ]
1622 $this->query( $query, $fname );
1623 // Insert the new row
1624 $query = $this->platform
->dispatchingInsertSqlText( $table, $row, [] );
1625 $this->query( $query, $fname );
1626 $affectedRowCount +
= $this->lastQueryAffectedRows
;
1627 $insertId = $insertId ?
: $this->lastQueryInsertId
;
1629 $this->endAtomic( $fname );
1630 } catch ( DBError
$e ) {
1631 $this->cancelAtomic( $fname );
1634 $this->lastEmulatedAffectedRows
= $affectedRowCount;
1635 $this->lastEmulatedInsertId
= $insertId;
1638 public function upsert( $table, array $rows, $uniqueKeys, array $set, $fname = __METHOD__
) {
1639 $uniqueKey = $this->platform
->normalizeUpsertParams( $uniqueKeys, $rows );
1643 $this->platform
->assertValidUpsertSetArray( $set, $uniqueKey, $rows );
1645 $encTable = $this->tableName( $table );
1646 $sqlColumnAssignments = $this->makeList( $set, self
::LIST_SET
);
1647 // Get any AUTO_INCREMENT/SERIAL column for this table so we can set insertId()
1648 $autoIncrementColumn = $this->getInsertIdColumnForUpsert( $table );
1649 // Check if there is a SQL assignment expression in $set (as generated by SQLPlatform::buildExcludedValue)
1650 $useWith = (bool)array_filter(
1652 static function ( $v, $k ) {
1653 return $v instanceof RawSQLValue ||
is_int( $k );
1655 ARRAY_FILTER_USE_BOTH
1657 // Subclasses might need explicit type casting within "WITH...AS (VALUES ...)"
1658 // so that these CTE rows can be referenced within the SET clause assigments.
1659 $typeByColumn = $useWith ?
$this->getValueTypesForWithClause( $table ) : [];
1662 $affectedRowCount = 0;
1664 $this->startAtomic( $fname, self
::ATOMIC_CANCELABLE
);
1666 foreach ( $rows as $row ) {
1667 // Update any existing conflicting row (including ones inserted from $rows)
1668 [ $sqlColumns, $sqlTuples, $sqlVals ] = $this->platform
->makeInsertLists(
1673 $sqlConditions = $this->platform
->makeKeyCollisionCondition(
1678 ( $useWith ?
"WITH __VALS ($sqlVals) AS (VALUES $sqlTuples) " : "" ) .
1679 "UPDATE $encTable SET $sqlColumnAssignments " .
1680 "WHERE ($sqlConditions)",
1681 self
::QUERY_CHANGE_ROWS
,
1685 $this->query( $query, $fname );
1686 $rowsUpdated = $this->lastQueryAffectedRows
;
1687 $affectedRowCount +
= $rowsUpdated;
1688 if ( $rowsUpdated > 0 ) {
1689 // Conflicting row found and updated
1690 if ( $first && $autoIncrementColumn !== null ) {
1691 // @TODO: use "RETURNING" instead (when supported by SQLite)
1693 "SELECT $autoIncrementColumn AS id FROM $encTable " .
1694 "WHERE ($sqlConditions)",
1695 self
::QUERY_CHANGE_NONE
,
1698 $sRes = $this->query( $query, $fname, self
::QUERY_CHANGE_ROWS
);
1699 $insertId = (int)$sRes->fetchRow()['id'];
1702 // No conflicting row found
1704 "INSERT INTO $encTable ($sqlColumns) VALUES $sqlTuples",
1705 self
::QUERY_CHANGE_ROWS
,
1709 $this->query( $query, $fname );
1710 $affectedRowCount +
= $this->lastQueryAffectedRows
;
1714 $this->endAtomic( $fname );
1715 } catch ( DBError
$e ) {
1716 $this->cancelAtomic( $fname );
1719 $this->lastEmulatedAffectedRows
= $affectedRowCount;
1720 $this->lastEmulatedInsertId
= $insertId;
1725 * @param string $table The unqualified name of a table
1726 * @return string|null The AUTO_INCREMENT/SERIAL column; null if not needed
1728 protected function getInsertIdColumnForUpsert( $table ) {
1733 * @param string $table The unqualified name of a table
1734 * @return array<string,string> Map of (column => type); [] if not needed
1736 protected function getValueTypesForWithClause( $table ) {
1740 public function deleteJoin(
1748 $sql = $this->platform
->deleteJoinSqlText( $delTable, $joinTable, $delVar, $joinVar, $conds );
1749 $query = new Query( $sql, self
::QUERY_CHANGE_ROWS
, 'DELETE', $delTable );
1750 $this->query( $query, $fname );
1753 public function delete( $table, $conds, $fname = __METHOD__
) {
1754 $this->query( $this->platform
->deleteSqlText( $table, $conds ), $fname );
1759 final public function insertSelect(
1764 $fname = __METHOD__
,
1765 $insertOptions = [],
1766 $selectOptions = [],
1767 $selectJoinConds = []
1769 static $hints = [ 'NO_AUTO_COLUMNS' ];
1771 $insertOptions = $this->platform
->normalizeOptions( $insertOptions );
1772 $selectOptions = $this->platform
->normalizeOptions( $selectOptions );
1774 if ( $this->cliMode
&& $this->isInsertSelectSafe( $insertOptions, $selectOptions, $fname ) ) {
1775 // For massive migrations with downtime, we don't want to select everything
1776 // into memory and OOM, so do all this native on the server side if possible.
1777 $this->doInsertSelectNative(
1783 array_diff( $insertOptions, $hints ),
1788 $this->doInsertSelectGeneric(
1794 array_diff( $insertOptions, $hints ),
1804 * @param array $insertOptions
1805 * @param array $selectOptions
1806 * @param string $fname
1807 * @return bool Whether an INSERT SELECT with these options will be replication safe
1810 protected function isInsertSelectSafe( array $insertOptions, array $selectOptions, $fname ) {
1815 * Implementation of insertSelect() based on select() and insert()
1817 * @see IDatabase::insertSelect()
1818 * @param string $destTable Unqualified name of destination table
1819 * @param string|array $srcTable Unqualified name of source table
1820 * @param array $varMap
1821 * @param array $conds
1822 * @param string $fname
1823 * @param array $insertOptions
1824 * @param array $selectOptions
1825 * @param array $selectJoinConds
1828 private function doInsertSelectGeneric(
1834 array $insertOptions,
1835 array $selectOptions,
1838 // For web requests, do a locking SELECT and then INSERT. This puts the SELECT burden
1839 // on only the primary DB (without needing row-based-replication). It also makes it easy to
1840 // know how big the INSERT is going to be.
1842 foreach ( $varMap as $dstColumn => $sourceColumnOrSql ) {
1843 $fields[] = $this->platform
->fieldNameWithAlias( $sourceColumnOrSql, $dstColumn );
1845 $res = $this->select(
1847 implode( ',', $fields ),
1850 array_merge( $selectOptions, [ 'FOR UPDATE' ] ),
1854 $affectedRowCount = 0;
1857 $this->startAtomic( $fname, self
::ATOMIC_CANCELABLE
);
1860 foreach ( $res as $row ) {
1861 $rows[] = (array)$row;
1863 // Avoid inserts that are too huge
1864 $rowBatches = array_chunk( $rows, $this->nonNativeInsertSelectBatchSize
);
1865 foreach ( $rowBatches as $rows ) {
1866 $query = $this->platform
->dispatchingInsertSqlText( $destTable, $rows, $insertOptions );
1867 $this->query( $query, $fname );
1868 $affectedRowCount +
= $this->lastQueryAffectedRows
;
1869 $insertId = $insertId ?
: $this->lastQueryInsertId
;
1871 $this->endAtomic( $fname );
1872 } catch ( DBError
$e ) {
1873 $this->cancelAtomic( $fname );
1877 $this->lastEmulatedAffectedRows
= $affectedRowCount;
1878 $this->lastEmulatedInsertId
= $insertId;
1882 * Native server-side implementation of insertSelect() for situations where
1883 * we don't want to select everything into memory
1885 * @see IDatabase::insertSelect()
1886 * @param string $destTable The unqualified name of destination table
1887 * @param string|array $srcTable The unqualified name of source table
1888 * @param array $varMap
1889 * @param array $conds
1890 * @param string $fname
1891 * @param array $insertOptions
1892 * @param array $selectOptions
1893 * @param array $selectJoinConds
1896 protected function doInsertSelectNative(
1902 array $insertOptions,
1903 array $selectOptions,
1906 $sql = $this->platform
->insertSelectNativeSqlText(
1918 self
::QUERY_CHANGE_ROWS
,
1922 $this->query( $query, $fname );
1926 * Do not use this method outside of Database/DBError classes
1928 * @param int|string $errno
1929 * @return bool Whether the given query error was a connection drop
1932 protected function isConnectionError( $errno ) {
1937 * @param int|string $errno
1938 * @return bool Whether it is known that the last query error only caused statement rollback
1939 * @note This is for backwards compatibility for callers catching DBError exceptions in
1940 * order to ignore problems like duplicate key errors or foreign key violations
1943 protected function isKnownStatementRollbackError( $errno ) {
1944 return false; // don't know; it could have caused a transaction rollback
1950 public function serverIsReadOnly() {
1954 final public function onTransactionResolution( callable
$callback, $fname = __METHOD__
) {
1955 $this->transactionManager
->onTransactionResolution( $this, $callback, $fname );
1958 final public function onTransactionCommitOrIdle( callable
$callback, $fname = __METHOD__
) {
1959 if ( !$this->trxLevel() && $this->getTransactionRoundFname() !== null ) {
1960 // This DB handle is set to participate in LoadBalancer transaction rounds and
1961 // an explicit transaction round is active. Start an implicit transaction on this
1962 // DB handle (setting trxAutomatic) similar to how query() does in such situations.
1963 $this->begin( __METHOD__
, self
::TRANSACTION_INTERNAL
);
1966 $this->transactionManager
->addPostCommitOrIdleCallback( $callback, $fname );
1967 if ( !$this->trxLevel() ) {
1969 $this->runOnTransactionIdleCallbacks( self
::TRIGGER_IDLE
, $dbErrors );
1976 final public function onTransactionPreCommitOrIdle( callable
$callback, $fname = __METHOD__
) {
1977 if ( !$this->trxLevel() && $this->getTransactionRoundFname() !== null ) {
1978 // This DB handle is set to participate in LoadBalancer transaction rounds and
1979 // an explicit transaction round is active. Start an implicit transaction on this
1980 // DB handle (setting trxAutomatic) similar to how query() does in such situations.
1981 $this->begin( __METHOD__
, self
::TRANSACTION_INTERNAL
);
1984 if ( $this->trxLevel() ) {
1985 $this->transactionManager
->addPreCommitOrIdleCallback(
1990 // No transaction is active nor will start implicitly, so make one for this callback
1991 $this->startAtomic( __METHOD__
, self
::ATOMIC_CANCELABLE
);
1994 } catch ( Throwable
$e ) {
1995 // Avoid confusing error reporting during critical section errors
1996 if ( !$this->csmError
) {
1997 $this->cancelAtomic( __METHOD__
);
2001 $this->endAtomic( __METHOD__
);
2005 final public function setTransactionListener( $name, ?callable
$callback = null ) {
2006 $this->transactionManager
->setTransactionListener( $name, $callback );
2010 * Whether to disable running of post-COMMIT/ROLLBACK callbacks
2012 * @internal This method should not be used outside of Database/LoadBalancer
2015 * @param bool $suppress
2017 final public function setTrxEndCallbackSuppression( $suppress ) {
2018 $this->transactionManager
->setTrxEndCallbackSuppression( $suppress );
2022 * Consume and run any "on transaction idle/resolution" callbacks
2024 * @internal This method should not be used outside of Database/LoadBalancer
2027 * @param int $trigger IDatabase::TRIGGER_* constant
2028 * @param DBError[] &$errors DB exceptions caught [returned]
2029 * @return int Number of callbacks attempted
2030 * @throws DBUnexpectedError
2031 * @throws Throwable Any non-DBError exception thrown by a callback
2033 public function runOnTransactionIdleCallbacks( $trigger, array &$errors = [] ) {
2034 if ( $this->trxLevel() ) {
2035 throw new DBUnexpectedError( $this, __METHOD__
. ': a transaction is still open' );
2038 if ( $this->transactionManager
->isEndCallbacksSuppressed() ) {
2039 // Execution deferred by LoadBalancer for explicit execution later
2043 $cs = $this->commenceCriticalSection( __METHOD__
);
2046 $autoTrx = $this->flagsHolder
->hasImplicitTrxFlag(); // automatic begin() enabled?
2047 // Drain the queues of transaction "idle" and "end" callbacks until they are empty
2049 $callbackEntries = $this->transactionManager
->consumeEndCallbacks();
2050 $count +
= count( $callbackEntries );
2051 foreach ( $callbackEntries as $entry ) {
2052 $this->flagsHolder
->clearFlag( self
::DBO_TRX
); // make each query its own transaction
2054 $entry[0]( $trigger, $this );
2055 } catch ( DBError
$ex ) {
2056 call_user_func( $this->errorLogger
, $ex );
2058 // Some callbacks may use startAtomic/endAtomic, so make sure
2059 // their transactions are ended so other callbacks don't fail
2060 if ( $this->trxLevel() ) {
2061 $this->rollback( __METHOD__
, self
::FLUSHING_INTERNAL
);
2065 $this->flagsHolder
->setFlag( self
::DBO_TRX
); // restore automatic begin()
2067 $this->flagsHolder
->clearFlag( self
::DBO_TRX
); // restore auto-commit
2071 } while ( $this->transactionManager
->countPostCommitOrIdleCallbacks() );
2073 $this->completeCriticalSection( __METHOD__
, $cs );
2079 * Actually run any "transaction listener" callbacks
2081 * @internal This method should not be used outside of Database/LoadBalancer
2084 * @param int $trigger IDatabase::TRIGGER_* constant
2085 * @param DBError[] &$errors DB exceptions caught [returned]
2086 * @throws Throwable Any non-DBError exception thrown by a callback
2088 public function runTransactionListenerCallbacks( $trigger, array &$errors = [] ) {
2089 if ( $this->transactionManager
->isEndCallbacksSuppressed() ) {
2090 // Execution deferred by LoadBalancer for explicit execution later
2094 // These callbacks should only be registered in setup, thus no iteration is needed
2095 foreach ( $this->transactionManager
->getRecurringCallbacks() as $callback ) {
2097 $callback( $trigger, $this );
2098 } catch ( DBError
$ex ) {
2099 ( $this->errorLogger
)( $ex );
2106 * Handle "on transaction idle/resolution" and "transaction listener" callbacks post-COMMIT
2108 * @throws DBError The first DBError exception thrown by a callback
2109 * @throws Throwable Any non-DBError exception thrown by a callback
2111 private function runTransactionPostCommitCallbacks() {
2113 $this->runOnTransactionIdleCallbacks( self
::TRIGGER_COMMIT
, $dbErrors );
2114 $this->runTransactionListenerCallbacks( self
::TRIGGER_COMMIT
, $dbErrors );
2115 $this->lastEmulatedAffectedRows
= 0; // for the sake of consistency
2122 * Handle "on transaction idle/resolution" and "transaction listener" callbacks post-ROLLBACK
2124 * This will suppress and log any DBError exceptions
2126 * @throws Throwable Any non-DBError exception thrown by a callback
2128 private function runTransactionPostRollbackCallbacks() {
2129 $this->runOnTransactionIdleCallbacks( self
::TRIGGER_ROLLBACK
);
2130 $this->runTransactionListenerCallbacks( self
::TRIGGER_ROLLBACK
);
2131 $this->lastEmulatedAffectedRows
= 0; // for the sake of consistency
2134 final public function startAtomic(
2135 $fname = __METHOD__
,
2136 $cancelable = self
::ATOMIC_NOT_CANCELABLE
2138 $cs = $this->commenceCriticalSection( __METHOD__
);
2140 if ( $this->trxLevel() ) {
2141 // This atomic section is only one part of a larger transaction
2142 $sectionOwnsTrx = false;
2144 // Start an implicit transaction (sets trxAutomatic)
2146 $this->begin( $fname, self
::TRANSACTION_INTERNAL
);
2147 } catch ( DBError
$e ) {
2148 $this->completeCriticalSection( __METHOD__
, $cs );
2151 if ( $this->flagsHolder
->hasImplicitTrxFlag() ) {
2152 // This DB handle participates in LoadBalancer transaction rounds; all atomic
2153 // sections should be buffered into one transaction (e.g. to keep web requests
2154 // transactional). Note that an implicit transaction round is considered to be
2155 // active when no there is no explicit transaction round.
2156 $sectionOwnsTrx = false;
2158 // This DB handle does not participate in LoadBalancer transaction rounds;
2159 // each topmost atomic section will use its own transaction.
2160 $sectionOwnsTrx = true;
2162 $this->transactionManager
->setAutomaticAtomic( $sectionOwnsTrx );
2165 if ( $cancelable === self
::ATOMIC_CANCELABLE
) {
2166 if ( $sectionOwnsTrx ) {
2167 // This atomic section is synonymous with the whole transaction; just
2168 // use full COMMIT/ROLLBACK in endAtomic()/cancelAtomic(), respectively
2169 $savepointId = self
::NOT_APPLICABLE
;
2171 // This atomic section is only part of the whole transaction; use a SAVEPOINT
2172 // query so that its changes can be cancelled without losing the rest of the
2173 // transaction (e.g. changes from other sections or from outside of sections)
2175 $savepointId = $this->transactionManager
->nextSavePointId( $this, $fname );
2176 $sql = $this->platform
->savepointSqlText( $savepointId );
2177 $query = new Query( $sql, self
::QUERY_CHANGE_TRX
, 'SAVEPOINT' );
2178 $this->query( $query, $fname );
2179 } catch ( DBError
$e ) {
2180 $this->completeCriticalSection( __METHOD__
, $cs, $e );
2185 $savepointId = null;
2188 $sectionId = new AtomicSectionIdentifier
;
2189 $this->transactionManager
->addToAtomicLevels( $fname, $sectionId, $savepointId );
2191 $this->completeCriticalSection( __METHOD__
, $cs );
2196 final public function endAtomic( $fname = __METHOD__
) {
2197 [ $savepointId, $sectionId ] = $this->transactionManager
->onEndAtomic( $this, $fname );
2199 $runPostCommitCallbacks = false;
2201 $cs = $this->commenceCriticalSection( __METHOD__
);
2203 // Remove the last section (no need to re-index the array)
2204 $finalLevelOfImplicitTrxPopped = $this->transactionManager
->popAtomicLevel();
2207 if ( $finalLevelOfImplicitTrxPopped ) {
2208 $this->commit( $fname, self
::FLUSHING_INTERNAL
);
2209 $runPostCommitCallbacks = true;
2210 } elseif ( $savepointId !== null && $savepointId !== self
::NOT_APPLICABLE
) {
2211 $sql = $this->platform
->releaseSavepointSqlText( $savepointId );
2212 $query = new Query( $sql, self
::QUERY_CHANGE_TRX
, 'RELEASE SAVEPOINT' );
2213 $this->query( $query, $fname );
2215 } catch ( DBError
$e ) {
2216 $this->completeCriticalSection( __METHOD__
, $cs, $e );
2220 $this->transactionManager
->onEndAtomicInCriticalSection( $sectionId );
2222 $this->completeCriticalSection( __METHOD__
, $cs );
2224 if ( $runPostCommitCallbacks ) {
2225 $this->runTransactionPostCommitCallbacks();
2229 final public function cancelAtomic(
2230 $fname = __METHOD__
,
2231 ?AtomicSectionIdentifier
$sectionId = null
2233 $this->transactionManager
->onCancelAtomicBeforeCriticalSection( $this, $fname );
2234 $pos = $this->transactionManager
->getPositionFromSectionId( $sectionId );
2236 throw new DBUnexpectedError( $this, "Atomic section not found (for $fname)" );
2239 $cs = $this->commenceCriticalSection( __METHOD__
);
2240 $runPostRollbackCallbacks = false;
2241 [ $savedFname, $excisedSectionIds, $newTopSectionId, $savedSectionId, $savepointId ] =
2242 $this->transactionManager
->cancelAtomic( $pos );
2245 if ( $savedFname !== $fname ) {
2246 $e = new DBUnexpectedError(
2248 "Invalid atomic section ended (got $fname but expected $savedFname)"
2250 $this->completeCriticalSection( __METHOD__
, $cs, $e );
2254 // Remove the last section (no need to re-index the array)
2255 $this->transactionManager
->popAtomicLevel();
2256 $excisedSectionIds[] = $savedSectionId;
2257 $newTopSectionId = $this->transactionManager
->currentAtomicSectionId();
2259 if ( $savepointId !== null ) {
2260 // Rollback the transaction changes proposed within this atomic section
2261 if ( $savepointId === self
::NOT_APPLICABLE
) {
2262 // Atomic section started the transaction; rollback the whole transaction
2263 // and trigger cancellation callbacks for all active atomic sections
2264 $this->rollback( $fname, self
::FLUSHING_INTERNAL
);
2265 $runPostRollbackCallbacks = true;
2267 // Atomic section nested within the transaction; rollback the transaction
2268 // to the state prior to this section and trigger its cancellation callbacks
2269 $sql = $this->platform
->rollbackToSavepointSqlText( $savepointId );
2270 $query = new Query( $sql, self
::QUERY_CHANGE_TRX
, 'ROLLBACK TO SAVEPOINT' );
2271 $this->query( $query, $fname );
2272 $this->transactionManager
->setTrxStatusToOk(); // no exception; recovered
2275 // Put the transaction into an error state if it's not already in one
2276 $trxError = new DBUnexpectedError(
2278 "Uncancelable atomic section canceled (got $fname)"
2280 $this->transactionManager
->setTransactionError( $trxError );
2283 // Fix up callbacks owned by the sections that were just cancelled.
2284 // All callbacks should have an owner that is present in trxAtomicLevels.
2285 $this->transactionManager
->modifyCallbacksForCancel(
2291 $this->lastEmulatedAffectedRows
= 0; // for the sake of consistency
2293 $this->completeCriticalSection( __METHOD__
, $cs );
2295 if ( $runPostRollbackCallbacks ) {
2296 $this->runTransactionPostRollbackCallbacks();
2300 final public function doAtomicSection(
2303 $cancelable = self
::ATOMIC_NOT_CANCELABLE
2305 $sectionId = $this->startAtomic( $fname, $cancelable );
2307 $res = $callback( $this, $fname );
2308 } catch ( Throwable
$e ) {
2309 // Avoid confusing error reporting during critical section errors
2310 if ( !$this->csmError
) {
2311 $this->cancelAtomic( $fname, $sectionId );
2316 $this->endAtomic( $fname );
2321 final public function begin( $fname = __METHOD__
, $mode = self
::TRANSACTION_EXPLICIT
) {
2322 static $modes = [ self
::TRANSACTION_EXPLICIT
, self
::TRANSACTION_INTERNAL
];
2323 if ( !in_array( $mode, $modes, true ) ) {
2324 throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'" );
2327 $this->transactionManager
->onBegin( $this, $fname );
2329 if ( $this->flagsHolder
->hasImplicitTrxFlag() && $mode !== self
::TRANSACTION_INTERNAL
) {
2330 $msg = "$fname: implicit transaction expected (DBO_TRX set)";
2331 throw new DBUnexpectedError( $this, $msg );
2334 $this->assertHasConnectionHandle();
2336 $cs = $this->commenceCriticalSection( __METHOD__
);
2337 $timeStart = microtime( true );
2339 $this->doBegin( $fname );
2340 } catch ( DBError
$e ) {
2341 $this->completeCriticalSection( __METHOD__
, $cs );
2344 $timeEnd = microtime( true );
2345 // Treat "BEGIN" as a trivial query to gauge the RTT delay
2346 $rtt = max( $timeEnd - $timeStart, 0.0 );
2347 $this->transactionManager
->onBeginInCriticalSection( $mode, $fname, $rtt );
2348 $this->replicationReporter
->resetReplicationLagStatus( $this );
2349 $this->completeCriticalSection( __METHOD__
, $cs );
2353 * Issues the BEGIN command to the database server.
2355 * @see Database::begin()
2356 * @param string $fname
2359 protected function doBegin( $fname ) {
2360 $query = new Query( 'BEGIN', self
::QUERY_CHANGE_TRX
, 'BEGIN' );
2361 $this->query( $query, $fname );
2364 final public function commit( $fname = __METHOD__
, $flush = self
::FLUSHING_ONE
) {
2365 static $modes = [ self
::FLUSHING_ONE
, self
::FLUSHING_ALL_PEERS
, self
::FLUSHING_INTERNAL
];
2366 if ( !in_array( $flush, $modes, true ) ) {
2367 throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'" );
2370 if ( !$this->transactionManager
->onCommit( $this, $fname, $flush ) ) {
2374 $this->assertHasConnectionHandle();
2376 $this->runOnTransactionPreCommitCallbacks();
2378 $cs = $this->commenceCriticalSection( __METHOD__
);
2380 if ( $this->trxLevel() ) {
2381 $query = new Query( 'COMMIT', self
::QUERY_CHANGE_TRX
, 'COMMIT' );
2382 $this->query( $query, $fname );
2384 } catch ( DBError
$e ) {
2385 $this->completeCriticalSection( __METHOD__
, $cs );
2388 $lastWriteTime = $this->transactionManager
->onCommitInCriticalSection( $this );
2389 if ( $lastWriteTime ) {
2390 $this->lastWriteTime
= $lastWriteTime;
2392 // With FLUSHING_ALL_PEERS, callbacks will run when requested by a dedicated phase
2393 // within LoadBalancer. With FLUSHING_INTERNAL, callbacks will run when requested by
2394 // the Database caller during a safe point. This avoids isolation and recursion issues.
2395 if ( $flush === self
::FLUSHING_ONE
) {
2396 $this->runTransactionPostCommitCallbacks();
2398 $this->completeCriticalSection( __METHOD__
, $cs );
2401 final public function rollback( $fname = __METHOD__
, $flush = self
::FLUSHING_ONE
) {
2403 $flush !== self
::FLUSHING_INTERNAL
&&
2404 $flush !== self
::FLUSHING_ALL_PEERS
&&
2405 $this->flagsHolder
->hasImplicitTrxFlag()
2407 throw new DBUnexpectedError(
2409 "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
2413 if ( !$this->trxLevel() ) {
2414 $this->transactionManager
->setTrxStatusToNone();
2415 $this->transactionManager
->clearPreEndCallbacks();
2416 if ( $this->transactionManager
->trxLevel() === TransactionManager
::STATUS_TRX_ERROR
) {
2417 $this->logger
->info(
2418 "$fname: acknowledged server-side transaction loss on {db_server}",
2419 $this->getLogContext()
2426 $this->assertHasConnectionHandle();
2428 if ( $this->csmError
) {
2429 // Since the session state is corrupt, we cannot just rollback the transaction
2430 // while preserving the non-transaction session state. The handle will remain
2431 // marked as corrupt until flushSession() is called to reset the connection
2432 // and deal with any remaining callbacks.
2433 $this->logger
->info(
2434 "$fname: acknowledged client-side transaction loss on {db_server}",
2435 $this->getLogContext()
2441 $cs = $this->commenceCriticalSection( __METHOD__
);
2442 if ( $this->trxLevel() ) {
2443 // Disconnects cause rollback anyway, so ignore those errors
2445 $this->platform
->rollbackSqlText(),
2446 self
::QUERY_SILENCE_ERRORS | self
::QUERY_CHANGE_TRX
,
2449 $this->query( $query, $fname );
2451 $this->transactionManager
->onRollbackInCriticalSection( $this );
2452 // With FLUSHING_ALL_PEERS, callbacks will run when requested by a dedicated phase
2453 // within LoadBalancer. With FLUSHING_INTERNAL, callbacks will run when requested by
2454 // the Database caller during a safe point. This avoids isolation and recursion issues.
2455 if ( $flush === self
::FLUSHING_ONE
) {
2456 $this->runTransactionPostRollbackCallbacks();
2458 $this->completeCriticalSection( __METHOD__
, $cs );
2462 * @internal Only for tests and highly discouraged
2463 * @param TransactionManager $transactionManager
2465 public function setTransactionManager( TransactionManager
$transactionManager ) {
2466 $this->transactionManager
= $transactionManager;
2469 public function flushSession( $fname = __METHOD__
, $flush = self
::FLUSHING_ONE
) {
2471 $flush !== self
::FLUSHING_INTERNAL
&&
2472 $flush !== self
::FLUSHING_ALL_PEERS
&&
2473 $this->flagsHolder
->hasImplicitTrxFlag()
2475 throw new DBUnexpectedError(
2477 "$fname: Expected mass flush of all peer connections (DBO_TRX set)"
2481 if ( $this->csmError
) {
2482 // If a critical section error occurred, such as Excimer timeout exceptions raised
2483 // before a query response was marshalled, destroy the connection handle and reset
2484 // the session state tracking variables. The value of trxLevel() is irrelevant here,
2485 // and, in fact, might be 1 due to rollback() deferring critical section recovery.
2486 $this->logger
->info(
2487 "$fname: acknowledged client-side session loss on {db_server}",
2488 $this->getLogContext()
2490 $this->csmError
= null;
2491 $this->csmFname
= null;
2492 $this->replaceLostConnection( 2048, __METHOD__
);
2497 if ( $this->trxLevel() ) {
2498 // Any existing transaction should have been rolled back already
2499 throw new DBUnexpectedError(
2501 "$fname: transaction still in progress (not yet rolled back)"
2505 if ( $this->transactionManager
->sessionStatus() === TransactionManager
::STATUS_SESS_ERROR
) {
2506 // If the session state was already lost due to either an unacknowledged session
2507 // state loss error (e.g. dropped connection) or an explicit connection close call,
2508 // then there is nothing to do here. Note that in such cases, even temporary tables
2509 // and server-side config variables are lost (invocation of this method is assumed
2510 // to imply that such losses are tolerable).
2511 $this->logger
->info(
2512 "$fname: acknowledged server-side session loss on {db_server}",
2513 $this->getLogContext()
2515 } elseif ( $this->isOpen() ) {
2516 // Connection handle exists; server-side session state must be flushed
2517 $this->doFlushSession( $fname );
2518 $this->sessionNamedLocks
= [];
2521 $this->transactionManager
->clearSessionError();
2525 * Reset the server-side session state for named locks and table locks
2527 * Connection and query errors will be suppressed and logged
2529 * @param string $fname
2532 protected function doFlushSession( $fname ) {
2536 public function flushSnapshot( $fname = __METHOD__
, $flush = self
::FLUSHING_ONE
) {
2537 $this->transactionManager
->onFlushSnapshot(
2541 $this->getTransactionRoundFname()
2544 $this->transactionManager
->sessionStatus() === TransactionManager
::STATUS_SESS_ERROR ||
2545 $this->transactionManager
->trxStatus() === TransactionManager
::STATUS_TRX_ERROR
2547 $this->rollback( $fname, self
::FLUSHING_INTERNAL
);
2549 $this->commit( $fname, self
::FLUSHING_INTERNAL
);
2553 public function duplicateTableStructure(
2559 throw new RuntimeException( __METHOD__
. ' is not implemented in descendant class' );
2562 public function listTables( $prefix = null, $fname = __METHOD__
) {
2563 throw new RuntimeException( __METHOD__
. ' is not implemented in descendant class' );
2566 public function affectedRows() {
2567 $this->lastEmulatedAffectedRows ??
= $this->lastQueryAffectedRows
;
2569 return $this->lastEmulatedAffectedRows
;
2572 public function insertId() {
2573 if ( $this->lastEmulatedInsertId
=== null ) {
2574 // Guard against misuse of this method by checking affectedRows(). Note that calls
2575 // to insert() with "IGNORE" and calls to insertSelect() might not add any rows.
2576 if ( $this->affectedRows() ) {
2577 $this->lastEmulatedInsertId
= $this->lastInsertId();
2579 $this->lastEmulatedInsertId
= 0;
2583 return $this->lastEmulatedInsertId
;
2587 * Get a row ID from the last insert statement to implicitly assign one within the session
2589 * If the statement involved assigning sequence IDs to multiple rows, then the return value
2590 * will be any one of those values (database-specific). If the statement was an "UPSERT" and
2591 * some existing rows were updated, then the result will either reflect only IDs of created
2592 * rows or it will reflect IDs of both created and updated rows (this is database-specific).
2594 * The result is unspecified if the statement gave an error.
2596 * @return int Sequence ID, 0 (if none)
2599 abstract protected function lastInsertId();
2601 public function ping() {
2602 if ( $this->isOpen() ) {
2603 // If the connection was recently used, assume that it is still good
2604 if ( ( microtime( true ) - $this->lastPing
) < self
::PING_TTL
) {
2607 // Send a trivial query to test the connection, triggering an automatic
2608 // reconnection attempt if the connection was lost
2611 self
::QUERY_IGNORE_DBO_TRX | self
::QUERY_SILENCE_ERRORS | self
::QUERY_CHANGE_NONE
,
2614 $res = $this->query( $query, __METHOD__
);
2615 $ok = ( $res !== false );
2617 // Try to re-establish a connection
2618 $ok = $this->replaceLostConnection( null, __METHOD__
);
2625 * Close any existing (dead) database connection and open a new connection
2627 * @param int|null $lastErrno
2628 * @param string $fname
2629 * @return bool True if new connection is opened successfully, false if error
2631 protected function replaceLostConnection( $lastErrno, $fname ) {
2632 if ( $this->conn
) {
2633 $this->closeConnection();
2635 $this->handleSessionLossPreconnect();
2640 $this->connectionParams
[self
::CONN_HOST
],
2641 $this->connectionParams
[self
::CONN_USER
],
2642 $this->connectionParams
[self
::CONN_PASSWORD
],
2643 $this->currentDomain
->getDatabase(),
2644 $this->currentDomain
->getSchema(),
2645 $this->tablePrefix()
2647 $this->lastPing
= microtime( true );
2650 $this->logger
->warning(
2651 $fname . ': lost connection to {db_server} with error {errno}; reconnected',
2652 $this->getLogContext( [
2653 'exception' => new RuntimeException(),
2654 'db_log_category' => 'connection',
2655 'errno' => $lastErrno
2658 } catch ( DBConnectionError
$e ) {
2661 $this->logger
->error(
2662 $fname . ': lost connection to {db_server} with error {errno}; reconnection failed: {connect_msg}',
2663 $this->getLogContext( [
2664 'exception' => new RuntimeException(),
2665 'db_log_category' => 'connection',
2666 'errno' => $lastErrno,
2667 'connect_msg' => $e->getMessage()
2672 // Handle callbacks in trxEndCallbacks, e.g. onTransactionResolution().
2673 // If callback suppression is set then the array will remain unhandled.
2674 $this->runOnTransactionIdleCallbacks( self
::TRIGGER_ROLLBACK
);
2675 // Handle callbacks in trxRecurringCallbacks, e.g. setTransactionListener().
2676 // If callback suppression is set then the array will remain unhandled.
2677 $this->runTransactionListenerCallbacks( self
::TRIGGER_ROLLBACK
);
2683 * Merge the result of getSessionLagStatus() for several DBs
2684 * using the most pessimistic values to estimate the lag of
2685 * any data derived from them in combination
2687 * This is information is useful for caching modules
2689 * @see WANObjectCache::set()
2690 * @see WANObjectCache::getWithSetCallback()
2692 * @param IReadableDatabase|null ...$dbs
2693 * Note: For backward compatibility, it is allowed for null values
2694 * to be passed among the parameters. This is deprecated since 1.36,
2695 * only IReadableDatabase objects should be passed.
2697 * @return array Map of values:
2698 * - lag: highest lag of any of the DBs or false on error (e.g. replication stopped)
2699 * - since: oldest UNIX timestamp of any of the DB lag estimates
2700 * - pending: whether any of the DBs have uncommitted changes
2704 public static function getCacheSetOptions( ?IReadableDatabase
...$dbs ) {
2705 $res = [ 'lag' => 0, 'since' => INF
, 'pending' => false ];
2707 foreach ( func_get_args() as $db ) {
2708 if ( $db instanceof IReadableDatabase
) {
2709 $status = $db->getSessionLagStatus();
2711 if ( $status['lag'] === false ) {
2712 $res['lag'] = false;
2713 } elseif ( $res['lag'] !== false ) {
2714 $res['lag'] = max( $res['lag'], $status['lag'] );
2716 $res['since'] = min( $res['since'], $status['since'] );
2719 if ( $db instanceof IDatabaseForOwner
) {
2720 $res['pending'] = $res['pending'] ?
: $db->writesPending();
2727 public function encodeBlob( $b ) {
2731 public function decodeBlob( $b ) {
2732 if ( $b instanceof Blob
) {
2738 public function setSessionOptions( array $options ) {
2741 public function sourceFile(
2743 ?callable
$lineCallback = null,
2744 ?callable
$resultCallback = null,
2746 ?callable
$inputCallback = null
2748 AtEase
::suppressWarnings();
2749 $fp = fopen( $filename, 'r' );
2750 AtEase
::restoreWarnings();
2752 if ( $fp === false ) {
2753 throw new RuntimeException( "Could not open \"{$filename}\"" );
2757 $fname = __METHOD__
. "( $filename )";
2761 return $this->sourceStream(
2773 public function sourceStream(
2775 ?callable
$lineCallback = null,
2776 ?callable
$resultCallback = null,
2777 $fname = __METHOD__
,
2778 ?callable
$inputCallback = null
2780 $delimiterReset = new ScopedCallback(
2781 function ( $delimiter ) {
2782 $this->delimiter
= $delimiter;
2784 [ $this->delimiter
]
2788 while ( !feof( $fp ) ) {
2789 if ( $lineCallback ) {
2790 call_user_func( $lineCallback );
2793 $line = trim( fgets( $fp ) );
2795 if ( $line == '' ) {
2799 if ( $line[0] == '-' && $line[1] == '-' ) {
2807 $done = $this->streamStatementEnd( $cmd, $line );
2811 if ( $done ||
feof( $fp ) ) {
2812 $cmd = $this->platform
->replaceVars( $cmd );
2814 if ( $inputCallback ) {
2815 $callbackResult = $inputCallback( $cmd );
2817 if ( is_string( $callbackResult ) ||
!$callbackResult ) {
2818 $cmd = $callbackResult;
2823 $res = $this->query( $cmd, $fname );
2825 if ( $resultCallback ) {
2826 $resultCallback( $res, $this );
2829 if ( $res === false ) {
2830 $err = $this->lastError();
2832 return "Query \"{$cmd}\" failed with error code \"$err\".\n";
2839 ScopedCallback
::consume( $delimiterReset );
2844 * Called by sourceStream() to check if we've reached a statement end
2846 * @param string &$sql SQL assembled so far
2847 * @param string &$newLine New line about to be added to $sql
2848 * @return bool Whether $newLine contains end of the statement
2850 public function streamStatementEnd( &$sql, &$newLine ) {
2851 if ( $this->delimiter
) {
2853 $newLine = preg_replace(
2854 '/' . preg_quote( $this->delimiter
, '/' ) . '$/',
2858 if ( $newLine != $prev ) {
2869 public function lockIsFree( $lockName, $method ) {
2870 // RDBMs methods for checking named locks may or may not count this thread itself.
2871 // In MySQL, IS_FREE_LOCK() returns 0 if the thread already has the lock. This is
2872 // the behavior chosen by the interface for this method.
2873 if ( isset( $this->sessionNamedLocks
[$lockName] ) ) {
2874 $lockIsFree = false;
2876 $lockIsFree = $this->doLockIsFree( $lockName, $method );
2885 * @param string $lockName
2886 * @param string $method
2887 * @return bool Success
2890 protected function doLockIsFree( string $lockName, string $method ) {
2891 return true; // not implemented
2897 public function lock( $lockName, $method, $timeout = 5, $flags = 0 ) {
2898 $lockTsUnix = $this->doLock( $lockName, $method, $timeout );
2899 if ( $lockTsUnix !== null ) {
2901 $this->sessionNamedLocks
[$lockName] = [
2902 'ts' => $lockTsUnix,
2903 'trxId' => $this->transactionManager
->getTrxId()
2907 $this->logger
->info(
2908 __METHOD__
. ": failed to acquire lock '{lockname}'",
2910 'lockname' => $lockName,
2911 'db_log_category' => 'locking'
2916 return $this->flagsHolder
::contains( $flags, self
::LOCK_TIMESTAMP
) ?
$lockTsUnix : $locked;
2922 * @param string $lockName
2923 * @param string $method
2924 * @param int $timeout
2925 * @return float|null UNIX timestamp of lock acquisition; null on failure
2928 protected function doLock( string $lockName, string $method, int $timeout ) {
2929 return microtime( true ); // not implemented
2935 public function unlock( $lockName, $method ) {
2936 if ( !isset( $this->sessionNamedLocks
[$lockName] ) ) {
2938 $this->logger
->warning(
2939 __METHOD__
. ": trying to release unheld lock '$lockName'\n",
2940 [ 'db_log_category' => 'locking' ]
2943 $released = $this->doUnlock( $lockName, $method );
2945 unset( $this->sessionNamedLocks
[$lockName] );
2947 $this->logger
->warning(
2948 __METHOD__
. ": failed to release lock '$lockName'\n",
2949 [ 'db_log_category' => 'locking' ]
2960 * @param string $lockName
2961 * @param string $method
2962 * @return bool Success
2965 protected function doUnlock( string $lockName, string $method ) {
2966 return true; // not implemented
2969 public function getScopedLockAndFlush( $lockKey, $fname, $timeout ) {
2970 $this->transactionManager
->onGetScopedLockAndFlush( $this, $fname );
2972 if ( !$this->lock( $lockKey, $fname, $timeout ) ) {
2976 $unlocker = new ScopedCallback( function () use ( $lockKey, $fname ) {
2977 // Note that the callback can be reached due to an exception making the calling
2978 // function end early. If the transaction/session is in an error state, avoid log
2979 // spam and confusing replacement of an original DBError with one about unlock().
2980 // Unlock query will fail anyway; avoid possibly triggering errors in rollback()
2982 $this->transactionManager
->sessionStatus() === TransactionManager
::STATUS_SESS_ERROR ||
2983 $this->transactionManager
->trxStatus() === TransactionManager
::STATUS_TRX_ERROR
2987 if ( $this->trxLevel() ) {
2988 $this->onTransactionResolution(
2989 function () use ( $lockKey, $fname ) {
2990 $this->unlock( $lockKey, $fname );
2995 $this->unlock( $lockKey, $fname );
2999 $this->commit( $fname, self
::FLUSHING_INTERNAL
);
3004 public function dropTable( $table, $fname = __METHOD__
) {
3005 if ( !$this->tableExists( $table, $fname ) ) {
3010 $this->platform
->dropTableSqlText( $table ),
3011 self
::QUERY_CHANGE_SCHEMA
,
3015 $this->query( $query, $fname );
3020 public function truncateTable( $table, $fname = __METHOD__
) {
3021 $sql = "TRUNCATE TABLE " . $this->tableName( $table );
3022 $query = new Query( $sql, self
::QUERY_CHANGE_SCHEMA
, 'TRUNCATE', $table );
3023 $this->query( $query, $fname );
3026 public function isReadOnly() {
3027 return ( $this->getReadOnlyReason() !== null );
3031 * @return array|null Tuple of (reason string, "role" or "lb") if read-only; null otherwise
3033 protected function getReadOnlyReason() {
3034 $reason = $this->replicationReporter
->getTopologyBasedReadOnlyReason();
3039 $reason = $this->getLBInfo( self
::LB_READ_ONLY_REASON
);
3040 if ( is_string( $reason ) ) {
3041 return [ $reason, 'lb' ];
3048 * Get the underlying binding connection handle
3050 * Makes sure the connection resource is set (disconnects and ping() failure can unset it).
3051 * This catches broken callers than catch and ignore disconnection exceptions.
3052 * Unlike checking isOpen(), this is safe to call inside of open().
3055 * @throws DBUnexpectedError
3058 protected function getBindingHandle() {
3059 if ( !$this->conn
) {
3060 throw new DBUnexpectedError(
3062 'DB connection was already closed or the connection dropped'
3070 * Demark the start of a critical section of session/transaction state changes
3072 * Use this to disable potentially DB handles due to corruption from highly unexpected
3073 * exceptions (e.g. from zend timers or coding errors) preempting execution of methods.
3075 * Callers must demark completion of the critical section with completeCriticalSection().
3076 * Callers should handle DBError exceptions that do not cause object state corruption by
3077 * catching them, calling completeCriticalSection(), and then rethrowing them.
3080 * $cs = $this->commenceCriticalSection( __METHOD__ );
3082 * //...send a query that changes the session/transaction state...
3083 * } catch ( DBError $e ) {
3084 * $this->completeCriticalSection( __METHOD__, $cs );
3085 * throw $expectedException;
3088 * //...send another query that changes the session/transaction state...
3089 * } catch ( DBError $trxError ) {
3090 * // Require ROLLBACK before allowing any other queries from outside callers
3091 * $this->completeCriticalSection( __METHOD__, $cs, $trxError );
3092 * throw $expectedException;
3094 * // ...update session state fields of $this...
3095 * $this->completeCriticalSection( __METHOD__, $cs );
3098 * @see Database::completeCriticalSection()
3101 * @param string $fname Caller name
3102 * @return CriticalSectionScope|null RAII-style monitor (topmost sections only)
3103 * @throws DBUnexpectedError If an unresolved critical section error already exists
3105 protected function commenceCriticalSection( string $fname ) {
3106 if ( $this->csmError
) {
3107 throw new DBUnexpectedError(
3109 "Cannot execute $fname critical section while session state is out of sync.\n\n" .
3110 $this->csmError
->getMessage() . "\n" .
3111 $this->csmError
->getTraceAsString()
3115 if ( $this->csmId
) {
3116 $csm = null; // fold into the outer critical section
3117 } elseif ( $this->csProvider
) {
3118 $csm = $this->csProvider
->scopedEnter(
3120 null, // emergency limit (default)
3121 null, // emergency callback (default)
3122 function () use ( $fname ) {
3123 // Mark a critical section as having been aborted by an error
3124 $e = new RuntimeException( "A critical section from {$fname} has failed" );
3125 $this->csmError
= $e;
3126 $this->csmId
= null;
3129 $this->csmId
= $csm->getId();
3130 $this->csmFname
= $fname;
3132 $csm = null; // not supported
3139 * Demark the completion of a critical section of session/transaction state changes
3141 * @see Database::commenceCriticalSection()
3144 * @param string $fname Caller name
3145 * @param CriticalSectionScope|null $csm RAII-style monitor (topmost sections only)
3146 * @param Throwable|null $trxError Error that requires setting STATUS_TRX_ERROR (if any)
3148 protected function completeCriticalSection(
3150 ?CriticalSectionScope
$csm,
3151 ?Throwable
$trxError = null
3153 if ( $csm !== null ) {
3154 if ( $this->csmId
=== null ) {
3155 throw new LogicException( "$fname critical section is not active" );
3156 } elseif ( $csm->getId() !== $this->csmId
) {
3157 throw new LogicException(
3158 "$fname critical section is not the active ({$this->csmFname}) one"
3163 $this->csmId
= null;
3167 $this->transactionManager
->setTransactionError( $trxError );
3171 public function __toString() {
3172 $id = spl_object_id( $this );
3174 $description = $this->getType() . ' object #' . $id;
3175 // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.is_resource
3176 if ( is_resource( $this->conn
) ) {
3177 $description .= ' (' . (string)$this->conn
. ')'; // "resource id #<ID>"
3178 } elseif ( is_object( $this->conn
) ) {
3179 $handleId = spl_object_id( $this->conn
);
3180 $description .= " (handle id #$handleId)";
3183 return $description;
3187 * Make sure that copies do not share the same client binding handle
3188 * @throws DBConnectionError
3190 public function __clone() {
3191 $this->logger
->warning(
3192 "Cloning " . static::class . " is not recommended; forking connection",
3194 'exception' => new RuntimeException(),
3195 'db_log_category' => 'connection'
3199 if ( $this->isOpen() ) {
3200 // Open a new connection resource without messing with the old one
3202 $this->transactionManager
->clearEndCallbacks();
3203 $this->handleSessionLossPreconnect(); // no trx or locks anymore
3205 $this->connectionParams
[self
::CONN_HOST
],
3206 $this->connectionParams
[self
::CONN_USER
],
3207 $this->connectionParams
[self
::CONN_PASSWORD
],
3208 $this->currentDomain
->getDatabase(),
3209 $this->currentDomain
->getSchema(),
3210 $this->tablePrefix()
3212 $this->lastPing
= microtime( true );
3217 * Called by serialize. Throw an exception when DB connection is serialized.
3218 * This causes problems on some database engines because the connection is
3219 * not restored on unserialize.
3222 public function __sleep() {
3223 throw new RuntimeException( 'Database serialization may cause problems, since ' .
3224 'the connection is not restored on wakeup' );
3228 * Run a few simple checks and close dangling connections
3230 public function __destruct() {
3231 if ( $this->transactionManager
) {
3232 // Tests mock this class and disable constructor.
3233 $this->transactionManager
->onDestruct();
3236 $danglingWriters = $this->pendingWriteAndCallbackCallers();
3237 if ( $danglingWriters ) {
3238 $fnames = implode( ', ', $danglingWriters );
3239 trigger_error( "DB transaction writes or callbacks still pending ($fnames)" );
3242 if ( $this->conn
) {
3243 // Avoid connection leaks. Normally, resources close at script completion.
3244 // The connection might already be closed in PHP by now, so suppress warnings.
3245 AtEase
::suppressWarnings();
3246 $this->closeConnection();
3247 AtEase
::restoreWarnings();
3252 /* Start of methods delegated to DatabaseFlags. Avoid using them outside of rdbms library */
3254 public function setFlag( $flag, $remember = self
::REMEMBER_NOTHING
) {
3255 $this->flagsHolder
->setFlag( $flag, $remember );
3258 public function clearFlag( $flag, $remember = self
::REMEMBER_NOTHING
) {
3259 $this->flagsHolder
->clearFlag( $flag, $remember );
3262 public function restoreFlags( $state = self
::RESTORE_PRIOR
) {
3263 $this->flagsHolder
->restoreFlags( $state );
3266 public function getFlag( $flag ) {
3267 return $this->flagsHolder
->getFlag( $flag );
3270 /* End of methods delegated to DatabaseFlags. */
3272 /* Start of methods delegated to TransactionManager. Avoid using them outside of rdbms library */
3274 final public function trxLevel() {
3275 // FIXME: A lot of tests disable constructor leading to trx manager being
3276 // null and breaking, this is unacceptable but hopefully this should
3277 // happen less by moving these functions to the transaction manager class.
3278 if ( !$this->transactionManager
) {
3279 $this->transactionManager
= new TransactionManager( new NullLogger() );
3281 return $this->transactionManager
->trxLevel();
3284 public function trxTimestamp() {
3285 return $this->transactionManager
->trxTimestamp();
3288 public function trxStatus() {
3289 return $this->transactionManager
->trxStatus();
3292 public function writesPending() {
3293 return $this->transactionManager
->writesPending();
3296 public function writesOrCallbacksPending() {
3297 return $this->transactionManager
->writesOrCallbacksPending();
3300 public function pendingWriteQueryDuration( $type = self
::ESTIMATE_TOTAL
) {
3301 return $this->transactionManager
->pendingWriteQueryDuration( $type );
3304 public function pendingWriteCallers() {
3305 if ( !$this->transactionManager
) {
3308 return $this->transactionManager
->pendingWriteCallers();
3311 public function pendingWriteAndCallbackCallers() {
3312 if ( !$this->transactionManager
) {
3315 return $this->transactionManager
->pendingWriteAndCallbackCallers();
3318 public function runOnTransactionPreCommitCallbacks() {
3319 return $this->transactionManager
->runOnTransactionPreCommitCallbacks( $this );
3322 public function explicitTrxActive() {
3323 return $this->transactionManager
->explicitTrxActive();
3326 /* End of methods delegated to TransactionManager. */
3328 /* Start of methods delegated to SQLPlatform. Avoid using them outside of rdbms library */
3330 public function implicitOrderby() {
3331 return $this->platform
->implicitOrderby();
3334 public function selectSQLText(
3335 $tables, $vars, $conds = '', $fname = __METHOD__
, $options = [], $join_conds = []
3337 return $this->platform
->selectSQLText( $tables, $vars, $conds, $fname, $options, $join_conds );
3340 public function buildComparison( string $op, array $conds ): string {
3341 return $this->platform
->buildComparison( $op, $conds );
3344 public function makeList( array $a, $mode = self
::LIST_COMMA
) {
3345 return $this->platform
->makeList( $a, $mode );
3348 public function makeWhereFrom2d( $data, $baseKey, $subKey ) {
3349 return $this->platform
->makeWhereFrom2d( $data, $baseKey, $subKey );
3352 public function factorConds( $condsArray ) {
3353 return $this->platform
->factorConds( $condsArray );
3356 public function bitNot( $field ) {
3357 return $this->platform
->bitNot( $field );
3360 public function bitAnd( $fieldLeft, $fieldRight ) {
3361 return $this->platform
->bitAnd( $fieldLeft, $fieldRight );
3364 public function bitOr( $fieldLeft, $fieldRight ) {
3365 return $this->platform
->bitOr( $fieldLeft, $fieldRight );
3368 public function buildConcat( $stringList ) {
3369 return $this->platform
->buildConcat( $stringList );
3372 public function buildGreatest( $fields, $values ) {
3373 return $this->platform
->buildGreatest( $fields, $values );
3376 public function buildLeast( $fields, $values ) {
3377 return $this->platform
->buildLeast( $fields, $values );
3380 public function buildSubstring( $input, $startPosition, $length = null ) {
3381 return $this->platform
->buildSubstring( $input, $startPosition, $length );
3384 public function buildStringCast( $field ) {
3385 return $this->platform
->buildStringCast( $field );
3388 public function buildIntegerCast( $field ) {
3389 return $this->platform
->buildIntegerCast( $field );
3392 public function tableName( string $name, $format = 'quoted' ) {
3393 return $this->platform
->tableName( $name, $format );
3396 public function tableNamesN( ...$tables ) {
3397 return $this->platform
->tableNamesN( ...$tables );
3400 public function addIdentifierQuotes( $s ) {
3401 return $this->platform
->addIdentifierQuotes( $s );
3404 public function isQuotedIdentifier( $name ) {
3405 return $this->platform
->isQuotedIdentifier( $name );
3408 public function buildLike( $param, ...$params ) {
3409 return $this->platform
->buildLike( $param, ...$params );
3412 public function anyChar() {
3413 return $this->platform
->anyChar();
3416 public function anyString() {
3417 return $this->platform
->anyString();
3420 public function limitResult( $sql, $limit, $offset = false ) {
3421 return $this->platform
->limitResult( $sql, $limit, $offset );
3424 public function unionSupportsOrderAndLimit() {
3425 return $this->platform
->unionSupportsOrderAndLimit();
3428 public function unionQueries( $sqls, $all, $options = [] ) {
3429 return $this->platform
->unionQueries( $sqls, $all, $options );
3432 public function conditional( $cond, $caseTrueExpression, $caseFalseExpression ) {
3433 return $this->platform
->conditional( $cond, $caseTrueExpression, $caseFalseExpression );
3436 public function strreplace( $orig, $old, $new ) {
3437 return $this->platform
->strreplace( $orig, $old, $new );
3440 public function timestamp( $ts = 0 ) {
3441 return $this->platform
->timestamp( $ts );
3444 public function timestampOrNull( $ts = null ) {
3445 return $this->platform
->timestampOrNull( $ts );
3448 public function getInfinity() {
3449 return $this->platform
->getInfinity();
3452 public function encodeExpiry( $expiry ) {
3453 return $this->platform
->encodeExpiry( $expiry );
3456 public function decodeExpiry( $expiry, $format = TS_MW
) {
3457 return $this->platform
->decodeExpiry( $expiry, $format );
3460 public function setTableAliases( array $aliases ) {
3461 $this->platform
->setTableAliases( $aliases );
3464 public function getTableAliases() {
3465 return $this->platform
->getTableAliases();
3468 public function setIndexAliases( array $aliases ) {
3469 $this->platform
->setIndexAliases( $aliases );
3472 public function buildGroupConcatField(
3473 $delim, $tables, $field, $conds = '', $join_conds = []
3475 return $this->platform
->buildGroupConcatField( $delim, $tables, $field, $conds, $join_conds );
3478 public function buildSelectSubquery(
3479 $tables, $vars, $conds = '', $fname = __METHOD__
,
3480 $options = [], $join_conds = []
3482 return $this->platform
->buildSelectSubquery( $tables, $vars, $conds, $fname, $options, $join_conds );
3485 public function buildExcludedValue( $column ) {
3486 return $this->platform
->buildExcludedValue( $column );
3489 public function setSchemaVars( $vars ) {
3490 $this->platform
->setSchemaVars( $vars );
3493 /* End of methods delegated to SQLPlatform. */
3495 /* Start of methods delegated to ReplicationReporter. */
3496 public function primaryPosWait( DBPrimaryPos
$pos, $timeout ) {
3497 return $this->replicationReporter
->primaryPosWait( $this, $pos, $timeout );
3500 public function getPrimaryPos() {
3501 return $this->replicationReporter
->getPrimaryPos( $this );
3504 public function getLag() {
3505 return $this->replicationReporter
->getLag( $this );
3508 public function getSessionLagStatus() {
3509 return $this->replicationReporter
->getSessionLagStatus( $this );
3512 /* End of methods delegated to ReplicationReporter. */