Issue #3364706 by mondrake, daffie, yustinTR: Refactor transactions

merge-requests/4659/head
catch 2023-08-31 09:13:37 +01:00
parent c90f093c11
commit a6b64f1635
16 changed files with 1114 additions and 140 deletions

View File

@ -68,7 +68,14 @@ trait CacheTagsChecksumTrait {
$in_transaction = $this->getDatabaseConnection()->inTransaction();
if ($in_transaction) {
if (empty($this->delayedTags)) {
$this->getDatabaseConnection()->addRootTransactionEndCallback([$this, 'rootTransactionEndCallback']);
// @todo in drupal:11.0.0, remove the conditional and only call the
// TransactionManager().
if ($this->getDatabaseConnection()->transactionManager()) {
$this->getDatabaseConnection()->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionEndCallback']);
}
else {
$this->getDatabaseConnection()->addRootTransactionEndCallback([$this, 'rootTransactionEndCallback']);
}
}
$this->delayedTags = Cache::mergeTags($this->delayedTags, $tags);
}

View File

@ -13,6 +13,7 @@ use Drupal\Core\Database\Query\Select;
use Drupal\Core\Database\Query\Truncate;
use Drupal\Core\Database\Query\Update;
use Drupal\Core\Database\Query\Upsert;
use Drupal\Core\Database\Transaction\TransactionManagerInterface;
use Drupal\Core\Pager\PagerManagerInterface;
/**
@ -62,6 +63,11 @@ abstract class Connection {
* transaction.
*
* @var array
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. The
* transaction stack is now managed by TransactionManager.
*
* @see https://www.drupal.org/node/3381002
*/
protected $transactionLayers = [];
@ -204,6 +210,11 @@ abstract class Connection {
* Post-root (non-nested) transaction commit callbacks.
*
* @var callable[]
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. The
* transaction end callbacks are now managed by TransactionManager.
*
* @see https://www.drupal.org/node/3381002
*/
protected $rootTransactionEndCallbacks = [];
@ -226,6 +237,11 @@ abstract class Connection {
*/
private array $enabledEvents = [];
/**
* The transaction manager.
*/
protected TransactionManagerInterface|FALSE $transactionManager;
/**
* Constructs a Connection object.
*
@ -276,6 +292,20 @@ abstract class Connection {
$this->connection = NULL;
}
/**
* Returns the client-level database connection object.
*
* This method should normally be used only within database driver code. Not
* doing so constitutes a risk of introducing code that is not database
* independent.
*
* @return object
* The client-level database connection, for example \PDO.
*/
public function getClientConnection(): object {
return $this->connection;
}
/**
* Returns the default query options for any given query.
*
@ -1337,6 +1367,41 @@ abstract class Connection {
return addcslashes($string, '\%_');
}
/**
* Returns the transaction manager.
*
* @return \Drupal\Core\Database\Transaction\TransactionManagerInterface|false
* The transaction manager, or FALSE if not available.
*/
public function transactionManager(): TransactionManagerInterface|FALSE {
if (!isset($this->transactionManager)) {
try {
$this->transactionManager = $this->driverTransactionManager();
}
catch (\LogicException $e) {
$this->transactionManager = FALSE;
}
}
return $this->transactionManager;
}
/**
* Returns a new instance of the driver's transaction manager.
*
* Database drivers must implement their own class extending from
* \Drupal\Core\Database\Transaction\TransactionManagerBase, and instantiate
* it here.
*
* @return \Drupal\Core\Database\Transaction\TransactionManagerInterface
* The transaction manager.
*
* @throws \LogicException
* If the transaction manager is undefined or unavailable.
*/
protected function driverTransactionManager(): TransactionManagerInterface {
throw new \LogicException('The database driver has no TransactionManager implementation');
}
/**
* Determines if there is an active transaction open.
*
@ -1344,7 +1409,13 @@ abstract class Connection {
* TRUE if we're currently in a transaction, FALSE otherwise.
*/
public function inTransaction() {
if ($this->transactionManager()) {
return $this->transactionManager()->inTransaction();
}
// Start of BC layer.
// @phpstan-ignore-next-line
return ($this->transactionDepth() > 0);
// End of BC layer.
}
/**
@ -1352,8 +1423,17 @@ abstract class Connection {
*
* @return int
* The current transaction depth.
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not
* access the transaction stack depth, it is an implementation detail.
*
* @see https://www.drupal.org/node/3381002
*/
public function transactionDepth() {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not access the transaction stack depth, it is an implementation detail. See https://www.drupal.org/node/3381002', E_USER_DEPRECATED);
if ($this->transactionManager()) {
return $this->transactionManager()->stackDepth();
}
return count($this->transactionLayers);
}
@ -1368,9 +1448,12 @@ abstract class Connection {
*
* @see \Drupal\Core\Database\Transaction
*
* @todo in drupal:11.0.0, return a new Transaction instance directly.
* @todo in drupal:11.0.0, push to the TransactionManager directly.
*/
public function startTransaction($name = '') {
if ($this->transactionManager()) {
return $this->transactionManager()->push($name);
}
$class = $this->getDriverClass('Transaction');
return new $class($this, $name);
}
@ -1388,8 +1471,18 @@ abstract class Connection {
* @throws \Drupal\Core\Database\TransactionNoActiveException
*
* @see \Drupal\Core\Database\Transaction::rollBack()
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not
* rollback the connection, roll back the Transaction objects instead.
*
* @see https://www.drupal.org/node/3381002
*/
public function rollBack($savepoint_name = 'drupal_transaction') {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not rollback the connection, roll back the Transaction objects instead. See https://www.drupal.org/node/3381002', E_USER_DEPRECATED);
if ($this->transactionManager()) {
$this->transactionManager()->rollback($savepoint_name);
return;
}
if (!$this->inTransaction()) {
throw new TransactionNoActiveException();
}
@ -1447,8 +1540,14 @@ abstract class Connection {
* @throws \Drupal\Core\Database\TransactionNameNonUniqueException
*
* @see \Drupal\Core\Database\Transaction
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use
* TransactionManagerInterface methods instead.
*
* @see https://www.drupal.org/node/3381002
*/
public function pushTransaction($name) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use TransactionManagerInterface methods instead. See https://www.drupal.org/node/3381002', E_USER_DEPRECATED);
if (isset($this->transactionLayers[$name])) {
throw new TransactionNameNonUniqueException($name . " is already in use.");
}
@ -1477,8 +1576,14 @@ abstract class Connection {
* @throws \Drupal\Core\Database\TransactionCommitFailedException
*
* @see \Drupal\Core\Database\Transaction
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use
* TransactionManagerInterface methods instead.
*
* @see https://www.drupal.org/node/3381002
*/
public function popTransaction($name) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use TransactionManagerInterface methods instead. See https://www.drupal.org/node/3381002', E_USER_DEPRECATED);
// The transaction has already been committed earlier. There is nothing we
// need to do. If this transaction was part of an earlier out-of-order
// rollback, an exception would already have been thrown by
@ -1512,8 +1617,18 @@ abstract class Connection {
* The callback to invoke.
*
* @see \Drupal\Core\Database\Connection::doCommit()
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use
* TransactionManagerInterface::addPostTransactionCallback() instead.
*
* @see https://www.drupal.org/node/3381002
*/
public function addRootTransactionEndCallback(callable $callback) {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use TransactionManagerInterface::addPostTransactionCallback() instead. See https://www.drupal.org/node/3381002', E_USER_DEPRECATED);
if ($this->transactionManager()) {
$this->transactionManager()->addPostTransactionCallback($callback);
return;
}
if (!$this->transactionLayers) {
throw new \LogicException('Root transaction end callbacks can only be added when there is an active transaction.');
}
@ -1524,8 +1639,14 @@ abstract class Connection {
* Commit all the transaction layers that can commit.
*
* @internal
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use
* TransactionManagerInterface methods instead.
*
* @see https://www.drupal.org/node/3381002
*/
protected function popCommittableTransactions() {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use TransactionManagerInterface methods instead. See https://www.drupal.org/node/3381002', E_USER_DEPRECATED);
// Commit all the committable layers.
foreach (array_reverse($this->transactionLayers) as $name => $active) {
// Stop once we found an active transaction.
@ -1548,8 +1669,14 @@ abstract class Connection {
* Do the actual commit, invoke post-commit callbacks.
*
* @internal
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use
* TransactionManagerInterface methods instead.
*
* @see https://www.drupal.org/node/3381002
*/
protected function doCommit() {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use TransactionManagerInterface methods instead. See https://www.drupal.org/node/3381002', E_USER_DEPRECATED);
$success = $this->connection->commit();
if (!empty($this->rootTransactionEndCallbacks)) {
$callbacks = $this->rootTransactionEndCallbacks;
@ -1687,8 +1814,14 @@ abstract class Connection {
* @throws \Drupal\Core\Database\TransactionExplicitCommitNotAllowedException
*
* @see \Drupal\Core\Database\Transaction
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not
* commit the connection, void the Transaction objects instead.
*
* @see https://www.drupal.org/node/3381002
*/
public function commit() {
@trigger_error(__METHOD__ . '() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not commit the connection, void the Transaction objects instead. See https://www.drupal.org/node/3381002', E_USER_DEPRECATED);
throw new TransactionExplicitCommitNotAllowedException();
}

View File

@ -48,9 +48,16 @@ class Transaction {
protected $name;
public function __construct(Connection $connection, $name = NULL) {
if ($connection->transactionManager()) {
$this->connection = $connection;
$this->name = $name;
return;
}
// Start of BC layer.
$this->connection = $connection;
// If there is no transaction depth, then no transaction has started. Name
// the transaction 'drupal_transaction'.
// @phpstan-ignore-next-line
if (!$depth = $connection->transactionDepth()) {
$this->name = 'drupal_transaction';
}
@ -62,14 +69,23 @@ class Transaction {
else {
$this->name = $name;
}
// @phpstan-ignore-next-line
$this->connection->pushTransaction($this->name);
// End of BC layer.
}
public function __destruct() {
if ($this->connection->transactionManager()) {
$this->connection->transactionManager()->unpile($this->name);
return;
}
// Start of BC layer.
// If we rolled back then the transaction would have already been popped.
if (!$this->rolledBack) {
// @phpstan-ignore-next-line
$this->connection->popTransaction($this->name);
}
// End of BC layer.
}
/**
@ -90,8 +106,15 @@ class Transaction {
* @see \Drupal\Core\Database\Connection::rollBack()
*/
public function rollBack() {
if ($this->connection->transactionManager()) {
$this->connection->transactionManager()->rollback($this->name);
return;
}
// Start of BC layer.
$this->rolledBack = TRUE;
// @phpstan-ignore-next-line
$this->connection->rollBack($this->name);
// End of BC layer.
}
}

View File

@ -0,0 +1,29 @@
<?php
/**
* @file
* Enumeration of the possible states of a client connection transaction.
*/
declare(strict_types=1);
namespace Drupal\Core\Database\Transaction;
/**
* Enumeration of the possible states of a client connection transaction.
*/
enum ClientConnectionTransactionState {
case Active;
case RolledBack;
case RollbackFailed;
case Committed;
case CommitFailed;
// In some cases the active transaction can be automatically committed by
// the database server (for example, MySql when a DDL statement is executed
// during a transaction). We track such cases with 'Voided' when we can
// detect them.
case Voided;
}

View File

@ -0,0 +1,20 @@
<?php
/**
* @file
* Enumeration of the types of items in the Drupal transaction stack.
*/
declare(strict_types=1);
namespace Drupal\Core\Database\Transaction;
/**
* Enumeration of the types of items in the Drupal transaction stack.
*/
enum StackItemType {
case Root;
case Savepoint;
}

View File

@ -0,0 +1,413 @@
<?php
declare(strict_types=1);
namespace Drupal\Core\Database\Transaction;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Transaction;
use Drupal\Core\Database\TransactionCommitFailedException;
use Drupal\Core\Database\TransactionNameNonUniqueException;
use Drupal\Core\Database\TransactionNoActiveException;
use Drupal\Core\Database\TransactionOutOfOrderException;
/**
* The database transaction manager base class.
*
* On many databases transactions cannot nest. Instead, we track nested calls
* to transactions and collapse them into a single client transaction.
*
* Database drivers must implement their own class extending from this, and
* instantiate it via their Connection::driverTransactionManager() method.
*
* @see \Drupal\Core\Database\Connection::driverTransactionManager()
*/
abstract class TransactionManagerBase implements TransactionManagerInterface {
/**
* The stack of Drupal transactions currently active.
*
* This is not a real LIFO (Last In, First Out) stack, where we would only
* remove the layers according to the order they were introduced. For commits
* the layer order is enforced, while for rollbacks the API allows to
* rollback to savepoints before the last one.
*
* @var array<string,StackItemType>
*/
private array $stack = [];
/**
* A list of Drupal transactions rolled back but not yet unpiled.
*
* @var array<string,true>
*/
private array $rollbacks = [];
/**
* A list of post-transaction callbacks.
*
* @var callable[]
*/
private array $postTransactionCallbacks = [];
/**
* The state of the underlying client connection transaction.
*
* Note that this is a proxy of the actual state on the database server,
* best determined through calls to methods in this class. The actual
* state on the database server could be different.
*/
private ClientConnectionTransactionState $connectionTransactionState;
/**
* Constructor.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
*/
public function __construct(
protected readonly Connection $connection,
) {
}
/**
* Returns the current depth of the transaction stack.
*
* @return int
* The current depth of the transaction stack.
*
* @todo consider making this function protected.
*
* @internal
*/
public function stackDepth(): int {
return count($this->stack());
}
/**
* Returns the content of the transaction stack.
*
* Drivers should not override this method unless they also override the
* $stack property.
*
* phpcs:ignore Drupal.Commenting.FunctionComment.InvalidReturn
* @return array<string,StackItemType>
* The elements of the transaction stack.
*/
protected function stack(): array {
return $this->stack;
}
/**
* Resets the transaction stack.
*
* Drivers should not override this method unless they also override the
* $stack property.
*/
protected function resetStack(): void {
$this->stack = [];
}
/**
* Adds an item to the transaction stack.
*
* Drivers should not override this method unless they also override the
* $stack property.
*
* @param string $name
* The name of the transaction.
* @param \Drupal\Core\Database\Transaction\StackItemType $type
* The stack item type.
*/
protected function addStackItem(string $name, StackItemType $type): void {
$this->stack[$name] = $type;
}
/**
* Removes an item from the transaction stack.
*
* Drivers should not override this method unless they also override the
* $stack property.
*
* @param string $name
* The name of the transaction.
*/
protected function removeStackItem(string $name): void {
unset($this->stack[$name]);
}
/**
* {@inheritdoc}
*/
public function inTransaction(): bool {
return (bool) $this->stackDepth() && $this->getConnectionTransactionState() === ClientConnectionTransactionState::Active;
}
/**
* {@inheritdoc}
*/
public function push(string $name = ''): Transaction {
if (!$this->inTransaction()) {
// If there is no transaction active, name the transaction
// 'drupal_transaction'.
$name = 'drupal_transaction';
}
elseif (!$name) {
// Within transactions, savepoints are used. Each savepoint requires a
// name. So if no name is present we need to create one.
$name = 'savepoint_' . $this->stackDepth();
}
if ($this->has($name)) {
throw new TransactionNameNonUniqueException($name . " is already in use.");
}
// Do the client-level processing.
if ($this->stackDepth() === 0) {
$this->beginClientTransaction();
$type = StackItemType::Root;
$this->setConnectionTransactionState(ClientConnectionTransactionState::Active);
}
else {
// If we're already in a Drupal transaction then we want to create a
// database savepoint, rather than try to begin another database
// transaction.
$this->addClientSavepoint($name);
$type = StackItemType::Savepoint;
}
// Push the transaction on the stack, increasing its depth.
$this->addStackItem($name, $type);
return new Transaction($this->connection, $name);
}
/**
* {@inheritdoc}
*/
public function unpile(string $name): void {
// If an already rolled back Drupal transaction, do nothing on the client
// connection, just cleanup the list of transactions rolled back.
if (isset($this->rollbacks[$name])) {
unset($this->rollbacks[$name]);
return;
}
if ($name !== 'drupal_transaction' && !$this->has($name)) {
throw new TransactionOutOfOrderException();
}
// Release the client transaction savepoint in case the Drupal transaction
// is not a root one.
if (
$this->has($name)
&& $this->stack()[$name] === StackItemType::Savepoint
&& $this->getConnectionTransactionState() === ClientConnectionTransactionState::Active
) {
$this->releaseClientSavepoint($name);
}
// Remove the transaction from the stack.
$this->removeStackItem($name);
// If this was the last Drupal transaction open, we can commit the client
// transaction.
if (
$this->stackDepth() === 0
&& $this->getConnectionTransactionState() === ClientConnectionTransactionState::Active
) {
$this->processRootCommit();
}
}
/**
* {@inheritdoc}
*/
public function rollback(string $name): void {
if (!$this->inTransaction()) {
throw new TransactionNoActiveException();
}
// Do the client-level processing.
match ($this->stack()[$name]) {
StackItemType::Root => $this->processRootRollback(),
StackItemType::Savepoint => $this->rollbackClientSavepoint($name),
};
// Rolled back item should match the last one in stack.
if ($name !== array_key_last($this->stack())) {
throw new TransactionOutOfOrderException();
}
$this->rollbacks[$name] = TRUE;
$this->removeStackItem($name);
// If this was the last Drupal transaction open, we can commit the client
// transaction.
if ($this->stackDepth() === 0 && $this->getConnectionTransactionState() === ClientConnectionTransactionState::Active) {
$this->processRootCommit();
}
}
/**
* {@inheritdoc}
*/
public function addPostTransactionCallback(callable $callback): void {
if (!$this->inTransaction()) {
throw new \LogicException('Root transaction end callbacks can only be added when there is an active transaction.');
}
$this->postTransactionCallbacks[] = $callback;
}
/**
* {@inheritdoc}
*/
public function has(string $name): bool {
return isset($this->stack()[$name]);
}
/**
* Sets the state of the client connection transaction.
*
* Note that this is a proxy of the actual state on the database server,
* best determined through calls to methods in this class. The actual
* state on the database server could be different.
*
* Drivers should not override this method unless they also override the
* $connectionTransactionState property.
*
* @param \Drupal\Core\Database\Transaction\ClientConnectionTransactionState $state
* The state of the client connection.
*/
protected function setConnectionTransactionState(ClientConnectionTransactionState $state): void {
$this->connectionTransactionState = $state;
}
/**
* Gets the state of the client connection transaction.
*
* Note that this is a proxy of the actual state on the database server,
* best determined through calls to methods in this class. The actual
* state on the database server could be different.
*
* Drivers should not override this method unless they also override the
* $connectionTransactionState property.
*
* @return \Drupal\Core\Database\Transaction\ClientConnectionTransactionState
* The state of the client connection.
*/
protected function getConnectionTransactionState(): ClientConnectionTransactionState {
return $this->connectionTransactionState;
}
/**
* Processes the root transaction rollback.
*/
protected function processRootRollback(): void {
$this->processPostTransactionCallbacks();
$this->rollbackClientTransaction();
}
/**
* Processes the root transaction commit.
*
* @throws \Drupal\Core\Database\TransactionCommitFailedException
* If the commit of the root transaction failed.
*/
protected function processRootCommit(): void {
$clientCommit = $this->commitClientTransaction();
$this->processPostTransactionCallbacks();
if (!$clientCommit) {
throw new TransactionCommitFailedException();
}
}
/**
* Processes the post-transaction callbacks.
*/
protected function processPostTransactionCallbacks(): void {
if (!empty($this->postTransactionCallbacks)) {
$callbacks = $this->postTransactionCallbacks;
$this->postTransactionCallbacks = [];
foreach ($callbacks as $callback) {
call_user_func($callback, $this->getConnectionTransactionState() === ClientConnectionTransactionState::Committed || $this->getConnectionTransactionState() === ClientConnectionTransactionState::Voided);
}
}
}
/**
* Begins a transaction on the client connection.
*
* @return bool
* Returns TRUE on success or FALSE on failure.
*/
abstract protected function beginClientTransaction(): bool;
/**
* Adds a savepoint on the client transaction.
*
* This is a generic implementation. Drivers should override this method
* to use a method specific for their client connection.
*
* @param string $name
* The name of the savepoint.
*
* @return bool
* Returns TRUE on success or FALSE on failure.
*/
protected function addClientSavepoint(string $name): bool {
$this->connection->query('SAVEPOINT ' . $name);
return TRUE;
}
/**
* Rolls back to a savepoint on the client transaction.
*
* This is a generic implementation. Drivers should override this method
* to use a method specific for their client connection.
*
* @param string $name
* The name of the savepoint.
*
* @return bool
* Returns TRUE on success or FALSE on failure.
*/
protected function rollbackClientSavepoint(string $name): bool {
$this->connection->query('ROLLBACK TO SAVEPOINT ' . $name);
return TRUE;
}
/**
* Releases a savepoint on the client transaction.
*
* This is a generic implementation. Drivers should override this method
* to use a method specific for their client connection.
*
* @param string $name
* The name of the savepoint.
*
* @return bool
* Returns TRUE on success or FALSE on failure.
*/
protected function releaseClientSavepoint(string $name): bool {
$this->connection->query('RELEASE SAVEPOINT ' . $name);
return TRUE;
}
/**
* Rolls back a client transaction.
*
* @return bool
* Returns TRUE on success or FALSE on failure.
*/
abstract protected function rollbackClientTransaction(): bool;
/**
* Commits a client transaction.
*
* @return bool
* Returns TRUE on success or FALSE on failure.
*/
abstract protected function commitClientTransaction(): bool;
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Drupal\Core\Database\Transaction;
use Drupal\Core\Database\Transaction;
/**
* Interface for the database transaction manager classes.
*/
interface TransactionManagerInterface {
/**
* Determines if there is an active transaction open.
*
* @return bool
* TRUE if we're currently in a transaction, FALSE otherwise.
*/
public function inTransaction(): bool;
/**
* Checks if a named Drupal transaction is active.
*
* @param string $name
* The name of the transaction.
*
* @return bool
* TRUE if the transaction is active, FALSE otherwise.
*/
public function has(string $name): bool;
/**
* Pushes a new Drupal transaction on the stack.
*
* This begins a client connection transaction if there is not one active,
* or adds a savepoint to the active one.
*
* @param string $name
* (optional) The name of the savepoint.
*
* @return \Drupal\Core\Database\Transaction
* A Transaction object.
*
* @throws \Drupal\Core\Database\TransactionNameNonUniqueException
* If a Drupal Transaction with the specified name exists already.
*/
public function push(string $name = ''): Transaction;
/**
* Removes a Drupal transaction from the stack.
*
* The unpiled item does not necessarily need to be the last on the stack.
* This method should only be called by a Transaction object going out of
* scope.
*
* @param string $name
* (optional) The name of the savepoint.
*
* @throws \Drupal\Core\Database\TransactionOutOfOrderException
* If a Drupal Transaction with the specified name does not exist.
* @throws \Drupal\Core\Database\TransactionCommitFailedException
* If the commit of the root transaction failed.
*/
public function unpile(string $name): void;
/**
* Rolls back a Drupal transaction.
*
* Rollbacks for nested transactions need to occur in reverse order to the
* pushes to the stack. Rolling back the last active Drupal transaction leads
* to rolling back the client connection (or to committing it in the edge
* case when the root was unpiled earlier).
*
* @param string $name
* (optional) The name of the savepoint.
*
* @throws \Drupal\Core\Database\TransactionNoActiveException
* If there is no active client connection.
* @throws \Drupal\Core\Database\TransactionOutOfOrderException
* If the order of rollback is not in reverse sequence against the pushes
* to the stack.
* @throws \Drupal\Core\Database\TransactionCommitFailedException
* If the commit of the root transaction failed.
*/
public function rollback(string $name): 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.
*
* Another use case is that services using alternative backends like Redis
* 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.
*
* @param callable $callback
* The callback to invoke.
*
* @throws \LogicException
* When a callback addition is attempted but no transaction is active.
*/
public function addPostTransactionCallback(callable $callback): void;
}

View File

@ -7,13 +7,11 @@ use Drupal\Core\Database\Database;
use Drupal\Core\Database\DatabaseAccessDeniedException;
use Drupal\Core\Database\DatabaseConnectionRefusedException;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\Core\Database\DatabaseNotFoundException;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\StatementWrapperIterator;
use Drupal\Core\Database\SupportsTemporaryTablesInterface;
use Drupal\Core\Database\Transaction;
use Drupal\Core\Database\TransactionNoActiveException;
use Drupal\Core\Database\Transaction\TransactionManagerInterface;
/**
* @addtogroup database
@ -410,109 +408,6 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn
}
}
/**
* Overridden to work around issues to MySQL not supporting transactional DDL.
*/
protected function popCommittableTransactions() {
// Commit all the committable layers.
foreach (array_reverse($this->transactionLayers) as $name => $active) {
// Stop once we found an active transaction.
if ($active) {
break;
}
// If there are no more layers left then we should commit.
unset($this->transactionLayers[$name]);
if (empty($this->transactionLayers)) {
$this->doCommit();
}
else {
// Attempt to release this savepoint in the standard way.
try {
$this->query('RELEASE SAVEPOINT ' . $name);
}
catch (DatabaseExceptionWrapper $e) {
// However, in MySQL (InnoDB), savepoints are automatically committed
// when tables are altered or created (DDL transactions are not
// supported). This can cause exceptions due to trying to release
// savepoints which no longer exist.
//
// To avoid exceptions when no actual error has occurred, we silently
// succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
if ($e->getPrevious()->errorInfo[1] == '1305') {
// If one SAVEPOINT was released automatically, then all were.
// Therefore, clean the transaction stack.
$this->transactionLayers = [];
// We also have to explain to PDO that the transaction stack has
// been cleaned-up.
$this->doCommit();
}
else {
throw $e;
}
}
}
}
}
/**
* {@inheritdoc}
*/
public function rollBack($savepoint_name = 'drupal_transaction') {
// MySQL will automatically commit transactions when tables are altered or
// created (DDL transactions are not supported). Prevent triggering an
// exception to ensure that the error that has caused the rollback is
// properly reported.
if (!$this->connection->inTransaction()) {
// On PHP 7 $this->connection->inTransaction() will return TRUE and
// $this->connection->rollback() does not throw an exception; the
// following code is unreachable.
// If \Drupal\Core\Database\Connection::rollBack() would throw an
// exception then continue to throw an exception.
if (!$this->inTransaction()) {
throw new TransactionNoActiveException();
}
// A previous rollback to an earlier savepoint may mean that the savepoint
// in question has already been accidentally committed.
if (!isset($this->transactionLayers[$savepoint_name])) {
throw new TransactionNoActiveException();
}
trigger_error('Rollback attempted when there is no active transaction. This can cause data integrity issues.', E_USER_WARNING);
return;
}
return parent::rollBack($savepoint_name);
}
/**
* {@inheritdoc}
*/
protected function doCommit() {
// MySQL will automatically commit transactions when tables are altered or
// created (DDL transactions are not supported). Prevent triggering an
// exception in this case as all statements have been committed.
if ($this->connection->inTransaction()) {
// On PHP 7 $this->connection->inTransaction() will return TRUE and
// $this->connection->commit() does not throw an exception.
$success = parent::doCommit();
}
else {
// Process the post-root (non-nested) transaction commit callbacks. The
// following code is copied from
// \Drupal\Core\Database\Connection::doCommit()
$success = TRUE;
if (!empty($this->rootTransactionEndCallbacks)) {
$callbacks = $this->rootTransactionEndCallbacks;
$this->rootTransactionEndCallbacks = [];
foreach ($callbacks as $callback) {
call_user_func($callback, $success);
}
}
}
return $success;
}
/**
* {@inheritdoc}
*/
@ -586,11 +481,18 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn
return new Condition($conjunction);
}
/**
* {@inheritdoc}
*/
protected function driverTransactionManager(): TransactionManagerInterface {
return new TransactionManager($this);
}
/**
* {@inheritdoc}
*/
public function startTransaction($name = '') {
return new Transaction($this, $name);
return $this->transactionManager()->push($name);
}
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Drupal\mysql\Driver\Database\mysql;
use Drupal\Core\Database\Transaction\ClientConnectionTransactionState;
use Drupal\Core\Database\Transaction\TransactionManagerBase;
/**
* MySql implementation of TransactionManagerInterface.
*
* MySQL will automatically commit transactions when tables are altered or
* created (DDL transactions are not supported). However, pdo_mysql tracks
* whether a client connection is still active and we can prevent triggering
* exceptions.
*/
class TransactionManager extends TransactionManagerBase {
/**
* {@inheritdoc}
*/
protected function beginClientTransaction(): bool {
return $this->connection->getClientConnection()->beginTransaction();
}
/**
* {@inheritdoc}
*/
protected function processRootCommit(): void {
if (!$this->connection->getClientConnection()->inTransaction()) {
$this->setConnectionTransactionState(ClientConnectionTransactionState::Voided);
$this->processPostTransactionCallbacks();
return;
}
parent::processRootCommit();
}
/**
* {@inheritdoc}
*/
protected function rollbackClientSavepoint(string $name): bool {
if (!$this->connection->getClientConnection()->inTransaction()) {
$this->resetStack();
$this->setConnectionTransactionState(ClientConnectionTransactionState::Voided);
$this->processPostTransactionCallbacks();
return TRUE;
}
return parent::rollbackClientSavepoint($name);
}
/**
* {@inheritdoc}
*/
protected function releaseClientSavepoint(string $name): bool {
if (!$this->connection->getClientConnection()->inTransaction()) {
$this->resetStack();
$this->setConnectionTransactionState(ClientConnectionTransactionState::Voided);
$this->processPostTransactionCallbacks();
return TRUE;
}
return parent::releaseClientSavepoint($name);
}
/**
* {@inheritdoc}
*/
protected function commitClientTransaction(): bool {
if (!$this->connection->getClientConnection()->inTransaction()) {
$this->setConnectionTransactionState(ClientConnectionTransactionState::Voided);
$this->processPostTransactionCallbacks();
return TRUE;
}
$clientCommit = $this->connection->getClientConnection()->commit();
$this->setConnectionTransactionState($clientCommit ?
ClientConnectionTransactionState::Committed :
ClientConnectionTransactionState::CommitFailed
);
return $clientCommit;
}
/**
* {@inheritdoc}
*/
protected function rollbackClientTransaction(): bool {
if (!$this->connection->getClientConnection()->inTransaction()) {
$this->setConnectionTransactionState(ClientConnectionTransactionState::Voided);
$this->processPostTransactionCallbacks();
trigger_error('Rollback attempted when there is no active transaction. This can cause data integrity issues.', E_USER_WARNING);
}
$clientRollback = $this->connection->getClientConnection()->rollBack();
$this->setConnectionTransactionState($clientRollback ?
ClientConnectionTransactionState::RolledBack :
ClientConnectionTransactionState::RollbackFailed
);
return $clientRollback;
}
}

View File

@ -11,7 +11,7 @@ use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Database\StatementWrapperIterator;
use Drupal\Core\Database\SupportsTemporaryTablesInterface;
use Drupal\Core\Database\Transaction;
use Drupal\Core\Database\Transaction\TransactionManagerInterface;
// cSpell:ignore ilike nextval
@ -72,6 +72,21 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn
*/
protected $identifierQuotes = ['"', '"'];
/**
* An array of transaction savepoints.
*
* The main use for this array is to store information about transaction
* savepoints opened to to mimic MySql's InnoDB functionality, which provides
* an inherent savepoint before any query in a transaction.
*
* @see ::addSavepoint()
* @see ::releaseSavepoint()
* @see ::rollbackSavepoint()
*
* @var array<string,Transaction>
*/
protected array $savepoints = [];
/**
* Constructs a connection object.
*/
@ -198,7 +213,7 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn
// - A 'mimic_implicit_commit' does not exist already.
// - The query is not a savepoint query.
$wrap_with_savepoint = $this->inTransaction() &&
!isset($this->transactionLayers['mimic_implicit_commit']) &&
!$this->transactionManager()->has('mimic_implicit_commit') &&
!(is_string($query) && (
stripos($query, 'ROLLBACK TO SAVEPOINT ') === 0 ||
stripos($query, 'RELEASE SAVEPOINT ') === 0 ||
@ -380,12 +395,10 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn
* @param $savepoint_name
* A string representing the savepoint name. By default,
* "mimic_implicit_commit" is used.
*
* @see Drupal\Core\Database\Connection::pushTransaction()
*/
public function addSavepoint($savepoint_name = 'mimic_implicit_commit') {
if ($this->inTransaction()) {
$this->pushTransaction($savepoint_name);
$this->savepoints[$savepoint_name] = $this->startTransaction($savepoint_name);
}
}
@ -395,12 +408,10 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn
* @param $savepoint_name
* A string representing the savepoint name. By default,
* "mimic_implicit_commit" is used.
*
* @see Drupal\Core\Database\Connection::popTransaction()
*/
public function releaseSavepoint($savepoint_name = 'mimic_implicit_commit') {
if (isset($this->transactionLayers[$savepoint_name])) {
$this->popTransaction($savepoint_name);
if ($this->inTransaction() && $this->transactionManager()->has($savepoint_name)) {
unset($this->savepoints[$savepoint_name]);
}
}
@ -412,8 +423,9 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn
* "mimic_implicit_commit" is used.
*/
public function rollbackSavepoint($savepoint_name = 'mimic_implicit_commit') {
if (isset($this->transactionLayers[$savepoint_name])) {
$this->rollBack($savepoint_name);
if ($this->inTransaction() && $this->transactionManager()->has($savepoint_name)) {
$this->savepoints[$savepoint_name]->rollBack();
unset($this->savepoints[$savepoint_name]);
}
}
@ -502,11 +514,18 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn
return new Condition($conjunction);
}
/**
* {@inheritdoc}
*/
protected function driverTransactionManager(): TransactionManagerInterface {
return new TransactionManager($this);
}
/**
* {@inheritdoc}
*/
public function startTransaction($name = '') {
return new Transaction($this, $name);
return $this->transactionManager()->push($name);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\pgsql\Driver\Database\pgsql;
use Drupal\Core\Database\Transaction\ClientConnectionTransactionState;
use Drupal\Core\Database\Transaction\TransactionManagerBase;
/**
* PostgreSql implementation of TransactionManagerInterface.
*/
class TransactionManager extends TransactionManagerBase {
/**
* {@inheritdoc}
*/
protected function beginClientTransaction(): bool {
return $this->connection->getClientConnection()->beginTransaction();
}
/**
* {@inheritdoc}
*/
protected function rollbackClientTransaction(): bool {
$clientRollback = $this->connection->getClientConnection()->rollBack();
$this->setConnectionTransactionState($clientRollback ?
ClientConnectionTransactionState::RolledBack :
ClientConnectionTransactionState::RollbackFailed
);
return $clientRollback;
}
/**
* {@inheritdoc}
*/
protected function commitClientTransaction(): bool {
$clientCommit = $this->connection->getClientConnection()->commit();
$this->setConnectionTransactionState($clientCommit ?
ClientConnectionTransactionState::Committed :
ClientConnectionTransactionState::CommitFailed
);
return $clientCommit;
}
}

View File

@ -9,7 +9,7 @@ use Drupal\Core\Database\ExceptionHandler;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Database\SupportsTemporaryTablesInterface;
use Drupal\Core\Database\Transaction;
use Drupal\Core\Database\Transaction\TransactionManagerInterface;
/**
* SQLite implementation of \Drupal\Core\Database\Connection.
@ -30,6 +30,11 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn
* Whether or not the active transaction (if any) will be rolled back.
*
* @var bool
*
* @deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. It is
* unused.
*
* @see https://www.drupal.org/node/3381002
*/
protected $willRollback;
@ -584,11 +589,18 @@ class Connection extends DatabaseConnection implements SupportsTemporaryTablesIn
return new Condition($conjunction);
}
/**
* {@inheritdoc}
*/
protected function driverTransactionManager(): TransactionManagerInterface {
return new TransactionManager($this);
}
/**
* {@inheritdoc}
*/
public function startTransaction($name = '') {
return new Transaction($this, $name);
return $this->transactionManager()->push($name);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Drupal\sqlite\Driver\Database\sqlite;
use Drupal\Core\Database\Transaction\ClientConnectionTransactionState;
use Drupal\Core\Database\Transaction\TransactionManagerBase;
/**
* SQLite implementation of TransactionManagerInterface.
*/
class TransactionManager extends TransactionManagerBase {
/**
* {@inheritdoc}
*/
protected function beginClientTransaction(): bool {
return $this->connection->getClientConnection()->beginTransaction();
}
/**
* {@inheritdoc}
*/
protected function rollbackClientTransaction(): bool {
$clientRollback = $this->connection->getClientConnection()->rollBack();
$this->setConnectionTransactionState($clientRollback ?
ClientConnectionTransactionState::RolledBack :
ClientConnectionTransactionState::RollbackFailed
);
return $clientRollback;
}
/**
* {@inheritdoc}
*/
protected function commitClientTransaction(): bool {
$clientCommit = $this->connection->getClientConnection()->commit();
$this->setConnectionTransactionState($clientCommit ?
ClientConnectionTransactionState::Committed :
ClientConnectionTransactionState::CommitFailed
);
return $clientCommit;
}
}

View File

@ -68,6 +68,14 @@ class EndOfTransactionQueriesTest extends KernelTestBase {
$executed_statements = [];
foreach (Database::getLog('testEntitySave') as $log) {
// Exclude transaction related statements from the log.
if (
str_starts_with($log['query'], 'ROLLBACK TO SAVEPOINT ') ||
str_starts_with($log['query'], 'RELEASE SAVEPOINT ') ||
str_starts_with($log['query'], 'SAVEPOINT ')
) {
continue;
}
$executed_statements[] = $log['query'];
}
$last_statement_index = max(array_keys($executed_statements));

View File

@ -130,7 +130,7 @@ class DeleteTruncateTest extends DatabaseTestBase {
// Roll back the transaction, and check that we are back to status before
// insert and truncate.
$this->connection->rollBack();
$transaction->rollBack();
$this->assertFalse($this->connection->inTransaction());
$num_records_after = $this->connection->select('test')->countQuery()->execute()->fetchField();
$this->assertEquals($num_records_before, $num_records_after);

View File

@ -2,9 +2,10 @@
namespace Drupal\KernelTests\Core\Database;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Database\TransactionOutOfOrderException;
use Drupal\Core\Database\TransactionExplicitCommitNotAllowedException;
use Drupal\Core\Database\TransactionNameNonUniqueException;
use Drupal\Core\Database\TransactionNoActiveException;
use Drupal\Core\Database\TransactionOutOfOrderException;
use PHPUnit\Framework\Error\Warning;
/**
@ -34,6 +35,11 @@ use PHPUnit\Framework\Error\Warning;
*/
class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
/**
* Keeps track of the post-transaction callback action executed.
*/
protected ?string $postTransactionCallbackAction = NULL;
/**
* Encapsulates a transaction's "inner layer" with an "outer layer".
*
@ -57,7 +63,7 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
* Whether to execute a DDL statement during the inner transaction.
*/
protected function transactionOuterLayer($suffix, $rollback = FALSE, $ddl_statement = FALSE) {
$depth = $this->connection->transactionDepth();
$depth = $this->connection->transactionManager()->stackDepth();
$txn = $this->connection->startTransaction();
// Insert a single row into the testing table.
@ -80,7 +86,7 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
// Roll back the transaction, if requested.
// This rollback should propagate to the last savepoint.
$txn->rollBack();
$this->assertSame($depth, $this->connection->transactionDepth(), 'Transaction has rolled back to the last savepoint after calling rollBack().');
$this->assertSame($depth, $this->connection->transactionManager()->stackDepth(), 'Transaction has rolled back to the last savepoint after calling rollBack().');
}
}
@ -98,14 +104,14 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
* Whether to execute a DDL statement during the transaction.
*/
protected function transactionInnerLayer($suffix, $rollback = FALSE, $ddl_statement = FALSE) {
$depth = $this->connection->transactionDepth();
$depth = $this->connection->transactionManager()->stackDepth();
// Start a transaction. If we're being called from ->transactionOuterLayer,
// then we're already in a transaction. Normally, that would make starting
// a transaction here dangerous, but the database API handles this problem
// for us by tracking the nesting and avoiding the danger.
$txn = $this->connection->startTransaction();
$depth2 = $this->connection->transactionDepth();
$depth2 = $this->connection->transactionManager()->stackDepth();
$this->assertGreaterThan($depth, $depth2, 'Transaction depth has increased with new transaction.');
// Insert a single row into the testing table.
@ -138,7 +144,7 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
// Roll back the transaction, if requested.
// This rollback should propagate to the last savepoint.
$txn->rollBack();
$this->assertSame($depth, $this->connection->transactionDepth(), 'Transaction has rolled back to the last savepoint after calling rollBack().');
$this->assertSame($depth, $this->connection->transactionManager()->stackDepth(), 'Transaction has rolled back to the last savepoint after calling rollBack().');
}
}
@ -267,7 +273,7 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
try {
// Rollback the outer transaction.
$transaction->rollBack();
// @see \Drupal\mysql\Driver\Database\mysql\Connection::rollBack()
// @see \Drupal\mysql\Driver\Database\mysql\TransactionManager::rollbackClientTransaction()
$this->fail('Rolling back a transaction containing DDL should produce a warning.');
}
catch (Warning $warning) {
@ -313,6 +319,7 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
protected function cleanUp() {
$this->connection->truncate('test')
->execute();
$this->postTransactionCallbackAction = NULL;
}
/**
@ -326,11 +333,8 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
* @internal
*/
public function assertRowPresent(string $name, string $message = NULL): void {
if (!isset($message)) {
$message = new FormattableMarkup('Row %name is present.', ['%name' => $name]);
}
$present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField();
$this->assertTrue($present, $message);
$this->assertTrue($present, $message ?? "Row '{$name}' should be present, but it actually does not exist.");
}
/**
@ -344,11 +348,8 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
* @internal
*/
public function assertRowAbsent(string $name, string $message = NULL): void {
if (!isset($message)) {
$message = new FormattableMarkup('Row %name is absent.', ['%name' => $name]);
}
$present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField();
$this->assertFalse($present, $message);
$this->assertFalse($present, $message ?? "Row '{$name}' should be absent, but it actually exists.");
}
/**
@ -575,4 +576,108 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
$this->assertEquals('24', $saved_age);
}
/**
* Tests for transaction names.
*/
public function testTransactionName(): void {
$transaction = $this->connection->startTransaction();
$this->assertSame('drupal_transaction', $transaction->name());
$savepoint1 = $this->connection->startTransaction();
$this->assertSame('savepoint_1', $savepoint1->name());
$this->expectException(TransactionNameNonUniqueException::class);
$this->expectExceptionMessage("savepoint_1 is already in use.");
$savepointFailure = $this->connection->startTransaction('savepoint_1');
}
/**
* Tests that adding a post-transaction callback fails with no transaction.
*/
public function testRootTransactionEndCallbackAddedWithoutTransaction(): void {
$this->expectException(\LogicException::class);
$this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
}
/**
* Tests post-transaction callback executes after transaction commit.
*/
public function testRootTransactionEndCallbackCalledOnCommit(): void {
$this->cleanUp();
$transaction = $this->connection->startTransaction();
$this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
$this->insertRow('row');
$this->assertNull($this->postTransactionCallbackAction);
unset($transaction);
$this->assertSame('rtcCommit', $this->postTransactionCallbackAction);
$this->assertRowPresent('row');
$this->assertRowPresent('rtcCommit');
}
/**
* Tests post-transaction callback executes after transaction rollback.
*/
public function testRootTransactionEndCallbackCalledOnRollback(): void {
$this->cleanUp();
$transaction = $this->connection->startTransaction();
$this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
$this->insertRow('row');
$this->assertNull($this->postTransactionCallbackAction);
$transaction->rollBack();
$this->assertSame('rtcRollback', $this->postTransactionCallbackAction);
unset($transaction);
$this->assertRowAbsent('row');
// The row insert should be missing since the client rollback occurs after
// the processing of the callbacks.
$this->assertRowAbsent('rtcRollback');
}
/**
* A post-transaction callback for testing purposes.
*/
public function rootTransactionCallback(bool $success): void {
$this->postTransactionCallbackAction = $success ? 'rtcCommit' : 'rtcRollback';
$this->insertRow($this->postTransactionCallbackAction);
}
/**
* Tests deprecation of Connection methods.
*
* @group legacy
*/
public function testConnectionDeprecations(): void {
$this->cleanUp();
$transaction = $this->connection->startTransaction();
$this->expectDeprecation('Drupal\\Core\\Database\\Connection::transactionDepth() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not access the transaction stack depth, it is an implementation detail. See https://www.drupal.org/node/3381002');
$this->assertSame(1, $this->connection->transactionDepth());
$this->insertRow('row');
$this->expectDeprecation('Drupal\\Core\\Database\\Connection::rollBack() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not rollback the connection, roll back the Transaction objects instead. See https://www.drupal.org/node/3381002');
$this->connection->rollback();
$transaction = NULL;
$this->assertRowAbsent('row');
$this->cleanUp();
$transaction = $this->connection->startTransaction();
$this->expectDeprecation('Drupal\\Core\\Database\\Connection::addRootTransactionEndCallback() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use TransactionManagerInterface::addPostTransactionCallback() instead. See https://www.drupal.org/node/3381002');
$this->connection->addRootTransactionEndCallback([$this, 'rootTransactionCallback']);
$this->insertRow('row');
$this->expectDeprecation('Drupal\\Core\\Database\\Connection::commit() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Do not commit the connection, void the Transaction objects instead. See https://www.drupal.org/node/3381002');
try {
$this->connection->commit();
}
catch (TransactionExplicitCommitNotAllowedException $e) {
// Do nothing.
}
$transaction = NULL;
$this->assertRowPresent('row');
$this->cleanUp();
$this->expectDeprecation('Drupal\\Core\\Database\\Connection::pushTransaction() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use TransactionManagerInterface methods instead. See https://www.drupal.org/node/3381002');
$this->connection->pushTransaction('foo');
$this->expectDeprecation('Drupal\\Core\\Database\\Connection::popTransaction() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use TransactionManagerInterface methods instead. See https://www.drupal.org/node/3381002');
$this->expectDeprecation('Drupal\\Core\\Database\\Connection::popCommittableTransactions() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use TransactionManagerInterface methods instead. See https://www.drupal.org/node/3381002');
$this->expectDeprecation('Drupal\\Core\\Database\\Connection::doCommit() is deprecated in drupal:10.2.0 and is removed from drupal:11.0.0. Use TransactionManagerInterface methods instead. See https://www.drupal.org/node/3381002');
$this->connection->popTransaction('foo');
}
}