| // Copyright 2011 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "sql/transaction.h" |
| |
| #include <memory> |
| |
| #include "base/files/scoped_temp_dir.h" |
| #include "sql/database.h" |
| #include "sql/statement.h" |
| #include "sql/test/test_helpers.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace sql { |
| |
| namespace { |
| |
| class SQLTransactionTest : public testing::Test { |
| public: |
| void SetUp() override { |
| ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| db_path_ = temp_dir_.GetPath().AppendASCII("transaction_test.sqlite"); |
| ASSERT_TRUE(db_.Open(db_path_)); |
| |
| ASSERT_TRUE(db_.Execute("CREATE TABLE foo (a, b)")); |
| } |
| |
| // Returns the number of rows in table "foo". |
| int CountFoo() { |
| Statement count(db_.GetUniqueStatement("SELECT count(*) FROM foo")); |
| count.Step(); |
| return count.ColumnInt(0); |
| } |
| |
| protected: |
| base::ScopedTempDir temp_dir_; |
| Database db_{sql::DatabaseOptions().set_exclusive_locking(false), |
| test::kTestTag}; |
| base::FilePath db_path_; |
| }; |
| |
| TEST_F(SQLTransactionTest, Commit) { |
| { |
| Transaction transaction(&db_); |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_FALSE(transaction.IsActiveForTesting()); |
| |
| ASSERT_TRUE(transaction.Begin()); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_TRUE(transaction.IsActiveForTesting()); |
| |
| ASSERT_TRUE(db_.Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); |
| ASSERT_EQ(1, CountFoo()) << "INSERT did not work as intended"; |
| |
| transaction.Commit(); |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_FALSE(transaction.IsActiveForTesting()); |
| } |
| |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_EQ(1, CountFoo()) << "Transaction changes not committed"; |
| } |
| |
| // Regression test for <https://crbug.com/326498384>. |
| TEST_F(SQLTransactionTest, CloseDatabase) { |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| |
| { |
| Transaction transaction(&db_); |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_FALSE(transaction.IsActiveForTesting()); |
| |
| ASSERT_TRUE(transaction.Begin()); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_TRUE(transaction.IsActiveForTesting()); |
| |
| db_.Close(); |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| } |
| |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| } |
| |
| TEST_F(SQLTransactionTest, RollbackOnDestruction) { |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| |
| { |
| Transaction transaction(&db_); |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_FALSE(transaction.IsActiveForTesting()); |
| |
| ASSERT_TRUE(transaction.Begin()); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_TRUE(transaction.IsActiveForTesting()); |
| |
| ASSERT_TRUE(db_.Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); |
| ASSERT_EQ(1, CountFoo()) << "INSERT did not work as intended"; |
| } |
| |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_EQ(0, CountFoo()) << "Transaction changes not rolled back"; |
| } |
| |
| TEST_F(SQLTransactionTest, ExplicitRollback) { |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| |
| { |
| Transaction transaction(&db_); |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_FALSE(transaction.IsActiveForTesting()); |
| |
| ASSERT_TRUE(transaction.Begin()); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_TRUE(transaction.IsActiveForTesting()); |
| |
| ASSERT_TRUE(db_.Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); |
| ASSERT_EQ(1, CountFoo()) << "INSERT did not work as intended"; |
| |
| transaction.Rollback(); |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_FALSE(transaction.IsActiveForTesting()); |
| EXPECT_EQ(0, CountFoo()) << "Transaction changes not rolled back"; |
| } |
| |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_EQ(0, CountFoo()) << "Transaction changes not rolled back"; |
| } |
| |
| // Rolling back any part of a transaction should roll back all of them. |
| TEST_F(SQLTransactionTest, NestedRollback) { |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_EQ(0, db_.transaction_nesting()); |
| |
| // Outermost transaction. |
| { |
| Transaction outer_txn(&db_); |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_EQ(0, db_.transaction_nesting()); |
| |
| ASSERT_TRUE(outer_txn.Begin()); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(1, db_.transaction_nesting()); |
| |
| // First inner transaction is committed. |
| { |
| Transaction committed_inner_txn(&db_); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(1, db_.transaction_nesting()); |
| |
| ASSERT_TRUE(committed_inner_txn.Begin()); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(2, db_.transaction_nesting()); |
| |
| ASSERT_TRUE(db_.Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); |
| ASSERT_EQ(1, CountFoo()) << "INSERT did not work as intended"; |
| |
| committed_inner_txn.Commit(); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(1, db_.transaction_nesting()); |
| } |
| |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(1, db_.transaction_nesting()); |
| EXPECT_EQ(1, CountFoo()) << "First inner transaction did not commit"; |
| |
| // Second inner transaction is rolled back. |
| { |
| Transaction rolled_back_inner_txn(&db_); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(1, db_.transaction_nesting()); |
| |
| ASSERT_TRUE(rolled_back_inner_txn.Begin()); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(2, db_.transaction_nesting()); |
| |
| ASSERT_TRUE(db_.Execute("INSERT INTO foo (a, b) VALUES (2, 3)")); |
| ASSERT_EQ(2, CountFoo()) << "INSERT did not work as intended"; |
| |
| rolled_back_inner_txn.Rollback(); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(1, db_.transaction_nesting()); |
| EXPECT_EQ(2, CountFoo()) |
| << "Nested transaction rollback deferred to top-level transaction"; |
| } |
| |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(1, db_.transaction_nesting()); |
| EXPECT_EQ(2, CountFoo()) |
| << "Nested transaction rollback deferred to top-level transaction"; |
| |
| // Third inner transaction fails in Begin(), because a nested transaction |
| // has already been rolled back. |
| { |
| Transaction failed_inner_txn(&db_); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(1, db_.transaction_nesting()); |
| |
| EXPECT_FALSE(failed_inner_txn.Begin()); |
| EXPECT_TRUE(db_.HasActiveTransactions()); |
| EXPECT_EQ(1, db_.transaction_nesting()); |
| } |
| } |
| |
| EXPECT_FALSE(db_.HasActiveTransactions()); |
| EXPECT_EQ(0, db_.transaction_nesting()); |
| EXPECT_EQ(0, CountFoo()); |
| } |
| |
| TEST_F(SQLTransactionTest, TransactionCommitWithPendingWriter) { |
| ASSERT_TRUE(db_.Execute("CREATE TABLE rows (id)")); |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows (id) VALUES (12)")); |
| |
| Transaction transaction(&db_); |
| EXPECT_TRUE(transaction.Begin()); |
| |
| // The'RETURNING' clause changes the behavior of the statement to return a |
| // row. A pending write statement is kept alive in the sqlite connection. |
| Statement update( |
| db_.GetUniqueStatement("UPDATE rows SET id = 2 * id RETURNING id")); |
| EXPECT_TRUE(update.Step()); |
| |
| // The commit will fail due to the pending writer. |
| EXPECT_FALSE(transaction.Commit()); |
| |
| EXPECT_FALSE(update.Step()); |
| EXPECT_TRUE(update.Succeeded()); |
| } |
| |
| TEST_F(SQLTransactionTest, TransactionCommitWithActiveReader) { |
| Database other_db(sql::DatabaseOptions().set_exclusive_locking(false), |
| test::kTestTag); |
| ASSERT_TRUE(other_db.Open(db_path_)); |
| |
| ASSERT_TRUE(db_.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)")); |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows(id) VALUES(1)")); |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows(id) VALUES(2)")); |
| |
| Transaction transaction(&db_); |
| EXPECT_TRUE(transaction.Begin()); |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows(id) VALUES(3)")); |
| |
| Statement select(other_db.GetUniqueStatement("SELECT * FROM rows")); |
| EXPECT_TRUE(select.Step()); |
| |
| // The commit will fail with a SQL_BUSY error code since there is an |
| // open statement. When this error code is detected, a explicit rollback is |
| // issued by the Database code to close the pending transaction. |
| EXPECT_FALSE(transaction.Commit()); |
| |
| // The open statements on a different connections remain valid since the |
| // modifications were reverted. Statement is expected to work. |
| EXPECT_TRUE(select.Step()); |
| } |
| |
| TEST_F(SQLTransactionTest, TransactionCommitWithActiveTransaction) { |
| Database other_db(sql::DatabaseOptions().set_exclusive_locking(false), |
| test::kTestTag); |
| ASSERT_TRUE(other_db.Open(db_path_)); |
| |
| ASSERT_TRUE(db_.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)")); |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows(id) VALUES(1)")); |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows(id) VALUES(2)")); |
| |
| Transaction transaction(&db_); |
| EXPECT_TRUE(transaction.Begin()); |
| |
| Transaction other_transaction(&other_db); |
| EXPECT_TRUE(other_transaction.Begin()); |
| Statement select(other_db.GetUniqueStatement("SELECT * FROM rows")); |
| EXPECT_TRUE(select.Step()); |
| |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows(id) VALUES(3)")); |
| |
| // The commit will fail with a SQL_BUSY error code since there is an |
| // open statement. When this error code is detected, a explicit rollback is |
| // issued by the Database code to close the pending transaction. |
| EXPECT_FALSE(transaction.Commit()); |
| |
| // Statement is expected to work. |
| EXPECT_TRUE(select.Step()); |
| ASSERT_TRUE(other_transaction.Commit()); |
| } |
| |
| TEST_F(SQLTransactionTest, TransactionOnRazedDB) { |
| ASSERT_TRUE(db_.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)")); |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows(id) VALUES(1)")); |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows(id) VALUES(2)")); |
| |
| Transaction transaction(&db_); |
| EXPECT_TRUE(transaction.Begin()); |
| |
| Statement select(db_.GetUniqueStatement("SELECT * FROM rows")); |
| EXPECT_TRUE(select.Step()); |
| |
| // Raze won't succeed if there is a pending transaction. The pending commit |
| // will succeed to apply the modifications. |
| EXPECT_FALSE(db_.Raze()); |
| EXPECT_TRUE(transaction.Commit()); |
| } |
| |
| TEST_F(SQLTransactionTest, TransactionOnPoisonedDB) { |
| ASSERT_TRUE(db_.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)")); |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows(id) VALUES(1)")); |
| |
| Transaction transaction(&db_); |
| EXPECT_TRUE(transaction.Begin()); |
| db_.Poison(); |
| EXPECT_FALSE(transaction.Commit()); |
| } |
| |
| TEST_F(SQLTransactionTest, TransactionOnClosedDB) { |
| ASSERT_TRUE(db_.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)")); |
| ASSERT_TRUE(db_.Execute("INSERT INTO rows(id) VALUES(1)")); |
| |
| Transaction transaction(&db_); |
| EXPECT_TRUE(transaction.Begin()); |
| db_.Close(); |
| EXPECT_FALSE(transaction.Commit()); |
| } |
| |
| TEST(SQLTransactionDatabaseDestroyedTest, BeginIsNoOp) { |
| auto db = std::make_unique<Database>(test::kTestTag); |
| ASSERT_TRUE(db->OpenInMemory()); |
| Transaction transaction(db.get()); |
| db.reset(); |
| ASSERT_FALSE(transaction.Begin()); |
| } |
| |
| TEST(SQLTransactionDatabaseDestroyedTest, RollbackIsNoOp) { |
| auto db = std::make_unique<Database>(test::kTestTag); |
| ASSERT_TRUE(db->OpenInMemory()); |
| Transaction transaction(db.get()); |
| ASSERT_TRUE(transaction.Begin()); |
| EXPECT_TRUE(db->HasActiveTransactions()); |
| db.reset(); |
| // `Transaction::Rollback()` does not return a value, so we cannot verify |
| // externally whether it returned early. |
| transaction.Rollback(); |
| } |
| |
| TEST(SQLTransactionDatabaseDestroyedTest, CommitIsNoOp) { |
| auto db = std::make_unique<Database>(test::kTestTag); |
| ASSERT_TRUE(db->OpenInMemory()); |
| Transaction transaction(db.get()); |
| ASSERT_TRUE(transaction.Begin()); |
| EXPECT_TRUE(db->HasActiveTransactions()); |
| db.reset(); |
| ASSERT_FALSE(transaction.Commit()); |
| } |
| |
| } // namespace |
| |
| } // namespace sql |