Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2013 The Chromium Authors. All rights reserved. | 1 // Copyright 2013 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "sql/recovery.h" | 5 #include "sql/recovery.h" |
| 6 | 6 |
| 7 #include <stddef.h> | 7 #include <stddef.h> |
| 8 | 8 |
| 9 #include "base/files/file_path.h" | 9 #include "base/files/file_path.h" |
| 10 #include "base/format_macros.h" | 10 #include "base/format_macros.h" |
| (...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 77 | 77 |
| 78 // GetMetaVersionNumber() successfully completed. | 78 // GetMetaVersionNumber() successfully completed. |
| 79 RECOVERY_SUCCESS_META_VERSION, | 79 RECOVERY_SUCCESS_META_VERSION, |
| 80 | 80 |
| 81 // Failed in querying recovery meta table. | 81 // Failed in querying recovery meta table. |
| 82 RECOVERY_FAILED_META_QUERY, | 82 RECOVERY_FAILED_META_QUERY, |
| 83 | 83 |
| 84 // No version key in recovery meta table. | 84 // No version key in recovery meta table. |
| 85 RECOVERY_FAILED_META_NO_VERSION, | 85 RECOVERY_FAILED_META_NO_VERSION, |
| 86 | 86 |
| 87 // Automatically recovered entire database successfully. | |
| 88 RECOVERY_SUCCESS_AUTORECOVERDB, | |
| 89 | |
| 90 // Database was so broken recovery couldn't be entered. | |
| 91 RECOVERY_FAILED_AUTORECOVERDB_BEGIN, | |
| 92 | |
| 93 // Failed to copy schema to autorecover db. | |
| 94 RECOVERY_FAILED_AUTORECOVERDB_SCHEMA, | |
| 95 | |
| 96 // Distinguish failure in querying schema from failure in creating schema. | |
| 97 // These sum to RECOVERY_FAILED_AUTORECOVERDB_SCHEMA. | |
| 98 RECOVERY_FAILED_AUTORECOVERDB_SCHEMACREATE, | |
| 99 RECOVERY_FAILED_AUTORECOVERDB_SCHEMASELECT, | |
| 100 | |
| 101 // Failed querying tables to recover. Should be impossible. | |
| 102 RECOVERY_FAILED_AUTORECOVERDB_NAMESELECT, | |
| 103 | |
| 104 // Failed to recover an individual table. | |
| 105 RECOVERY_FAILED_AUTORECOVERDB_TABLE, | |
| 106 | |
| 107 // Failed to recover [sqlite_sequence] table. | |
| 108 RECOVERY_FAILED_AUTORECOVERDB_SEQUENCE, | |
| 109 | |
| 110 // Failed to recover triggers or views or virtual tables. | |
| 111 RECOVERY_FAILED_AUTORECOVERDB_AUX, | |
| 112 | |
| 87 // Always keep this at the end. | 113 // Always keep this at the end. |
| 88 RECOVERY_EVENT_MAX, | 114 RECOVERY_EVENT_MAX, |
| 89 }; | 115 }; |
| 90 | 116 |
| 91 void RecordRecoveryEvent(RecoveryEventType recovery_event) { | 117 void RecordRecoveryEvent(RecoveryEventType recovery_event) { |
| 92 UMA_HISTOGRAM_ENUMERATION("Sqlite.RecoveryEvents", | 118 UMA_HISTOGRAM_ENUMERATION("Sqlite.RecoveryEvents", |
| 93 recovery_event, RECOVERY_EVENT_MAX); | 119 recovery_event, RECOVERY_EVENT_MAX); |
| 94 } | 120 } |
| 95 | 121 |
| 96 } // namespace | 122 } // namespace |
| (...skipping 404 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 501 RecordRecoveryEvent(RECOVERY_FAILED_META_NO_VERSION); | 527 RecordRecoveryEvent(RECOVERY_FAILED_META_NO_VERSION); |
| 502 } | 528 } |
| 503 return false; | 529 return false; |
| 504 } | 530 } |
| 505 | 531 |
| 506 RecordRecoveryEvent(RECOVERY_SUCCESS_META_VERSION); | 532 RecordRecoveryEvent(RECOVERY_SUCCESS_META_VERSION); |
| 507 *version = recovery_version.ColumnInt(0); | 533 *version = recovery_version.ColumnInt(0); |
| 508 return true; | 534 return true; |
| 509 } | 535 } |
| 510 | 536 |
| 537 namespace { | |
| 538 | |
| 539 // Collect statements from [corrupt.sqlite_master.sql] which start with | |
| 540 // |prefix|, and apply them to [main]. Skip any table named 'sqlite_sequence', | |
| 541 // 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
| |
| 542 // | |
| 543 // Returns true if all of the matching items were created in the main database. | |
| 544 // Returns false if an item fails on creation, or if the corrupt database schema | |
| 545 // cannot be queried. | |
| 546 bool SchemaCopyHelper(Connection* db, const char* prefix) { | |
| 547 const size_t prefix_len = strlen(prefix); | |
| 548 DCHECK_EQ(' ', prefix[prefix_len-1]); | |
| 549 sql::Statement s(db->GetUniqueStatement( | |
| 550 "SELECT DISTINCT sql FROM corrupt.sqlite_master " | |
| 551 "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.
| |
| 552 s.BindInt(0, prefix_len); | |
| 553 s.BindCString(1, prefix); | |
| 554 | |
| 555 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
| |
| 556 std::string sql = s.ColumnString(0); | |
| 557 sql.insert(prefix_len, "main."); | |
| 558 if (!db->Execute(sql.c_str())) { | |
| 559 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_SCHEMACREATE); | |
| 560 return false; | |
| 561 } | |
| 562 } | |
| 563 if (!s.Succeeded()) { | |
| 564 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_SCHEMASELECT); | |
| 565 return false; | |
| 566 } | |
| 567 return true; | |
| 568 } | |
| 569 | |
| 570 } // namespace | |
| 571 | |
| 572 // This method is derived from SQLite's vacuum.c. VACUUM operates very | |
| 573 // similarily, creating a new database, populating the schema, then copying the | |
| 574 // data. | |
| 575 // | |
| 576 // TODO(shess): This conservatively uses Rollback() rather than Unrecoverable(). | |
| 577 // With Rollback(), it is expected that the database will continue to generate | |
| 578 // errors. Change the failure cases to Unrecoverable() if/when histogram | |
| 579 // results indicate that everything is working reasonably. | |
| 580 // | |
| 581 // static | |
| 582 void Recovery::RecoverDatabaseOrRaze(Connection* db, | |
| 583 const base::FilePath& db_path) { | |
| 584 std::unique_ptr<sql::Recovery> recovery = sql::Recovery::Begin(db, db_path); | |
| 585 if (!recovery) { | |
| 586 // TODO(shess): If recovery can't even get started, Raze() or Delete(). | |
| 587 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_BEGIN); | |
| 588 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.
| |
| 589 return; | |
| 590 } | |
| 591 | |
| 592 #if DCHECK_IS_ON() | |
| 593 // This code silently fails to recover fts3 virtual tables. At this time no | |
| 594 // browser database contain fts3 tables. Just to be safe, complain loudly if | |
| 595 // the database contains virtual tables. | |
| 596 // | |
| 597 // fts3 has an [x_segdir] table containing a column [end_block INTEGER]. But | |
| 598 // it actually stores either an integer or a text containing a pair of | |
| 599 // integers separated by a space. AutoRecoverTable() trusts the INTEGER tag | |
| 600 // when setting up the recover vtable, so those rows get dropped. Setting | |
| 601 // that column to ANY may work. | |
| 602 if (db->is_open()) { | |
| 603 sql::Statement s(db->GetUniqueStatement( | |
| 604 "SELECT 1 FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE %'")); | |
| 605 DCHECK(!s.Step()) << "Recovery of virtual tables not supported"; | |
| 606 } | |
| 607 #endif | |
| 608 | |
| 609 // TODO(shess): vacuum.c turns off checks and foreign keys. | |
| 610 | |
| 611 // TODO(shess): vacuum.c turns synchronous=OFF for the target. I do not fully | |
| 612 // understand this, as the temporary db should not have a journal file at all. | |
| 613 // Perhaps it does in case of cache spill? | |
| 614 | |
| 615 // Copy table schema from [corrupt] to [main]. | |
| 616 if (!SchemaCopyHelper(recovery->db(), "CREATE TABLE ") || | |
| 617 !SchemaCopyHelper(recovery->db(), "CREATE INDEX ") || | |
| 618 !SchemaCopyHelper(recovery->db(), "CREATE UNIQUE INDEX ")) { | |
| 619 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_SCHEMA); | |
| 620 Recovery::Rollback(std::move(recovery)); | |
| 621 return; | |
| 622 } | |
| 623 | |
| 624 // Run auto-recover against each table, skipping the sequence table. This is | |
| 625 // necessary because earlier table recovery may populate the sequence table, | |
| 626 // then copying the sequence table would create duplicates. | |
| 627 { | |
| 628 sql::Statement s(recovery->db()->GetUniqueStatement( | |
| 629 "SELECT name FROM sqlite_master WHERE sql LIKE 'CREATE TABLE %' " | |
| 630 "AND name!='sqlite_sequence'")); | |
| 631 while (s.Step()) { | |
| 632 const std::string name = s.ColumnString(0); | |
| 633 size_t rows_recovered; | |
| 634 if (!recovery->AutoRecoverTable(name.c_str(), &rows_recovered)) { | |
| 635 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_TABLE); | |
| 636 Recovery::Rollback(std::move(recovery)); | |
| 637 return; | |
| 638 } | |
| 639 } | |
| 640 if (!s.Succeeded()) { | |
| 641 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_NAMESELECT); | |
| 642 Recovery::Rollback(std::move(recovery)); | |
| 643 return; | |
| 644 } | |
| 645 } | |
| 646 | |
| 647 // Overwrite any sequences created. | |
| 648 if (recovery->db()->DoesTableExist("corrupt.sqlite_sequence")) { | |
| 649 ignore_result(recovery->db()->Execute("DELETE FROM main.sqlite_sequence")); | |
| 650 size_t rows_recovered; | |
| 651 if (!recovery->AutoRecoverTable("sqlite_sequence", &rows_recovered)) { | |
| 652 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_SEQUENCE); | |
| 653 Recovery::Rollback(std::move(recovery)); | |
| 654 return; | |
| 655 } | |
| 656 } | |
| 657 | |
| 658 // Copy triggers, views, and virtual tables directly to sqlite_master. Any | |
| 659 // tables they refer to should already exist. | |
| 660 // TODO(shess): The rootpage=0 test is from vacuum.c. Consider instead: | |
| 661 // 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_
| |
| 662 char kCreateMetaItems[] = | |
| 663 "INSERT INTO main.sqlite_master " | |
| 664 "SELECT type, name, tbl_name, rootpage, sql " | |
| 665 "FROM corrupt.sqlite_master " | |
| 666 "WHERE type='view' OR type='trigger' OR (type='table' AND rootpage=0)"; | |
| 667 if (!recovery->db()->Execute(kCreateMetaItems)) { | |
| 668 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVERDB_AUX); | |
| 669 Recovery::Rollback(std::move(recovery)); | |
| 670 return; | |
| 671 } | |
| 672 | |
| 673 RecordRecoveryEvent(RECOVERY_SUCCESS_AUTORECOVERDB); | |
| 674 ignore_result(Recovery::Recovered(std::move(recovery))); | |
| 675 } | |
| 676 | |
| 677 // static | |
| 678 bool Recovery::ShouldRecoverOrRaze(int extended_error) { | |
| 679 // Trim extended error codes. | |
| 680 int error = extended_error & 0xFF; | |
| 681 switch (error) { | |
| 682 case SQLITE_CANTOPEN: | |
| 683 // SQLITE_CANTOPEN is associated with an entirely broken file (for | |
| 684 // 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
| |
| 685 return true; | |
| 686 | |
| 687 case SQLITE_NOTADB: | |
| 688 // SQLITE_NOTADB happens if the SQLite header is broken. Some versions of | |
| 689 // 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
| |
| 690 return true; | |
| 691 | |
| 692 case SQLITE_CORRUPT: | |
| 693 // SQLITE_CORRUPT generally means that the database is readable as a | |
| 694 // SQLite database, but some inconsistency has been detected by SQLite. | |
| 695 // In many cases the inconsistency is relatively trivial, such as if an | |
| 696 // index refers to a row which was deleted, in which case most or even all | |
| 697 // of the data can be recovered. This can also be reported if parts of | |
| 698 // the file have been overwritten with garbage data, in which recovery | |
| 699 // should be able to recover partial data. | |
| 700 return true; | |
| 701 | |
| 702 // TODO(shess): Possible future options for automated fixing: | |
| 703 // - SQLITE_PERM - permissions could be fixed. | |
| 704 // - SQLITE_READONLY - permissions could be fixed. | |
| 705 // - SQLITE_IOERR - rewrite using new blocks. | |
| 706 // - SQLITE_FULL - recover in memory and rewrite subset of data. | |
| 707 | |
| 708 default: | |
| 709 return false; | |
| 710 } | |
| 711 } | |
| 712 | |
| 511 } // namespace sql | 713 } // namespace sql |
| OLD | NEW |