chromium/chrome/browser/ash/file_system_provider/content_cache/context_database.cc

// Copyright 2024 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/file_system_provider/content_cache/context_database.h"

#include <memory>
#include <optional>
#include <sstream>

#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "sql/statement.h"
#include "sql/transaction.h"

namespace ash::file_system_provider {

namespace {

static constexpr char kItemsCreateTableSql[] =
    // clang-format off
    "CREATE TABLE IF NOT EXISTS items ("
        "id INTEGER NOT NULL UNIQUE, "
        "fsp_path TEXT NOT NULL, "
        "version_tag TEXT NOT NULL, "
        "accessed_time INTEGER NOT NULL, "
        "UNIQUE(fsp_path, version_tag) ON CONFLICT REPLACE, "
        "PRIMARY KEY(id AUTOINCREMENT))";
// clang-format on

static constexpr char kInsertItemSql[] =
    // clang-format off
    "INSERT INTO items "
    "(fsp_path, version_tag, accessed_time) VALUES (?, ?, ?) "
    "RETURNING id";
// clang-format on

static constexpr char kSelectItemByIdSql[] =
    // clang-format off
    "SELECT fsp_path, version_tag, accessed_time FROM items WHERE id=? LIMIT 1";
// clang-format on

static constexpr char kSelectAllItemsSql[] =
    // clang-format off
    "SELECT id, fsp_path, version_tag, accessed_time FROM items";
// clang-format on

static constexpr char kUpdateAccessedTimeByIdSql[] =
    // clang-format off
    "UPDATE items SET accessed_time = ? WHERE id = ?";
// clang-format on

}  // namespace

ContextDatabase::ContextDatabase(const base::FilePath& db_path)
    : db_path_(db_path), db_({sql::DatabaseOptions{}}) {
  // Can be constructed on any sequence, the first call to `Initialize` should
  // be made on the blocking task runner.
  DETACH_FROM_SEQUENCE(sequence_checker_);
}

// The current database version number.
constexpr int ContextDatabase::kCurrentVersionNumber = 1;

// The oldest version that is still compatible with `kCurrentVersionNumber`.
constexpr int ContextDatabase::kCompatibleVersionNumber = 1;

ContextDatabase::Item::Item(int64_t id,
                            const std::string& fsp_path,
                            const std::string& version_tag,
                            base::Time accessed_time)
    : id(id),
      fsp_path(fsp_path),
      version_tag(version_tag),
      accessed_time(accessed_time) {}

bool ContextDatabase::Initialize() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  db_.set_histogram_tag("FSPContextDatabase");

  // TODO(b/332636364): Once the logic for the database has landed, let's stop
  // removing the database on every `Initialize` call.
  if (!Raze()) {
    LOG(ERROR) << "Failed to remove old database";
    return false;
  }

  DCHECK(!db_.is_open()) << "Database is already open";

  if (db_path_.empty() && !db_.OpenInMemory()) {
    LOG(ERROR) << "In memory database initialization failed";
    return false;
  } else if (!db_path_.empty() && !db_.Open(db_path_)) {
    LOG(ERROR) << "Initialization of '" << db_path_ << "' failed";
    Raze();
    return false;
  }

  sql::Transaction committer(&db_);
  if (!committer.Begin()) {
    LOG(ERROR) << "Can't start SQL transaction";
    return false;
  }

  if (!db_.Execute(kItemsCreateTableSql)) {
    LOG(ERROR) << "Can't setup items table";
    Raze();
    return false;
  }

  if (!meta_table_.Init(&db_, kCurrentVersionNumber,
                        kCompatibleVersionNumber)) {
    Raze();
    return false;
  }

  return committer.Commit();
}

bool ContextDatabase::AddItem(const base::FilePath& fsp_path,
                              const std::string& version_tag,
                              base::Time accessed_time,
                              int64_t* inserted_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (fsp_path.empty() || version_tag.empty() || accessed_time.is_null()) {
    return false;
  }

  std::unique_ptr<sql::Statement> statement = std::make_unique<sql::Statement>(
      db_.GetCachedStatement(SQL_FROM_HERE, kInsertItemSql));
  if (!statement) {
    LOG(ERROR) << "Couldn't create SQL statement";
    return false;
  }

  statement->BindString(0, fsp_path.value());
  statement->BindString(1, version_tag);
  statement->BindInt64(2, accessed_time.InMillisecondsSinceUnixEpoch());
  if (!statement->Step()) {
    LOG(ERROR) << "Couldn't execute statement";
    return false;
  }

  *inserted_id = statement->ColumnInt64(0);
  return true;
}

