blob: 0088eac0f7275979a4c259d4b935764547377b41 [file] [edit]
// 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 <stdint.h>
#include <memory>
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/test/bind.h"
#include "base/test/gtest_util.h"
#include "sql/database.h"
#include "sql/sqlite_result_code.h"
#include "sql/sqlite_result_code_values.h"
#include "sql/statement.h"
#include "sql/test/drive_error_test_vfs.h"
#include "sql/test/matchers.h"
#include "sql/test/test_helpers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace sql {
namespace {
using ::sql::test::PrimaryErrorIs;
using ::testing::Bool;
using ::testing::Contains;
using ::testing::TestParamInfo;
using ::testing::WithParamInterface;
class SQLTransactionTest : public testing::Test {
public:
void SetUp() override {
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
db_path_ = temp_dir_.GetPath().AppendASCII("transaction_test.sqlite");
}
protected:
base::ScopedTempDir temp_dir_;
base::FilePath db_path_;
};
using SQLTransactionDeathTest = SQLTransactionTest;
class SQLTransactionJournalTypeTest : public SQLTransactionTest,
public WithParamInterface<bool> {
protected:
static bool WalModeEnabled() { return GetParam(); }
};
INSTANTIATE_TEST_SUITE_P(
,
SQLTransactionJournalTypeTest,
Bool(),
[](const TestParamInfo<SQLTransactionJournalTypeTest::ParamType>& info) {
return info.param ? "WALEnabled" : "WALDisabled";
});
// Returns the number of rows in table "foo".
int CountFoo(Database& db) {
Statement count(db.GetUniqueStatement("SELECT count(*) FROM foo"));
count.Step();
return count.ColumnInt(0);
}
TEST_F(SQLTransactionTest, Commit) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE foo (a, b)"));
{
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(db)) << "INSERT did not work as intended";
transaction.Commit();
EXPECT_FALSE(db.HasActiveTransactions());
EXPECT_FALSE(transaction.IsActiveForTesting());
}
EXPECT_FALSE(db.HasActiveTransactions());
EXPECT_EQ(1, CountFoo(db)) << "Transaction changes not committed";
}
// Regression test for <https://crbug.com/326498384>.
TEST_F(SQLTransactionTest, CloseDatabase) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE foo (a, b)"));
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) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE foo (a, b)"));
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(db)) << "INSERT did not work as intended";
}
EXPECT_FALSE(db.HasActiveTransactions());
EXPECT_EQ(0, CountFoo(db)) << "Transaction changes not rolled back";
}
TEST_F(SQLTransactionTest, ExplicitRollback) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE foo (a, b)"));
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(db)) << "INSERT did not work as intended";
transaction.Rollback();
EXPECT_FALSE(db.HasActiveTransactions());
EXPECT_FALSE(transaction.IsActiveForTesting());
EXPECT_EQ(0, CountFoo(db)) << "Transaction changes not rolled back";
}
EXPECT_FALSE(db.HasActiveTransactions());
EXPECT_EQ(0, CountFoo(db)) << "Transaction changes not rolled back";
}
// Rolling back any part of a transaction should roll back all of them.
TEST_F(SQLTransactionTest, NestedRollback) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE foo (a, b)"));
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(db)) << "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(db)) << "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(db)) << "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(db))
<< "Nested transaction rollback deferred to top-level transaction";
}
EXPECT_TRUE(db.HasActiveTransactions());
EXPECT_EQ(1, db.transaction_nesting());
EXPECT_EQ(2, CountFoo(db))
<< "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(db));
}
TEST_F(SQLTransactionTest, TransactionCommitWithPendingWriter) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
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 db(sql::DatabaseOptions().set_exclusive_locking(false),
test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
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 db(sql::DatabaseOptions().set_exclusive_locking(false),
test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
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, CreateTransactionOnRazedDB) {
Database db(test::kTestTag);
ASSERT_TRUE(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)"));
EXPECT_TRUE(db.Raze());
// Transactions can be created and used normally after `Raze()`.
Transaction transaction(&db);
EXPECT_TRUE(transaction.Begin());
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)"));
ASSERT_TRUE(db.Execute("INSERT INTO rows(id) VALUES(2)"));
EXPECT_TRUE(transaction.Commit());
Statement select(db.GetUniqueStatement("SELECT id FROM rows"));
EXPECT_TRUE(select.Step());
EXPECT_EQ(select.ColumnInt(0), 2);
EXPECT_FALSE(select.Step());
}
TEST_F(SQLTransactionTest, BeginOnRazedDB) {
Database db(test::kTestTag);
ASSERT_TRUE(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)"));
Transaction transaction(&db);
EXPECT_TRUE(db.Raze());
// Transactions still work normally after `Raze()`.
EXPECT_TRUE(transaction.Begin());
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)"));
ASSERT_TRUE(db.Execute("INSERT INTO rows(id) VALUES(2)"));
EXPECT_TRUE(transaction.Commit());
Statement select(db.GetUniqueStatement("SELECT id FROM rows"));
EXPECT_TRUE(select.Step());
EXPECT_EQ(select.ColumnInt(0), 2);
EXPECT_FALSE(select.Step());
}
TEST_F(SQLTransactionTest, RollbackOnRazedDB) {
Database db(test::kTestTag);
ASSERT_TRUE(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)"));
Transaction transaction(&db);
EXPECT_TRUE(transaction.Begin());
ASSERT_TRUE(db.Execute("INSERT INTO rows(id) VALUES(2)"));
// Raze won't succeed if there is a pending transaction. The pending commit
// will succeed to apply the modifications.
EXPECT_FALSE(db.Raze());
transaction.Rollback();
Statement select(db.GetUniqueStatement("SELECT id FROM rows"));
EXPECT_TRUE(select.Step());
EXPECT_EQ(select.ColumnInt(0), 1);
EXPECT_FALSE(select.Step());
}
TEST_F(SQLTransactionTest, CommitOnRazedDB) {
Database db(test::kTestTag);
ASSERT_TRUE(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)"));
Transaction transaction(&db);
EXPECT_TRUE(transaction.Begin());
ASSERT_TRUE(db.Execute("INSERT INTO rows(id) VALUES(2)"));
// 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());
Statement select(db.GetUniqueStatement("SELECT id FROM rows ORDER BY id"));
EXPECT_TRUE(select.Step());
EXPECT_EQ(select.ColumnInt(0), 1);
EXPECT_TRUE(select.Step());
EXPECT_EQ(select.ColumnInt(0), 2);
EXPECT_FALSE(select.Step());
}
TEST_F(SQLTransactionTest, CreateTransactionOnPoisonedDb) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)"));
db.Poison();
Transaction transaction(&db);
EXPECT_FALSE(transaction.Begin());
}
TEST_F(SQLTransactionTest, BeginOnPoisonedDb) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)"));
Transaction transaction(&db);
db.Poison();
EXPECT_FALSE(transaction.Begin());
}
TEST_F(SQLTransactionTest, RollbackOnPoisonedDb) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)"));
Transaction transaction(&db);
EXPECT_TRUE(transaction.Begin());
db.Poison();
transaction.Rollback();
}
TEST_F(SQLTransactionTest, CommitOnPoisonedDb) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)"));
Transaction transaction(&db);
EXPECT_TRUE(transaction.Begin());
db.Poison();
EXPECT_FALSE(transaction.Commit());
}
TEST_F(SQLTransactionTest, CreateTransactionOnClosedDb) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)"));
db.Close();
Transaction transaction(&db);
EXPECT_FALSE(transaction.Begin());
}
TEST_F(SQLTransactionTest, BeginOnClosedDb) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)"));
Transaction transaction(&db);
db.Close();
EXPECT_FALSE(transaction.Begin());
}
TEST_F(SQLTransactionTest, RollbackOnClosedDb) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)"));
Transaction transaction(&db);
EXPECT_TRUE(transaction.Begin());
db.Close();
transaction.Rollback();
}
TEST_F(SQLTransactionTest, CommitOnClosedDb) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id INTEGER PRIMARY KEY)"));
Transaction transaction(&db);
EXPECT_TRUE(transaction.Begin());
db.Close();
EXPECT_FALSE(transaction.Commit());
}
TEST_F(SQLTransactionTest, BeginOnDestroyedDb) {
auto db = std::make_unique<Database>(test::kTestTag);
ASSERT_TRUE(db->Open(db_path_));
Transaction transaction(db.get());
db.reset();
ASSERT_FALSE(transaction.Begin());
}
TEST_F(SQLTransactionTest, RollbackOnDestroyedDb) {
auto db = std::make_unique<Database>(test::kTestTag);
ASSERT_TRUE(db->Open(db_path_));
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_F(SQLTransactionTest, CommitOnDestroyedDb) {
auto db = std::make_unique<Database>(test::kTestTag);
ASSERT_TRUE(db->Open(db_path_));
Transaction transaction(db.get());
ASSERT_TRUE(transaction.Begin());
EXPECT_TRUE(db->HasActiveTransactions());
db.reset();
ASSERT_FALSE(transaction.Commit());
}
TEST_F(SQLTransactionDeathTest, CreateTransactionFromNullptr) {
EXPECT_CHECK_DEATH(Transaction transaction(nullptr));
}
TEST_F(SQLTransactionTest, BeginBeforeDbOpen) {
Database db(test::kTestTag);
Transaction transaction(&db);
EXPECT_FALSE(transaction.Begin());
}
TEST_F(SQLTransactionTest, CloseDbInTransactionCommitErrorCallback) {
test::DriveErrorTestVfs test_vfs;
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
db.set_error_callback(
base::BindLambdaForTesting([&](int error, sql::Statement* statement) {
EXPECT_EQ(ToSqliteResultCode(error), SqliteResultCode::kFullDisk);
db.RazeAndPoison();
}));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(id)"));
Transaction transaction(&db);
ASSERT_TRUE(transaction.Begin());
ASSERT_TRUE(db.Execute("INSERT INTO rows(id) VALUES(1)"));
test_vfs.set_drive_full(true);
EXPECT_FALSE(transaction.Commit());
EXPECT_THAT(test_vfs.errors_produced(), Contains(SqliteErrorCode::kFullDisk));
ASSERT_FALSE(db.is_open());
}
int64_t GetJournalSize(const base::FilePath& db_path, bool is_wal_mode) {
base::FilePath journal_path = is_wal_mode
? Database::WriteAheadLogPath(db_path)
: Database::JournalPath(db_path);
return base::GetFileSize(journal_path).value_or(0);
}
TEST_P(SQLTransactionJournalTypeTest, TransactionRollbackDiskUnusable) {
test::DriveErrorTestVfs test_vfs;
Database db(
DatabaseOptions().set_cache_size(1).set_wal_mode(WalModeEnabled()),
test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
db.set_error_callback(base::BindLambdaForTesting(
[&](int sqlite_error, sql::Statement* statement) {
FAIL() << "Error callback should not have been called";
}));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(key, value)"));
ASSERT_TRUE(db.Execute("INSERT INTO rows(key, value) VALUES(1, 'original')"));
Transaction transaction(&db);
ASSERT_TRUE(transaction.Begin());
ASSERT_TRUE(db.Execute("UPDATE rows SET value = 'modified' WHERE key = 1"));
// Write more rows until the memory cache is full and SQLite is forced to
// write to the journal file.
const int64_t start_journal_size = GetJournalSize(db_path_, WalModeEnabled());
while (GetJournalSize(db_path_, WalModeEnabled()) == start_journal_size) {
ASSERT_TRUE(db.Execute("INSERT INTO rows(key, value) VALUES(42, 'foo')"));
}
// Make the drive unusable, causing file operations to fail with SQLITE_IOERR.
// If WAL mode is disabled, the rollback will fail because the journal file
// cannot be read to restore the main database file. If WAL mode is enabled,
// SQLite just rewind the WAL file leaving the rolled-back data there. No file
// IO usually happens. But since lots of data was written in the transaction,
// the database page 1 had to be modified to keep track of the new pages
// created. The rollback therefore needs to restore page 1 to its original
// state. It does so by reading it from the main database file. If this fails
// on an SQLITE_IOERR, the rollback aborts.
test_vfs.set_drive_unusable(true);
transaction.Rollback();
test_vfs.set_drive_unusable(false);
EXPECT_THAT(test_vfs.errors_produced(),
Contains(PrimaryErrorIs(SqliteErrorCode::kIo)));
// The rollback failed, leaving behind a hot journal. But the database is
// still valid. The next time the database is used, the hot journal will be
// used to restore the database.
Statement select(
db.GetUniqueStatement("SELECT value FROM rows WHERE key = 1"));
ASSERT_TRUE(select.Step());
EXPECT_EQ(select.ColumnString(0), "original");
}
class SqliteHardHeapLimitScope {
public:
explicit SqliteHardHeapLimitScope(int64_t limit)
: original_limit_(sqlite3_hard_heap_limit64(limit)) {}
~SqliteHardHeapLimitScope() { sqlite3_hard_heap_limit64(original_limit_); }
private:
const int64_t original_limit_;
};
TEST_F(SQLTransactionTest, TransactionBeginOutOfMemory) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
Transaction transaction(&db);
bool error_callback_invoked = false;
db.set_error_callback(base::BindLambdaForTesting(
[&](int sqlite_error, sql::Statement* statement) {
error_callback_invoked = true;
}));
// Set a hard heap limit to make SQLite fail allocating memory when parsing
// the "BEGIN" statement.
SqliteHardHeapLimitScope hard_heap_limit(1);
EXPECT_FALSE(transaction.Begin());
EXPECT_TRUE(error_callback_invoked);
}
TEST_F(SQLTransactionTest, TransactionRollbackOutOfMemory) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
bool error_callback_invoked = false;
db.set_error_callback(base::BindLambdaForTesting(
[&](int sqlite_error, sql::Statement* statement) {
error_callback_invoked = true;
}));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(key, value)"));
ASSERT_TRUE(db.Execute("INSERT INTO rows(key, value) VALUES(1, 'original')"));
Transaction transaction(&db);
ASSERT_TRUE(transaction.Begin());
ASSERT_TRUE(db.Execute("UPDATE rows SET value = 'modified' WHERE key = 1"));
{
// Set a hard heap limit to prevent SQLite from allocating memory. The
// rollback still works because the "ROLLBACK" statement was prepared
// beforehand.
SqliteHardHeapLimitScope hard_heap_limit(1);
transaction.Rollback();
}
Statement select(
db.GetUniqueStatement("SELECT value FROM rows WHERE key = 1"));
ASSERT_TRUE(select.Step());
EXPECT_EQ(select.ColumnString(0), "original");
EXPECT_FALSE(error_callback_invoked);
}
TEST_F(SQLTransactionTest, TransactionCommitOutOfMemory) {
Database db(test::kTestTag);
ASSERT_TRUE(db.Open(db_path_));
bool error_callback_invoked = false;
db.set_error_callback(base::BindLambdaForTesting(
[&](int sqlite_error, sql::Statement* statement) {
error_callback_invoked = true;
}));
ASSERT_TRUE(db.Execute("CREATE TABLE rows(key, value)"));
ASSERT_TRUE(db.Execute("INSERT INTO rows(key, value) VALUES(1, 'original')"));
Transaction transaction(&db);
ASSERT_TRUE(transaction.Begin());
ASSERT_TRUE(db.Execute("UPDATE rows SET value = 'modified' WHERE key = 1"));
{
// Set a hard heap limit to prevent SQLite from allocating memory. The
// commit still works because the "COMMIT" statement was prepared
// beforehand.
SqliteHardHeapLimitScope hard_heap_limit(1);
EXPECT_TRUE(transaction.Commit());
}
Statement select(
db.GetUniqueStatement("SELECT value FROM rows WHERE key = 1"));
ASSERT_TRUE(select.Step());
EXPECT_EQ(select.ColumnString(0), "modified");
EXPECT_FALSE(error_callback_invoked);
}
} // namespace
} // namespace sql