View source
<?php
namespace Drupal\KernelTests\Core\Database;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Database\TransactionOutOfOrderException;
use Drupal\Core\Database\TransactionNoActiveException;
use PHPUnit\Framework\Error\Warning;
class TransactionTest extends DatabaseTestBase {
protected function transactionOuterLayer($suffix, $rollback = FALSE, $ddl_statement = FALSE) {
$depth = $this->connection
->transactionDepth();
$txn = $this->connection
->startTransaction();
$this->connection
->insert('test')
->fields([
'name' => 'David' . $suffix,
'age' => '24',
])
->execute();
$this
->assertTrue($this->connection
->inTransaction(), 'In transaction before calling nested transaction.');
$this
->transactionInnerLayer($suffix, $rollback, $ddl_statement);
$this
->assertTrue($this->connection
->inTransaction(), 'In transaction after calling nested transaction.');
if ($rollback) {
$txn
->rollBack();
$this
->assertSame($depth, $this->connection
->transactionDepth(), 'Transaction has rolled back to the last savepoint after calling rollBack().');
}
}
protected function transactionInnerLayer($suffix, $rollback = FALSE, $ddl_statement = FALSE) {
$depth = $this->connection
->transactionDepth();
$txn = $this->connection
->startTransaction();
$depth2 = $this->connection
->transactionDepth();
$this
->assertGreaterThan($depth, $depth2, 'Transaction depth has increased with new transaction.');
$this->connection
->insert('test')
->fields([
'name' => 'Daniel' . $suffix,
'age' => '19',
])
->execute();
$this
->assertTrue($this->connection
->inTransaction(), 'In transaction inside nested transaction.');
if ($ddl_statement) {
$table = [
'fields' => [
'id' => [
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
],
'primary key' => [
'id',
],
];
$this->connection
->schema()
->createTable('database_test_1', $table);
$this
->assertTrue($this->connection
->inTransaction(), 'In transaction inside nested transaction.');
}
if ($rollback) {
$txn
->rollBack();
$this
->assertSame($depth, $this->connection
->transactionDepth(), 'Transaction has rolled back to the last savepoint after calling rollBack().');
}
}
public function testTransactionRollBackSupported() {
try {
$this
->transactionOuterLayer('B', TRUE);
$saved_age = $this->connection
->query('SELECT [age] FROM {test} WHERE [name] = :name', [
':name' => 'DavidB',
])
->fetchField();
$this
->assertNotSame('24', $saved_age, 'Cannot retrieve DavidB row after commit.');
$saved_age = $this->connection
->query('SELECT [age] FROM {test} WHERE [name] = :name', [
':name' => 'DanielB',
])
->fetchField();
$this
->assertNotSame('19', $saved_age, 'Cannot retrieve DanielB row after commit.');
} catch (\Exception $e) {
$this
->fail($e
->getMessage());
}
}
public function testCommittedTransaction() {
try {
$this
->transactionOuterLayer('A');
$saved_age = $this->connection
->query('SELECT [age] FROM {test} WHERE [name] = :name', [
':name' => 'DavidA',
])
->fetchField();
$this
->assertSame('24', $saved_age, 'Can retrieve DavidA row after commit.');
$saved_age = $this->connection
->query('SELECT [age] FROM {test} WHERE [name] = :name', [
':name' => 'DanielA',
])
->fetchField();
$this
->assertSame('19', $saved_age, 'Can retrieve DanielA row after commit.');
} catch (\Exception $e) {
$this
->fail($e
->getMessage());
}
}
public function testTransactionWithDdlStatement() {
$transaction = $this->connection
->startTransaction();
$this
->insertRow('row');
$this
->executeDDLStatement();
unset($transaction);
$this
->assertRowPresent('row');
$this
->cleanUp();
$transaction = $this->connection
->startTransaction();
$this
->executeDDLStatement();
$this
->insertRow('row');
unset($transaction);
$this
->assertRowPresent('row');
$this
->cleanUp();
$transaction = $this->connection
->startTransaction();
$transaction2 = $this->connection
->startTransaction();
$this
->executeDDLStatement();
unset($transaction2);
$transaction3 = $this->connection
->startTransaction();
$this
->insertRow('row');
unset($transaction3);
unset($transaction);
$this
->assertRowPresent('row');
$this
->cleanUp();
$transaction = $this->connection
->startTransaction();
$transaction2 = $this->connection
->startTransaction();
$this
->executeDDLStatement();
unset($transaction2);
$transaction3 = $this->connection
->startTransaction();
$this
->insertRow('row');
$transaction3
->rollBack();
unset($transaction3);
unset($transaction);
$this
->assertRowAbsent('row');
if ($this->connection
->supportsTransactionalDDL()) {
$this
->cleanUp();
$transaction = $this->connection
->startTransaction();
$this
->insertRow('row');
$this
->executeDDLStatement();
$transaction
->rollBack();
unset($transaction);
$this
->assertRowAbsent('row');
$this
->cleanUp();
$transaction = $this->connection
->startTransaction();
$transaction2 = $this->connection
->startTransaction();
$this
->executeDDLStatement();
unset($transaction2);
$transaction3 = $this->connection
->startTransaction();
$this
->insertRow('row');
unset($transaction3);
$transaction
->rollBack();
unset($transaction);
$this
->assertRowAbsent('row');
}
else {
$this
->cleanUp();
$transaction = $this->connection
->startTransaction();
$this
->insertRow('row');
$this
->executeDDLStatement();
try {
$transaction
->rollBack();
$this
->fail('Rolling back a transaction containing DDL should produce a warning.');
} catch (Warning $warning) {
$this
->assertSame('Rollback attempted when there is no active transaction. This can cause data integrity issues.', $warning
->getMessage());
}
unset($transaction);
$this
->assertRowPresent('row');
}
}
protected function insertRow($name) {
$this->connection
->insert('test')
->fields([
'name' => $name,
])
->execute();
}
protected function executeDDLStatement() {
static $count = 0;
$table = [
'fields' => [
'id' => [
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
],
'primary key' => [
'id',
],
];
$this->connection
->schema()
->createTable('database_test_' . ++$count, $table);
}
protected function cleanUp() {
$this->connection
->truncate('test')
->execute();
}
public function assertRowPresent(string $name, string $message = NULL) : void {
if (!isset($message)) {
$message = new FormattableMarkup('Row %name is present.', [
'%name' => $name,
]);
}
$present = (bool) $this->connection
->query('SELECT 1 FROM {test} WHERE [name] = :name', [
':name' => $name,
])
->fetchField();
$this
->assertTrue($present, $message);
}
public function assertRowAbsent(string $name, string $message = NULL) : void {
if (!isset($message)) {
$message = new FormattableMarkup('Row %name is absent.', [
'%name' => $name,
]);
}
$present = (bool) $this->connection
->query('SELECT 1 FROM {test} WHERE [name] = :name', [
':name' => $name,
])
->fetchField();
$this
->assertFalse($present, $message);
}
public function testTransactionStacking() {
$transaction = $this->connection
->startTransaction();
$this
->insertRow('outer');
$transaction2 = $this->connection
->startTransaction();
$this
->insertRow('inner');
unset($transaction2);
$this
->assertTrue($this->connection
->inTransaction(), 'Still in a transaction after popping the inner transaction');
unset($transaction);
$this
->assertFalse($this->connection
->inTransaction(), 'Transaction closed after popping the outer transaction');
$this
->assertRowPresent('outer');
$this
->assertRowPresent('inner');
$this
->cleanUp();
$transaction = $this->connection
->startTransaction();
$this
->insertRow('outer');
$transaction2 = $this->connection
->startTransaction();
$this
->insertRow('inner');
unset($transaction);
$this
->insertRow('inner-after-outer-commit');
$this
->assertTrue($this->connection
->inTransaction(), 'Still in a transaction after popping the outer transaction');
unset($transaction2);
$this
->assertFalse($this->connection
->inTransaction(), 'Transaction closed after popping the inner transaction');
$this
->assertRowPresent('outer');
$this
->assertRowPresent('inner');
$this
->assertRowPresent('inner-after-outer-commit');
$this
->cleanUp();
$transaction = $this->connection
->startTransaction();
$this
->insertRow('outer');
$transaction2 = $this->connection
->startTransaction();
$this
->insertRow('inner');
$transaction2
->rollBack();
unset($transaction2);
$this
->assertTrue($this->connection
->inTransaction(), 'Still in a transaction after popping the outer transaction');
$this
->insertRow('outer-after-inner-rollback');
unset($transaction);
$this
->assertFalse($this->connection
->inTransaction(), 'Transaction closed after popping the inner transaction');
$this
->assertRowPresent('outer');
$this
->assertRowAbsent('inner');
$this
->assertRowPresent('outer-after-inner-rollback');
$this
->cleanUp();
$transaction = $this->connection
->startTransaction();
$this
->insertRow('outer');
$transaction2 = $this->connection
->startTransaction();
$this
->insertRow('inner');
unset($transaction);
$this
->assertTrue($this->connection
->inTransaction(), 'Still in a transaction after popping the outer transaction');
$transaction2
->rollBack();
unset($transaction2);
$this
->assertFalse($this->connection
->inTransaction(), 'Transaction closed after popping the inner transaction');
$this
->assertRowPresent('outer');
$this
->assertRowAbsent('inner');
$this
->cleanUp();
$transaction = $this->connection
->startTransaction();
$this
->insertRow('outer');
$transaction2 = $this->connection
->startTransaction();
$this
->insertRow('inner');
$transaction3 = $this->connection
->startTransaction();
$this
->insertRow('inner2');
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
->assertFalse($this->connection
->inTransaction(), 'No more in a transaction after rolling back the outer transaction');
unset($transaction3);
try {
$transaction
->rollBack();
unset($transaction2);
$this
->fail('Trying to commit an inner transaction resulted in an exception.');
} catch (TransactionNoActiveException $e) {
}
$this
->assertRowAbsent('outer');
$this
->assertRowAbsent('inner');
$this
->assertRowAbsent('inner2');
}
public function testQueryFailureInTransaction() {
$transaction = $this->connection
->startTransaction('test_transaction');
$this->connection
->schema()
->dropTable('test');
try {
$this->connection
->query('SELECT [age] FROM {test} WHERE [name] = :name', [
':name' => 'David',
])
->fetchField();
$this
->fail('Using the query method should have failed.');
} catch (\Exception $e) {
}
try {
$this->connection
->select('test')
->fields('test', [
'name',
])
->execute();
$this
->fail('Select query should have failed.');
} catch (\Exception $e) {
}
try {
$this->connection
->insert('test')
->fields([
'name' => 'David',
'age' => '24',
])
->execute();
$this
->fail('Insert query should have failed.');
} catch (\Exception $e) {
}
try {
$this->connection
->update('test')
->fields([
'name' => 'Tiffany',
])
->condition('id', 1)
->execute();
$this
->fail('Update query should have failed.');
} catch (\Exception $e) {
}
try {
$this->connection
->delete('test')
->condition('id', 1)
->execute();
$this
->fail('Delete query should have failed.');
} catch (\Exception $e) {
}
try {
$this->connection
->merge('test')
->key('job', 'Presenter')
->fields([
'age' => '31',
'name' => 'Tiffany',
])
->execute();
$this
->fail('Merge query should have failed.');
} catch (\Exception $e) {
}
try {
$this->connection
->upsert('test')
->key('job')
->fields([
'job',
'age',
'name',
])
->values([
'job' => 'Presenter',
'age' => 31,
'name' => 'Tiffany',
])
->execute();
$this
->fail('Upsert query should have failed.');
} catch (\Exception $e) {
}
$this
->installSchema('database_test', [
'test',
]);
$this->connection
->insert('test')
->fields([
'name' => 'David',
'age' => '24',
])
->execute();
unset($transaction);
$saved_age = $this->connection
->query('SELECT [age] FROM {test} WHERE [name] = :name', [
':name' => 'David',
])
->fetchField();
$this
->assertEquals('24', $saved_age);
}
}