diff --git a/sql/BUILD.gn b/sql/BUILD.gn index 489ebf2d600d21..0d39932180c39e 100644 --- a/sql/BUILD.gn +++ b/sql/BUILD.gn @@ -94,6 +94,7 @@ test("sql_unittests") { sources = [ "database_unittest.cc", "meta_table_unittest.cc", + "recover_module_unittest.cc", "recovery_unittest.cc", "sql_memory_dump_provider_unittest.cc", "sqlite_features_unittest.cc", diff --git a/sql/internal_api_token.h b/sql/internal_api_token.h index 2f93e59d205191..4d6b24a5bbcaca 100644 --- a/sql/internal_api_token.h +++ b/sql/internal_api_token.h @@ -7,6 +7,10 @@ namespace sql { +namespace test { +struct ColumnInfo; +} // namespace test + // Restricts access to APIs internal to the //sql package. // // This implements Java's package-private via the passkey idiom. @@ -18,6 +22,7 @@ class InternalApiToken { friend class DatabaseTestPeer; friend class Recovery; + friend struct test::ColumnInfo; }; } // namespace sql diff --git a/sql/recover_module_unittest.cc b/sql/recover_module_unittest.cc new file mode 100644 index 00000000000000..05a6527ee933e2 --- /dev/null +++ b/sql/recover_module_unittest.cc @@ -0,0 +1,1143 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include +#include + +#include "base/strings/stringprintf.h" +#include "build/build_config.h" // For OS_ANDROID used to disable a test. +#include "sql/database.h" +#include "sql/statement.h" +#include "sql/test/database_test_peer.h" +#include "sql/test/scoped_error_expecter.h" +#include "sql/test/sql_test_base.h" +#include "sql/test/test_helpers.h" +#include "sql/transaction.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/sqlite/sqlite3.h" + +namespace sql { +namespace recover { + +class RecoverModuleTest : public sql::SQLTestBase { + public: + void SetUp() override { + SQLTestBase::SetUp(); + ASSERT_TRUE(DatabaseTestPeer::EnableRecoveryExtension(&db())); + } +}; + +TEST_F(RecoverModuleTest, CreateVtable) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + EXPECT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(backing, t TEXT)")); +} +TEST_F(RecoverModuleTest, CreateVtableWithDatabaseSpecifier) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + EXPECT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(main.backing, t TEXT)")); +} +TEST_F(RecoverModuleTest, CreateVtableOnSqliteMaster) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + EXPECT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing USING recover(" + "sqlite_master, type TEXT, name TEXT, tbl_name TEXT, " + "rootpage INTEGER, sql TEXT)")); +} + +TEST_F(RecoverModuleTest, CreateVtableFailsOnNonTempTable) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_MISUSE); + EXPECT_FALSE(db().Execute( + "CREATE VIRTUAL TABLE recover_backing USING recover(backing, t TEXT)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} +TEST_F(RecoverModuleTest, CreateVtableFailsOnMissingTable) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_CORRUPT); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_missing " + "USING recover(missing, t TEXT)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} +TEST_F(RecoverModuleTest, DISABLED_CreateVtableFailsOnMissingDatabase) { + // TODO(pwnall): Enable test after removing incorrect DLOG(FATAL) from + // sql::Statement::Execute(). + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_ERROR); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(db.backing, t TEXT)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} +TEST_F(RecoverModuleTest, CreateVtableFailsOnTableWithInvalidQualifier) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_CORRUPT); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(backing invalid, t TEXT)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} +TEST_F(RecoverModuleTest, DISABLED_CreateVtableFailsOnMissingTableName) { + // TODO(pwnall): Enable test after removing incorrect DLOG(FATAL) from + // sql::Statement::Execute(). + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_ERROR); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(main., t TEXT)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} +TEST_F(RecoverModuleTest, CreateVtableFailsOnMissingSchemaSpec) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_MISUSE); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(backing)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} +TEST_F(RecoverModuleTest, CreateVtableFailsOnMissingDbName) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_MISUSE); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(.backing)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} + +TEST_F(RecoverModuleTest, ColumnTypeMappingAny) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + EXPECT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(backing, t ANY)")); + + sql::test::ColumnInfo column_info = + sql::test::ColumnInfo::Create(&db(), "temp", "recover_backing", "t"); + EXPECT_EQ("(nullptr)", column_info.data_type); + EXPECT_EQ("BINARY", column_info.collation_sequence); + EXPECT_FALSE(column_info.has_non_null_constraint); + EXPECT_FALSE(column_info.is_in_primary_key); + EXPECT_FALSE(column_info.is_auto_incremented); +} +TEST_F(RecoverModuleTest, ColumnTypeMappingAnyNotNull) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + EXPECT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(backing, t ANY NOT NULL)")); + + sql::test::ColumnInfo column_info = + sql::test::ColumnInfo::Create(&db(), "temp", "recover_backing", "t"); + EXPECT_EQ("(nullptr)", column_info.data_type); + EXPECT_EQ("BINARY", column_info.collation_sequence); + EXPECT_TRUE(column_info.has_non_null_constraint); + EXPECT_FALSE(column_info.is_in_primary_key); + EXPECT_FALSE(column_info.is_auto_incremented); +} +TEST_F(RecoverModuleTest, ColumnTypeMappingAnyStrict) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_MISUSE); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(backing, t ANY STRICT)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} + +TEST_F(RecoverModuleTest, ColumnTypeExtraKeyword) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_MISUSE); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(backing, t INTEGER SOMETHING)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} +TEST_F(RecoverModuleTest, ColumnTypeNotNullExtraKeyword) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_MISUSE); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(backing, t INTEGER NOT NULL SOMETHING)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} +TEST_F(RecoverModuleTest, ColumnTypeDoubleTypes) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_MISUSE); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(backing, t INTEGER FLOAT)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} +TEST_F(RecoverModuleTest, ColumnTypeNotNullDoubleTypes) { + ASSERT_TRUE(db().Execute("CREATE TABLE backing(t TEXT)")); + { + sql::test::ScopedErrorExpecter error_expecter; + error_expecter.ExpectError(SQLITE_MISUSE); + EXPECT_FALSE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_backing USING recover(" + "backing, t INTEGER NOT NULL TEXT)")); + EXPECT_TRUE(error_expecter.SawExpectedErrors()); + } +} + +class RecoverModuleColumnTypeMappingTest + : public RecoverModuleTest, + public ::testing::WithParamInterface< + std::tuple> { + public: + void SetUp() override { + RecoverModuleTest::SetUp(); + std::string sql = + base::StringPrintf("CREATE TABLE backing(data %s)", SchemaType()); + ASSERT_TRUE(db().Execute(sql.c_str())); + } + + protected: + void CreateRecoveryTable(const char* suffix) { + std::string sql = base::StringPrintf( + "CREATE VIRTUAL TABLE temp.recover_backing " + "USING recover(backing, data %s%s)", + SchemaType(), suffix); + ASSERT_TRUE(db().Execute(sql.c_str())); + } + + const char* SchemaType() const { return std::get<0>(GetParam()); } + const char* ExpectedType() const { return std::get<1>(GetParam()); } + bool IsAlwaysNonNull() const { return std::get<2>(GetParam()); } +}; +TEST_P(RecoverModuleColumnTypeMappingTest, Unqualified) { + CreateRecoveryTable(""); + sql::test::ColumnInfo column_info = + sql::test::ColumnInfo::Create(&db(), "temp", "recover_backing", "data"); + EXPECT_EQ(ExpectedType(), column_info.data_type); + EXPECT_EQ("BINARY", column_info.collation_sequence); + EXPECT_EQ(IsAlwaysNonNull(), column_info.has_non_null_constraint); + EXPECT_FALSE(column_info.is_in_primary_key); + EXPECT_FALSE(column_info.is_auto_incremented); +} +TEST_P(RecoverModuleColumnTypeMappingTest, NotNull) { + CreateRecoveryTable(" NOT NULL"); + sql::test::ColumnInfo column_info = + sql::test::ColumnInfo::Create(&db(), "temp", "recover_backing", "data"); + EXPECT_EQ(ExpectedType(), column_info.data_type); + EXPECT_EQ("BINARY", column_info.collation_sequence); + EXPECT_TRUE(column_info.has_non_null_constraint); + EXPECT_FALSE(column_info.is_in_primary_key); + EXPECT_FALSE(column_info.is_auto_incremented); +} +TEST_P(RecoverModuleColumnTypeMappingTest, Strict) { + CreateRecoveryTable(" STRICT"); + sql::test::ColumnInfo column_info = + sql::test::ColumnInfo::Create(&db(), "temp", "recover_backing", "data"); + EXPECT_EQ(ExpectedType(), column_info.data_type); + EXPECT_EQ("BINARY", column_info.collation_sequence); + EXPECT_EQ(IsAlwaysNonNull(), column_info.has_non_null_constraint); + EXPECT_FALSE(column_info.is_in_primary_key); + EXPECT_FALSE(column_info.is_auto_incremented); +} +TEST_P(RecoverModuleColumnTypeMappingTest, StrictNotNull) { + CreateRecoveryTable(" STRICT NOT NULL"); + sql::test::ColumnInfo column_info = + sql::test::ColumnInfo::Create(&db(), "temp", "recover_backing", "data"); + EXPECT_EQ(ExpectedType(), column_info.data_type); + EXPECT_EQ("BINARY", column_info.collation_sequence); + EXPECT_TRUE(column_info.has_non_null_constraint); + EXPECT_FALSE(column_info.is_in_primary_key); + EXPECT_FALSE(column_info.is_auto_incremented); +} +INSTANTIATE_TEST_SUITE_P( + , + RecoverModuleColumnTypeMappingTest, + ::testing::Values(std::make_tuple("TEXT", "TEXT", false), + std::make_tuple("INTEGER", "INTEGER", false), + std::make_tuple("FLOAT", "FLOAT", false), + std::make_tuple("BLOB", "BLOB", false), + std::make_tuple("NUMERIC", "NUMERIC", false), + std::make_tuple("ROWID", "INTEGER", true))); + +namespace { + +void GenerateAlteredTable(sql::Database* db) { + ASSERT_TRUE(db->Execute("CREATE TABLE altered(t TEXT)")); + ASSERT_TRUE(db->Execute("INSERT INTO altered VALUES('a')")); + ASSERT_TRUE(db->Execute("INSERT INTO altered VALUES('b')")); + ASSERT_TRUE(db->Execute("INSERT INTO altered VALUES('c')")); + ASSERT_TRUE(db->Execute( + "ALTER TABLE altered ADD COLUMN i INTEGER NOT NULL DEFAULT 10")); + ASSERT_TRUE(db->Execute("INSERT INTO altered VALUES('d', 5)")); +} + +} // namespace + +TEST_F(RecoverModuleTest, ReadFromAlteredTableNullDefaults) { + GenerateAlteredTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_altered " + "USING recover(altered, t TEXT, i INTEGER)")); + + sql::Statement statement(db().GetUniqueStatement( + "SELECT t, i FROM recover_altered ORDER BY rowid")); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ("a", statement.ColumnString(0)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(1)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ("b", statement.ColumnString(0)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(1)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ("c", statement.ColumnString(0)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(1)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ("d", statement.ColumnString(0)); + EXPECT_EQ(5, statement.ColumnInt(1)); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, ReadFromAlteredTableSkipsNulls) { + GenerateAlteredTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_altered " + "USING recover(altered, t TEXT, i INTEGER NOT NULL)")); + + sql::Statement statement(db().GetUniqueStatement( + "SELECT t, i FROM recover_altered ORDER BY rowid")); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ("d", statement.ColumnString(0)); + EXPECT_EQ(5, statement.ColumnInt(1)); + EXPECT_FALSE(statement.Step()); +} + +namespace { + +void GenerateSizedTable(sql::Database* db, + int row_count, + const std::string& prefix) { + ASSERT_TRUE(db->Execute("CREATE TABLE sized(t TEXT, i INTEGER)")); + + sql::Transaction transaction(db); + ASSERT_TRUE(transaction.Begin()); + sql::Statement statement( + db->GetUniqueStatement("INSERT INTO sized VALUES(?, ?)")); + + for (int i = 0; i < row_count; ++i) { + statement.BindString(0, base::StringPrintf("%s%d", prefix.c_str(), i)); + statement.BindInt(1, i); + ASSERT_TRUE(statement.Run()); + statement.Reset(/* clear_bound_vars= */ true); + } + ASSERT_TRUE(transaction.Commit()); +} + +} // namespace + +TEST_F(RecoverModuleTest, LeafNodes) { + GenerateSizedTable(&db(), 10, "Leaf-node-generating line "); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_sized " + "USING recover(sized, t TEXT, i INTEGER NOT NULL)")); + + sql::Statement statement( + db().GetUniqueStatement("SELECT t, i FROM recover_sized ORDER BY rowid")); + for (int i = 0; i < 10; ++i) { + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(base::StringPrintf("Leaf-node-generating line %d", i), + statement.ColumnString(0)); + EXPECT_EQ(i, statement.ColumnInt(1)); + } + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, EmptyTable) { + GenerateSizedTable(&db(), 0, ""); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_sized " + "USING recover(sized, t TEXT, i INTEGER NOT NULL)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, t, i FROM recover_sized ORDER BY rowid")); + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, SingleLevelInteriorNodes) { + GenerateSizedTable(&db(), 100, "Interior-node-generating line "); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_sized " + "USING recover(sized, t TEXT, i INTEGER NOT NULL)")); + + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, t, i FROM recover_sized ORDER BY rowid")); + for (int i = 0; i < 100; ++i) { + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(i + 1, statement.ColumnInt(0)); + EXPECT_EQ(base::StringPrintf("Interior-node-generating line %d", i), + statement.ColumnString(1)); + EXPECT_EQ(i, statement.ColumnInt(2)); + } + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, MultiLevelInteriorNodes) { + GenerateSizedTable(&db(), 5000, "Interior-node-generating line "); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_sized " + "USING recover(sized, t TEXT, i INTEGER NOT NULL)")); + + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, t, i FROM recover_sized ORDER BY rowid")); + for (int i = 0; i < 5000; ++i) { + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(i + 1, statement.ColumnInt(0)); + EXPECT_EQ(base::StringPrintf("Interior-node-generating line %d", i), + statement.ColumnString(1)); + EXPECT_EQ(i, statement.ColumnInt(2)); + } + EXPECT_FALSE(statement.Step()); +} + +namespace { + +void GenerateTypesTable(sql::Database* db) { + ASSERT_TRUE(db->Execute("CREATE TABLE types(rowtype TEXT, value)")); + + ASSERT_TRUE(db->Execute("INSERT INTO types VALUES('NULL', NULL)")); + ASSERT_TRUE(db->Execute("INSERT INTO types VALUES('INTEGER', 17)")); + ASSERT_TRUE(db->Execute("INSERT INTO types VALUES('FLOAT', 3.1415927)")); + ASSERT_TRUE(db->Execute("INSERT INTO types VALUES('TEXT', 'This is text')")); + ASSERT_TRUE(db->Execute( + "INSERT INTO types VALUES('BLOB', CAST('This is a blob' AS BLOB))")); +} + +} // namespace + +TEST_F(RecoverModuleTest, Any) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value ANY)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(1, statement.ColumnInt(0)); + EXPECT_EQ("NULL", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(2, statement.ColumnInt(0)); + EXPECT_EQ("INTEGER", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kInteger, statement.GetColumnType(2)); + EXPECT_EQ(17, statement.ColumnInt(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(3, statement.ColumnInt(0)); + EXPECT_EQ("FLOAT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kFloat, statement.GetColumnType(2)); + EXPECT_DOUBLE_EQ(3.1415927, statement.ColumnDouble(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(4, statement.ColumnInt(0)); + EXPECT_EQ("TEXT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kText, statement.GetColumnType(2)); + EXPECT_EQ("This is text", statement.ColumnString(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(5, statement.ColumnInt(0)); + EXPECT_EQ("BLOB", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kBlob, statement.GetColumnType(2)); + std::string blob_text; + ASSERT_TRUE(statement.ColumnBlobAsString(2, &blob_text)); + EXPECT_EQ("This is a blob", blob_text); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, Integers) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value INTEGER)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(1, statement.ColumnInt(0)); + EXPECT_EQ("NULL", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(2, statement.ColumnInt(0)); + EXPECT_EQ("INTEGER", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kInteger, statement.GetColumnType(2)); + EXPECT_EQ(17, statement.ColumnInt(2)); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, NonNullIntegers) { + GenerateTypesTable(&db()); + ASSERT_TRUE(db().Execute( + "CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value INTEGER NOT NULL)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(2, statement.ColumnInt(0)); + EXPECT_EQ("INTEGER", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kInteger, statement.GetColumnType(2)); + EXPECT_EQ(17, statement.ColumnInt(2)); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, Floats) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value FLOAT)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(1, statement.ColumnInt(0)); + EXPECT_EQ("NULL", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(2, statement.ColumnInt(0)); + EXPECT_EQ("INTEGER", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kFloat, statement.GetColumnType(2)); + EXPECT_EQ(17, statement.ColumnInt(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(3, statement.ColumnInt(0)); + EXPECT_EQ("FLOAT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kFloat, statement.GetColumnType(2)); + EXPECT_DOUBLE_EQ(3.1415927, statement.ColumnDouble(2)); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, NonNullFloats) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value FLOAT NOT NULL)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(2, statement.ColumnInt(0)); + EXPECT_EQ("INTEGER", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kFloat, statement.GetColumnType(2)); + EXPECT_EQ(17, statement.ColumnInt(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(3, statement.ColumnInt(0)); + EXPECT_EQ("FLOAT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kFloat, statement.GetColumnType(2)); + EXPECT_DOUBLE_EQ(3.1415927, statement.ColumnDouble(2)); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, FloatsStrict) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value FLOAT STRICT)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(1, statement.ColumnInt(0)); + EXPECT_EQ("NULL", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(3, statement.ColumnInt(0)); + EXPECT_EQ("FLOAT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kFloat, statement.GetColumnType(2)); + EXPECT_DOUBLE_EQ(3.1415927, statement.ColumnDouble(2)); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, NonNullFloatsStrict) { + GenerateTypesTable(&db()); + ASSERT_TRUE(db().Execute( + "CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value FLOAT STRICT NOT NULL)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(3, statement.ColumnInt(0)); + EXPECT_EQ("FLOAT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kFloat, statement.GetColumnType(2)); + EXPECT_DOUBLE_EQ(3.1415927, statement.ColumnDouble(2)); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, Texts) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value TEXT)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(1, statement.ColumnInt(0)); + EXPECT_EQ("NULL", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(4, statement.ColumnInt(0)); + EXPECT_EQ("TEXT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kText, statement.GetColumnType(2)); + EXPECT_EQ("This is text", statement.ColumnString(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(5, statement.ColumnInt(0)); + EXPECT_EQ("BLOB", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kBlob, statement.GetColumnType(2)); + std::string blob_text; + ASSERT_TRUE(statement.ColumnBlobAsString(2, &blob_text)); + EXPECT_EQ("This is a blob", blob_text); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, NonNullTexts) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value TEXT NOT NULL)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(4, statement.ColumnInt(0)); + EXPECT_EQ("TEXT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kText, statement.GetColumnType(2)); + EXPECT_EQ("This is text", statement.ColumnString(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(5, statement.ColumnInt(0)); + EXPECT_EQ("BLOB", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kBlob, statement.GetColumnType(2)); + std::string blob_text; + ASSERT_TRUE(statement.ColumnBlobAsString(2, &blob_text)); + EXPECT_EQ("This is a blob", blob_text); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, TextsStrict) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value TEXT STRICT)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(1, statement.ColumnInt(0)); + EXPECT_EQ("NULL", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(4, statement.ColumnInt(0)); + EXPECT_EQ("TEXT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kText, statement.GetColumnType(2)); + EXPECT_EQ("This is text", statement.ColumnString(2)); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, NonNullTextsStrict) { + GenerateTypesTable(&db()); + ASSERT_TRUE(db().Execute( + "CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value TEXT STRICT NOT NULL)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(4, statement.ColumnInt(0)); + EXPECT_EQ("TEXT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kText, statement.GetColumnType(2)); + EXPECT_EQ("This is text", statement.ColumnString(2)); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, Blobs) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value BLOB)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(1, statement.ColumnInt(0)); + EXPECT_EQ("NULL", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(5, statement.ColumnInt(0)); + EXPECT_EQ("BLOB", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kBlob, statement.GetColumnType(2)); + std::string blob_text; + ASSERT_TRUE(statement.ColumnBlobAsString(2, &blob_text)); + EXPECT_EQ("This is a blob", blob_text); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, NonNullBlobs) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value BLOB NOT NULL)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(5, statement.ColumnInt(0)); + EXPECT_EQ("BLOB", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kBlob, statement.GetColumnType(2)); + std::string blob_text; + ASSERT_TRUE(statement.ColumnBlobAsString(2, &blob_text)); + EXPECT_EQ("This is a blob", blob_text); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, AnyNonNull) { + GenerateTypesTable(&db()); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_types " + "USING recover(types, rowtype TEXT, value ANY NOT NULL)")); + sql::Statement statement(db().GetUniqueStatement( + "SELECT rowid, rowtype, value FROM recover_types")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(2, statement.ColumnInt(0)); + EXPECT_EQ("INTEGER", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kInteger, statement.GetColumnType(2)); + EXPECT_EQ(17, statement.ColumnInt(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(3, statement.ColumnInt(0)); + EXPECT_EQ("FLOAT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kFloat, statement.GetColumnType(2)); + EXPECT_DOUBLE_EQ(3.1415927, statement.ColumnDouble(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(4, statement.ColumnInt(0)); + EXPECT_EQ("TEXT", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kText, statement.GetColumnType(2)); + EXPECT_EQ("This is text", statement.ColumnString(2)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(5, statement.ColumnInt(0)); + EXPECT_EQ("BLOB", statement.ColumnString(1)); + EXPECT_EQ(sql::ColumnType::kBlob, statement.GetColumnType(2)); + std::string blob_text; + ASSERT_TRUE(statement.ColumnBlobAsString(2, &blob_text)); + EXPECT_EQ("This is a blob", blob_text); + + EXPECT_FALSE(statement.Step()); +} + +TEST_F(RecoverModuleTest, RowidAlias) { + GenerateTypesTable(&db()); + + // The id column is an alias for rowid, and its values get serialized as NULL. + ASSERT_TRUE(db().Execute( + "CREATE TABLE types2(id INTEGER PRIMARY KEY, rowtype TEXT, value)")); + ASSERT_TRUE( + db().Execute("INSERT INTO types2(id, rowtype, value) " + "SELECT rowid, rowtype, value FROM types WHERE true")); + ASSERT_TRUE(db().Execute( + "CREATE VIRTUAL TABLE temp.recover_types2 " + "USING recover(types2, id ROWID NOT NULL, rowtype TEXT, value ANY)")); + + sql::Statement statement( + db().GetUniqueStatement("SELECT id, rowid, rowtype, value FROM types2")); + + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(1, statement.ColumnInt(0)); + EXPECT_EQ(1, statement.ColumnInt(1)); + EXPECT_EQ("NULL", statement.ColumnString(2)); + EXPECT_EQ(sql::ColumnType::kNull, statement.GetColumnType(3)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(2, statement.ColumnInt(0)); + EXPECT_EQ(2, statement.ColumnInt(1)); + EXPECT_EQ("INTEGER", statement.ColumnString(2)); + EXPECT_EQ(17, statement.ColumnInt(3)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(3, statement.ColumnInt(0)); + EXPECT_EQ(3, statement.ColumnInt(1)); + EXPECT_EQ("FLOAT", statement.ColumnString(2)); + EXPECT_DOUBLE_EQ(3.1415927, statement.ColumnDouble(3)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(4, statement.ColumnInt(0)); + EXPECT_EQ(4, statement.ColumnInt(1)); + EXPECT_EQ("TEXT", statement.ColumnString(2)); + EXPECT_EQ("This is text", statement.ColumnString(3)); + ASSERT_TRUE(statement.Step()); + EXPECT_EQ(5, statement.ColumnInt(0)); + EXPECT_EQ(5, statement.ColumnInt(1)); + EXPECT_EQ("BLOB", statement.ColumnString(2)); + std::string blob_text; + ASSERT_TRUE(statement.ColumnBlobAsString(3, &blob_text)); + EXPECT_EQ("This is a blob", blob_text); + + EXPECT_FALSE(statement.Step()); +} + +#if OS_ANDROID +// The SQLite patch fails this test on Android. The rewritten recovery code +// passes this test. So, the test will be unconditionally enabled when +// https://crrev.com/c/1546942 lands. +#define MAYBE_IntegerEncodings DISABLED_IntegerEncodings +#else +#define MAYBE_IntegerEncodings IntegerEncodings +#endif +TEST_F(RecoverModuleTest, MAYBE_IntegerEncodings) { + ASSERT_TRUE(db().Execute("CREATE TABLE integers(value)")); + + const std::vector values = { + // Encoded directly in type info. + 0, + 1, + // 8-bit signed. + 2, + -2, + 127, + -128, + // 16-bit signed. + 12345, + -12345, + 32767, + -32768, + // 24-bit signed. + 1234567, + -1234567, + 8388607, + -8388608, + // 32-bit signed. + 1234567890, + -1234567890, + 2147483647, + -2147483647, + // 48-bit signed. + 123456789012345, + -123456789012345, + 140737488355327, + -140737488355327, + // 64-bit signed. + 1234567890123456789, + -1234567890123456789, + 9223372036854775807, + -9223372036854775807, + }; + sql::Statement insert( + db().GetUniqueStatement("INSERT INTO integers VALUES(?)")); + for (int64_t value : values) { + insert.BindInt64(0, value); + ASSERT_TRUE(insert.Run()); + insert.Reset(/* clear_bound_vars= */ true); + } + + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_integers " + "USING recover(integers, value INTEGER)")); + sql::Statement select( + db().GetUniqueStatement("SELECT rowid, value FROM recover_integers")); + for (size_t i = 0; i < values.size(); ++i) { + ASSERT_TRUE(select.Step()) << "Was attemping to read " << values[i]; + EXPECT_EQ(static_cast(i + 1), select.ColumnInt(0)); + EXPECT_EQ(values[i], select.ColumnInt64(1)); + } + EXPECT_FALSE(select.Step()); +} + +TEST_F(RecoverModuleTest, VarintEncodings) { + const std::vector values = { + // 1-byte varints. + 0x00, + 0x01, + 0x02, + 0x7e, + 0x7f, + // 2-byte varints + 0x80, + 0x81, + 0xff, + 0x0100, + 0x0101, + 0x1234, + 0x1ffe, + 0x1fff, + 0x3ffe, + 0x3fff, + // 3-byte varints + 0x4000, + 0x4001, + 0x0ffffe, + 0x0fffff, + 0x123456, + 0x1fedcb, + 0x1ffffe, + 0x1fffff, + // 4-byte varints + 0x200000, + 0x200001, + 0x123456, + 0xfedcba, + 0xfffffe, + 0xffffff, + 0x01234567, + 0x0fedcba9, + 0x0ffffffe, + 0x0fffffff, + // 5-byte varints + 0x10000000, + 0x10000001, + 0x12345678, + 0xfedcba98, + 0x0123456789, + 0x07fffffffe, + 0x07ffffffff, + // 6-byte varints + 0x0800000000, + 0x0800000001, + 0x123456789a, + 0xfedcba9876, + 0x0123456789ab, + 0x03fffffffffe, + 0x03ffffffffff, + // 7-byte varints + 0x040000000000, + 0x40000000001, + 0xfedcba987654, + 0x0123456789abcd, + 0x01fffffffffffe, + 0x01ffffffffffff, + // 8-byte varints + 0x02000000000000, + 0x2000000000001, + 0x0fedcba9876543, + 0x123456789abcde, + 0xfedcba98765432, + 0xfffffffffffffe, + 0xffffffffffffff, + // 9-byte positive varints + 0x0100000000000000, + 0x0100000000000001, + 0x123456789abcdef0, + 0xfedcba9876543210, + 0x7ffffffffffffffe, + 0x7fffffffffffffff, + // 9-byte negative varints + -0x01, + -0x02, + -0x7e, + -0x7f, + -0x80, + -0x81, + -0x123456789abcdef0, + -0xfedcba9876543210, + -0x7fffffffffffffff, + -0x8000000000000000, + }; + + ASSERT_TRUE(db().Execute("CREATE TABLE varints(value INTEGER PRIMARY KEY)")); + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_varints " + "USING recover(varints, value ROWID)")); + + for (int64_t value : values) { + sql::Statement insert( + db().GetUniqueStatement("INSERT INTO varints VALUES(?)")); + insert.BindInt64(0, value); + ASSERT_TRUE(insert.Run()); + + sql::Statement select( + db().GetUniqueStatement("SELECT rowid, value FROM recover_varints")); + ASSERT_TRUE(select.Step()) << "Was attemping to read " << value; + EXPECT_EQ(value, select.ColumnInt64(0)); + EXPECT_EQ(value, select.ColumnInt64(1)); + EXPECT_FALSE(select.Step()); + + ASSERT_TRUE(db().Execute("DELETE FROM varints")); + } +} + +TEST_F(RecoverModuleTest, TextEncodings) { + ASSERT_TRUE(db().Execute("CREATE TABLE encodings(t TEXT)")); + + const std::vector values = { + u8"Mjollnir", u8"Mjölnir", u8"Mjǫlnir", + u8"Mjölner", u8"Mjølner", u8"ハンマー", + }; + + sql::Statement insert( + db().GetUniqueStatement("INSERT INTO encodings VALUES(?)")); + for (const std::string& value : values) { + insert.BindString(0, value); + ASSERT_TRUE(insert.Run()); + insert.Reset(/* clear_bound_vars= */ true); + } + + ASSERT_TRUE( + db().Execute("CREATE VIRTUAL TABLE temp.recover_encodings " + "USING recover(encodings, t TEXT)")); + sql::Statement select( + db().GetUniqueStatement("SELECT rowid, t FROM recover_encodings")); + for (size_t i = 0; i < values.size(); ++i) { + ASSERT_TRUE(select.Step()); + EXPECT_EQ(static_cast(i + 1), select.ColumnInt(0)); + EXPECT_EQ(values[i], select.ColumnString(1)); + } + EXPECT_FALSE(select.Step()); +} + +namespace { + +std::string RandomString(int size) { + std::mt19937 rng; + std::uniform_int_distribution random_char(32, 127); + std::string large_value; + large_value.reserve(size); + for (int i = 0; i < size; ++i) + large_value.push_back(random_char(rng)); + return large_value; +} + +void CheckLargeValueRecovery(sql::Database* db, int value_size) { + const std::string large_value = RandomString(value_size); + + ASSERT_TRUE(db->Execute("CREATE TABLE overflow(t TEXT)")); + sql::Statement insert( + db->GetUniqueStatement("INSERT INTO overflow VALUES(?)")); + insert.BindString(0, large_value); + ASSERT_TRUE(insert.Run()); + + ASSERT_TRUE(db->Execute("VACUUM")); + + ASSERT_TRUE( + db->Execute("CREATE VIRTUAL TABLE temp.recover_overflow " + "USING recover(overflow, t TEXT)")); + sql::Statement select( + db->GetUniqueStatement("SELECT rowid, t FROM recover_overflow")); + ASSERT_TRUE(select.Step()); + EXPECT_EQ(1, select.ColumnInt(0)); + EXPECT_EQ(large_value, select.ColumnString(1)); +} + +bool HasEnabledAutoVacuum(sql::Database* db) { + sql::Statement pragma(db->GetUniqueStatement("PRAGMA auto_vacuum")); + EXPECT_TRUE(pragma.Step()); + return pragma.ColumnInt(0) != 0; +} + +// The overhead in the table page is: +// * 35 bytes - the cell size cutoff that causes a cell's payload to overflow +// * 1 byte - record header size +// * 2-3 bytes - type ID for the text column +// - texts of 58-8185 bytes use 2 bytes +// - texts of 8186-1048569 bytes use 3 bytes +// +// The overhead below assumes a 2-byte string type ID. +constexpr int kRecordOverhead = 38; + +// Each overflow page uses 4 bytes to store the pointer to the next page. +constexpr int kOverflowOverhead = 4; + +} // namespace + +TEST_F(RecoverModuleTest, ValueWithoutOverflow) { + CheckLargeValueRecovery(&db(), db().page_size() - kRecordOverhead); + int auto_vacuum_pages = HasEnabledAutoVacuum(&db()) ? 1 : 0; + ASSERT_EQ(2 + auto_vacuum_pages, sql::test::GetPageCount(&db())) + << "Database should have a root page and a leaf page"; +} + +TEST_F(RecoverModuleTest, ValueWithOneByteOverflow) { + CheckLargeValueRecovery(&db(), db().page_size() - kRecordOverhead + 1); + int auto_vacuum_pages = HasEnabledAutoVacuum(&db()) ? 1 : 0; + ASSERT_EQ(3 + auto_vacuum_pages, sql::test::GetPageCount(&db())) + << "Database should have a root page, a leaf page, and 1 overflow page"; +} + +TEST_F(RecoverModuleTest, ValueWithOneOverflowPage) { + CheckLargeValueRecovery( + &db(), db().page_size() - kRecordOverhead + db().page_size() / 2); + int auto_vacuum_pages = HasEnabledAutoVacuum(&db()) ? 1 : 0; + ASSERT_EQ(3 + auto_vacuum_pages, sql::test::GetPageCount(&db())) + << "Database should have a root page, a leaf page, and 1 overflow page"; +} + +TEST_F(RecoverModuleTest, ValueWithOneFullOverflowPage) { + CheckLargeValueRecovery(&db(), db().page_size() - kRecordOverhead + + db().page_size() - kOverflowOverhead); + int auto_vacuum_pages = HasEnabledAutoVacuum(&db()) ? 1 : 0; + ASSERT_EQ(3 + auto_vacuum_pages, sql::test::GetPageCount(&db())) + << "Database should have a root page, a leaf page, and 1 overflow page"; +} + +TEST_F(RecoverModuleTest, ValueWithOneByteSecondOverflowPage) { + CheckLargeValueRecovery(&db(), db().page_size() - kRecordOverhead + + db().page_size() - kOverflowOverhead + 1); + int auto_vacuum_pages = HasEnabledAutoVacuum(&db()) ? 1 : 0; + ASSERT_EQ(4 + auto_vacuum_pages, sql::test::GetPageCount(&db())) + << "Database should have a root page, a leaf page, and 2 overflow pages"; +} + +TEST_F(RecoverModuleTest, ValueWithTwoOverflowPages) { + CheckLargeValueRecovery(&db(), db().page_size() - kRecordOverhead + + db().page_size() - kOverflowOverhead + + db().page_size() / 2); + int auto_vacuum_pages = HasEnabledAutoVacuum(&db()) ? 1 : 0; + ASSERT_EQ(4 + auto_vacuum_pages, sql::test::GetPageCount(&db())) + << "Database should have a root page, a leaf page, and 2 overflow pages"; +} + +TEST_F(RecoverModuleTest, ValueWithTwoFullOverflowPages) { + // This value is large enough that the varint encoding of its type ID takes up + // 3 bytes, instead of 2. + CheckLargeValueRecovery(&db(), + db().page_size() - kRecordOverhead + + (db().page_size() - kOverflowOverhead) * 2 - 1); + int auto_vacuum_pages = HasEnabledAutoVacuum(&db()) ? 1 : 0; + ASSERT_EQ(4 + auto_vacuum_pages, sql::test::GetPageCount(&db())) + << "Database should have a root page, a leaf page, and 2 overflow pages"; +} + +} // namespace recover +} // namespace sql diff --git a/sql/recovery.cc b/sql/recovery.cc index 0d5000ef67c4d9..dfd80dbed1543e 100644 --- a/sql/recovery.cc +++ b/sql/recovery.cc @@ -239,7 +239,7 @@ bool Recovery::Init(const base::FilePath& db_path) { } // Enable the recover virtual table for this connection. - int rc = chrome_sqlite3_recoverVtableInit(recover_db_.db(InternalApiToken())); + int rc = EnableRecoveryExtension(&recover_db_, InternalApiToken()); if (rc != SQLITE_OK) { RecordRecoveryEvent(RECOVERY_FAILED_VIRTUAL_TABLE_INIT); LOG(ERROR) << "Failed to initialize recover module: " @@ -798,4 +798,9 @@ bool Recovery::ShouldRecover(int extended_error) { } } +// static +int Recovery::EnableRecoveryExtension(Database* db, InternalApiToken) { + return chrome_sqlite3_recoverVtableInit(db->db(InternalApiToken())); +} + } // namespace sql diff --git a/sql/recovery.h b/sql/recovery.h index aaaf780d5a6cf8..bb44a78ac941ca 100644 --- a/sql/recovery.h +++ b/sql/recovery.h @@ -12,6 +12,7 @@ #include "base/component_export.h" #include "base/macros.h" #include "sql/database.h" +#include "sql/internal_api_token.h" namespace base { class FilePath; @@ -175,6 +176,11 @@ class COMPONENT_EXPORT(SQL) Recovery { // the database. static bool ShouldRecover(int extended_error); + // Enables the "recover" SQLite extension for a database connection. + // + // Returns a SQLite error code. + static int EnableRecoveryExtension(Database* db, InternalApiToken); + private: explicit Recovery(Database* database); diff --git a/sql/test/database_test_peer.cc b/sql/test/database_test_peer.cc index 957c1cd587fbe9..be0a8af0347e50 100644 --- a/sql/test/database_test_peer.cc +++ b/sql/test/database_test_peer.cc @@ -7,6 +7,8 @@ #include "base/files/file_path.h" #include "sql/database.h" #include "sql/internal_api_token.h" +#include "sql/recovery.h" +#include "third_party/sqlite/sqlite3.h" namespace sql { @@ -24,4 +26,9 @@ bool DatabaseTestPeer::DetachDatabase(Database* db, return db->DetachDatabase(attachment_point, InternalApiToken()); } +// static +bool DatabaseTestPeer::EnableRecoveryExtension(Database* db) { + return Recovery::EnableRecoveryExtension(db, InternalApiToken()) == SQLITE_OK; +} + } // namespace sql diff --git a/sql/test/database_test_peer.h b/sql/test/database_test_peer.h index 322727d92924ab..0a01b5a03894dd 100644 --- a/sql/test/database_test_peer.h +++ b/sql/test/database_test_peer.h @@ -19,6 +19,8 @@ class DatabaseTestPeer { const base::FilePath& other_db_path, const char* attachment_point); static bool DetachDatabase(Database* db, const char* attachment_point); + + static bool EnableRecoveryExtension(Database* db); }; } // namespace sql diff --git a/sql/test/test_helpers.cc b/sql/test/test_helpers.cc index 244fab6cffdcd9..0a149a2a0a7b50 100644 --- a/sql/test/test_helpers.cc +++ b/sql/test/test_helpers.cc @@ -12,10 +12,12 @@ #include "base/files/file_util.h" #include "base/files/scoped_file.h" +#include "base/logging.h" #include "base/threading/thread_restrictions.h" #include "sql/database.h" #include "sql/statement.h" #include "testing/gtest/include/gtest/gtest.h" +#include "third_party/sqlite/sqlite3.h" namespace { @@ -143,7 +145,7 @@ bool CorruptTableOrIndex(const base::FilePath& db_path, if (!db.Open(db_path)) return false; - int page_size = 0; + int page_size = db.page_size(); if (!GetPageSize(&db, &page_size)) return false; @@ -297,5 +299,37 @@ std::string ExecuteWithResults(sql::Database* db, return ret; } +int GetPageCount(sql::Database* db) { + sql::Statement statement(db->GetUniqueStatement("PRAGMA page_count")); + CHECK(statement.Step()); + return statement.ColumnInt(0); +} + +// static +ColumnInfo ColumnInfo::Create(sql::Database* db, + const std::string& db_name, + const std::string& table_name, + const std::string& column_name) { + sqlite3* const sqlite3_db = db->db(InternalApiToken()); + + const char* data_type; + const char* collation_sequence; + int not_null; + int primary_key; + int auto_increment; + int status = sqlite3_table_column_metadata( + sqlite3_db, db_name.c_str(), table_name.c_str(), column_name.c_str(), + &data_type, &collation_sequence, ¬_null, &primary_key, + &auto_increment); + CHECK_EQ(status, SQLITE_OK) << "SQLite error: " << sqlite3_errmsg(sqlite3_db); + + // This happens when virtual tables report no type information. + if (data_type == nullptr) + data_type = "(nullptr)"; + + return {std::string(data_type), std::string(collation_sequence), + not_null != 0, primary_key != 0, auto_increment != 0}; +} + } // namespace test } // namespace sql diff --git a/sql/test/test_helpers.h b/sql/test/test_helpers.h index 07f901c11d73a2..39059e4def1279 100644 --- a/sql/test/test_helpers.h +++ b/sql/test/test_helpers.h @@ -119,6 +119,39 @@ std::string ExecuteWithResults(sql::Database* db, const char* column_sep, const char* row_sep); +// Returns the database size, in pages. Crashes on SQLite errors. +int GetPageCount(sql::Database* db); + +// Column information returned by GetColumnInfo. +// +// C++ wrapper around the out-params of sqlite3_table_column_metadata(). +struct ColumnInfo { + // Retrieves schema information for a column in a table. + // + // Crashes on SQLite errors. + // + // |db_name| should be "main" for the connection's main (opened) database, and + // "temp" for the connection's temporary (in-memory) database. + // + // This is a static method rather than a function so it can be listed in the + // InternalApiToken access control list. + static ColumnInfo Create(sql::Database* db, + const std::string& db_name, + const std::string& table_name, + const std::string& column_name) WARN_UNUSED_RESULT; + + // The native data type. Example: "INTEGER". + std::string data_type; + // Default collation sequence for sorting. Example: "BINARY". + std::string collation_sequence; + // True if the column has a "NOT NULL" constraint. + bool has_non_null_constraint; + // True if the column is included in the table's PRIMARY KEY. + bool is_in_primary_key; + // True if the column is AUTOINCREMENT. + bool is_auto_incremented; +}; + } // namespace test } // namespace sql