? postinstall.sh ? preinstall.sh ? trxn_per_connection.patch ? sites/default/files ? sites/default/settings.php Index: includes/database/database.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/database.inc,v retrieving revision 1.2 diff -u -r1.2 database.inc --- includes/database/database.inc 31 Aug 2008 09:12:35 -0000 1.2 +++ includes/database/database.inc 31 Aug 2008 15:27:25 -0000 @@ -124,7 +124,6 @@ * DELETE queries have a similar pattern. */ - /** * Base Database API class. * @@ -145,11 +144,32 @@ * @todo Remove this variable. */ public $lastStatement; + + /** + * Track the number of "layers" of transactions currently active. + * + * On many databases transactions cannot nest. Instead, we track + * nested calls to transactions and collapse them into a single + * transaction. + * + * @var int + */ + protected $transactionLayers; + + /** + * Whether or not the active transaction (if any) will be rolled back. + * + * @var boolean + */ + protected $willRollBack; function __construct($dsn, $username, $password, $driver_options = array()) { $driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION; // Because the other methods don't seem to work right. parent::__construct($dsn, $username, $password, $driver_options); $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('DatabaseStatement', array($this))); + + // We start in autocommit mode. + $this->transactionLayers = 0; } /** @@ -506,6 +526,13 @@ } /** + * Returns TRUE if we're currently in a transaction, FALSE otherwise. + */ + public function inTransaction() { + return ($this->transactionLayers > 0); + } + + /** * Returns a new DatabaseTransaction object on this connection. * * @param $required @@ -514,20 +541,92 @@ * this method will throw an exception and the operation will not be possible. * @see DatabaseTransaction */ - public function startTransaction($required = FALSE) { - static $class_type; - + public function beginTransaction($required = FALSE) { if ($required && !$this->supportsTransactions()) { throw new TransactionsNotSupportedException(); } - if (empty($class_type)) { - $class_type = 'DatabaseTransaction_' . $this->driver(); - if (!class_exists($class_type)) { - $class_type = 'DatabaseTransaction'; + return new DatabaseTransaction($this); + } + + /** + * Schedule the current transaction for rollback. Throws an exception if no + * transaction is active. + */ + public function rollBack() { + if ($this->transactionLayers == 0) { + throw new NoActiveTransactionException(); + } + + $this->willRollBack = TRUE; + } + + /** + * Determine if this transaction will roll back. Use this function to skip + * further operations if the current transaction is already scheduled to + * roll back. Throws an exception if no transaction is active. + * + * @return + * TRUE if the transaction will roll back, FALSE otherwise. + */ + public function willRollBack() { + if ($this->transactionLayers == 0) { + throw new NoActiveTransactionException(); + } + + return $this->willRollBack; + } + + /** + * Increases the depth of transaction nesting, beginning a transaction if one + * is not already active. + * + * @see DatabaseTransaction + */ + public function pushTransaction() { + ++$this->transactionLayers; + + if ($this->transactionLayers == 1) { + if ($this->supportsTransactions()) { + parent::beginTransaction(); + } + + // Reset any scheduled rollback + $this->willRollBack = FALSE; + } + } + + /** + * Decreases the depth of transaction nesting, committing or rolling back a + * transaction if we are on the outermost layer of transactions. Throws a + * NoActiveTransactionException exception if no transaction is active. + * + * @see DatabaseTransaction + */ + public function popTransaction() { + if ($this->transactionLayers == 0) { + throw new NoActiveTransactionException(); + } + + --$this->transactionLayers; + + if ($this->transactionLayers == 0 && $this->supportsTransactions()) { + if ($this->willRollBack) { + parent::rollBack(); + } + else { + parent::commit(); } } - return new $class_type($this); + } + + /** + * Throws an exception to deny direct access to transaction commits. + * + * @see DatabaseTransaction + */ + public function commit() { + throw new ExplicitTransactionsNotSupportedException(); } /** @@ -595,6 +694,11 @@ abstract public function supportsTransactions(); /** + * Determine if this driver supports transactional DDL. + */ + abstract public function supportsTransactionalDDL(); + + /** * Returns the type of the database being accessed. */ abstract public function databaseType(); @@ -856,7 +960,20 @@ class TransactionsNotSupportedException extends PDOException { } /** - * A wrapper class for creating and managing database transactions. + * Exception to throw when popTransaction() is called when no transaction is active. + */ +class NoActiveTransactionException extends PDOException { } + +/** + * Exception to deny attempts to explicitly manage transactions. + * + * This exception will be thrown when the PDO connection commit() is called. + * Code should never call this method directly. + */ +class ExplicitTransactionsNotSupportedException extends PDOException { } + +/** + * A wrapper class for managing database transaction nesting and lifetime. * * Not all databases or database configurations support transactions. For * example, MySQL MyISAM tables do not. It is also easy to begin a transaction @@ -865,107 +982,29 @@ * * 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. + * it will automatically commit or roll back. * * In the vast majority of cases, you should not instantiate this class directly. - * Instead, call ->startTransaction() from the appropriate connection object. + * Instead, call $connection->beginTransaction(), where $connection is a + * DatabaseConnection object. */ class DatabaseTransaction { /** - * The connection object for this transaction. + * A reference to the connection object for this transaction. * * @var DatabaseConnection */ protected $connection; - - /** - * Whether or not this connection supports transactions. - * - * This can be derived from the connection itself with a method call, - * but is cached here for performance. - * - * @var boolean - */ - protected $supportsTransactions; - - /** - * Whether or not this transaction has been rolled back. - * - * @var boolean - */ - protected $hasRolledBack = FALSE; - - /** - * Whether or not this transaction has been committed. - * - * @var boolean - */ - protected $hasCommitted = FALSE; - - /** - * Track the number of "layers" of transactions currently active. - * - * On many databases transactions cannot nest. Instead, we track - * nested calls to transactions and collapse them into a single - * transaction. - * - * @var int - */ - protected static $layers = 0; - - public function __construct(DatabaseConnection $connection) { - $this->connection = $connection; - $this->supportsTransactions = $connection->supportsTransactions(); - - if (self::$layers == 0 && $this->supportsTransactions) { - $connection->beginTransaction(); - } - - ++self::$layers; - } - - /** - * Commit this transaction. - */ - public function commit() { - --self::$layers; - if (self::$layers == 0 && $this->supportsTransactions) { - $this->connection->commit(); - $this->hasCommitted = TRUE; - } - } - - /** - * Roll back this transaction. - */ - public function rollBack() { - if ($this->supportsTransactions) { - $this->connection->rollBack(); - $this->hasRolledBack = TRUE; - } - } - - /** - * Determine if this transaction has already been rolled back. - * - * @return - * TRUE if the transaction has been rolled back, FALSE otherwise. - */ - public function hasRolledBack() { - return $this->hasRolledBack; + + public function __construct(DatabaseConnection &$connection) { + $this->connection = &$connection; + $this->connection->pushTransaction(); } public function __destruct() { - --self::$layers; - if (self::$layers == 0 && $this->supportsTransactions && !$this->hasRolledBack && !$this->hasCommitted) { - $this->connection->commit(); - } + $this->connection->popTransaction(); } - } /** Index: includes/database/mysql/database.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/mysql/database.inc,v retrieving revision 1.1 diff -u -r1.1 database.inc --- includes/database/mysql/database.inc 21 Aug 2008 19:36:36 -0000 1.1 +++ includes/database/mysql/database.inc 31 Aug 2008 15:27:25 -0000 @@ -56,6 +56,11 @@ public function supportsTransactions() { return $this->transactionSupport; } + + public function supportsTransactionalDDL() { + // No engine for MySQL supports transactional DDL. + return FALSE; + } public function escapeTable($table) { return preg_replace('/[^A-Za-z0-9_]+/', '', $table); Index: includes/database/pgsql/database.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/database/pgsql/database.inc,v retrieving revision 1.2 diff -u -r1.2 database.inc --- includes/database/pgsql/database.inc 22 Aug 2008 12:46:25 -0000 1.2 +++ includes/database/pgsql/database.inc 31 Aug 2008 15:27:25 -0000 @@ -100,6 +100,11 @@ return $this->transactionSupport; } + public function supportsTransactionalDDL() { + // We only want to use transactional DDL if overall transaction support is enabled. + return $this->transactionSupport; + } + public function escapeTable($table) { return preg_replace('/[^A-Za-z0-9_]+/', '', $table); } Index: modules/simpletest/tests/database_test.test =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/database_test.test,v retrieving revision 1.2 diff -u -r1.2 database_test.test --- modules/simpletest/tests/database_test.test 31 Aug 2008 15:05:29 -0000 1.2 +++ modules/simpletest/tests/database_test.test 31 Aug 2008 15:27:26 -0000 @@ -1533,3 +1533,89 @@ } } +/** + * Test more complex select statements. + */ +class DatabaseTransactionTestCase extends DatabaseTestCase { + + function getInfo() { + return array( + 'name' => t('Transaction tests'), + 'description' => t('Test the transaction abstraction system.'), + 'group' => t('Database'), + ); + } + + function transactionOuterLayer($suffix, $rollback = FALSE) { + $connection = Database::getActiveConnection('default'); + $txn = $connection->beginTransaction(TRUE); + + $query = db_insert('test'); + $query->fields(array( + 'name' => 'David' . $suffix, + 'age' => '24', + )); + $query->execute(); + + $this->assertTrue($connection->inTransaction(), t('In transaction before calling nested transaction.')); + + $this->transactionInnerLayer($suffix, $rollback); + + $this->assertTrue($connection->inTransaction(), t('In transaction after calling nested transaction.')); + } + + function transactionInnerLayer($suffix, $rollback = FALSE) { + $connection = Database::getActiveConnection('default'); + $txn = $connection->beginTransaction(TRUE); + + $query = db_insert('test'); + $query->fields(array( + 'name' => 'Daniel' . $suffix, + 'age' => '19', + )); + $query->execute(); + + $this->assertTrue($connection->inTransaction(), t('In transaction inside nested transaction.')); + + if ($rollback) { + $connection->rollBack(); + $this->assertTrue($connection->willRollBack(), t('Transaction is scheduled to roll back after calling rollBack().')); + } + } + + /** + * Test transaction rollback. + */ + function testTransactionRollBack() { + try { + $this->transactionOuterLayer('B', TRUE); + + $saved_age = db_query("SELECT age FROM {test} WHERE name = :name", array(':name' => 'DavidB'))->fetchField(); + $this->assertNotIdentical($saved_age, '24', t('Cannot retrieve DavidB row after commit.')); + + $saved_age = db_query("SELECT age FROM {test} WHERE name = :name", array(':name' => 'DanielB'))->fetchField(); + $this->assertNotIdentical($saved_age, '19', t('Cannot retrieve DanielB row after commit.')); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } + + /** + * Test committed transaction. + */ + function testCommittedTransaction() { + try { + $this->transactionOuterLayer('A'); + + $saved_age = db_query("SELECT age FROM {test} WHERE name = :name", array(':name' => 'DavidA'))->fetchField(); + $this->assertIdentical($saved_age, '24', t('Can retrieve DavidA row after commit.')); + + $saved_age = db_query("SELECT age FROM {test} WHERE name = :name", array(':name' => 'DanielA'))->fetchField(); + $this->assertIdentical($saved_age, '19', t('Can retrieve DanielA row after commit.')); + } + catch(Exception $e) { + $this->assertTrue(FALSE, $e->getMessage()); + } + } +} \ No newline at end of file