Unverified Commit 3004fc8a authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3384999 by mondrake, alexpott, daffie, smustgrave, quietone: Introduce...

Issue #3384999 by mondrake, alexpott, daffie, smustgrave, quietone: Introduce a Schema::executeDdlStatement method
parent 8fd18feb
Loading
Loading
Loading
Loading
Loading
+8 −5
Original line number Diff line number Diff line
@@ -34,6 +34,12 @@ public function __construct(Connection $connection, $table, array $options = [])
  /**
   * Executes the TRUNCATE query.
   *
   * In most cases, TRUNCATE is not a transaction safe statement as it is a DDL
   * statement which results in an implicit COMMIT. When we are in a
   * transaction, fallback to the slower, but transactional, DELETE.
   * PostgreSQL also locks the entire table for a TRUNCATE strongly reducing
   * the concurrency with other transactions.
   *
   * @return int|null
   *   Return value is dependent on whether the executed SQL statement is a
   *   TRUNCATE or a DELETE. TRUNCATE is DDL and no information on affected
@@ -66,11 +72,8 @@ public function __toString() {
    // Create a sanitized comment string to prepend to the query.
    $comments = $this->connection->makeComment($this->comments);

    // In most cases, TRUNCATE is not a transaction safe statement as it is a
    // DDL statement which results in an implicit COMMIT. When we are in a
    // transaction, fallback to the slower, but transactional, DELETE.
    // PostgreSQL also locks the entire table for a TRUNCATE strongly reducing
    // the concurrency with other transactions.
    // The statement actually built depends on whether a transaction is active.
    // @see ::execute()
    if ($this->connection->inTransaction()) {
      return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '}';
    }
+26 −1
Original line number Diff line number Diff line
@@ -117,6 +117,31 @@ public function prefixNonTable($table) {
    return implode('_', $args);
  }

  /**
   * Executes a data definition language (DDL) statement.
   *
   * This method allows to void an active transaction when the driver does
   * not support transactional DDL.
   *
   * @param string $sql
   *   The DDL statement to execute. This is a SQL string that may contain
   *   placeholders.
   * @param array $arguments
   *   (Optional) The associative array of arguments for the prepared
   *   statement.
   * @param array $options
   *   (Optional) An associative array of options to control how the query is
   *   run. The given options will be merged with self::defaultOptions().
   */
  protected function executeDdlStatement(string $sql, array $arguments = [], array $options = []): void {
    $this->connection->query($sql, $arguments, $options);
    // DDL statements when in a transaction force a commit in some databases.
    // Void the transaction in that case.
    if (!$this->connection->supportsTransactionalDDL() && $this->connection->transactionManager()->inTransaction()) {
      $this->connection->transactionManager()->voidClientTransaction();
    }
  }

  /**
   * Build a condition to match a table name against a standard information_schema.
   *
@@ -616,7 +641,7 @@ public function createTable($name, $table) {
    }
    $statements = $this->createTableSql($name, $table);
    foreach ($statements as $statement) {
      $this->connection->query($statement);
      $this->executeDdlStatement($statement);
    }
  }

+16 −14
Original line number Diff line number Diff line
@@ -5,21 +5,14 @@
/**
 * A wrapper class for creating and managing database transactions.
 *
 * Not all databases or database configurations support transactions. For
 * example, MySQL MyISAM tables do not. It is also easy to begin a transaction
 * and then forget to commit it, which can lead to connection errors when
 * another transaction is started.
 *
 * This class acts as a wrapper for transactions. To begin a transaction,
 * simply instantiate it. When the object goes out of scope and is destroyed
 * it will automatically commit. It also will check to see if the specified
 * connection supports transactions. If not, it will simply skip any transaction
 * commands, allowing user-space code to proceed normally. The only difference
 * is that rollbacks won't actually do anything.
 * To begin a transaction, simply start it. When the object goes out of scope
 * and is destroyed it will automatically commit.
 *
 * In the vast majority of cases, you should not instantiate this class
 * directly. Instead, call ->startTransaction(), from the appropriate connection
 * object.
 *
 * @see \Drupal\Core\Database\Connection::startTransaction()
 */
class Transaction {

@@ -34,6 +27,13 @@ public function __construct(
    Database::commitAllOnShutdown();
  }

