blob: 040694cf4389f328c6d7bbd7f0d524febbf3eb5e [file] [log] [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 <memory>
#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 {
struct Feature;
class FilePath;
}
namespace sql {
// WARNING: This API is still experimental. See https://crbug.com/1385500.
//
// Uses SQLite's built-in corruption recovery module to recover the database.
// See https://www.sqlite.org/recovery.html
//
// For now, feature teams should use only the `RecoverIfPossible()` method -
// which falls back to the legacy `sql::Recovery` below if necessary - in lieu
// of calling `RecoverDatabase()` directly.
class COMPONENT_EXPORT(SQL) BuiltInRecovery {
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(https://crbug.com/1385500): 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,
};
[[nodiscard]] static bool IsSupported();
// 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);
// WARNING: This API is experimental. For now, please 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.
//
// 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(https://crbug.com/1385500): 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 few key differences:
// - Uses `BuiltInRecovery` or the legacy `Recovery` to recover the
// database, as appropriate. This method facilitates the migration to the
// newer recovery module with minimal impact on feature teams. The
// expectation is that `Recovery` will eventually be removed entirely.
// See https://crbug.com/1385500.
// - 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.
// - Includes the option to pass a per-database feature flag indicating
// whether `BuiltInRecovery` should be used to recover this database, if
// it's supported.
//
// Recommended usage from within a database error callback:
//
// // Attempt to recover the database, if recovery is possible.
// if (sql::BuiltInRecovery::RecoverIfPossible(
// &db, extended_error,
// sql::BuiltInRecovery::Strategy::kRecoverWithMetaVersionOrRaze,
// &features::kMyFeatureTeamShouldUseBuiltInRecoveryIfSupported)) {
// // 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,
const base::Feature* const use_builtin_recovery_if_supported_flag =
nullptr);
BuiltInRecovery(const BuiltInRecovery&) = delete;
BuiltInRecovery& operator=(const BuiltInRecovery&) = delete;
private:
BuiltInRecovery(Database* database, Strategy strategy);
~BuiltInRecovery();
// 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_;
// 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_;
};
// Recovery module for sql/. The basic idea is to create a fresh database and
// populate it with the recovered contents of the original database. If
// recovery is successful, the recovered database is backed up over the original
// database. If recovery is not successful, the original database is razed. In
// either case, the original handle is poisoned so that operations on the stack
// do not accidentally disrupt the restored data.
//
// RecoverDatabase() automates this, including recoverying the schema of from
// the suspect database. If a database requires special handling, such as
// recovering between different schema, or tables requiring post-processing,
// then the module can be used manually like:
//
// {
// std::unique_ptr<sql::Recovery> r =
// sql::Recovery::Begin(orig_db, orig_db_path);
// if (r) {
// // Create the schema to recover to. On failure, clear the
// // database.
// if (!r.db()->Execute(kCreateSchemaSql)) {
// sql::Recovery::Unrecoverable(std::move(r));
// return;
// }
//
// // Recover data in "mytable".
// size_t rows_recovered = 0;
// if (!r.AutoRecoverTable("mytable", 0, &rows_recovered)) {
// sql::Recovery::Unrecoverable(std::move(r));
// return;
// }
//
// // Manually cleanup additional constraints.
// if (!r.db()->Execute(kCleanupSql)) {
// sql::Recovery::Unrecoverable(std::move(r));
// return;
// }
//
// // Commit the recovered data to the original database file.
// sql::Recovery::Recovered(std::move(r));
// }
// }
//
// If Recovered() is not called, then RazeAndPoison() is called on
// orig_db.
class COMPONENT_EXPORT(SQL) Recovery {
public:
Recovery(const Recovery&) = delete;
Recovery& operator=(const Recovery&) = delete;
~Recovery();
// Begin the recovery process by opening a temporary database handle
// and attach the existing database to it at "corrupt". To prevent
// deadlock, all transactions on |database| are rolled back.
//
// Returns nullptr in case of failure, with no cleanup done on the
// original database (except for breaking the transactions). The
// caller should Raze() or otherwise cleanup as appropriate.
//
// TODO(shess): Later versions of SQLite allow extracting the path
// from the database.
// TODO(shess): Allow specifying the connection point?
[[nodiscard]] static std::unique_ptr<Recovery> Begin(
Database* database,
const base::FilePath& db_path);
// Mark recovery completed by replicating the recovery database over
// the original database, then closing the recovery database. The
// original database handle is poisoned, causing future calls
// against it to fail.
//
// If Recovered() is not called, the destructor will call
// Unrecoverable().
//
// TODO(shess): At this time, this function can fail while leaving
// the original database intact. Figure out which failure cases
// should go to RazeAndPoison() instead.
[[nodiscard]] static bool Recovered(std::unique_ptr<Recovery> r);
// Indicate that the database is unrecoverable. The original
// database is razed, and the handle poisoned.
static void Unrecoverable(std::unique_ptr<Recovery> r);
// When initially developing recovery code, sometimes the possible
// database states are not well-understood without further
// diagnostics. Abandon recovery but do not raze the original
// database.
// NOTE(shess): Only call this when adding recovery support. In the
// steady state, all databases should progress to recovered or razed.
static void Rollback(std::unique_ptr<Recovery> r);
// Handle to the temporary recovery database.
sql::Database* db() { return &recover_db_; }
// Attempt to recover the named table from the corrupt database into
// the recovery database using a temporary recover virtual table.
// The virtual table schema is derived from the named table's schema
// in database [main]. Data is copied using INSERT OR IGNORE, so
// duplicates are dropped.
//
// If the source table has fewer columns than the target, the target
// DEFAULT value will be used for those columns.
//
// Returns true if all operations succeeded, with the number of rows
// recovered in |*rows_recovered|.
//
// NOTE(shess): Due to a flaw in the recovery virtual table, at this
// time this code injects the DEFAULT value of the target table in
// locations where the recovery table returns nullptr. This is not
// entirely correct, because it happens both when there is a short
// row (correct) but also where there is an actual NULL value
// (incorrect).
//
// TODO(shess): Flag for INSERT OR REPLACE vs IGNORE.
// TODO(shess): Handle extended table names.
bool AutoRecoverTable(const char* table_name, size_t* rows_recovered);
// Setup a recover virtual table at temp.recover_meta, reading from
// corrupt.meta. Returns true if created.
// TODO(shess): Perhaps integrate into Begin().
// TODO(shess): Add helpers to fetch additional items from the meta
// table as needed.
bool SetupMeta();
// Fetch the version number from temp.recover_meta. Returns false
// if the query fails, or if there is no version row. Otherwise
// returns true, with the version in |*version_number|.
//
// Only valid to call after successful SetupMeta().
bool GetMetaVersionNumber(int* version_number);
// Attempt to recover the database by creating a new database with schema from
// |db|, then copying over as much data as possible. If successful, the
// recovery handle is returned to allow the caller to make additional changes,
// such as validating constraints not expressed in the schema.
//
// In case of SQLITE_NOTADB, the database is deemed unrecoverable and deleted.
[[nodiscard]] static std::unique_ptr<Recovery> BeginRecoverDatabase(
Database* db,
const base::FilePath& db_path);
// Call BeginRecoverDatabase() to recover the database, then commit the
// changes using Recovered(). After this call, the |db| handle will be
// poisoned (though technically remaining open) so that future calls will
// return errors until the handle is re-opened.
static void RecoverDatabase(Database* db, const base::FilePath& db_path);
// Variant on RecoverDatabase() which requires that the database have a valid
// meta table with a version value. The meta version value is used by some
// clients to make assertions about the database schema. If this information
// cannot be determined, the database is considered unrecoverable.
static void RecoverDatabaseWithMetaVersion(Database* db,
const base::FilePath& db_path);
// Returns true for SQLite errors which RecoverDatabase() can plausibly fix.
// This does not guarantee that RecoverDatabase() will successfully recover
// the database.
static bool ShouldRecover(int extended_error);
// Enables the "recover" SQLite extension for a database connection.
//
// Returns a SQLite error code.
static int EnableRecoveryExtension(Database* db, InternalApiToken);
private:
explicit Recovery(Database* database);
// Setup the recovery database handle for Begin(). Returns false in
// case anything failed.
[[nodiscard]] bool Init(const base::FilePath& db_path);
// Copy the recovered database over the original database.
[[nodiscard]] bool Backup();
// Close the recovery database, and poison the original handle.
// |raze| controls whether the original database is razed or just
// poisoned.
enum Disposition {
RAZE_AND_POISON,
POISON,
};
void Shutdown(Disposition raze);
raw_ptr<Database> db_; // Original Database connection.
Database recover_db_; // Recovery Database connection.
};
} // namespace sql
#endif // SQL_RECOVERY_H_