Commit a7b4bdef authored by Dries's avatar Dries

- Patch #301049 by David Strauss, Josh Waihi, Crell, et al: transaction...

- Patch #301049 by David Strauss, Josh Waihi, Crell, et al: transaction nesting was not tracked by connection, better documentation, and better tests.
parent 9a32ca46
This diff is collapsed.
...@@ -16,6 +16,9 @@ class DatabaseConnection_mysql extends DatabaseConnection { ...@@ -16,6 +16,9 @@ class DatabaseConnection_mysql extends DatabaseConnection {
public function __construct(array $connection_options = array()) { public function __construct(array $connection_options = array()) {
// This driver defaults to non transaction support. // This driver defaults to non transaction support.
$this->transactionSupport = !empty($connection_option['transactions']); $this->transactionSupport = !empty($connection_option['transactions']);
// MySQL never supports transactional DDL.
$this->transactionalDDLSupport = FALSE;
// Default to TCP connection on port 3306. // Default to TCP connection on port 3306.
if (empty($connection_options['port'])) { if (empty($connection_options['port'])) {
......
...@@ -16,6 +16,10 @@ class DatabaseConnection_pgsql extends DatabaseConnection { ...@@ -16,6 +16,10 @@ class DatabaseConnection_pgsql extends DatabaseConnection {
public function __construct(array $connection_options = array()) { public function __construct(array $connection_options = array()) {
// This driver defaults to transaction support, except if explicitly passed FALSE. // This driver defaults to transaction support, except if explicitly passed FALSE.
$this->transactionSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] === FALSE; $this->transactionSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] === FALSE;
// Transactional DDL is always available in PostgreSQL,
// but we'll only enable it if standard transactions are.
$this->transactionalDDLSupport = $this->transactionSupport;
// Default to TCP connection on port 5432. // Default to TCP connection on port 5432.
if (empty($connection_options['port'])) { if (empty($connection_options['port'])) {
......
...@@ -2113,3 +2113,227 @@ class DatabaseQueryTestCase extends DatabaseTestCase { ...@@ -2113,3 +2113,227 @@ class DatabaseQueryTestCase extends DatabaseTestCase {
$this->assertEqual(count($names), 3, t('Correct number of names returned')); $this->assertEqual(count($names), 3, t('Correct number of names returned'));
} }
} }
/**
* Test transaction support, particularly nesting.
*
* We test nesting by having two transaction layers, an outer and inner. The
* outer layer encapsulates the inner layer. Our transaction nesting abstraction
* should allow the outer layer function to call any function it wants,
* especially the inner layer that starts its own transaction, and be
* confident that, when the function it calls returns, its own transaction
* is still "alive."
*
* Call structure:
* transactionOuterLayer()
* Start transaction
* transactionInnerLayer()
* Start transaction (does nothing in database)
* [Maybe decide to roll back]
* Do more stuff
* Should still be in transaction A
*
*/
class DatabaseTransactionTestCase extends DatabaseTestCase {
function getInfo() {
return array(
'name' => t('Transaction tests'),
'description' => t('Test the transaction abstraction system.'),
'group' => t('Database'),
);
}
/**
* Helper method for transaction unit test. This "outer layer" transaction
* starts and then encapsulates the "inner layer" transaction. This nesting
* is used to evaluate whether the the database transaction API properly
* supports nesting. By "properly supports," we mean the outer transaction
* continues to exist regardless of what functions are called and whether
* those functions start their own transactions.
*
* In contrast, a typical database would commit the outer transaction, start
* a new transaction for the inner layer, commit the inner layer transaction,
* and then be confused when the outer layer transaction tries to commit its
* transaction (which was already committed when the inner transaction
* started).
*
* @param $suffix
* Suffix to add to field values to differentiate tests.
* @param $rollback
* Whether or not to try rolling back the transaction when we're done.
*/
protected function transactionOuterLayer($suffix, $rollback = FALSE) {
$connection = Database::getActiveConnection();
$txn = db_transaction();
// Insert a single row into the testing table.
db_insert('test')
->fields(array(
'name' => 'David' . $suffix,
'age' => '24',
))
->execute();
$this->assertTrue($connection->inTransaction(), t('In transaction before calling nested transaction.'));
// We're already in a transaction, but we call ->transactionInnerLayer
// to nest another transaction inside the current one.
$this->transactionInnerLayer($suffix, $rollback);
$this->assertTrue($connection->inTransaction(), t('In transaction after calling nested transaction.'));
}
/**
* Helper method for transaction unit tests. This "inner layer" transaction
* is either used alone or nested inside of the "outer layer" transaction.
*
* @param $suffix
* Suffix to add to field values to differentiate tests.
* @param $rollback
* Whether or not to try rolling back the transaction when we're done.
*/
protected function transactionInnerLayer($suffix, $rollback = FALSE) {
$connection = Database::getActiveConnection();
// 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 = db_transaction();
// Insert a single row into the testing table.
db_insert('test')
->fields(array(
'name' => 'Daniel' . $suffix,
'age' => '19',
))
->execute();
$this->assertTrue($connection->inTransaction(), t('In transaction inside nested transaction.'));
if ($rollback) {
// Roll back the transaction, if requested.
// This rollback should propagate to the the outer transaction, if present.
$connection->rollBack();
$this->assertTrue($connection->willRollBack(), t('Transaction is scheduled to roll back after calling rollBack().'));
}
}
/**
* Test that a database that claims to support transactions will return a transaction object.
*
* If the active connection does not support transactions, this test does nothing.
*/
function testTransactionsSupported() {
try {
$connection = Database::getActiveConnection();
if ($connection->supportsTransactions()) {
// Start a "required" transaction. This should fail if we do
// this on a database that does not actually support transactions.
$txn = db_transaction(TRUE);
}
$this->pass('Transaction started successfully.');
}
catch (TransactionsNotSupportedException $e) {
$this->fail("Exception thrown when it shouldn't have been.");
}
}
/**
* Test that a database that doesn't support transactions fails correctly.
*
* If the active connection supports transactions, this test does nothing.
*/
function testTransactionsNotSupported() {
try {
$connection = Database::getActiveConnection();
if (!$connection->supportsTransactions()) {
// Start a "required" transaction. This should fail if we do this
// on a database that does not actually support transactions, and
// the current database does claim to NOT support transactions.
$txn = db_transaction(TRUE);
}
$this->fail('No transaction failure registered.');
}
catch (TransactionsNotSupportedException $e) {
$this->pass('Exception thrown for unsupported transactions.');
}
}
/**
* Test transaction rollback on a database that supports transactions.
*
* If the active connection does not support transactions, this test does nothing.
*/
function testTransactionRollBackSupported() {
// This test won't work right if transactions are not supported.
if (!Database::getActiveConnection()->supportsTransactions()) {
return;
}
try {
// Create two nested transactions. Roll back from the inner one.
$this->transactionOuterLayer('B', TRUE);
// Neither of the rows we inserted in the two transaction layers
// should be present in the tables post-rollback.
$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->fail($e->getMessage());
}
}
/**
* Test transaction rollback on a database that does not support transactions.
*
* If the active driver supports transactions, this test does nothing.
*/
function testTransactionRollBackNotSupported() {
// This test won't work right if transactions are supported.
if (Database::getActiveConnection()->supportsTransactions()) {
return;
}
try {
// Create two nested transactions. Attempt to roll back from the inner one.
$this->transactionOuterLayer('B', TRUE);
// Because our current database claims to not support transactions,
// the inserted rows should be present despite the attempt to roll back.
$saved_age = db_query("SELECT age FROM {test} WHERE name = :name", array(':name' => 'DavidB'))->fetchField();
$this->assertIdentical($saved_age, '24', t('DavidB not rolled back, since transactions are not supported.'));
$saved_age = db_query("SELECT age FROM {test} WHERE name = :name", array(':name' => 'DanielB'))->fetchField();
$this->assertIdentical($saved_age, '19', t('DanielB not rolled back, since transactions are not supported.'));
}
catch(Exception $e) {
$this->fail($e->getMessage());
}
}
/**
* Test committed transaction.
*
* The behavior of this test should be identical for connections that support
* transactions and those that do not.
*/
function testCommittedTransaction() {
try {
// Create two nested transactions. The changes should be committed.
$this->transactionOuterLayer('A');
// Because we committed, both of the inserted rows should be present.
$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->fail($e->getMessage());
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment