Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(354)

Side by Side Diff: sql/recovery.cc

Issue 1832173002: [sql] Database recovery system for Shortcuts. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Address review comments from #9 and #10. Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698