chromium/chrome/browser/ash/app_list/search/local_image_search/sql_database.cc

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

#include "chrome/browser/ash/app_list/search/local_image_search/sql_database.h"

#include <string>

#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "sql/error_delegate_util.h"
#include "sql/statement.h"

namespace app_list {
namespace {

constexpr int kUninitializedDbVersionNumber = 1;

// These values persist to logs. Entries should not be renumbered and numeric
// values should never be reused.
enum class Status {
  kOk = 0,
  kFailedToCreatDirectory = 1,
  kFailedToOpenDb = 2,
  kFailedToInitializeMetaTable = 3,
  kFailedToSetVersionNumber = 4,
  kFailedToMigrateSchema = 5,

  kMaxValue = kFailedToMigrateSchema,
};

void LogStatusUma(const std::string& name, Status status) {
  base::UmaHistogramEnumeration(name, status);
}

}  // namespace

SqlDatabase::SqlDatabase(
    const base::FilePath& path_to_db,
    const std::string& histogram_tag,
    int current_version_number,
    base::RepeatingCallback<int(SqlDatabase* db)> create_table_schema,
    base::RepeatingCallback<int(SqlDatabase* db, int current_version_number)>
        migrate_table_schema)
    : create_table_schema_(std::move(create_table_schema)),
      migrate_table_schema_(std::move(migrate_table_schema)),
      path_to_db_(path_to_db),
      histogram_tag_(histogram_tag),
      db_(sql::Database(sql::DatabaseOptions())),
      meta_table_(sql::MetaTable()),
      current_version_number_(current_version_number) {
  DETACH_FROM_SEQUENCE(sequence_checker_);
  DCHECK_GT(current_version_number_, 1);
  DCHECK(!path_to_db_.empty());
}

SqlDatabase::~SqlDatabase() = default;

bool SqlDatabase::Initialize() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!db_.is_open());

  const base::FilePath dir = path_to_db_.DirName();
  if (!base::PathExists(dir) && !base::CreateDirectory(dir)) {
    LOG(ERROR) << "Could not create a directory to the new database.";
    LogStatusUma(histogram_tag_, Status::kFailedToCreatDirectory);
    return false;
  }

  db_.set_histogram_tag(histogram_tag_);

  if (!db_.Open(path_to_db_)) {
    LOG(ERROR) << "Unable to open " << path_to_db_;
    LogStatusUma(histogram_tag_, Status::kFailedToOpenDb);
    RazeDb();
    return false;
  }

  // Either initializes a new meta table or loads it from the db if exists.
  if (!meta_table_.Init(&db_, kUninitializedDbVersionNumber,
                        kUninitializedDbVersionNumber)) {
    LogStatusUma(histogram_tag_, Status::kFailedToInitializeMetaTable);
    RazeDb();
    return false;
  }

  if (meta_table_.GetVersionNumber() == current_version_number_) {
    LogStatusUma(histogram_tag_, Status::kOk);
    return true;
  }

  if (meta_table_.GetVersionNumber() == kUninitializedDbVersionNumber) {
    const int new_version_number = create_table_schema_.Run(this);
    DCHECK_GT(new_version_number, 1);

    if (!meta_table_.SetVersionNumber(new_version_number) ||
        !meta_table_.SetCompatibleVersionNumber(new_version_number)) {
      LogStatusUma(histogram_tag_, Status::kFailedToSetVersionNumber);
      RazeDb();
      return false;
    }
    LogStatusUma(histogram_tag_, Status::kOk);
    return true;
  }

  if (!MigrateDatabaseSchema()) {
    LOG(ERROR) << "Unable to migrate the schema";
    LogStatusUma(histogram_tag_, Status::kFailedToMigrateSchema);
    RazeDb();
    return false;
  }
  // base::Unretained is safe because `this` owns (and therefore outlives) the
  // sql::Database held by `db_`. That is, `db_` calls the error callback and
  // if `this` destroyed then `db_` is destroyed, as well.
  db_.set_error_callback(base::BindRepeating(&SqlDatabase::OnErrorCallback,
                                             base::Unretained(this)));
  LogStatusUma(histogram_tag_, Status::kOk);
  return true;
}

bool SqlDatabase::MigrateDatabaseSchema() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // May happen if the code migrated from dev to stable channel.
  if (meta_table_.GetVersionNumber() > current_version_number_) {
    LOG(ERROR) << "Database is too new. Deleting the db.";
    Close();
    return sql::Database::Delete(path_to_db_) && Initialize();
  }

  if (migrate_table_schema_.Run(this, meta_table_.GetVersionNumber()) <
      meta_table_.GetVersionNumber()) {
    LOG(ERROR) << "Failed to migrate the schema. Deleting the db.";
    RazeDb();
    return false;
  }

  return true;
}

void SqlDatabase::Close() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  db_.reset_error_callback();
  meta_table_.Reset();
  db_.Close();
}

void SqlDatabase::OnErrorCallback(int error, sql::Statement* stmt) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  LOG(ERROR) << "Error Code: " << sql::ToSqliteResultCode(error);
  if (stmt) {
    LOG(ERROR) << "Statement: " << stmt->GetSQLStatement();
  }
  if (sql::IsErrorCatastrophic(error)) {
    LOG(ERROR) << "The error is catastrophic. Razing db.";
    RazeDb();
  }
}

std::unique_ptr<sql::Statement> SqlDatabase::GetStatementForQuery(
    const sql::StatementID& sql_from_here,
    base::cstring_view query) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!db_.is_open()) {
    return nullptr;
  }

  DVLOG(1) << "Making statement for query: " << query;
  DCHECK(db_.IsSQLValid(query));
  return std::make_unique<sql::Statement>(
      db_.GetCachedStatement(sql_from_here, query));
}

bool SqlDatabase::RazeDb() {
  DVLOG(1) << "Razing db.";
  meta_table_.Reset();
  if (db_.is_open()) {
    db_.Poison();
    // Sometimes it fails to do it due to locks or open handles.
    if (!sql::Database::Delete(path_to_db_)) {
      return false;
    }
  }
  return true;
}

base::FilePath SqlDatabase::GetPathToDb() const {
  return path_to_db_;
}

}  // namespace app_list