Index: sql/recovery_unittest.cc |
diff --git a/sql/recovery_unittest.cc b/sql/recovery_unittest.cc |
index fc7c2f2dc5b725888fcbb0f4aaf6f613ffc9a286..896728bb6d63d466128597c2415c9c96eff23389 100644 |
--- a/sql/recovery_unittest.cc |
+++ b/sql/recovery_unittest.cc |
@@ -6,6 +6,7 @@ |
#include "base/file_util.h" |
#include "base/files/scoped_temp_dir.h" |
#include "base/logging.h" |
+#include "base/strings/string_number_conversions.h" |
#include "base/strings/stringprintf.h" |
#include "sql/connection.h" |
#include "sql/meta_table.h" |
@@ -32,7 +33,15 @@ std::string ExecuteWithResults(sql::Connection* db, |
for (int i = 0; i < s.ColumnCount(); ++i) { |
if (i > 0) |
ret += column_sep; |
- ret += s.ColumnString(i); |
+ if (s.ColumnType(i) == sql::COLUMN_TYPE_NULL) { |
+ ret += "<null>"; |
+ } else if (s.ColumnType(i) == sql::COLUMN_TYPE_BLOB) { |
+ ret += "<x'"; |
+ ret += base::HexEncode(s.ColumnBlob(i), s.ColumnByteLength(i)); |
+ ret += "'>"; |
+ } else { |
+ ret += s.ColumnString(i); |
+ } |
} |
} |
return ret; |
@@ -420,6 +429,327 @@ TEST_F(SQLRecoveryTest, RecoverCorruptTable) { |
const char kSelectSql[] = "SELECT v FROM x WHERE id = 0"; |
EXPECT_EQ("100", ExecuteWithResults(&db(), kSelectSql, "|", ",")); |
} |
+ |
+TEST_F(SQLRecoveryTest, Meta) { |
+ const int kVersion = 3; |
+ const int kCompatibleVersion = 2; |
+ |
+ { |
+ sql::MetaTable meta; |
+ EXPECT_TRUE(meta.Init(&db(), kVersion, kCompatibleVersion)); |
+ EXPECT_EQ(kVersion, meta.GetVersionNumber()); |
+ } |
+ |
+ // Test expected case where everything works. |
+ { |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path()); |
+ EXPECT_TRUE(recovery->SetupMeta()); |
+ int version = 0; |
+ EXPECT_TRUE(recovery->GetMetaVersionNumber(&version)); |
+ EXPECT_EQ(kVersion, version); |
+ |
+ sql::Recovery::Rollback(recovery.Pass()); |
+ } |
+ ASSERT_TRUE(Reopen()); // Handle was poisoned. |
+ |
+ // Test version row missing. |
+ EXPECT_TRUE(db().Execute("DELETE FROM meta WHERE key = 'version'")); |
+ { |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path()); |
+ EXPECT_TRUE(recovery->SetupMeta()); |
+ int version = 0; |
+ EXPECT_FALSE(recovery->GetMetaVersionNumber(&version)); |
+ EXPECT_EQ(0, version); |
+ |
+ sql::Recovery::Rollback(recovery.Pass()); |
+ } |
+ ASSERT_TRUE(Reopen()); // Handle was poisoned. |
+ |
+ // Test meta table missing. |
+ EXPECT_TRUE(db().Execute("DROP TABLE meta")); |
+ { |
+ sql::ScopedErrorIgnorer ignore_errors; |
+ ignore_errors.IgnoreError(SQLITE_CORRUPT); // From virtual table. |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path()); |
+ EXPECT_FALSE(recovery->SetupMeta()); |
+ ASSERT_TRUE(ignore_errors.CheckIgnoredErrors()); |
+ } |
+} |
+ |
+// Baseline AutoRecoverTable() test. |
+TEST_F(SQLRecoveryTest, AutoRecoverTable) { |
+ // BIGINT and VARCHAR to test type affinity. |
+ const char kCreateSql[] = "CREATE TABLE x (id BIGINT, t TEXT, v VARCHAR)"; |
+ ASSERT_TRUE(db().Execute(kCreateSql)); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (11, 'This is', 'a test')")); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (5, 'That was', 'a test')")); |
+ |
+ // Save aside a copy of the original schema and data. |
+ const std::string orig_schema(GetSchema(&db())); |
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1"; |
+ const std::string orig_data(ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+ |
+ // Create a lame-duck table which will not be propagated by recovery to |
+ // detect that the recovery code actually ran. |
+ ASSERT_TRUE(db().Execute("CREATE TABLE y (c TEXT)")); |
+ ASSERT_NE(orig_schema, GetSchema(&db())); |
+ |
+ { |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path()); |
+ ASSERT_TRUE(recovery->db()->Execute(kCreateSql)); |
+ |
+ // Save a copy of the temp db's schema before recovering the table. |
+ const char kTempSchemaSql[] = "SELECT name, sql FROM sqlite_temp_master"; |
+ const std::string temp_schema( |
+ ExecuteWithResults(recovery->db(), kTempSchemaSql, "|", "\n")); |
+ |
+ size_t rows = 0; |
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 0, &rows)); |
+ EXPECT_EQ(2u, rows); |
+ |
+ // Test that any additional temp tables were cleaned up. |
+ EXPECT_EQ(temp_schema, |
+ ExecuteWithResults(recovery->db(), kTempSchemaSql, "|", "\n")); |
+ |
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass())); |
+ } |
+ |
+ // Since the database was not corrupt, the entire schema and all |
+ // data should be recovered. |
+ ASSERT_TRUE(Reopen()); |
+ ASSERT_EQ(orig_schema, GetSchema(&db())); |
+ ASSERT_EQ(orig_data, ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+ |
+ // Recovery fails if the target table doesn't exist. |
+ { |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path()); |
+ ASSERT_TRUE(recovery->db()->Execute(kCreateSql)); |
+ |
+ // TODO(shess): Should this failure implicitly lead to Raze()? |
+ size_t rows = 0; |
+ EXPECT_FALSE(recovery->AutoRecoverTable("y", 0, &rows)); |
+ |
+ sql::Recovery::Unrecoverable(recovery.Pass()); |
+ } |
+} |
+ |
+// Test that default values correctly replace nulls. The recovery |
+// virtual table reads directly from the database, so DEFAULT is not |
+// interpretted at that level. |
+TEST_F(SQLRecoveryTest, AutoRecoverTableWithDefault) { |
+ ASSERT_TRUE(db().Execute("CREATE TABLE x (id INTEGER)")); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (5)")); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (15)")); |
+ |
+ // ALTER effectively leaves the new columns NULL in the first two |
+ // rows. The row with 17 will get the default injected at insert |
+ // time, while the row with 42 will get the actual value provided. |
+ // Embedded "'" to make sure default-handling continues to be quoted |
+ // correctly. |
+ ASSERT_TRUE(db().Execute("ALTER TABLE x ADD COLUMN t TEXT DEFAULT 'a''a'")); |
+ ASSERT_TRUE(db().Execute("ALTER TABLE x ADD COLUMN b BLOB DEFAULT x'AA55'")); |
+ ASSERT_TRUE(db().Execute("ALTER TABLE x ADD COLUMN i INT DEFAULT 93")); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x (id) VALUES (17)")); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (42, 'b', x'1234', 12)")); |
+ |
+ // Save aside a copy of the original schema and data. |
+ const std::string orig_schema(GetSchema(&db())); |
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1"; |
+ const std::string orig_data(ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+ |
+ // Create a lame-duck table which will not be propagated by recovery to |
+ // detect that the recovery code actually ran. |
+ ASSERT_TRUE(db().Execute("CREATE TABLE y (c TEXT)")); |
+ ASSERT_NE(orig_schema, GetSchema(&db())); |
+ |
+ // Mechanically adjust the stored schema and data to allow detecting |
+ // where the default value is coming from. The target table is just |
+ // like the original with the default for [t] changed, to signal |
+ // defaults coming from the recovery system. The two %5 rows should |
+ // get the target-table default for [t], while the others should get |
+ // the source-table default. |
+ std::string final_schema(orig_schema); |
+ std::string final_data(orig_data); |
+ size_t pos; |
+ while ((pos = final_schema.find("'a''a'")) != std::string::npos) { |
+ final_schema.replace(pos, 6, "'c''c'"); |
+ } |
+ while ((pos = final_data.find("5|a'a")) != std::string::npos) { |
+ final_data.replace(pos, 5, "5|c'c"); |
+ } |
+ |
+ { |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path()); |
+ // Different default to detect which table provides the default. |
+ ASSERT_TRUE(recovery->db()->Execute(final_schema.c_str())); |
+ |
+ size_t rows = 0; |
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 0, &rows)); |
+ EXPECT_EQ(4u, rows); |
+ |
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass())); |
+ } |
+ |
+ // Since the database was not corrupt, the entire schema and all |
+ // data should be recovered. |
+ ASSERT_TRUE(Reopen()); |
+ ASSERT_EQ(final_schema, GetSchema(&db())); |
+ ASSERT_EQ(final_data, ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+} |
+ |
+// Test that rows with NULL in a NOT NULL column are filtered |
+// correctly. In the wild, this would probably happen due to |
+// corruption, but here it is simulated by recovering a table which |
+// allowed nulls into a table which does not. |
+TEST_F(SQLRecoveryTest, AutoRecoverTableNullFilter) { |
+ const char kOrigSchema[] = "CREATE TABLE x (id INTEGER, t TEXT)"; |
+ const char kFinalSchema[] = "CREATE TABLE x (id INTEGER, t TEXT NOT NULL)"; |
+ |
+ ASSERT_TRUE(db().Execute(kOrigSchema)); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (5, null)")); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (15, 'this is a test')")); |
+ |
+ // Create a lame-duck table which will not be propagated by recovery to |
+ // detect that the recovery code actually ran. |
+ ASSERT_EQ(kOrigSchema, GetSchema(&db())); |
+ ASSERT_TRUE(db().Execute("CREATE TABLE y (c TEXT)")); |
+ ASSERT_NE(kOrigSchema, GetSchema(&db())); |
+ |
+ { |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path()); |
+ ASSERT_TRUE(recovery->db()->Execute(kFinalSchema)); |
+ |
+ size_t rows = 0; |
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 0, &rows)); |
+ EXPECT_EQ(1u, rows); |
+ |
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass())); |
+ } |
+ |
+ // The schema should be the same, but only one row of data should |
+ // have been recovered. |
+ ASSERT_TRUE(Reopen()); |
+ ASSERT_EQ(kFinalSchema, GetSchema(&db())); |
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1"; |
+ ASSERT_EQ("15|this is a test", ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+} |
+ |
+// Test AutoRecoverTable with a ROWID alias. |
+TEST_F(SQLRecoveryTest, AutoRecoverTableWithRowid) { |
+ // The rowid alias is almost always the first column, intentionally |
+ // put it later. |
+ const char kCreateSql[] = |
+ "CREATE TABLE x (t TEXT, id INTEGER PRIMARY KEY NOT NULL)"; |
+ ASSERT_TRUE(db().Execute(kCreateSql)); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES ('This is a test', null)")); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES ('That was a test', null)")); |
+ |
+ // Save aside a copy of the original schema and data. |
+ const std::string orig_schema(GetSchema(&db())); |
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1"; |
+ const std::string orig_data(ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+ |
+ // Create a lame-duck table which will not be propagated by recovery to |
+ // detect that the recovery code actually ran. |
+ ASSERT_TRUE(db().Execute("CREATE TABLE y (c TEXT)")); |
+ ASSERT_NE(orig_schema, GetSchema(&db())); |
+ |
+ { |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path()); |
+ ASSERT_TRUE(recovery->db()->Execute(kCreateSql)); |
+ |
+ size_t rows = 0; |
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 0, &rows)); |
+ EXPECT_EQ(2u, rows); |
+ |
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass())); |
+ } |
+ |
+ // Since the database was not corrupt, the entire schema and all |
+ // data should be recovered. |
+ ASSERT_TRUE(Reopen()); |
+ ASSERT_EQ(orig_schema, GetSchema(&db())); |
+ ASSERT_EQ(orig_data, ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+} |
+ |
+// Test that a compound primary key doesn't fire the ROWID code. |
+TEST_F(SQLRecoveryTest, AutoRecoverTableWithCompoundKey) { |
+ const char kCreateSql[] = |
+ "CREATE TABLE x (" |
+ "id INTEGER NOT NULL," |
+ "id2 TEXT NOT NULL," |
+ "t TEXT," |
+ "PRIMARY KEY (id, id2)" |
+ ")"; |
+ ASSERT_TRUE(db().Execute(kCreateSql)); |
+ |
+ // NOTE(shess): Do not accidentally use [id] 1, 2, 3, as those will |
+ // be the ROWID values. |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (1, 'a', 'This is a test')")); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (1, 'b', 'That was a test')")); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (2, 'a', 'Another test')")); |
+ |
+ // Save aside a copy of the original schema and data. |
+ const std::string orig_schema(GetSchema(&db())); |
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1"; |
+ const std::string orig_data(ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+ |
+ // Create a lame-duck table which will not be propagated by recovery to |
+ // detect that the recovery code actually ran. |
+ ASSERT_TRUE(db().Execute("CREATE TABLE y (c TEXT)")); |
+ ASSERT_NE(orig_schema, GetSchema(&db())); |
+ |
+ { |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path()); |
+ ASSERT_TRUE(recovery->db()->Execute(kCreateSql)); |
+ |
+ size_t rows = 0; |
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 0, &rows)); |
+ EXPECT_EQ(3u, rows); |
+ |
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass())); |
+ } |
+ |
+ // Since the database was not corrupt, the entire schema and all |
+ // data should be recovered. |
+ ASSERT_TRUE(Reopen()); |
+ ASSERT_EQ(orig_schema, GetSchema(&db())); |
+ ASSERT_EQ(orig_data, ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+} |
+ |
+// Test |extend_columns| support. |
+TEST_F(SQLRecoveryTest, AutoRecoverTableExtendColumns) { |
+ const char kCreateSql[] = "CREATE TABLE x (id INTEGER PRIMARY KEY, t0 TEXT)"; |
+ ASSERT_TRUE(db().Execute(kCreateSql)); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (1, 'This is')")); |
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (2, 'That was')")); |
+ |
+ // Save aside a copy of the original schema and data. |
+ const std::string orig_schema(GetSchema(&db())); |
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1"; |
+ const std::string orig_data(ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+ |
+ // Modify the table to add a column, and add data to that column. |
+ ASSERT_TRUE(db().Execute("ALTER TABLE x ADD COLUMN t1 TEXT")); |
+ ASSERT_TRUE(db().Execute("UPDATE x SET t1 = 'a test'")); |
+ ASSERT_NE(orig_schema, GetSchema(&db())); |
+ ASSERT_NE(orig_data, ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+ |
+ { |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path()); |
+ ASSERT_TRUE(recovery->db()->Execute(kCreateSql)); |
+ size_t rows = 0; |
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 1, &rows)); |
+ EXPECT_EQ(2u, rows); |
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass())); |
+ } |
+ |
+ // Since the database was not corrupt, the entire schema and all |
+ // data should be recovered. |
+ ASSERT_TRUE(Reopen()); |
+ ASSERT_EQ(orig_schema, GetSchema(&db())); |
+ ASSERT_EQ(orig_data, ExecuteWithResults(&db(), kXSql, "|", "\n")); |
+} |
#endif // !defined(USE_SYSTEM_SQLITE) |
} // namespace |