std::unique_ptr<std::optional<ContextDatabase::Item>>
ContextDatabase::GetItemById(int64_t item_id) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  if (item_id < 0) {
    return nullptr;
  }

  std::unique_ptr<sql::Statement> statement = std::make_unique<sql::Statement>(
      db_.GetCachedStatement(SQL_FROM_HERE, kSelectItemByIdSql));
  if (!statement) {
    LOG(ERROR) << "Couldn't create SQL statement";
    return nullptr;
  }

  statement->BindInt64(0, item_id);
  if (!statement->Step()) {
    // In the event the `Step()` failed, this could simply mean there is no item
    // for the `item_id`. `Succeeded` will return true in this case.
    if (statement->Succeeded()) {
      return std::make_unique<std::optional<Item>>(std::nullopt);
    }
    LOG(ERROR) << "Couldn't execute statement";
    return nullptr;
  }

  return std::make_unique<std::optional<ContextDatabase::Item>>(Item(
      item_id,
      /*fsp_path=*/statement->ColumnString(0),
      /*version_tag=*/statement->ColumnString(1),
      /*accessed_time=*/
      base::Time::FromMillisecondsSinceUnixEpoch(statement->ColumnInt64(2))));
}

bool ContextDatabase::UpdateAccessedTime(int64_t item_id,
                                         base::Time new_accessed_time) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  std::unique_ptr<sql::Statement> statement = std::make_unique<sql::Statement>(
      db_.GetCachedStatement(SQL_FROM_HERE, kUpdateAccessedTimeByIdSql));
  if (!statement) {
    LOG(ERROR) << "Couldn't create SQL statement";
    return false;
  }

  statement->BindInt64(0, new_accessed_time.InMillisecondsSinceUnixEpoch());
  statement->BindInt64(1, item_id);
  return statement->Run();
}

ContextDatabase::IdToItemMap ContextDatabase::GetAllItems() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  std::unique_ptr<sql::Statement> statement = std::make_unique<sql::Statement>(
      db_.GetCachedStatement(SQL_FROM_HERE, kSelectAllItemsSql));
  if (!statement) {
    LOG(ERROR) << "Couldn't create SQL statement";
    return {};
  }

  std::map<int64_t, Item> items;
  while (statement->Step()) {
    items.try_emplace(
        statement->ColumnInt64(0), statement->ColumnInt64(0),
        statement->ColumnString(1), statement->ColumnString(2),
        base::Time::FromMillisecondsSinceUnixEpoch(statement->ColumnInt64(3)));
  }

  if (!statement->Succeeded()) {
    return {};
  }

  return items;
}

bool ContextDatabase::RemoveItemsByIds(std::vector<int64_t> item_ids) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  std::stringstream delete_in_clause;
  for (size_t i = 0; i < item_ids.size(); i++) {
    delete_in_clause << base::NumberToString(item_ids.at(i));
    if (i < item_ids.size() - 1) {
      delete_in_clause << "','";
    }
  }

  const std::string remove_items_by_id_sql = base::StrCat(
      {"DELETE FROM items WHERE id IN ('", delete_in_clause.str(), "')"});
  CHECK(db_.IsSQLValid(remove_items_by_id_sql));

  // TODO(b/341833149): Cache the statement.
  std::unique_ptr<sql::Statement> statement = std::make_unique<sql::Statement>(
      db_.GetUniqueStatement(remove_items_by_id_sql));
  if (!statement) {
    LOG(ERROR) << "Couldn't create SQL statement";
    return {};
  }

  return statement->Run();
}

base::WeakPtr<ContextDatabase> ContextDatabase::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

bool ContextDatabase::Raze() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  meta_table_.Reset();
  if (!db_.is_open()) {
    return true;
  }

  db_.Poison();
  return sql::Database::Delete(db_path_);
}

ContextDatabase::~ContextDatabase() = default;

}  // namespace ash::file_system_provider