Avi Drissman | 69b874f | 2022-09-15 19:11:14 | [diff] [blame] | 1 | // Copyright 2013 The Chromium Authors |
shess@chromium.org | 8d40941 | 2013-07-19 18:25:30 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
Arthur Sonzogni | 68107b0 | 2024-08-20 15:38:30 | [diff] [blame] | 5 | #ifdef UNSAFE_BUFFERS_BUILD |
| 6 | // TODO(crbug.com/40285824): Remove this and convert code to safer constructs. |
| 7 | #pragma allow_unsafe_buffers |
| 8 | #endif |
| 9 | |
dcheng | e4860045 | 2015-12-28 02:24:50 | [diff] [blame] | 10 | #include"sql/recovery.h" |
avi | 0b51920 | 2015-12-21 07:25:19 | [diff] [blame] | 11 | |
dcheng | e4860045 | 2015-12-28 02:24:50 | [diff] [blame] | 12 | #include<stddef.h> |
mostynb | d82cd995 | 2016-04-11 20:05:34 | [diff] [blame] | 13 | |
Peter Kasting | 3f01b69 | 2025-01-27 19:50:47 | [diff] [blame] | 14 | #include<algorithm> |
Takuto Ikuta | 2eb6134 | 2024-05-10 09:05:35 | [diff] [blame] | 15 | #include<cstdint> |
shess@chromium.org | ae4f162 | 2013-12-08 06:49:12 | [diff] [blame] | 16 | #include<string> |
Takuto Ikuta | 2eb6134 | 2024-05-10 09:05:35 | [diff] [blame] | 17 | #include<tuple> |
dcheng | e4860045 | 2015-12-28 02:24:50 | [diff] [blame] | 18 | #include<utility> |
Takuto Ikuta | 2eb6134 | 2024-05-10 09:05:35 | [diff] [blame] | 19 | #include<vector> |
shess@chromium.org | ae4f162 | 2013-12-08 06:49:12 | [diff] [blame] | 20 | |
Peter Boström | 59a246e8 | 2025-03-17 03:55:33 | [diff] [blame] | 21 | #include"base/dcheck_is_on.h" |
Takuto Ikuta | 2eb6134 | 2024-05-10 09:05:35 | [diff] [blame] | 22 | #include"base/files/file.h" |
shess@chromium.org | ae4f162 | 2013-12-08 06:49:12 | [diff] [blame] | 23 | #include"base/files/file_path.h" |
thestig | 22dfc401 | 2014-09-05 08:29:44 | [diff] [blame] | 24 | #include"base/files/file_util.h" |
shess@chromium.org | 8d40941 | 2013-07-19 18:25:30 | [diff] [blame] | 25 | #include"base/files/scoped_temp_dir.h" |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 26 | #include"base/functional/callback_forward.h" |
Austin Sullivan | 2fbe496e | 2023-05-18 19:48:52 | [diff] [blame] | 27 | #include"base/functional/callback_helpers.h" |
shess@chromium.org | cfb82161 | 2014-07-10 00:48:06 | [diff] [blame] | 28 | #include"base/path_service.h" |
Austin Sullivan | 7b54340 | 2023-10-07 00:55:09 | [diff] [blame] | 29 | #include"base/strings/strcat.h" |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 30 | #include"base/strings/string_number_conversions.h" |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 31 | #include"base/test/bind.h" |
Austin Sullivan | 2fbe496e | 2023-05-18 19:48:52 | [diff] [blame] | 32 | #include"base/test/gtest_util.h" |
Austin Sullivan | c99e930 | 2023-05-19 02:41:45 | [diff] [blame] | 33 | #include"base/test/metrics/histogram_tester.h" |
Takuto Ikuta | 2eb6134 | 2024-05-10 09:05:35 | [diff] [blame] | 34 | #include"build/buildflag.h" |
Victor Costan | cfbfa60 | 2018-08-01 23:24:46 | [diff] [blame] | 35 | #include"sql/database.h" |
shess@chromium.org | 8d40941 | 2013-07-19 18:25:30 | [diff] [blame] | 36 | #include"sql/meta_table.h" |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 37 | #include"sql/sqlite_result_code.h" |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 38 | #include"sql/sqlite_result_code_values.h" |
shess@chromium.org | 8d40941 | 2013-07-19 18:25:30 | [diff] [blame] | 39 | #include"sql/statement.h" |
shess | 97681440 | 2016-06-21 06:56:25 | [diff] [blame] | 40 | #include"sql/test/scoped_error_expecter.h" |
shess@chromium.org | ae4f162 | 2013-12-08 06:49:12 | [diff] [blame] | 41 | #include"sql/test/test_helpers.h" |
shess@chromium.org | 8d40941 | 2013-07-19 18:25:30 | [diff] [blame] | 42 | #include"testing/gtest/include/gtest/gtest.h" |
| 43 | #include"third_party/sqlite/sqlite3.h" |
| 44 | |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 45 | namespace sql{ |
| 46 | |
shess@chromium.org | 8d40941 | 2013-07-19 18:25:30 | [diff] [blame] | 47 | namespace{ |
| 48 | |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 49 | using test::ExecuteWithResult; |
| 50 | using test::ExecuteWithResults; |
shess@chromium.org | 8d40941 | 2013-07-19 18:25:30 | [diff] [blame] | 51 | |
Austin Sullivan | c99e930 | 2023-05-19 02:41:45 | [diff] [blame] | 52 | constexprchar kRecoveryResultHistogramName[]="Sql.Recovery.Result"; |
Austin Sullivan | 5493084 | 2023-06-06 21:09:01 | [diff] [blame] | 53 | constexprchar kRecoveryResultCodeHistogramName[]="Sql.Recovery.ResultCode"; |
Austin Sullivan | c99e930 | 2023-05-19 02:41:45 | [diff] [blame] | 54 | |
shess@chromium.org | 8d40941 | 2013-07-19 18:25:30 | [diff] [blame] | 55 | // Dump consistent human-readable representation of the database |
| 56 | // schema. For tables or indices, this will contain the sql command |
| 57 | // to create the table or index. For certain automatic SQLite |
| 58 | // structures with no sql, the name is used. |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 59 | std::stringGetSchema(Database* db){ |
Victor Costan | 1d86835 | 2018-06-26 19:06:48 | [diff] [blame] | 60 | staticconstchar kSql[]= |
John Delaney | 86dbec6 | 2021-08-24 15:05:21 | [diff] [blame] | 61 | "SELECT COALESCE(sql, name) FROM sqlite_schema ORDER BY 1"; |
shess@chromium.org | 8d40941 | 2013-07-19 18:25:30 | [diff] [blame] | 62 | returnExecuteWithResults(db, kSql,"|","\n"); |
| 63 | } |
| 64 | |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 65 | // Parameterized to test with and without WAL mode enabled. |
| 66 | classSqlRecoveryTest:public testing::Test, |
| 67 | public testing::WithParamInterface<bool>{ |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 68 | public: |
Anthony Vallée-Dubois | 71d70313 | 2024-12-05 00:17:43 | [diff] [blame] | 69 | SqlRecoveryTest() |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 70 | : db_(DatabaseOptions().set_wal_mode(ShouldEnableWal()), test::kTestTag){ |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 71 | } |
| 72 | |
| 73 | boolShouldEnableWal(){returnGetParam();} |
| 74 | |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 75 | voidSetUp() override{ |
| 76 | ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); |
| 77 | db_path_= temp_dir_.GetPath().AppendASCII("recovery_test.sqlite"); |
| 78 | ASSERT_TRUE(db_.Open(db_path_)); |
| 79 | } |
| 80 | |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 81 | voidTearDown() override{ |
| 82 | if(db_.is_open()){ |
| 83 | db_.Close(); |
| 84 | } |
| 85 | // Ensure the database, along with any recovery files, are cleaned up. |
| 86 | ASSERT_TRUE(base::DeleteFile(db_path_)); |
| 87 | ASSERT_TRUE(base::DeleteFile(db_path_.AddExtensionASCII(".backup"))); |
| 88 | ASSERT_TRUE(temp_dir_.Delete()); |
| 89 | } |
| 90 | |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 91 | boolReopen(){ |
| 92 | db_.Close(); |
| 93 | return db_.Open(db_path_); |
| 94 | } |
| 95 | |
| 96 | boolOverwriteDatabaseHeader(){ |
| 97 | base::File file(db_path_, |
| 98 | base::File::FLAG_CREATE_ALWAYS| base::File::FLAG_WRITE); |
| 99 | staticconstexprchar kText[]="Now is the winter of our discontent."; |
| 100 | constexprint kTextBytes=sizeof(kText)-1; |
| 101 | return file.Write(0, kText, kTextBytes)== kTextBytes; |
| 102 | } |
| 103 | |
| 104 | protected: |
| 105 | base::ScopedTempDir temp_dir_; |
| 106 | base::FilePath db_path_; |
| 107 | Database db_; |
Austin Sullivan | c99e930 | 2023-05-19 02:41:45 | [diff] [blame] | 108 | base::HistogramTester histogram_tester_; |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 109 | }; |
| 110 | |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 111 | #if BUILDFLAG(IS_FUCHSIA) |
| 112 | // WAL + recovery is not supported on Fuchsia, so only test without WAL mode. |
| 113 | INSTANTIATE_TEST_SUITE_P(All,SqlRecoveryTest, testing::Values(false)); |
| 114 | #else |
| 115 | INSTANTIATE_TEST_SUITE_P(All,SqlRecoveryTest, testing::Bool()); |
| 116 | #endif |
Austin Sullivan | 18fdb6c9 | 2023-05-18 20:55:49 | [diff] [blame] | 117 | |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 118 | TEST_P(SqlRecoveryTest,ShouldAttemptRecovery){ |
Austin Sullivan | 18fdb6c9 | 2023-05-18 20:55:49 | [diff] [blame] | 119 | // Attempt to recover from corruption. |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 120 | ASSERT_TRUE(Recovery::ShouldAttemptRecovery(&db_, SQLITE_CORRUPT)); |
Austin Sullivan | 18fdb6c9 | 2023-05-18 20:55:49 | [diff] [blame] | 121 | |
| 122 | // Do not attempt to recover from transient errors. |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 123 | EXPECT_FALSE(Recovery::ShouldAttemptRecovery(&db_, SQLITE_BUSY)); |
Austin Sullivan | 18fdb6c9 | 2023-05-18 20:55:49 | [diff] [blame] | 124 | |
| 125 | // Do not attempt to recover null databases. |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 126 | EXPECT_FALSE(Recovery::ShouldAttemptRecovery(nullptr, SQLITE_CORRUPT)); |
Austin Sullivan | 18fdb6c9 | 2023-05-18 20:55:49 | [diff] [blame] | 127 | |
| 128 | // Do not attempt to recover closed databases. |
Anthony Vallée-Dubois | e3c9491 | 2024-12-12 16:47:47 | [diff] [blame] | 129 | Database invalid_db(test::kTestTag); |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 130 | EXPECT_FALSE(Recovery::ShouldAttemptRecovery(&invalid_db, SQLITE_CORRUPT)); |
Austin Sullivan | 18fdb6c9 | 2023-05-18 20:55:49 | [diff] [blame] | 131 | |
| 132 | // Do not attempt to recover in-memory databases. |
| 133 | ASSERT_TRUE(invalid_db.OpenInMemory()); |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 134 | EXPECT_FALSE(Recovery::ShouldAttemptRecovery(&invalid_db, SQLITE_CORRUPT)); |
Austin Sullivan | 18fdb6c9 | 2023-05-18 20:55:49 | [diff] [blame] | 135 | |
| 136 | // Return true for databases which have an error callback set, even though |
| 137 | // the error callback should be reset before recovery is attempted. |
| 138 | db_.set_error_callback(base::DoNothing()); |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 139 | EXPECT_TRUE(Recovery::ShouldAttemptRecovery(&db_, SQLITE_CORRUPT)); |
Austin Sullivan | 18fdb6c9 | 2023-05-18 20:55:49 | [diff] [blame] | 140 | } |
| 141 | |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 142 | TEST_P(SqlRecoveryTest,RecoverCorruptIndex){ |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 143 | staticconstchar kCreateTable[]= |
| 144 | "CREATE TABLE rows(indexed INTEGER NOT NULL, unindexed INTEGER NOT NULL)"; |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 145 | ASSERT_TRUE(db_.Execute(kCreateTable)); |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 146 | |
| 147 | staticconstchar kCreateIndex[]= |
| 148 | "CREATE UNIQUE INDEX rows_index ON rows(indexed)"; |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 149 | ASSERT_TRUE(db_.Execute(kCreateIndex)); |
shess@chromium.org | dd325f05 | 2013-08-06 02:37:40 | [diff] [blame] | 150 | |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 151 | // Populate the table with powers of two. These numbers make it easy to see if |
| 152 | // SUM() missed a row. |
| 153 | ASSERT_TRUE(db_.Execute("INSERT INTO rows(indexed, unindexed) VALUES(1, 1)")); |
| 154 | ASSERT_TRUE(db_.Execute("INSERT INTO rows(indexed, unindexed) VALUES(2, 2)")); |
| 155 | ASSERT_TRUE(db_.Execute("INSERT INTO rows(indexed, unindexed) VALUES(4, 4)")); |
| 156 | ASSERT_TRUE(db_.Execute("INSERT INTO rows(indexed, unindexed) VALUES(8, 8)")); |
shess@chromium.org | dd325f05 | 2013-08-06 02:37:40 | [diff] [blame] | 157 | |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 158 | db_.Close(); |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 159 | ASSERT_TRUE(test::CorruptIndexRootPage(db_path_,"rows_index")); |
shess@chromium.org | dd325f05 | 2013-08-06 02:37:40 | [diff] [blame] | 160 | ASSERT_TRUE(Reopen()); |
| 161 | |
| 162 | int error= SQLITE_OK; |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 163 | db_.set_error_callback( |
| 164 | base::BindLambdaForTesting([&](int sqlite_error,Statement* statement){ |
| 165 | error= sqlite_error; |
shess@chromium.org | dd325f05 | 2013-08-06 02:37:40 | [diff] [blame] | 166 | |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 167 | // Recovery::Begin() does not support a pre-existing error callback. |
| 168 | db_.reset_error_callback(); |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 169 | |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 170 | EXPECT_EQ( |
| 171 | Recovery::RecoverDatabase(&db_,Recovery::Strategy::kRecoverOrRaze), |
| 172 | SqliteResultCode::kOk); |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 173 | histogram_tester_.ExpectUniqueSample(kRecoveryResultHistogramName, |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 174 | Recovery::Result::kSuccess, |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 175 | /*expected_bucket_count=*/1); |
| 176 | histogram_tester_.ExpectUniqueSample(kRecoveryResultCodeHistogramName, |
| 177 | SqliteLoggedResultCode::kNoError, |
| 178 | /*expected_bucket_count=*/1); |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 179 | })); |
shess@chromium.org | dd325f05 | 2013-08-06 02:37:40 | [diff] [blame] | 180 | |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 181 | // SUM(unindexed) heavily nudges SQLite to use the table instead of the index. |
| 182 | staticconstchar kUnindexedCountSql[]="SELECT SUM(unindexed) FROM rows"; |
| 183 | EXPECT_EQ("15",ExecuteWithResult(&db_, kUnindexedCountSql)) |
| 184 | <<"Table scan should not fail due to corrupt index"; |
| 185 | EXPECT_EQ(SQLITE_OK, error) |
| 186 | <<"Successful statement execution should not invoke the error callback"; |
shess@chromium.org | dd325f05 | 2013-08-06 02:37:40 | [diff] [blame] | 187 | |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 188 | staticconstchar kIndexedCountSql[]= |
| 189 | "SELECT SUM(indexed) FROM rows INDEXED BY rows_index"; |
| 190 | EXPECT_EQ("",ExecuteWithResult(&db_, kIndexedCountSql)) |
| 191 | <<"Index scan on corrupt index should fail"; |
| 192 | EXPECT_EQ(SQLITE_CORRUPT, error) |
| 193 | <<"Error callback should be called during scan on corrupt index"; |
shess@chromium.org | dd325f05 | 2013-08-06 02:37:40 | [diff] [blame] | 194 | |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 195 | EXPECT_EQ("",ExecuteWithResult(&db_, kUnindexedCountSql)) |
| 196 | <<"Table scan should not succeed anymore on a poisoned database"; |
shess@chromium.org | dd325f05 | 2013-08-06 02:37:40 | [diff] [blame] | 197 | |
| 198 | ASSERT_TRUE(Reopen()); |
| 199 | |
| 200 | // The recovered table has consistency between the index and the table. |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 201 | EXPECT_EQ("15",ExecuteWithResult(&db_, kUnindexedCountSql)) |
| 202 | <<"Table should survive database recovery"; |
| 203 | EXPECT_EQ("15",ExecuteWithResult(&db_, kIndexedCountSql)) |
| 204 | <<"Index should be reconstructed during database recovery"; |
| 205 | } |
shess@chromium.org | dd325f05 | 2013-08-06 02:37:40 | [diff] [blame] | 206 | |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 207 | TEST_P(SqlRecoveryTest,RecoverCorruptTable){ |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 208 | // The `filler` column is used to cause a record to overflow multiple pages. |
| 209 | staticconstchar kCreateTable[]= |
| 210 | // clang-format off |
| 211 | "CREATE TABLE rows(indexed INTEGER NOT NULL, unindexed INTEGER NOT NULL," |
| 212 | "filler BLOB NOT NULL)"; |
| 213 | // clang-format on |
| 214 | ASSERT_TRUE(db_.Execute(kCreateTable)); |
| 215 | |
| 216 | staticconstchar kCreateIndex[]= |
| 217 | "CREATE UNIQUE INDEX rows_index ON rows(indexed)"; |
| 218 | ASSERT_TRUE(db_.Execute(kCreateIndex)); |
| 219 | |
| 220 | // Populate the table with powers of two. These numbers make it easy to see if |
| 221 | // SUM() missed a row. |
| 222 | ASSERT_TRUE(db_.Execute( |
| 223 | "INSERT INTO rows(indexed, unindexed, filler) VALUES(1, 1, x'31')")); |
| 224 | ASSERT_TRUE(db_.Execute( |
| 225 | "INSERT INTO rows(indexed, unindexed, filler) VALUES(2, 2, x'32')")); |
| 226 | ASSERT_TRUE(db_.Execute( |
| 227 | "INSERT INTO rows(indexed, unindexed, filler) VALUES(4, 4, x'34')")); |
| 228 | |
| 229 | constexprint kDbPageSize=4096; |
| 230 | { |
| 231 | // Insert a record that will overflow the page. |
| 232 | std::vector<uint8_t> large_buffer; |
| 233 | ASSERT_EQ(db_.page_size(), kDbPageSize) |
| 234 | <<"Page overflow relies on specific size"; |
| 235 | large_buffer.resize(kDbPageSize*2); |
Peter Kasting | 3f01b69 | 2025-01-27 19:50:47 | [diff] [blame] | 236 | std::ranges::fill(large_buffer,'8'); |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 237 | Statement insert(db_.GetUniqueStatement( |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 238 | "INSERT INTO rows(indexed,unindexed,filler) VALUES(8,8,?)")); |
| 239 | insert.BindBlob(0, large_buffer); |
| 240 | ASSERT_TRUE(insert.Run()); |
| 241 | } |
| 242 | |
| 243 | db_.Close(); |
| 244 | { |
| 245 | // Zero out the last page of the database. This should be the overflow page |
| 246 | // allocated for the last inserted row. So, deleting it should corrupt the |
| 247 | // rows table. |
| 248 | base::File db_file(db_path_, base::File::FLAG_OPEN| base::File::FLAG_READ| |
| 249 | base::File::FLAG_WRITE); |
| 250 | ASSERT_TRUE(db_file.IsValid()); |
| 251 | int64_t db_size= db_file.GetLength(); |
| 252 | ASSERT_GT(db_size, kDbPageSize) |
| 253 | <<"The database should have multiple pages"; |
| 254 | ASSERT_TRUE(db_file.SetLength(db_size- kDbPageSize)); |
| 255 | } |
| 256 | |
| 257 | { |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 258 | test::ScopedErrorExpecter expecter; |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 259 | expecter.ExpectError(SQLITE_CORRUPT); |
Dan McArdle | 4feabeb5 | 2024-01-03 15:17:13 | [diff] [blame] | 260 | ASSERT_FALSE(Reopen()); |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 261 | EXPECT_TRUE(expecter.SawExpectedErrors()); |
| 262 | // PRAGMAs executed inside Database::Open() will error out. |
| 263 | } |
| 264 | |
| 265 | int error= SQLITE_OK; |
| 266 | db_.set_error_callback( |
| 267 | base::BindLambdaForTesting([&](int sqlite_error,Statement* statement){ |
| 268 | error= sqlite_error; |
| 269 | |
| 270 | // Recovery::Begin() does not support a pre-existing error callback. |
| 271 | db_.reset_error_callback(); |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 272 | |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 273 | EXPECT_EQ( |
| 274 | Recovery::RecoverDatabase(&db_,Recovery::Strategy::kRecoverOrRaze), |
| 275 | SqliteResultCode::kOk); |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 276 | })); |
| 277 | |
| 278 | // SUM(unindexed) heavily nudges SQLite to use the table instead of the index. |
| 279 | staticconstchar kUnindexedCountSql[]="SELECT SUM(unindexed) FROM rows"; |
| 280 | EXPECT_FALSE(db_.Execute(kUnindexedCountSql)) |
| 281 | <<"Table scan on corrupt table should fail"; |
| 282 | EXPECT_EQ(SQLITE_CORRUPT, error) |
| 283 | <<"Error callback should be called during scan on corrupt index"; |
| 284 | |
| 285 | ASSERT_TRUE(Reopen()); |
| 286 | |
| 287 | // All rows should be recovered. Only the BLOB in the last row was damaged. |
| 288 | EXPECT_EQ("15",ExecuteWithResult(&db_, kUnindexedCountSql)) |
| 289 | <<"Table should survive database recovery"; |
| 290 | staticconstchar kIndexedCountSql[]= |
| 291 | "SELECT SUM(indexed) FROM rows INDEXED BY rows_index"; |
| 292 | EXPECT_EQ("15",ExecuteWithResult(&db_, kIndexedCountSql)) |
| 293 | <<"Index should be reconstructed during database recovery"; |
shess@chromium.org | dd325f05 | 2013-08-06 02:37:40 | [diff] [blame] | 294 | } |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 295 | |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 296 | TEST_P(SqlRecoveryTest,Meta){ |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 297 | constint kVersion=3; |
| 298 | constint kCompatibleVersion=2; |
| 299 | |
| 300 | { |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 301 | MetaTable meta; |
| 302 | EXPECT_TRUE(meta.Init(&db_, kVersion, kCompatibleVersion)); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 303 | EXPECT_EQ(kVersion, meta.GetVersionNumber()); |
| 304 | } |
| 305 | |
| 306 | // Test expected case where everything works. |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 307 | EXPECT_EQ(Recovery::RecoverDatabase( |
| 308 | &db_,Recovery::Strategy::kRecoverWithMetaVersionOrRaze), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 309 | SqliteResultCode::kOk); |
| 310 | histogram_tester_.ExpectUniqueSample(kRecoveryResultHistogramName, |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 311 | Recovery::Result::kSuccess, |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 312 | /*expected_bucket_count=*/1); |
| 313 | histogram_tester_.ExpectUniqueSample(kRecoveryResultCodeHistogramName, |
| 314 | SqliteLoggedResultCode::kNoError, |
| 315 | /*expected_bucket_count=*/1); |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 316 | |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 317 | ASSERT_TRUE(Reopen());// Handle was poisoned. |
| 318 | |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 319 | ASSERT_TRUE(db_.DoesTableExist("meta")); |
| 320 | |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 321 | // Test version row missing. |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 322 | EXPECT_TRUE(db_.Execute("DELETE FROM meta WHERE key = 'version'")); |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 323 | |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 324 | EXPECT_EQ(Recovery::RecoverDatabase( |
| 325 | &db_,Recovery::Strategy::kRecoverWithMetaVersionOrRaze), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 326 | SqliteResultCode::kError); |
| 327 | histogram_tester_.ExpectBucketCount( |
| 328 | kRecoveryResultHistogramName, |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 329 | Recovery::Result::kFailedMetaTableVersionWasInvalid, |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 330 | /*expected_count=*/1); |
| 331 | histogram_tester_.ExpectUniqueSample(kRecoveryResultCodeHistogramName, |
| 332 | SqliteLoggedResultCode::kNoError, |
| 333 | /*expected_bucket_count=*/2); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 334 | ASSERT_TRUE(Reopen());// Handle was poisoned. |
| 335 | |
| 336 | // Test meta table missing. |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 337 | ASSERT_FALSE(db_.DoesTableExist("meta")); |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 338 | |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 339 | EXPECT_EQ(Recovery::RecoverDatabase( |
| 340 | &db_,Recovery::Strategy::kRecoverWithMetaVersionOrRaze), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 341 | SqliteResultCode::kError); |
| 342 | histogram_tester_.ExpectBucketCount( |
| 343 | kRecoveryResultHistogramName, |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 344 | Recovery::Result::kFailedMetaTableDoesNotExist, |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 345 | /*expected_count=*/1); |
| 346 | histogram_tester_.ExpectUniqueSample(kRecoveryResultCodeHistogramName, |
| 347 | SqliteLoggedResultCode::kNoError, |
| 348 | /*expected_bucket_count=*/3); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 349 | } |
| 350 | |
| 351 | // Baseline AutoRecoverTable() test. |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 352 | TEST_P(SqlRecoveryTest,AutoRecoverTable){ |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 353 | // BIGINT and VARCHAR to test type affinity. |
Victor Costan | 1d86835 | 2018-06-26 19:06:48 | [diff] [blame] | 354 | staticconstchar kCreateSql[]= |
| 355 | "CREATE TABLE x (id BIGINT, t TEXT, v VARCHAR)"; |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 356 | ASSERT_TRUE(db_.Execute(kCreateSql)); |
| 357 | ASSERT_TRUE(db_.Execute("INSERT INTO x VALUES (11, 'This is', 'a test')")); |
| 358 | ASSERT_TRUE(db_.Execute("INSERT INTO x VALUES (5, 'That was', 'a test')")); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 359 | |
| 360 | // Save aside a copy of the original schema and data. |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 361 | const std::string orig_schema(GetSchema(&db_)); |
Victor Costan | 1d86835 | 2018-06-26 19:06:48 | [diff] [blame] | 362 | staticconstchar kXSql[]="SELECT * FROM x ORDER BY 1"; |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 363 | const std::string orig_data(ExecuteWithResults(&db_, kXSql,"|","\n")); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 364 | |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 365 | EXPECT_EQ(Recovery::RecoverDatabase(&db_,Recovery::Strategy::kRecoverOrRaze), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 366 | SqliteResultCode::kOk); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 367 | |
| 368 | // Since the database was not corrupt, the entire schema and all |
| 369 | // data should be recovered. |
| 370 | ASSERT_TRUE(Reopen()); |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 371 | ASSERT_EQ(orig_schema,GetSchema(&db_)); |
| 372 | ASSERT_EQ(orig_data,ExecuteWithResults(&db_, kXSql,"|","\n")); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 373 | |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 374 | // Recovery succeeds silently, since there's nothing to do. |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 375 | EXPECT_EQ(Recovery::RecoverDatabase(&db_,Recovery::Strategy::kRecoverOrRaze), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 376 | SqliteResultCode::kOk); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 377 | } |
| 378 | |
| 379 | // Test that default values correctly replace nulls. The recovery |
| 380 | // virtual table reads directly from the database, so DEFAULT is not |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 381 | // interpreted at that level. |
| 382 | TEST_P(SqlRecoveryTest,AutoRecoverTableWithDefault){ |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 383 | ASSERT_TRUE(db_.Execute("CREATE TABLE x (id INTEGER)")); |
| 384 | ASSERT_TRUE(db_.Execute("INSERT INTO x VALUES (5)")); |
| 385 | ASSERT_TRUE(db_.Execute("INSERT INTO x VALUES (15)")); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 386 | |
| 387 | // ALTER effectively leaves the new columns NULL in the first two |
| 388 | // rows. The row with 17 will get the default injected at insert |
| 389 | // time, while the row with 42 will get the actual value provided. |
| 390 | // Embedded "'" to make sure default-handling continues to be quoted |
| 391 | // correctly. |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 392 | ASSERT_TRUE(db_.Execute("ALTER TABLE x ADD COLUMN t TEXT DEFAULT 'a''a'")); |
| 393 | ASSERT_TRUE(db_.Execute("ALTER TABLE x ADD COLUMN b BLOB DEFAULT x'AA55'")); |
| 394 | ASSERT_TRUE(db_.Execute("ALTER TABLE x ADD COLUMN i INT DEFAULT 93")); |
| 395 | ASSERT_TRUE(db_.Execute("INSERT INTO x (id) VALUES (17)")); |
| 396 | ASSERT_TRUE(db_.Execute("INSERT INTO x VALUES (42, 'b', x'1234', 12)")); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 397 | |
| 398 | // Save aside a copy of the original schema and data. |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 399 | const std::string orig_schema(GetSchema(&db_)); |
Victor Costan | 1d86835 | 2018-06-26 19:06:48 | [diff] [blame] | 400 | staticconstchar kXSql[]="SELECT * FROM x ORDER BY 1"; |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 401 | const std::string orig_data(ExecuteWithResults(&db_, kXSql,"|","\n")); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 402 | |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 403 | std::string final_schema(orig_schema); |
| 404 | std::string final_data(orig_data); |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 405 | EXPECT_EQ(Recovery::RecoverDatabase(&db_,Recovery::Strategy::kRecoverOrRaze), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 406 | SqliteResultCode::kOk); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 407 | |
| 408 | // Since the database was not corrupt, the entire schema and all |
| 409 | // data should be recovered. |
| 410 | ASSERT_TRUE(Reopen()); |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 411 | ASSERT_EQ(final_schema,GetSchema(&db_)); |
| 412 | ASSERT_EQ(final_data,ExecuteWithResults(&db_, kXSql,"|","\n")); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 413 | } |
| 414 | |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 415 | // Test AutoRecoverTable with a ROWID alias. |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 416 | TEST_P(SqlRecoveryTest,AutoRecoverTableWithRowid){ |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 417 | // The rowid alias is almost always the first column, intentionally |
| 418 | // put it later. |
Victor Costan | 1d86835 | 2018-06-26 19:06:48 | [diff] [blame] | 419 | staticconstchar kCreateSql[]= |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 420 | "CREATE TABLE x (t TEXT, id INTEGER PRIMARY KEY NOT NULL)"; |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 421 | ASSERT_TRUE(db_.Execute(kCreateSql)); |
| 422 | ASSERT_TRUE(db_.Execute("INSERT INTO x VALUES ('This is a test', NULL)")); |
| 423 | ASSERT_TRUE(db_.Execute("INSERT INTO x VALUES ('That was a test', NULL)")); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 424 | |
| 425 | // Save aside a copy of the original schema and data. |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 426 | const std::string orig_schema(GetSchema(&db_)); |
Victor Costan | 1d86835 | 2018-06-26 19:06:48 | [diff] [blame] | 427 | staticconstchar kXSql[]="SELECT * FROM x ORDER BY 1"; |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 428 | const std::string orig_data(ExecuteWithResults(&db_, kXSql,"|","\n")); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 429 | |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 430 | EXPECT_EQ(Recovery::RecoverDatabase(&db_,Recovery::Strategy::kRecoverOrRaze), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 431 | SqliteResultCode::kOk); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 432 | |
| 433 | // Since the database was not corrupt, the entire schema and all |
| 434 | // data should be recovered. |
| 435 | ASSERT_TRUE(Reopen()); |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 436 | ASSERT_EQ(orig_schema,GetSchema(&db_)); |
| 437 | ASSERT_EQ(orig_data,ExecuteWithResults(&db_, kXSql,"|","\n")); |
kinuko@chromium.org | a8848a7 | 2013-11-18 04:18:47 | [diff] [blame] | 438 | } |
| 439 | |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 440 | voidTestRecoverDatabase(Database& db, |
| 441 | const base::FilePath& db_path, |
| 442 | bool with_meta, |
| 443 | base::OnceClosure run_recovery){ |
| 444 | constint kVersion=3; |
| 445 | constint kCompatibleVersion=2; |
| 446 | |
| 447 | if(with_meta){ |
| 448 | MetaTable meta; |
| 449 | EXPECT_TRUE(meta.Init(&db, kVersion, kCompatibleVersion)); |
| 450 | EXPECT_EQ(kVersion, meta.GetVersionNumber()); |
| 451 | EXPECT_EQ(kCompatibleVersion, meta.GetCompatibleVersionNumber()); |
| 452 | } |
| 453 | |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 454 | // As a side effect, AUTOINCREMENT creates the sqlite_sequence table for |
| 455 | // RecoverDatabase() to handle. |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 456 | ASSERT_TRUE(db.Execute( |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 457 | "CREATE TABLE table1(id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT)")); |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 458 | EXPECT_TRUE(db.Execute("INSERT INTO table1(value) VALUES('turtle')")); |
| 459 | EXPECT_TRUE(db.Execute("INSERT INTO table1(value) VALUES('truck')")); |
| 460 | EXPECT_TRUE(db.Execute("INSERT INTO table1(value) VALUES('trailer')")); |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 461 | |
| 462 | // This table needs index and a unique index to work. |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 463 | ASSERT_TRUE(db.Execute("CREATE TABLE table2(name TEXT, value TEXT)")); |
| 464 | ASSERT_TRUE(db.Execute("CREATE UNIQUE INDEX table2_name ON table2(name)")); |
| 465 | ASSERT_TRUE(db.Execute("CREATE INDEX table2_value ON table2(value)")); |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 466 | EXPECT_TRUE( |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 467 | db.Execute("INSERT INTO table2(name, value) VALUES('jim', 'telephone')")); |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 468 | EXPECT_TRUE( |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 469 | db.Execute("INSERT INTO table2(name, value) VALUES('bob', 'truck')")); |
| 470 | EXPECT_TRUE( |
| 471 | db.Execute("INSERT INTO table2(name, value) VALUES('dean', 'trailer')")); |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 472 | |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 473 | // Save aside a copy of the original schema, verifying that it has the created |
| 474 | // items plus the sqlite_sequence table. |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 475 | const std::string original_schema=GetSchema(&db); |
Peter Kasting | 3f01b69 | 2025-01-27 19:50:47 | [diff] [blame] | 476 | ASSERT_EQ(with_meta?6:4, std::ranges::count(original_schema,'\n')) |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 477 | << original_schema; |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 478 | |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 479 | staticconstexprchar kTable1Sql[]="SELECT * FROM table1 ORDER BY 1"; |
| 480 | staticconstexprchar kTable2Sql[]="SELECT * FROM table2 ORDER BY 1"; |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 481 | EXPECT_EQ("1|turtle\n2|truck\n3|trailer", |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 482 | ExecuteWithResults(&db, kTable1Sql,"|","\n")); |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 483 | EXPECT_EQ("bob|truck\ndean|trailer\njim|telephone", |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 484 | ExecuteWithResults(&db, kTable2Sql,"|","\n")); |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 485 | |
| 486 | // Database handle is valid before recovery, poisoned after. |
John Delaney | 86dbec6 | 2021-08-24 15:05:21 | [diff] [blame] | 487 | staticconstexprchar kTrivialSql[]="SELECT COUNT(*) FROM sqlite_schema"; |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 488 | EXPECT_TRUE(db.IsSQLValid(kTrivialSql)); |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 489 | |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 490 | std::move(run_recovery).Run(); |
| 491 | |
| 492 | EXPECT_FALSE(db.is_open()); |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 493 | |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 494 | // Since the database was not corrupt, the entire schema and all data should |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 495 | // be recovered. Re-open the database. |
| 496 | db.Close(); |
| 497 | ASSERT_TRUE(db.Open(db_path)); |
| 498 | ASSERT_EQ(original_schema,GetSchema(&db)); |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 499 | EXPECT_EQ("1|turtle\n2|truck\n3|trailer", |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 500 | ExecuteWithResults(&db, kTable1Sql,"|","\n")); |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 501 | EXPECT_EQ("bob|truck\ndean|trailer\njim|telephone", |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 502 | ExecuteWithResults(&db, kTable2Sql,"|","\n")); |
| 503 | |
| 504 | if(with_meta){ |
| 505 | MetaTable meta; |
| 506 | EXPECT_TRUE(meta.Init(&db, kVersion, kCompatibleVersion)); |
| 507 | EXPECT_EQ(kVersion, meta.GetVersionNumber()); |
| 508 | EXPECT_EQ(kCompatibleVersion, meta.GetCompatibleVersionNumber()); |
| 509 | } |
| 510 | } |
| 511 | |
| 512 | TEST_P(SqlRecoveryTest,RecoverDatabase){ |
| 513 | auto run_recovery= base::BindLambdaForTesting([&](){ |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 514 | EXPECT_EQ( |
| 515 | Recovery::RecoverDatabase(&db_,Recovery::Strategy::kRecoverOrRaze), |
| 516 | SqliteResultCode::kOk); |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 517 | }); |
| 518 | |
| 519 | TestRecoverDatabase(db_, db_path_,/*with_meta=*/false, |
| 520 | std::move(run_recovery)); |
| 521 | } |
| 522 | |
| 523 | TEST_P(SqlRecoveryTest,RecoverDatabaseMeta){ |
| 524 | auto run_recovery= base::BindLambdaForTesting([&](){ |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 525 | EXPECT_EQ(Recovery::RecoverDatabase( |
| 526 | &db_,Recovery::Strategy::kRecoverWithMetaVersionOrRaze), |
| 527 | SqliteResultCode::kOk); |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 528 | }); |
| 529 | |
| 530 | TestRecoverDatabase(db_, db_path_,/*with_meta=*/true, |
| 531 | std::move(run_recovery)); |
| 532 | } |
| 533 | |
| 534 | TEST_P(SqlRecoveryTest,RecoverIfPossible){ |
| 535 | auto run_recovery= base::BindLambdaForTesting([&](){ |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 536 | EXPECT_TRUE(Recovery::RecoverIfPossible( |
| 537 | &db_, SQLITE_CORRUPT,Recovery::Strategy::kRecoverOrRaze)); |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 538 | }); |
| 539 | |
| 540 | TestRecoverDatabase(db_, db_path_,/*with_meta=*/false, |
| 541 | std::move(run_recovery)); |
| 542 | } |
| 543 | |
| 544 | TEST_P(SqlRecoveryTest,RecoverIfPossibleMeta){ |
| 545 | auto run_recovery= base::BindLambdaForTesting([&](){ |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 546 | EXPECT_TRUE(Recovery::RecoverIfPossible( |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 547 | &db_, SQLITE_CORRUPT, |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 548 | Recovery::Strategy::kRecoverWithMetaVersionOrRaze)); |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 549 | }); |
| 550 | |
| 551 | TestRecoverDatabase(db_, db_path_,/*with_meta=*/true, |
| 552 | std::move(run_recovery)); |
| 553 | } |
| 554 | |
Dan McArdle | 4feabeb5 | 2024-01-03 15:17:13 | [diff] [blame] | 555 | TEST_P(SqlRecoveryTest,RecoverIfPossibleWithoutErrorCallback){ |
| 556 | auto run_recovery= base::BindLambdaForTesting([&](){ |
| 557 | // `RecoverIfPossible()` should not set an error callback. |
| 558 | EXPECT_FALSE(db_.has_error_callback()); |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 559 | bool recovery_was_attempted=Recovery::RecoverIfPossible( |
Dan McArdle | 4feabeb5 | 2024-01-03 15:17:13 | [diff] [blame] | 560 | &db_, SQLITE_CORRUPT, |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 561 | Recovery::Strategy::kRecoverWithMetaVersionOrRaze); |
Dan McArdle | 4feabeb5 | 2024-01-03 15:17:13 | [diff] [blame] | 562 | EXPECT_TRUE(recovery_was_attempted); |
| 563 | EXPECT_FALSE(db_.has_error_callback()); |
| 564 | }); |
| 565 | |
| 566 | TestRecoverDatabase(db_, db_path_,/*with_meta=*/true, |
| 567 | std::move(run_recovery)); |
| 568 | } |
| 569 | |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 570 | TEST_P(SqlRecoveryTest,RecoverIfPossibleWithErrorCallback){ |
| 571 | auto run_recovery= base::BindLambdaForTesting([&](){ |
| 572 | db_.set_error_callback(base::DoNothing()); |
| 573 | // The error callback should be reset during `RecoverIfPossible()` if |
| 574 | // recovery was attempted. |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 575 | bool recovery_was_attempted=Recovery::RecoverIfPossible( |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 576 | &db_, SQLITE_CORRUPT, |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 577 | Recovery::Strategy::kRecoverWithMetaVersionOrRaze); |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 578 | EXPECT_TRUE(recovery_was_attempted); |
| 579 | EXPECT_NE(db_.has_error_callback(), recovery_was_attempted); |
| 580 | }); |
| 581 | |
| 582 | TestRecoverDatabase(db_, db_path_,/*with_meta=*/true, |
| 583 | std::move(run_recovery)); |
| 584 | } |
| 585 | |
| 586 | TEST_P(SqlRecoveryTest,RecoverIfPossibleWithClosedDatabase){ |
| 587 | auto run_recovery= base::BindLambdaForTesting([&](){ |
| 588 | // Recovery should not be attempted on a closed database. |
| 589 | db_.Close(); |
| 590 | |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 591 | EXPECT_FALSE(Recovery::RecoverIfPossible( |
| 592 | &db_, SQLITE_CORRUPT,Recovery::Strategy::kRecoverOrRaze)); |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 593 | }); |
| 594 | |
| 595 | TestRecoverDatabase(db_, db_path_,/*with_meta=*/false, |
| 596 | std::move(run_recovery)); |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 597 | } |
| 598 | |
Austin Sullivan | 7b54340 | 2023-10-07 00:55:09 | [diff] [blame] | 599 | TEST_P(SqlRecoveryTest,RecoverIfPossibleWithPerDatabaseUma){ |
| 600 | auto run_recovery= base::BindLambdaForTesting([&](){ |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 601 | EXPECT_TRUE(Recovery::RecoverIfPossible( |
| 602 | &db_, SQLITE_CORRUPT,Recovery::Strategy::kRecoverOrRaze)); |
Austin Sullivan | 7b54340 | 2023-10-07 00:55:09 | [diff] [blame] | 603 | }); |
| 604 | |
| 605 | TestRecoverDatabase(db_, db_path_,/*with_meta=*/false, |
| 606 | std::move(run_recovery)); |
| 607 | |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 608 | // Log to the overall histograms. |
| 609 | histogram_tester_.ExpectUniqueSample(kRecoveryResultHistogramName, |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 610 | Recovery::Result::kSuccess, |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 611 | /*expected_bucket_count=*/1); |
| 612 | histogram_tester_.ExpectUniqueSample(kRecoveryResultCodeHistogramName, |
| 613 | SqliteLoggedResultCode::kNoError, |
| 614 | /*expected_bucket_count=*/1); |
| 615 | // And the histograms for this specific feature. |
| 616 | histogram_tester_.ExpectUniqueSample( |
Anthony Vallée-Dubois | dd2d901 | 2024-12-18 23:25:28 | [diff] [blame] | 617 | base::StrCat({kRecoveryResultHistogramName,".", test::kTestTag.value}), |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 618 | Recovery::Result::kSuccess, |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 619 | /*expected_bucket_count=*/1); |
| 620 | histogram_tester_.ExpectUniqueSample( |
Anthony Vallée-Dubois | dd2d901 | 2024-12-18 23:25:28 | [diff] [blame] | 621 | base::StrCat( |
| 622 | {kRecoveryResultCodeHistogramName,".", test::kTestTag.value}), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 623 | SqliteLoggedResultCode::kNoError, |
| 624 | /*expected_bucket_count=*/1); |
Austin Sullivan | 7b54340 | 2023-10-07 00:55:09 | [diff] [blame] | 625 | } |
| 626 | |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 627 | TEST_P(SqlRecoveryTest,RecoverDatabaseWithView){ |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 628 | db_.Close(); |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 629 | Database db(DatabaseOptions().set_enable_views_discouraged(true), |
| 630 | test::kTestTag); |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 631 | ASSERT_TRUE(db.Open(db_path_)); |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 632 | |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 633 | ASSERT_TRUE(db.Execute( |
| 634 | "CREATE TABLE table1(id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT)")); |
| 635 | EXPECT_TRUE(db.Execute("INSERT INTO table1(value) VALUES('turtle')")); |
| 636 | EXPECT_TRUE(db.Execute("INSERT INTO table1(value) VALUES('truck')")); |
| 637 | EXPECT_TRUE(db.Execute("INSERT INTO table1(value) VALUES('trailer')")); |
| 638 | |
| 639 | ASSERT_TRUE(db.Execute("CREATE TABLE table2(name TEXT, value TEXT)")); |
| 640 | ASSERT_TRUE(db.Execute("CREATE UNIQUE INDEX table2_name ON table2(name)")); |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 641 | EXPECT_TRUE( |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 642 | db.Execute("INSERT INTO table2(name, value) VALUES('jim', 'telephone')")); |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 643 | EXPECT_TRUE( |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 644 | db.Execute("INSERT INTO table2(name, value) VALUES('bob', 'truck')")); |
| 645 | EXPECT_TRUE( |
| 646 | db.Execute("INSERT INTO table2(name, value) VALUES('dean', 'trailer')")); |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 647 | |
| 648 | // View which is the intersection of [table1.value] and [table2.value]. |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 649 | ASSERT_TRUE(db.Execute( |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 650 | "CREATE VIEW view_table12 AS SELECT table1.value FROM table1, table2 " |
| 651 | "WHERE table1.value = table2.value")); |
| 652 | |
| 653 | staticconstexprchar kViewSql[]="SELECT * FROM view_table12 ORDER BY 1"; |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 654 | EXPECT_EQ("trailer\ntruck",ExecuteWithResults(&db, kViewSql,"|","\n")); |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 655 | |
| 656 | // Save aside a copy of the original schema, verifying that it has the created |
| 657 | // items plus the sqlite_sequence table. |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 658 | const std::string original_schema=GetSchema(&db); |
Peter Kasting | 3f01b69 | 2025-01-27 19:50:47 | [diff] [blame] | 659 | ASSERT_EQ(4, std::ranges::count(original_schema,'\n'))<< original_schema; |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 660 | |
| 661 | // Database handle is valid before recovery, poisoned after. |
John Delaney | 86dbec6 | 2021-08-24 15:05:21 | [diff] [blame] | 662 | staticconstexprchar kTrivialSql[]="SELECT COUNT(*) FROM sqlite_schema"; |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 663 | EXPECT_TRUE(db.IsSQLValid(kTrivialSql)); |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 664 | EXPECT_EQ(Recovery::RecoverDatabase(&db,Recovery::Strategy::kRecoverOrRaze), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 665 | SqliteResultCode::kOk); |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 666 | EXPECT_FALSE(db.IsSQLValid(kTrivialSql)); |
Victor Costan | 95848c0 | 2021-07-17 00:06:29 | [diff] [blame] | 667 | |
| 668 | // Since the database was not corrupt, the entire schema and all data should |
| 669 | // be recovered. |
Victor Costan | fe078f9 | 2021-07-19 20:02:59 | [diff] [blame] | 670 | db.Close(); |
| 671 | ASSERT_TRUE(db.Open(db_path_)); |
| 672 | EXPECT_EQ("trailer\ntruck",ExecuteWithResults(&db, kViewSql,"|","\n")); |
shess | a402e75 | 2016-07-02 00:25:11 | [diff] [blame] | 673 | } |
| 674 | |
shess | 00d65d4 | 2017-03-02 21:12:19 | [diff] [blame] | 675 | // When RecoverDatabase() encounters SQLITE_NOTADB, the database is deleted. |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 676 | TEST_P(SqlRecoveryTest,RecoverDatabaseDelete){ |
shess | 00d65d4 | 2017-03-02 21:12:19 | [diff] [blame] | 677 | // Create a valid database, then write junk over the header. This should lead |
| 678 | // to SQLITE_NOTADB, which will cause ATTACH to fail. |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 679 | ASSERT_TRUE(db_.Execute("CREATE TABLE x (t TEXT)")); |
| 680 | ASSERT_TRUE(db_.Execute("INSERT INTO x VALUES ('This is a test')")); |
| 681 | db_.Close(); |
| 682 | ASSERT_TRUE(OverwriteDatabaseHeader()); |
shess | 00d65d4 | 2017-03-02 21:12:19 | [diff] [blame] | 683 | |
| 684 | { |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 685 | test::ScopedErrorExpecter expecter; |
shess | 00d65d4 | 2017-03-02 21:12:19 | [diff] [blame] | 686 | expecter.ExpectError(SQLITE_NOTADB); |
| 687 | |
| 688 | // Reopen() here because it will see SQLITE_NOTADB. |
Dan McArdle | 4feabeb5 | 2024-01-03 15:17:13 | [diff] [blame] | 689 | ASSERT_FALSE(Reopen()); |
shess | 00d65d4 | 2017-03-02 21:12:19 | [diff] [blame] | 690 | |
| 691 | // This should "recover" the database by making it valid, but empty. |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 692 | EXPECT_EQ( |
| 693 | Recovery::RecoverDatabase(&db_,Recovery::Strategy::kRecoverOrRaze), |
| 694 | SqliteResultCode::kNotADatabase); |
| 695 | histogram_tester_.ExpectUniqueSample(kRecoveryResultHistogramName, |
| 696 | Recovery::Result::kFailedRecoveryRun, |
| 697 | /*expected_bucket_count=*/1); |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 698 | histogram_tester_.ExpectUniqueSample(kRecoveryResultCodeHistogramName, |
| 699 | SqliteLoggedResultCode::kNotADatabase, |
| 700 | /*expected_bucket_count=*/1); |
shess | 00d65d4 | 2017-03-02 21:12:19 | [diff] [blame] | 701 | ASSERT_TRUE(expecter.SawExpectedErrors()); |
| 702 | } |
| 703 | |
| 704 | // Recovery poisoned the handle, must re-open. |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 705 | db_.Close(); |
shess | 00d65d4 | 2017-03-02 21:12:19 | [diff] [blame] | 706 | ASSERT_TRUE(Reopen()); |
| 707 | |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 708 | EXPECT_EQ("",GetSchema(&db_)); |
shess | 00d65d4 | 2017-03-02 21:12:19 | [diff] [blame] | 709 | } |
| 710 | |
shess | 5207e145 | 2017-04-12 23:55:33 | [diff] [blame] | 711 | // Allow callers to validate the database between recovery and commit. |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 712 | TEST_P(SqlRecoveryTest,BeginRecoverDatabase){ |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 713 | staticconstchar kCreateTable[]= |
| 714 | "CREATE TABLE rows(indexed INTEGER NOT NULL, unindexed INTEGER NOT NULL)"; |
| 715 | ASSERT_TRUE(db_.Execute(kCreateTable)); |
shess | 5207e145 | 2017-04-12 23:55:33 | [diff] [blame] | 716 | |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 717 | ASSERT_TRUE(db_.Execute("CREATE UNIQUE INDEX rows_index ON rows(indexed)")); |
| 718 | |
| 719 | // Populate the table with powers of two. These numbers make it easy to see if |
| 720 | // SUM() missed a row. |
| 721 | ASSERT_TRUE(db_.Execute("INSERT INTO rows(indexed, unindexed) VALUES(1, 1)")); |
| 722 | ASSERT_TRUE(db_.Execute("INSERT INTO rows(indexed, unindexed) VALUES(2, 2)")); |
| 723 | ASSERT_TRUE(db_.Execute("INSERT INTO rows(indexed, unindexed) VALUES(4, 4)")); |
| 724 | ASSERT_TRUE(db_.Execute("INSERT INTO rows(indexed, unindexed) VALUES(8, 8)")); |
| 725 | |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 726 | db_.Close(); |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 727 | ASSERT_TRUE(test::CorruptIndexRootPage(db_path_,"rows_index")); |
shess | 5207e145 | 2017-04-12 23:55:33 | [diff] [blame] | 728 | ASSERT_TRUE(Reopen()); |
| 729 | |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 730 | staticconstchar kIndexedCountSql[]= |
| 731 | "SELECT SUM(indexed) FROM rows INDEXED BY rows_index"; |
| 732 | { |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 733 | test::ScopedErrorExpecter expecter; |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 734 | expecter.ExpectError(SQLITE_CORRUPT); |
| 735 | EXPECT_EQ("",ExecuteWithResult(&db_, kIndexedCountSql)) |
| 736 | <<"Index should still be corrupted after recovery rollback"; |
| 737 | EXPECT_TRUE(expecter.SawExpectedErrors()) |
| 738 | <<"Index should still be corrupted after recovery rollback"; |
| 739 | } |
| 740 | |
| 741 | // Run recovery code, then commit. The index is recovered. |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 742 | EXPECT_EQ(Recovery::RecoverDatabase(&db_,Recovery::Strategy::kRecoverOrRaze), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 743 | SqliteResultCode::kOk); |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 744 | db_.Close(); |
shess | 5207e145 | 2017-04-12 23:55:33 | [diff] [blame] | 745 | ASSERT_TRUE(Reopen()); |
Victor Costan | 63b2c4c4 | 2022-02-26 03:43:44 | [diff] [blame] | 746 | |
| 747 | EXPECT_EQ("15",ExecuteWithResult(&db_, kIndexedCountSql)) |
| 748 | <<"Index should be reconstructed after database recovery"; |
shess | 5207e145 | 2017-04-12 23:55:33 | [diff] [blame] | 749 | } |
| 750 | |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 751 | TEST_P(SqlRecoveryTest,AttachFailure){ |
shess | 6318811 | 2016-08-27 10:26:23 | [diff] [blame] | 752 | // Create a valid database, then write junk over the header. This should lead |
| 753 | // to SQLITE_NOTADB, which will cause ATTACH to fail. |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 754 | ASSERT_TRUE(db_.Execute("CREATE TABLE x (t TEXT)")); |
| 755 | ASSERT_TRUE(db_.Execute("INSERT INTO x VALUES ('This is a test')")); |
| 756 | db_.Close(); |
| 757 | ASSERT_TRUE(OverwriteDatabaseHeader()); |
shess | 6318811 | 2016-08-27 10:26:23 | [diff] [blame] | 758 | |
shess | 6318811 | 2016-08-27 10:26:23 | [diff] [blame] | 759 | { |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 760 | test::ScopedErrorExpecter expecter; |
shess | 6318811 | 2016-08-27 10:26:23 | [diff] [blame] | 761 | expecter.ExpectError(SQLITE_NOTADB); |
| 762 | |
| 763 | // Reopen() here because it will see SQLITE_NOTADB. |
Dan McArdle | 4feabeb5 | 2024-01-03 15:17:13 | [diff] [blame] | 764 | ASSERT_FALSE(Reopen()); |
shess | 6318811 | 2016-08-27 10:26:23 | [diff] [blame] | 765 | |
| 766 | // Begin() should fail. |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 767 | EXPECT_EQ( |
| 768 | Recovery::RecoverDatabase(&db_,Recovery::Strategy::kRecoverOrRaze), |
| 769 | SqliteResultCode::kNotADatabase); |
| 770 | histogram_tester_.ExpectUniqueSample(kRecoveryResultHistogramName, |
| 771 | Recovery::Result::kFailedRecoveryRun, |
| 772 | /*expected_bucket_count=*/1); |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 773 | histogram_tester_.ExpectUniqueSample(kRecoveryResultCodeHistogramName, |
| 774 | SqliteLoggedResultCode::kNotADatabase, |
| 775 | /*expected_bucket_count=*/1); |
shess | 6318811 | 2016-08-27 10:26:23 | [diff] [blame] | 776 | ASSERT_TRUE(expecter.SawExpectedErrors()); |
| 777 | } |
shess | 6318811 | 2016-08-27 10:26:23 | [diff] [blame] | 778 | } |
| 779 | |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 780 | // Helper for SqlRecoveryTest.PageSize. Creates a fresh db based on db_prefix, |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 781 | // with the given initial page size, and verifies it against the expected size. |
| 782 | // Then changes to the final page size and recovers, verifying that the |
| 783 | // recovered database ends up with the expected final page size. |
| 784 | voidTestPageSize(const base::FilePath& db_prefix, |
| 785 | int initial_page_size, |
| 786 | const std::string& expected_initial_page_size, |
| 787 | int final_page_size, |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 788 | const std::string& expected_final_page_size){ |
Victor Costan | 1d86835 | 2018-06-26 19:06:48 | [diff] [blame] | 789 | staticconstchar kCreateSql[]="CREATE TABLE x (t TEXT)"; |
| 790 | staticconstchar kInsertSql1[]="INSERT INTO x VALUES ('This is a test')"; |
| 791 | staticconstchar kInsertSql2[]="INSERT INTO x VALUES ('That was a test')"; |
| 792 | staticconstchar kSelectSql[]="SELECT * FROM x ORDER BY t"; |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 793 | |
| 794 | const base::FilePath db_path= db_prefix.InsertBeforeExtensionASCII( |
Raul Tambre | 6c708e3 | 2019-02-08 22:35:14 | [diff] [blame] | 795 | base::NumberToString(initial_page_size)); |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 796 | Database::Delete(db_path); |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 797 | Database db{DatabaseOptions().set_page_size(initial_page_size), |
| 798 | test::kTestTag}; |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 799 | ASSERT_TRUE(db.Open(db_path)); |
| 800 | ASSERT_TRUE(db.Execute(kCreateSql)); |
| 801 | ASSERT_TRUE(db.Execute(kInsertSql1)); |
| 802 | ASSERT_TRUE(db.Execute(kInsertSql2)); |
| 803 | ASSERT_EQ(expected_initial_page_size, |
| 804 | ExecuteWithResult(&db,"PRAGMA page_size")); |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 805 | db.Close(); |
| 806 | |
Shubham Aggarwal | 7b60fe6e | 2020-10-15 06:00:28 | [diff] [blame] | 807 | // Re-open the database while setting a new |options.page_size| in the object. |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 808 | Database recover_db(DatabaseOptions().set_page_size(final_page_size), |
| 809 | test::kTestTag); |
Shubham Aggarwal | 7b60fe6e | 2020-10-15 06:00:28 | [diff] [blame] | 810 | ASSERT_TRUE(recover_db.Open(db_path)); |
| 811 | // Recovery will use the page size set in the database object, which may not |
| 812 | // match the file's page size. |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 813 | EXPECT_EQ(Recovery::RecoverDatabase(&recover_db, |
| 814 | Recovery::Strategy::kRecoverOrRaze), |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 815 | SqliteResultCode::kOk); |
Shubham Aggarwal | 7b60fe6e | 2020-10-15 06:00:28 | [diff] [blame] | 816 | |
| 817 | // Recovery poisoned the handle, must re-open. |
| 818 | recover_db.Close(); |
| 819 | |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 820 | // Make sure the page size is read from the file. |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 821 | Database recovered_db(test::kTestTag); |
Shubham Aggarwal | 7b60fe6e | 2020-10-15 06:00:28 | [diff] [blame] | 822 | ASSERT_TRUE(recovered_db.Open(db_path)); |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 823 | ASSERT_EQ(expected_final_page_size, |
Shubham Aggarwal | 7b60fe6e | 2020-10-15 06:00:28 | [diff] [blame] | 824 | ExecuteWithResult(&recovered_db,"PRAGMA page_size")); |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 825 | EXPECT_EQ("That was a test\nThis is a test", |
Shubham Aggarwal | 7b60fe6e | 2020-10-15 06:00:28 | [diff] [blame] | 826 | ExecuteWithResults(&recovered_db, kSelectSql,"|","\n")); |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 827 | } |
| 828 | |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 829 | // Verify that Recovery maintains the page size, and the virtual table |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 830 | // works with page sizes other than SQLite's default. Also verify the case |
| 831 | // where the default page size has changed. |
Austin Sullivan | 0593ef9 | 2023-05-17 20:46:30 | [diff] [blame] | 832 | TEST_P(SqlRecoveryTest,PageSize){ |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 833 | const std::string default_page_size= |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 834 | ExecuteWithResult(&db_,"PRAGMA page_size"); |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 835 | |
Victor Costan | 7f6abbbe | 2018-07-29 02:57:27 | [diff] [blame] | 836 | // Check the default page size first. |
| 837 | EXPECT_NO_FATAL_FAILURE(TestPageSize( |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 838 | db_path_,DatabaseOptions::kDefaultPageSize, default_page_size, |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 839 | DatabaseOptions::kDefaultPageSize, default_page_size)); |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 840 | |
Victor Costan | 7f6abbbe | 2018-07-29 02:57:27 | [diff] [blame] | 841 | // Sync uses 32k pages. |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 842 | EXPECT_NO_FATAL_FAILURE( |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 843 | TestPageSize(db_path_,32768,"32768",32768,"32768")); |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 844 | |
| 845 | // Many clients use 4k pages. This is the SQLite default after 3.12.0. |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 846 | EXPECT_NO_FATAL_FAILURE(TestPageSize(db_path_,4096,"4096",4096,"4096")); |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 847 | |
| 848 | // 1k is the default page size before 3.12.0. |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 849 | EXPECT_NO_FATAL_FAILURE(TestPageSize(db_path_,1024,"1024",1024,"1024")); |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 850 | |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 851 | ASSERT_NE("2048", default_page_size); |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 852 | // Databases with no page size specified should recover to the page size of |
| 853 | // the source database. |
| 854 | EXPECT_NO_FATAL_FAILURE(TestPageSize( |
| 855 | db_path_,2048,"2048",DatabaseOptions::kDefaultPageSize,"2048")); |
shess | 0b8d593 | 2016-10-27 19:54:12 | [diff] [blame] | 856 | } |
| 857 | |
Austin Sullivan | 2fbe496e | 2023-05-18 19:48:52 | [diff] [blame] | 858 | TEST_P(SqlRecoveryTest,CannotRecoverClosedDb){ |
| 859 | db_.Close(); |
| 860 | |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 861 | EXPECT_CHECK_DEATH(std::ignore=Recovery::RecoverDatabase( |
| 862 | &db_,Recovery::Strategy::kRecoverOrRaze)); |
Austin Sullivan | 2fbe496e | 2023-05-18 19:48:52 | [diff] [blame] | 863 | } |
| 864 | |
| 865 | TEST_P(SqlRecoveryTest,CannotRecoverDbWithErrorCallback){ |
| 866 | db_.set_error_callback(base::DoNothing()); |
| 867 | |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 868 | EXPECT_CHECK_DEATH(std::ignore=Recovery::RecoverDatabase( |
| 869 | &db_,Recovery::Strategy::kRecoverOrRaze)); |
Austin Sullivan | 2fbe496e | 2023-05-18 19:48:52 | [diff] [blame] | 870 | } |
| 871 | |
Alison Gale | 71bd8f15 | 2024-04-26 22:38:20 | [diff] [blame] | 872 | // TODO(crbug.com/40199997): Ideally this would be a |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 873 | // `SqlRecoveryTest`, but `Recovery::RecoverDatabase()` does not DCHECK |
| 874 | // that it is passed a non-null database pointer and will instead likely result |
| 875 | // in unexpected behavior or crashes. |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 876 | TEST_P(SqlRecoveryTest,CannotRecoverNullDb){ |
Peter Boström | 59a246e8 | 2025-03-17 03:55:33 | [diff] [blame] | 877 | // TODO(pbos): Consider consolidating these so that DCHECK builds crash in the |
| 878 | // same spot. Probably either by upgrading DCHECKs to CHECKs, or if feasible |
| 879 | // by setting up the test to make it past failing DCHECKs to the expected |
| 880 | // CHECK. |
| 881 | if(DCHECK_IS_ON()){ |
| 882 | EXPECT_DCHECK_DEATH(std::ignore=Recovery::RecoverDatabase( |
| 883 | nullptr,Recovery::Strategy::kRecoverOrRaze)); |
| 884 | }else{ |
| 885 | EXPECT_CHECK_DEATH(std::ignore=Recovery::RecoverDatabase( |
| 886 | nullptr,Recovery::Strategy::kRecoverOrRaze)); |
| 887 | } |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 888 | } |
| 889 | |
Alison Gale | 71bd8f15 | 2024-04-26 22:38:20 | [diff] [blame] | 890 | // TODO(crbug.com/40199997): Ideally this would be a |
Austin Sullivan | 265c201b | 2023-05-31 02:04:26 | [diff] [blame] | 891 | // `SqlRecoveryTest`, but `Recovery::RecoverDatabase()` does not DCHECK |
| 892 | // whether the database is in-memory and will instead likely result in |
| 893 | // unexpected behavior or crashes. |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 894 | TEST_P(SqlRecoveryTest,CannotRecoverInMemoryDb){ |
Anthony Vallée-Dubois | e3c9491 | 2024-12-12 16:47:47 | [diff] [blame] | 895 | Database in_memory_db(test::kTestTag); |
Austin Sullivan | 2fbe496e | 2023-05-18 19:48:52 | [diff] [blame] | 896 | ASSERT_TRUE(in_memory_db.OpenInMemory()); |
| 897 | |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 898 | EXPECT_CHECK_DEATH(std::ignore=Recovery::RecoverDatabase( |
| 899 | &in_memory_db,Recovery::Strategy::kRecoverOrRaze)); |
Austin Sullivan | 2fbe496e | 2023-05-18 19:48:52 | [diff] [blame] | 900 | } |
| 901 | |
Austin Sullivan | bfea855f | 2024-01-09 21:40:20 | [diff] [blame] | 902 | // This test mimics the case where a database that was using WAL mode crashed, |
| 903 | // then next Chrome launch the database is not opened in WAL mode. This may |
| 904 | // occur when e.g. WAL mode if configured via Finch and the user not in the |
| 905 | // experiment group on the second launch of Chrome. |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 906 | TEST_P(SqlRecoveryTest, PRE_RecoverFormerlyWalDbAfterCrash){ |
Austin Sullivan | bfea855f | 2024-01-09 21:40:20 | [diff] [blame] | 907 | base::FilePath wal_db_path= |
| 908 | temp_dir_.GetPath().AppendASCII("recovery_wal_test.sqlite"); |
| 909 | |
| 910 | // Open the DB in WAL mode to set journal_mode="wal". |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 911 | Database wal_db{DatabaseOptions().set_wal_mode(true), test::kTestTag}; |
Austin Sullivan | bfea855f | 2024-01-09 21:40:20 | [diff] [blame] | 912 | ASSERT_TRUE(wal_db.Open(wal_db_path)); |
| 913 | |
| 914 | EXPECT_TRUE(wal_db.UseWALMode()); |
| 915 | EXPECT_EQ(ExecuteWithResult(&wal_db,"PRAGMA journal_mode"),"wal"); |
| 916 | |
| 917 | // Crash the database somehow, foregoing the opportunity for any cleanup. |
| 918 | wal_db.set_error_callback(base::DoNothing()); |
| 919 | EXPECT_DCHECK_DEATH(wal_db.set_error_callback(base::DoNothing())); |
| 920 | } |
| 921 | |
Evan Stade | b3be637 | 2024-02-27 21:11:09 | [diff] [blame] | 922 | TEST_P(SqlRecoveryTest,RecoverFormerlyWalDbAfterCrash){ |
Austin Sullivan | bfea855f | 2024-01-09 21:40:20 | [diff] [blame] | 923 | base::FilePath wal_db_path= |
| 924 | temp_dir_.GetPath().AppendASCII("recovery_wal_test.sqlite"); |
| 925 | |
Anthony Vallée-Dubois | 9adf0bb | 2025-02-03 17:01:41 | [diff] [blame] | 926 | Database non_wal_db{DatabaseOptions().set_wal_mode(false), test::kTestTag}; |
Austin Sullivan | bfea855f | 2024-01-09 21:40:20 | [diff] [blame] | 927 | ASSERT_TRUE(non_wal_db.Open(wal_db_path)); |
| 928 | |
| 929 | auto run_recovery= base::BindLambdaForTesting([&](){ |
Evan Stade | 4c7d6b3 | 2024-03-12 16:50:27 | [diff] [blame] | 930 | EXPECT_EQ( |
| 931 | Recovery::RecoverDatabase( |
| 932 | &non_wal_db,Recovery::Strategy::kRecoverWithMetaVersionOrRaze), |
| 933 | SqliteResultCode::kOk); |
Austin Sullivan | bfea855f | 2024-01-09 21:40:20 | [diff] [blame] | 934 | }); |
| 935 | |
| 936 | TestRecoverDatabase(non_wal_db, wal_db_path,/*with_meta=*/true, |
| 937 | std::move(run_recovery)); |
| 938 | } |
| 939 | |
shess@chromium.org | 8d40941 | 2013-07-19 18:25:30 | [diff] [blame] | 940 | }// namespace |
Victor Costan | 49a903a | 2021-05-07 22:21:00 | [diff] [blame] | 941 | |
| 942 | }// namespace sql |