Chromium Code Reviews| Index: sql/recovery.cc |
| diff --git a/sql/recovery.cc b/sql/recovery.cc |
| index 92c7f875e152f9d234f1d00ab6c4b23f6376b151..1a8fe7bada4da8362d9d87789caf03f71906f393 100644 |
| --- a/sql/recovery.cc |
| +++ b/sql/recovery.cc |
| @@ -84,6 +84,32 @@ enum RecoveryEventType { |
| // No version key in recovery meta table. |
| RECOVERY_FAILED_META_NO_VERSION, |
| + // Automatically recovered entire database successfully. |
| + 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. |
| + // These sum to RECOVERY_FAILED_AUTORECOVERDB_SCHEMA. |
| + 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, |
| }; |
| @@ -508,4 +534,180 @@ bool Recovery::GetMetaVersionNumber(int* version) { |
| return true; |
| } |
| +namespace { |
| + |
| +// Collect statements from [corrupt.sqlite_master.sql] which start with |
| +// |prefix|, and apply them to [main]. Skip any table named 'sqlite_sequence', |
| +// which is created on demand by SQLite if any tables use AUTOINCREMENT. |
|
Mark P
2016/04/19 23:34:28
You still should allude to this limitation here, i
Scott Hess - ex-Googler
2016/05/13 21:24:36
Done.
I'm reluctant to get too elaborate with the
|
| +// |
| +// Returns true if all of the matching items were created in the main database. |
| +// Returns false if an item fails on creation, or if the corrupt database schema |
| +// cannot be queried. |
| +bool SchemaCopyHelper(Connection* db, const char* prefix) { |
| + const size_t prefix_len = strlen(prefix); |
| + DCHECK_EQ(' ', prefix[prefix_len-1]); |
| + sql::Statement s(db->GetUniqueStatement( |
| + "SELECT DISTINCT sql FROM corrupt.sqlite_master " |
| + "WHERE substr(sql, 1, ?)=? AND name<>'sqlite_sequence'")); |
|
Mark P
2016/04/19 23:34:28
You didn't do this rewrite you promised.
Scott Hess - ex-Googler
2016/05/13 21:24:36
Was thinking out loud, earlier. Done.
|
| + s.BindInt(0, prefix_len); |
| + s.BindCString(1, prefix); |
| + |
| + while (s.Step()) { |
|
Mark P
2016/04/19 23:34:28
You're not concerned with having a failed attempt
Scott Hess - ex-Googler
2016/05/13 21:24:36
In case of failure the recovery database [main] do
|
| + 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) { |
| + std::unique_ptr<sql::Recovery> recovery = sql::Recovery::Begin(db, db_path); |
| + if (!recovery) { |
| + // TODO(shess): If recovery can't even get started, Raze() or Delete(). |
| + RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_BEGIN); |
| + db->Poison(); |
|
Mark P
2016/04/19 23:34:28
In this case, I think it'd be better in the header
Scott Hess - ex-Googler
2016/05/13 21:24:36
Done.
Scott Hess - ex-Googler
2016/06/27 21:35:38
Header-comment-change really done, this time.
|
| + return; |
| + } |
| + |
| +#if DCHECK_IS_ON() |
| + // This code silently fails to recover fts3 virtual tables. At this time no |
| + // browser database contain fts3 tables. Just to be safe, complain loudly if |
| + // the database contains virtual tables. |
| + // |
| + // 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() trusts the INTEGER tag |
| + // when setting up the recover vtable, so those rows get dropped. Setting |
| + // that column to ANY may work. |
| + 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)); |
| + 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'")); |
| + 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/19 23:34:28
If it seems safer to you, why don't you do it (and
Scott Hess - ex-Googler
2016/05/13 21:24:36
Assuming my reasoning is correct, then the DCHECK_
|
| + 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) { |
| + // Trim extended error codes. |
| + int error = extended_error & 0xFF; |
| + switch (error) { |
| + case SQLITE_CANTOPEN: |
| + // SQLITE_CANTOPEN is associated with an entirely broken file (for |
| + // instance a symlink to a non-existent path, or the file is a directory). |
|
Mark P
2016/04/19 23:34:28
How is this recoverable? This comment implies you
Scott Hess - ex-Googler
2016/05/13 21:24:36
I intended this for the delete option, which you a
|
| + return true; |
| + |
| + case SQLITE_NOTADB: |
| + // SQLITE_NOTADB happens if the SQLite header is broken. Some versions of |
| + // SQLite return this where other versions return SQLITE_CORRUPT. |
|
Mark P
2016/04/19 23:34:28
How is this likely to be recoverable?
Scott Hess - ex-Googler
2016/05/13 21:24:36
In the case where it was incorrectly returning SQL
Scott Hess - ex-Googler
2016/06/27 21:35:38
Adjusted the comment a bit more. Long-term, this
|
| + return true; |
| + |
| + case SQLITE_CORRUPT: |
| + // SQLITE_CORRUPT generally means that the database is readable as a |
| + // SQLite database, but some inconsistency has been detected by SQLite. |
| + // In many cases the inconsistency is relatively trivial, such as if an |
| + // index refers to a row which was deleted, in which case most or even all |
| + // of the data can be recovered. This can also be reported if parts of |
| + // the file have been overwritten with garbage data, in which recovery |
| + // should be able to recover partial data. |
| + return true; |
| + |
| + // TODO(shess): Possible future options for automated fixing: |
| + // - SQLITE_PERM - permissions could be fixed. |
| + // - SQLITE_READONLY - permissions could be fixed. |
| + // - SQLITE_IOERR - rewrite using new blocks. |
| + // - SQLITE_FULL - recover in memory and rewrite subset of data. |
| + |
| + default: |
| + return false; |
| + } |
| +} |
| + |
| } // namespace sql |