// Copyright 2013 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/transaction.h"

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/not_fatal_until.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/trace_event/trace_event.h"
#include "base/types/expected_macros.h"
#include "base/unguessable_token.h"
#include "components/services/storage/indexed_db/locks/partitioned_lock_manager.h"
#include "components/services/storage/privileged/mojom/indexed_db_client_state_checker.mojom-shared.h"
#include "components/services/storage/privileged/mojom/indexed_db_internals_types.mojom.h"
#include "components/services/storage/public/mojom/blob_storage_context.mojom-shared.h"
#include "content/browser/indexed_db/indexed_db_external_object.h"
#include "content/browser/indexed_db/indexed_db_external_object_storage.h"
#include "content/browser/indexed_db/indexed_db_reporting.h"
#include "content/browser/indexed_db/instance/bucket_context.h"
#include "content/browser/indexed_db/instance/callback_helpers.h"
#include "content/browser/indexed_db/instance/connection.h"
#include "content/browser/indexed_db/instance/cursor.h"
#include "content/browser/indexed_db/instance/database.h"
#include "content/browser/indexed_db/instance/database_callbacks.h"
#include "content/browser/indexed_db/instance/index_writer.h"
#include "content/browser/indexed_db/instance/lock_request_data.h"
#include "content/browser/indexed_db/status.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "third_party/blink/public/common/indexeddb/indexeddb_metadata.h"
#include "third_party/blink/public/mojom/indexeddb/indexeddb.mojom.h"
#include "third_party/perfetto/include/perfetto/tracing/track.h"

using blink::IndexedDBIndexKeys;
using blink::IndexedDBIndexMetadata;
using blink::IndexedDBKey;
using blink::IndexedDBKeyPath;
using blink::IndexedDBObjectStoreMetadata;

