| // 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 |