*/
namespace Wikimedia\Rdbms;
+use InvalidArgumentException;
use Wikimedia\ScopedCallback;
use RuntimeException;
-use UnexpectedValueException;
use stdClass;
/**
/** @var string Transaction is requested internally via DBO_TRX/startAtomic() */
const TRANSACTION_INTERNAL = 'implicit';
- /** @var string Transaction operation comes from service managing all DBs */
+ /** @var string Atomic section is not cancelable */
+ const ATOMIC_NOT_CANCELABLE = '';
+ /** @var string Atomic section is cancelable */
+ const ATOMIC_CANCELABLE = 'cancelable';
+
+ /** @var string Commit/rollback is from outside the IDatabase handle and connection manager */
+ const FLUSHING_ONE = '';
+ /** @var string Commit/rollback is from the connection manager for the IDatabase handle */
const FLUSHING_ALL_PEERS = 'flush';
- /** @var string Transaction operation comes from the database class internally */
+ /** @var string Commit/rollback is from the IDatabase handle internally */
const FLUSHING_INTERNAL = 'flush';
/** @var string Do not remember the prior flags */
const DBO_NOBUFFER = 2;
/** @var int Ignore query errors (internal use only!) */
const DBO_IGNORE = 4;
- /** @var int Autoatically start transaction on first query (work with ILoadBalancer rounds) */
+ /** @var int Automatically start a transaction before running a query if none is active */
const DBO_TRX = 8;
/** @var int Use DBO_TRX in non-CLI mode */
const DBO_DEFAULT = 16;
* Should return true if unsure.
*
* @return bool
+ * @deprecated Since 1.31; use lastDoneWrites()
*/
public function doneWrites();
public function writesPending();
/**
- * Returns true if there is a transaction open with possible write
+ * Returns true if there is a transaction/round open with possible write
* queries or transaction pre-commit/idle callbacks waiting on it to finish.
* This does *not* count recurring callbacks, e.g. from setTransactionListener().
*
public function getType();
/**
- * Open a connection to the database. Usually aborts on failure
+ * Open a new connection to the database (closing any existing one)
*
* @param string $server Database server host
* @param string $user Database user name
*/
public function lastError();
- /**
- * mysql_fetch_field() wrapper
- * Returns false if the field doesn't exist
- *
- * @param string $table Table name
- * @param string $field Field name
- *
- * @return Field
- */
- public function fieldInfo( $table, $field );
-
/**
* Get the number of rows affected by the last write query
* @see https://secure.php.net/mysql_affected_rows
public function getServerVersion();
/**
- * Closes a database connection.
- * if it is open : commits any open transactions
+ * Close the database connection
+ *
+ * This should only be called after any transactions have been resolved,
+ * aside from read-only transactions (assuming no callbacks are registered).
+ * If a transaction is still open anyway, it will be committed if possible.
*
* @throws DBError
* @return bool Operation success. true if already closed.
*/
public function close();
- /**
- * @param string $error Fallback error message, used if none is given by DB
- * @throws DBConnectionError
- */
- public function reportConnectionError( $error = 'Unknown error' );
-
/**
* Run an SQL query and return the result. Normally throws a DBQueryError
* on failure. If errors are ignored, returns false instead.
*
+ * If a connection loss is detected, then an attempt to reconnect will be made.
+ * For queries that involve no larger transactions or locks, they will be re-issued
+ * for convenience, provided the connection was re-established.
+ *
* In new code, the query wrappers select(), insert(), update(), delete(),
* etc. should be used where possible, since they give much better DBMS
* independence and automatically quote or validate user input in a variety
*/
public function query( $sql, $fname = __METHOD__, $tempIgnore = false );
- /**
- * Report a query error. Log the error, and if neither the object ignore
- * flag nor the $tempIgnore flag is set, throw a DBQueryError.
- *
- * @param string $error
- * @param int $errno
- * @param string $sql
- * @param string $fname
- * @param bool $tempIgnore
- * @throws DBQueryError
- */
- public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false );
-
/**
* Free a result object returned by query() or select(). It's usually not
* necessary to call this, just use unset() or let the variable holding
* This includes the user table in the query, with the alias "a" available
* for use in field names (e.g. a.user_name).
*
+ * A derived table, defined by the result of selectSQLText(), requires an alias
+ * key and a Subquery instance value which wraps the SQL query, for example:
+ *
+ * [ 'c' => new Subquery( 'SELECT ...' ) ]
+ *
* Joins using parentheses for grouping (since MediaWiki 1.31) may be
* constructed using nested arrays. For example,
*
* doing UNION queries, where the SQL text of each query is needed. In general,
* however, callers outside of Database classes should just use select().
*
+ * @see IDatabase::select()
+ *
* @param string|array $table Table name
* @param string|array $vars Field names
* @param string|array $conds Conditions
* @param string $fname Caller function name
* @param string|array $options Query options
* @param string|array $join_conds Join conditions
- *
- * @return string SQL query string.
- * @see IDatabase::select()
+ * @return string SQL query string
*/
public function selectSQLText(
$table, $vars, $conds = '', $fname = __METHOD__,
* Takes the same arguments as IDatabase::select().
*
* @param string $table Table name
- * @param string $vars Unused
+ * @param string $var Column for which NULL values are not counted [default "*"]
* @param array|string $conds Filters on the table
* @param string $fname Function name for profiling
* @param array $options Options for select
+ * @param array|string $join_conds Join conditions
* @return int Row count
* @throws DBError
*/
public function estimateRowCount(
- $table, $vars = '*', $conds = '', $fname = __METHOD__, $options = []
+ $table, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
);
/**
* @since 1.27 Added $join_conds parameter
*
* @param array|string $tables Table names
- * @param string $vars Unused
+ * @param string $var Column for which NULL values are not counted [default "*"]
* @param array|string $conds Filters on the table
* @param string $fname Function name for profiling
* @param array $options Options for select
* @throws DBError
*/
public function selectRowCount(
- $tables, $vars = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
+ $tables, $var = '*', $conds = '', $fname = __METHOD__, $options = [], $join_conds = []
);
/**
*/
public function tableExists( $table, $fname = __METHOD__ );
- /**
- * Determines if a given index is unique
- *
- * @param string $table
- * @param string $index
- *
- * @return bool
- */
- public function indexUnique( $table, $index );
-
/**
* INSERT wrapper, inserts an array into a table.
*
* Example usage:
* @code
* $sql = $db->makeList( [
- * 'rev_user' => $id,
+ * 'rev_page' => $id,
* $db->makeList( [ 'rev_minor' => 1, 'rev_len' < 500 ], $db::LIST_OR ] )
* ], $db::LIST_AND );
* @endcode
- * This would set $sql to "rev_user = '$id' AND (rev_minor = '1' OR rev_len < '500')"
+ * This would set $sql to "rev_page = '$id' AND (rev_minor = '1' OR rev_len < '500')"
*
* @param array $a Containing the data
* @param int $mode IDatabase class constant:
$delim, $table, $field, $conds = '', $join_conds = []
);
+ /**
+ * Build a SUBSTRING function.
+ *
+ * Behavior for non-ASCII values is undefined.
+ *
+ * @param string $input Field name
+ * @param int $startPosition Positive integer
+ * @param int|null $length Non-negative integer length or null for no limit
+ * @throws InvalidArgumentException
+ * @return string SQL text
+ * @since 1.31
+ */
+ public function buildSubString( $input, $startPosition, $length = null );
+
/**
* @param string $field Field or column to cast
* @return string
*/
public function buildStringCast( $field );
+ /**
+ * @param string $field Field or column to cast
+ * @return string
+ * @since 1.31
+ */
+ public function buildIntegerCast( $field );
+
+ /**
+ * Equivalent to IDatabase::selectSQLText() except wraps the result in Subqyery
+ *
+ * @see IDatabase::selectSQLText()
+ *
+ * @param string|array $table Table name
+ * @param string|array $vars Field names
+ * @param string|array $conds Conditions
+ * @param string $fname Caller function name
+ * @param string|array $options Query options
+ * @param string|array $join_conds Join conditions
+ * @return Subquery
+ * @since 1.31
+ */
+ public function buildSelectSubquery(
+ $table, $vars, $conds = '', $fname = __METHOD__,
+ $options = [], $join_conds = []
+ );
+
/**
* Returns true if DBs are assumed to be on potentially different servers
*
* @param string $fname The function name of the caller, from __METHOD__
*
* @param array $insertOptions Options for the INSERT part of the query, see
- * IDatabase::insert() for details.
+ * IDatabase::insert() for details. Also, one additional option is
+ * available: pass 'NO_AUTO_COLUMNS' to hint that the query does not use
+ * an auto-increment or sequence to determine any column values.
* @param array $selectOptions Options for the SELECT part of the query, see
* IDatabase::select() for details.
* @param array $selectJoinConds Join conditions for the SELECT part of the query, see
/**
* Determines if the last failure was due to a deadlock
*
+ * Note that during a deadlock, the prior transaction will have been lost
+ *
* @return bool
*/
public function wasDeadlock();
/**
* Determines if the last failure was due to a lock timeout
*
+ * Note that during a lock wait timeout, the prior transaction will have been lost
+ *
* @return bool
*/
public function wasLockTimeout();
/**
- * Determines if the last query error was due to a dropped connection and should
- * be dealt with by pinging the connection and reissuing the query.
+ * Determines if the last query error was due to a dropped connection
+ *
+ * Note that during a connection loss, the prior transaction will have been lost
*
* @return bool
+ * @since 1.31
*/
- public function wasErrorReissuable();
+ public function wasConnectionLoss();
/**
* Determines if the last failure was due to the database being read-only.
*/
public function wasReadOnlyError();
+ /**
+ * Determines if the last query error was due to something outside of the query itself
+ *
+ * Note that the transaction may have been lost, discarding prior writes and results
+ *
+ * @return bool
+ */
+ public function wasErrorReissuable();
+
/**
* Wait for the replica DB to catch up to a given master position
*
/**
* Run a callback as soon as the current transaction commits or rolls back.
* An error is thrown if no transaction is pending. Queries in the function will run in
- * AUTO-COMMIT mode unless there are begin() calls. Callbacks must commit any transactions
+ * AUTOCOMMIT mode unless there are begin() calls. Callbacks must commit any transactions
* that they begin.
*
* This is useful for combining cooperative locks and DB transactions.
*
+ * @note: do not assume that *other* IDatabase instances will be AUTOCOMMIT mode
+ *
* The callback takes one argument:
* - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK)
*
/**
* Run a callback as soon as there is no transaction pending.
* If there is a transaction and it is rolled back, then the callback is cancelled.
- * Queries in the function will run in AUTO-COMMIT mode unless there are begin() calls.
+ *
+ * When transaction round mode (DBO_TRX) is set, the callback will run at the end
+ * of the round, just after all peer transactions COMMIT. If the transaction round
+ * is rolled back, then the callback is cancelled.
+ *
+ * Queries in the function will run in AUTOCOMMIT mode unless there are begin() calls.
* Callbacks must commit any transactions that they begin.
*
* This is useful for updates to different systems or when separate transactions are needed.
* For example, one might want to enqueue jobs into a system outside the database, but only
* after the database is updated so that the jobs will see the data when they actually run.
- * It can also be used for updates that easily cause deadlocks if locks are held too long.
+ * It can also be used for updates that easily suffer from lock timeouts and deadlocks,
+ * but where atomicity is not essential.
*
* Updates will execute in the order they were enqueued.
*
+ * @note: do not assume that *other* IDatabase instances will be AUTOCOMMIT mode
+ *
* The callback takes one argument:
* - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_IDLE)
*
/**
* Run a callback before the current transaction commits or now if there is none.
* If there is a transaction and it is rolled back, then the callback is cancelled.
+ *
+ * When transaction round mode (DBO_TRX) is set, the callback will run at the end
+ * of the round, just before all peer transactions COMMIT. If the transaction round
+ * is rolled back, then the callback is cancelled.
+ *
* Callbacks must not start nor commit any transactions. If no transaction is active,
* then a transaction will wrap the callback.
*
- * This is useful for updates that easily cause deadlocks if locks are held too long
+ * This is useful for updates that easily suffer from lock timeouts and deadlocks,
* but where atomicity is strongly desired for these updates and some related updates.
*
* Updates will execute in the order they were enqueued.
public function setTransactionListener( $name, callable $callback = null );
/**
- * Begin an atomic section of statements
+ * Begin an atomic section of SQL statements
+ *
+ * Start an implicit transaction if no transaction is already active, set a savepoint
+ * (if $cancelable is ATOMIC_CANCELABLE), and track the given section name to enforce
+ * that the transaction is not committed prematurely. The end of the section must be
+ * signified exactly once, either by endAtomic() or cancelAtomic(). Sections can have
+ * have layers of inner sections (sub-sections), but all sections must be ended in order
+ * of innermost to outermost. Transactions cannot be started or committed until all
+ * atomic sections are closed.
+ *
+ * ATOMIC_CANCELABLE is useful when the caller needs to handle specific failure cases
+ * by discarding the section's writes. This should not be used for failures when:
+ * - upsert() could easily be used instead
+ * - insert() with IGNORE could easily be used instead
+ * - select() with FOR UPDATE could be checked before issuing writes instead
+ * - The failure is from code that runs after the first write but doesn't need to
+ * - The failures are from contention solvable via onTransactionPreCommitOrIdle()
+ * - The failures are deadlocks; the RDBMs usually discard the whole transaction
*
- * If a transaction has been started already, just keep track of the given
- * section name to make sure the transaction is not committed pre-maturely.
- * This function can be used in layers (with sub-sections), so use a stack
- * to keep track of the different atomic sections. If there is no transaction,
- * start one implicitly.
+ * @note: callers must use additional measures for situations involving two or more
+ * (peer) transactions (e.g. updating two database servers at once). The transaction
+ * and savepoint logic of this method only applies to this specific IDatabase instance.
*
- * The goal of this function is to create an atomic section of SQL queries
- * without having to start a new transaction if it already exists.
+ * Example usage:
+ * @code
+ * // Start a transaction if there isn't one already
+ * $dbw->startAtomic( __METHOD__ );
+ * // Serialize these thread table updates
+ * $dbw->select( 'thread', '1', [ 'td_id' => $tid ], __METHOD__, 'FOR UPDATE' );
+ * // Add a new comment for the thread
+ * $dbw->insert( 'comment', $row, __METHOD__ );
+ * $cid = $db->insertId();
+ * // Update thread reference to last comment
+ * $dbw->update( 'thread', [ 'td_latest' => $cid ], [ 'td_id' => $tid ], __METHOD__ );
+ * // Demark the end of this conceptual unit of updates
+ * $dbw->endAtomic( __METHOD__ );
+ * @endcode
*
- * All atomic levels *must* be explicitly closed using IDatabase::endAtomic(),
- * and any database transactions cannot be began or committed until all atomic
- * levels are closed. There is no such thing as implicitly opening or closing
- * an atomic section.
+ * Example usage (atomic changes that might have to be discarded):
+ * @code
+ * // Start a transaction if there isn't one already
+ * $sectionId = $dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE );
+ * // Create new record metadata row
+ * $dbw->insert( 'records', $row, __METHOD__ );
+ * // Figure out where to store the data based on the new row's ID
+ * $path = $recordDirectory . '/' . $dbw->insertId();
+ * // Write the record data to the storage system
+ * $status = $fileBackend->create( [ 'dst' => $path, 'content' => $data ] );
+ * if ( $status->isOK() ) {
+ * // Try to cleanup files orphaned by transaction rollback
+ * $dbw->onTransactionResolution(
+ * function ( $type ) use ( $fileBackend, $path ) {
+ * if ( $type === IDatabase::TRIGGER_ROLLBACK ) {
+ * $fileBackend->delete( [ 'src' => $path ] );
+ * }
+ * },
+ * __METHOD__
+ * );
+ * // Demark the end of this conceptual unit of updates
+ * $dbw->endAtomic( __METHOD__ );
+ * } else {
+ * // Discard these writes from the transaction (preserving prior writes)
+ * $dbw->cancelAtomic( __METHOD__, $sectionId );
+ * }
+ * @endcode
*
* @since 1.23
* @param string $fname
+ * @param string $cancelable Pass self::ATOMIC_CANCELABLE to use a
+ * savepoint and enable self::cancelAtomic() for this section.
+ * @return AtomicSectionIdentifier section ID token
* @throws DBError
*/
- public function startAtomic( $fname = __METHOD__ );
+ public function startAtomic( $fname = __METHOD__, $cancelable = self::ATOMIC_NOT_CANCELABLE );
/**
* Ends an atomic section of SQL statements
public function endAtomic( $fname = __METHOD__ );
/**
- * Run a callback to do an atomic set of updates for this database
+ * Cancel an atomic section of SQL statements
+ *
+ * This will roll back only the statements executed since the start of the
+ * most recent atomic section, and close that section. If a transaction was
+ * open before the corresponding startAtomic() call, any statements before
+ * that call are *not* rolled back and the transaction remains open. If the
+ * corresponding startAtomic() implicitly started a transaction, that
+ * transaction is rolled back.
+ *
+ * @note: callers must use additional measures for situations involving two or more
+ * (peer) transactions (e.g. updating two database servers at once). The transaction
+ * and savepoint logic of startAtomic() are bound to specific IDatabase instances.
+ *
+ * Note that a call to IDatabase::rollback() will also roll back any open atomic sections.
+ *
+ * @note As a micro-optimization to save a few DB calls, this method may only
+ * be called when startAtomic() was called with the ATOMIC_CANCELABLE flag.
+ * @since 1.31
+ * @see IDatabase::startAtomic
+ * @param string $fname
+ * @param AtomicSectionIdentifier $sectionId Section ID from startAtomic();
+ * passing this enables cancellation of unclosed nested sections [optional]
+ * @throws DBError
+ */
+ public function cancelAtomic( $fname = __METHOD__, AtomicSectionIdentifier $sectionId = null );
+
+ /**
+ * Perform an atomic section of reversable SQL statements from a callback
*
* The $callback takes the following arguments:
* - This database object
* - The value of $fname
*
- * If any exception occurs in the callback, then rollback() will be called and the error will
- * be re-thrown. It may also be that the rollback itself fails with an exception before then.
- * In any case, such errors are expected to terminate the request, without any outside caller
- * attempting to catch errors and commit anyway. Note that any rollback undoes all prior
- * atomic section and uncommitted updates, which trashes the current request, requiring an
- * error to be displayed.
+ * This will execute the callback inside a pair of startAtomic()/endAtomic() calls.
+ * If any exception occurs during execution of the callback, it will be handled as follows:
+ * - If $cancelable is ATOMIC_CANCELABLE, cancelAtomic() will be called to back out any
+ * (and only) statements executed during the atomic section. If that succeeds, then the
+ * exception will be re-thrown; if it fails, then a different exception will be thrown
+ * and any further query attempts will fail until rollback() is called.
+ * - If $cancelable is ATOMIC_NOT_CANCELABLE, cancelAtomic() will be called to mark the
+ * end of the section and the error will be re-thrown. Any further query attempts will
+ * fail until rollback() is called.
+ *
+ * This method is convenient for letting calls to the caller of this method be wrapped
+ * in a try/catch blocks for exception types that imply that the caller failed but was
+ * able to properly discard the changes it made in the transaction. This method can be
+ * an alternative to explicit calls to startAtomic()/endAtomic()/cancelAtomic().
+ *
+ * Example usage, "RecordStore::save" method:
+ * @code
+ * $dbw->doAtomicSection( __METHOD__, function ( $dbw ) use ( $record ) {
+ * // Create new record metadata row
+ * $dbw->insert( 'records', $record->toArray(), __METHOD__ );
+ * // Figure out where to store the data based on the new row's ID
+ * $path = $this->recordDirectory . '/' . $dbw->insertId();
+ * // Write the record data to the storage system;
+ * // blob store throughs StoreFailureException on failure
+ * $this->blobStore->create( $path, $record->getJSON() );
+ * // Try to cleanup files orphaned by transaction rollback
+ * $dbw->onTransactionResolution(
+ * function ( $type ) use ( $path ) {
+ * if ( $type === IDatabase::TRIGGER_ROLLBACK ) {
+ * $this->blobStore->delete( $path );
+ * }
+ * },
+ * __METHOD__
+ * );
+ * }, $dbw::ATOMIC_CANCELABLE );
+ * @endcode
*
- * This can be an alternative to explicit startAtomic()/endAtomic() calls.
+ * Example usage, caller of the "RecordStore::save" method:
+ * @code
+ * $dbw->startAtomic( __METHOD__ );
+ * // ...various SQL writes happen...
+ * try {
+ * $recordStore->save( $record );
+ * } catch ( StoreFailureException $e ) {
+ * // ...various SQL writes happen...
+ * }
+ * // ...various SQL writes happen...
+ * $dbw->endAtomic( __METHOD__ );
+ * @endcode
*
* @see Database::startAtomic
* @see Database::endAtomic
+ * @see Database::cancelAtomic
*
* @param string $fname Caller name (usually __METHOD__)
* @param callable $callback Callback that issues DB updates
+ * @param string $cancelable Pass self::ATOMIC_CANCELABLE to use a
+ * savepoint and enable self::cancelAtomic() for this section.
* @return mixed $res Result of the callback (since 1.28)
* @throws DBError
* @throws RuntimeException
- * @throws UnexpectedValueException
- * @since 1.27
+ * @since 1.27; prior to 1.31 this did a rollback() instead of
+ * cancelAtomic(), and assumed no callers up the stack would ever try to
+ * catch the exception.
*/
- public function doAtomicSection( $fname, callable $callback );
+ public function doAtomicSection(
+ $fname, callable $callback, $cancelable = self::ATOMIC_NOT_CANCELABLE
+ );
/**
* Begin a transaction. If a transaction is already in progress,
*/
public function flushSnapshot( $fname = __METHOD__ );
- /**
- * List all tables on the database
- *
- * @param string $prefix Only show tables with this prefix, e.g. mw_
- * @param string $fname Calling function name
- * @throws DBError
- * @return array
- */
- public function listTables( $prefix = null, $fname = __METHOD__ );
-
/**
* Convert a timestamp in one of the formats accepted by wfTimestamp()
* to the format used for inserting into timestamp fields in this DBMS.
public function ping( &$rtt = null );
/**
- * Get replica DB lag. Currently supported only by MySQL.
+ * Get the amount of replication lag for this database server
*
- * Note that this function will generate a fatal error on many
- * installations. Most callers should use LoadBalancer::safeGetLag()
- * instead.
+ * Callers should avoid using this method while a transaction is active
*
* @return int|bool Database replication lag in seconds or false on error
* @throws DBError
* This is useful when transactions might use snapshot isolation
* (e.g. REPEATABLE-READ in innodb), so the "real" lag of that data
* is this lag plus transaction duration. If they don't, it is still
- * safe to be pessimistic. In AUTO-COMMIT mode, this still gives an
+ * safe to be pessimistic. In AUTOCOMMIT mode, this still gives an
* indication of the staleness of subsequent reads.
*
* @return array ('lag': seconds or false on error, 'since': UNIX timestamp of BEGIN)
public function setSchemaVars( $vars );
/**
- * Check to see if a named lock is available (non-blocking)
+ * Check to see if a named lock is not locked by any thread (non-blocking)
*
* @param string $lockName Name of lock to poll
* @param string $method Name of method calling us
* @since 1.28
*/
public function setTableAliases( array $aliases );
+
+ /**
+ * Convert certain index names to alternative names before querying the DB
+ *
+ * Note that this applies to indexes regardless of the table they belong to.
+ *
+ * This can be employed when an index was renamed X => Y in code, but the new Y-named
+ * indexes were not yet built on all DBs. After all the Y-named ones are added by the DBA,
+ * the aliases can be removed, and then the old X-named indexes dropped.
+ *
+ * @param string[] $aliases
+ * @return mixed
+ * @since 1.31
+ */
+ public function setIndexAliases( array $aliases );
}
class_alias( IDatabase::class, 'IDatabase' );