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

#include "components/persistent_cache/sqlite/sqlite_backend_impl.h"

#include <stdint.h>

#include <functional>
#include <memory>
#include <optional>
#include <tuple>
#include <utility>

#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/ref_counted_memory.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/unsafe_shared_memory_region.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_view_util.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/trace_event/trace_event.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "components/persistent_cache/backend_type.h"
#include "components/persistent_cache/client.h"
#include "components/persistent_cache/metrics_util.h"
#include "components/persistent_cache/sqlite/vfs_util.h"
#include "components/persistent_cache/transaction_error.h"
#include "components/sqlite_vfs/client.h"
#include "components/sqlite_vfs/lock_state.h"
#include "components/sqlite_vfs/pending_file_set.h"
#include "components/sqlite_vfs/sandboxed_file.h"
#include "components/sqlite_vfs/sqlite_sandboxed_vfs.h"
#include "components/sqlite_vfs/vfs_utils.h"
#include "sql/database.h"
#include "sql/statement.h"
#include "sql/streaming_blob_handle.h"
#include "sql/transaction.h"

namespace persistent_cache {

namespace {

sql::Database::Tag TagFromClient(Client client) {
  switch (client) {
    case Client::kCodeCache:
      return sql::Database::Tag("CodeCache");
    case Client::kShaderCache:
      return sql::Database::Tag("ShaderCache");
    case Client::kTest:
      return sql::Database::Tag("Test");
  }
}

}  // namespace

// static
base::expected<std::unique_ptr<Backend>, TransactionError>
SqliteBackendImpl::Bind(PendingBackend pending_backend, Client client) {
  const auto access_rights =
      pending_backend.pending_file_set.read_write
          ? sqlite_vfs::SandboxedFile::AccessRights::kReadWrite
          : sqlite_vfs::SandboxedFile::AccessRights::kReadOnly;

  auto file_set = sqlite_vfs::SqliteVfsFileSet::Bind(
      VfsClientFromClient(client), std::move(pending_backend.pending_file_set));
  if (!file_set.has_value()) {
    // `pending_backend.pending_file_set` was consumed, but failed to produce a
    // SqliteVfsFileSet. This is typically a failure to map the shared locks
    // into this process's address space. There is no need to delete the files.
    return base::unexpected(TransactionError::kConnectionError);
  }
  auto instance =
      base::WrapUnique(new SqliteBackendImpl(*std::move(file_set), client));

  base::ElapsedTimer timer;
  RETURN_IF_ERROR(instance->Initialize());

  base::UmaHistogramMicrosecondsTimes(
      GetHistogramName(
          client, "BackendInitialize",
          access_rights == sqlite_vfs::SandboxedFile::AccessRights::kReadWrite),
      timer.Elapsed());
  return instance;
}

SqliteBackendImpl::SqliteBackendImpl(sqlite_vfs::SqliteVfsFileSet vfs_file_set,
                                     Client client)
    : database_path_(vfs_file_set.GetDbVirtualFilePath()),
      vfs_file_set_(std::move(vfs_file_set)),
      unregister_runner_(sqlite_vfs::SqliteSandboxedVfsDelegate::GetInstance()
                             ->RegisterSandboxedFiles(vfs_file_set_)),
      db_(std::in_place,
          sql::DatabaseOptions()
              .set_read_only(vfs_file_set_.read_only())
              // Set the database's locking_mode to EXCLUSIVE if the file set
              // supports only a single connection to the database.
              .set_exclusive_locking(vfs_file_set_.is_single_connection())
              // Enable write-ahead logging as indicated.
              .set_wal_mode(vfs_file_set_.wal_mode())
              .set_vfs_name_discouraged(
                  sqlite_vfs::SqliteSandboxedVfsDelegate::kSqliteVfsName)
              // Prevent SQLite from trying to use mmap, as SandboxedVfs does
              // not currently support this.
              .set_mmap_enabled(false),
          TagFromClient(client)) {}

SqliteBackendImpl::~SqliteBackendImpl() {
  base::AutoLock lock(lock_, base::subtle::LockTracking::kEnabled);
  db_.reset();
}

base::expected<void, TransactionError> SqliteBackendImpl::Initialize() {
  TRACE_EVENT("persistent_cache", "Initialize");

  // Open  `db_` under `lock_` with lock tracking enabled. This allows this
  // class to be usable from multiple threads even though `sql::Database` is
  // sequence bound.
  base::AutoLock lock(lock_, base::subtle::LockTracking::kEnabled);

  RETURN_IF_ERROR(InitializeImpl(),
                  [](int error_code) { return TranslateError(error_code); });

  return base::ok();
}

base::expected<void, int> SqliteBackendImpl::InitializeImpl() {
  // Install an error callback to capture the first error, if any, encountered
  // while opening a database. Database::GetErrorCode() returns a generic error
  // after a failed Open(), so it is of no use in this case.
  std::optional<int> error_code;
  db_->set_error_callback(base::BindRepeating(
      [](std::optional<int>& error_code_out, int error,
         sql::Statement* statement) {
        if (!error_code_out.has_value()) {
          error_code_out = error;
        }
      },
      std::ref(error_code)));

  const bool open_success = db_->Open(database_path_);
  db_->reset_error_callback();

  if (!open_success) {
    return base::unexpected(error_code.value_or(SQLITE_CANTOPEN));
  }

  // Check the user-version (https://sqlite.org/pragma.html#pragma_user_version)
  // to see if there has been a schema change since the last time this database
  // was modified.
  int detected_user_version;
  if (sql::Statement get_user_version_stm(
          db_->GetUniqueStatement("PRAGMA user_version"));
      get_user_version_stm.is_valid() && get_user_version_stm.Step()) {
    detected_user_version = get_user_version_stm.ColumnInt(0);
  } else {
    return base::unexpected(db_->GetErrorCode());
  }

  if (detected_user_version == kCurrentUserVersion) {
    return base::ok();
  }

  // A read only connection cannot do anything to recover from a mismatched
  // user version.
  if (IsReadOnly()) {
    return base::unexpected(SQLITE_READONLY);
  }

  // This is either a new database (user-version has never been set) or was last
  // written with an old schema. Recreate the table with the current schema and
  // update the user-version.

  // Begin an explicit transaction so that creating the table and
  // setting the associated user version is done atomically.
  sql::Transaction transaction(&*db_);
  if (!transaction.Begin()) {
    return base::unexpected(db_->GetErrorCode());
  }

  if (!db_->Execute("DROP TABLE IF EXISTS entries")) {
    return base::unexpected(db_->GetErrorCode());
  }

  // IMPORTANT: Revise the DROP TABLE statement above if more than the one
  // "entries" table is created here.
  if (!db_->Execute("CREATE TABLE entries(key BLOB PRIMARY KEY "
                    "UNIQUE NOT NULL, content BLOB NOT NULL,"
                    " input_signature INTEGER, write_timestamp INTEGER)")) {
    return base::unexpected(db_->GetErrorCode());
  }

  if (!db_->Execute(
          base::StrCat({"PRAGMA user_version=",
                        base::NumberToString(kCurrentUserVersion)}))) {
    return base::unexpected(db_->GetErrorCode());
  }

  if (!transaction.Commit()) {
    return base::unexpected(db_->GetErrorCode());
  }

  return base::ok();
}

base::expected<std::optional<EntryMetadata>, TransactionError>
SqliteBackendImpl::Find(base::span<const uint8_t> key,
                        BufferProvider buffer_provider) {
  base::AutoLock lock(lock_, base::subtle::LockTracking::kEnabled);
  CHECK_GT(key.size(), 0ull);
  TRACE_EVENT("persistent_cache", "Find");

  ASSIGN_OR_RETURN(auto metadata, FindImpl(key, buffer_provider),
                   [](int error_code) {
                     return TranslateError(error_code);
                   });
  return metadata;
}

base::expected<void, TransactionError> SqliteBackendImpl::Insert(
    base::span<const uint8_t> key,
    base::span<const uint8_t> content,
    EntryMetadata metadata) {
  base::AutoLock lock(lock_, base::subtle::LockTracking::kEnabled);

  CHECK_GT(key.size(), 0ull);
  TRACE_EVENT("persistent_cache", "Insert");

  // The caller should not specify a write timestamp.
  CHECK_EQ(metadata.write_timestamp, 0);

  RETURN_IF_ERROR(InsertImpl(key, content, metadata.input_signature),
                  [](int error_code) { return TranslateError(error_code); });

  return base::ok();
}

base::expected<void, int> SqliteBackendImpl::ExecuteStatementForTesting(
    base::cstring_view statement) {
  base::AutoLock lock(lock_, base::subtle::LockTracking::kEnabled);

  if (!db_->Execute(statement)) {
    return base::unexpected(db_->GetErrorCode());
  }

  return base::ok();
}

base::expected<std::optional<EntryMetadata>, int> SqliteBackendImpl::FindImpl(
    base::span<const uint8_t> key,
    BufferProvider buffer_provider) {
  // Begin an explicit read transaction under which multiple statements will be
  // used to read from the database if the database may have multiple
  // connections. A transaction is not necessary if the database is opened for a
  // single connection, as it is not possible for another connection to modify
  // the database between the statements below.
  std::optional<sql::Transaction> transaction;
  if (!vfs_file_set_.is_single_connection() &&
      !transaction.emplace(&*db_).Begin()) {
    return base::unexpected(db_->GetErrorCode());
  }

  // Read the rowid and metadata.
  sql::Statement stm(
      db_->GetCachedStatement(SQL_FROM_HERE,
                              "SELECT rowid, input_signature, write_timestamp "
                              "FROM entries WHERE key = ?"));
  if (!stm.is_valid()) {
    return base::unexpected(db_->GetErrorCode());
  }

  stm.BindBlob(0, key);

  if (!stm.Step()) {
    if (stm.Succeeded()) {
      // Cache miss. Do not run `buffer_provider`, return no value.
      return std::nullopt;
    }
    // Error stepping.
    return base::unexpected(db_->GetErrorCode());
  }

  // Open a handle to get the size of the content.
  if (auto blob =
          db_->GetStreamingBlob("entries", "content", stm.ColumnInt64(0),
                                /*readonly=*/true);
      blob.has_value()) {
    bool succeeded = true;
    size_t content_size = base::checked_cast<size_t>(blob->GetSize());
    // Get a buffer from the caller.
    if (base::span<uint8_t> content_buffer = buffer_provider(content_size);
        !content_buffer.empty()) {
      CHECK_EQ(content_buffer.size(), content_size);
      // Copy the content from the database directly into the caller's buffer.
      succeeded = blob->Read(/*offset=*/0, content_buffer);
    }
    if (succeeded) {
      return EntryMetadata{
          .input_signature = stm.ColumnInt64(1),
          .write_timestamp =
              stm.ColumnTime(2).ToDeltaSinceWindowsEpoch().InMicroseconds()};
    }
  }

  return base::unexpected(db_->GetErrorCode());
}

base::expected<void, int> SqliteBackendImpl::InsertImpl(
    base::span<const uint8_t> key,
    base::span<const uint8_t> content,
    int64_t input_signature) {
  if (sql::Statement stm(db_->GetCachedStatement(
          SQL_FROM_HERE,
          "REPLACE INTO entries (key, content, input_signature, "
          "write_timestamp) VALUES (?, ?, ?, ?)"));
      stm.is_valid()) {
    stm.BindBlob(0, key);
    // SAFETY: SQLite reads from `content` while in scope. Internally,
    // sql::Statement clears its bindings upon destruction, guaranteeing it
    // cannot hold a dangling reference once this block ends.
    stm.BindBlob(1,
                 base::MakeRefCounted<base::RefCountedStaticMemory>(content));
    stm.BindInt64(2, input_signature);
    stm.BindTime(3, base::Time::Now());
    if (stm.Run()) {
      return base::ok();
    }
  }
  return base::unexpected(db_->GetErrorCode());
}

// static
TransactionError SqliteBackendImpl::TranslateError(int error_code) {
  // Handle extended result codes directly.
  if (error_code == SQLITE_IOERR_LOCK) {
    // Lock abandonment.
    return TransactionError::kConnectionError;
  }

  // Narrow the error down to its primary result code.
  switch (error_code & 0xFF) {
    case SQLITE_BUSY:
    case SQLITE_FULL:
    case SQLITE_INTERRUPT:
    case SQLITE_NOMEM:
      return TransactionError::kTransient;

    case SQLITE_CANTOPEN:
    case SQLITE_READONLY:  // Cannot modify a read-only database.
      // This also includes SQLITE_READONLY_ROLLBACK, which can happen if a
      // read-only connection is the first to discover that a hot journal needs
      // to be rolled back.
      return TransactionError::kConnectionError;

    case SQLITE_ERROR:
    case SQLITE_CORRUPT:
    case SQLITE_IOERR:
    case SQLITE_NOTADB:
      // The underlying file cannot be trusted. The best course of action is for
      // the owner to close the connection and delete the underlying files.
      return TransactionError::kPermanent;
  }

  // Remaining errors are considered transient. The Sql.Database.Statement.Error
  // metrics should be monitored to ensure that there are no surprising
  // permanent errors wrongly handled here as this will mean unusable databases
  // that keep being used.
  return TransactionError::kTransient;
}

BackendType SqliteBackendImpl::GetType() const {
  return BackendType::kSqlite;
}

bool SqliteBackendImpl::IsReadOnly() const {
  return vfs_file_set_.read_only();
}

LockState SqliteBackendImpl::Abandon() {
  // Read only instances do not have the privilege of abandoning an instance.
  CHECK(!IsReadOnly());
  switch (vfs_file_set_.Abandon()) {
    case sqlite_vfs::LockState::kNotHeld:
      return LockState::kNotHeld;
    case sqlite_vfs::LockState::kReading:
      return LockState::kReading;
    case sqlite_vfs::LockState::kWriting:
      return LockState::kWriting;
  }
}

}  // namespace persistent_cache
