transactionDepth(); $txn = db_transaction(); // Insert a single row into the testing table. db_insert('test') ->fields([ 'name' => 'David' . $suffix, 'age' => '24', ]) ->execute(); $this->assertTrue($connection->inTransaction(), '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, $ddl_statement); $this->assertTrue($connection->inTransaction(), 'In transaction after calling nested transaction.'); if ($rollback) { // Roll back the transaction, if requested. // This rollback should propagate to the last savepoint. $txn->rollBack(); $this->assertTrue(($connection->transactionDepth() == $depth), 'Transaction has rolled back to the last savepoint after calling rollBack().'); } } /** * Creates an "inner layer" transaction. * * 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. * @param $ddl_statement * Whether to execute a DDL statement during the transaction. */ protected function transactionInnerLayer($suffix, $rollback = FALSE, $ddl_statement = FALSE) { $connection = Database::getConnection(); $depth = $connection->transactionDepth(); // 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(); $depth2 = $connection->transactionDepth(); $this->assertTrue($depth < $depth2, 'Transaction depth is has increased with new transaction.'); // Insert a single row into the testing table. db_insert('test') ->fields([ 'name' => 'Daniel' . $suffix, 'age' => '19', ]) ->execute(); $this->assertTrue($connection->inTransaction(), 'In transaction inside nested transaction.'); if ($ddl_statement) { $table = [ 'fields' => [ 'id' => [ 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, ], ], 'primary key' => ['id'], ]; db_create_table('database_test_1', $table); $this->assertTrue($connection->inTransaction(), 'In transaction inside nested transaction.'); } if ($rollback) { // Roll back the transaction, if requested. // This rollback should propagate to the last savepoint. $txn->rollBack(); $this->assertTrue(($connection->transactionDepth() == $depth), 'Transaction has rolled back to the last savepoint after calling rollBack().'); } } /** * Tests transaction rollback on a database that supports transactions. * * If the active connection does not support transactions, this test does * nothing. */ public function testTransactionRollBackSupported() { // This test won't work right if transactions are not supported. if (!Database::getConnection()->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', [':name' => 'DavidB'])->fetchField(); $this->assertNotIdentical($saved_age, '24', 'Cannot retrieve DavidB row after commit.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', [':name' => 'DanielB'])->fetchField(); $this->assertNotIdentical($saved_age, '19', 'Cannot retrieve DanielB row after commit.'); } catch (\Exception $e) { $this->fail($e->getMessage()); } } /** * Tests transaction rollback on a database that doesn't support transactions. * * If the active driver supports transactions, this test does nothing. */ public function testTransactionRollBackNotSupported() { // This test won't work right if transactions are supported. if (Database::getConnection()->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', [':name' => 'DavidB'])->fetchField(); $this->assertIdentical($saved_age, '24', 'DavidB not rolled back, since transactions are not supported.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', [':name' => 'DanielB'])->fetchField(); $this->assertIdentical($saved_age, '19', 'DanielB not rolled back, since transactions are not supported.'); } catch (\Exception $e) { $this->fail($e->getMessage()); } } /** * Tests a committed transaction. * * The behavior of this test should be identical for connections that support * transactions and those that do not. */ public 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', [':name' => 'DavidA'])->fetchField(); $this->assertIdentical($saved_age, '24', 'Can retrieve DavidA row after commit.'); $saved_age = db_query('SELECT age FROM {test} WHERE name = :name', [':name' => 'DanielA'])->fetchField(); $this->assertIdentical($saved_age, '19', 'Can retrieve DanielA row after commit.'); } catch (\Exception $e) { $this->fail($e->getMessage()); } } /** * Tests the compatibility of transactions with DDL statements. */ public function testTransactionWithDdlStatement() { // First, test that a commit works normally, even with DDL statements. $transaction = db_transaction(); $this->insertRow('row'); $this->executeDDLStatement(); unset($transaction); $this->assertRowPresent('row'); // Even in different order. $this->cleanUp(); $transaction = db_transaction(); $this->executeDDLStatement(); $this->insertRow('row'); unset($transaction); $this->assertRowPresent('row'); // Even with stacking. $this->cleanUp(); $transaction = db_transaction(); $transaction2 = db_transaction(); $this->executeDDLStatement(); unset($transaction2); $transaction3 = db_transaction(); $this->insertRow('row'); unset($transaction3); unset($transaction); $this->assertRowPresent('row'); // A transaction after a DDL statement should still work the same. $this->cleanUp(); $transaction = db_transaction(); $transaction2 = db_transaction(); $this->executeDDLStatement(); unset($transaction2); $transaction3 = db_transaction(); $this->insertRow('row'); $transaction3->rollBack(); unset($transaction3); unset($transaction); $this->assertRowAbsent('row'); // The behavior of a rollback depends on the type of database server. if (Database::getConnection()->supportsTransactionalDDL()) { // For database servers that support transactional DDL, a rollback // of a transaction including DDL statements should be possible. $this->cleanUp(); $transaction = db_transaction(); $this->insertRow('row'); $this->executeDDLStatement(); $transaction->rollBack(); unset($transaction); $this->assertRowAbsent('row'); // Including with stacking. $this->cleanUp(); $transaction = db_transaction(); $transaction2 = db_transaction(); $this->executeDDLStatement(); unset($transaction2); $transaction3 = db_transaction(); $this->insertRow('row'); unset($transaction3); $transaction->rollBack(); unset($transaction); $this->assertRowAbsent('row'); } else { // For database servers that do not support transactional DDL, // the DDL statement should commit the transaction stack. $this->cleanUp(); $transaction = db_transaction(); $this->insertRow('row'); $this->executeDDLStatement(); // Rollback the outer transaction. try { $transaction->rollBack(); unset($transaction); // @TODO: an exception should be triggered here, but is not, because // "ROLLBACK" fails silently in MySQL if there is no transaction active. // $this->fail(t('Rolling back a transaction containing DDL should fail.')); } catch (TransactionNoActiveException $e) { $this->pass('Rolling back a transaction containing DDL should fail.'); } $this->assertRowPresent('row'); } } /** * Inserts a single row into the testing table. */ protected function insertRow($name) { db_insert('test') ->fields([ 'name' => $name, ]) ->execute(); } /** * Executes a DDL statement. */ protected function executeDDLStatement() { static $count = 0; $table = [ 'fields' => [ 'id' => [ 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, ], ], 'primary key' => ['id'], ]; db_create_table('database_test_' . ++$count, $table); } /** * Starts over for a new test. */ protected function cleanUp() { db_truncate('test') ->execute(); } /** * Asserts that a given row is present in the test table. * * @param $name * The name of the row. * @param $message * The message to log for the assertion. */ public function assertRowPresent($name, $message = NULL) { if (!isset($message)) { $message = format_string('Row %name is present.', ['%name' => $name]); } $present = (boolean) db_query('SELECT 1 FROM {test} WHERE name = :name', [':name' => $name])->fetchField(); return $this->assertTrue($present, $message); } /** * Asserts that a given row is absent from the test table. * * @param $name * The name of the row. * @param $message * The message to log for the assertion. */ public function assertRowAbsent($name, $message = NULL) { if (!isset($message)) { $message = format_string('Row %name is absent.', ['%name' => $name]); } $present = (boolean) db_query('SELECT 1 FROM {test} WHERE name = :name', [':name' => $name])->fetchField(); return $this->assertFalse($present, $message); } /** * Tests transaction stacking, commit, and rollback. */ public function testTransactionStacking() { // This test won't work right if transactions are not supported. if (!Database::getConnection()->supportsTransactions()) { return; } $database = Database::getConnection(); // Standard case: pop the inner transaction before the outer transaction. $transaction = db_transaction(); $this->insertRow('outer'); $transaction2 = db_transaction(); $this->insertRow('inner'); // Pop the inner transaction. unset($transaction2); $this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the inner transaction'); // Pop the outer transaction. unset($transaction); $this->assertFalse($database->inTransaction(), 'Transaction closed after popping the outer transaction'); $this->assertRowPresent('outer'); $this->assertRowPresent('inner'); // Pop the transaction in a different order they have been pushed. $this->cleanUp(); $transaction = db_transaction(); $this->insertRow('outer'); $transaction2 = db_transaction(); $this->insertRow('inner'); // Pop the outer transaction, nothing should happen. unset($transaction); $this->insertRow('inner-after-outer-commit'); $this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the outer transaction'); // Pop the inner transaction, the whole transaction should commit. unset($transaction2); $this->assertFalse($database->inTransaction(), 'Transaction closed after popping the inner transaction'); $this->assertRowPresent('outer'); $this->assertRowPresent('inner'); $this->assertRowPresent('inner-after-outer-commit'); // Rollback the inner transaction. $this->cleanUp(); $transaction = db_transaction(); $this->insertRow('outer'); $transaction2 = db_transaction(); $this->insertRow('inner'); // Now rollback the inner transaction. $transaction2->rollBack(); unset($transaction2); $this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the outer transaction'); // Pop the outer transaction, it should commit. $this->insertRow('outer-after-inner-rollback'); unset($transaction); $this->assertFalse($database->inTransaction(), 'Transaction closed after popping the inner transaction'); $this->assertRowPresent('outer'); $this->assertRowAbsent('inner'); $this->assertRowPresent('outer-after-inner-rollback'); // Rollback the inner transaction after committing the outer one. $this->cleanUp(); $transaction = db_transaction(); $this->insertRow('outer'); $transaction2 = db_transaction(); $this->insertRow('inner'); // Pop the outer transaction, nothing should happen. unset($transaction); $this->assertTrue($database->inTransaction(), 'Still in a transaction after popping the outer transaction'); // Now rollback the inner transaction, it should rollback. $transaction2->rollBack(); unset($transaction2); $this->assertFalse($database->inTransaction(), 'Transaction closed after popping the inner transaction'); $this->assertRowPresent('outer'); $this->assertRowAbsent('inner'); // Rollback the outer transaction while the inner transaction is active. // In that case, an exception will be triggered because we cannot // ensure that the final result will have any meaning. $this->cleanUp(); $transaction = db_transaction(); $this->insertRow('outer'); $transaction2 = db_transaction(); $this->insertRow('inner'); $transaction3 = db_transaction(); $this->insertRow('inner2'); // Rollback the outer transaction. try { $transaction->rollBack(); unset($transaction); $this->fail('Rolling back the outer transaction while the inner transaction is active resulted in an exception.'); } catch (TransactionOutOfOrderException $e) { $this->pass('Rolling back the outer transaction while the inner transaction is active resulted in an exception.'); } $this->assertFalse($database->inTransaction(), 'No more in a transaction after rolling back the outer transaction'); // Try to commit one inner transaction. unset($transaction3); $this->pass('Trying to commit an inner transaction resulted in an exception.'); // Try to rollback one inner transaction. try { $transaction->rollBack(); unset($transaction2); $this->fail('Trying to commit an inner transaction resulted in an exception.'); } catch (TransactionNoActiveException $e) { $this->pass('Trying to commit an inner transaction resulted in an exception.'); } $this->assertRowAbsent('outer'); $this->assertRowAbsent('inner'); $this->assertRowAbsent('inner2'); } /** * Tests that transactions can continue to be used if a query fails. */ public function testQueryFailureInTransaction() { $connection = Database::getConnection(); $transaction = $connection->startTransaction('test_transaction'); $connection->schema()->dropTable('test'); // Test a failed query using the query() method. try { $connection->query('SELECT age FROM {test} WHERE name = :name', [':name' => 'David'])->fetchField(); $this->fail('Using the query method failed.'); } catch (\Exception $e) { $this->pass('Using the query method failed.'); } // Test a failed select query. try { $connection->select('test') ->fields('test', ['name']) ->execute(); $this->fail('Select query failed.'); } catch (\Exception $e) { $this->pass('Select query failed.'); } // Test a failed insert query. try { $connection->insert('test') ->fields([ 'name' => 'David', 'age' => '24', ]) ->execute(); $this->fail('Insert query failed.'); } catch (\Exception $e) { $this->pass('Insert query failed.'); } // Test a failed update query. try { $connection->update('test') ->fields(['name' => 'Tiffany']) ->condition('id', 1) ->execute(); $this->fail('Update query failed.'); } catch (\Exception $e) { $this->pass('Update query failed.'); } // Test a failed delete query. try { $connection->delete('test') ->condition('id', 1) ->execute(); $this->fail('Delete query failed.'); } catch (\Exception $e) { $this->pass('Delete query failed.'); } // Test a failed merge query. try { $connection->merge('test') ->key('job', 'Presenter') ->fields([ 'age' => '31', 'name' => 'Tiffany', ]) ->execute(); $this->fail('Merge query failed.'); } catch (\Exception $e) { $this->pass('Merge query failed.'); } // Test a failed upsert query. try { $connection->upsert('test') ->key('job') ->fields(['job', 'age', 'name']) ->values([ 'job' => 'Presenter', 'age' => 31, 'name' => 'Tiffany', ]) ->execute(); $this->fail('Upset query failed.'); } catch (\Exception $e) { $this->pass('Upset query failed.'); } // Create the missing schema and insert a row. $this->installSchema('database_test', ['test']); $connection->insert('test') ->fields([ 'name' => 'David', 'age' => '24', ]) ->execute(); // Commit the transaction. unset($transaction); $saved_age = $connection->query('SELECT age FROM {test} WHERE name = :name', [':name' => 'David'])->fetchField(); $this->assertEqual('24', $saved_age); } }