protected $password;
/** @var string */
protected $dbName;
- /** @var array[] $aliases Map of (table => (dbname, schema, prefix) map) */
+ /** @var array[] Map of (table => (dbname, schema, prefix) map) */
protected $tableAliases = [];
+ /** @var string[] Map of (index alias => index) */
+ protected $indexAliases = [];
/** @var bool Whether this PHP instance is for a CLI script */
protected $cliMode;
/** @var string Agent name for query profiling */
public function writesOrCallbacksPending() {
return $this->trxLevel && (
- $this->trxDoneWrites || $this->trxIdleCallbacks || $this->trxPreCommitCallbacks
+ $this->trxDoneWrites ||
+ $this->trxIdleCallbacks ||
+ $this->trxPreCommitCallbacks ||
+ $this->trxEndCallbacks
);
}
public function close() {
if ( $this->conn ) {
+ // Resolve any dangling transaction first
if ( $this->trxLevel() ) {
+ // Meaningful transactions should ideally have been resolved by now
+ if ( $this->writesOrCallbacksPending() ) {
+ $this->queryLogger->warning(
+ __METHOD__ . ": writes or callbacks still pending.",
+ [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
+ );
+ }
+ // Check if it is possible to properly commit and trigger callbacks
+ if ( $this->trxEndCallbacksSuppressed ) {
+ throw new DBUnexpectedError(
+ $this,
+ __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
+ );
+ }
+ // Commit the changes and run any callbacks as needed
$this->commit( __METHOD__, self::FLUSHING_INTERNAL );
}
-
+ // Close the actual connection in the binding handle
$closed = $this->closeConnection();
$this->conn = false;
- } elseif (
- $this->trxIdleCallbacks ||
- $this->trxPreCommitCallbacks ||
- $this->trxEndCallbacks
- ) { // sanity
- throw new RuntimeException( "Transaction callbacks still pending." );
+ // Sanity check that no callbacks are dangling
+ if (
+ $this->trxIdleCallbacks || $this->trxPreCommitCallbacks || $this->trxEndCallbacks
+ ) {
+ throw new RuntimeException( "Transaction callbacks still pending." );
+ }
} else {
- $closed = true;
+ $closed = true; // already closed; nothing to do
}
+
$this->opened = false;
return $closed;
$this->queryLogger->warning( $msg, $params +
[ 'trace' => ( new RuntimeException() )->getTraceAsString() ] );
- if ( !$recoverable ) {
- # Callers may catch the exception and continue to use the DB
- $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
- } else {
+ if ( $recoverable ) {
# Should be safe to silently retry the query
$ret = $this->doProfiledQuery( $sql, $commentedSql, $isNonTempWrite, $fname );
+ } else {
+ # Callers may catch the exception and continue to use the DB
+ $this->reportQueryError( $lastError, $lastErrno, $sql, $fname );
}
} else {
$msg = __METHOD__ . ': lost connection to {dbserver} permanently';
*/
private function handleSessionLoss() {
$this->trxLevel = 0;
- $this->trxIdleCallbacks = []; // T67263
- $this->trxPreCommitCallbacks = []; // T67263
+ $this->trxIdleCallbacks = []; // T67263; transaction already lost
+ $this->trxPreCommitCallbacks = []; // T67263; transaction already lost
$this->sessionTempTables = [];
$this->namedLocksHeld = [];
+
+ // Note: if callback suppression is set then some *Callbacks arrays are not cleared here
+ $e = null;
try {
// Handle callbacks in trxEndCallbacks
$this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+ } catch ( Exception $ex ) {
+ // Already logged; move on...
+ $e = $e ?: $ex;
+ }
+ try {
+ // Handle callbacks in trxRecurringCallbacks
$this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
- return null;
- } catch ( Exception $e ) {
+ } catch ( Exception $ex ) {
// Already logged; move on...
- return $e;
+ $e = $e ?: $ex;
}
+
+ return $e;
}
/**
* @return string
*/
protected function indexName( $index ) {
- return $index;
+ return isset( $this->indexAliases[$index] )
+ ? $this->indexAliases[$index]
+ : $index;
}
public function addQuotes( $s ) {
$this->tableAliases = $aliases;
}
+ public function setIndexAliases( array $aliases ) {
+ $this->indexAliases = $aliases;
+ }
+
/**
* @return bool Whether a DB user is required to access the DB
* @since 1.28