1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195

sql / recovery.h [blame]

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef SQL_RECOVERY_H_
#define SQL_RECOVERY_H_

#include <stddef.h>

#include <string>

#include "base/component_export.h"
#include "base/memory/raw_ptr.h"
#include "sql/database.h"
#include "sql/internal_api_token.h"
#include "sql/sqlite_result_code_values.h"

namespace base {
class FilePath;
}

namespace sql {

// Recovery module for sql/. Please see the `RecoverIfPossible()` method for how
// to use this class.
//
// This module is capable of recovering databases which the legacy recovery
// module could not recover. These include:
//   - tables with the WITHOUT ROWID optimization
//   - databases which use Write-Ahead Log (i.e. WAL mode)
//     - NOTE: as WAL mode is still experimental (see https://crbug.com/1416213)
//       recovery should not be attempted on WAL databases for now.
//
// Uses SQLite's recovery extension: https://www.sqlite.org/recovery.html
class COMPONENT_EXPORT(SQL) Recovery {
 public:
  enum class Strategy {
    // Razes the database if it could not be recovered.
    kRecoverOrRaze,

    // Razes the database if it could not be recovered, or if a valid meta table
    // with a version value could not be determined from the recovered database.
    // Use this strategy if your client makes assertions about the version of
    // the database schema.
    kRecoverWithMetaVersionOrRaze,

    // TODO(crbug.com/40061775): Consider exposing a way to keep around a
    // successfully-recovered, but unsuccessfully-restored database if needed.
  };

  // These values are persisted to logs. Entries should not be renumbered
  // and numeric values should never be reused.
  enum class Result {
    // Outcome not yet known. This value should never be logged.
    kUnknown = 0,

    // Successfully completed the full database recovery process.
    kSuccess = 1,

    // Failed to initialize and configure the sqlite3_recover object.
    kFailedRecoveryInit = 2,

    // Failed to run recovery with the sqlite3_recover object.
    kFailedRecoveryRun = 3,

    // The database was successfully recovered to a backup, but we could not
    // open the newly-recovered database in order to copy it to the original
    // database.
    kFailedToOpenRecoveredDatabase = 4,

    // The database was successfully recovered to a backup, but a meta table
    // could not be found in the recovered database.
    // Only valid when using Strategy::kRecoverWithMetaVersionOrRaze.
    kFailedMetaTableDoesNotExist = 5,

    // The database was successfully recovered to a backup, but the meta table
    // could not be initialized.
    // Only valid when using Strategy::kRecoverWithMetaVersionOrRaze.
    kFailedMetaTableInit = 6,

    // The database was successfully recovered to a backup, but a valid
    // (meaning, positive) version number could not be read from the meta table.
    // Only valid when using Strategy::kRecoverWithMetaVersionOrRaze.
    kFailedMetaTableVersionWasInvalid = 7,

    // Failed to initialize and configure the sqlite3_backup object.
    kFailedBackupInit = 8,

    // Failed to run backup with the sqlite3_backup object.
    kFailedBackupRun = 9,
    kMaxValue = kFailedBackupRun,
  };

  // Returns true if `RecoverDatabase()` can plausibly fix `database` given this
  // `extended_error`. This does not guarantee that `RecoverDatabase()` will
  // successfully recover the database.
  //
  // Note that even if this method returns true, the database's error callback
  // must be reset before recovery can be attempted.
  [[nodiscard]] static bool ShouldAttemptRecovery(Database* database,
                                                  int extended_error);

  // Use `RecoverIfPossible()` below rather than using this method directly.
  //
  // Attempts to recover `database`, and razes the database if it could not be
  // recovered according to `strategy`. After attempting recovery, the database
  // can be re-opened and assumed to be free of corruption.
  //
  // Use Database::set_histogram_tag() to log UMA for recovery results specific
  // to the given feature database.
  //
  // It is not considered an error if some or all of the data cannot be
  // recovered due to database corruption, so it is possible that some records
  // could not be salvaged from the corrupted database.
  // TODO(crbug.com/40061775): Support the lost-and-found table if the
  // need arises to try to restore all these records.
  //
  // It is illegal to attempt recovery if:
  //   - `database` is null,
  //   - `database` is not open,
  //   - `database` is an in-memory or temporary database, or
  //   - `database` has an error callback set
  //
  // During the recovery process, `database` is poisoned so that operations on
  // the stack do not accidentally disrupt the restored data.
  //
  // Returns a SQLite error code specifying whether the database was
  // successfully recovered.
  [[nodiscard]] static SqliteResultCode RecoverDatabase(Database* database,
                                                        Strategy strategy);

  // Similar to `RecoverDatabase()` above, but with a couple key differences:
  //   - Can be called without first checking `ShouldAttemptRecovery()`.
  //   - `database`'s error callback will be reset if recovery is attempted.
  //   - Must only be called from within a database error callback.
  //
  // Recommended usage from within a database error callback:
  //
  //  // Attempt to recover the database, if recovery is possible.
  //  if (sql::Recovery::RecoverIfPossible(
  //          &db, extended_error,
  //          sql::Recovery::Strategy::kRecoverWithMetaVersionOrRaze)) {
  //    // Recovery was attempted. The database handle has been poisoned and the
  //    // error callback has been reset.
  //
  //    // ...
  //  }
  //
  [[nodiscard]] static bool RecoverIfPossible(Database* database,
                                              int extended_error,
                                              Strategy strategy);

  Recovery(const Recovery&) = delete;
  Recovery& operator=(const Recovery&) = delete;

 private:
  Recovery(Database* database, Strategy strategy);
  ~Recovery();

  // Entry point.
  SqliteResultCode RecoverAndReplaceDatabase();

  // Use SQLite's corruption recovery module to store the recovered content in
  // `recover_db_`. See https://www.sqlite.org/recovery.html
  SqliteResultCode AttemptToRecoverDatabaseToBackup();

  bool RecoveredDbHasValidMetaTable();

  // Use SQLite's Online Backup API to replace the original database with
  // `recover_db_`. See https://www.sqlite.org/backup.html
  SqliteResultCode ReplaceOriginalWithRecoveredDb();

  void SetRecoverySucceeded();
  void SetRecoveryFailed(Result failure_result, SqliteResultCode result_code);

  const Strategy strategy_;

  // If non-empty, UMA will be logged with the result of the recovery for this
  // specific database.
  std::string database_uma_name_;

  // Result of the recovery. This value must be set to something other than
  // `kUnknown` before this object is destroyed.
  Result result_ = Result::kUnknown;
  SqliteResultCode sqlite_result_code_ = SqliteResultCode::kOk;

  raw_ptr<Database> db_;  // Original Database connection.
  Database recover_db_;   // Recovery Database connection.

  base::FilePath recovery_database_path_;
};

}  // namespace sql

#endif  // SQL_RECOVERY_H_