namespace content::indexed_db {

namespace {

// Disabled in some tests.
bool g_inactivity_timeout_enabled = true;

// Used for UMA metrics - do not change values.
enum UmaIDBException {
  UmaIDBExceptionUnknownError = 0,
  UmaIDBExceptionConstraintError = 1,
  UmaIDBExceptionDataError = 2,
  UmaIDBExceptionVersionError = 3,
  UmaIDBExceptionAbortError = 4,
  UmaIDBExceptionQuotaError = 5,
  UmaIDBExceptionTimeoutError = 6,
  UmaIDBExceptionExclusiveMaxValue = 7
};

// Used for UMA metrics - do not change mappings.
UmaIDBException ExceptionCodeToUmaEnum(blink::mojom::IDBException code) {
  switch (code) {
    case blink::mojom::IDBException::kUnknownError:
      return UmaIDBExceptionUnknownError;
    case blink::mojom::IDBException::kConstraintError:
      return UmaIDBExceptionConstraintError;
    case blink::mojom::IDBException::kDataError:
      return UmaIDBExceptionDataError;
    case blink::mojom::IDBException::kVersionError:
      return UmaIDBExceptionVersionError;
    case blink::mojom::IDBException::kAbortError:
      return UmaIDBExceptionAbortError;
    case blink::mojom::IDBException::kQuotaError:
      return UmaIDBExceptionQuotaError;
    case blink::mojom::IDBException::kTimeoutError:
      return UmaIDBExceptionTimeoutError;
    default:
      NOTREACHED();
  }
}

}  // namespace

Transaction::Task::Task(std::string operation_name_for_metrics,
                        Operation operation,
                        VerificationCallback verify)
    : operation_name_for_metrics(std::move(operation_name_for_metrics)),
      operation(std::move(operation)),
      verify(std::move(verify)) {}

Transaction::Task::Task(Task&&) = default;
Transaction::Task& Transaction::Task::operator=(Task&&) = default;
Transaction::Task::~Task() = default;

Transaction::Transaction(
    int64_t id,
    Connection* connection,
    const std::set<int64_t>& object_store_ids,
    blink::mojom::IDBTransactionMode mode,
    blink::mojom::IDBTransactionDurability durability,
    BucketContext& bucket_context,
    std::unique_ptr<BackingStore::Transaction> backing_store_transaction)
    : id_(id),
      object_store_ids_(object_store_ids),
      mode_(mode),
      durability_(durability),
      connection_(*connection),
      bucket_context_(&bucket_context),
      backing_store_transaction_(std::move(backing_store_transaction)),
      receiver_(this) {
  TRACE_EVENT_BEGIN("IndexedDB", "Transaction::lifetime",
                    perfetto::Track::FromPointer(this));

  locks_receiver_.SetUserData(
      LockRequestData::kKey,
      std::make_unique<LockRequestData>(connection->client_token(),
                                        connection->scheduling_priority()));

  database_ = connection_->database();
  if (database_) {
    for (const PartitionedLockManager::PartitionedLockRequest& lock_request :
         database_->BuildLockRequestsForTransaction(mode_, scope())) {
      lock_ids_.insert(lock_request.lock_id);
    }
  }

  diagnostics_.tasks_scheduled = 0;
  diagnostics_.tasks_completed = 0;
  diagnostics_.creation_time = base::Time::Now();
  SetState(state_);  // Process the initial state.
}

Transaction::~Transaction() {
  // Corresponds to the TRACE_EVENT_BEGIN in the constructor.
  TRACE_EVENT_END("IndexedDB", perfetto::Track::FromPointer(this));
  // It shouldn't be possible for this object to get deleted until it's either
  // complete or aborted.
  CHECK_EQ(state_, FINISHED);
  CHECK(preemptive_task_queue_.empty());
  CHECK_EQ(pending_preemptive_events_, 0);
  CHECK(task_queue_.empty());
}

void Transaction::BindReceiver(
    mojo::PendingAssociatedReceiver<blink::mojom::IDBTransaction>
        mojo_receiver) {
  receiver_.Bind(std::move(mojo_receiver));
}

void Transaction::SetCommitFlag() {
  // The frontend suggests that we commit, but we may have previously initiated
  // an abort.
  if (!IsAcceptingRequests()) {
    return;
  }

  is_commit_pending_ = true;
  bucket_context_->QueueRunTasks();
}

void Transaction::ScheduleTask(blink::mojom::IDBTaskType type,
                               std::string operation_name_for_metrics,
                               Operation operation,
                               VerificationCallback verify) {
  TRACE_EVENT0("IndexedDB", "Transaction::ScheduleTask");

  if (state_ == FINISHED) {
    TRACE_EVENT_INSTANT("IndexedDB", "Transaction::ScheduleTask - Finished");
    return;
  }

  timeout_timer_.Stop();
  used_ = true;
  if (type == blink::mojom::IDBTaskType::Normal) {
    task_queue_.emplace(std::move(operation_name_for_metrics),
                        std::move(operation), std::move(verify));
    ++diagnostics_.tasks_scheduled;
    NotifyOfIdbInternalsRelevantChange();
  } else {
    preemptive_task_queue_.emplace(std::move(operation_name_for_metrics),
                                   std::move(operation), std::move(verify));
  }
  if (state() == STARTED) {
    bucket_context_->QueueRunTasks();
  } else {
    TRACE_EVENT_INSTANT("IndexedDB", "Transaction::ScheduleTask - Not started");
  }
}

void Transaction::Abort(const DatabaseError& error) {
  if (state_ == FINISHED) {
    bucket_context_ = nullptr;
    return;
  }

  base::UmaHistogramEnumeration("WebCore.IndexedDB.TransactionAbortReason",
                                ExceptionCodeToUmaEnum(error.code()),
                                UmaIDBExceptionExclusiveMaxValue);

  aborted_ = true;
  timeout_timer_.Stop();

  SetState(FINISHED);

  if (backing_store_transaction_begun_) {
    backing_store_transaction_->Rollback();
  }

  preemptive_task_queue_ = {};
  pending_preemptive_events_ = 0;
  task_queue_ = {};
  backing_store_transaction_.reset();
  locks_receiver_.locks.clear();
  locks_receiver_.CancelLockRequest();

  connection_->callbacks()->OnAbort(*this, error);

  bucket_context_->QueueRunTasks();
  bucket_context_ = nullptr;
}

bool Transaction::IsTaskQueueEmpty() const {
  return preemptive_task_queue_.empty() && task_queue_.empty();
}

bool Transaction::HasPendingTasks() const {
  return pending_preemptive_events_ || !IsTaskQueueEmpty();
}

void Transaction::RegisterOpenCursor(Cursor* cursor) {
  open_cursors_.insert(cursor);
}

void Transaction::UnregisterOpenCursor(Cursor* cursor) {
  open_cursors_.erase(cursor);
}

void Transaction::DontAllowInactiveClientToBlockOthers(
    storage::mojom::DisallowInactiveClientReason reason) {
  if (state_ == STARTED && IsTransactionBlockingOtherClients()) {
    connection_->DisallowInactiveClient(reason, base::DoNothing());
  }
}

bool Transaction::IsTransactionBlockingOtherClients(
    bool consider_priority) const {
  CHECK_EQ(state_, STARTED);

  if (database_->OnlyHasOneClient()) {
    return false;
  }

  std::optional<int> this_priority;
  if (consider_priority) {
    this_priority = connection_->scheduling_priority();
  }
  const base::UnguessableToken& this_token = connection_->client_token();
  return bucket_context_->lock_manager().IsBlockingAnyRequest(
      lock_ids(), [&this_priority, &this_token](
                      const PartitionedLockHolder& blocked_lock_holder) {
        auto* lock_request_data = static_cast<LockRequestData*>(
            blocked_lock_holder.GetUserData(LockRequestData::kKey));
        if (!lock_request_data) {
          return true;
        }
        // If `this`
        //   * comes from a background client (priority > 0), and
        //   * is equal or higher priority than the blocked
        //   transaction's client
        //     (aka equally or less severely throttled)
        // then don't worry about blocking it.
        if (this_priority && (*this_priority > 0) &&
            (*this_priority <= lock_request_data->scheduling_priority)) {
          return false;
        }
        return lock_request_data->client_token != this_token;
      });
}

void Transaction::Start() {
  TRACE_EVENT0("IndexedDB", "Transaction::Start");

  // The transaction has the potential to be aborted after the Start() task was
  // posted.
  if (state_ == FINISHED) {
    CHECK(locks_receiver_.locks.empty());
    return;
  }
  CHECK_EQ(CREATED, state_);
  SetState(STARTED);
  CHECK(!locks_receiver_.locks.empty());
  diagnostics_.start_time = base::Time::Now();

  // If the client is in BFCache, the transaction will get stuck, so evict it if
  // necessary.
  DontAllowInactiveClientToBlockOthers(
      storage::mojom::DisallowInactiveClientReason::
          kTransactionIsStartingWhileBlockingOthers);

  const base::TimeDelta time_queued =
      diagnostics_.start_time - diagnostics_.creation_time;
  switch (mode_) {
    case blink::mojom::IDBTransactionMode::ReadOnly:
      base::UmaHistogramMediumTimes(
          "WebCore.IndexedDB.Transaction.ReadOnly.TimeQueued", time_queued);
      break;
    case blink::mojom::IDBTransactionMode::ReadWrite:
      base::UmaHistogramMediumTimes(
          "WebCore.IndexedDB.Transaction.ReadWrite.TimeQueued", time_queued);
      break;
    case blink::mojom::IDBTransactionMode::VersionChange:
      base::UmaHistogramMediumTimes(
          "WebCore.IndexedDB.Transaction.VersionChange.TimeQueued",
          time_queued);
      break;
  }

  bucket_context_->QueueRunTasks();
}

// static
void Transaction::DisableInactivityTimeoutForTesting() {
  g_inactivity_timeout_enabled = false;
}

void Transaction::CreateObjectStore(int64_t object_store_id,
                                    const std::u16string& name,
                                    const IndexedDBKeyPath& key_path,
                                    bool auto_increment) {
  if (!connection_
           ->GetTransactionAndVerifyState(
               id(), blink::mojom::IDBTransactionMode::VersionChange)
           .has_value()) {
    return;
  }

  ScheduleTask(
      blink::mojom::IDBTaskType::Preemptive, "CreateObjectStore",
      base::BindOnce(
          [](int64_t object_store_id, const std::u16string& name,
             const IndexedDBKeyPath& key_path, bool auto_increment,
             Transaction* transaction) {
            return transaction->BackingStoreTransaction()->CreateObjectStore(
                object_store_id, name, key_path, auto_increment);
          },
          object_store_id, name, key_path, auto_increment),
      // The object store ID must be a valid new ID.
      base::BindOnce(
          [](int64_t object_store_id,
             mojo::ReportBadMessageCallback report_bad_message_callback,
             Transaction& transaction) {
            if (transaction.connection_->database()->IsObjectStoreIdInMetadata(
                    object_store_id) ||
                object_store_id <= transaction.connection_->database()
                                       ->metadata()
                                       .max_object_store_id) {
              ReportBadMessage(
                  BadMessageReason::kTransactionCreateObjectStoreInvalidId,
                  "Invalid object_store_id",
                  std::move(report_bad_message_callback));
              return Status::InvalidArgument("Invalid object_store_id.");
            }

            return Status::OK();
          },
          object_store_id, mojo::GetBadMessageCallback()));
}

void Transaction::DeleteObjectStore(int64_t object_store_id) {
  if (!connection_
           ->GetTransactionAndVerifyState(
               id(), blink::mojom::IDBTransactionMode::VersionChange)
           .has_value()) {
    return;
  }

  ScheduleTask(
      "DeleteObjectStore",
      base::BindOnce(
          [](int64_t object_store_id, Transaction* transaction) {
            return transaction->BackingStoreTransaction()->DeleteObjectStore(
                object_store_id);
          },
          object_store_id),
      ObjectStoreMustExist(object_store_id));
}

void Transaction::Put(int64_t object_store_id,
                      blink::mojom::IDBValuePtr input_value,
                      IndexedDBKey key,
                      blink::mojom::IDBPutMode mode,
                      std::vector<IndexedDBIndexKeys> index_keys,
                      blink::mojom::IDBTransaction::PutCallback callback) {
  if (mode_ == blink::mojom::IDBTransactionMode::ReadOnly) {
    ReportBadMessage(BadMessageReason::kTransactionPutReadOnly,
                     "Attempted to Put on readonly txn.",
                     receiver_.GetBadMessageCallback());
    return;
  }

  if (!IsAcceptingRequests() || !connection_->IsConnected()) {
    DatabaseError error(blink::mojom::IDBException::kUnknownError,
                        "Not connected.");
    std::move(callback).Run(
        blink::mojom::IDBTransactionPutResult::NewErrorResult(
            blink::mojom::IDBError::New(error.code(), error.message())));
    return;
  }

  if (input_value->bits.storage_type() ==
      mojo_base::BigBuffer::StorageType::kInvalidBuffer) {
    ReportBadMessage(BadMessageReason::kTransactionPutInvalidValue,
                     "Attempted to Put invalid SSV.",
                     receiver_.GetBadMessageCallback());
    return;
  }

  std::vector<IndexedDBExternalObject> external_objects;
  uint64_t total_blob_size = 0;
  if (!input_value->external_objects.empty() &&
      !CreateExternalObjects(input_value, &external_objects,
                             &total_blob_size)) {
    ReportBadMessage(BadMessageReason::kTransactionPutInvalidExternalObjects,
                     "Couldn't deserialize external objects.",
                     receiver_.GetBadMessageCallback());
    return;
  }

  // Increment the total transaction size by the size of this put.
  preliminary_size_estimate_ +=
      input_value->bits.size() + key.size_estimate() + total_blob_size;
  // Warm up the disk space cache.
  bucket_context_->CheckCanUseDiskSpace(preliminary_size_estimate_, {});

  IndexedDBValue value;
  value.bits = std::move(input_value->bits);
  value.external_objects = std::move(external_objects);

  blink::mojom::IDBTransaction::PutCallback wrapped_callback =
      CreateCallbackAbortOnDestruct<blink::mojom::IDBTransaction::PutCallback,
                                    blink::mojom::IDBTransactionPutResultPtr>(
          std::move(callback), AsWeakPtr());

  // This is decremented in DoPut.
  in_flight_memory_ += value.SizeEstimate();
  ScheduleTask(
      "PutRecord",
      base::BindOnce(&Transaction::DoPut, base::Unretained(this),
                     object_store_id, std::move(value), std::move(key), mode,
                     std::move(index_keys), std::move(wrapped_callback),
                     // The validation checks are a bit too complex to squeeze
                     // into a lambda, so they're performed in `DoPut`.
                     receiver_.GetBadMessageCallback()));
}

Status Transaction::DoPut(int64_t object_store_id,
                          IndexedDBValue value,
                          IndexedDBKey key,
                          blink::mojom::IDBPutMode put_mode,
                          std::vector<IndexedDBIndexKeys> index_keys,
                          blink::mojom::IDBTransaction::PutCallback callback,
                          mojo::ReportBadMessageCallback bad_message_callback,
                          Transaction* txn) {
  CHECK_EQ(this, txn);
  TRACE_EVENT2("IndexedDB", "Database::PutOperation", "txn.id", id(), "size",
               value.SizeEstimate());
  bool key_was_generated = false;
  in_flight_memory_ -= value.SizeEstimate();
  CHECK(in_flight_memory_.IsValid());
  auto on_put_error = [this](blink::mojom::IDBTransaction::PutCallback callback,
                             blink::mojom::IDBException code,
                             const std::u16string& message) {
    IncrementNumErrorsSent();
    std::move(callback).Run(
        blink::mojom::IDBTransactionPutResult::NewErrorResult(
            blink::mojom::IDBError::New(code, message)));
  };

  const IndexedDBObjectStoreMetadata* object_store =
      connection_->database()->GetObjectStoreMetadataIfExists(object_store_id);
  if (!object_store) {
    ReportBadMessage(BadMessageReason::kTransactionDoPutInvalidObjectStoreId,
                     "Invalid object_store_id",
                     std::move(bad_message_callback));
    return Status::InvalidArgument("Invalid object_store_id.");
  }
  for (const IndexedDBIndexKeys& it : index_keys) {
    if (!object_store->indexes.contains(it.id)) {
      ReportBadMessage(BadMessageReason::kTransactionDoPutInvalidIndexId,
                       "Invalid index id", std::move(bad_message_callback));
      return Status::InvalidArgument("Invalid index id");
    }
  }

  if (put_mode != blink::mojom::IDBPutMode::CursorUpdate &&
      object_store->auto_increment && !key.IsValid()) {
    IndexedDBKey auto_inc_key = GenerateAutoIncrementKey(object_store_id);
    key_was_generated = true;
    if (!auto_inc_key.IsValid()) {
      on_put_error(std::move(callback),
                   blink::mojom::IDBException::kConstraintError,
                   u"Maximum key generator value reached.");
      return Status::OK();
    }
    key = std::move(auto_inc_key);
  }

  if (!key.IsValid()) {
    ReportBadMessage(BadMessageReason::kTransactionDoPutInvalidKey,
                     "Invalid key", std::move(bad_message_callback));
    return Status::InvalidArgument("Invalid key");
  }

  if (put_mode == blink::mojom::IDBPutMode::AddOnly) {
    ASSIGN_OR_RETURN(
        std::optional<BackingStore::RecordIdentifier> preexisting_record,
        BackingStoreTransaction()->KeyExistsInObjectStore(object_store_id,
                                                          key));
    if (preexisting_record) {
      on_put_error(std::move(callback),
                   blink::mojom::IDBException::kConstraintError,
                   u"Key already exists in the object store.");
      return Status::OK();
    }
  }

  ASSIGN_OR_RETURN(
      std::vector<std::unique_ptr<IndexWriter>> index_writers,
      MakeIndexWriters(this, *object_store, key, key_was_generated,
                       std::move(index_keys)),
      [&](IndexWriterError error) {
        switch (error.type) {
          case IndexWriterError::Type::kInvalidKey:
            ReportBadMessage(BadMessageReason::kTransactionDoPutInvalidIndexKey,
                             "Invalid index key",
                             std::move(bad_message_callback));
            return Status::InvalidArgument("Invalid index key");
          case IndexWriterError::Type::kBackingStoreError:
            on_put_error(
                std::move(callback), blink::mojom::IDBException::kUnknownError,
                u"Internal error: backing store error updating index keys.");
            return Status::OK();
          case IndexWriterError::Type::kConstraintError:
            on_put_error(std::move(callback),
                         blink::mojom::IDBException::kConstraintError,
                         error.message);
            return Status::OK();
        }
      });

  // Before this point, don't do any mutation. After this point, rollback the
  // transaction in case of error.
  StatusOr<BackingStore::RecordIdentifier> new_record =
      BackingStoreTransaction()->PutRecord(object_store_id, key,
                                           std::move(value));
  // Only LevelDB can return an InvalidArgument, so simplify to
  // `ASSIGN_OR_RETURN` when SQLite is the only backing store.
  if (!new_record.has_value()) {
    if (new_record.error().IsInvalidArgument()) {
      ReportBadMessage(BadMessageReason::kTransactionDoPutInvalidRecord,
                       new_record.error().ToString(),
                       std::move(bad_message_callback));
    }
    return new_record.error();
  }

  {
    TRACE_EVENT1("IndexedDB", "Database::PutOperation.UpdateIndexes", "txn.id",
                 id());
    for (const auto& writer : index_writers) {
      writer->WriteIndexKeys(*new_record, BackingStoreTransaction(),
                             object_store_id);
    }
  }

  if (object_store->auto_increment &&
      put_mode != blink::mojom::IDBPutMode::CursorUpdate &&
      key.type() == blink::mojom::IDBKeyType::Number) {
    TRACE_EVENT1("IndexedDB", "Database::PutOperation.AutoIncrement", "txn.id",
                 id());
    // Maximum integer uniquely representable as ECMAScript number.
    const double max_generator_value = 9007199254740992.0;
    int64_t new_max = 1 + base::saturated_cast<int64_t>(floor(
                              std::min(key.number(), max_generator_value)));
    IDB_RETURN_IF_ERROR(
        BackingStoreTransaction()->MaybeUpdateKeyGeneratorCurrentNumber(
            object_store_id, new_max, key_was_generated));
  }
  {
    TRACE_EVENT1("IndexedDB", "Database::PutOperation.Callbacks", "txn.id",
                 id());
    std::move(callback).Run(
        blink::mojom::IDBTransactionPutResult::NewKey(std::move(key)));
  }

  bucket_context().delegate().on_content_changed.Run(
      connection_->database()->name(), object_store->name);
  return Status::OK();
}

void Transaction::SetIndexKeys(int64_t object_store_id,
                               IndexedDBKey primary_key,
                               IndexedDBIndexKeys index_keys) {
  if (!IsAcceptingRequests() || !connection_->IsConnected()) {
    return;
  }

  if (mode() != blink::mojom::IDBTransactionMode::VersionChange) {
    ReportBadMessage(
        BadMessageReason::kTransactionSetIndexKeysWrongMode,
        "SetIndexKeys must be called from a version change transaction.");
    return;
  }

  if (!primary_key.IsValid() ||
      !std::ranges::all_of(index_keys.keys, &IndexedDBKey::IsValid)) {
    ReportBadMessage(BadMessageReason::kTransactionSetIndexKeysInvalidKey,
                     "SetIndexKeys used with invalid key.");
    return;
  }

  const int64_t index_id = index_keys.id;
  ScheduleTask(blink::mojom::IDBTaskType::Preemptive, "SetIndexKeys",
               base::BindOnce(&Transaction::DoSetIndexKeys,
                              base::Unretained(this), object_store_id,
                              std::move(primary_key), std::move(index_keys)),
               ObjectStoreAndIndexMustExist(object_store_id, index_id));
}

Status Transaction::DoSetIndexKeys(int64_t object_store_id,
                                   IndexedDBKey primary_key,
                                   IndexedDBIndexKeys index_keys,
                                   Transaction* transaction) {
  CHECK_EQ(this, transaction);
  TRACE_EVENT1("IndexedDB", "Database::SetIndexKeysOperation", "txn.id", id());
  CHECK_EQ(mode(), blink::mojom::IDBTransactionMode::VersionChange);

  ASSIGN_OR_RETURN(std::optional<BackingStore::RecordIdentifier> found_record,
                   BackingStoreTransaction()->KeyExistsInObjectStore(
                       object_store_id, primary_key));
  if (!found_record) {
    Abort(DatabaseError(blink::mojom::IDBException::kUnknownError,
                        "Internal error setting index keys for object store."));
    // TODO(crbug.com/489361938): this should probably be a corruption status.
    return Status::OK();
  }

  const IndexedDBObjectStoreMetadata& object_store_metadata =
      connection_->database()->GetObjectStoreMetadata(object_store_id);
  std::vector<IndexedDBIndexKeys> keys_vec;
  keys_vec.emplace_back(std::move(index_keys));
  ASSIGN_OR_RETURN(
      std::vector<std::unique_ptr<IndexWriter>> index_writers,
      MakeIndexWriters(this, object_store_metadata, primary_key,
                       /*key_was_generated=*/false, std::move(keys_vec)),
      [&](IndexWriterError error) {
        switch (error.type) {
          case IndexWriterError::Type::kInvalidKey:
            // Invalid keys are rejected right away by `SetIndexKeys`.
            NOTREACHED();
          case IndexWriterError::Type::kBackingStoreError:
            Abort(DatabaseError(blink::mojom::IDBException::kUnknownError,
                                "Internal error: backing store error updating "
                                "index keys."));
            // TODO(crbug.com/489361938): this should probably be a corruption
            // status.
            return Status::OK();
          case IndexWriterError::Type::kConstraintError:
            Abort(DatabaseError(blink::mojom::IDBException::kConstraintError,
                                error.message));
            // TODO(crbug.com/489361938): this should probably be a corruption
            // status.
            return Status::OK();
        }
      });

  for (const auto& writer : index_writers) {
    IDB_RETURN_IF_ERROR(writer->WriteIndexKeys(
        *found_record, BackingStoreTransaction(), object_store_id));
  }
  return Status::OK();
}

void Transaction::SetIndexKeysDone() {
  if (!IsAcceptingRequests() || !connection_->IsConnected()) {
    return;
  }

  if (mode() != blink::mojom::IDBTransactionMode::VersionChange) {
    ReportBadMessage(
        BadMessageReason::kTransactionSetIndexKeysDoneWrongMode,
        "SetIndexKeysDone must be called from a version change transaction.");
    return;
  }

  ScheduleTask(
      blink::mojom::IDBTaskType::Preemptive,
      /*operation_name_for_metrics=*/{},
      base::BindOnce([](Transaction* transaction) {
        transaction->DidCompletePreemptiveEvent();
        return Status::OK();
      }),
      base::BindOnce(
          [](mojo::ReportBadMessageCallback report_bad_message_callback,
             Transaction& transaction) {
            if (transaction.pending_preemptive_events_ == 0) {
              constexpr std::string_view kErrorMessage =
                  "SetIndexKeysDone called without beginning indexing";
              ReportBadMessage(
                  BadMessageReason::kTransactionSetIndexKeysDoneWithoutIndexing,
                  kErrorMessage, std::move(report_bad_message_callback));
              return Status::InvalidArgument(kErrorMessage);
            }
            return Status::OK();
          },
          mojo::GetBadMessageCallback()));
}

void Transaction::Commit(int64_t num_errors_handled) {
  if (!IsAcceptingRequests() || !connection_->IsConnected()) {
    return;
  }

  num_errors_handled_ = num_errors_handled;

  // Always allow empty or delete-only transactions.
  if (preliminary_size_estimate_ <= 0) {
    SetCommitFlag();
    return;
  }

  bucket_context_->CheckCanUseDiskSpace(
      preliminary_size_estimate_, base::BindOnce(&Transaction::OnQuotaCheckDone,
                                                 ptr_factory_.GetWeakPtr()));
}

void Transaction::OnQuotaCheckDone(bool allowed) {
  // May have disconnected while quota check was pending.
  if (!connection_->IsConnected()) {
    return;
  }

  if (allowed) {
    SetCommitFlag();
  } else {
    Abort(DatabaseError(blink::mojom::IDBException::kQuotaError));
  }
}

bool Transaction::CreateExternalObjects(
    blink::mojom::IDBValuePtr& value,
    std::vector<IndexedDBExternalObject>* external_objects,
    uint64_t* total_size) {
  // Should only be called if there are external objects to process.
  CHECK(!value->external_objects.empty());

  base::CheckedNumeric<uint64_t> total_blob_size = 0;
  external_objects->resize(value->external_objects.size());
  for (size_t i = 0; i < value->external_objects.size(); ++i) {
    auto& object = value->external_objects[i];
    switch (object->which()) {
      case blink::mojom::IDBExternalObject::Tag::kBlobOrFile: {
        blink::mojom::IDBBlobInfoPtr& info = object->get_blob_or_file();
        if (info->size < 0) {
          return false;
        }

        total_blob_size += info->size;

        if (info->file) {
          if (info->file->last_modified.ToDeltaSinceWindowsEpoch() <
              base::TimeDelta()) {
            return false;
          }
          (*external_objects)[i] = IndexedDBExternalObject(
              std::move(info->blob), info->file->name, info->mime_type,
              info->file->last_modified, info->size);
        } else {
          (*external_objects)[i] = IndexedDBExternalObject(
              std::move(info->blob), info->mime_type, info->size);
        }
        break;
      }
      case blink::mojom::IDBExternalObject::Tag::kFileSystemAccessToken:
        (*external_objects)[i] = IndexedDBExternalObject(
            std::move(object->get_file_system_access_token()));
        break;
    }
  }
  return total_blob_size.AssignIfValid(total_size);
}

void Transaction::BlobWriteComplete(base::TimeTicks start_time, Status result) {
  TRACE_EVENT0("IndexedDB", "Transaction::BlobWriteComplete");
  if (state_ == FINISHED) {  // aborted
    return;
  }
  CHECK_EQ(state_, COMMITTING);

  LogStatus(result, "IndexedDB.BackingStore.WriteBlobs",
            bucket_context_->GetHistogramSuffix());

  if (!result.ok()) {
    Abort(DatabaseError(blink::mojom::IDBException::kDataError,
                        base::ASCIIToUTF16(absl::StrFormat(
                            "Failed to write blobs (%s)", result.ToString()))));
    return;
  }

  LogDuration(base::TimeTicks::Now() - start_time,
              "IndexedDB.BackendDuration.WriteBlobs",
              bucket_context_->GetHistogramSuffix());
  ScheduleTask(
      /*operation_name_for_metrics=*/{},
      base::IgnoreArgs<Transaction*>(base::BindOnce(
          &Transaction::CommitPhaseTwo, base::Unretained(this))));
  bucket_context_->QueueRunTasks();
}

Status Transaction::DoPendingCommit() {
  TRACE_EVENT1("IndexedDB", "Transaction::DoPendingCommit", "txn.id", id());
  CHECK(is_commit_pending_, base::NotFatalUntil::M145);

  timeout_timer_.Stop();

  // In multiprocess ports, front-end may have requested a commit but
  // an abort has already been initiated asynchronously by the
  // back-end.
  if (state_ == FINISHED) {
    return Status::OK();
  }
  CHECK_NE(state_, COMMITTING);

  // Front-end has requested a commit, but this transaction is blocked by
  // other transactions. The commit will be initiated when the transaction
  // coordinator unblocks this transaction.
  if (state_ != STARTED) {
    return Status::OK();
  }

  // Front-end has requested a commit, but there may be tasks like
  // create_index which are considered synchronous by the front-end
  // but are processed asynchronously.
  if (HasPendingTasks()) {
    return Status::OK();
  }

  // If a transaction is being committed but it has sent more errors to the
  // front end than have been handled at this point, the transaction should be
  // aborted as it is unknown whether or not any errors unaccounted for will be
  // properly handled.
  if (num_errors_sent_ != num_errors_handled_) {
    is_commit_pending_ = false;
    Abort(DatabaseError(blink::mojom::IDBException::kUnknownError));
    return Status::OK();
  }

  SetState(COMMITTING);

  if (!used_) {
    return CommitPhaseTwo();
  }

  // CommitPhaseOne will not call the callback if there are no blobs to write.
  base::ElapsedTimer timer;
  ASSIGN_OR_RETURN(
      bool async_work_in_progress,
      LOG_RESULT(
          backing_store_transaction_->CommitPhaseOne(
              /*blob_write_callback=*/
              base::BindOnce(&Transaction::BlobWriteComplete,
                             ptr_factory_.GetWeakPtr(), base::TimeTicks::Now()),
              // This callback is only used by SQLite. The LevelDB version of
              // this code lives in `BackingStore::Transaction::WriteNewBlobs`.
              /*serialize_fsa_handle=*/
              base::BindRepeating(
                  [](base::WeakPtr<Transaction> transaction,
                     blink::mojom::FileSystemAccessTransferToken& token_remote,
                     base::OnceCallback<void(const std::vector<uint8_t>&)>
                         deliver_serialized_token) {
                    if (!transaction) {
                      return;
                    }

                    // TODO(dmurph): Refactor IndexedDBExternalObject to not use
                    // a SharedRemote, so this code can just move the remote,
                    // instead of cloning.
                    mojo::PendingRemote<
                        blink::mojom::FileSystemAccessTransferToken>
                        token_clone;
                    token_remote.Clone(
                        token_clone.InitWithNewPipeAndPassReceiver());
                    transaction->bucket_context()
                        .file_system_access_context()
                        ->SerializeHandle(std::move(token_clone),
                                          std::move(deliver_serialized_token));
                  },
                  ptr_factory_.GetWeakPtr())),
          "IndexedDB.BackingStore.CommitPhaseOne",
          bucket_context_->GetHistogramSuffix()));
  commit_synchronous_duration_ = timer.Elapsed();
  if (async_work_in_progress) {
    return Status::OK();
  }
  return CommitPhaseTwo();
}

Status Transaction::CommitPhaseTwo() {
  // Abort may have been called just as the blob write completed.
  if (state_ == FINISHED) {
    return Status::OK();
  }

  CHECK_EQ(state_, COMMITTING);

  SetState(FINISHED);

  Status s;
  bool committed;
  if (!used_) {
    committed = true;
  } else {
    base::ElapsedTimer timer;
    s = LogStatus(backing_store_transaction_->CommitPhaseTwo(),
                  "IndexedDB.BackingStore.CommitPhaseTwo",
                  bucket_context_->GetHistogramSuffix());
    commit_synchronous_duration_ += timer.Elapsed();

    if (s.ok()) {
      LogDuration(commit_synchronous_duration_,
                  "IndexedDB.BackendDuration.CommitTransaction",
                  bucket_context_->GetHistogramSuffix());
    }

    // This measurement includes the time it takes to commit to the backing
    // store (i.e. LevelDB), not just the blobs.
    const base::TimeDelta active_time =
        base::Time::Now() - diagnostics_.start_time;

    switch (mode_) {
      case blink::mojom::IDBTransactionMode::ReadOnly:
        base::UmaHistogramMediumTimes(
            "WebCore.IndexedDB.Transaction.ReadOnly.TimeActive2", active_time);
        break;
      case blink::mojom::IDBTransactionMode::ReadWrite:
        base::UmaHistogramMediumTimes(
            "WebCore.IndexedDB.Transaction.ReadWrite.TimeActive2", active_time);
        break;
      case blink::mojom::IDBTransactionMode::VersionChange:
        base::UmaHistogramMediumTimes(
            "WebCore.IndexedDB.Transaction.VersionChange.TimeActive2",
            active_time);
        break;
      default:
        NOTREACHED();
    }

    committed = s.ok();
  }

  // Backing store resources (held via cursors) must be released
  // before script callbacks are fired, as the script callbacks may
  // release references and allow the backing store itself to be
  // released, and order is critical.
  CloseOpenCursors();
  backing_store_transaction_.reset();

  // Transactions must also be marked as completed before the
  // front-end is notified, as the transaction completion unblocks
  // operations like closing connections.
  locks_receiver_.locks.clear();

  if (committed) {
    if (mode() != blink::mojom::IDBTransactionMode::ReadOnly) {
      const bool did_sync =
          mode() == blink::mojom::IDBTransactionMode::VersionChange ||
          durability_ == blink::mojom::IDBTransactionDurability::Strict;
      bucket_context_->delegate().on_files_written.Run(did_sync);
    }

    {
      TRACE_EVENT1("IndexedDB",
                   "Transaction::CommitPhaseTwo.TransactionCompleteCallbacks",
                   "txn.id", id());
      connection_->callbacks()->OnComplete(*this);
    }

    return s;
  }

  DatabaseError error;
  if (s.IndicatesDiskFull()) {
    error =
        DatabaseError(blink::mojom::IDBException::kQuotaError,
                      "Encountered disk full while committing transaction.");
  } else {
    error = DatabaseError(blink::mojom::IDBException::kUnknownError,
                          "Internal error committing transaction.");
  }
  connection_->callbacks()->OnAbort(*this, error);
  return s;
}

Status Transaction::RunTasks() {
  TRACE_EVENT1("IndexedDB", "Transaction::RunTasks", "txn.id", id());

  // No re-entrancy allowed.
  CHECK(!processing_event_queue_);
  // Should not be called after completion.
  CHECK(!aborted_);
  CHECK_NE(state_, FINISHED);

  if (IsTaskQueueEmpty() && !is_commit_pending_) {
    return Status::OK();
  }

  if (!backing_store_transaction_begun_) {
    base::ElapsedTimer timer;
    IDB_RETURN_IF_ERROR(LogStatus(
        backing_store_transaction_->Begin(std::move(locks_receiver_.locks)),
        "IndexedDB.BackingStore.BeginTransaction",
        bucket_context_->GetHistogramSuffix()));
    LogDuration(timer.Elapsed(), "IndexedDB.BackendDuration.BeginTransaction",
                bucket_context_->GetHistogramSuffix());
    backing_store_transaction_begun_ = true;
  }

  // `AutoReset` is not used because `this` may be destroyed before the end of
  // this method.
  base::ScopedClosureRunner reset(base::BindOnce(
      [](base::WeakPtr<Transaction> txn) {
        if (txn) {
          txn->processing_event_queue_ = false;
        }
      },
      ptr_factory_.GetWeakPtr()));
  processing_event_queue_ = true;
  base::WeakPtr<Transaction> weak_this = ptr_factory_.GetWeakPtr();

  bool run_preemptive_queue =
      !preemptive_task_queue_.empty() || pending_preemptive_events_ != 0;
  TaskQueue* task_queue =
      run_preemptive_queue ? &preemptive_task_queue_ : &task_queue_;
  while (!task_queue->empty() && state_ != FINISHED) {
    base::ElapsedTimer timer;
    CHECK(state_ == STARTED || state_ == COMMITTING) << state_;
    Task task = std::move(task_queue->front());
    task_queue->pop();
    Status result =
        task.verify ? std::move(task.verify).Run(*this) : Status::OK();
    if (result.ok()) {
      // The operation may invalidate the bucket context handle.
      std::string_view histogram_suffix = bucket_context_->GetHistogramSuffix();
      result = std::move(task.operation).Run(this);
      if (!task.operation_name_for_metrics.empty()) {
        LogStatus(result,
                  base::StrCat({"IndexedDB.BackingStore.",
                                task.operation_name_for_metrics}),
                  histogram_suffix);
        if (result.ok()) {
          LogDuration(timer.Elapsed(),
                      base::StrCat({"IndexedDB.BackendDuration.",
                                    task.operation_name_for_metrics}),
                      histogram_suffix);
        }
      }
    }
    if (weak_this && !run_preemptive_queue) {
      CHECK(diagnostics_.tasks_completed < diagnostics_.tasks_scheduled);
      ++diagnostics_.tasks_completed;
      NotifyOfIdbInternalsRelevantChange();
    }

    IDB_RETURN_IF_ERROR(result);
    // If running the task destroyed `this`, `result` should have been an error.
    CHECK(weak_this);

    run_preemptive_queue =
        !preemptive_task_queue_.empty() || pending_preemptive_events_ != 0;
    // Event itself may change which queue should be processed next.
    task_queue = run_preemptive_queue ? &preemptive_task_queue_ : &task_queue_;
  }

  if (!HasPendingTasks() && state_ == STARTED) {
    if (is_commit_pending_) {
      // If there are no pending tasks, we haven't already committed/aborted,
      // and the front-end requested a commit, it is now safe to do so.
      IDB_RETURN_IF_ERROR(DoPendingCommit());
    } else if (g_inactivity_timeout_enabled) {
      // Otherwise, start a timer in case the front-end gets wedged and never
      // requests further activity.
      timeout_timer_.Start(
          FROM_HERE, kInactivityTimeout,
          base::BindRepeating(&Transaction::OnInactivityTimeout,
                              ptr_factory_.GetWeakPtr()));
    }
  }

  return Status::OK();
}

storage::mojom::IdbTransactionMetadataPtr Transaction::GetIdbInternalsMetadata()
    const {
  storage::mojom::IdbTransactionMetadataPtr info =
      storage::mojom::IdbTransactionMetadata::New();
  info->mode = static_cast<storage::mojom::IdbTransactionMode>(mode());
  switch (state()) {
    case Transaction::CREATED:
      info->state = storage::mojom::IdbTransactionState::kBlocked;
      break;
    case Transaction::STARTED:
      info->state = diagnostics().tasks_scheduled > 0
                        ? storage::mojom::IdbTransactionState::kRunning
                        : storage::mojom::IdbTransactionState::kStarted;
      break;
    case Transaction::COMMITTING:
      info->state = storage::mojom::IdbTransactionState::kCommitting;
      break;
    case Transaction::FINISHED:
      info->state = storage::mojom::IdbTransactionState::kFinished;
      break;
  }

  info->tid = id();
  info->connection_id = connection_->id();
  info->client_token = connection_->client_token().ToString();
  info->age =
      (base::Time::Now() - diagnostics().creation_time).InMillisecondsF();
  if (diagnostics().start_time.InMillisecondsSinceUnixEpoch() > 0) {
    info->runtime =
        (base::Time::Now() - diagnostics().start_time).InMillisecondsF();
  }
  info->tasks_scheduled = diagnostics().tasks_scheduled;
  info->tasks_completed = diagnostics().tasks_completed;

  for (int64_t id : scope()) {
    auto stores_it = database_->metadata().object_stores.find(id);
    if (stores_it != database_->metadata().object_stores.end()) {
      info->scope.emplace_back(stores_it->second.name);
    }
  }
  return info;
}

void Transaction::NotifyOfIdbInternalsRelevantChange() {
  // This metadata is included in the databases metadata, so call up the chain.
  if (database_) {
    database_->NotifyOfIdbInternalsRelevantChange();
  }
}

void Transaction::OnInactivityTimeout() {
  // The timeout timer should only be running when these conditions are met:
  CHECK(used_, base::NotFatalUntil::M145);
  CHECK(task_queue_.empty(), base::NotFatalUntil::M145);
  CHECK(preemptive_task_queue_.empty(), base::NotFatalUntil::M145);

  const size_t num_transactions_across_all_connections =
      database_->GetNumTransactionsAcrossAllConnections();

  // Histograms to diagnose memory leak crbug.com/381086791.
  // TODO(crbug.com/381086791): Remove after the leak is fixed.
  base::UmaHistogramEnumeration("IndexedDB.TransactionTimeout.Mode", mode_);
  base::UmaHistogramBoolean("IndexedDB.TransactionTimeout.CommitPending",
                            is_commit_pending_);
  base::UmaHistogramBoolean("IndexedDB.TransactionTimeout.IsAborted", aborted_);
  base::UmaHistogramBoolean("IndexedDB.TransactionTimeout.TaskRunQueued",
                            bucket_context_->task_run_queued());
  base::UmaHistogramCounts10000(
      "IndexedDB.TransactionTimeout.NumTransactionsInDB",
      num_transactions_across_all_connections);
  base::UmaHistogramBoolean("IndexedDB.TransactionTimeout.IsConnected",
                            connection_->IsConnected());
  base::UmaHistogramCounts10000(
      "IndexedDB.TransactionTimeout.NumTransactionsInConnection",
      connection_->transactions().size());

  // Same histograms as above, but only when there are a lot of transactions in
  // the connection.
  if (connection_->transactions().size() > 10000) {
    base::UmaHistogramEnumeration(
        "IndexedDB.TransactionTimeout.10kTransactions.Mode", mode_);
    base::UmaHistogramBoolean(
        "IndexedDB.TransactionTimeout.10kTransactions.CommitPending",
        is_commit_pending_);
    base::UmaHistogramBoolean(
        "IndexedDB.TransactionTimeout.10kTransactions.IsAborted", aborted_);
    base::UmaHistogramBoolean(
        "IndexedDB.TransactionTimeout.10kTransactions.TaskRunQueued",
        bucket_context_->task_run_queued());
    base::UmaHistogramCounts100000(
        "IndexedDB.TransactionTimeout.10kTransactions.NumTransactionsInDB",
        num_transactions_across_all_connections);
    base::UmaHistogramBoolean(
        "IndexedDB.TransactionTimeout.10kTransactions.IsConnected",
        connection_->IsConnected());
    base::UmaHistogramCounts100000(
        "IndexedDB.TransactionTimeout.10kTransactions."
        "NumTransactionsInConnection",
        connection_->transactions().size());
  }

  if (!IsTransactionBlockingOtherClients(/*consider_priority=*/true)) {
    return;
  }

  Abort(DatabaseError(blink::mojom::IDBException::kTimeoutError,
                      u"Transaction timed out due to inactivity."));
}

void Transaction::SetState(State state) {
  state_ = state;
    scheduling_priority_at_last_state_change_ =
        connection_->scheduling_priority();
  if (!IsAcceptingRequests()) {
    CloseOpenCursors();
  }
  NotifyOfIdbInternalsRelevantChange();
}

void Transaction::CloseOpenCursors() {
  TRACE_EVENT1("IndexedDB", "Transaction::CloseOpenCursors", "txn.id", id());

  // Cursor::Close() indirectly mutates |open_cursors_|, when it calls
  // Transaction::UnregisterOpenCursor().
  std::set<raw_ptr<Cursor, SetExperimental>> open_cursors =
      std::move(open_cursors_);
  open_cursors_.clear();
  for (Cursor* cursor : open_cursors) {
    cursor->Close();
  }
}

void Transaction::OnSchedulingPriorityUpdated(int new_priority) {
  auto* lock_request_data = static_cast<LockRequestData*>(
      locks_receiver_.GetUserData(LockRequestData::kKey));
  CHECK(lock_request_data);
  lock_request_data->scheduling_priority = new_priority;
}

IndexedDBKey Transaction::GenerateAutoIncrementKey(int64_t object_store_id) {
  ASSIGN_OR_RETURN(
      int64_t current_number,
      BackingStoreTransaction()->GetKeyGeneratorCurrentNumber(object_store_id),
      [](auto) {
        LOG(ERROR) << "Failed to GetKeyGeneratorCurrentNumber";
        return IndexedDBKey();
      });
  // Maximum integer uniquely representable as ECMAScript number.
  const int64_t max_generator_value = 9007199254740992LL;
  if (current_number < 0 || current_number > max_generator_value) {
    return {};
  }

  return IndexedDBKey(current_number, blink::mojom::IDBKeyType::Number);
}

blink::mojom::IDBValuePtr Transaction::BuildMojoValue(IndexedDBValue value) {
  return backing_store_transaction_->BuildMojoValue(
      std::move(value),
      // Note that this callback is only used by the SQLite store. The LevelDB
      // store reaches directly into the bucket context and its
      // FileSystemAccessContext (a layering violation).
      /*deserialize_handle=*/
      base::BindRepeating(
          &storage::mojom::FileSystemAccessContext::DeserializeHandle,
          base::Unretained(bucket_context_->file_system_access_context()),
          bucket_context_->bucket_info().storage_key));
}

// static
Transaction::VerificationCallback Transaction::ObjectStoreMustExist(
    int64_t object_store_id) {
  return base::BindOnce(
      [](int64_t object_store_id,
         mojo::ReportBadMessageCallback report_bad_message_callback,
         Transaction& transaction) {
        if (!transaction.connection_->database()->IsObjectStoreIdInMetadata(
                object_store_id)) {
          ReportBadMessage(
              BadMessageReason::kTransactionObjectStoreMustExistInvalidId,
              "Invalid object_store_id",
              std::move(report_bad_message_callback));
          return Status::InvalidArgument("Invalid object_store_id.");
        }

        return Status::OK();
      },
      object_store_id, mojo::GetBadMessageCallback());
}

// static
Transaction::VerificationCallback Transaction::ObjectStoreAndIndexMustExist(
    int64_t object_store_id,
    std::optional<int64_t> index_id) {
  return base::BindOnce(
      [](int64_t object_store_id, std::optional<int64_t> index_id,
         mojo::ReportBadMessageCallback report_bad_message_callback,
         Transaction& transaction) {
        if (index_id.has_value() &&
            *index_id == IndexedDBIndexMetadata::kInvalidId) {
          ReportBadMessage(
              BadMessageReason::
                  kTransactionObjectStoreAndIndexMustExistInvalidIndexId,
              "index_id must be valid", std::move(report_bad_message_callback));
          return Status::InvalidArgument("index_id must be valid.");
        }
        if (!transaction.connection_->database()
                 ->IsObjectStoreIdAndMaybeIndexIdInMetadata(
                     object_store_id,
                     index_id.value_or(IndexedDBIndexMetadata::kInvalidId))) {
          ReportBadMessage(
              BadMessageReason::
                  kTransactionObjectStoreAndIndexMustExistInvalidIds,
              "Invalid object_store_id or index_id",
              std::move(report_bad_message_callback));
          return Status::InvalidArgument(
              "Invalid object_store_id or index_id.");
        }

        return Status::OK();
      },
      object_store_id, index_id, mojo::GetBadMessageCallback());
}

}  // namespace content::indexed_db