  /**
   * Destructs the object.
   *
   * Depending on the nesting level of the object, this leads to a COMMIT (for
   * a root item) or to a RELEASE SAVEPOINT (for a savepoint item) executed on
   * the database.
   */
  public function __destruct() {
    $this->connection->transactionManager()->unpile($this->name, $this->id);
  }
@@ -49,11 +49,13 @@ public function name() {
   * Rolls back the current transaction.
   *
   * This is just a wrapper method to rollback whatever transaction stack we are
   * currently in, which is managed by the connection object itself. Note that
   * logging needs to happen after a transaction has been rolled back or the log
   * currently in, which is managed by the TransactionManager. Note that logging
   * needs to happen after a transaction has been rolled back or the log
   * messages will be rolled back too.
   *
   * @see \Drupal\Core\Database\Connection::rollBack()
   * Depending on the nesting level of the object, this leads to a ROLLBACK (for
   * a root item) or to a ROLLBACK TO SAVEPOINT (for a savepoint item) executed
   * on the database.
   */
  public function rollBack() {
    $this->connection->transactionManager()->rollback($this->name, $this->id);
+10 −1
Original line number Diff line number Diff line
@@ -87,6 +87,8 @@ abstract class TransactionManagerBase implements TransactionManagerInterface {
   * A list of post-transaction callbacks.
   *
   * @var callable[]
   *
   * @see \Drupal\Core\Database\Transaction\TransactionManagerInterface::addPostTransactionCallback()
   */
  private array $postTransactionCallbacks = [];

@@ -139,7 +141,6 @@ public function stackDepth(): int {
   * Drivers should not override this method unless they also override the
   * $stack property.
   *
   * phpcs:ignore Drupal.Commenting.FunctionComment.InvalidReturn
   * @return array<string,StackItem>
   *   The elements of the transaction stack.
   */
@@ -349,6 +350,14 @@ public function unpile(string $name, string $id): void {
   * {@inheritdoc}
   */
  public function rollback(string $name, string $id): void {
    // If the transaction was voided, we cannot rollback. Fail silently but
    // trigger a user warning.
    if ($this->getConnectionTransactionState() === ClientConnectionTransactionState::Voided) {
      $this->connectionTransactionState = ClientConnectionTransactionState::RollbackFailed;
      trigger_error('Transaction::rollBack() failed because of a prior execution of a DDL statement.', E_USER_WARNING);
      return;
    }

    // Rolled back item should match the last one in stack.
    if ($id != array_key_last($this->stack()) || $name !== $this->stack()[$id]->name) {
      throw new TransactionOutOfOrderException("Error attempting rollback of {$id}\\{$name}. Active stack: " . $this->dumpStackItemsAsString());
+22 −6
Original line number Diff line number Diff line
@@ -67,6 +67,8 @@ public function push(string $name = ''): Transaction;
   *   If a Drupal Transaction with the specified name does not exist.
   * @throws \Drupal\Core\Database\TransactionCommitFailedException
   *   If the commit of the root transaction failed.
   *
   * @see \Drupal\Core\Database\Transaction::__destruct()
   */
  public function unpile(string $name, string $id): void;

@@ -92,6 +94,8 @@ public function unpile(string $name, string $id): void;
   *   to the stack.
   * @throws \Drupal\Core\Database\TransactionCommitFailedException
   *   If the commit of the root transaction failed.
   *
   * @see \Drupal\Core\Database\Transaction::rollback()
   */
  public function rollback(string $name, string $id): void;

@@ -102,7 +106,7 @@ public function rollback(string $name, string $id): void;
   * database server (for example, MySql when a DDL statement is executed
   * during a transaction). In such cases we need to void the remaining items
   * on the stack so that when outliving Transaction object get out of scope
   * the do not try operations on the database.
   * they will not try operations on the database.
   *
   * This method should only be called internally by a database driver.
   */
@@ -111,9 +115,6 @@ public function voidClientTransaction(): void;
  /**
   * Adds a root transaction end callback.
   *
   * These callbacks are invoked immediately after the client transaction has
   * been committed or rolled back.
   *
   * It can for example be used to avoid deadlocks on write-heavy tables that
   * do not need to be part of the transaction, like cache tag invalidations.
   *
@@ -121,8 +122,23 @@ public function voidClientTransaction(): void;
   * and Memcache cache implementations can replicate the transaction-behavior
   * of the database cache backend and avoid race conditions.
   *
   * An argument is passed to the callbacks that indicates whether the
   * transaction was successful or not.
   * These callbacks are invoked during the destruction of the root Transaction
   * object.
   *
   * The callback should have the following signature:
   * @code
   *   callback(
   *     bool $success,
   *   ): void
   * @endcode
   *
   * When callbacks are executed, the $success parameter passed to the callbacks
   * is a boolean that indicates
   *   - if TRUE, that the complete transaction was successfully committed, or
   *     in the edge case of a transaction that was auto-committed after a DDL
   *     statement, that no rollbacks were attempted after the DDL statement;
   *   - if FALSE, that the complete transaction was rolled back, or that the
   *     transaction processing failed for any other reason.
   *
   * @param callable $callback
   *   The callback to invoke.
Loading