Index: sql/recovery.cc |
diff --git a/sql/recovery.cc b/sql/recovery.cc |
index 7b9ca9f6fee65724a310e1f34620b97faf60bc7b..9e6932777f21e395822f9c0c27c6936fe59811c6 100644 |
--- a/sql/recovery.cc |
+++ b/sql/recovery.cc |
@@ -84,6 +84,31 @@ enum RecoveryEventType { |
// No version key in recovery meta table. |
RECOVERY_FAILED_META_NO_VERSION, |
+ // Automatically recovered entire database successfully. |
Mark P
2016/04/07 23:09:43
This comment sounds like it could equally apply to
Scott Hess - ex-Googler
2016/04/15 00:38:15
The earlier applies to a specific table, this appl
Mark P
2016/04/19 23:34:27
No, I thought you were being sloppy about database
|
+ RECOVERY_SUCCESS_AUTORECOVERDB, |
+ |
+ // Database was so broken recovery couldn't be entered. |
+ RECOVERY_FAILED_AUTORECOVERDB_BEGIN, |
+ |
+ // Failed to copy schema to autorecover db. |
+ RECOVERY_FAILED_AUTORECOVERDB_SCHEMA, |
+ |
+ // Distinguish failure in querying schema from failure in creating schema. |
Mark P
2016/04/08 20:12:06
Please clearly state these are subcategories of RE
Scott Hess - ex-Googler
2016/04/15 00:38:14
Done.
|
+ RECOVERY_FAILED_AUTORECOVERDB_SCHEMACREATE, |
+ RECOVERY_FAILED_AUTORECOVERDB_SCHEMASELECT, |
+ |
+ // Failed querying tables to recover. Should be impossible. |
+ RECOVERY_FAILED_AUTORECOVERDB_NAMESELECT, |
+ |
+ // Failed to recover an individual table. |
+ RECOVERY_FAILED_AUTORECOVERDB_TABLE, |
+ |
+ // Failed to recover [sqlite_sequence] table. |
+ RECOVERY_FAILED_AUTORECOVERDB_SEQUENCE, |
+ |
+ // Failed to recover triggers or views or virtual tables. |
+ RECOVERY_FAILED_AUTORECOVERDB_AUX, |
+ |
// Always keep this at the end. |
RECOVERY_EVENT_MAX, |
}; |
@@ -509,4 +534,161 @@ bool Recovery::GetMetaVersionNumber(int* version) { |
return true; |
} |
+namespace { |
+ |
+// Collect each SQL statement from [corrupt.sqlite_master] which starts with |
+// |prefix|, and apply it to [main]. Skip 'sqlite_sequence', which will be |
Mark P
2016/04/07 23:09:43
This comment is too broad. Below you explicitly a
Scott Hess - ex-Googler
2016/04/15 00:38:15
I agree that the example case isn't what I want, b
|
+// created on demand by SQLite if any tables use AUTOINCREMENT. |
Mark P
2016/04/07 23:09:43
This comment should mention the meaning of the ret
Scott Hess - ex-Googler
2016/04/15 00:38:15
Done.
|
+bool SchemaCopyHelper(Connection* db, const char* prefix) { |
+ const size_t prefix_len = strlen(prefix); |
+ sql::Statement s(db->GetUniqueStatement( |
+ "SELECT sql FROM corrupt.sqlite_master " |
+ "WHERE substr(sql, 1, ?)=? AND name<>'sqlite_sequence'")); |
Mark P
2016/04/07 23:09:43
What happens if prefix_length > length(sql)? What
Scott Hess - ex-Googler
2016/04/15 00:38:14
I think I'll just rewrite this as a full table sca
|
+ s.BindInt(0, prefix_len); |
+ s.BindCString(1, prefix); |
+ |
+ // TODO(shess): In case of row duplication when balancing a [sqlite_master] |
+ // btree, this could inject IF NOT EXISTS. I suspect the occurance is too low |
+ // to be worth the complexity, though. |
Mark P
2016/04/07 23:09:43
Can you expand this slightly
to be worth complexit
Scott Hess - ex-Googler
2016/04/15 00:38:15
I think I'll remove the comment entirely. A commo
|
+ |
Mark P
2016/04/07 23:09:43
This blank line here feels unnecessary (but then I
Scott Hess - ex-Googler
2016/04/15 00:38:15
Done.
|
+ while (s.Step()) { |
Mark P
2016/04/07 23:09:43
Should all these executions be wrapped in a transa
Scott Hess - ex-Googler
2016/04/15 00:38:14
Recovery happens to a temporary database, which is
|
+ std::string sql = s.ColumnString(0); |
+ sql.insert(prefix_len, "main."); |
+ if (!db->Execute(sql.c_str())) { |
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_SCHEMACREATE); |
+ return false; |
+ } |
+ } |
+ if (!s.Succeeded()) { |
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_SCHEMASELECT); |
+ return false; |
+ } |
+ return true; |
+} |
+ |
+} // namespace |
+ |
+// This method is derived from SQLite's vacuum.c. VACUUM operates very |
+// similarily, creating a new database, populating the schema, then copying the |
+// data. |
+// |
+// TODO(shess): This conservatively uses Rollback() rather than Unrecoverable(). |
+// With Rollback(), it is expected that the database will continue to generate |
+// errors. Change the failure cases to Unrecoverable() if/when histogram |
+// results indicate that everything is working reasonably. |
+// |
+// static |
+void Recovery::RecoverDatabaseOrRaze(Connection* db, |
+ const base::FilePath& db_path) { |
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(db, db_path); |
+ if (!recovery) { |
+ // TODO(shess): If recovery can't even get started, consider Raze() or |
+ // Delete(). |
+ // |
+ // Reviewing histograms, the majority of Begin() failures are in the ATTACH |
+ // (788k/month). This may happen for SQLITE_CANTOPEN and SQLITE_NOTADB |
+ // cases which cannot be processed using writable_schema. The second |
+ // biggest case is lack of vtable support on iOS, which is fixed but not yet |
+ // in the channel (175k/month). Lastly, there were 3 failures in a month |
Mark P
2016/04/08 20:12:06
Please don't put non-normalized numbers in comment
Scott Hess - ex-Googler
2016/04/15 00:38:14
Maybe I'll keep the comment in my local client, si
|
+ // opening the temporary table - in theory, that may work later, but in |
+ // practice 3 in 1M might be a reasonable false-positive rate. |
Mark P
2016/04/08 20:12:06
I think this last sentence isn't worth mentioning
Scott Hess - ex-Googler
2016/04/15 00:38:14
Acknowledged.
|
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_BEGIN); |
+ db->Poison(); |
Mark P
2016/04/08 20:12:06
In the header comment, you write the function will
Scott Hess - ex-Googler
2016/04/15 00:38:15
The originating caller's Open()/Close() calls shou
|
+ return; |
+ } |
+ |
+#if DCHECK_IS_ON() |
Mark P
2016/04/08 20:12:06
I don't understand this block of code. That said,
Scott Hess - ex-Googler
2016/04/15 00:38:15
fts3 is a virtual table which we used to use in hi
|
+ // fts3 has an [x_segdir] table containing a column [end_block INTEGER]. But |
+ // it actually stores either an integer or a text containing a pair of |
+ // integers separated by a space. AutoRecoverTable() trust the INTEGER tag |
+ // when setting up the recover vtable, so those rows get dropped. Setting |
+ // that column to ANY may work, but maybe it's better to fail hard until |
+ // someone needs to recover a virtual table. |
+ if (db->is_open()) { |
+ sql::Statement s(db->GetUniqueStatement( |
+ "SELECT 1 FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE %'")); |
+ DCHECK(!s.Step()) << "Recovery of virtual tables not supported"; |
+ } |
+#endif |
+ |
+ // TODO(shess): vacuum.c turns off checks and foreign keys. |
+ |
+ // TODO(shess): vacuum.c turns synchronous=OFF for the target. I do not fully |
+ // understand this, as the temporary db should not have a journal file at all. |
+ // Perhaps it does in case of cache spill? |
+ |
+ // Copy table schema from [corrupt] to [main]. |
+ if (!SchemaCopyHelper(recovery->db(), "CREATE TABLE ") || |
+ !SchemaCopyHelper(recovery->db(), "CREATE INDEX ") || |
+ !SchemaCopyHelper(recovery->db(), "CREATE UNIQUE INDEX ")) { |
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_SCHEMA); |
+ Recovery::Rollback(std::move(recovery)); |
Mark P
2016/04/08 20:12:06
Header talks about poisoning; why are you using a
Scott Hess - ex-Googler
2016/04/15 00:38:15
See comment I made about the RecoverDatabaseOrRaze
|
+ return; |
+ } |
+ |
+ // Run auto-recover against each table, skipping the sequence table. This is |
+ // necessary because earlier table recovery may populate the sequence table, |
+ // then copying the sequence table would create duplicates. |
+ { |
+ sql::Statement s(recovery->db()->GetUniqueStatement( |
+ "SELECT name FROM sqlite_master WHERE sql LIKE 'CREATE TABLE %' " |
+ "AND name!='sqlite_sequence'")); |
Mark P
2016/04/08 20:12:06
AND != corrupt.sqlite_sequence ?
Scott Hess - ex-Googler
2016/04/15 00:38:15
sqlite_master is implicitly main.sqlite_master .
Mark P
2016/04/19 23:34:27
I'm sorry for making your brain explode. ;-)
|
+ while (s.Step()) { |
+ const std::string name = s.ColumnString(0); |
+ size_t rows_recovered; |
+ if (!recovery->AutoRecoverTable(name.c_str(), &rows_recovered)) { |
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_TABLE); |
+ Recovery::Rollback(std::move(recovery)); |
+ return; |
+ } |
+ } |
+ if (!s.Succeeded()) { |
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_NAMESELECT); |
+ Recovery::Rollback(std::move(recovery)); |
+ return; |
+ } |
+ } |
+ |
+ // Overwrite any sequences created. |
+ if (recovery->db()->DoesTableExist("corrupt.sqlite_sequence")) { |
+ ignore_result(recovery->db()->Execute("DELETE FROM main.sqlite_sequence")); |
+ size_t rows_recovered; |
+ if (!recovery->AutoRecoverTable("sqlite_sequence", &rows_recovered)) { |
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_SEQUENCE); |
+ Recovery::Rollback(std::move(recovery)); |
+ return; |
+ } |
+ } |
+ |
+ // Copy triggers, views, and virtual tables directly to sqlite_master. Any |
+ // tables they refer to should already exist. |
+ // TODO(shess): The rootpage=0 test is from vacuum.c. Consider instead: |
+ // sql LIKE "CREATE VIRTUAL TABLE %" |
Mark P
2016/04/08 20:12:06
Do you have a sense of whether this will be differ
Scott Hess - ex-Googler
2016/04/15 00:38:15
I do not fully understand why vacuum.c uses the ro
|
+ char kCreateMetaItems[] = |
+ "INSERT INTO main.sqlite_master " |
+ "SELECT type, name, tbl_name, rootpage, sql " |
+ "FROM corrupt.sqlite_master " |
+ "WHERE type='view' OR type='trigger' OR (type='table' AND rootpage=0)"; |
+ if (!recovery->db()->Execute(kCreateMetaItems)) { |
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_AUX); |
+ Recovery::Rollback(std::move(recovery)); |
+ return; |
+ } |
+ |
+ RecordRecoveryEvent(RECOVERY_SUCCESS_AUTORECOVERDB); |
+ ignore_result(Recovery::Recovered(std::move(recovery))); |
+} |
+ |
+// static |
+bool Recovery::ShouldRecoverOrRaze(int extended_error) { |
+ switch (extended_error & 0xFF) { |
Mark P
2016/04/07 23:09:43
You do the same & 0xFF in connection.cc as well.
Scott Hess - ex-Googler
2016/04/15 00:38:15
Copied language from connection.cc. Unfortunately
|
+ case SQLITE_CANTOPEN: |
+ case SQLITE_NOTADB: |
+ case SQLITE_CORRUPT: |
+ return true; |
+ default: |
+ return false; |
+ } |
+} |
+ |
} // namespace sql |