Commit 7af90e5f authored by catch's avatar catch

Issue #2487269 by alexpott, Oliver Sommersberg, zhuber: Postgres insert...

Issue #2487269 by alexpott, Oliver Sommersberg, zhuber: Postgres insert queries that fail in a transaction break the entire transaction
parent a9c6d59c
......@@ -138,7 +138,38 @@ public function query($query, array $args = array(), $options = array()) {
}
}
return parent::query($query, $args, $options);
// We need to wrap queries with a savepoint if:
// - Currently in a transaction.
// - A 'mimic_implicit_commit' does not exist already.
// - The query is not a savepoint query.
$wrap_with_savepoint = $this->inTransaction() &&
!isset($this->transactionLayers['mimic_implicit_commit']) &&
!(is_string($query) && (
stripos($query, 'ROLLBACK TO SAVEPOINT ') === 0 ||
stripos($query, 'RELEASE SAVEPOINT ') === 0 ||
stripos($query, 'SAVEPOINT ') === 0
)
);
if ($wrap_with_savepoint) {
// Create a savepoint so we can rollback a failed query. This is so we can
// mimic MySQL and SQLite transactions which don't fail if a single query
// fails. This is important for tables that are created on demand. For
// example, \Drupal\Core\Cache\DatabaseBackend.
$this->addSavepoint();
try {
$return = parent::query($query, $args, $options);
$this->releaseSavepoint();
}
catch (\Exception $e) {
$this->rollbackSavepoint();
throw $e;
}
}
else {
$return = parent::query($query, $args, $options);
}
return $return;
}
public function prepareQuery($query) {
......
......@@ -99,12 +99,25 @@ public function execute() {
elseif ($options['return'] == Database::RETURN_INSERT_ID) {
$options['return'] = Database::RETURN_NULL;
}
// Only use the returned last_insert_id if it is not already set.
if (!empty($last_insert_id)) {
$this->connection->query($stmt, array(), $options);
// Create a savepoint so we can rollback a failed query. This is so we can
// mimic MySQL and SQLite transactions which don't fail if a single query
// fails. This is important for tables that are created on demand. For
// example, \Drupal\Core\Cache\DatabaseBackend.
$this->connection->addSavepoint();
try {
// Only use the returned last_insert_id if it is not already set.
if (!empty($last_insert_id)) {
$this->connection->query($stmt, array(), $options);
}
else {
$last_insert_id = $this->connection->query($stmt, array(), $options);
}
$this->connection->releaseSavepoint();
}
else {
$last_insert_id = $this->connection->query($stmt, array(), $options);
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
// Re-initialize the values array so that we can re-use this query.
......
......@@ -76,11 +76,23 @@ public function execute() {
$options['sequence_name'] = $table_information->sequences[0];
}
$this->connection->query($stmt, [], $options);
// Re-initialize the values array so that we can re-use this query.
$this->insertValues = [];
// Create a savepoint so we can rollback a failed query. This is so we can
// mimic MySQL and SQLite transactions which don't fail if a single query
// fails. This is important for tables that are created on demand. For
// example, \Drupal\Core\Cache\DatabaseBackend.
$this->connection->addSavepoint();
try {
$this->connection->query($stmt, [], $options);
$this->connection->releaseSavepoint();
}
catch (\Exception $e) {
$this->connection->rollbackSavepoint();
throw $e;
}
return TRUE;
}
......
......@@ -492,5 +492,124 @@ function testTransactionStacking() {
$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', array(':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(array(
'name' => 'David',
'age' => '24',
))
->execute();
// Commit the transaction.
unset($transaction);
$saved_age = $connection->query('SELECT age FROM {test} WHERE name = :name', array(':name' => 'David'))->fetchField();
$this->assertEqual('24', $saved_age);
}
}
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