Loading core/lib/Drupal/Core/Database/Query/Truncate.php +8 −5 Original line number Diff line number Diff line Loading @@ -34,6 +34,12 @@ public function __construct(Connection $connection, $table, array $options = []) /** * Executes the TRUNCATE query. * * In most cases, TRUNCATE is not a transaction safe statement as it is a DDL * statement which results in an implicit COMMIT. When we are in a * transaction, fallback to the slower, but transactional, DELETE. * PostgreSQL also locks the entire table for a TRUNCATE strongly reducing * the concurrency with other transactions. * * @return int|null * Return value is dependent on whether the executed SQL statement is a * TRUNCATE or a DELETE. TRUNCATE is DDL and no information on affected Loading Loading @@ -66,11 +72,8 @@ public function __toString() { // Create a sanitized comment string to prepend to the query. $comments = $this->connection->makeComment($this->comments); // In most cases, TRUNCATE is not a transaction safe statement as it is a // DDL statement which results in an implicit COMMIT. When we are in a // transaction, fallback to the slower, but transactional, DELETE. // PostgreSQL also locks the entire table for a TRUNCATE strongly reducing // the concurrency with other transactions. // The statement actually built depends on whether a transaction is active. // @see ::execute() if ($this->connection->inTransaction()) { return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}'; } Loading core/lib/Drupal/Core/Database/Schema.php +26 −1 Original line number Diff line number Diff line Loading @@ -117,6 +117,31 @@ public function prefixNonTable($table) { return implode('_', $args); } /** * Executes a data definition language (DDL) statement. * * This method allows to void an active transaction when the driver does * not support transactional DDL. * * @param string $sql * The DDL statement to execute. This is a SQL string that may contain * placeholders. * @param array $arguments * (Optional) The associative array of arguments for the prepared * statement. * @param array $options * (Optional) An associative array of options to control how the query is * run. The given options will be merged with self::defaultOptions(). */ protected function executeDdlStatement(string $sql, array $arguments = [], array $options = []): void { $this->connection->query($sql, $arguments, $options); // DDL statements when in a transaction force a commit in some databases. // Void the transaction in that case. if (!$this->connection->supportsTransactionalDDL() && $this->connection->transactionManager()->inTransaction()) { $this->connection->transactionManager()->voidClientTransaction(); } } /** * Build a condition to match a table name against a standard information_schema. * Loading Loading @@ -616,7 +641,7 @@ public function createTable($name, $table) { } $statements = $this->createTableSql($name, $table); foreach ($statements as $statement) { $this->connection->query($statement); $this->executeDdlStatement($statement); } } Loading core/lib/Drupal/Core/Database/Transaction.php +16 −14 Original line number Diff line number Diff line Loading @@ -5,21 +5,14 @@ /** * A wrapper class for creating and managing database transactions. * * Not all databases or database configurations support transactions. For * example, MySQL MyISAM tables do not. It is also easy to begin a transaction * and then forget to commit it, which can lead to connection errors when * another transaction is started. * * This class acts as a wrapper for transactions. To begin a transaction, * simply instantiate it. When the object goes out of scope and is destroyed * it will automatically commit. It also will check to see if the specified * connection supports transactions. If not, it will simply skip any transaction * commands, allowing user-space code to proceed normally. The only difference * is that rollbacks won't actually do anything. * To begin a transaction, simply start it. When the object goes out of scope * and is destroyed it will automatically commit. * * In the vast majority of cases, you should not instantiate this class * directly. Instead, call ->startTransaction(), from the appropriate connection * object. * * @see \Drupal\Core\Database\Connection::startTransaction() */ class Transaction { Loading @@ -34,6 +27,13 @@ public function __construct( Database::commitAllOnShutdown(); } /** * Destructs the object. * * Depending on the nesting level of the object, this leads to a COMMIT (for * a root item) or to a RELEASE SAVEPOINT (for a savepoint item) executed on * the database. */ public function __destruct() { $this->connection->transactionManager()->unpile($this->name, $this->id); } Loading @@ -49,11 +49,13 @@ public function name() { * Rolls back the current transaction. * * This is just a wrapper method to rollback whatever transaction stack we are * currently in, which is managed by the connection object itself. Note that * logging needs to happen after a transaction has been rolled back or the log * currently in, which is managed by the TransactionManager. Note that logging * needs to happen after a transaction has been rolled back or the log * messages will be rolled back too. * * @see \Drupal\Core\Database\Connection::rollBack() * Depending on the nesting level of the object, this leads to a ROLLBACK (for * a root item) or to a ROLLBACK TO SAVEPOINT (for a savepoint item) executed * on the database. */ public function rollBack() { $this->connection->transactionManager()->rollback($this->name, $this->id); Loading core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php +10 −1 Original line number Diff line number Diff line Loading @@ -87,6 +87,8 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { * A list of post-transaction callbacks. * * @var callable[] * * @see \Drupal\Core\Database\Transaction\TransactionManagerInterface::addPostTransactionCallback() */ private array $postTransactionCallbacks = []; Loading Loading @@ -139,7 +141,6 @@ public function stackDepth(): int { * Drivers should not override this method unless they also override the * $stack property. * * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidReturn * @return array<string,StackItem> * The elements of the transaction stack. */ Loading Loading @@ -349,6 +350,14 @@ public function unpile(string $name, string $id): void { * {@inheritdoc} */ public function rollback(string $name, string $id): void { // If the transaction was voided, we cannot rollback. Fail silently but // trigger a user warning. if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Voided) { $this->connectionTransactionState = ClientConnectionTransactionState::RollbackFailed; trigger_error('Transaction::rollBack() failed because of a prior execution of a DDL statement.', E_USER_WARNING); return; } // Rolled back item should match the last one in stack. if ($id != array_key_last($this->stack()) || $name !== $this->stack()[$id]->name) { throw new TransactionOutOfOrderException("Error attempting rollback of {$id}\\{$name}. Active stack: " . $this->dumpStackItemsAsString()); Loading core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php +22 −6 Original line number Diff line number Diff line Loading @@ -67,6 +67,8 @@ public function push(string $name = ''): Transaction; * If a Drupal Transaction with the specified name does not exist. * @throws \Drupal\Core\Database\TransactionCommitFailedException * If the commit of the root transaction failed. * * @see \Drupal\Core\Database\Transaction::__destruct() */ public function unpile(string $name, string $id): void; Loading @@ -92,6 +94,8 @@ public function unpile(string $name, string $id): void; * to the stack. * @throws \Drupal\Core\Database\TransactionCommitFailedException * If the commit of the root transaction failed. * * @see \Drupal\Core\Database\Transaction::rollback() */ public function rollback(string $name, string $id): void; Loading @@ -102,7 +106,7 @@ public function rollback(string $name, string $id): void; * database server (for example, MySql when a DDL statement is executed * during a transaction). In such cases we need to void the remaining items * on the stack so that when outliving Transaction object get out of scope * the do not try operations on the database. * they will not try operations on the database. * * This method should only be called internally by a database driver. */ Loading @@ -111,9 +115,6 @@ public function voidClientTransaction(): void; /** * Adds a root transaction end callback. * * These callbacks are invoked immediately after the client transaction has * been committed or rolled back. * * It can for example be used to avoid deadlocks on write-heavy tables that * do not need to be part of the transaction, like cache tag invalidations. * Loading @@ -121,8 +122,23 @@ public function voidClientTransaction(): void; * and Memcache cache implementations can replicate the transaction-behavior * of the database cache backend and avoid race conditions. * * An argument is passed to the callbacks that indicates whether the * transaction was successful or not. * These callbacks are invoked during the destruction of the root Transaction * object. * * The callback should have the following signature: * @code * callback( * bool $success, * ): void * @endcode * * When callbacks are executed, the $success parameter passed to the callbacks * is a boolean that indicates * - if TRUE, that the complete transaction was successfully committed, or * in the edge case of a transaction that was auto-committed after a DDL * statement, that no rollbacks were attempted after the DDL statement; * - if FALSE, that the complete transaction was rolled back, or that the * transaction processing failed for any other reason. * * @param callable $callback * The callback to invoke. Loading Loading
core/lib/Drupal/Core/Database/Query/Truncate.php +8 −5 Original line number Diff line number Diff line Loading @@ -34,6 +34,12 @@ public function __construct(Connection $connection, $table, array $options = []) /** * Executes the TRUNCATE query. * * In most cases, TRUNCATE is not a transaction safe statement as it is a DDL * statement which results in an implicit COMMIT. When we are in a * transaction, fallback to the slower, but transactional, DELETE. * PostgreSQL also locks the entire table for a TRUNCATE strongly reducing * the concurrency with other transactions. * * @return int|null * Return value is dependent on whether the executed SQL statement is a * TRUNCATE or a DELETE. TRUNCATE is DDL and no information on affected Loading Loading @@ -66,11 +72,8 @@ public function __toString() { // Create a sanitized comment string to prepend to the query. $comments = $this->connection->makeComment($this->comments); // In most cases, TRUNCATE is not a transaction safe statement as it is a // DDL statement which results in an implicit COMMIT. When we are in a // transaction, fallback to the slower, but transactional, DELETE. // PostgreSQL also locks the entire table for a TRUNCATE strongly reducing // the concurrency with other transactions. // The statement actually built depends on whether a transaction is active. // @see ::execute() if ($this->connection->inTransaction()) { return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}'; } Loading
core/lib/Drupal/Core/Database/Schema.php +26 −1 Original line number Diff line number Diff line Loading @@ -117,6 +117,31 @@ public function prefixNonTable($table) { return implode('_', $args); } /** * Executes a data definition language (DDL) statement. * * This method allows to void an active transaction when the driver does * not support transactional DDL. * * @param string $sql * The DDL statement to execute. This is a SQL string that may contain * placeholders. * @param array $arguments * (Optional) The associative array of arguments for the prepared * statement. * @param array $options * (Optional) An associative array of options to control how the query is * run. The given options will be merged with self::defaultOptions(). */ protected function executeDdlStatement(string $sql, array $arguments = [], array $options = []): void { $this->connection->query($sql, $arguments, $options); // DDL statements when in a transaction force a commit in some databases. // Void the transaction in that case. if (!$this->connection->supportsTransactionalDDL() && $this->connection->transactionManager()->inTransaction()) { $this->connection->transactionManager()->voidClientTransaction(); } } /** * Build a condition to match a table name against a standard information_schema. * Loading Loading @@ -616,7 +641,7 @@ public function createTable($name, $table) { } $statements = $this->createTableSql($name, $table); foreach ($statements as $statement) { $this->connection->query($statement); $this->executeDdlStatement($statement); } } Loading
core/lib/Drupal/Core/Database/Transaction.php +16 −14 Original line number Diff line number Diff line Loading @@ -5,21 +5,14 @@ /** * A wrapper class for creating and managing database transactions. * * Not all databases or database configurations support transactions. For * example, MySQL MyISAM tables do not. It is also easy to begin a transaction * and then forget to commit it, which can lead to connection errors when * another transaction is started. * * This class acts as a wrapper for transactions. To begin a transaction, * simply instantiate it. When the object goes out of scope and is destroyed * it will automatically commit. It also will check to see if the specified * connection supports transactions. If not, it will simply skip any transaction * commands, allowing user-space code to proceed normally. The only difference * is that rollbacks won't actually do anything. * To begin a transaction, simply start it. When the object goes out of scope * and is destroyed it will automatically commit. * * In the vast majority of cases, you should not instantiate this class * directly. Instead, call ->startTransaction(), from the appropriate connection * object. * * @see \Drupal\Core\Database\Connection::startTransaction() */ class Transaction { Loading @@ -34,6 +27,13 @@ public function __construct( Database::commitAllOnShutdown(); } /** * Destructs the object. * * Depending on the nesting level of the object, this leads to a COMMIT (for * a root item) or to a RELEASE SAVEPOINT (for a savepoint item) executed on * the database. */ public function __destruct() { $this->connection->transactionManager()->unpile($this->name, $this->id); } Loading @@ -49,11 +49,13 @@ public function name() { * Rolls back the current transaction. * * This is just a wrapper method to rollback whatever transaction stack we are * currently in, which is managed by the connection object itself. Note that * logging needs to happen after a transaction has been rolled back or the log * currently in, which is managed by the TransactionManager. Note that logging * needs to happen after a transaction has been rolled back or the log * messages will be rolled back too. * * @see \Drupal\Core\Database\Connection::rollBack() * Depending on the nesting level of the object, this leads to a ROLLBACK (for * a root item) or to a ROLLBACK TO SAVEPOINT (for a savepoint item) executed * on the database. */ public function rollBack() { $this->connection->transactionManager()->rollback($this->name, $this->id); Loading
core/lib/Drupal/Core/Database/Transaction/TransactionManagerBase.php +10 −1 Original line number Diff line number Diff line Loading @@ -87,6 +87,8 @@ abstract class TransactionManagerBase implements TransactionManagerInterface { * A list of post-transaction callbacks. * * @var callable[] * * @see \Drupal\Core\Database\Transaction\TransactionManagerInterface::addPostTransactionCallback() */ private array $postTransactionCallbacks = []; Loading Loading @@ -139,7 +141,6 @@ public function stackDepth(): int { * Drivers should not override this method unless they also override the * $stack property. * * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidReturn * @return array<string,StackItem> * The elements of the transaction stack. */ Loading Loading @@ -349,6 +350,14 @@ public function unpile(string $name, string $id): void { * {@inheritdoc} */ public function rollback(string $name, string $id): void { // If the transaction was voided, we cannot rollback. Fail silently but // trigger a user warning. if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Voided) { $this->connectionTransactionState = ClientConnectionTransactionState::RollbackFailed; trigger_error('Transaction::rollBack() failed because of a prior execution of a DDL statement.', E_USER_WARNING); return; } // Rolled back item should match the last one in stack. if ($id != array_key_last($this->stack()) || $name !== $this->stack()[$id]->name) { throw new TransactionOutOfOrderException("Error attempting rollback of {$id}\\{$name}. Active stack: " . $this->dumpStackItemsAsString()); Loading
core/lib/Drupal/Core/Database/Transaction/TransactionManagerInterface.php +22 −6 Original line number Diff line number Diff line Loading @@ -67,6 +67,8 @@ public function push(string $name = ''): Transaction; * If a Drupal Transaction with the specified name does not exist. * @throws \Drupal\Core\Database\TransactionCommitFailedException * If the commit of the root transaction failed. * * @see \Drupal\Core\Database\Transaction::__destruct() */ public function unpile(string $name, string $id): void; Loading @@ -92,6 +94,8 @@ public function unpile(string $name, string $id): void; * to the stack. * @throws \Drupal\Core\Database\TransactionCommitFailedException * If the commit of the root transaction failed. * * @see \Drupal\Core\Database\Transaction::rollback() */ public function rollback(string $name, string $id): void; Loading @@ -102,7 +106,7 @@ public function rollback(string $name, string $id): void; * database server (for example, MySql when a DDL statement is executed * during a transaction). In such cases we need to void the remaining items * on the stack so that when outliving Transaction object get out of scope * the do not try operations on the database. * they will not try operations on the database. * * This method should only be called internally by a database driver. */ Loading @@ -111,9 +115,6 @@ public function voidClientTransaction(): void; /** * Adds a root transaction end callback. * * These callbacks are invoked immediately after the client transaction has * been committed or rolled back. * * It can for example be used to avoid deadlocks on write-heavy tables that * do not need to be part of the transaction, like cache tag invalidations. * Loading @@ -121,8 +122,23 @@ public function voidClientTransaction(): void; * and Memcache cache implementations can replicate the transaction-behavior * of the database cache backend and avoid race conditions. * * An argument is passed to the callbacks that indicates whether the * transaction was successful or not. * These callbacks are invoked during the destruction of the root Transaction * object. * * The callback should have the following signature: * @code * callback( * bool $success, * ): void * @endcode * * When callbacks are executed, the $success parameter passed to the callbacks * is a boolean that indicates * - if TRUE, that the complete transaction was successfully committed, or * in the edge case of a transaction that was auto-committed after a DDL * statement, that no rollbacks were attempted after the DDL statement; * - if FALSE, that the complete transaction was rolled back, or that the * transaction processing failed for any other reason. * * @param callable $callback * The callback to invoke. Loading