blob: 814351b79e0a02575d9ecb7c09eae6fc1e5434e9 [file] [log] [blame]
// 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 "chrome/browser/tab/tab_state_storage_database.h"
#include <memory>
#include <sstream>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/types/pass_key.h"
#include "chrome/browser/tab/protocol/children.pb.h"
#include "chrome/browser/tab/protocol/token.pb.h"
#include "chrome/browser/tab/storage_id.h"
#include "chrome/browser/tab/storage_loaded_data.h"
#include "sql/database.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "third_party/abseil-cpp//absl/container/flat_hash_map.h"
namespace tabs {
namespace {
using OpenTransaction = TabStateStorageDatabase::OpenTransaction;
// Update log:
// ??-08-2025, Version 1: Initial version of the database schema.
// 11-11-2025, Version 2: Add window_tag and is_off_the_record columns.
// 19-11-2025, Version 3: Change storage id type from int to blob and use token.
const int kCurrentVersionNumber = 3;
// The last version of the database schema that is compatible with the current
// version. Any changes made to the database schema that would break
// compatibility with the current version should increment this number to
// trigger a raze and rebuild of the database schema. NOTE: once this database
// become the primary database for restore this number should NEVER be
// incremented and we should use a graceful upgrade path. We can however,
// consider inlining the upgrade path and incrementing this number if a
// significant enough time (O(years)) have passed that there is no longer a
// reasonable expectation that out-of-date users would be able to restore their
// session.
const int kCompatibleVersionNumber = 3;
static_assert(
kCurrentVersionNumber >= kCompatibleVersionNumber,
"Current version must be greater than or equal to compatible version.");
constexpr char kTabsTableName[] = "nodes";
bool ExecuteSql(sql::Database* db, base::cstring_view sql_command) {
DCHECK(db->IsSQLValid(sql_command)) << sql_command << " is not valid SQL.";
return db->Execute(sql_command);
}
bool CreateSchema(sql::Database* db, sql::MetaTable* meta_table) {
DCHECK(db->HasActiveTransactions());
static constexpr char kCreateTabSchemaSql[] =
"CREATE TABLE IF NOT EXISTS nodes("
"id BLOB PRIMARY KEY NOT NULL,"
"window_tag TEXT NOT NULL,"
"is_off_the_record INTEGER NOT NULL,"
"type INTEGER NOT NULL,"
"children BLOB,"
"payload BLOB)";
static constexpr char kCreateIndexSql[] =
"CREATE INDEX IF NOT EXISTS nodes_window_index "
"ON nodes(window_tag, is_off_the_record)";
if (!ExecuteSql(db, kCreateTabSchemaSql)) {
DLOG(ERROR) << "Failed to create tab schema.";
return false;
}
if (!ExecuteSql(db, kCreateIndexSql)) {
DLOG(ERROR) << "Failed to create index.";
return false;
}
return true;
}
// TODO(crbug.com/459435876): Add histograms and enums to track database
// initialization failures.
bool InitSchema(sql::Database* db, sql::MetaTable* meta_table) {
bool has_metatable = meta_table->DoesTableExist(db);
if (!has_metatable && db->DoesTableExist(kTabsTableName)) {
db->Raze();
}
if (sql::MetaTable::RazeIfIncompatible(db, kCompatibleVersionNumber,
kCurrentVersionNumber) ==
sql::RazeIfIncompatibleResult::kFailed) {
LOG(ERROR) << "TabStateStorageDatabase failed to raze when an incompatible "
"version was detected.";
return false;
}
sql::Transaction transaction(db);
if (!transaction.Begin()) {
DLOG(ERROR) << "Transaction could not be started.";
return false;
}
if (!meta_table->Init(db, kCurrentVersionNumber, kCompatibleVersionNumber)) {
LOG(ERROR) << "TabStateStorageDatabase failed to initialize meta table.";
return false;
}
// This implies that the database was rolled back, without a downgrade path.
// This should never happen.
if (meta_table->GetCompatibleVersionNumber() > kCurrentVersionNumber) {
LOG(ERROR)
<< "TabStateStorageDatabase has a compatible version greater than the "
"current version. Did a rollback occur without a downgrade path?";
return false;
}
// Do not cache if the schema exists, it may have been razed earlier and needs
// to be recreated.
if (!db->DoesTableExist(kTabsTableName) && !CreateSchema(db, meta_table)) {
LOG(ERROR) << "TabStateStorageDatabase failed to create schema.";
return false;
}
// Any graceful upgrade logic when changing versions should go here in version
// upgrade order.
return meta_table->SetVersionNumber(kCurrentVersionNumber) &&
meta_table->SetCompatibleVersionNumber(kCompatibleVersionNumber) &&
transaction.Commit();
}
} // namespace
OpenTransaction::OpenTransaction(sql::Database* db,
base::PassKey<TabStateStorageDatabase>)
: transaction_(db) {}
OpenTransaction::~OpenTransaction() = default;
bool OpenTransaction::HasFailed() {
return mark_failed_;
}
void OpenTransaction::MarkFailed() {
mark_failed_ = true;
}
sql::Transaction* OpenTransaction::GetTransaction(
base::PassKey<TabStateStorageDatabase>) {
return &transaction_;
}
// static
bool TabStateStorageDatabase::OpenTransaction::IsValid(
OpenTransaction* transaction) {
return transaction && !transaction->HasFailed();
}
TabStateStorageDatabase::TabStateStorageDatabase(
const base::FilePath& profile_path)
: profile_path_(profile_path),
db_(sql::DatabaseOptions().set_preload(true).set_exclusive_locking(true),
sql::Database::Tag("TabStateStorage")) {}
TabStateStorageDatabase::~TabStateStorageDatabase() = default;
bool TabStateStorageDatabase::Initialize() {
base::FilePath db_dir = profile_path_.Append(FILE_PATH_LITERAL("Tabs"));
if (!base::CreateDirectory(db_dir)) {
LOG(ERROR) << "Failed to create directory for tab state storage database: "
<< db_dir;
return false;
}
const base::FilePath db_path = db_dir.Append(FILE_PATH_LITERAL("TabDB"));
if (!db_.Open(db_path)) {
LOG(ERROR) << "Failed to open tab state storage database: "
<< db_.GetErrorMessage();
return false;
}
if (!InitSchema(&db_, &meta_table_)) {
DLOG(ERROR) << "Failed to create schema for tab state storage database: "
<< db_.GetErrorMessage();
db_.Close();
return false;
}
return true;
}
bool TabStateStorageDatabase::SaveNode(OpenTransaction* transaction,
StorageId id,
std::string window_tag,
bool is_off_the_record,
TabStorageType type,
std::vector<uint8_t> payload,
std::vector<uint8_t> children) {
// TODO(crbug.com/451614469): Add support for OTR.
if (is_off_the_record) {
DLOG(ERROR) << "OTR saves are not supported yet.";
// Pretend we succeeded to avoid rollback.
return true;
}
DCHECK(OpenTransaction::IsValid(transaction));
static constexpr char kInsertNodeSql[] =
"INSERT OR REPLACE INTO nodes"
"(id, window_tag, is_off_the_record, type, payload, children)"
"VALUES (?,?,?,?,?,?)";
DCHECK(db_.IsSQLValid(kInsertNodeSql));
sql::Statement write_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kInsertNodeSql));
write_statement.BindBlob(0, StorageIdToBlob(id));
write_statement.BindString(1, window_tag);
write_statement.BindInt(2, static_cast<int>(is_off_the_record));
write_statement.BindInt(3, static_cast<int>(type));
write_statement.BindBlob(4, std::move(payload));
write_statement.BindBlob(5, std::move(children));
return write_statement.Run();
}
bool TabStateStorageDatabase::SaveNodePayload(OpenTransaction* transaction,
StorageId id,
std::vector<uint8_t> payload) {
DCHECK(OpenTransaction::IsValid(transaction));
static constexpr char kUpdatePayloadSql[] =
"UPDATE nodes "
"SET payload = ? "
"WHERE id = ?";
DCHECK(db_.IsSQLValid(kUpdatePayloadSql));
sql::Statement write_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kUpdatePayloadSql));
write_statement.BindBlob(0, std::move(payload));
write_statement.BindBlob(1, StorageIdToBlob(id));
return write_statement.Run();
}
bool TabStateStorageDatabase::SaveNodeChildren(OpenTransaction* transaction,
StorageId id,
std::vector<uint8_t> children) {
DCHECK(OpenTransaction::IsValid(transaction));
static constexpr char kUpdateChildrenSql[] =
"UPDATE nodes "
"SET children = ? "
"WHERE id = ?";
DCHECK(db_.IsSQLValid(kUpdateChildrenSql));
sql::Statement write_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kUpdateChildrenSql));
write_statement.BindBlob(0, std::move(children));
write_statement.BindBlob(1, StorageIdToBlob(id));
return write_statement.Run();
}
bool TabStateStorageDatabase::RemoveNode(OpenTransaction* transaction,
StorageId id) {
DCHECK(OpenTransaction::IsValid(transaction));
static constexpr char kDeleteNodeSql[] =
"DELETE FROM nodes "
"WHERE id = ?";
DCHECK(db_.IsSQLValid(kDeleteNodeSql));
sql::Statement write_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kDeleteNodeSql));
write_statement.BindBlob(0, StorageIdToBlob(id));
return write_statement.Run();
}
OpenTransaction* TabStateStorageDatabase::CreateTransaction() {
DCHECK(!open_transaction_) << "An open transaction already exists.";
open_transaction_.emplace(&db_, base::PassKey<TabStateStorageDatabase>());
sql::Transaction* transaction_ptr = open_transaction_->GetTransaction(
base::PassKey<TabStateStorageDatabase>());
if (!transaction_ptr->Begin()) {
DLOG(ERROR) << "Failed to begin transaction.";
open_transaction_->MarkFailed();
}
return &*open_transaction_;
}
bool TabStateStorageDatabase::CloseTransaction(
OpenTransaction* open_transaction) {
DCHECK(open_transaction_) << "There is no open transaction.";
DCHECK_EQ(open_transaction, &*open_transaction_) << "Transaction mismatch.";
sql::Transaction* transaction = open_transaction->GetTransaction(
base::PassKey<TabStateStorageDatabase>());
bool success = false;
if (open_transaction->HasFailed()) {
transaction->Rollback();
DLOG(ERROR) << "Transaction rolled back.";
} else {
success = transaction->Commit();
if (!success) {
DLOG(ERROR) << "Failed to commit transaction.";
// TODO(crbug.com/454005648): If possible, record the reason for commit
// failure here.
}
}
open_transaction_.reset();
return success;
}
std::unique_ptr<StorageLoadedData> TabStateStorageDatabase::LoadAllNodes(
std::string_view window_tag,
bool is_off_the_record,
std::unique_ptr<StorageLoadedData::Builder> builder) {
static constexpr char kSelectAllNodesSql[] =
"SELECT id, type, payload, children FROM nodes "
"WHERE window_tag = ? AND is_off_the_record = ?";
sql::Statement select_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kSelectAllNodesSql));
select_statement.BindString(0, window_tag);
select_statement.BindInt(1, static_cast<int>(is_off_the_record));
while (select_statement.Step()) {
StorageId id = StorageIdFromBlob(select_statement.ColumnBlob(0));
TabStorageType type =
static_cast<TabStorageType>(select_statement.ColumnInt(1));
builder->AddNode(id, type, select_statement.ColumnBlob(2),
base::PassKey<TabStateStorageDatabase>());
builder->AddChildren(id, type, select_statement.ColumnBlob(3),
base::PassKey<TabStateStorageDatabase>());
}
return builder->Build();
}
void TabStateStorageDatabase::ClearAllNodes() {
static constexpr char kDeleteAllNodesSql[] = "DELETE FROM nodes";
sql::Statement delete_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kDeleteAllNodesSql));
delete_statement.Run();
}
void TabStateStorageDatabase::ClearWindow(std::string_view window_tag) {
static constexpr char kDeleteWindowSql[] =
"DELETE FROM nodes WHERE window_tag = ?";
sql::Statement delete_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kDeleteWindowSql));
delete_statement.BindString(0, window_tag);
delete_statement.Run();
}
#if defined(NDEBUG)
void TabStateStorageDatabase::PrintAll() {
static constexpr char kSelectAllNodesSql[] =
"SELECT id, window_tag, is_off_the_record, type, children FROM nodes";
sql::Statement select_statement(
db_.GetCachedStatement(SQL_FROM_HERE, kSelectAllNodesSql));
int start_int = 0;
absl::flat_hash_map<StorageId, int> storage_id_to_int;
std::stringstream ss;
ss << "Nodes Table Dump:\n";
while (select_statement.Step()) {
StorageId id = StorageIdFromBlob(select_statement.ColumnBlob(0));
if (!storage_id_to_int.contains(id)) {
start_int++;
storage_id_to_int[id] = start_int;
}
std::string window_tag = select_statement.ColumnString(1);
bool is_off_the_record = select_statement.ColumnInt(2);
TabStorageType type =
static_cast<TabStorageType>(select_statement.ColumnInt(3));
base::span<const uint8_t> children_unparsed =
select_statement.ColumnBlob(4);
tabs_pb::Children children;
children.ParseFromArray(children_unparsed.data(), children_unparsed.size());
std::string children_str;
if (children.storage_id_size() != 0) {
for (int i = 0; i < children.storage_id_size(); i++) {
tabs_pb::Token child = children.storage_id().at(i);
StorageId child_id = StorageIdFromTokenProto(child);
if (!storage_id_to_int.contains(child_id)) {
start_int++;
storage_id_to_int[child_id] = start_int;
}
children_str += base::NumberToString(storage_id_to_int[child_id]);
if (i + 1 != children.storage_id_size()) {
children_str += ", ";
}
}
children_str = ", children=" + children_str;
}
ss << "Node: id=" << storage_id_to_int[id] << ", window_tag=" << window_tag
<< ", is_off_the_record=" << is_off_the_record
<< ", type=" << static_cast<int>(type) << children_str << "\n";
}
ss << "\nInt to Storage Id Map:\n";
for (const auto& [storage_id, temp_int] : storage_id_to_int) {
ss << "Entry: storage_id=" << storage_id.ToString()
<< ", temp_int=" << temp_int << "\n";
}
VLOG(1) << ss.str();
}
#endif
} // namespace tabs