Issue #3364706 by mondrake, daffie, yustinTR: Refactor transactions
parent
c90f093c11
commit
a6b64f1635
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue