blob: fb26c2350695110d08c38afe2ca05e46e8034824 [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 "content/browser/indexed_db/instance/sqlite/database_connection.h"
#include <memory>
#include <string>
#include <utility>
#include "base/check.h"
#include "base/containers/heap_array.h"
#include "base/functional/callback.h"
#include "base/memory/ptr_util.h"
#include "base/notimplemented.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/types/expected.h"
#include "build/build_config.h"
#include "content/browser/indexed_db/indexed_db_value.h"
#include "content/browser/indexed_db/instance/backing_store.h"
#include "content/browser/indexed_db/instance/record.h"
#include "content/browser/indexed_db/instance/sqlite/backing_store_cursor_impl.h"
#include "content/browser/indexed_db/instance/sqlite/backing_store_transaction_impl.h"
#include "content/browser/indexed_db/instance/sqlite/record_iterator.h"
#include "content/browser/indexed_db/status.h"
#include "sql/database.h"
#include "sql/error_delegate_util.h"
#include "sql/meta_table.h"
#include "sql/recovery.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "third_party/blink/public/common/indexeddb/indexeddb_key.h"
#include "third_party/blink/public/common/indexeddb/indexeddb_key_path.h"
#include "third_party/blink/public/common/indexeddb/indexeddb_metadata.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom.h"
// TODO(crbug.com/40253999): Rename the file to indicate that it contains
// backend-agnostic utils to encode/decode IDB types, and potentially move the
// (Encode/Decode)KeyPath methods below to this file.
#include "content/browser/indexed_db/indexed_db_leveldb_coding.h"
// TODO(crbug.com/40253999): Remove after handling all error cases.
#define TRANSIENT_CHECK(condition) CHECK(condition)
// Returns an error if the given SQL statement has not succeeded/is no longer
// valid (`Succeeded()` returns false; `db_` has an error).
//
// This should be used after `Statement::Step()` returns false.
#define RETURN_IF_STATEMENT_ERRORED(statement) \
if (!statement.Succeeded()) { \
return base::unexpected(Status(*db_)); \
}
// Runs the statement and returns if there was an error. For use with functions
// that return StatusOr<T>.
#define RUN_STATEMENT_RETURN_ON_ERROR(statement) \
if (!statement.Run()) { \
return base::unexpected(Status(*db_)); \
}
// Runs the statement and returns if there was an error. For use with functions
// that return Status.
#define RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement) \
if (!statement.Run()) { \
return Status(*db_); \
}
namespace content::indexed_db::sqlite {
namespace {
// The separator used to join the strings when encoding an `IndexedDBKeyPath` of
// type array. Spaces are not allowed in the individual strings, which makes
// this a convenient choice.
constexpr char16_t kKeyPathSeparator[] = u" ";
void BindKeyPath(sql::Statement& statement,
int param_index,
const blink::IndexedDBKeyPath& key_path) {
switch (key_path.type()) {
case blink::mojom::IDBKeyPathType::Null:
statement.BindNull(param_index);
break;
case blink::mojom::IDBKeyPathType::String:
statement.BindBlob(param_index, key_path.string());
break;
case blink::mojom::IDBKeyPathType::Array:
statement.BindBlob(param_index,
base::JoinString(key_path.array(), kKeyPathSeparator));
break;
default:
NOTREACHED();
}
}
blink::IndexedDBKeyPath ColumnKeyPath(sql::Statement& statement,
int column_index) {
if (statement.GetColumnType(column_index) == sql::ColumnType::kNull) {
// `Null` key path.
return blink::IndexedDBKeyPath();
}
std::u16string encoded;
TRANSIENT_CHECK(statement.ColumnBlobAsString16(column_index, &encoded));
std::vector<std::u16string> parts = base::SplitString(
encoded, kKeyPathSeparator, base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (parts.empty()) {
// Empty `String` key path.
return blink::IndexedDBKeyPath(std::u16string());
}
if (parts.size() == 1) {
// Non-empty `String` key path.
return blink::IndexedDBKeyPath(std::move(parts.front()));
}
// `Array` key path.
return blink::IndexedDBKeyPath(std::move(parts));
}
// These are schema versions of our implementation of `sql::Database`; not the
// version supplied by the application for the IndexedDB database.
//
// The version used to initialize the meta table for the first time.
constexpr int kEmptySchemaVersion = 1;
constexpr int kCurrentSchemaVersion = 10;
constexpr int kCompatibleSchemaVersion = kCurrentSchemaVersion;
// Atomically creates the current schema for a new `db`, inserts the initial
// IndexedDB metadata entry with `name`, and sets the current version in
// `meta_table`.
void InitializeNewDatabase(sql::Database* db,
std::u16string_view name,
sql::MetaTable* meta_table) {
sql::Transaction transaction(db);
TRANSIENT_CHECK(transaction.Begin());
// Create the tables.
//
// Note on the schema: The IDB spec defines the "name"
// (https://www.w3.org/TR/IndexedDB/#name) of the database, object stores and
// indexes as an arbitrary sequence of 16-bit code units, which implies that
// the application-supplied name strings need not be valid UTF-16.
// "key_path"s are always valid UTF-16 since they contain only identifiers
// (required to be valid UTF-16) and periods.
// However, to avoid unnecessary conversion from UTF-16 to UTF-8 and back,
// all 16-bit strings are stored as BLOBs.
//
// Though object store names and index names (within an object store) are
// unique, this is not enforced in the schema itself since this constraint can
// be transiently violated at the backing store level (the IDs are always
// guaranteed to be unique, however). This is because creation of object
// stores and indexes happens on the preemptive task queue while deletion
// happens on the regular queue.
//
// Stores a single row containing the properties of
// `IndexedDBDatabaseMetadata` for this database.
TRANSIENT_CHECK(
db->Execute("CREATE TABLE indexed_db_metadata "
"(name BLOB NOT NULL,"
" version INTEGER NOT NULL)"));
TRANSIENT_CHECK(
db->Execute("CREATE TABLE object_stores "
"(id INTEGER PRIMARY KEY,"
" name BLOB NOT NULL,"
" key_path BLOB,"
" auto_increment INTEGER NOT NULL,"
" key_generator_current_number INTEGER NOT NULL)"));
TRANSIENT_CHECK(
db->Execute("CREATE TABLE indexes "
"(object_store_id INTEGER NOT NULL,"
" id INTEGER NOT NULL,"
" name BLOB NOT NULL,"
" key_path BLOB,"
" is_unique INTEGER NOT NULL,"
" multi_entry INTEGER NOT NULL,"
" PRIMARY KEY (object_store_id, id)"
") WITHOUT ROWID"));
// Stores object store records. The rows are immutable - updating the value
// for a combination of object_store_id and key is accomplished by deleting
// the previous row and inserting a new one (see `PutRecord()`).
TRANSIENT_CHECK(
db->Execute("CREATE TABLE records "
"(row_id INTEGER PRIMARY KEY,"
" object_store_id INTEGER NOT NULL,"
" key BLOB NOT NULL,"
" value BLOB NOT NULL)"));
// Create the index separately so it can be given a name (which is referenced
// by tests).
TRANSIENT_CHECK(db->Execute(
"CREATE UNIQUE INDEX records_by_key ON records(object_store_id, key)"));
// Stores the mapping of object store records to the index keys that reference
// them: record_row_id -> [index_id, key]. In general, a record may be
// referenced by multiple keys across multiple indexes (on the same object
// store). Since the row_id of a record uniquely identifies the record across
// object stores, object_store_id is not part of the primary key of this
// table. Deleting a record leads to the deletion of all index references to
// it (through the on_record_deleted trigger).
// The object store ID and record key are redundant information, but including
// them here and creating an index over them expedites cursor iteration
// because it removes the need to JOIN against the records table. See
// https://crbug.com/433318798.
TRANSIENT_CHECK(
db->Execute("CREATE TABLE index_references "
"(record_row_id INTEGER NOT NULL,"
" index_id INTEGER NOT NULL,"
" key BLOB NOT NULL,"
" object_store_id INTEGER NOT NULL,"
" record_key BLOB NOT NULL,"
" PRIMARY KEY (record_row_id, index_id, key)"
") WITHOUT ROWID"));
TRANSIENT_CHECK(db->Execute(
"CREATE INDEX index_references_by_key "
"ON index_references (object_store_id, index_id, key, record_key)"));
// This table stores blob metadata and its actual bytes. A blob should only
// appear once, regardless of how many records point to it. The columns in
// this table should be effectively const, as SQLite blob handles will be used
// to stream out of the table, and the associated row must never change while
// blob handles are active. Blobs will be removed from this table when no
// references remain (see `blob_references`).
//
// TODO(crbug.com/419208485): consider taking into account the blob's UUID to
// further avoid duplication.
TRANSIENT_CHECK(db->Execute(
"CREATE TABLE blobs "
// This row id will be used as the IndexedDBExternalObject::blob_number_.
"(row_id INTEGER PRIMARY KEY,"
// Corresponds to `IndexedDBExternalObject::ObjectType`.
" object_type INTEGER NOT NULL,"
// This can be null if the blob is stored on disk, which will be the
// case for legacy blobs. It's also temporarily null while FSA handles are
// being serialized into a token (after which point, this holds the
// token).
" bytes BLOB,"
" mime_type TEXT," // Null for FSA handles.
" size_bytes INTEGER," // Null for FSA handles.
" file_name BLOB," // only for files
" last_modified INTEGER)" // only for files
));
// Blobs may be referenced by rows in `records` or by active connections to
// clients.
// TODO(crbug.com/419208485): Consider making this a WITHOUT ROWID table.
// Since NULL values are not allowed in the primary key of such a table, a
// specific value of record_row_id will be needed to represent active blobs.
TRANSIENT_CHECK(
db->Execute("CREATE TABLE blob_references "
"(row_id INTEGER PRIMARY KEY,"
" blob_row_id INTEGER NOT NULL,"
// record_row_id will be null when the reference corresponds
// to an active blob reference (represented in the browser by
// ActiveBlobStreamer). Otherwise it will be the id of the
// record row that holds the reference.
" record_row_id INTEGER)"));
TRANSIENT_CHECK(
db->Execute("CREATE INDEX blob_references_by_blob "
"ON blob_references (blob_row_id)"));
TRANSIENT_CHECK(
db->Execute("CREATE INDEX blob_references_by_record "
"ON blob_references (record_row_id)"));
// Create deletion triggers. Deletion triggers are not used for the
// object_stores and indexes tables since their deletion occurs only through
// dedicated functions intended specifically for this purpose.
TRANSIENT_CHECK(db->Execute(
"CREATE TRIGGER on_record_deleted AFTER DELETE ON records "
"BEGIN"
" DELETE FROM index_references WHERE record_row_id = OLD.row_id;"
" DELETE FROM blob_references WHERE record_row_id = OLD.row_id;"
"END"));
TRANSIENT_CHECK(db->Execute(
"CREATE TRIGGER on_blob_reference_deleted"
" AFTER DELETE ON blob_references "
"WHEN NOT EXISTS"
" (SELECT 1 FROM blob_references WHERE blob_row_id = OLD.blob_row_id) "
"BEGIN"
" DELETE FROM blobs WHERE row_id = OLD.blob_row_id;"
"END"));
// Insert the initial metadata entry.
sql::Statement statement(
db->GetUniqueStatement("INSERT INTO indexed_db_metadata "
"(name, version) VALUES (?, ?)"));
statement.BindBlob(0, std::u16string(name));
statement.BindInt64(1, blink::IndexedDBDatabaseMetadata::NO_VERSION);
TRANSIENT_CHECK(statement.Run());
// Set the current version in the meta table.
TRANSIENT_CHECK(meta_table->SetVersionNumber(kCurrentSchemaVersion));
TRANSIENT_CHECK(transaction.Commit());
}
blink::IndexedDBDatabaseMetadata GenerateIndexedDbMetadata(sql::Database* db) {
blink::IndexedDBDatabaseMetadata metadata;
// Set the database name and version.
{
sql::Statement statement(db->GetReadonlyStatement(
"SELECT name, version FROM indexed_db_metadata"));
TRANSIENT_CHECK(statement.Step());
statement.ColumnBlobAsString16(0, &metadata.name);
metadata.version = statement.ColumnInt64(1);
}
// Populate object store metadata.
{
sql::Statement statement(db->GetReadonlyStatement(
"SELECT id, name, key_path, auto_increment FROM object_stores"));
int64_t max_object_store_id = 0;
while (statement.Step()) {
blink::IndexedDBObjectStoreMetadata store_metadata;
store_metadata.id = statement.ColumnInt64(0);
statement.ColumnBlobAsString16(1, &store_metadata.name);
store_metadata.key_path = ColumnKeyPath(statement, 2);
store_metadata.auto_increment = statement.ColumnBool(3);
max_object_store_id = std::max(max_object_store_id, store_metadata.id);
metadata.object_stores[store_metadata.id] = std::move(store_metadata);
}
TRANSIENT_CHECK(statement.Succeeded());
metadata.max_object_store_id = max_object_store_id;
}
// Populate index metadata.
{
sql::Statement statement(db->GetReadonlyStatement(
"SELECT object_store_id, id, name, key_path, is_unique, multi_entry "
"FROM indexes"));
while (statement.Step()) {
blink::IndexedDBIndexMetadata index_metadata;
int64_t object_store_id = statement.ColumnInt64(0);
index_metadata.id = statement.ColumnInt64(1);
statement.ColumnBlobAsString16(2, &index_metadata.name);
index_metadata.key_path = ColumnKeyPath(statement, 3);
index_metadata.unique = statement.ColumnBool(4);
index_metadata.multi_entry = statement.ColumnBool(5);
blink::IndexedDBObjectStoreMetadata& store_metadata =
metadata.object_stores[object_store_id];
store_metadata.max_index_id =
std::max(store_metadata.max_index_id, index_metadata.id);
store_metadata.indexes[index_metadata.id] = std::move(index_metadata);
}
TRANSIENT_CHECK(statement.Succeeded());
}
return metadata;
}
std::vector<std::string_view> StartRecordRangeQuery(
std::string_view command,
const blink::IndexedDBKeyRange& key_range) {
std::vector<std::string_view> query_pieces{command};
query_pieces.push_back(
" FROM records"
" WHERE object_store_id = ?");
if (key_range.lower().IsValid()) {
query_pieces.push_back(key_range.lower_open() ? " AND key > ?"
: " AND key >= ?");
}
if (key_range.upper().IsValid()) {
query_pieces.push_back(key_range.upper_open() ? " AND key < ?"
: " AND key <= ?");
}
return query_pieces;
}
// Returns the next index for binding subsequent parameters.
int BindRecordRangeQueryParams(sql::Statement& statement,
int64_t object_store_id,
const blink::IndexedDBKeyRange& key_range) {
int param_index = 0;
statement.BindInt64(param_index++, object_store_id);
if (key_range.lower().IsValid()) {
statement.BindBlob(param_index++, EncodeSortableIDBKey(key_range.lower()));
}
if (key_range.upper().IsValid()) {
statement.BindBlob(param_index++, EncodeSortableIDBKey(key_range.upper()));
}
return param_index;
}
class ObjectStoreRecordIterator : public RecordIterator {
public:
ObjectStoreRecordIterator(base::WeakPtr<DatabaseConnection> db, bool key_only)
: db_(db), key_only_(key_only) {}
~ObjectStoreRecordIterator() override {
if (db_) {
db_->ReleaseLongLivedStatement(statement_id_);
}
}
// If Initialize() returns an error or nullptr, `this` should be discarded.
StatusOr<std::unique_ptr<Record>> Initialize(
int64_t object_store_id,
const blink::IndexedDBKeyRange& key_range,
bool ascending_order) {
std::vector<std::string_view> query_pieces = StartRecordRangeQuery(
key_only_ ? "SELECT key" : "SELECT key, value, row_id", key_range);
if (ascending_order) {
query_pieces.push_back(
" AND (@is_first_seek = 1 OR key > @position)"
" AND (@target_key IS NULL OR key >= @target_key)"
" ORDER BY key ASC");
} else {
query_pieces.push_back(
" AND (@is_first_seek = 1 OR key < @position)"
" AND (@target_key IS NULL OR key <= @target_key)"
" ORDER BY key DESC");
}
// LIMIT is needed to use OFFSET. A negative LIMIT implies no limit on the
// number of rows returned:
// https://www.sqlite.org/lang_select.html#the_limit_clause.
query_pieces.push_back(" LIMIT -1 OFFSET @offset");
sql::Statement* statement;
std::tie(statement_id_, statement) =
db_->CreateLongLivedStatement(base::StrCat(query_pieces));
int param_index =
BindRecordRangeQueryParams(*statement, object_store_id, key_range);
// Store the variable parameter indexes and attempt to find the initial
// record in the range.
statement->BindBool(is_first_seek_index_ = param_index++, true);
statement->BindNull(position_index_ = param_index++);
statement->BindNull(target_key_index_ = param_index++);
statement->BindInt64(offset_index_ = param_index++, 0);
if (statement->Step()) {
return ReadRow(*statement);
}
if (statement->Succeeded()) {
// Empty range.
return nullptr;
}
return base::unexpected(db_->GetStatusOfLastOperation());
}
void SavePosition() override { saved_position_ = position_; }
bool TryResetToLastSavedPosition() override {
if (!saved_position_) {
return false;
}
position_ = *std::move(saved_position_);
saved_position_.reset();
return true;
}
protected:
// RecordIterator:
void BindParameters(sql::Statement& statement,
const blink::IndexedDBKey& target_key,
const blink::IndexedDBKey& target_primary_key,
uint32_t offset) override {
// `target_primary_key` is not expected when iterating over object store
// records.
CHECK(!target_primary_key.IsValid());
statement.BindBool(is_first_seek_index_, false);
statement.BindBlob(position_index_, position_);
if (target_key.IsValid()) {
statement.BindBlob(target_key_index_, EncodeSortableIDBKey(target_key));
} else {
statement.BindNull(target_key_index_);
}
statement.BindInt64(offset_index_, offset);
}
StatusOr<std::unique_ptr<Record>> ReadRow(
sql::Statement& statement) override {
CHECK(statement.Succeeded());
statement.ColumnBlobAsString(0, &position_);
blink::IndexedDBKey key = DecodeSortableIDBKey(position_);
if (key_only_) {
return std::make_unique<ObjectStoreKeyOnlyRecord>(std::move(key));
}
IndexedDBValue value;
statement.ColumnBlobAsVector(1, &value.bits);
int64_t record_row_id = statement.ColumnInt64(2);
return db_
->AddExternalObjectMetadataToValue(std::move(value), record_row_id)
.transform([&](IndexedDBValue value_with_metadata) {
return std::make_unique<ObjectStoreRecord>(
std::move(key), std::move(value_with_metadata));
});
}
sql::Statement* GetStatement() override {
if (!db_) {
return nullptr;
}
return db_->GetLongLivedStatement(statement_id_);
}
private:
base::WeakPtr<DatabaseConnection> db_;
uint64_t statement_id_ = 0;
bool key_only_ = false;
int is_first_seek_index_ = 0;
int position_index_ = 0;
int target_key_index_ = 0;
int offset_index_ = 0;
// Encoded key from the current record, tracking the position in the range.
std::string position_;
std::optional<std::string> saved_position_;
};
class IndexRecordIterator : public RecordIterator {
public:
// If `first_primary_keys_only` is true, `this` will iterate over only the
// first (i.e., smallest) primary key for each index key in `key_range` (this
// correlates to the nextunique/prevunique IndexedDB cursor direction). Else,
// all the primary keys are iterated over for each index key in the range.
IndexRecordIterator(base::WeakPtr<DatabaseConnection> db,
bool key_only,
bool first_primary_keys_only)
: db_(db),
key_only_(key_only),
first_primary_keys_only_(first_primary_keys_only) {}
~IndexRecordIterator() override {
if (db_) {
db_->ReleaseLongLivedStatement(statement_id_);
}
}
// If Initialize() returns an error or nullptr, `this` should be discarded.
StatusOr<std::unique_ptr<Record>> Initialize(
int64_t object_store_id,
int64_t index_id,
const blink::IndexedDBKeyRange& key_range,
bool ascending_order) {
std::vector<std::string_view> query_pieces{
"SELECT index_references.key AS index_key"};
if (first_primary_keys_only_) {
query_pieces.push_back(", MIN(record_key)");
} else {
query_pieces.push_back(", record_key");
}
if (key_only_) {
query_pieces.push_back(" FROM index_references");
} else {
query_pieces.push_back(
", records.value"
", records.row_id"
" FROM index_references INNER JOIN records"
" ON index_references.record_row_id = records.row_id");
}
query_pieces.push_back(
" WHERE index_references.object_store_id = @object_store_id"
" AND index_references.index_id = @index_id");
if (key_range.lower().IsValid()) {
query_pieces.push_back(key_range.lower_open()
? " AND index_key > @lower"
: " AND index_key >= @lower");
}
if (key_range.upper().IsValid()) {
query_pieces.push_back(key_range.upper_open()
? " AND index_key < @upper"
: " AND index_key <= @upper");
}
if (ascending_order) {
if (first_primary_keys_only_) {
query_pieces.push_back(
" AND (@is_first_seek = 1 OR index_key > @position)"
" AND (@target_key IS NULL OR index_key >= @target_key)"
" GROUP BY index_key"
" ORDER BY index_key ASC");
} else {
query_pieces.push_back(
" AND "
" ("
" @is_first_seek = 1"
" OR (index_key = @position AND record_key >"
" @object_store_position)"
" OR index_key > @position"
" )"
" AND (@target_key IS NULL OR index_key >= @target_key)"
" AND "
" ("
" @target_primary_key IS NULL"
" OR (index_key = @target_key AND record_key >="
" @target_primary_key)"
" OR index_key > @target_key"
" )"
" ORDER BY index_key ASC, record_key ASC");
}
} else {
if (first_primary_keys_only_) {
query_pieces.push_back(
" AND (@is_first_seek = 1 OR index_key < @position)"
" AND (@target_key IS NULL OR index_key <= @target_key)"
" GROUP BY index_key"
" ORDER BY index_key DESC");
} else {
query_pieces.push_back(
" AND "
" ("
" @is_first_seek = 1"
" OR (index_key = @position AND record_key < "
" @object_store_position)"
" OR index_key < @position"
" )"
" AND (@target_key IS NULL OR index_key <= @target_key)"
" AND "
" ("
" @target_primary_key IS NULL"
" OR (index_key = @target_key AND record_key <= "
" @target_primary_key)"
" OR index_key < @target_key"
" )"
" ORDER BY index_key DESC, record_key DESC");
}
}
// LIMIT is needed to use OFFSET. A negative LIMIT implies no limit on the
// number of rows returned:
// https://www.sqlite.org/lang_select.html#the_limit_clause.
query_pieces.push_back(" LIMIT -1 OFFSET @offset");
sql::Statement* statement;
std::tie(statement_id_, statement) =
db_->CreateLongLivedStatement(base::StrCat(query_pieces));
int param_index = 0;
statement->BindInt64(param_index++, object_store_id);
statement->BindInt64(param_index++, index_id);
if (key_range.lower().IsValid()) {
statement->BindBlob(param_index++,
EncodeSortableIDBKey(key_range.lower()));
}
if (key_range.upper().IsValid()) {
statement->BindBlob(param_index++,
EncodeSortableIDBKey(key_range.upper()));
}
// Store the variable parameter indexes and attempt to find the initial
// record in the range.
statement->BindBool(is_first_seek_index_ = param_index++, true);
statement->BindNull(position_index_ = param_index++);
if (!first_primary_keys_only_) {
object_store_position_index_ = param_index++;
statement->BindNull(object_store_position_index_.value());
}
statement->BindNull(target_key_index_ = param_index++);
if (!first_primary_keys_only_) {
target_primary_key_index_ = param_index++;
statement->BindNull(target_primary_key_index_.value());
}
statement->BindInt64(offset_index_ = param_index++, 0);
if (statement->Step()) {
return ReadRow(*statement);
}
if (statement->Succeeded()) {
// Empty range.
return nullptr;
}
return base::unexpected(db_->GetStatusOfLastOperation());
}
void SavePosition() override {
saved_position_ = {position_, object_store_position_};
}
bool TryResetToLastSavedPosition() override {
if (!saved_position_) {
return false;
}
std::tie(position_, object_store_position_) = *std::move(saved_position_);
saved_position_.reset();
return true;
}
protected:
// RecordIterator:
void BindParameters(sql::Statement& statement,
const blink::IndexedDBKey& target_key,
const blink::IndexedDBKey& target_primary_key,
uint32_t offset) override {
// `target_primary_key` is not expected when iterating over only the
// first primary keys.
CHECK(!first_primary_keys_only_ || !target_primary_key.IsValid());
statement.BindBool(is_first_seek_index_, false);
statement.BindBlob(position_index_, position_);
if (target_key.IsValid()) {
statement.BindBlob(target_key_index_, EncodeSortableIDBKey(target_key));
} else {
statement.BindNull(target_key_index_);
}
statement.BindInt64(offset_index_, offset);
if (!first_primary_keys_only_) {
statement.BindBlob(object_store_position_index_.value(),
object_store_position_);
if (target_primary_key.IsValid()) {
statement.BindBlob(target_primary_key_index_.value(),
EncodeSortableIDBKey(target_primary_key));
} else {
statement.BindNull(target_primary_key_index_.value());
}
}
}
StatusOr<std::unique_ptr<Record>> ReadRow(
sql::Statement& statement) override {
CHECK(statement.Succeeded());
statement.ColumnBlobAsString(0, &position_);
blink::IndexedDBKey key = DecodeSortableIDBKey(position_);
statement.ColumnBlobAsString(1, &object_store_position_);
blink::IndexedDBKey primary_key =
DecodeSortableIDBKey(object_store_position_);
if (key_only_) {
return std::make_unique<IndexKeyOnlyRecord>(std::move(key),
std::move(primary_key));
}
IndexedDBValue value;
statement.ColumnBlobAsVector(2, &value.bits);
int64_t record_row_id = statement.ColumnInt64(3);
return db_
->AddExternalObjectMetadataToValue(std::move(value), record_row_id)
.transform([&](IndexedDBValue value_with_metadata) {
return std::make_unique<IndexRecord>(std::move(key),
std::move(primary_key),
std::move(value_with_metadata));
});
}
sql::Statement* GetStatement() override {
if (!db_) {
return nullptr;
}
return db_->GetLongLivedStatement(statement_id_);
}
private:
base::WeakPtr<DatabaseConnection> db_;
uint64_t statement_id_ = 0;
bool key_only_ = false;
bool first_primary_keys_only_ = false;
int is_first_seek_index_ = 0;
int position_index_ = 0;
int target_key_index_ = 0;
int offset_index_ = 0;
// Set iff `first_primary_keys_only_` is false.
std::optional<int> object_store_position_index_;
std::optional<int> target_primary_key_index_;
// Encoded key from the current record.
std::string position_;
// Encoded primary key from the current record.
std::string object_store_position_;
std::optional<std::tuple<std::string, std::string>> saved_position_;
};
} // namespace
// static
StatusOr<std::unique_ptr<DatabaseConnection>> DatabaseConnection::Open(
std::optional<std::u16string_view> name,
base::FilePath path,
BackingStoreImpl& backing_store) {
constexpr sql::Database::Tag kSqlTag = "IndexedDB";
constexpr sql::Database::Tag kSqlTagInMemory = "IndexedDBEphemeral";
auto db =
std::make_unique<sql::Database>(sql::DatabaseOptions()
.set_exclusive_locking(true)
.set_wal_mode(true)
.set_enable_triggers(true),
path.empty() ? kSqlTagInMemory : kSqlTag);
if (path.empty()) {
TRANSIENT_CHECK(db->OpenInMemory());
} else {
TRANSIENT_CHECK(db->Open(path));
}
// What SQLite calls "recursive" triggers are required for SQLite to execute
// a DELETE ON trigger after `INSERT OR REPLACE` replaces a row.
TRANSIENT_CHECK(db->Execute("PRAGMA recursive_triggers=ON"));
auto meta_table = std::make_unique<sql::MetaTable>();
TRANSIENT_CHECK(meta_table->Init(db.get(), kEmptySchemaVersion,
kCompatibleSchemaVersion));
switch (meta_table->GetVersionNumber()) {
case kEmptySchemaVersion:
TRANSIENT_CHECK(name.has_value());
InitializeNewDatabase(db.get(), *name, meta_table.get());
break;
// ...
// Schema upgrades go here.
// ...
case kCurrentSchemaVersion:
// Already current.
break;
default:
NOTREACHED();
}
blink::IndexedDBDatabaseMetadata metadata =
GenerateIndexedDbMetadata(db.get());
// Database corruption can cause a mismatch.
if (name) {
TRANSIENT_CHECK(metadata.name == *name);
}
return base::WrapUnique(new DatabaseConnection(
std::move(path), std::move(db), std::move(meta_table),
std::move(metadata), backing_store));
}
// static
void DatabaseConnection::Release(base::WeakPtr<DatabaseConnection> db) {
if (!db) {
return;
}
// TODO(crbug.com/419203257): Consider delaying destruction by a short period
// in case the page reopens the same database soon.
DatabaseConnection* db_ptr = db.get();
db.reset();
if (db_ptr->CanBeDestroyed()) {
db_ptr->backing_store_->DestroyConnection(db_ptr->metadata_.name);
}
}
DatabaseConnection::DatabaseConnection(
base::FilePath path,
std::unique_ptr<sql::Database> db,
std::unique_ptr<sql::MetaTable> meta_table,
blink::IndexedDBDatabaseMetadata metadata,
BackingStoreImpl& backing_store)
: path_(path),
db_(std::move(db)),
meta_table_(std::move(meta_table)),
metadata_(std::move(metadata)),
backing_store_(backing_store) {
// There should be no active blobs in this database at this point, so we can
// remove blob references that were associated with active blobs. These may
// have been left behind if Chromium crashed. Deleting the blob references
// should also delete the blob if appropriate.
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"DELETE FROM blob_references WHERE record_row_id IS NULL"));
TRANSIENT_CHECK(statement.Run());
}
DatabaseConnection::~DatabaseConnection() {
if (path_.empty()) {
return;
}
// If in a zygotic state, `DeleteIdbDatabase()` has been called.
if (IsZygotic()) {
db_.reset();
sql::Database::Delete(path_);
} else if (db_ && !sql::IsSqliteSuccessCode(
sql::ToSqliteResultCode(db_->GetErrorCode()))) {
// Note that `DatabaseConnection` does not set an error callback on
// sql::Database. Instead, errors are returned for individual operations,
// which will trickle up through backing store agnostic code and close all
// `Transaction`s, `Connection`s and `Database`s. When the last
// `BackingStore::Database` is deleted, `this` will be deleted, at which
// point recovery will be attempted if appropriate.
#if BUILDFLAG(IS_FUCHSIA)
// Recovery is not supported with WAL mode DBs in Fuchsia.
if (db_->is_open() && sql::IsErrorCatastrophic(db_->GetErrorCode())) {
db_->RazeAndPoison();
}
#else
// `RecoverIfPossible` will no-op for several reasons including if the error is
// thought to be transient.
std::ignore = sql::Recovery::RecoverIfPossible(
db_.get(), db_->GetErrorCode(),
sql::Recovery::Strategy::kRecoverWithMetaVersionOrRaze);
#endif
}
}
base::WeakPtr<DatabaseConnection> DatabaseConnection::GetWeakPtr() {
return weak_factory_.GetWeakPtr();
}
bool DatabaseConnection::IsZygotic() const {
return metadata().version == blink::IndexedDBDatabaseMetadata::NO_VERSION;
}
int64_t DatabaseConnection::GetCommittedVersion() const {
return metadata_snapshot_ ? metadata_snapshot_->version : metadata_.version;
}
uint64_t DatabaseConnection::GetInMemorySize() const {
CHECK(path_.empty());
// TODO(crbug.com/419203257): For consistency, consider using this logic while
// reporting usage of on-disk databases too.
//
// The maximum page count is ~2^32: https://www.sqlite.org/limits.html.
uint32_t page_count = 0;
// The maximum page size is 65536 bytes.
uint16_t page_size = 0;
{
sql::Statement statement(db_->GetReadonlyStatement("PRAGMA page_count"));
TRANSIENT_CHECK(statement.Step());
page_count = static_cast<uint32_t>(statement.ColumnInt(0));
}
{
sql::Statement statement(db_->GetReadonlyStatement("PRAGMA page_size"));
TRANSIENT_CHECK(statement.Step());
page_size = static_cast<uint16_t>(statement.ColumnInt(0));
}
return static_cast<uint64_t>(page_count) * page_size;
}
std::unique_ptr<BackingStoreTransactionImpl>
DatabaseConnection::CreateTransaction(
base::PassKey<BackingStoreDatabaseImpl>,
blink::mojom::IDBTransactionDurability durability,
blink::mojom::IDBTransactionMode mode) {
return std::make_unique<BackingStoreTransactionImpl>(GetWeakPtr(), durability,
mode);
}
void DatabaseConnection::BeginTransaction(
base::PassKey<BackingStoreTransactionImpl>,
const BackingStoreTransactionImpl& transaction) {
// No other transaction can begin while a version change transaction is
// active.
CHECK(!HasActiveVersionChangeTransaction());
if (transaction.mode() == blink::mojom::IDBTransactionMode::ReadOnly) {
// Nothing to do.
return;
}
CHECK(!active_rw_transaction_);
active_rw_transaction_ = std::make_unique<sql::Transaction>(db_.get());
if (transaction.durability() ==
blink::mojom::IDBTransactionDurability::Strict) {
TRANSIENT_CHECK(db_->Execute("PRAGMA synchronous=FULL"));
} else {
// WAL mode is guaranteed to be consistent only with synchronous=NORMAL or
// higher: https://www.sqlite.org/pragma.html#pragma_synchronous.
TRANSIENT_CHECK(db_->Execute("PRAGMA synchronous=NORMAL"));
}
// TODO(crbug.com/40253999): How do we surface the error if this call fails?
TRANSIENT_CHECK(active_rw_transaction_->Begin());
if (transaction.mode() == blink::mojom::IDBTransactionMode::VersionChange) {
metadata_snapshot_.emplace(metadata_);
}
}
Status DatabaseConnection::CommitTransactionPhaseOne(
base::PassKey<BackingStoreTransactionImpl>,
const BackingStoreTransactionImpl& transaction,
BlobWriteCallback callback,
SerializeFsaCallback serialize_fsa_handle) {
if (transaction.mode() == blink::mojom::IDBTransactionMode::ReadOnly ||
blobs_to_write_.empty()) {
return std::move(callback).Run(
BlobWriteResult::kRunPhaseTwoAndReturnResult,
storage::mojom::WriteBlobToFileResult::kSuccess);
}
CHECK(blob_write_callback_.is_null());
CHECK(blob_writers_.empty());
CHECK_EQ(outstanding_external_object_writes_, 0U);
blob_write_callback_ = std::move(callback);
auto blobs_to_write = std::move(blobs_to_write_);
for (auto& [blob_row_id, external_object] : blobs_to_write) {
++outstanding_external_object_writes_;
if (external_object.object_type() ==
IndexedDBExternalObject::ObjectType::kFileSystemAccessHandle) {
serialize_fsa_handle.Run(
*external_object.file_system_access_token_remote(),
base::BindOnce(&DatabaseConnection::OnFsaHandleSerialized,
blob_writers_weak_factory_.GetWeakPtr(), blob_row_id));
continue;
}
std::optional<sql::StreamingBlobHandle> blob_for_writing =
db_->GetStreamingBlob("blobs", "bytes", blob_row_id,
/*readonly=*/false);
TRANSIENT_CHECK(blob_for_writing);
std::unique_ptr<BlobWriter> writer = BlobWriter::WriteBlobIntoDatabase(
external_object, *std::move(blob_for_writing),
base::BindOnce(&DatabaseConnection::OnBlobWriteComplete,
blob_writers_weak_factory_.GetWeakPtr(), blob_row_id));
if (!writer) {
CancelBlobWriting();
// This is currently ignored as the error is already surfaced through
// `blob_write_callback_`.
return Status::IOError();
}
blob_writers_[blob_row_id] = std::move(writer);
}
return Status::OK();
}
void DatabaseConnection::OnBlobWriteComplete(int64_t blob_row_id,
bool success) {
blob_writers_.erase(blob_row_id);
if (!success) {
CancelBlobWriting();
return;
}
if (--outstanding_external_object_writes_ == 0) {
std::move(blob_write_callback_)
.Run(BlobWriteResult::kRunPhaseTwoAsync,
storage::mojom::WriteBlobToFileResult::kSuccess);
}
}
void DatabaseConnection::OnFsaHandleSerialized(
int64_t blob_row_id,
const std::vector<uint8_t>& data) {
if (!data.empty()) {
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
"UPDATE blobs "
"SET bytes = ? "
"WHERE row_id = ?"));
statement.BindBlob(0, data);
statement.BindInt64(1, blob_row_id);
TRANSIENT_CHECK(statement.Run());
}
OnBlobWriteComplete(blob_row_id, /*success=*/!data.empty());
}
void DatabaseConnection::CancelBlobWriting() {
blob_writers_weak_factory_.InvalidateWeakPtrs();
blob_writers_.clear();
outstanding_external_object_writes_ = 0;
if (blob_write_callback_) {
std::move(blob_write_callback_)
.Run(BlobWriteResult::kRunPhaseTwoAsync,
storage::mojom::WriteBlobToFileResult::kError);
}
}
Status DatabaseConnection::CommitTransactionPhaseTwo(
base::PassKey<BackingStoreTransactionImpl>,
const BackingStoreTransactionImpl& transaction) {
if (transaction.mode() == blink::mojom::IDBTransactionMode::ReadOnly) {
// Nothing to do.
return Status::OK();
}
// No need to sync active blobs when the transaction successfully commits.
sync_active_blobs_after_transaction_ = false;
TRANSIENT_CHECK(active_rw_transaction_->Commit());
if (transaction.mode() == blink::mojom::IDBTransactionMode::VersionChange) {
CHECK(metadata_snapshot_.has_value());
metadata_snapshot_.reset();
}
return Status::OK();
}
void DatabaseConnection::RollBackTransaction(
base::PassKey<BackingStoreTransactionImpl>,
const BackingStoreTransactionImpl& transaction) {
if (transaction.mode() == blink::mojom::IDBTransactionMode::ReadOnly) {
// Nothing to do.
return;
}
// Abort ongoing blob writes, if any.
// TODO(crbug.com/419208485): Be sure to test this case.
blob_write_callback_.Reset();
CancelBlobWriting();
active_rw_transaction_->Rollback();
if (transaction.mode() == blink::mojom::IDBTransactionMode::VersionChange) {
CHECK(metadata_snapshot_.has_value());
metadata_ = *std::move(metadata_snapshot_);
metadata_snapshot_.reset();
}
}
void DatabaseConnection::EndTransaction(
base::PassKey<BackingStoreTransactionImpl>,
const BackingStoreTransactionImpl& transaction) {
if (transaction.mode() == blink::mojom::IDBTransactionMode::ReadOnly) {
return;
}
// The transaction may have been committed, rolled back, or neither. If
// neither, this will cause a rollback, although this should only occur if
// there were no statements executed anyway.
CHECK(active_rw_transaction_);
active_rw_transaction_.reset();
// If the transaction is rolled back, recent changes to the blob_references
// table may be lost. Make sure that table is up to date with memory state.
if (sync_active_blobs_after_transaction_) {
sql::Transaction sql_transaction(db_.get());
TRANSIENT_CHECK(sql_transaction.Begin());
// Step 1, mark existing active references with an invalid (but not null)
// row id. This can't immediately remove them as that could trigger cleanup
// of the underlying blob.
{
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE,
"UPDATE blob_references SET record_row_id = 0"
" WHERE record_row_id IS NULL"));
TRANSIENT_CHECK(statement.Run());
}
// Step 2, make add all the active references.
for (auto& [blob_number, _] : active_blobs_) {
AddActiveBlobReference(blob_number);
}
// Step 3, remove the old references.
{
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"DELETE FROM blob_references WHERE record_row_id = 0"));
TRANSIENT_CHECK(statement.Run());
}
TRANSIENT_CHECK(sql_transaction.Commit());
sync_active_blobs_after_transaction_ = false;
}
}
Status DatabaseConnection::SetDatabaseVersion(
base::PassKey<BackingStoreTransactionImpl>,
int64_t version) {
CHECK(HasActiveVersionChangeTransaction());
sql::Statement statement(
db_->GetUniqueStatement("UPDATE indexed_db_metadata SET version = ?"));
statement.BindInt64(0, version);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
metadata_.version = version;
return Status::OK();
}
Status DatabaseConnection::CreateObjectStore(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
std::u16string name,
blink::IndexedDBKeyPath key_path,
bool auto_increment) {
CHECK(HasActiveVersionChangeTransaction());
if (metadata_.object_stores.contains(object_store_id) ||
object_store_id <= metadata_.max_object_store_id) {
return Status::InvalidArgument("Invalid object_store_id");
}
blink::IndexedDBObjectStoreMetadata metadata(
std::move(name), object_store_id, std::move(key_path), auto_increment);
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"INSERT INTO object_stores "
"(id, name, key_path, auto_increment, key_generator_current_number) "
"VALUES (?, ?, ?, ?, ?)"));
statement.BindInt64(0, metadata.id);
statement.BindBlob(1, metadata.name);
BindKeyPath(statement, 2, metadata.key_path);
statement.BindBool(3, metadata.auto_increment);
statement.BindInt64(4, ObjectStoreMetaDataKey::kKeyGeneratorInitialNumber);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
metadata_.object_stores[object_store_id] = std::move(metadata);
metadata_.max_object_store_id = object_store_id;
return Status::OK();
}
Status DatabaseConnection::DeleteObjectStore(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id) {
CHECK(HasActiveVersionChangeTransaction());
if (!metadata_.object_stores.contains(object_store_id)) {
return Status::InvalidArgument("Invalid object_store_id.");
}
{
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"DELETE FROM index_references WHERE object_store_id = ?"));
statement.BindInt64(0, object_store_id);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
}
{
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE, "DELETE FROM indexes WHERE object_store_id = ?"));
statement.BindInt64(0, object_store_id);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
}
{
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE, "DELETE FROM records WHERE object_store_id = ?"));
statement.BindInt64(0, object_store_id);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
}
{
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE, "DELETE FROM object_stores WHERE id = ?"));
statement.BindInt64(0, object_store_id);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
}
CHECK(metadata_.object_stores.erase(object_store_id) == 1);
return Status::OK();
}
Status DatabaseConnection::RenameObjectStore(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
const std::u16string& new_name) {
CHECK(HasActiveVersionChangeTransaction());
if (!metadata_.object_stores.contains(object_store_id)) {
return Status::InvalidArgument("Invalid object_store_id.");
}
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE, "UPDATE object_stores SET name = ? WHERE id = ?"));
statement.BindBlob(0, new_name);
statement.BindInt64(1, object_store_id);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
metadata_.object_stores.at(object_store_id).name = new_name;
return Status::OK();
}
Status DatabaseConnection::CreateIndex(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
blink::IndexedDBIndexMetadata index) {
CHECK(HasActiveVersionChangeTransaction());
if (!metadata_.object_stores.contains(object_store_id)) {
return Status::InvalidArgument("Invalid object_store_id.");
}
blink::IndexedDBObjectStoreMetadata& object_store =
metadata_.object_stores.at(object_store_id);
int64_t index_id = index.id;
if (object_store.indexes.contains(index_id) ||
index_id <= object_store.max_index_id) {
return Status::InvalidArgument("Invalid index_id.");
}
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"INSERT INTO indexes "
"(object_store_id, id, name, key_path, is_unique, multi_entry) "
"VALUES (?, ?, ?, ?, ?, ?)"));
statement.BindInt64(0, object_store_id);
statement.BindInt64(1, index_id);
statement.BindBlob(2, index.name);
BindKeyPath(statement, 3, index.key_path);
statement.BindBool(4, index.unique);
statement.BindBool(5, index.multi_entry);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
object_store.indexes[index_id] = std::move(index);
object_store.max_index_id = index_id;
return Status::OK();
}
Status DatabaseConnection::DeleteIndex(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
int64_t index_id) {
CHECK(HasActiveVersionChangeTransaction());
if (!metadata_.object_stores.contains(object_store_id)) {
return Status::InvalidArgument("Invalid object_store_id.");
}
if (!metadata_.object_stores.at(object_store_id).indexes.contains(index_id)) {
return Status::InvalidArgument("Invalid index_id.");
}
{
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE,
"DELETE FROM index_references "
"WHERE object_store_id = ? AND index_id = ?"));
statement.BindInt64(0, object_store_id);
statement.BindInt64(1, index_id);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
}
{
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"DELETE FROM indexes WHERE object_store_id = ? AND id = ?"));
statement.BindInt64(0, object_store_id);
statement.BindInt64(1, index_id);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
}
CHECK(metadata_.object_stores.at(object_store_id).indexes.erase(index_id) ==
1);
return Status::OK();
}
Status DatabaseConnection::RenameIndex(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
int64_t index_id,
const std::u16string& new_name) {
CHECK(HasActiveVersionChangeTransaction());
if (!metadata_.object_stores.contains(object_store_id)) {
return Status::InvalidArgument("Invalid object_store_id.");
}
if (!metadata_.object_stores.at(object_store_id).indexes.contains(index_id)) {
return Status::InvalidArgument("Invalid index_id.");
}
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"UPDATE indexes SET name = ? WHERE object_store_id = ? AND id = ?"));
statement.BindBlob(0, new_name);
statement.BindInt64(1, object_store_id);
statement.BindInt64(2, index_id);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
metadata_.object_stores.at(object_store_id).indexes.at(index_id).name =
new_name;
return Status::OK();
}
StatusOr<int64_t> DatabaseConnection::GetKeyGeneratorCurrentNumber(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id) {
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE,
"SELECT key_generator_current_number "
"FROM object_stores WHERE id = ?"));
statement.BindInt64(0, object_store_id);
statement.Step();
RETURN_IF_STATEMENT_ERRORED(statement);
return statement.ColumnInt64(0);
}
Status DatabaseConnection::MaybeUpdateKeyGeneratorCurrentNumber(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
int64_t new_number) {
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"UPDATE object_stores SET key_generator_current_number = ? "
"WHERE id = ? AND key_generator_current_number < ?"));
statement.BindInt64(0, new_number);
statement.BindInt64(1, object_store_id);
statement.BindInt64(2, new_number);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
return Status::OK();
}
StatusOr<std::optional<BackingStore::RecordIdentifier>>
DatabaseConnection::GetRecordIdentifierIfExists(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
const blink::IndexedDBKey& key) {
std::string encoded_key = EncodeSortableIDBKey(key);
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE,
"SELECT row_id FROM records "
"WHERE object_store_id = ? AND key = ?"));
statement.BindInt64(0, object_store_id);
statement.BindBlob(1, encoded_key);
if (statement.Step()) {
return BackingStore::RecordIdentifier{statement.ColumnInt64(0),
std::move(encoded_key)};
}
RETURN_IF_STATEMENT_ERRORED(statement);
return std::nullopt;
}
StatusOr<IndexedDBValue> DatabaseConnection::GetValue(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
const blink::IndexedDBKey& key) {
IndexedDBValue value;
int64_t record_row_id;
{
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE,
"SELECT row_id, value FROM records "
"WHERE object_store_id = ? AND key = ?"));
statement.BindInt64(0, object_store_id);
statement.BindBlob(1, EncodeSortableIDBKey(key));
if (!statement.Step()) {
RETURN_IF_STATEMENT_ERRORED(statement);
return IndexedDBValue();
}
record_row_id = statement.ColumnInt64(0);
statement.ColumnBlobAsVector(1, &value.bits);
}
return AddExternalObjectMetadataToValue(std::move(value), record_row_id);
}
StatusOr<IndexedDBValue> DatabaseConnection::AddExternalObjectMetadataToValue(
IndexedDBValue value,
int64_t record_row_id) {
// First add Blob and File objects' metadata (not FSA handles).
{
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"SELECT "
" blobs.row_id, object_type, mime_type, size_bytes, file_name, "
" last_modified "
"FROM blobs INNER JOIN blob_references"
" ON blob_references.blob_row_id = blobs.row_id "
"WHERE"
" blob_references.record_row_id = ? AND object_type != ? "
// The order is important because the serialized data uses indexes to
// refer to embedded external objects.
"ORDER BY blobs.row_id"));
statement.BindInt64(0, record_row_id);
statement.BindInt64(
1, static_cast<int>(
IndexedDBExternalObject::ObjectType::kFileSystemAccessHandle));
while (statement.Step()) {
const int64_t blob_row_id = statement.ColumnInt64(0);
if (auto it = blobs_to_write_.find(blob_row_id);
it != blobs_to_write_.end()) {
// If the blob is being written in this transaction, copy the external
// object (and later the Blob mojo endpoint) from `blobs_to_write_`.
value.external_objects.emplace_back(it->second);
} else {
auto object_type = static_cast<IndexedDBExternalObject::ObjectType>(
statement.ColumnInt(1));
if (object_type == IndexedDBExternalObject::ObjectType::kBlob) {
// Otherwise, create a new `IndexedDBExternalObject` from the
// database.
value.external_objects.emplace_back(
/*type=*/statement.ColumnString16(2),
/*size=*/statement.ColumnInt64(3), blob_row_id);
} else if (object_type == IndexedDBExternalObject::ObjectType::kFile) {
value.external_objects.emplace_back(
blob_row_id, /*type=*/statement.ColumnString16(2),
/*file_name=*/statement.ColumnString16(4),
/*last_modified=*/statement.ColumnTime(5),
/*size=*/statement.ColumnInt64(3));
} else {
NOTREACHED();
}
}
}
RETURN_IF_STATEMENT_ERRORED(statement);
}
// Then add FileSystemAccessHandle objects' metadata.
{
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"SELECT "
" blobs.row_id, bytes "
"FROM blobs INNER JOIN blob_references"
" ON blob_references.blob_row_id = blobs.row_id "
"WHERE"
" blob_references.record_row_id = ? AND object_type = ? "
"ORDER BY blobs.row_id"));
statement.BindInt64(0, record_row_id);
statement.BindInt64(
1, static_cast<int>(
IndexedDBExternalObject::ObjectType::kFileSystemAccessHandle));
while (statement.Step()) {
const int64_t blob_row_id = statement.ColumnInt64(0);
if (auto it = blobs_to_write_.find(blob_row_id);
it != blobs_to_write_.end()) {
mojo::PendingRemote<blink::mojom::FileSystemAccessTransferToken>
token_clone;
it->second.file_system_access_token_remote()->Clone(
token_clone.InitWithNewPipeAndPassReceiver());
value.external_objects.emplace_back(std::move(token_clone));
} else {
base::span<const uint8_t> serialized_handle = statement.ColumnBlob(1);
value.external_objects.emplace_back(std::vector<uint8_t>(
serialized_handle.begin(), serialized_handle.end()));
}
}
RETURN_IF_STATEMENT_ERRORED(statement);
}
return value;
}
StatusOr<BackingStore::RecordIdentifier> DatabaseConnection::PutRecord(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
const blink::IndexedDBKey& key,
IndexedDBValue value) {
// Insert record, including inline data.
const std::string encoded_key = EncodeSortableIDBKey(key);
{
// "INSERT OR REPLACE" deletes the row corresponding to
// [object_store_id, key] if it exists and inserts a new row with `value`.
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"INSERT OR REPLACE INTO records "
"(object_store_id, key, value) VALUES (?, ?, ?)"));
statement.BindInt64(0, object_store_id);
statement.BindBlob(1, encoded_key);
statement.BindBlob(2, std::move(value.bits));
RUN_STATEMENT_RETURN_ON_ERROR(statement);
}
const int64_t record_row_id = db_->GetLastInsertRowId();
// Insert external objects into relevant tables.
for (auto& external_object : value.external_objects) {
// Reserve space in the blob table. It's not actually written yet though.
if (external_object.object_type() ==
IndexedDBExternalObject::ObjectType::kFileSystemAccessHandle) {
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE,
"INSERT INTO blobs "
"(object_type) "
"VALUES (?)"));
statement.BindInt(0, static_cast<int>(external_object.object_type()));
RUN_STATEMENT_RETURN_ON_ERROR(statement);
} else {
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE,
"INSERT INTO blobs "
"(object_type, mime_type, size_bytes, "
"bytes, file_name, last_modified) "
"VALUES (?, ?, ?, ?, ?, ?)"));
statement.BindInt(0, static_cast<int>(external_object.object_type()));
statement.BindString16(1, external_object.type());
statement.BindInt64(2, external_object.size());
statement.BindBlobForStreaming(3, external_object.size());
if (external_object.object_type() ==
IndexedDBExternalObject::ObjectType::kBlob) {
statement.BindNull(4);
statement.BindNull(5);
} else {
CHECK_EQ(external_object.object_type(),
IndexedDBExternalObject::ObjectType::kFile);
statement.BindString16(4, external_object.file_name());
statement.BindTime(5, external_object.last_modified());
}
RUN_STATEMENT_RETURN_ON_ERROR(statement);
}
const int64_t blob_row_id = db_->GetLastInsertRowId();
external_object.set_blob_number(blob_row_id);
// Store the reference.
{
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE,
"INSERT INTO blob_references "
"(blob_row_id, record_row_id) "
"VALUES (?, ?)"));
statement.BindInt64(0, blob_row_id);
statement.BindInt64(1, record_row_id);
RUN_STATEMENT_RETURN_ON_ERROR(statement);
}
// TODO(crbug.com/419208485): Consider writing the blobs eagerly (but still
// asynchronously) so that transaction commit is expedited.
auto rv = blobs_to_write_.emplace(blob_row_id,
// TODO(crbug.com/419208485): this type is
// copy only at the moment.
std::move(external_object));
CHECK(rv.second);
}
return BackingStore::RecordIdentifier{record_row_id, std::move(encoded_key)};
}
Status DatabaseConnection::DeleteRange(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
const blink::IndexedDBKeyRange& key_range) {
std::vector<std::string_view> query_pieces =
StartRecordRangeQuery("DELETE", key_range);
sql::Statement statement(db_->GetUniqueStatement(base::StrCat(query_pieces)));
BindRecordRangeQueryParams(statement, object_store_id, key_range);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
return Status::OK();
}
Status DatabaseConnection::ClearObjectStore(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id) {
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE, "DELETE FROM records WHERE object_store_id = ?"));
statement.BindInt64(0, object_store_id);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
return Status::OK();
}
StatusOr<uint32_t> DatabaseConnection::GetObjectStoreKeyCount(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
blink::IndexedDBKeyRange key_range) {
std::vector<std::string_view> query_pieces =
StartRecordRangeQuery("SELECT COUNT()", key_range);
// TODO(crbug.com/40253999): Evaluate performance benefit of using
// `GetCachedStatement()` instead.
sql::Statement statement(
db_->GetReadonlyStatement(base::StrCat(query_pieces)));
BindRecordRangeQueryParams(statement, object_store_id, key_range);
if (!statement.Step()) {
RETURN_IF_STATEMENT_ERRORED(statement);
// COUNT() can't fail to return a value.
NOTREACHED();
}
return statement.ColumnInt(0);
}
Status DatabaseConnection::PutIndexDataForRecord(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
int64_t index_id,
const blink::IndexedDBKey& key,
const BackingStore::RecordIdentifier& record) {
// `PutIndexDataForRecord()` can be called more than once with the same `key`
// and `record` - in the case of multi-entry indexes, for example.
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"INSERT OR IGNORE INTO index_references "
"(record_row_id, index_id, key, object_store_id, record_key) "
"VALUES (?, ?, ?, ?, ?)"));
statement.BindInt64(0, record.number);
statement.BindInt64(1, index_id);
statement.BindBlob(2, EncodeSortableIDBKey(key));
statement.BindInt64(3, object_store_id);
statement.BindBlob(4, record.data);
RUN_STATEMENT_RETURN_STATUS_ON_ERROR(statement);
return Status::OK();
}
StatusOr<blink::IndexedDBKey> DatabaseConnection::GetFirstPrimaryKeyForIndexKey(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
int64_t index_id,
const blink::IndexedDBKey& key) {
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE,
"SELECT MIN(record_key) FROM index_references "
"WHERE object_store_id = ? AND index_id = ? AND key = ?"));
statement.BindInt64(0, object_store_id);
statement.BindInt64(1, index_id);
statement.BindBlob(2, EncodeSortableIDBKey(key));
if (statement.Step()) {
std::string primary_key;
statement.ColumnBlobAsString(0, &primary_key);
return DecodeSortableIDBKey(primary_key);
}
RETURN_IF_STATEMENT_ERRORED(statement);
// Not found.
return blink::IndexedDBKey();
}
StatusOr<uint32_t> DatabaseConnection::GetIndexKeyCount(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
int64_t index_id,
blink::IndexedDBKeyRange key_range) {
std::vector<std::string_view> query_pieces{
"SELECT COUNT() FROM index_references WHERE object_store_id = ?"
" AND index_id = ?"};
if (key_range.lower().IsValid()) {
query_pieces.push_back(key_range.lower_open() ? " AND key > ?"
: " AND key >= ?");
}
if (key_range.upper().IsValid()) {
query_pieces.push_back(key_range.upper_open() ? " AND key < ?"
: " AND key <= ?");
}
sql::Statement statement(
db_->GetReadonlyStatement(base::StrCat(query_pieces)));
int param_index = 0;
statement.BindInt64(param_index++, object_store_id);
statement.BindInt64(param_index++, index_id);
if (key_range.lower().IsValid()) {
statement.BindBlob(param_index++, EncodeSortableIDBKey(key_range.lower()));
}
if (key_range.upper().IsValid()) {
statement.BindBlob(param_index++, EncodeSortableIDBKey(key_range.upper()));
}
if (!statement.Step()) {
RETURN_IF_STATEMENT_ERRORED(statement);
// COUNT() can't fail to return a value.
NOTREACHED();
}
return statement.ColumnInt(0);
}
std::vector<blink::mojom::IDBExternalObjectPtr>
DatabaseConnection::CreateAllExternalObjects(
base::PassKey<BackingStoreTransactionImpl>,
const std::vector<IndexedDBExternalObject>& objects,
DeserializeFsaCallback deserialize_fsa_handle) {
std::vector<blink::mojom::IDBExternalObjectPtr> mojo_objects;
IndexedDBExternalObject::ConvertToMojo(objects, &mojo_objects);
for (size_t i = 0; i < objects.size(); ++i) {
const IndexedDBExternalObject& object = objects[i];
blink::mojom::IDBExternalObjectPtr& mojo_object = mojo_objects[i];
if (object.object_type() ==
IndexedDBExternalObject::ObjectType::kFileSystemAccessHandle) {
mojo::PendingRemote<blink::mojom::FileSystemAccessTransferToken>
mojo_token;
if (object.is_file_system_access_remote_valid()) {
// The remote will be valid if this is a pending FSA handle i.e. came
// from `blobs_to_write_`.
object.file_system_access_token_remote()->Clone(
mojo_token.InitWithNewPipeAndPassReceiver());
} else {
CHECK(!object.serialized_file_system_access_handle().empty());
deserialize_fsa_handle.Run(
object.serialized_file_system_access_handle(),
mojo_token.InitWithNewPipeAndPassReceiver());
}
mojo_object->set_file_system_access_token(std::move(mojo_token));
continue;
}
mojo::PendingReceiver<blink::mojom::Blob> receiver =
mojo_object->get_blob_or_file()->blob.InitWithNewPipeAndPassReceiver();
// The remote will be valid if this is a pending blob i.e. came from
// `blobs_to_write_`.
if (object.is_remote_valid()) {
object.Clone(std::move(receiver));
continue;
}
// Otherwise the blob is in the database already. Look up or create the
// object that manages the active blob.
auto it = active_blobs_.find(object.blob_number());
if (it == active_blobs_.end()) {
std::optional<sql::StreamingBlobHandle> blob_for_reading =
db_->GetStreamingBlob("blobs", "bytes", object.blob_number(),
/*readonly=*/true);
TRANSIENT_CHECK(blob_for_reading);
auto streamer = std::make_unique<ActiveBlobStreamer>(
object, *std::move(blob_for_reading),
base::BindOnce(&DatabaseConnection::OnBlobBecameInactive,
base::Unretained(this), object.blob_number()));
it = active_blobs_.insert({object.blob_number(), std::move(streamer)})
.first;
AddActiveBlobReference(object.blob_number());
}
it->second->AddReceiver(std::move(receiver),
backing_store_->blob_storage_context());
}
return mojo_objects;
}
void DatabaseConnection::DeleteIdbDatabase(
base::PassKey<BackingStoreDatabaseImpl>) {
metadata_ = blink::IndexedDBDatabaseMetadata(metadata_.name);
weak_factory_.InvalidateWeakPtrs();
CHECK(!blob_writers_weak_factory_.HasWeakPtrs());
if (CanBeDestroyed()) {
// Fast path: skip explicitly deleting data as the whole database will be
// dropped.
backing_store_->DestroyConnection(metadata_.name);
// `this` is deleted.
return;
}
record_iterator_weak_factory_.InvalidateWeakPtrs();
statements_.clear();
// Since blobs are still active, reset to zygotic state instead of destroying.
TRANSIENT_CHECK(db_->Execute(
"DELETE FROM blob_references WHERE record_row_id IS NOT NULL"));
TRANSIENT_CHECK(db_->Execute("DELETE FROM index_references"));
TRANSIENT_CHECK(db_->Execute("DELETE FROM indexes"));
TRANSIENT_CHECK(db_->Execute("DELETE FROM records"));
TRANSIENT_CHECK(db_->Execute("DELETE FROM object_stores"));
{
sql::Statement statement(
db_->GetUniqueStatement("UPDATE indexed_db_metadata SET version = ?"));
statement.BindInt64(0, blink::IndexedDBDatabaseMetadata::NO_VERSION);
TRANSIENT_CHECK(statement.Run());
}
}
void DatabaseConnection::OnBlobBecameInactive(int64_t blob_number) {
CHECK_EQ(active_blobs_.erase(blob_number), 1U);
RemoveActiveBlobReference(blob_number);
}
void DatabaseConnection::AddActiveBlobReference(int64_t blob_number) {
if (active_rw_transaction_) {
sync_active_blobs_after_transaction_ = true;
}
sql::Statement statement(db_->GetCachedStatement(
SQL_FROM_HERE, "INSERT INTO blob_references (blob_row_id) VALUES (?)"));
statement.BindInt64(0, blob_number);
TRANSIENT_CHECK(statement.Run());
}
void DatabaseConnection::RemoveActiveBlobReference(int64_t blob_number) {
if (active_rw_transaction_) {
sync_active_blobs_after_transaction_ = true;
}
{
sql::Statement statement(
db_->GetCachedStatement(SQL_FROM_HERE,
"DELETE FROM blob_references "
"WHERE blob_row_id = ? "
"AND record_row_id IS NULL"));
statement.BindInt64(0, blob_number);
TRANSIENT_CHECK(statement.Run());
}
if (CanBeDestroyed()) {
backing_store_->DestroyConnection(metadata_.name);
// `this` is deleted.
return;
}
}
bool DatabaseConnection::CanBeDestroyed() const {
return active_blobs_.empty() && !weak_factory_.HasWeakPtrs();
}
StatusOr<std::unique_ptr<BackingStore::Cursor>>
DatabaseConnection::OpenObjectStoreCursor(
base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
const blink::IndexedDBKeyRange& key_range,
blink::mojom::IDBCursorDirection direction,
bool key_only) {
bool ascending_order =
(direction == blink::mojom::IDBCursorDirection::Next ||
direction == blink::mojom::IDBCursorDirection::NextNoDuplicate);
auto record_iterator = std::make_unique<ObjectStoreRecordIterator>(
record_iterator_weak_factory_.GetWeakPtr(), key_only);
return record_iterator
->Initialize(object_store_id, key_range, ascending_order)
.transform([&](std::unique_ptr<Record> first_record)
-> std::unique_ptr<BackingStore::Cursor> {
if (!first_record) {
return nullptr;
}
return std::make_unique<BackingStoreCursorImpl>(
std::move(record_iterator), std::move(first_record));
});
}
StatusOr<std::unique_ptr<BackingStore::Cursor>>
DatabaseConnection::OpenIndexCursor(base::PassKey<BackingStoreTransactionImpl>,
int64_t object_store_id,
int64_t index_id,
const blink::IndexedDBKeyRange& key_range,
blink::mojom::IDBCursorDirection direction,
bool key_only) {
bool ascending_order =
(direction == blink::mojom::IDBCursorDirection::Next ||
direction == blink::mojom::IDBCursorDirection::NextNoDuplicate);
// NoDuplicate => iterate over the first primary keys only.
bool first_primary_keys_only =
(direction == blink::mojom::IDBCursorDirection::NextNoDuplicate ||
direction == blink::mojom::IDBCursorDirection::PrevNoDuplicate);
auto record_iterator = std::make_unique<IndexRecordIterator>(
record_iterator_weak_factory_.GetWeakPtr(), key_only,
first_primary_keys_only);
return record_iterator
->Initialize(object_store_id, index_id, key_range, ascending_order)
.transform([&](std::unique_ptr<Record> first_record)
-> std::unique_ptr<BackingStore::Cursor> {
if (!first_record) {
return nullptr;
}
return std::make_unique<BackingStoreCursorImpl>(
std::move(record_iterator), std::move(first_record));
});
}
std::tuple<uint64_t, sql::Statement*>
DatabaseConnection::CreateLongLivedStatement(std::string query) {
auto it = statements_.emplace(
++next_statement_id_,
std::make_unique<sql::Statement>(db_->GetUniqueStatement(query)));
CHECK(it.second);
return {next_statement_id_, it.first->second.get()};
}
void DatabaseConnection::ReleaseLongLivedStatement(uint64_t id) {
CHECK_EQ(1U, statements_.erase(id));
}
sql::Statement* DatabaseConnection::GetLongLivedStatement(uint64_t id) {
auto it = statements_.find(id);
if (it == statements_.end()) {
return nullptr;
}
return it->second.get();
}
Status DatabaseConnection::GetStatusOfLastOperation() {
return Status(*db_);
}
} // namespace content::indexed_db::sqlite