// 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));
}

// Tests that poisoning and re-opening a `Database` after rolling-back a nested
// transaction correctly resets the `Database` to a functional state.
TEST_F(SQLTransactionTest, PoisonAndReopenDatabaseAfterNestedRollback) {
  Database db(test::kTestTag);
  ASSERT_TRUE(db.Open(db_path_));
  ASSERT_TRUE(db.Execute("CREATE TABLE foo (a)"));
  ASSERT_TRUE(db.Execute("INSERT INTO foo (a) VALUES (111)"));
  EXPECT_EQ(CountFoo(db), 1);

  {
    Transaction outer_txn(&db);
    ASSERT_TRUE(outer_txn.Begin());
    {
      Transaction inner_txn(&db);
      ASSERT_TRUE(inner_txn.Begin());
      ASSERT_TRUE(db.Execute("INSERT INTO foo (a) VALUES (222)"));
      inner_txn.Rollback();

      db.Poison();
    }
  }

  // The database should be usable once re-opened.
  db.Close();
  ASSERT_TRUE(db.Open(db_path_));
  EXPECT_EQ(CountFoo(db), 1);
  {
    Transaction transaction(&db);
    EXPECT_TRUE(transaction.Begin());
    ASSERT_TRUE(db.Execute("INSERT INTO foo (a) VALUES (333)"));
    EXPECT_TRUE(transaction.Commit());
  }
  EXPECT_EQ(CountFoo(db), 2);
}

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
