*/
final public function initConnection() {
if ( $this->isOpen() ) {
- throw new LogicException( __METHOD__ . ': already connected.' );
+ throw new LogicException( __METHOD__ . ': already connected' );
}
// Establish the connection
$this->doInitConnection();
$this->connectionParams['tablePrefix']
);
} else {
- throw new InvalidArgumentException( "No database user provided." );
+ throw new InvalidArgumentException( "No database user provided" );
}
}
public function dbSchema( $schema = null ) {
if ( strlen( $schema ) && $this->getDBname() === null ) {
- throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set." );
+ throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set" );
}
$old = $this->currentDomain->getSchema();
public function setFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
if ( ( $flag & self::DBO_IGNORE ) ) {
- throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
+ throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" );
}
if ( $remember === self::REMEMBER_PRIOR ) {
public function clearFlag( $flag, $remember = self::REMEMBER_NOTHING ) {
if ( ( $flag & self::DBO_IGNORE ) ) {
- throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed." );
+ throw new UnexpectedValueException( "Modifying DBO_IGNORE is not allowed" );
}
if ( $remember === self::REMEMBER_PRIOR ) {
$levels = $this->flatAtomicSectionList();
$exception = new DBUnexpectedError(
$this,
- __METHOD__ . ": atomic sections $levels are still open."
+ __METHOD__ . ": atomic sections $levels are still open"
);
} elseif ( $this->trxAutomatic ) {
// Only the connection manager can commit non-empty DBO_TRX transactions
$exception = new DBUnexpectedError(
$this,
__METHOD__ .
- ": mass commit/rollback of peer transaction required (DBO_TRX set)."
+ ": mass commit/rollback of peer transaction required (DBO_TRX set)"
);
}
} else {
// back, even if empty.
$exception = new DBUnexpectedError(
$this,
- __METHOD__ . ": transaction is still open (from {$this->trxFname})."
+ __METHOD__ . ": transaction is still open (from {$this->trxFname})"
);
}
if ( $this->trxEndCallbacksSuppressed ) {
$exception = $exception ?: new DBUnexpectedError(
$this,
- __METHOD__ . ': callbacks are suppressed; cannot properly commit.'
+ __METHOD__ . ': callbacks are suppressed; cannot properly commit'
);
}
$fnames = $this->pendingWriteAndCallbackCallers();
if ( $fnames ) {
throw new RuntimeException(
- "Transaction callbacks are still pending:\n" . implode( ', ', $fnames )
+ "Transaction callbacks are still pending: " . implode( ', ', $fnames )
);
}
}
*/
final protected function assertHasConnectionHandle() {
if ( !$this->isOpen() ) {
- throw new DBUnexpectedError( $this, "DB connection was already closed." );
+ throw new DBUnexpectedError( $this, "DB connection was already closed" );
}
}
if ( $this->getLBInfo( 'replica' ) === true ) {
throw new DBReadOnlyRoleError(
$this,
- 'Write operations are not allowed on replica database connections.'
+ 'Write operations are not allowed on replica database connections'
);
}
$reason = $this->getReadOnlyReason();
* For SELECT queries, this returns either:
* - a) A driver-specific value/resource, only on success. This can be iterated
* over by calling fetchObject()/fetchRow() until there are no more rows.
- * Alternatively, the result can be passed to resultObject() to obtain a
- * ResultWrapper instance which can then be iterated over via "foreach".
+ * Alternatively, the result can be passed to resultObject() to obtain an
+ * IResultWrapper instance which can then be iterated over via "foreach".
* - b) False, on any query failure
*
* For non-SELECT queries, this returns either:
protected function isTransactableQuery( $sql ) {
return !in_array(
$this->getQueryVerb( $sql ),
- [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE' ],
+ [ 'BEGIN', 'ROLLBACK', 'COMMIT', 'SET', 'SHOW', 'CREATE', 'ALTER', 'USE', 'SHOW' ],
true
);
}
/**
* @param string $sql A SQL query
* @param bool $pseudoPermanent Treat any table from CREATE TEMPORARY as pseudo-permanent
- * @return int|null A self::TEMP_* constant for temp table operations or null otherwise
+ * @return array A n-tuple of:
+ * - int|null: A self::TEMP_* constant for temp table operations or null otherwise
+ * - string|null: The name of the new temporary table $sql creates, or null
+ * - string|null: The name of the temporary table that $sql drops, or null
*/
- protected function registerTempTableWrite( $sql, $pseudoPermanent ) {
+ protected function getTempWrites( $sql, $pseudoPermanent ) {
static $qt = '[`"\']?(\w+)[`"\']?'; // quoted table
if ( preg_match(
$matches
) ) {
$type = $pseudoPermanent ? self::$TEMP_PSEUDO_PERMANENT : self::$TEMP_NORMAL;
- $this->sessionTempTables[$matches[1]] = $type;
- return $type;
+ return [ $type, $matches[1], null ];
} elseif ( preg_match(
'/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
$sql,
$matches
) ) {
- $type = $this->sessionTempTables[$matches[1]] ?? null;
- unset( $this->sessionTempTables[$matches[1]] );
-
- return $type;
+ return [ $this->sessionTempTables[$matches[1]] ?? null, null, $matches[1] ];
} elseif ( preg_match(
'/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
$sql,
$matches
) ) {
- return $this->sessionTempTables[$matches[1]] ?? null;
+ return [ $this->sessionTempTables[$matches[1]] ?? null, null, null ];
} elseif ( preg_match(
'/^(?:(?:INSERT|REPLACE)\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+' . $qt . '/i',
$sql,
$matches
) ) {
- return $this->sessionTempTables[$matches[1]] ?? null;
+ return [ $this->sessionTempTables[$matches[1]] ?? null, null, null ];
}
- return null;
+ return [ null, null, null ];
+ }
+
+ /**
+ * @param IResultWrapper|bool $ret
+ * @param int|null $tmpType TEMP_NORMAL or TEMP_PSEUDO_PERMANENT
+ * @param string|null $tmpNew Name of created temp table
+ * @param string|null $tmpDel Name of dropped temp table
+ */
+ protected function registerTempWrites( $ret, $tmpType, $tmpNew, $tmpDel ) {
+ if ( $ret !== false ) {
+ if ( $tmpNew !== null ) {
+ $this->sessionTempTables[$tmpNew] = $tmpType;
+ }
+ if ( $tmpDel !== null ) {
+ unset( $this->sessionTempTables[$tmpDel] );
+ }
+ }
}
public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
$priorTransaction = $this->trxLevel();
if ( $this->isWriteQuery( $sql ) ) {
- # In theory, non-persistent writes are allowed in read-only mode, but due to things
- # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
+ // In theory, non-persistent writes are allowed in read-only mode, but due to things
+ // like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
$this->assertIsWritableMaster();
- # Do not treat temporary table writes as "meaningful writes" since they are only
- # visible to one session and are not permanent. Profile them as reads. Integration
- # tests can override this behavior via $flags.
+ // Do not treat temporary table writes as "meaningful writes" since they are only
+ // visible to one session and are not permanent. Profile them as reads. Integration
+ // tests can override this behavior via $flags.
$pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
- $tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent );
- $isPermWrite = ( $tableType !== self::$TEMP_NORMAL );
- # DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
+ list( $tmpType, $tmpNew, $tmpDel ) = $this->getTempWrites( $sql, $pseudoPermanent );
+ $isPermWrite = ( $tmpType !== self::$TEMP_NORMAL );
+ // DBConnRef uses QUERY_REPLICA_ROLE to enforce the replica role for raw SQL queries
if ( $isPermWrite && $this->hasFlags( $flags, self::QUERY_REPLICA_ROLE ) ) {
throw new DBReadOnlyRoleError( $this, "Cannot write; target role is DB_REPLICA" );
}
} else {
+ // No permanent writes in this query
$isPermWrite = false;
+ // No temporary tables written to either
+ list( $tmpType, $tmpNew, $tmpDel ) = [ null, null, null ];
}
// Add trace comment to the begin of the sql string, right after the operator.
// Or, for one-word queries (like "BEGIN" or COMMIT") add it to the end (T44598)
$commentedSql = preg_replace( '/\s|$/', " /* $fname {$this->agent} */ ", $sql, 1 );
- // Send the query to the server and fetch any corresponding errors
+ // Send the query to the server and fetch any corresponding errors.
+ // This also doubles as a "ping" to see if the connection was dropped.
list( $ret, $err, $errno, $recoverableSR, $recoverableCL, $reconnected ) =
$this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
$this->executeQueryAttempt( $sql, $commentedSql, $isPermWrite, $fname, $flags );
}
+ // Register creation and dropping of temporary tables
+ $this->registerTempWrites( $ret, $tmpType, $tmpNew, $tmpDel );
+
$corruptedTrx = false;
if ( $ret === false ) {
private function assertQueryIsCurrentlyAllowed( $sql, $fname ) {
$verb = $this->getQueryVerb( $sql );
if ( $verb === 'USE' ) {
- throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead." );
+ throw new DBUnexpectedError( $this, "Got USE query; use selectDomain() instead" );
}
if ( $verb === 'ROLLBACK' ) { // transaction/savepoint
if ( $this->trxStatus < self::STATUS_TRX_OK ) {
throw new DBTransactionStateError(
$this,
- "Cannot execute query from $fname while transaction status is ERROR.",
+ "Cannot execute query from $fname while transaction status is ERROR",
[],
$this->trxStatusCause
);
*/
public function reportQueryError( $error, $errno, $sql, $fname, $ignore = false ) {
if ( $ignore ) {
- $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
+ $this->queryLogger->debug( "SQL ERROR (ignored): $error" );
} else {
$exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
'trace' => ( new RuntimeException() )->getTraceAsString()
] )
);
- $this->queryLogger->debug( "SQL ERROR: " . $error . "\n" );
+ $this->queryLogger->debug( "SQL ERROR: " . $error . "" );
if ( $this->wasQueryTimeout( $error, $errno ) ) {
$e = new DBQueryTimeoutError( $this, $error, $errno, $sql, $fname );
} elseif ( $this->wasConnectionError( $errno ) ) {
// functions. Discourage use of such queries to encourage compatibility.
call_user_func(
$this->deprecationLogger,
- __METHOD__ . ": aggregation used with a locking SELECT ($fname)."
+ __METHOD__ . ": aggregation used with a locking SELECT ($fname)"
);
}
} elseif ( count( $var ) == 1 ) {
$column = $var[0] ?? reset( $var );
} else {
- throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns.' );
+ throw new DBUnexpectedError( $this, __METHOD__ . ': got multiple columns' );
}
} else {
$column = $var;
if ( $name instanceof Subquery ) {
throw new DBUnexpectedError(
$this,
- __METHOD__ . ': got Subquery instance when expecting a string.'
+ __METHOD__ . ': got Subquery instance when expecting a string'
);
}
# surrounded by symbols which may be considered word breaks.
if ( preg_match( '/(^|\s)(DISTINCT|JOIN|ON|AS)(\s|$)/i', $name ) !== 0 ) {
$this->queryLogger->warning(
- __METHOD__ . ": use of subqueries is not supported this way.",
+ __METHOD__ . ": use of subqueries is not supported this way",
[ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
);
} elseif ( $table instanceof Subquery ) {
$quotedTable = (string)$table;
} else {
- throw new InvalidArgumentException( "Table must be a string or Subquery." );
+ throw new InvalidArgumentException( "Table must be a string or Subquery" );
}
if ( $alias === false || $alias === $table ) {
if ( $table instanceof Subquery ) {
- throw new InvalidArgumentException( "Subquery table missing alias." );
+ throw new InvalidArgumentException( "Subquery table missing alias" );
}
return $quotedTable;
public function limitResult( $sql, $limit, $offset = false ) {
if ( !is_numeric( $limit ) ) {
- throw new DBUnexpectedError( $this,
- "Invalid non-numeric limit passed to limitResult()\n" );
+ throw new DBUnexpectedError(
+ $this,
+ "Invalid non-numeric limit passed to " . __METHOD__
+ );
}
// This version works in MySQL and SQLite. It will very likely need to be
// overridden for most other RDBMS subclasses.
final public function onTransactionResolution( callable $callback, $fname = __METHOD__ ) {
if ( !$this->trxLevel() ) {
- throw new DBUnexpectedError( $this, "No transaction is active." );
+ throw new DBUnexpectedError( $this, "No transaction is active" );
}
$this->trxEndCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
}
final public function onAtomicSectionCancel( callable $callback, $fname = __METHOD__ ) {
if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
- throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
+ throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
}
$this->trxSectionCancelCallbacks[] = [ $callback, $fname, $this->currentAtomicSectionId() ];
}
*/
public function runOnTransactionIdleCallbacks( $trigger ) {
if ( $this->trxLevel() ) { // sanity
- throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open.' );
+ throw new DBUnexpectedError( $this, __METHOD__ . ': a transaction is still open' );
}
if ( $this->trxEndCallbacksSuppressed ) {
* Actually run any "atomic section cancel" callbacks.
*
* @param int $trigger IDatabase::TRIGGER_* constant
- * @param AtomicSectionIdentifier[]|null $sectionId Section IDs to cancel,
+ * @param AtomicSectionIdentifier[]|null $sectionIds Section IDs to cancel,
* null on transaction rollback
*/
private function runOnAtomicSectionCancelCallbacks(
final public function endAtomic( $fname = __METHOD__ ) {
if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
- throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
+ throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
}
// Check if the current section matches $fname
if ( $savedFname !== $fname ) {
throw new DBUnexpectedError(
$this,
- "Invalid atomic section ended (got $fname but expected $savedFname)."
+ "Invalid atomic section ended (got $fname but expected $savedFname)"
);
}
$fname = __METHOD__, AtomicSectionIdentifier $sectionId = null
) {
if ( !$this->trxLevel() || !$this->trxAtomicLevels ) {
- throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)." );
+ throw new DBUnexpectedError( $this, "No atomic section is open (got $fname)" );
}
$excisedIds = [];
if ( $savedFname !== $fname ) {
throw new DBUnexpectedError(
$this,
- "Invalid atomic section ended (got $fname but expected $savedFname)."
+ "Invalid atomic section ended (got $fname but expected $savedFname)"
);
}
$this->trxStatus = self::STATUS_TRX_ERROR;
$this->trxStatusCause = new DBUnexpectedError(
$this,
- "Uncancelable atomic section canceled (got $fname)."
+ "Uncancelable atomic section canceled (got $fname)"
);
}
} finally {
final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
static $modes = [ self::TRANSACTION_EXPLICIT, self::TRANSACTION_INTERNAL ];
if ( !in_array( $mode, $modes, true ) ) {
- throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'." );
+ throw new DBUnexpectedError( $this, "$fname: invalid mode parameter '$mode'" );
}
// Protect against mismatched atomic section, transaction nesting, and snapshot loss
if ( $this->trxLevel() ) {
if ( $this->trxAtomicLevels ) {
$levels = $this->flatAtomicSectionList();
- $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open.";
+ $msg = "$fname: Got explicit BEGIN while atomic section(s) $levels are open";
throw new DBUnexpectedError( $this, $msg );
} elseif ( !$this->trxAutomatic ) {
- $msg = "$fname: Explicit transaction already active (from {$this->trxFname}).";
+ $msg = "$fname: Explicit transaction already active (from {$this->trxFname})";
throw new DBUnexpectedError( $this, $msg );
} else {
- $msg = "$fname: Implicit transaction already active (from {$this->trxFname}).";
+ $msg = "$fname: Implicit transaction already active (from {$this->trxFname})";
throw new DBUnexpectedError( $this, $msg );
}
} elseif ( $this->getFlag( self::DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
- $msg = "$fname: Implicit transaction expected (DBO_TRX set).";
+ $msg = "$fname: Implicit transaction expected (DBO_TRX set)";
throw new DBUnexpectedError( $this, $msg );
}
final public function commit( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
static $modes = [ self::FLUSHING_ONE, self::FLUSHING_ALL_PEERS, self::FLUSHING_INTERNAL ];
if ( !in_array( $flush, $modes, true ) ) {
- throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'." );
+ throw new DBUnexpectedError( $this, "$fname: invalid flush parameter '$flush'" );
}
if ( $this->trxLevel() && $this->trxAtomicLevels ) {
$levels = $this->flatAtomicSectionList();
throw new DBUnexpectedError(
$this,
- "$fname: Got COMMIT while atomic sections $levels are still open."
+ "$fname: Got COMMIT while atomic sections $levels are still open"
);
}
} elseif ( !$this->trxAutomatic ) {
throw new DBUnexpectedError(
$this,
- "$fname: Flushing an explicit transaction, getting out of sync."
+ "$fname: Flushing an explicit transaction, getting out of sync"
);
}
} elseif ( !$this->trxLevel() ) {
$this->queryLogger->error(
- "$fname: No transaction to commit, something got out of sync." );
+ "$fname: No transaction to commit, something got out of sync" );
return; // nothing to do
} elseif ( $this->trxAutomatic ) {
throw new DBUnexpectedError(
$this,
- "$fname: Expected mass commit of all peer transactions (DBO_TRX set)."
+ "$fname: Expected mass commit of all peer transactions (DBO_TRX set)"
);
}
) {
throw new DBUnexpectedError(
$this,
- "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)."
+ "$fname: Expected mass rollback of all peer transactions (DBO_TRX set)"
);
}
}
}
- public function flushSnapshot( $fname = __METHOD__ ) {
- if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
+ public function flushSnapshot( $fname = __METHOD__, $flush = self::FLUSHING_ONE ) {
+ if ( $this->explicitTrxActive() ) {
+ // Committing this transaction would break callers that assume it is still open
+ throw new DBUnexpectedError(
+ $this,
+ "$fname: Cannot flush snapshot; " .
+ "explicit transaction '{$this->trxFname}' is still open"
+ );
+ } elseif ( $this->writesOrCallbacksPending() ) {
// This only flushes transactions to clear snapshots, not to write data
$fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
throw new DBUnexpectedError(
$this,
- "$fname: Cannot flush snapshot because writes are pending ($fnames)."
+ "$fname: Cannot flush snapshot; " .
+ "writes from transaction {$this->trxFname} are still pending ($fnames)"
+ );
+ } elseif (
+ $this->trxLevel() &&
+ $this->getTransactionRoundId() &&
+ $flush !== self::FLUSHING_INTERNAL &&
+ $flush !== self::FLUSHING_ALL_PEERS
+ ) {
+ $this->queryLogger->warning(
+ "$fname: Expected mass snapshot flush of all peer transactions " .
+ "in the explicit transactions round '{$this->getTransactionRoundId()}'",
+ [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
);
}
abstract protected function fetchAffectedRowCount();
/**
- * Take the result from a query, and wrap it in a ResultWrapper if
- * necessary. Boolean values are passed through as is, to indicate success
- * of write queries or failure.
+ * Take a query result and wrap it in an iterable result wrapper if necessary.
+ * Booleans are passed through as-is to indicate success/failure of write queries.
*
* Once upon a time, Database::query() returned a bare MySQL result
* resource, and it was necessary to call this function to convert it to
*/
protected function resultObject( $result ) {
if ( !$result ) {
- return false;
- } elseif ( $result instanceof ResultWrapper ) {
+ return false; // failed query
+ } elseif ( $result instanceof IResultWrapper ) {
return $result;
} elseif ( $result === true ) {
- // Successful write query
- return $result;
+ return $result; // succesful write query
} else {
return new ResultWrapper( $this, $result );
}
Wikimedia\restoreWarnings();
if ( $fp === false ) {
- throw new RuntimeException( "Could not open \"{$filename}\".\n" );
+ throw new RuntimeException( "Could not open \"{$filename}\"" );
}
if ( !$fname ) {
$fnames = implode( ', ', $this->pendingWriteAndCallbackCallers() );
throw new DBUnexpectedError(
$this,
- "$fname: Cannot flush pre-lock snapshot because writes are pending ($fnames)."
+ "$fname: Cannot flush pre-lock snapshot; " .
+ "writes from transaction {$this->trxFname} are still pending ($fnames)"
);
}
final public function lockTables( array $read, array $write, $method ) {
if ( $this->writesOrCallbacksPending() ) {
- throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending." );
+ throw new DBUnexpectedError( $this, "Transaction writes or callbacks still pending" );
}
if ( $this->tableLocksHaveTransactionScope() ) {
if ( !$this->conn ) {
throw new DBUnexpectedError(
$this,
- 'DB connection was already closed or the connection dropped.'
+ 'DB connection was already closed or the connection dropped'
);
}
*/
public function __clone() {
$this->connLogger->warning(
- "Cloning " . static::class . " is not recommended; forking connection:\n" .
- ( new RuntimeException() )->getTraceAsString()
+ "Cloning " . static::class . " is not recommended; forking connection",
+ [ 'trace' => ( new RuntimeException() )->getTraceAsString() ]
);
if ( $this->isOpen() ) {
*/
public function __sleep() {
throw new RuntimeException( 'Database serialization may cause problems, since ' .
- 'the connection is not restored on wakeup.' );
+ 'the connection is not restored on wakeup' );
}
/**
*/
public function __destruct() {
if ( $this->trxLevel() && $this->trxDoneWrites ) {
- trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})." );
+ trigger_error( "Uncommitted DB writes (transaction from {$this->trxFname})" );
}
$danglingWriters = $this->pendingWriteAndCallbackCallers();
if ( $danglingWriters ) {
$fnames = implode( ', ', $danglingWriters );
- trigger_error( "DB transaction writes or callbacks still pending ($fnames)." );
+ trigger_error( "DB transaction writes or callbacks still pending ($fnames)" );
}
if ( $this->conn ) {