Chromium Code Reviews| 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 |