dom/indexedDB/ActorsParent.cpp
author Dimi Lee <dlee@mozilla.com>
Sat, 08 Aug 2020 06:00:00 +0000
changeset 543991 fa0dbdf15f291e814b4854d515d7ef3e4548b7fb
parent 543718 8082abc38fdea9fa315b436c424e596376615288
permissions -rw-r--r--
Bug 1658010 - Add null pointer check before notifying content block event r=xeonchen Differential Revision: https://phabricator.services.mozilla.com/D86421

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ActorsParent.h"

#include <algorithm>
#include <numeric>
#include <stdint.h>  // UINTPTR_MAX, uintptr_t
#include <utility>
#include "FileManager.h"
#include "IDBCursorType.h"
#include "IDBObjectStore.h"
#include "IDBTransaction.h"
#include "IndexedDatabase.h"
#include "IndexedDatabaseInlines.h"
#include "IndexedDatabaseManager.h"
#include "js/StructuredClone.h"
#include "js/Value.h"
#include "jsapi.h"
#include "KeyPath.h"
#include "mozilla/ArrayAlgorithm.h"
#include "mozilla/Attributes.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/Casting.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/CycleCollectedJSRuntime.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/InitializedOnce.h"
#include "mozilla/JSObjectHolder.h"
#include "mozilla/LazyIdleThread.h"
#include "mozilla/Maybe.h"
#include "mozilla/NullPrincipal.h"
#include "mozilla/Preferences.h"
#include "mozilla/Result.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/SchedulerGroup.h"
#include "mozilla/SnappyCompressOutputStream.h"
#include "mozilla/SnappyUncompressInputStream.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/storage.h"
#include "mozilla/Telemetry.h"
#include "mozilla/Unused.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/UniquePtrExtensions.h"
#include "mozilla/dom/ContentParent.h"
#include "mozilla/dom/File.h"
#include "mozilla/dom/FileBlobImpl.h"
#include "mozilla/dom/BrowserParent.h"
#include "mozilla/dom/filehandle/ActorsParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBCursorParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseFileParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBDatabaseRequestParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBFactoryRequestParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBRequestParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBTransactionParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIDBVersionChangeTransactionParent.h"
#include "mozilla/dom/indexedDB/PBackgroundIndexedDBUtilsParent.h"
#include "mozilla/dom/IPCBlobUtils.h"
#include "mozilla/dom/quota/CheckedUnsafePtr.h"
#include "mozilla/dom/quota/Client.h"
#include "mozilla/dom/quota/FileStreams.h"
#include "mozilla/dom/quota/OriginScope.h"
#include "mozilla/dom/quota/QuotaCommon.h"
#include "mozilla/dom/quota/QuotaManager.h"
#include "mozilla/dom/quota/UsageInfo.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/ipc/InputStreamParams.h"
#include "mozilla/ipc/InputStreamUtils.h"
#include "mozilla/ipc/PBackground.h"
#include "mozilla/ipc/PBackgroundParent.h"
#include "mozilla/RemoteLazyInputStreamParent.h"
#include "mozilla/Scoped.h"
#include "mozilla/storage/Variant.h"
#include "mozIStorageFunction.h"
#include "mozIStorageProgressHandler.h"
#include "mozIStorageService.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsClassHashtable.h"
#include "nsCOMPtr.h"
#include "nsDataHashtable.h"
#include "nsEscape.h"
#include "nsExceptionHandler.h"
#include "nsHashKeys.h"
#include "nsNetUtil.h"
#include "nsIAsyncInputStream.h"
#include "nsIEventTarget.h"
#include "nsIFile.h"
#include "nsIFileURL.h"
#include "nsIFileProtocolHandler.h"
#include "nsIInputStream.h"
#include "nsInterfaceHashtable.h"
#include "nsIOutputStream.h"
#include "nsIPrincipal.h"
#include "nsISupports.h"
#include "nsISupportsImpl.h"
#include "nsISupportsPriority.h"
#include "nsIThread.h"
#include "nsITimer.h"
#include "nsIURI.h"
#include "nsIURIMutator.h"
#include "nsIXPConnect.h"
#include "nsNetUtil.h"
#include "nsPrintfCString.h"
#include "nsQueryObject.h"
#include "nsRefPtrHashtable.h"
#include "nsStreamUtils.h"
#include "nsString.h"
#include "nsStringStream.h"
#include "nsThreadPool.h"
#include "nsThreadUtils.h"
#include "nsXPCOMCID.h"
#include "PermissionRequestBase.h"
#include "ProfilerHelpers.h"
#include "prsystem.h"
#include "prtime.h"
#include "ReportInternalError.h"
#include "SafeRefPtr.h"
#include "snappy/snappy.h"

#define DISABLE_ASSERTS_FOR_FUZZING 0

#if DISABLE_ASSERTS_FOR_FUZZING
#  define ASSERT_UNLESS_FUZZING(...) \
    do {                             \
    } while (0)
#else
#  define ASSERT_UNLESS_FUZZING(...) MOZ_ASSERT(false, __VA_ARGS__)
#endif

#define IDB_DEBUG_LOG(_args) \
  MOZ_LOG(IndexedDatabaseManager::GetLoggingModule(), LogLevel::Debug, _args)

#if defined(MOZ_WIDGET_ANDROID)
#  define IDB_MOBILE
#endif

namespace mozilla {

MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedPRFileDesc, PRFileDesc,
                                          PR_Close);

namespace dom::indexedDB {

using namespace mozilla::dom::quota;
using namespace mozilla::ipc;
using mozilla::dom::quota::Client;

namespace {

class ConnectionPool;
class Database;
struct DatabaseActorInfo;
class DatabaseFile;
class DatabaseLoggingInfo;
class DatabaseMaintenance;
class Factory;
class Maintenance;
class MutableFile;
class OpenDatabaseOp;
class TransactionBase;
class TransactionDatabaseOperationBase;
class VersionChangeTransaction;
template <bool StatementHasIndexKeyBindings>
struct ValuePopulateResponseHelper;

/*******************************************************************************
 * Constants
 ******************************************************************************/

// If JS_STRUCTURED_CLONE_VERSION changes then we need to update our major
// schema version.
static_assert(JS_STRUCTURED_CLONE_VERSION == 8,
              "Need to update the major schema version.");

// Major schema version. Bump for almost everything.
const uint32_t kMajorSchemaVersion = 26;

// Minor schema version. Should almost always be 0 (maybe bump on release
// branches if we have to).
const uint32_t kMinorSchemaVersion = 0;

// The schema version we store in the SQLite database is a (signed) 32-bit
// integer. The major version is left-shifted 4 bits so the max value is
// 0xFFFFFFF. The minor version occupies the lower 4 bits and its max is 0xF.
static_assert(kMajorSchemaVersion <= 0xFFFFFFF,
              "Major version needs to fit in 28 bits.");
static_assert(kMinorSchemaVersion <= 0xF,
              "Minor version needs to fit in 4 bits.");

const int32_t kSQLiteSchemaVersion =
    int32_t((kMajorSchemaVersion << 4) + kMinorSchemaVersion);

const int32_t kStorageProgressGranularity = 1000;

// Changing the value here will override the page size of new databases only.
// A journal mode change and VACUUM are needed to change existing databases, so
// the best way to do that is to use the schema version upgrade mechanism.
const uint32_t kSQLitePageSizeOverride =
#ifdef IDB_MOBILE
    2048;
#else
    4096;
#endif

static_assert(kSQLitePageSizeOverride == /* mozStorage default */ 0 ||
                  (kSQLitePageSizeOverride % 2 == 0 &&
                   kSQLitePageSizeOverride >= 512 &&
                   kSQLitePageSizeOverride <= 65536),
              "Must be 0 (disabled) or a power of 2 between 512 and 65536!");

// Set to -1 to use SQLite's default, 0 to disable, or some positive number to
// enforce a custom limit.
const int32_t kMaxWALPages = 5000;  // 20MB on desktop, 10MB on mobile.

// Set to some multiple of the page size to grow the database in larger chunks.
const uint32_t kSQLiteGrowthIncrement = kSQLitePageSizeOverride * 2;

static_assert(kSQLiteGrowthIncrement >= 0 &&
                  kSQLiteGrowthIncrement % kSQLitePageSizeOverride == 0 &&
                  kSQLiteGrowthIncrement < uint32_t(INT32_MAX),
              "Must be 0 (disabled) or a positive multiple of the page size!");

// The maximum number of threads that can be used for database activity at a
// single time.
const uint32_t kMaxConnectionThreadCount = 20;

static_assert(kMaxConnectionThreadCount, "Must have at least one thread!");

// The maximum number of threads to keep when idle. Threads that become idle in
// excess of this number will be shut down immediately.
const uint32_t kMaxIdleConnectionThreadCount = 2;

static_assert(kMaxConnectionThreadCount >= kMaxIdleConnectionThreadCount,
              "Idle thread limit must be less than total thread limit!");

// The length of time that database connections will be held open after all
// transactions have completed before doing idle maintenance.
const uint32_t kConnectionIdleMaintenanceMS = 2 * 1000;  // 2 seconds

// The length of time that database connections will be held open after all
// transactions and maintenance have completed.
const uint32_t kConnectionIdleCloseMS = 10 * 1000;  // 10 seconds

// The length of time that idle threads will stay alive before being shut down.
const uint32_t kConnectionThreadIdleMS = 30 * 1000;  // 30 seconds

#define SAVEPOINT_CLAUSE "SAVEPOINT sp;"

const uint32_t kFileCopyBufferSize = 32768;

constexpr auto kJournalDirectoryName = u"journals"_ns;

constexpr auto kFileManagerDirectoryNameSuffix = u".files"_ns;
constexpr auto kSQLiteSuffix = u".sqlite"_ns;
constexpr auto kSQLiteJournalSuffix = u".sqlite-journal"_ns;
constexpr auto kSQLiteSHMSuffix = u".sqlite-shm"_ns;
constexpr auto kSQLiteWALSuffix = u".sqlite-wal"_ns;

const char kPrefFileHandleEnabled[] = "dom.fileHandle.enabled";

constexpr auto kPermissionStringBase = "indexedDB-chrome-"_ns;
constexpr auto kPermissionReadSuffix = "-read"_ns;
constexpr auto kPermissionWriteSuffix = "-write"_ns;

// The following constants define all names of binding parameters in statements,
// where they are bound by name. This should include all parameter names which
// are bound by name. Binding may be done by index when the statement definition
// and binding are done in the same local scope, and no other reasons prevent
// using the indexes (e.g. multiple statement variants with differing number or
// order of parameters). Neither the styles of specifying parameter names
// (literally vs. via these constants) nor the binding styles (by index vs. by
// name) should not be mixed for the same statement. The decision must be made
// for each statement based on the proximity of statement and binding calls.
constexpr auto kStmtParamNameCurrentKey = "current_key"_ns;
constexpr auto kStmtParamNameRangeBound = "range_bound"_ns;
constexpr auto kStmtParamNameObjectStorePosition = "object_store_position"_ns;
constexpr auto kStmtParamNameLowerKey = "lower_key"_ns;
constexpr auto kStmtParamNameUpperKey = "upper_key"_ns;
constexpr auto kStmtParamNameKey = "key"_ns;
constexpr auto kStmtParamNameObjectStoreId = "object_store_id"_ns;
constexpr auto kStmtParamNameIndexId = "index_id"_ns;
// TODO: Maybe the uses of kStmtParamNameId should be replaced by more
// specific constants such as kStmtParamNameObjectStoreId.
constexpr auto kStmtParamNameId = "id"_ns;
constexpr auto kStmtParamNameValue = "value"_ns;
constexpr auto kStmtParamNameObjectDataKey = "object_data_key"_ns;
constexpr auto kStmtParamNameIndexDataValues = "index_data_values"_ns;
constexpr auto kStmtParamNameData = "data"_ns;
constexpr auto kStmtParamNameFileIds = "file_ids"_ns;
constexpr auto kStmtParamNameValueLocale = "value_locale"_ns;
constexpr auto kStmtParamNameLimit = "limit"_ns;

// The following constants define some names of columns in tables, which are
// referred to in remote locations, e.g. in calls to
// GetBindingClauseForKeyRange.
constexpr auto kColumnNameKey = "key"_ns;
constexpr auto kColumnNameValue = "value"_ns;
constexpr auto kColumnNameAliasSortKey = "sort_column"_ns;

// SQL fragments used at multiple locations.
constexpr auto kOpenLimit = " LIMIT "_ns;

// The deletion marker file is created before RemoveDatabaseFilesAndDirectory
// begins deleting a database. It is removed as the last step of deletion. If a
// deletion marker file is found when initializing the origin, the deletion
// routine is run again to ensure that the database and all of its related files
// are removed. The primary goal of this mechanism is to avoid situations where
// a database has been partially deleted, leading to inconsistent state for the
// origin.
constexpr auto kIdbDeletionMarkerFilePrefix = u"idb-deleting-"_ns;

const uint32_t kDeleteTimeoutMs = 1000;

/**
 * Automatically crash the browser if IndexedDB shutdown takes this long.  We've
 * chosen a value that is longer than the value for QuotaManager shutdown timer
 * which is currently set to 30 seconds.  We've also chosen a value that is long
 * long enough that it is unlikely for the problem to be falsely triggered by
 * slow system I/O.  We've also chosen a value long enough so that automated
 * tests should time out and fail if IndexedDB shutdown hangs.  Also, this value
 * is long enough so that testers can notice the IndexedDB shutdown hang; we
 * want to know about the hangs, not hide them.  On the other hand this value is
 * less than 60 seconds which is used by nsTerminator to crash a hung main
 * process.
 */
#define SHUTDOWN_TIMEOUT_MS 50000

#ifdef DEBUG

const int32_t kDEBUGThreadPriority = nsISupportsPriority::PRIORITY_NORMAL;
const uint32_t kDEBUGThreadSleepMS = 0;

const int32_t kDEBUGTransactionThreadPriority =
    nsISupportsPriority::PRIORITY_NORMAL;
const uint32_t kDEBUGTransactionThreadSleepMS = 0;

#endif

/*******************************************************************************
 * Metadata classes
 ******************************************************************************/

// Can be instantiated either on the QuotaManager IO thread or on a
// versionchange transaction thread. These threads can never race so this is
// totally safe.
struct FullIndexMetadata {
  IndexMetadata mCommonMetadata = {0,     nsString(), KeyPath(0), nsCString(),
                                   false, false,      false};

  FlippedOnce<false> mDeleted;

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullIndexMetadata)

 private:
  ~FullIndexMetadata() = default;
};

typedef nsRefPtrHashtable<nsUint64HashKey, FullIndexMetadata> IndexTable;

// Can be instantiated either on the QuotaManager IO thread or on a
// versionchange transaction thread. These threads can never race so this is
// totally safe.
struct FullObjectStoreMetadata {
  ObjectStoreMetadata mCommonMetadata = {0, nsString(), KeyPath(0), false};
  IndexTable mIndexes;

  // These two members are only ever touched on a transaction thread!
  int64_t mNextAutoIncrementId = 0;
  int64_t mCommittedAutoIncrementId = 0;

  FlippedOnce<false> mDeleted;

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FullObjectStoreMetadata);

  bool HasLiveIndexes() const;

 private:
  ~FullObjectStoreMetadata() = default;
};

typedef nsRefPtrHashtable<nsUint64HashKey, FullObjectStoreMetadata>
    ObjectStoreTable;

using IndexOrObjectStoreId = int64_t;
static_assert(
    std::is_same_v<
        IndexOrObjectStoreId,
        std::remove_cv_t<std::remove_reference_t<decltype(
            std::declval<const ObjectStoreGetParams&>().objectStoreId())>>>);
static_assert(std::is_same_v<
              IndexOrObjectStoreId,
              std::remove_cv_t<std::remove_reference_t<decltype(
                  std::declval<const IndexGetParams&>().objectStoreId())>>>);

struct FullDatabaseMetadata final : AtomicSafeRefCounted<FullDatabaseMetadata> {
  DatabaseMetadata mCommonMetadata;
  nsCString mDatabaseId;
  nsString mFilePath;
  ObjectStoreTable mObjectStores;

  IndexOrObjectStoreId mNextObjectStoreId = 0;
  IndexOrObjectStoreId mNextIndexId = 0;

 public:
  explicit FullDatabaseMetadata(const DatabaseMetadata& aCommonMetadata)
      : mCommonMetadata(aCommonMetadata) {
    AssertIsOnBackgroundThread();
  }

  [[nodiscard]] SafeRefPtr<FullDatabaseMetadata> Duplicate() const;

  MOZ_DECLARE_REFCOUNTED_TYPENAME(FullDatabaseMetadata)
};

template <class Enumerable>
auto MatchMetadataNameOrId(const Enumerable& aEnumerable,
                           IndexOrObjectStoreId aId,
                           Maybe<const nsAString&> aName = Nothing()) {
  AssertIsOnBackgroundThread();
  MOZ_ASSERT(aId);

  const auto it = std::find_if(
      aEnumerable.cbegin(), aEnumerable.cend(),
      [aId, aName](const auto& entry) {
        MOZ_ASSERT(entry.GetKey() != 0);

        const auto& value = entry.GetData();
        MOZ_ASSERT(value);

        return !value->mDeleted &&
               (aId == value->mCommonMetadata.id() ||
                (aName && *aName == value->mCommonMetadata.name()));
      });

  return it != aEnumerable.cend() ? SomeRef(*it->GetData()) : Nothing();
}

struct IndexDataValue final {
  IndexOrObjectStoreId mIndexId;
  Key mPosition;
  Key mLocaleAwarePosition;
  bool mUnique;

  IndexDataValue() : mIndexId(0), mUnique(false) {
    MOZ_COUNT_CTOR(IndexDataValue);
  }

  IndexDataValue(IndexDataValue&& aOther)
      : mIndexId(aOther.mIndexId),
        mPosition(std::move(aOther.mPosition)),
        mLocaleAwarePosition(std::move(aOther.mLocaleAwarePosition)),
        mUnique(aOther.mUnique) {
    MOZ_ASSERT(!aOther.mPosition.IsUnset());

    MOZ_COUNT_CTOR(IndexDataValue);
  }

  IndexDataValue(IndexOrObjectStoreId aIndexId, bool aUnique,
                 const Key& aPosition)
      : mIndexId(aIndexId), mPosition(aPosition), mUnique(aUnique) {
    MOZ_ASSERT(!aPosition.IsUnset());

    MOZ_COUNT_CTOR(IndexDataValue);
  }

  IndexDataValue(IndexOrObjectStoreId aIndexId, bool aUnique,
                 const Key& aPosition, const Key& aLocaleAwarePosition)
      : mIndexId(aIndexId),
        mPosition(aPosition),
        mLocaleAwarePosition(aLocaleAwarePosition),
        mUnique(aUnique) {
    MOZ_ASSERT(!aPosition.IsUnset());

    MOZ_COUNT_CTOR(IndexDataValue);
  }

  MOZ_COUNTED_DTOR(IndexDataValue)

  bool operator==(const IndexDataValue& aOther) const {
    if (mIndexId != aOther.mIndexId) {
      return false;
    }
    if (mLocaleAwarePosition.IsUnset()) {
      return mPosition == aOther.mPosition;
    }
    return mLocaleAwarePosition == aOther.mLocaleAwarePosition;
  }

  bool operator<(const IndexDataValue& aOther) const {
    if (mIndexId == aOther.mIndexId) {
      if (mLocaleAwarePosition.IsUnset()) {
        return mPosition < aOther.mPosition;
      }
      return mLocaleAwarePosition < aOther.mLocaleAwarePosition;
    }

    return mIndexId < aOther.mIndexId;
  }
};

/*******************************************************************************
 * SQLite functions
 ******************************************************************************/

constexpr int32_t MakeSchemaVersion(uint32_t aMajorSchemaVersion,
                                    uint32_t aMinorSchemaVersion) {
  return int32_t((aMajorSchemaVersion << 4) + aMinorSchemaVersion);
}

// WARNING: the hash function used for the database name must not change.
// That's why this function exists separately from mozilla::HashString(), even
// though it is (at the time of writing) equivalent. See bug 780408 and bug
// 940315 for details.
uint32_t HashName(const nsAString& aName) {
  struct Helper {
    static uint32_t RotateBitsLeft32(uint32_t aValue, uint8_t aBits) {
      MOZ_ASSERT(aBits < 32);
      return (aValue << aBits) | (aValue >> (32 - aBits));
    }
  };

  static const uint32_t kGoldenRatioU32 = 0x9e3779b9u;

  return std::accumulate(aName.BeginReading(), aName.EndReading(), uint32_t(0),
                         [](uint32_t hash, char16_t ch) {
                           return kGoldenRatioU32 *
                                  (Helper::RotateBitsLeft32(hash, 5) ^ ch);
                         });
}

nsresult ClampResultCode(nsresult aResultCode) {
  if (NS_SUCCEEDED(aResultCode) ||
      NS_ERROR_GET_MODULE(aResultCode) == NS_ERROR_MODULE_DOM_INDEXEDDB) {
    return aResultCode;
  }

  switch (aResultCode) {
    case NS_ERROR_FILE_NO_DEVICE_SPACE:
      return NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
    case NS_ERROR_STORAGE_CONSTRAINT:
      return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR;
    default:
#ifdef DEBUG
      nsPrintfCString message("Converting non-IndexedDB error code (0x%" PRIX32
                              ") to "
                              "NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR",
                              static_cast<uint32_t>(aResultCode));
      NS_WARNING(message.get());
#else
        ;
#endif
  }

  IDB_REPORT_INTERNAL_ERR();
  return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
}

void GetDatabaseFilenameBase(const nsAString& aDatabaseName,
                             nsAutoString& aDatabaseFilenameBase) {
  MOZ_ASSERT(aDatabaseFilenameBase.IsEmpty());

  // WARNING: do not change this hash function. See the comment in HashName()
  // for details.
  aDatabaseFilenameBase.AppendInt(HashName(aDatabaseName));

  nsCString escapedName;
  if (!NS_Escape(NS_ConvertUTF16toUTF8(aDatabaseName), escapedName,
                 url_XPAlphas)) {
    MOZ_CRASH("Can't escape database name!");
  }

  const char* forwardIter = escapedName.BeginReading();
  const char* backwardIter = escapedName.EndReading() - 1;

  nsAutoCString substring;
  while (forwardIter <= backwardIter && substring.Length() < 21) {
    if (substring.Length() % 2) {
      substring.Append(*backwardIter--);
    } else {
      substring.Append(*forwardIter++);
    }
  }

  aDatabaseFilenameBase.AppendASCII(substring.get(), substring.Length());
}

uint32_t CompressedByteCountForNumber(uint64_t aNumber) {
  // All bytes have 7 bits available.
  uint32_t count = 1;
  while ((aNumber >>= 7)) {
    count++;
  }

  return count;
}

uint32_t CompressedByteCountForIndexId(IndexOrObjectStoreId aIndexId) {
  MOZ_ASSERT(aIndexId);
  MOZ_ASSERT(UINT64_MAX - uint64_t(aIndexId) >= uint64_t(aIndexId),
             "Overflow!");

  return CompressedByteCountForNumber(uint64_t(aIndexId * 2));
}

void WriteCompressedNumber(uint64_t aNumber, uint8_t** aIterator) {
  MOZ_ASSERT(aIterator);
  MOZ_ASSERT(*aIterator);

  uint8_t*& buffer = *aIterator;

#ifdef DEBUG
  const uint8_t* const bufferStart = buffer;
  const uint64_t originalNumber = aNumber;
#endif

  while (true) {
    uint64_t shiftedNumber = aNumber >> 7;
    if (shiftedNumber) {
      *buffer++ = uint8_t(0x80 | (aNumber & 0x7f));
      aNumber = shiftedNumber;
    } else {
      *buffer++ = uint8_t(aNumber);
      break;
    }
  }

  MOZ_ASSERT(buffer > bufferStart);
  MOZ_ASSERT(uint32_t(buffer - bufferStart) ==
             CompressedByteCountForNumber(originalNumber));
}

Result<std::pair<uint64_t, mozilla::Span<const uint8_t>>, nsresult>
ReadCompressedNumber(const Span<const uint8_t> aSpan) {
  uint8_t shiftCounter = 0;
  uint64_t result = 0;

  const auto end = aSpan.cend();

  const auto newPos =
      std::find_if(aSpan.cbegin(), end, [&result, &shiftCounter](uint8_t byte) {
        MOZ_ASSERT(shiftCounter <= 56, "Shifted too many bits!");

        result += (uint64_t(byte & 0x7f) << shiftCounter);
        shiftCounter += 7;

        return !(byte & 0x80);
      });

  if (NS_WARN_IF(newPos == end)) {
    MOZ_ASSERT(false);
    IDB_REPORT_INTERNAL_ERR();
    return Err(NS_ERROR_FILE_CORRUPTED);
  }

  return std::pair{result, Span{newPos + 1, end}};
}

void WriteCompressedIndexId(IndexOrObjectStoreId aIndexId, bool aUnique,
                            uint8_t** aIterator) {
  MOZ_ASSERT(aIndexId);
  MOZ_ASSERT(UINT64_MAX - uint64_t(aIndexId) >= uint64_t(aIndexId),
             "Overflow!");
  MOZ_ASSERT(aIterator);
  MOZ_ASSERT(*aIterator);

  const uint64_t indexId = (uint64_t(aIndexId * 2) | (aUnique ? 1 : 0));
  WriteCompressedNumber(indexId, aIterator);
}

Result<std::tuple<IndexOrObjectStoreId, bool, Span<const uint8_t>>, nsresult>
ReadCompressedIndexId(const Span<const uint8_t> aData) {
  auto readNumberOrErr = ReadCompressedNumber(aData);
  if (NS_WARN_IF(readNumberOrErr.isErr())) {
    return readNumberOrErr.propagateErr();
  }

  const auto [indexId, remainder] = readNumberOrErr.unwrap();

  MOZ_ASSERT(UINT64_MAX / 2 >= uint64_t(indexId), "Bad index id!");

  return std::tuple{IndexOrObjectStoreId(indexId >> 1), indexId % 2 == 1,
                    remainder};
}

// static
nsresult MakeCompressedIndexDataValues(
    const nsTArray<IndexDataValue>& aIndexValues,
    UniqueFreePtr<uint8_t>& aCompressedIndexDataValues,
    uint32_t* aCompressedIndexDataValuesLength) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());
  MOZ_ASSERT(!aCompressedIndexDataValues);
  MOZ_ASSERT(aCompressedIndexDataValuesLength);

  AUTO_PROFILER_LABEL("MakeCompressedIndexDataValues", DOM);

  const uint32_t arrayLength = aIndexValues.Length();
  if (!arrayLength) {
    *aCompressedIndexDataValuesLength = 0;
    return NS_OK;
  }

  // First calculate the size of the final buffer.
  uint32_t blobDataLength = 0;

  for (const IndexDataValue& info : aIndexValues) {
    const nsCString& keyBuffer = info.mPosition.GetBuffer();
    const nsCString& sortKeyBuffer = info.mLocaleAwarePosition.GetBuffer();
    const uint32_t keyBufferLength = keyBuffer.Length();
    const uint32_t sortKeyBufferLength = sortKeyBuffer.Length();

    MOZ_ASSERT(!keyBuffer.IsEmpty());

    const CheckedUint32 infoLength =
        CheckedUint32(CompressedByteCountForIndexId(info.mIndexId)) +
        CompressedByteCountForNumber(keyBufferLength) +
        CompressedByteCountForNumber(sortKeyBufferLength) + keyBufferLength +
        sortKeyBufferLength;
    // Don't let |infoLength| overflow.
    if (NS_WARN_IF(!infoLength.isValid())) {
      IDB_REPORT_INTERNAL_ERR();
      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
    }

    // Don't let |blobDataLength| overflow.
    if (NS_WARN_IF(UINT32_MAX - infoLength.value() < blobDataLength)) {
      IDB_REPORT_INTERNAL_ERR();
      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
    }

    blobDataLength += infoLength.value();
  }

  UniqueFreePtr<uint8_t> blobData(
      static_cast<uint8_t*>(malloc(blobDataLength)));
  if (NS_WARN_IF(!blobData)) {
    IDB_REPORT_INTERNAL_ERR();
    return NS_ERROR_OUT_OF_MEMORY;
  }

  uint8_t* blobDataIter = blobData.get();

  for (const IndexDataValue& info : aIndexValues) {
    const nsCString& keyBuffer = info.mPosition.GetBuffer();
    const nsCString& sortKeyBuffer = info.mLocaleAwarePosition.GetBuffer();
    const uint32_t keyBufferLength = keyBuffer.Length();
    const uint32_t sortKeyBufferLength = sortKeyBuffer.Length();

    WriteCompressedIndexId(info.mIndexId, info.mUnique, &blobDataIter);
    WriteCompressedNumber(keyBufferLength, &blobDataIter);

    memcpy(blobDataIter, keyBuffer.get(), keyBufferLength);
    blobDataIter += keyBufferLength;

    WriteCompressedNumber(sortKeyBufferLength, &blobDataIter);

    memcpy(blobDataIter, sortKeyBuffer.get(), sortKeyBufferLength);
    blobDataIter += sortKeyBufferLength;
  }

  MOZ_ASSERT(blobDataIter == blobData.get() + blobDataLength);

  aCompressedIndexDataValues = std::move(blobData);
  *aCompressedIndexDataValuesLength = uint32_t(blobDataLength);

  return NS_OK;
}

// aOutIndexValues is an output parameter, since its storage is reused.
nsresult ReadCompressedIndexDataValuesFromBlob(
    const Span<const uint8_t> aBlobData,
    nsTArray<IndexDataValue>& aOutIndexValues) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());
  MOZ_ASSERT(!aBlobData.IsEmpty());
  MOZ_ASSERT(aOutIndexValues.IsEmpty());

  AUTO_PROFILER_LABEL("ReadCompressedIndexDataValuesFromBlob", DOM);

  // XXX Is this check still necessary with a Span? Or should it rather be moved
  // to the caller?
  if (uintptr_t(aBlobData.Elements()) > UINTPTR_MAX - aBlobData.LengthBytes()) {
    IDB_REPORT_INTERNAL_ERR();
    return NS_ERROR_FILE_CORRUPTED;
  }

  for (auto remainder = aBlobData; !remainder.IsEmpty();) {
    auto readIndexIdOrErr = ReadCompressedIndexId(remainder);
    if (NS_WARN_IF(readIndexIdOrErr.isErr())) {
      return readIndexIdOrErr.unwrapErr();
    }

    const auto [indexId, unique, remainderAfterIndexId] =
        readIndexIdOrErr.unwrap();

    if (NS_WARN_IF(remainderAfterIndexId.IsEmpty())) {
      IDB_REPORT_INTERNAL_ERR();
      return NS_ERROR_FILE_CORRUPTED;
    }

    // Read key buffer length.
    auto readNumberOrErr = ReadCompressedNumber(remainderAfterIndexId);
    if (NS_WARN_IF(readNumberOrErr.isErr())) {
      return readNumberOrErr.unwrapErr();
    }

    const auto [keyBufferLength, remainderAfterKeyBufferLength] =
        readNumberOrErr.unwrap();

    if (NS_WARN_IF(remainderAfterKeyBufferLength.IsEmpty()) ||
        NS_WARN_IF(keyBufferLength > uint64_t(UINT32_MAX)) ||
        NS_WARN_IF(keyBufferLength > remainderAfterKeyBufferLength.Length())) {
      IDB_REPORT_INTERNAL_ERR();
      return NS_ERROR_FILE_CORRUPTED;
    }

    const auto [keyBuffer, remainderAfterKeyBuffer] =
        remainderAfterKeyBufferLength.SplitAt(keyBufferLength);
    auto idv =
        IndexDataValue{indexId, unique, Key{nsCString{AsChars(keyBuffer)}}};

    // Read sort key buffer length.
    readNumberOrErr = ReadCompressedNumber(remainderAfterKeyBuffer);
    if (NS_WARN_IF(readNumberOrErr.isErr())) {
      return readNumberOrErr.unwrapErr();
    }

    const auto [sortKeyBufferLength, remainderAfterSortKeyBufferLength] =
        readNumberOrErr.unwrap();

    remainder = remainderAfterSortKeyBufferLength;
    if (sortKeyBufferLength > 0) {
      if (NS_WARN_IF(remainder.IsEmpty()) ||
          NS_WARN_IF(sortKeyBufferLength > uint64_t(UINT32_MAX)) ||
          NS_WARN_IF(sortKeyBufferLength > remainder.Length())) {
        IDB_REPORT_INTERNAL_ERR();
        return NS_ERROR_FILE_CORRUPTED;
      }

      const auto [sortKeyBuffer, remainderAfterSortKeyBuffer] =
          remainder.SplitAt(sortKeyBufferLength);
      idv.mLocaleAwarePosition = Key{nsCString{AsChars(sortKeyBuffer)}};
      remainder = remainderAfterSortKeyBuffer;
    }

    if (NS_WARN_IF(!aOutIndexValues.AppendElement(std::move(idv), fallible))) {
      IDB_REPORT_INTERNAL_ERR();
      return NS_ERROR_OUT_OF_MEMORY;
    }
  }
  aOutIndexValues.Sort();

  return NS_OK;
}

// aOutIndexValues is an output parameter, since its storage is reused.
template <typename T>
nsresult ReadCompressedIndexDataValuesFromSource(
    T& aSource, uint32_t aColumnIndex,
    nsTArray<IndexDataValue>& aOutIndexValues) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());
  MOZ_ASSERT(aOutIndexValues.IsEmpty());

  int32_t columnType;
  nsresult rv = aSource.GetTypeOfIndex(aColumnIndex, &columnType);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (columnType == mozIStorageStatement::VALUE_TYPE_NULL) {
    return NS_OK;
  }

  MOZ_ASSERT(columnType == mozIStorageStatement::VALUE_TYPE_BLOB);

  const uint8_t* blobData;
  uint32_t blobDataLength;
  rv = aSource.GetSharedBlob(aColumnIndex, &blobDataLength, &blobData);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (NS_WARN_IF(!blobDataLength)) {
    IDB_REPORT_INTERNAL_ERR();
    return NS_ERROR_FILE_CORRUPTED;
  }

  rv = ReadCompressedIndexDataValuesFromBlob(Span(blobData, blobDataLength),
                                             aOutIndexValues);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

// aOutIndexValues is an output parameter, since its storage is reused.
nsresult ReadCompressedIndexDataValues(
    mozIStorageStatement& aStatement, uint32_t aColumnIndex,
    nsTArray<IndexDataValue>& aOutIndexValues) {
  return ReadCompressedIndexDataValuesFromSource(aStatement, aColumnIndex,
                                                 aOutIndexValues);
}

using IndexDataValuesAutoArray = AutoTArray<IndexDataValue, 32>;
Result<IndexDataValuesAutoArray, nsresult> ReadCompressedIndexDataValues(
    mozIStorageValueArray& aValues, uint32_t aColumnIndex) {
  IndexDataValuesAutoArray result;
  const nsresult rv =
      ReadCompressedIndexDataValuesFromSource(aValues, aColumnIndex, result);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  return result;
}

nsresult CreateFileTables(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("CreateFileTables", DOM);

  // Table `file`
  nsresult rv =
      aConnection.ExecuteSimpleSQL(nsLiteralCString("CREATE TABLE file ("
                                                    "id INTEGER PRIMARY KEY, "
                                                    "refcount INTEGER NOT NULL"
                                                    ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TRIGGER object_data_insert_trigger "
                       "AFTER INSERT ON object_data "
                       "FOR EACH ROW "
                       "WHEN NEW.file_ids IS NOT NULL "
                       "BEGIN "
                       "SELECT update_refcount(NULL, NEW.file_ids); "
                       "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TRIGGER object_data_update_trigger "
      "AFTER UPDATE OF file_ids ON object_data "
      "FOR EACH ROW "
      "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL "
      "BEGIN "
      "SELECT update_refcount(OLD.file_ids, NEW.file_ids); "
      "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TRIGGER object_data_delete_trigger "
                       "AFTER DELETE ON object_data "
                       "FOR EACH ROW WHEN OLD.file_ids IS NOT NULL "
                       "BEGIN "
                       "SELECT update_refcount(OLD.file_ids, NULL); "
                       "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TRIGGER file_update_trigger "
                       "AFTER UPDATE ON file "
                       "FOR EACH ROW WHEN NEW.refcount = 0 "
                       "BEGIN "
                       "DELETE FROM file WHERE id = OLD.id; "
                       "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult CreateTables(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("CreateTables", DOM);

  // Table `database`

  // There are two reasons for having the origin column.
  // First, we can ensure that we don't have collisions in the origin hash we
  // use for the path because when we open the db we can make sure that the
  // origins exactly match. Second, chrome code crawling through the idb
  // directory can figure out the origin of every db without having to
  // reverse-engineer our hash scheme.
  nsresult rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TABLE database"
                       "( name TEXT PRIMARY KEY"
                       ", origin TEXT NOT NULL"
                       ", version INTEGER NOT NULL DEFAULT 0"
                       ", last_vacuum_time INTEGER NOT NULL DEFAULT 0"
                       ", last_analyze_time INTEGER NOT NULL DEFAULT 0"
                       ", last_vacuum_size INTEGER NOT NULL DEFAULT 0"
                       ") WITHOUT ROWID;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Table `object_store`
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TABLE object_store"
                       "( id INTEGER PRIMARY KEY"
                       ", auto_increment INTEGER NOT NULL DEFAULT 0"
                       ", name TEXT NOT NULL"
                       ", key_path TEXT"
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Table `object_store_index`
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TABLE object_store_index"
                       "( id INTEGER PRIMARY KEY"
                       ", object_store_id INTEGER NOT NULL"
                       ", name TEXT NOT NULL"
                       ", key_path TEXT NOT NULL"
                       ", unique_index INTEGER NOT NULL"
                       ", multientry INTEGER NOT NULL"
                       ", locale TEXT"
                       ", is_auto_locale BOOLEAN NOT NULL"
                       ", FOREIGN KEY (object_store_id) "
                       "REFERENCES object_store(id) "
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Table `object_data`
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TABLE object_data"
                       "( object_store_id INTEGER NOT NULL"
                       ", key BLOB NOT NULL"
                       ", index_data_values BLOB DEFAULT NULL"
                       ", file_ids TEXT"
                       ", data BLOB NOT NULL"
                       ", PRIMARY KEY (object_store_id, key)"
                       ", FOREIGN KEY (object_store_id) "
                       "REFERENCES object_store(id) "
                       ") WITHOUT ROWID;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Table `index_data`
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TABLE index_data"
                       "( index_id INTEGER NOT NULL"
                       ", value BLOB NOT NULL"
                       ", object_data_key BLOB NOT NULL"
                       ", object_store_id INTEGER NOT NULL"
                       ", value_locale BLOB"
                       ", PRIMARY KEY (index_id, value, object_data_key)"
                       ", FOREIGN KEY (index_id) "
                       "REFERENCES object_store_index(id) "
                       ", FOREIGN KEY (object_store_id, object_data_key) "
                       "REFERENCES object_data(object_store_id, key) "
                       ") WITHOUT ROWID;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE INDEX index_data_value_locale_index "
      "ON index_data (index_id, value_locale, object_data_key, value) "
      "WHERE value_locale IS NOT NULL;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Table `unique_index_data`
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TABLE unique_index_data"
                       "( index_id INTEGER NOT NULL"
                       ", value BLOB NOT NULL"
                       ", object_store_id INTEGER NOT NULL"
                       ", object_data_key BLOB NOT NULL"
                       ", value_locale BLOB"
                       ", PRIMARY KEY (index_id, value)"
                       ", FOREIGN KEY (index_id) "
                       "REFERENCES object_store_index(id) "
                       ", FOREIGN KEY (object_store_id, object_data_key) "
                       "REFERENCES object_data(object_store_id, key) "
                       ") WITHOUT ROWID;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE INDEX unique_index_data_value_locale_index "
      "ON unique_index_data (index_id, value_locale, object_data_key, value) "
      "WHERE value_locale IS NOT NULL;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = CreateFileTables(aConnection);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(kSQLiteSchemaVersion);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom4To5(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom4To5", DOM);

  nsresult rv;

  // All we changed is the type of the version column, so lets try to
  // convert that to an integer, and if we fail, set it to 0.
  nsCOMPtr<mozIStorageStatement> stmt;
  rv = aConnection.CreateStatement(
      nsLiteralCString("SELECT name, version, dataVersion "
                       "FROM database"),
      getter_AddRefs(stmt));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsString name;
  int32_t intVersion;
  int64_t dataVersion;

  {
    mozStorageStatementScoper scoper(stmt);

    bool hasResults;
    rv = stmt->ExecuteStep(&hasResults);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
    if (NS_WARN_IF(!hasResults)) {
      return NS_ERROR_FAILURE;
    }

    nsString version;
    rv = stmt->GetString(1, version);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    intVersion = version.ToInteger(&rv);
    if (NS_FAILED(rv)) {
      intVersion = 0;
    }

    rv = stmt->GetString(0, name);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    rv = stmt->GetInt64(2, &dataVersion);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE database"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TABLE database ("
                       "name TEXT NOT NULL, "
                       "version INTEGER NOT NULL DEFAULT 0, "
                       "dataVersion INTEGER NOT NULL"
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // The parameter names are not used, parameters are bound by index only
  // locally in the same function.
  rv = aConnection.CreateStatement(
      nsLiteralCString("INSERT INTO database (name, version, dataVersion) "
                       "VALUES (:name, :version, :dataVersion)"),
      getter_AddRefs(stmt));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  {
    mozStorageStatementScoper scoper(stmt);

    rv = stmt->BindStringByIndex(0, name);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    rv = stmt->BindInt32ByIndex(1, intVersion);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    rv = stmt->BindInt64ByIndex(2, dataVersion);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    rv = stmt->Execute();
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }

  rv = aConnection.SetSchemaVersion(5);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom5To6(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom5To6", DOM);

  // First, drop all the indexes we're no longer going to use.
  nsresult rv = aConnection.ExecuteSimpleSQL("DROP INDEX key_index;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP INDEX ai_key_index;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP INDEX value_index;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP INDEX ai_value_index;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Now, reorder the columns of object_data to put the blob data last. We do
  // this by copying into a temporary table, dropping the original, then copying
  // back into a newly created table.
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "id INTEGER PRIMARY KEY, "
                       "object_store_id, "
                       "key_value, "
                       "data "
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO temp_upgrade "
                       "SELECT id, object_store_id, key_value, data "
                       "FROM object_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE object_data ("
      "id INTEGER PRIMARY KEY, "
      "object_store_id INTEGER NOT NULL, "
      "key_value DEFAULT NULL, "
      "data BLOB NOT NULL, "
      "UNIQUE (object_store_id, key_value), "
      "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO object_data "
                       "SELECT id, object_store_id, key_value, data "
                       "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // We need to add a unique constraint to our ai_object_data table. Copy all
  // the data out of it using a temporary table as before.
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "id INTEGER PRIMARY KEY, "
                       "object_store_id, "
                       "data "
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO temp_upgrade "
                       "SELECT id, object_store_id, data "
                       "FROM ai_object_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_object_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE ai_object_data ("
      "id INTEGER PRIMARY KEY AUTOINCREMENT, "
      "object_store_id INTEGER NOT NULL, "
      "data BLOB NOT NULL, "
      "UNIQUE (object_store_id, id), "
      "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO ai_object_data "
                       "SELECT id, object_store_id, data "
                       "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Fix up the index_data table. We're reordering the columns as well as
  // changing the primary key from being a simple id to being a composite.
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "index_id, "
                       "value, "
                       "object_data_key, "
                       "object_data_id "
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO temp_upgrade "
      "SELECT index_id, value, object_data_key, object_data_id "
      "FROM index_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE index_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE index_data ("
      "index_id INTEGER NOT NULL, "
      "value NOT NULL, "
      "object_data_key NOT NULL, "
      "object_data_id INTEGER NOT NULL, "
      "PRIMARY KEY (index_id, value, object_data_key), "
      "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE "
      "CASCADE, "
      "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT OR IGNORE INTO index_data "
      "SELECT index_id, value, object_data_key, object_data_id "
      "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE INDEX index_data_object_data_id_index "
                       "ON index_data (object_data_id);"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Fix up the unique_index_data table. We're reordering the columns as well as
  // changing the primary key from being a simple id to being a composite.
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "index_id, "
                       "value, "
                       "object_data_key, "
                       "object_data_id "
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO temp_upgrade "
      "SELECT index_id, value, object_data_key, object_data_id "
      "FROM unique_index_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE unique_index_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE unique_index_data ("
      "index_id INTEGER NOT NULL, "
      "value NOT NULL, "
      "object_data_key NOT NULL, "
      "object_data_id INTEGER NOT NULL, "
      "PRIMARY KEY (index_id, value, object_data_key), "
      "UNIQUE (index_id, value), "
      "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE "
      "CASCADE "
      "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO unique_index_data "
      "SELECT index_id, value, object_data_key, object_data_id "
      "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE INDEX unique_index_data_object_data_id_index "
                       "ON unique_index_data (object_data_id);"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Fix up the ai_index_data table. We're reordering the columns as well as
  // changing the primary key from being a simple id to being a composite.
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "index_id, "
                       "value, "
                       "ai_object_data_id "
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO temp_upgrade "
                       "SELECT index_id, value, ai_object_data_id "
                       "FROM ai_index_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_index_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE ai_index_data ("
      "index_id INTEGER NOT NULL, "
      "value NOT NULL, "
      "ai_object_data_id INTEGER NOT NULL, "
      "PRIMARY KEY (index_id, value, ai_object_data_id), "
      "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE "
      "CASCADE, "
      "FOREIGN KEY (ai_object_data_id) REFERENCES ai_object_data(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT OR IGNORE INTO ai_index_data "
                       "SELECT index_id, value, ai_object_data_id "
                       "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE INDEX ai_index_data_ai_object_data_id_index "
                       "ON ai_index_data (ai_object_data_id);"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Fix up the ai_unique_index_data table. We're reordering the columns as well
  // as changing the primary key from being a simple id to being a composite.
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "index_id, "
                       "value, "
                       "ai_object_data_id "
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO temp_upgrade "
                       "SELECT index_id, value, ai_object_data_id "
                       "FROM ai_unique_index_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_unique_index_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE ai_unique_index_data ("
      "index_id INTEGER NOT NULL, "
      "value NOT NULL, "
      "ai_object_data_id INTEGER NOT NULL, "
      "UNIQUE (index_id, value), "
      "PRIMARY KEY (index_id, value, ai_object_data_id), "
      "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE "
      "CASCADE, "
      "FOREIGN KEY (ai_object_data_id) REFERENCES ai_object_data(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO ai_unique_index_data "
                       "SELECT index_id, value, ai_object_data_id "
                       "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE INDEX ai_unique_index_data_ai_object_data_id_index "
      "ON ai_unique_index_data (ai_object_data_id);"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(6);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom6To7(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom6To7", DOM);

  nsresult rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "id, "
                       "name, "
                       "key_path, "
                       "auto_increment"
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO temp_upgrade "
                       "SELECT id, name, key_path, auto_increment "
                       "FROM object_store;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_store;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TABLE object_store ("
                       "id INTEGER PRIMARY KEY, "
                       "auto_increment INTEGER NOT NULL DEFAULT 0, "
                       "name TEXT NOT NULL, "
                       "key_path TEXT, "
                       "UNIQUE (name)"
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO object_store "
                       "SELECT id, auto_increment, name, nullif(key_path, '') "
                       "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(7);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom7To8(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom7To8", DOM);

  nsresult rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "id, "
                       "object_store_id, "
                       "name, "
                       "key_path, "
                       "unique_index, "
                       "object_store_autoincrement"
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO temp_upgrade "
                       "SELECT id, object_store_id, name, key_path, "
                       "unique_index, object_store_autoincrement "
                       "FROM object_store_index;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_store_index;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE object_store_index ("
      "id INTEGER, "
      "object_store_id INTEGER NOT NULL, "
      "name TEXT NOT NULL, "
      "key_path TEXT NOT NULL, "
      "unique_index INTEGER NOT NULL, "
      "multientry INTEGER NOT NULL, "
      "object_store_autoincrement INTERGER NOT NULL, "
      "PRIMARY KEY (id), "
      "UNIQUE (object_store_id, name), "
      "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO object_store_index "
                       "SELECT id, object_store_id, name, key_path, "
                       "unique_index, 0, object_store_autoincrement "
                       "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(8);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

class CompressDataBlobsFunction final : public mozIStorageFunction {
 public:
  NS_DECL_ISUPPORTS

 private:
  ~CompressDataBlobsFunction() = default;

  NS_IMETHOD
  OnFunctionCall(mozIStorageValueArray* aArguments,
                 nsIVariant** aResult) override {
    MOZ_ASSERT(aArguments);
    MOZ_ASSERT(aResult);

    AUTO_PROFILER_LABEL("CompressDataBlobsFunction::OnFunctionCall", DOM);

    uint32_t argc;
    nsresult rv = aArguments->GetNumEntries(&argc);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    if (argc != 1) {
      NS_WARNING("Don't call me with the wrong number of arguments!");
      return NS_ERROR_UNEXPECTED;
    }

    int32_t type;
    rv = aArguments->GetTypeOfIndex(0, &type);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    if (type != mozIStorageStatement::VALUE_TYPE_BLOB) {
      NS_WARNING("Don't call me with the wrong type of arguments!");
      return NS_ERROR_UNEXPECTED;
    }

    const uint8_t* uncompressed;
    uint32_t uncompressedLength;
    rv = aArguments->GetSharedBlob(0, &uncompressedLength, &uncompressed);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    size_t compressedLength = snappy::MaxCompressedLength(uncompressedLength);
    UniqueFreePtr<uint8_t> compressed(
        static_cast<uint8_t*>(malloc(compressedLength)));
    if (NS_WARN_IF(!compressed)) {
      return NS_ERROR_OUT_OF_MEMORY;
    }

    snappy::RawCompress(
        reinterpret_cast<const char*>(uncompressed), uncompressedLength,
        reinterpret_cast<char*>(compressed.get()), &compressedLength);

    std::pair<uint8_t*, int> data(compressed.release(), int(compressedLength));

    nsCOMPtr<nsIVariant> result =
        new mozilla::storage::AdoptedBlobVariant(data);

    result.forget(aResult);
    return NS_OK;
  }
};

nsresult UpgradeSchemaFrom8To9_0(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom8To9_0", DOM);

  // We no longer use the dataVersion column.
  nsresult rv =
      aConnection.ExecuteSimpleSQL("UPDATE database SET dataVersion = 0;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsCOMPtr<mozIStorageFunction> compressor = new CompressDataBlobsFunction();

  constexpr auto compressorName = "compress"_ns;

  rv = aConnection.CreateFunction(compressorName, 1, compressor);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Turn off foreign key constraints before we do anything here.
  rv = aConnection.ExecuteSimpleSQL(
      "UPDATE object_data SET data = compress(data);"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      "UPDATE ai_object_data SET data = compress(data);"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.RemoveFunction(compressorName);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(9, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom9_0To10_0(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom9_0To10_0", DOM);

  nsresult rv = aConnection.ExecuteSimpleSQL(
      "ALTER TABLE object_data ADD COLUMN file_ids TEXT;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      "ALTER TABLE ai_object_data ADD COLUMN file_ids TEXT;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = CreateFileTables(aConnection);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(10, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom10_0To11_0(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom10_0To11_0", DOM);

  nsresult rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "id, "
                       "object_store_id, "
                       "name, "
                       "key_path, "
                       "unique_index, "
                       "multientry"
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO temp_upgrade "
                       "SELECT id, object_store_id, name, key_path, "
                       "unique_index, multientry "
                       "FROM object_store_index;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_store_index;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE object_store_index ("
      "id INTEGER PRIMARY KEY, "
      "object_store_id INTEGER NOT NULL, "
      "name TEXT NOT NULL, "
      "key_path TEXT NOT NULL, "
      "unique_index INTEGER NOT NULL, "
      "multientry INTEGER NOT NULL, "
      "UNIQUE (object_store_id, name), "
      "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO object_store_index "
                       "SELECT id, object_store_id, name, key_path, "
                       "unique_index, multientry "
                       "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      "DROP TRIGGER object_data_insert_trigger;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO object_data (object_store_id, key_value, data, file_ids) "
      "SELECT object_store_id, id, data, file_ids "
      "FROM ai_object_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TRIGGER object_data_insert_trigger "
                       "AFTER INSERT ON object_data "
                       "FOR EACH ROW "
                       "WHEN NEW.file_ids IS NOT NULL "
                       "BEGIN "
                       "SELECT update_refcount(NULL, NEW.file_ids); "
                       "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO index_data (index_id, value, object_data_key, "
      "object_data_id) "
      "SELECT ai_index_data.index_id, ai_index_data.value, "
      "ai_index_data.ai_object_data_id, object_data.id "
      "FROM ai_index_data "
      "INNER JOIN object_store_index ON "
      "object_store_index.id = ai_index_data.index_id "
      "INNER JOIN object_data ON "
      "object_data.object_store_id = object_store_index.object_store_id AND "
      "object_data.key_value = ai_index_data.ai_object_data_id;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO unique_index_data (index_id, value, object_data_key, "
      "object_data_id) "
      "SELECT ai_unique_index_data.index_id, ai_unique_index_data.value, "
      "ai_unique_index_data.ai_object_data_id, object_data.id "
      "FROM ai_unique_index_data "
      "INNER JOIN object_store_index ON "
      "object_store_index.id = ai_unique_index_data.index_id "
      "INNER JOIN object_data ON "
      "object_data.object_store_id = object_store_index.object_store_id AND "
      "object_data.key_value = ai_unique_index_data.ai_object_data_id;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "UPDATE object_store "
      "SET auto_increment = (SELECT max(id) FROM ai_object_data) + 1 "
      "WHERE auto_increment;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_unique_index_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_index_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE ai_object_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(11, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

class EncodeKeysFunction final : public mozIStorageFunction {
 public:
  NS_DECL_ISUPPORTS

 private:
  ~EncodeKeysFunction() = default;

  NS_IMETHOD
  OnFunctionCall(mozIStorageValueArray* aArguments,
                 nsIVariant** aResult) override {
    MOZ_ASSERT(aArguments);
    MOZ_ASSERT(aResult);

    AUTO_PROFILER_LABEL("EncodeKeysFunction::OnFunctionCall", DOM);

    uint32_t argc;
    nsresult rv = aArguments->GetNumEntries(&argc);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    if (argc != 1) {
      NS_WARNING("Don't call me with the wrong number of arguments!");
      return NS_ERROR_UNEXPECTED;
    }

    int32_t type;
    rv = aArguments->GetTypeOfIndex(0, &type);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    Key key;
    if (type == mozIStorageStatement::VALUE_TYPE_INTEGER) {
      int64_t intKey;
      aArguments->GetInt64(0, &intKey);
      key.SetFromInteger(intKey);
    } else if (type == mozIStorageStatement::VALUE_TYPE_TEXT) {
      nsString stringKey;
      aArguments->GetString(0, stringKey);
      auto result = key.SetFromString(stringKey);
      if (!result.Is(Ok)) {
        return result.Is(SpecialValues::Invalid)
                   ? NS_ERROR_DOM_INDEXEDDB_DATA_ERR
                   : result.AsException().StealNSResult();
      }
    } else {
      NS_WARNING("Don't call me with the wrong type of arguments!");
      return NS_ERROR_UNEXPECTED;
    }

    const nsCString& buffer = key.GetBuffer();

    std::pair<const void*, int> data(static_cast<const void*>(buffer.get()),
                                     int(buffer.Length()));

    nsCOMPtr<nsIVariant> result = new mozilla::storage::BlobVariant(data);

    result.forget(aResult);
    return NS_OK;
  }
};

nsresult UpgradeSchemaFrom11_0To12_0(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom11_0To12_0", DOM);

  constexpr auto encoderName = "encode"_ns;

  nsCOMPtr<mozIStorageFunction> encoder = new EncodeKeysFunction();

  nsresult rv = aConnection.CreateFunction(encoderName, 1, encoder);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "id INTEGER PRIMARY KEY, "
                       "object_store_id, "
                       "key_value, "
                       "data, "
                       "file_ids "
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO temp_upgrade "
      "SELECT id, object_store_id, encode(key_value), data, file_ids "
      "FROM object_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE object_data ("
      "id INTEGER PRIMARY KEY, "
      "object_store_id INTEGER NOT NULL, "
      "key_value BLOB DEFAULT NULL, "
      "file_ids TEXT, "
      "data BLOB NOT NULL, "
      "UNIQUE (object_store_id, key_value), "
      "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO object_data "
                       "SELECT id, object_store_id, key_value, file_ids, data "
                       "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TRIGGER object_data_insert_trigger "
                       "AFTER INSERT ON object_data "
                       "FOR EACH ROW "
                       "WHEN NEW.file_ids IS NOT NULL "
                       "BEGIN "
                       "SELECT update_refcount(NULL, NEW.file_ids); "
                       "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TRIGGER object_data_update_trigger "
      "AFTER UPDATE OF file_ids ON object_data "
      "FOR EACH ROW "
      "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL "
      "BEGIN "
      "SELECT update_refcount(OLD.file_ids, NEW.file_ids); "
      "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TRIGGER object_data_delete_trigger "
                       "AFTER DELETE ON object_data "
                       "FOR EACH ROW WHEN OLD.file_ids IS NOT NULL "
                       "BEGIN "
                       "SELECT update_refcount(OLD.file_ids, NULL); "
                       "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "index_id, "
                       "value, "
                       "object_data_key, "
                       "object_data_id "
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO temp_upgrade "
      "SELECT index_id, encode(value), encode(object_data_key), object_data_id "
      "FROM index_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE index_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE index_data ("
      "index_id INTEGER NOT NULL, "
      "value BLOB NOT NULL, "
      "object_data_key BLOB NOT NULL, "
      "object_data_id INTEGER NOT NULL, "
      "PRIMARY KEY (index_id, value, object_data_key), "
      "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE "
      "CASCADE, "
      "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO index_data "
      "SELECT index_id, value, object_data_key, object_data_id "
      "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE INDEX index_data_object_data_id_index "
                       "ON index_data (object_data_id);"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TEMPORARY TABLE temp_upgrade ("
                       "index_id, "
                       "value, "
                       "object_data_key, "
                       "object_data_id "
                       ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO temp_upgrade "
      "SELECT index_id, encode(value), encode(object_data_key), object_data_id "
      "FROM unique_index_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE unique_index_data;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TABLE unique_index_data ("
      "index_id INTEGER NOT NULL, "
      "value BLOB NOT NULL, "
      "object_data_key BLOB NOT NULL, "
      "object_data_id INTEGER NOT NULL, "
      "PRIMARY KEY (index_id, value, object_data_key), "
      "UNIQUE (index_id, value), "
      "FOREIGN KEY (index_id) REFERENCES object_store_index(id) ON DELETE "
      "CASCADE "
      "FOREIGN KEY (object_data_id) REFERENCES object_data(id) ON DELETE "
      "CASCADE"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "INSERT INTO unique_index_data "
      "SELECT index_id, value, object_data_key, object_data_id "
      "FROM temp_upgrade;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE temp_upgrade;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE INDEX unique_index_data_object_data_id_index "
                       "ON unique_index_data (object_data_id);"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.RemoveFunction(encoderName);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(12, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom12_0To13_0(mozIStorageConnection& aConnection,
                                     bool* aVacuumNeeded) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom12_0To13_0", DOM);

  nsresult rv;

#ifdef IDB_MOBILE
  int32_t defaultPageSize;
  rv = aConnection.GetDefaultPageSize(&defaultPageSize);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Enable auto_vacuum mode and update the page size to the platform default.
  nsAutoCString upgradeQuery("PRAGMA auto_vacuum = FULL; PRAGMA page_size = ");
  upgradeQuery.AppendInt(defaultPageSize);

  rv = aConnection.ExecuteSimpleSQL(upgradeQuery);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  *aVacuumNeeded = true;
#endif

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(13, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom13_0To14_0(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  // The only change between 13 and 14 was a different structured
  // clone format, but it's backwards-compatible.
  nsresult rv = aConnection.SetSchemaVersion(MakeSchemaVersion(14, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom14_0To15_0(mozIStorageConnection& aConnection) {
  // The only change between 14 and 15 was a different structured
  // clone format, but it's backwards-compatible.
  nsresult rv = aConnection.SetSchemaVersion(MakeSchemaVersion(15, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom15_0To16_0(mozIStorageConnection& aConnection) {
  // The only change between 15 and 16 was a different structured
  // clone format, but it's backwards-compatible.
  nsresult rv = aConnection.SetSchemaVersion(MakeSchemaVersion(16, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom16_0To17_0(mozIStorageConnection& aConnection) {
  // The only change between 16 and 17 was a different structured
  // clone format, but it's backwards-compatible.
  nsresult rv = aConnection.SetSchemaVersion(MakeSchemaVersion(17, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

class UpgradeSchemaFrom17_0To18_0Helper final {
  class InsertIndexDataValuesFunction;
  class UpgradeKeyFunction;

 public:
  static nsresult DoUpgrade(mozIStorageConnection& aConnection,
                            const nsACString& aOrigin);

 private:
  static nsresult DoUpgradeInternal(mozIStorageConnection& aConnection,
                                    const nsACString& aOrigin);

  UpgradeSchemaFrom17_0To18_0Helper() = delete;
  ~UpgradeSchemaFrom17_0To18_0Helper() = delete;
};

class UpgradeSchemaFrom17_0To18_0Helper::InsertIndexDataValuesFunction final
    : public mozIStorageFunction {
 public:
  InsertIndexDataValuesFunction() = default;

  NS_DECL_ISUPPORTS

 private:
  ~InsertIndexDataValuesFunction() = default;

  NS_DECL_MOZISTORAGEFUNCTION
};

NS_IMPL_ISUPPORTS(
    UpgradeSchemaFrom17_0To18_0Helper::InsertIndexDataValuesFunction,
    mozIStorageFunction);

NS_IMETHODIMP
UpgradeSchemaFrom17_0To18_0Helper::InsertIndexDataValuesFunction::
    OnFunctionCall(mozIStorageValueArray* aValues, nsIVariant** _retval) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());
  MOZ_ASSERT(aValues);
  MOZ_ASSERT(_retval);

#ifdef DEBUG
  {
    uint32_t argCount;
    MOZ_ALWAYS_SUCCEEDS(aValues->GetNumEntries(&argCount));
    MOZ_ASSERT(argCount == 4);

    int32_t valueType;
    MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(0, &valueType));
    MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_NULL ||
               valueType == mozIStorageValueArray::VALUE_TYPE_BLOB);

    MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(1, &valueType));
    MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_INTEGER);

    MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(2, &valueType));
    MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_INTEGER);

    MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(3, &valueType));
    MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB);
  }
#endif

  // Read out the previous value. It may be NULL, in which case we'll just end
  // up with an empty array.
  auto indexValuesOrErr = ReadCompressedIndexDataValues(*aValues, 0);
  if (NS_WARN_IF(indexValuesOrErr.isErr())) {
    return indexValuesOrErr.unwrapErr();
  }

  IndexOrObjectStoreId indexId;
  nsresult rv = aValues->GetInt64(1, &indexId);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  int32_t unique;
  rv = aValues->GetInt32(2, &unique);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  Key value;
  rv = value.SetFromValueArray(aValues, 3);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  auto indexValues = indexValuesOrErr.unwrap();

  // Update the array with the new addition.
  if (NS_WARN_IF(!indexValues.InsertElementSorted(
          IndexDataValue(indexId, !!unique, value), fallible))) {
    IDB_REPORT_INTERNAL_ERR();
    return NS_ERROR_OUT_OF_MEMORY;
  }

  // Compress the array.
  UniqueFreePtr<uint8_t> indexValuesBlob;
  uint32_t indexValuesBlobLength;
  rv = MakeCompressedIndexDataValues(indexValues, indexValuesBlob,
                                     &indexValuesBlobLength);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // The compressed blob is the result of this function.
  std::pair<uint8_t*, int> indexValuesBlobPair(indexValuesBlob.release(),
                                               indexValuesBlobLength);

  nsCOMPtr<nsIVariant> result =
      new storage::AdoptedBlobVariant(indexValuesBlobPair);

  result.forget(_retval);
  return NS_OK;
}

class UpgradeSchemaFrom17_0To18_0Helper::UpgradeKeyFunction final
    : public mozIStorageFunction {
 public:
  UpgradeKeyFunction() = default;

  static nsresult CopyAndUpgradeKeyBuffer(const uint8_t* aSource,
                                          const uint8_t* aSourceEnd,
                                          uint8_t* aDestination) {
    return CopyAndUpgradeKeyBufferInternal(aSource, aSourceEnd, aDestination,
                                           0 /* aTagOffset */,
                                           0 /* aRecursionDepth */);
  }

  NS_DECL_ISUPPORTS

 private:
  ~UpgradeKeyFunction() = default;

  static nsresult CopyAndUpgradeKeyBufferInternal(const uint8_t*& aSource,
                                                  const uint8_t* aSourceEnd,
                                                  uint8_t*& aDestination,
                                                  uint8_t aTagOffset,
                                                  uint8_t aRecursionDepth);

  static uint32_t AdjustedSize(uint32_t aMaxSize, const uint8_t* aSource,
                               const uint8_t* aSourceEnd) {
    MOZ_ASSERT(aMaxSize);
    MOZ_ASSERT(aSource);
    MOZ_ASSERT(aSourceEnd);
    MOZ_ASSERT(aSource <= aSourceEnd);

    return std::min(aMaxSize, uint32_t(aSourceEnd - aSource));
  }

  NS_DECL_MOZISTORAGEFUNCTION
};

// static
nsresult UpgradeSchemaFrom17_0To18_0Helper::UpgradeKeyFunction::
    CopyAndUpgradeKeyBufferInternal(const uint8_t*& aSource,
                                    const uint8_t* aSourceEnd,
                                    uint8_t*& aDestination, uint8_t aTagOffset,
                                    uint8_t aRecursionDepth) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());
  MOZ_ASSERT(aSource);
  MOZ_ASSERT(*aSource);
  MOZ_ASSERT(aSourceEnd);
  MOZ_ASSERT(aSource < aSourceEnd);
  MOZ_ASSERT(aDestination);
  MOZ_ASSERT(aTagOffset <= Key::kMaxArrayCollapse);

  static constexpr uint8_t kOldNumberTag = 0x1;
  static constexpr uint8_t kOldDateTag = 0x2;
  static constexpr uint8_t kOldStringTag = 0x3;
  static constexpr uint8_t kOldArrayTag = 0x4;
  static constexpr uint8_t kOldMaxType = kOldArrayTag;

  if (NS_WARN_IF(aRecursionDepth > Key::kMaxRecursionDepth)) {
    IDB_REPORT_INTERNAL_ERR();
    return NS_ERROR_FILE_CORRUPTED;
  }

  const uint8_t sourceTag = *aSource - (aTagOffset * kOldMaxType);
  MOZ_ASSERT(sourceTag);

  if (NS_WARN_IF(sourceTag > kOldMaxType * Key::kMaxArrayCollapse)) {
    IDB_REPORT_INTERNAL_ERR();
    return NS_ERROR_FILE_CORRUPTED;
  }

  if (sourceTag == kOldNumberTag || sourceTag == kOldDateTag) {
    // Write the new tag.
    *aDestination++ = (sourceTag == kOldNumberTag ? Key::eFloat : Key::eDate) +
                      (aTagOffset * Key::eMaxType);
    aSource++;

    // Numbers and Dates are encoded as 64-bit integers, but trailing 0
    // bytes have been removed.
    const uint32_t byteCount =
        AdjustedSize(sizeof(uint64_t), aSource, aSourceEnd);

    aDestination = std::copy(aSource, aSource + byteCount, aDestination);
    aSource += byteCount;

    return NS_OK;
  }

  if (sourceTag == kOldStringTag) {
    // Write the new tag.
    *aDestination++ = Key::eString + (aTagOffset * Key::eMaxType);
    aSource++;

    while (aSource < aSourceEnd) {
      const uint8_t byte = *aSource++;
      *aDestination++ = byte;

      if (!byte) {
        // Just copied the terminator.
        break;
      }

      // Maybe copy one or two extra bytes if the byte is tagged and we have
      // enough source space.
      if (byte & 0x80) {
        const uint32_t byteCount =
            AdjustedSize((byte & 0x40) ? 2 : 1, aSource, aSourceEnd);

        aDestination = std::copy(aSource, aSource + byteCount, aDestination);
        aSource += byteCount;
      }
    }

    return NS_OK;
  }

  if (NS_WARN_IF(sourceTag < kOldArrayTag)) {
    IDB_REPORT_INTERNAL_ERR();
    return NS_ERROR_FILE_CORRUPTED;
  }

  aTagOffset++;

  if (aTagOffset == Key::kMaxArrayCollapse) {
    MOZ_ASSERT(sourceTag == kOldArrayTag);

    *aDestination++ = (aTagOffset * Key::eMaxType);
    aSource++;

    aTagOffset = 0;
  }

  while (aSource < aSourceEnd &&
         (*aSource - (aTagOffset * kOldMaxType)) != Key::eTerminator) {
    nsresult rv = CopyAndUpgradeKeyBufferInternal(
        aSource, aSourceEnd, aDestination, aTagOffset, aRecursionDepth + 1);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    aTagOffset = 0;
  }

  if (aSource < aSourceEnd) {
    MOZ_ASSERT((*aSource - (aTagOffset * kOldMaxType)) == Key::eTerminator);
    *aDestination++ = Key::eTerminator + (aTagOffset * Key::eMaxType);
    aSource++;
  }

  return NS_OK;
}

NS_IMPL_ISUPPORTS(UpgradeSchemaFrom17_0To18_0Helper::UpgradeKeyFunction,
                  mozIStorageFunction);

NS_IMETHODIMP
UpgradeSchemaFrom17_0To18_0Helper::UpgradeKeyFunction::OnFunctionCall(
    mozIStorageValueArray* aValues, nsIVariant** _retval) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());
  MOZ_ASSERT(aValues);
  MOZ_ASSERT(_retval);

#ifdef DEBUG
  {
    uint32_t argCount;
    MOZ_ALWAYS_SUCCEEDS(aValues->GetNumEntries(&argCount));
    MOZ_ASSERT(argCount == 1);

    int32_t valueType;
    MOZ_ALWAYS_SUCCEEDS(aValues->GetTypeOfIndex(0, &valueType));
    MOZ_ASSERT(valueType == mozIStorageValueArray::VALUE_TYPE_BLOB);
  }
#endif

  // Dig the old key out of the values.
  const uint8_t* blobData;
  uint32_t blobDataLength;
  nsresult rv = aValues->GetSharedBlob(0, &blobDataLength, &blobData);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Upgrading the key doesn't change the amount of space needed to hold it.
  UniqueFreePtr<uint8_t> upgradedBlobData(
      static_cast<uint8_t*>(malloc(blobDataLength)));
  if (NS_WARN_IF(!upgradedBlobData)) {
    IDB_REPORT_INTERNAL_ERR();
    return NS_ERROR_OUT_OF_MEMORY;
  }

  rv = CopyAndUpgradeKeyBuffer(blobData, blobData + blobDataLength,
                               upgradedBlobData.get());
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // The upgraded key is the result of this function.
  std::pair<uint8_t*, int> data(upgradedBlobData.release(),
                                int(blobDataLength));

  nsCOMPtr<nsIVariant> result = new mozilla::storage::AdoptedBlobVariant(data);

  result.forget(_retval);
  return NS_OK;
}

// static
nsresult UpgradeSchemaFrom17_0To18_0Helper::DoUpgrade(
    mozIStorageConnection& aConnection, const nsACString& aOrigin) {
  MOZ_ASSERT(!aOrigin.IsEmpty());

  // Register the |upgrade_key| function.
  RefPtr<UpgradeKeyFunction> updateFunction = new UpgradeKeyFunction();

  constexpr auto upgradeKeyFunctionName = "upgrade_key"_ns;

  nsresult rv =
      aConnection.CreateFunction(upgradeKeyFunctionName, 1, updateFunction);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Register the |insert_idv| function.
  RefPtr<InsertIndexDataValuesFunction> insertIDVFunction =
      new InsertIndexDataValuesFunction();

  constexpr auto insertIDVFunctionName = "insert_idv"_ns;

  rv = aConnection.CreateFunction(insertIDVFunctionName, 4, insertIDVFunction);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    MOZ_ALWAYS_SUCCEEDS(aConnection.RemoveFunction(upgradeKeyFunctionName));
    return rv;
  }

  rv = DoUpgradeInternal(aConnection, aOrigin);

  MOZ_ALWAYS_SUCCEEDS(aConnection.RemoveFunction(upgradeKeyFunctionName));
  MOZ_ALWAYS_SUCCEEDS(aConnection.RemoveFunction(insertIDVFunctionName));

  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

// static
nsresult UpgradeSchemaFrom17_0To18_0Helper::DoUpgradeInternal(
    mozIStorageConnection& aConnection, const nsACString& aOrigin) {
  MOZ_ASSERT(!aOrigin.IsEmpty());

  nsresult rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // Drop these triggers to avoid unnecessary work during the upgrade
      // process.
      "DROP TRIGGER object_data_insert_trigger;"
      "DROP TRIGGER object_data_update_trigger;"
      "DROP TRIGGER object_data_delete_trigger;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // Drop these indexes before we do anything else to free disk space.
      "DROP INDEX index_data_object_data_id_index;"
      "DROP INDEX unique_index_data_object_data_id_index;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Create the new tables and triggers first.

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // This will eventually become the |database| table.
      "CREATE TABLE database_upgrade "
      "( name TEXT PRIMARY KEY"
      ", origin TEXT NOT NULL"
      ", version INTEGER NOT NULL DEFAULT 0"
      ", last_vacuum_time INTEGER NOT NULL DEFAULT 0"
      ", last_analyze_time INTEGER NOT NULL DEFAULT 0"
      ", last_vacuum_size INTEGER NOT NULL DEFAULT 0"
      ") WITHOUT ROWID;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // This will eventually become the |object_store| table.
      "CREATE TABLE object_store_upgrade"
      "( id INTEGER PRIMARY KEY"
      ", auto_increment INTEGER NOT NULL DEFAULT 0"
      ", name TEXT NOT NULL"
      ", key_path TEXT"
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // This will eventually become the |object_store_index| table.
      "CREATE TABLE object_store_index_upgrade"
      "( id INTEGER PRIMARY KEY"
      ", object_store_id INTEGER NOT NULL"
      ", name TEXT NOT NULL"
      ", key_path TEXT NOT NULL"
      ", unique_index INTEGER NOT NULL"
      ", multientry INTEGER NOT NULL"
      ", FOREIGN KEY (object_store_id) "
      "REFERENCES object_store(id) "
      ");"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // This will eventually become the |object_data| table.
      "CREATE TABLE object_data_upgrade"
      "( object_store_id INTEGER NOT NULL"
      ", key BLOB NOT NULL"
      ", index_data_values BLOB DEFAULT NULL"
      ", file_ids TEXT"
      ", data BLOB NOT NULL"
      ", PRIMARY KEY (object_store_id, key)"
      ", FOREIGN KEY (object_store_id) "
      "REFERENCES object_store(id) "
      ") WITHOUT ROWID;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // This will eventually become the |index_data| table.
      "CREATE TABLE index_data_upgrade"
      "( index_id INTEGER NOT NULL"
      ", value BLOB NOT NULL"
      ", object_data_key BLOB NOT NULL"
      ", object_store_id INTEGER NOT NULL"
      ", PRIMARY KEY (index_id, value, object_data_key)"
      ", FOREIGN KEY (index_id) "
      "REFERENCES object_store_index(id) "
      ", FOREIGN KEY (object_store_id, object_data_key) "
      "REFERENCES object_data(object_store_id, key) "
      ") WITHOUT ROWID;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // This will eventually become the |unique_index_data| table.
      "CREATE TABLE unique_index_data_upgrade"
      "( index_id INTEGER NOT NULL"
      ", value BLOB NOT NULL"
      ", object_store_id INTEGER NOT NULL"
      ", object_data_key BLOB NOT NULL"
      ", PRIMARY KEY (index_id, value)"
      ", FOREIGN KEY (index_id) "
      "REFERENCES object_store_index(id) "
      ", FOREIGN KEY (object_store_id, object_data_key) "
      "REFERENCES object_data(object_store_id, key) "
      ") WITHOUT ROWID;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // Temporarily store |index_data_values| that we build during the upgrade
      // of the index tables. We will later move this to the |object_data|
      // table.
      "CREATE TEMPORARY TABLE temp_index_data_values "
      "( object_store_id INTEGER NOT NULL"
      ", key BLOB NOT NULL"
      ", index_data_values BLOB DEFAULT NULL"
      ", PRIMARY KEY (object_store_id, key)"
      ") WITHOUT ROWID;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // These two triggers help build the |index_data_values| blobs. The nested
      // SELECT statements help us achieve an "INSERT OR UPDATE"-like behavior.
      "CREATE TEMPORARY TRIGGER unique_index_data_upgrade_insert_trigger "
      "AFTER INSERT ON unique_index_data_upgrade "
      "BEGIN "
      "INSERT OR REPLACE INTO temp_index_data_values "
      "VALUES "
      "( NEW.object_store_id"
      ", NEW.object_data_key"
      ", insert_idv("
      "( SELECT index_data_values "
      "FROM temp_index_data_values "
      "WHERE object_store_id = NEW.object_store_id "
      "AND key = NEW.object_data_key "
      "), NEW.index_id"
      ", 1" /* unique */
      ", NEW.value"
      ")"
      ");"
      "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TEMPORARY TRIGGER index_data_upgrade_insert_trigger "
      "AFTER INSERT ON index_data_upgrade "
      "BEGIN "
      "INSERT OR REPLACE INTO temp_index_data_values "
      "VALUES "
      "( NEW.object_store_id"
      ", NEW.object_data_key"
      ", insert_idv("
      "("
      "SELECT index_data_values "
      "FROM temp_index_data_values "
      "WHERE object_store_id = NEW.object_store_id "
      "AND key = NEW.object_data_key "
      "), NEW.index_id"
      ", 0" /* not unique */
      ", NEW.value"
      ")"
      ");"
      "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Update the |unique_index_data| table to change the column order, remove the
  // ON DELETE CASCADE clauses, and to apply the WITHOUT ROWID optimization.
  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // Insert all the data.
      "INSERT INTO unique_index_data_upgrade "
      "SELECT "
      "unique_index_data.index_id, "
      "upgrade_key(unique_index_data.value), "
      "object_data.object_store_id, "
      "upgrade_key(unique_index_data.object_data_key) "
      "FROM unique_index_data "
      "JOIN object_data "
      "ON unique_index_data.object_data_id = object_data.id;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // The trigger is no longer needed.
      "DROP TRIGGER unique_index_data_upgrade_insert_trigger;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // The old table is no longer needed.
      "DROP TABLE unique_index_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // Rename the table.
      "ALTER TABLE unique_index_data_upgrade "
      "RENAME TO unique_index_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Update the |index_data| table to change the column order, remove the ON
  // DELETE CASCADE clauses, and to apply the WITHOUT ROWID optimization.
  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // Insert all the data.
      "INSERT INTO index_data_upgrade "
      "SELECT "
      "index_data.index_id, "
      "upgrade_key(index_data.value), "
      "upgrade_key(index_data.object_data_key), "
      "object_data.object_store_id "
      "FROM index_data "
      "JOIN object_data "
      "ON index_data.object_data_id = object_data.id;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // The trigger is no longer needed.
      "DROP TRIGGER index_data_upgrade_insert_trigger;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // The old table is no longer needed.
      "DROP TABLE index_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // Rename the table.
      "ALTER TABLE index_data_upgrade "
      "RENAME TO index_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Update the |object_data| table to add the |index_data_values| column,
  // remove the ON DELETE CASCADE clause, and apply the WITHOUT ROWID
  // optimization.
  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // Insert all the data.
      "INSERT INTO object_data_upgrade "
      "SELECT "
      "object_data.object_store_id, "
      "upgrade_key(object_data.key_value), "
      "temp_index_data_values.index_data_values, "
      "object_data.file_ids, "
      "object_data.data "
      "FROM object_data "
      "LEFT JOIN temp_index_data_values "
      "ON object_data.object_store_id = "
      "temp_index_data_values.object_store_id "
      "AND upgrade_key(object_data.key_value) = "
      "temp_index_data_values.key;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // The temporary table is no longer needed.
      "DROP TABLE temp_index_data_values;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // The old table is no longer needed.
      "DROP TABLE object_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      // Rename the table.
      "ALTER TABLE object_data_upgrade "
      "RENAME TO object_data;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Update the |object_store_index| table to remove the UNIQUE constraint and
  // the ON DELETE CASCADE clause.
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO object_store_index_upgrade "
                       "SELECT * "
                       "FROM object_store_index;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_store_index;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("ALTER TABLE object_store_index_upgrade "
                       "RENAME TO object_store_index;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Update the |object_store| table to remove the UNIQUE constraint.
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("INSERT INTO object_store_upgrade "
                       "SELECT * "
                       "FROM object_store;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE object_store;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("ALTER TABLE object_store_upgrade "
                       "RENAME TO object_store;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Update the |database| table to include the origin, vacuum information, and
  // apply the WITHOUT ROWID optimization.
  nsCOMPtr<mozIStorageStatement> stmt;

  // The parameter names are not used, parameters are bound by index only
  // locally in the same function.
  rv = aConnection.CreateStatement(
      nsLiteralCString("INSERT INTO database_upgrade "
                       "SELECT name, :origin, version, 0, 0, 0 "
                       "FROM database;"),
      getter_AddRefs(stmt));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = stmt->BindUTF8StringByIndex(0, aOrigin);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = stmt->Execute();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL("DROP TABLE database;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("ALTER TABLE database_upgrade "
                       "RENAME TO database;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

#ifdef DEBUG
  {
    // Make sure there's only one entry in the |database| table.
    nsCOMPtr<mozIStorageStatement> stmt;
    MOZ_ASSERT(NS_SUCCEEDED(
        aConnection.CreateStatement(nsLiteralCString("SELECT COUNT(*) "
                                                     "FROM database;"),
                                    getter_AddRefs(stmt))));

    bool hasResult;
    MOZ_ASSERT(NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)));

    int64_t count;
    MOZ_ASSERT(NS_SUCCEEDED(stmt->GetInt64(0, &count)));

    MOZ_ASSERT(count == 1);
  }
#endif

  // Recreate file table triggers.
  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TRIGGER object_data_insert_trigger "
                       "AFTER INSERT ON object_data "
                       "WHEN NEW.file_ids IS NOT NULL "
                       "BEGIN "
                       "SELECT update_refcount(NULL, NEW.file_ids);"
                       "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TRIGGER object_data_update_trigger "
      "AFTER UPDATE OF file_ids ON object_data "
      "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL "
      "BEGIN "
      "SELECT update_refcount(OLD.file_ids, NEW.file_ids);"
      "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("CREATE TRIGGER object_data_delete_trigger "
                       "AFTER DELETE ON object_data "
                       "WHEN OLD.file_ids IS NOT NULL "
                       "BEGIN "
                       "SELECT update_refcount(OLD.file_ids, NULL);"
                       "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Finally, turn on auto_vacuum mode. We use full auto_vacuum mode to reclaim
  // disk space on mobile devices (at the cost of some COMMIT speed), and
  // incremental auto_vacuum mode on desktop builds.
  rv = aConnection.ExecuteSimpleSQL(
#ifdef IDB_MOBILE
      "PRAGMA auto_vacuum = FULL;"_ns
#else
      "PRAGMA auto_vacuum = INCREMENTAL;"_ns
#endif
  );
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(18, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom17_0To18_0(mozIStorageConnection& aConnection,
                                     const nsACString& aOrigin) {
  MOZ_ASSERT(!aOrigin.IsEmpty());

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom17_0To18_0", DOM);

  return UpgradeSchemaFrom17_0To18_0Helper::DoUpgrade(aConnection, aOrigin);
}

nsresult UpgradeSchemaFrom18_0To19_0(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  nsresult rv;
  AUTO_PROFILER_LABEL("UpgradeSchemaFrom18_0To19_0", DOM);

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("ALTER TABLE object_store_index "
                       "ADD COLUMN locale TEXT;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("ALTER TABLE object_store_index "
                       "ADD COLUMN is_auto_locale BOOLEAN;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("ALTER TABLE index_data "
                       "ADD COLUMN value_locale BLOB;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("ALTER TABLE unique_index_data "
                       "ADD COLUMN value_locale BLOB;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE INDEX index_data_value_locale_index "
      "ON index_data (index_id, value_locale, object_data_key, value) "
      "WHERE value_locale IS NOT NULL;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE INDEX unique_index_data_value_locale_index "
      "ON unique_index_data (index_id, value_locale, object_data_key, value) "
      "WHERE value_locale IS NOT NULL;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(19, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

class UpgradeFileIdsFunction final : public mozIStorageFunction {
  SafeRefPtr<FileManager> mFileManager;

 public:
  UpgradeFileIdsFunction() { AssertIsOnIOThread(); }

  nsresult Init(nsIFile* aFMDirectory, mozIStorageConnection& aConnection);

  NS_DECL_ISUPPORTS

 private:
  ~UpgradeFileIdsFunction() {
    AssertIsOnIOThread();

    if (mFileManager) {
      mFileManager->Invalidate();
    }
  }

  NS_IMETHOD
  OnFunctionCall(mozIStorageValueArray* aArguments,
                 nsIVariant** aResult) override;
};

nsresult UpgradeSchemaFrom19_0To20_0(nsIFile* aFMDirectory,
                                     mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom19_0To20_0", DOM);

  nsCOMPtr<mozIStorageStatement> stmt;
  nsresult rv = aConnection.CreateStatement(
      nsLiteralCString("SELECT count(*) "
                       "FROM object_data "
                       "WHERE file_ids IS NOT NULL"),
      getter_AddRefs(stmt));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  int64_t count;

  {
    mozStorageStatementScoper scoper(stmt);

    bool hasResult;
    rv = stmt->ExecuteStep(&hasResult);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    if (NS_WARN_IF(!hasResult)) {
      MOZ_ASSERT(false, "This should never be possible!");
      return NS_ERROR_FAILURE;
    }

    count = stmt->AsInt64(0);
    if (NS_WARN_IF(count < 0)) {
      MOZ_ASSERT(false, "This should never be possible!");
      return NS_ERROR_FAILURE;
    }
  }

  if (count == 0) {
    // Nothing to upgrade.
    rv = aConnection.SetSchemaVersion(MakeSchemaVersion(20, 0));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    return NS_OK;
  }

  RefPtr<UpgradeFileIdsFunction> function = new UpgradeFileIdsFunction();

  rv = function->Init(aFMDirectory, aConnection);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  constexpr auto functionName = "upgrade"_ns;

  rv = aConnection.CreateFunction(functionName, 2, function);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Disable update trigger.
  rv = aConnection.ExecuteSimpleSQL(
      "DROP TRIGGER object_data_update_trigger;"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("UPDATE object_data "
                       "SET file_ids = upgrade(file_ids, data) "
                       "WHERE file_ids IS NOT NULL;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  // Enable update trigger.
  rv = aConnection.ExecuteSimpleSQL(nsLiteralCString(
      "CREATE TRIGGER object_data_update_trigger "
      "AFTER UPDATE OF file_ids ON object_data "
      "FOR EACH ROW "
      "WHEN OLD.file_ids IS NOT NULL OR NEW.file_ids IS NOT NULL "
      "BEGIN "
      "SELECT update_refcount(OLD.file_ids, NEW.file_ids); "
      "END;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.RemoveFunction(functionName);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(20, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

class UpgradeIndexDataValuesFunction final : public mozIStorageFunction {
 public:
  UpgradeIndexDataValuesFunction() { AssertIsOnIOThread(); }

  NS_DECL_ISUPPORTS

 private:
  ~UpgradeIndexDataValuesFunction() { AssertIsOnIOThread(); }

  using IndexDataValuesArray = IndexDataValuesAutoArray;
  Result<IndexDataValuesArray, nsresult> ReadOldCompressedIDVFromBlob(
      Span<const uint8_t> aBlobData);

  NS_IMETHOD
  OnFunctionCall(mozIStorageValueArray* aArguments,
                 nsIVariant** aResult) override;
};

NS_IMPL_ISUPPORTS(UpgradeIndexDataValuesFunction, mozIStorageFunction)

Result<UpgradeIndexDataValuesFunction::IndexDataValuesArray, nsresult>
UpgradeIndexDataValuesFunction::ReadOldCompressedIDVFromBlob(
    const Span<const uint8_t> aBlobData) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());

  IndexOrObjectStoreId indexId;
  bool unique;
  bool nextIndexIdAlreadyRead = false;

  IndexDataValuesArray result;
  for (auto remainder = aBlobData; !remainder.IsEmpty();) {
    if (!nextIndexIdAlreadyRead) {
      auto readNumberOrErr = ReadCompressedIndexId(remainder);
      if (NS_WARN_IF(readNumberOrErr.isErr())) {
        return readNumberOrErr.propagateErr();
      }

      std::tie(indexId, unique, remainder) = readNumberOrErr.unwrap();
    }
    nextIndexIdAlreadyRead = false;

    if (NS_WARN_IF(remainder.IsEmpty())) {
      IDB_REPORT_INTERNAL_ERR();
      return Err(NS_ERROR_FILE_CORRUPTED);
    }

    // Read key buffer length.
    auto readNumberOrErr = ReadCompressedNumber(remainder);
    if (NS_WARN_IF(readNumberOrErr.isErr())) {
      return readNumberOrErr.propagateErr();
    }

    const auto [keyBufferLength, remainderAfterKeyBufferLength] =
        readNumberOrErr.unwrap();

    if (NS_WARN_IF(remainderAfterKeyBufferLength.IsEmpty()) ||
        NS_WARN_IF(keyBufferLength > uint64_t(UINT32_MAX)) ||
        NS_WARN_IF(keyBufferLength > remainderAfterKeyBufferLength.Length())) {
      IDB_REPORT_INTERNAL_ERR();
      return Err(NS_ERROR_FILE_CORRUPTED);
    }

    const auto [keyBuffer, remainderAfterKeyBuffer] =
        remainderAfterKeyBufferLength.SplitAt(keyBufferLength);
    if (NS_WARN_IF(!result.EmplaceBack(fallible, indexId, unique,
                                       Key{nsCString{AsChars(keyBuffer)}}))) {
      IDB_REPORT_INTERNAL_ERR();
      return Err(NS_ERROR_OUT_OF_MEMORY);
    }

    remainder = remainderAfterKeyBuffer;
    if (!remainder.IsEmpty()) {
      // Read either a sort key buffer length or an index id.
      auto readNumberOrErr = ReadCompressedNumber(remainder);
      if (NS_WARN_IF(readNumberOrErr.isErr())) {
        return readNumberOrErr.propagateErr();
      }

      const auto [maybeIndexId, remainderAfterIndexId] =
          readNumberOrErr.unwrap();

      // Locale-aware indexes haven't been around long enough to have any users,
      // we can safely assume all sort key buffer lengths will be zero.
      // XXX This duplicates logic from ReadCompressedIndexId.
      if (maybeIndexId != 0) {
        unique = maybeIndexId % 2 == 1;
        indexId = maybeIndexId / 2;
        nextIndexIdAlreadyRead = true;
      }

      remainder = remainderAfterIndexId;
    }
  }
  result.Sort();

  return result;
}

NS_IMETHODIMP
UpgradeIndexDataValuesFunction::OnFunctionCall(
    mozIStorageValueArray* aArguments, nsIVariant** aResult) {
  MOZ_ASSERT(aArguments);
  MOZ_ASSERT(aResult);

  AUTO_PROFILER_LABEL("UpgradeIndexDataValuesFunction::OnFunctionCall", DOM);

  uint32_t argc;
  nsresult rv = aArguments->GetNumEntries(&argc);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (argc != 1) {
    NS_WARNING("Don't call me with the wrong number of arguments!");
    return NS_ERROR_UNEXPECTED;
  }

  int32_t type;
  rv = aArguments->GetTypeOfIndex(0, &type);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (type != mozIStorageStatement::VALUE_TYPE_BLOB) {
    NS_WARNING("Don't call me with the wrong type of arguments!");
    return NS_ERROR_UNEXPECTED;
  }

  const uint8_t* oldBlob;
  uint32_t oldBlobLength;
  rv = aArguments->GetSharedBlob(0, &oldBlobLength, &oldBlob);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  auto oldIdvOrErr = ReadOldCompressedIDVFromBlob(Span(oldBlob, oldBlobLength));
  if (NS_WARN_IF(oldIdvOrErr.isErr())) {
    return oldIdvOrErr.unwrapErr();
  }

  UniqueFreePtr<uint8_t> newIdv;
  uint32_t newIdvLength;
  rv = MakeCompressedIndexDataValues(oldIdvOrErr.unwrap(), newIdv,
                                     &newIdvLength);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  std::pair<uint8_t*, int> data(newIdv.release(), newIdvLength);

  nsCOMPtr<nsIVariant> result = new storage::AdoptedBlobVariant(data);

  result.forget(aResult);
  return NS_OK;
}

nsresult UpgradeSchemaFrom20_0To21_0(mozIStorageConnection& aConnection) {
  // This should have been part of the 18 to 19 upgrade, where we changed the
  // layout of the index_data_values blobs but didn't upgrade the existing data.
  // See bug 1202788.

  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom20_0To21_0", DOM);

  RefPtr<UpgradeIndexDataValuesFunction> function =
      new UpgradeIndexDataValuesFunction();

  constexpr auto functionName = "upgrade_idv"_ns;

  nsresult rv = aConnection.CreateFunction(functionName, 1, function);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("UPDATE object_data "
                       "SET index_data_values = upgrade_idv(index_data_values) "
                       "WHERE index_data_values IS NOT NULL;"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.RemoveFunction(functionName);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(21, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom21_0To22_0(mozIStorageConnection& aConnection) {
  // The only change between 21 and 22 was a different structured clone format,
  // but it's backwards-compatible.
  nsresult rv = aConnection.SetSchemaVersion(MakeSchemaVersion(22, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom22_0To23_0(mozIStorageConnection& aConnection,
                                     const nsACString& aOrigin) {
  AssertIsOnIOThread();

  MOZ_ASSERT(!aOrigin.IsEmpty());

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom22_0To23_0", DOM);

  nsCOMPtr<mozIStorageStatement> stmt;
  // The parameter names are not used, parameters are bound by index only
  // locally in the same function.
  nsresult rv =
      aConnection.CreateStatement(nsLiteralCString("UPDATE database "
                                                   "SET origin = :origin;"),
                                  getter_AddRefs(stmt));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = stmt->BindUTF8StringByIndex(0, aOrigin);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = stmt->Execute();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(23, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom23_0To24_0(mozIStorageConnection& aConnection) {
  // The only change between 23 and 24 was a different structured clone format,
  // but it's backwards-compatible.
  nsresult rv = aConnection.SetSchemaVersion(MakeSchemaVersion(24, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult UpgradeSchemaFrom24_0To25_0(mozIStorageConnection& aConnection) {
  // The changes between 24 and 25 were an upgraded snappy library, a different
  // structured clone format and a different file_ds format. But everything is
  // backwards-compatible.
  nsresult rv = aConnection.SetSchemaVersion(MakeSchemaVersion(25, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

class StripObsoleteOriginAttributesFunction final : public mozIStorageFunction {
 public:
  NS_DECL_ISUPPORTS

 private:
  ~StripObsoleteOriginAttributesFunction() = default;

  NS_IMETHOD
  OnFunctionCall(mozIStorageValueArray* aArguments,
                 nsIVariant** aResult) override {
    MOZ_ASSERT(aArguments);
    MOZ_ASSERT(aResult);

    AUTO_PROFILER_LABEL("StripObsoleteOriginAttributesFunction::OnFunctionCall",
                        DOM);

#ifdef DEBUG
    {
      uint32_t argCount;
      MOZ_ALWAYS_SUCCEEDS(aArguments->GetNumEntries(&argCount));
      MOZ_ASSERT(argCount == 1);

      int32_t type;
      MOZ_ALWAYS_SUCCEEDS(aArguments->GetTypeOfIndex(0, &type));
      MOZ_ASSERT(type == mozIStorageValueArray::VALUE_TYPE_TEXT);
    }
#endif

    nsCString origin;
    nsresult rv = aArguments->GetUTF8String(0, origin);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    // Deserialize and re-serialize to automatically drop any obsolete origin
    // attributes.
    OriginAttributes oa;

    nsCString originNoSuffix;
    bool ok = oa.PopulateFromOrigin(origin, originNoSuffix);
    if (NS_WARN_IF(!ok)) {
      return NS_ERROR_FAILURE;
    }

    nsCString suffix;
    oa.CreateSuffix(suffix);

    nsCOMPtr<nsIVariant> result =
        new mozilla::storage::UTF8TextVariant(originNoSuffix + suffix);

    result.forget(aResult);
    return NS_OK;
  }
};

nsresult UpgradeSchemaFrom25_0To26_0(mozIStorageConnection& aConnection) {
  AssertIsOnIOThread();

  AUTO_PROFILER_LABEL("UpgradeSchemaFrom25_0To26_0", DOM);

  constexpr auto functionName = "strip_obsolete_attributes"_ns;

  nsCOMPtr<mozIStorageFunction> stripObsoleteAttributes =
      new StripObsoleteOriginAttributesFunction();

  nsresult rv = aConnection.CreateFunction(functionName,
                                           /* aNumArguments */ 1,
                                           stripObsoleteAttributes);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.ExecuteSimpleSQL(
      nsLiteralCString("UPDATE DATABASE "
                       "SET origin = strip_obsolete_attributes(origin) "
                       "WHERE origin LIKE '%^%';"));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.RemoveFunction(functionName);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  rv = aConnection.SetSchemaVersion(MakeSchemaVersion(26, 0));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

Result<nsCOMPtr<nsIFileURL>, nsresult> GetDatabaseFileURL(
    nsIFile& aDatabaseFile, const int64_t aDirectoryLockId,
    const uint32_t aTelemetryId) {
  MOZ_ASSERT(aDirectoryLockId >= -1);

  nsresult rv;

  nsCOMPtr<nsIProtocolHandler> protocolHandler(
      do_GetService(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX "file", &rv));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  nsCOMPtr<nsIFileProtocolHandler> fileHandler(
      do_QueryInterface(protocolHandler, &rv));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  nsCOMPtr<nsIURIMutator> mutator;
  rv = fileHandler->NewFileURIMutator(&aDatabaseFile, getter_AddRefs(mutator));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  // aDirectoryLockId should only be -1 when we are called from
  // FileManager::InitDirectory when the temporary storage hasn't been
  // initialized yet. At that time, the in-memory objects (e.g. OriginInfo) are
  // only being created so it doesn't make sense to tunnel quota information to
  // TelemetryVFS to get corresponding QuotaObject instances for SQLite files.
  const nsCString directoryLockIdClause =
      aDirectoryLockId >= 0
          ? "&directoryLockId="_ns + IntCString(aDirectoryLockId)
          : EmptyCString();

  nsAutoCString telemetryFilenameClause;
  if (aTelemetryId) {
    telemetryFilenameClause.AssignLiteral("&telemetryFilename=indexedDB-");
    telemetryFilenameClause.AppendInt(aTelemetryId);
    telemetryFilenameClause.Append(NS_ConvertUTF16toUTF8(kSQLiteSuffix));
  }

  nsCOMPtr<nsIFileURL> result;
  rv = NS_MutateURI(mutator)
           .SetQuery("cache=private"_ns + directoryLockIdClause +
                     telemetryFilenameClause)
           .Finalize(result);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  return result;
}

nsresult SetDefaultPragmas(mozIStorageConnection& aConnection) {
  MOZ_ASSERT(!NS_IsMainThread());

  static constexpr auto kBuiltInPragmas =
      // We use foreign keys in DEBUG builds only because there is a performance
      // cost to using them.
      "PRAGMA foreign_keys = "
#ifdef DEBUG
      "ON"
#else
      "OFF"
#endif
      ";"

      // The "INSERT OR REPLACE" statement doesn't fire the update trigger,
      // instead it fires only the insert trigger. This confuses the update
      // refcount function. This behavior changes with enabled recursive
      // triggers, so the statement fires the delete trigger first and then the
      // insert trigger.
      "PRAGMA recursive_triggers = ON;"

      // We aggressively truncate the database file when idle so don't bother
      // overwriting the WAL with 0 during active periods.
      "PRAGMA secure_delete = OFF;"_ns;

  nsresult rv = aConnection.ExecuteSimpleSQL(kBuiltInPragmas);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  nsAutoCString pragmaStmt;
  pragmaStmt.AssignLiteral("PRAGMA synchronous = ");

  if (IndexedDatabaseManager::FullSynchronous()) {
    pragmaStmt.AppendLiteral("FULL");
  } else {
    pragmaStmt.AppendLiteral("NORMAL");
  }
  pragmaStmt.Append(';');

  rv = aConnection.ExecuteSimpleSQL(pragmaStmt);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

#ifndef IDB_MOBILE
  if (kSQLiteGrowthIncrement) {
    // This is just an optimization so ignore the failure if the disk is
    // currently too full.
    rv = aConnection.SetGrowthIncrement(kSQLiteGrowthIncrement, EmptyCString());
    if (rv != NS_ERROR_FILE_TOO_BIG && NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }
#endif  // IDB_MOBILE

  return NS_OK;
}

nsresult SetJournalMode(mozIStorageConnection& aConnection) {
  MOZ_ASSERT(!NS_IsMainThread());

  // Try enabling WAL mode. This can fail in various circumstances so we have to
  // check the results here.
  constexpr auto journalModeQueryStart = "PRAGMA journal_mode = "_ns;
  constexpr auto journalModeWAL = "wal"_ns;

  nsCOMPtr<mozIStorageStatement> stmt;
  nsresult rv = aConnection.CreateStatement(
      journalModeQueryStart + journalModeWAL, getter_AddRefs(stmt));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  bool hasResult;
  rv = stmt->ExecuteStep(&hasResult);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  MOZ_ASSERT(hasResult);

  nsCString journalMode;
  rv = stmt->GetUTF8String(0, journalMode);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (journalMode.Equals(journalModeWAL)) {
    // WAL mode successfully enabled. Maybe set limits on its size here.
    if (kMaxWALPages >= 0) {
      nsAutoCString pageCount;
      pageCount.AppendInt(kMaxWALPages);

      rv = aConnection.ExecuteSimpleSQL("PRAGMA wal_autocheckpoint = "_ns +
                                        pageCount);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    }
  } else {
    NS_WARNING("Failed to set WAL mode, falling back to normal journal mode.");
#ifdef IDB_MOBILE
    rv = aConnection.ExecuteSimpleSQL(journalModeQueryStart + "truncate"_ns);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
#endif
  }

  return NS_OK;
}

template <typename V>
auto ValOrErr(V aVal, const nsresult aRv) {
  return NS_SUCCEEDED(aRv) ? WrapMovingNotNull(std::move(aVal))
                           : Result<MovingNotNull<V>, nsresult>{Err(aRv)};
}

template <class FileOrURLType>
struct StorageOpenTraits;

template <>
struct StorageOpenTraits<nsIFileURL> {
  static Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult> Open(
      mozIStorageService& aStorageService, nsIFileURL& aFileURL) {
    nsCOMPtr<mozIStorageConnection> connection;
    nsresult rv = aStorageService.OpenDatabaseWithFileURL(
        &aFileURL, getter_AddRefs(connection));
    return ValOrErr(std::move(connection), rv);
  }

#ifdef DEBUG
  static void GetPath(nsIFileURL& aFileURL, nsCString& aPath) {
    MOZ_ALWAYS_SUCCEEDS(aFileURL.GetFileName(aPath));
  }
#endif
};

template <>
struct StorageOpenTraits<nsIFile> {
  static Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult> Open(
      mozIStorageService& aStorageService, nsIFile& aFile) {
    nsCOMPtr<mozIStorageConnection> connection;
    nsresult rv = aStorageService.OpenUnsharedDatabase(
        &aFile, getter_AddRefs(connection));
    return ValOrErr(std::move(connection), rv);
  }

#ifdef DEBUG
  static void GetPath(nsIFile& aFile, nsCString& aPath) {
    nsString path;
    MOZ_ALWAYS_SUCCEEDS(aFile.GetPath(path));

    LossyCopyUTF16toASCII(path, aPath);
  }
#endif
};

template <class FileOrURLType>
Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
OpenDatabaseAndHandleBusy(mozIStorageService& aStorageService,
                          FileOrURLType& aFileOrURL) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());

  auto connectionOrErr =
      StorageOpenTraits<FileOrURLType>::Open(aStorageService, aFileOrURL);

  if (connectionOrErr.isErr() &&
      connectionOrErr.inspectErr() == NS_ERROR_STORAGE_BUSY) {
#ifdef DEBUG
    {
      nsCString path;
      StorageOpenTraits<FileOrURLType>::GetPath(aFileOrURL, path);

      nsPrintfCString message(
          "Received NS_ERROR_STORAGE_BUSY when attempting "
          "to open database '%s', retrying for up to 10 "
          "seconds",
          path.get());
      NS_WARNING(message.get());
    }
#endif

    // Another thread must be checkpointing the WAL. Wait up to 10 seconds for
    // that to complete.
    const TimeStamp start = TimeStamp::NowLoRes();

    while (true) {
      PR_Sleep(PR_MillisecondsToInterval(100));

      connectionOrErr =
          StorageOpenTraits<FileOrURLType>::Open(aStorageService, aFileOrURL);
      if (!connectionOrErr.isErr() ||
          connectionOrErr.inspectErr() != NS_ERROR_STORAGE_BUSY ||
          TimeStamp::NowLoRes() - start > TimeDuration::FromSeconds(10)) {
        break;
      }
    }
  }

  if (NS_WARN_IF(connectionOrErr.isErr())) {
    return connectionOrErr.propagateErr();
  }

  return connectionOrErr;
}

Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
CreateStorageConnection(nsIFile& aDBFile, nsIFile& aFMDirectory,
                        const nsAString& aName, const nsACString& aOrigin,
                        const int64_t aDirectoryLockId,
                        const uint32_t aTelemetryId) {
  AssertIsOnIOThread();
  MOZ_ASSERT(aDirectoryLockId >= -1);

  AUTO_PROFILER_LABEL("CreateStorageConnection", DOM);

  bool exists;

  auto dbFileUrlOrErr =
      GetDatabaseFileURL(aDBFile, aDirectoryLockId, aTelemetryId);
  if (NS_WARN_IF(dbFileUrlOrErr.isErr())) {
    return dbFileUrlOrErr.propagateErr();
  }

  auto dbFileUrl = dbFileUrlOrErr.unwrap();

  nsresult rv;
  nsCOMPtr<mozIStorageService> ss =
      do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  auto connectionOrErr = OpenDatabaseAndHandleBusy(*ss, *dbFileUrl);
  if (connectionOrErr.isErr()) {
    if (connectionOrErr.inspectErr() == NS_ERROR_FILE_CORRUPTED) {
      // If we're just opening the database during origin initialization, then
      // we don't want to erase any files. The failure here will fail origin
      // initialization too.
      if (aName.IsVoid()) {
        return Err(rv);
      }

      // Nuke the database file.
      rv = aDBFile.Remove(false);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      rv = aFMDirectory.Exists(&exists);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      if (exists) {
        bool isDirectory;
        rv = aFMDirectory.IsDirectory(&isDirectory);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return Err(rv);
        }
        if (NS_WARN_IF(!isDirectory)) {
          IDB_REPORT_INTERNAL_ERR();
          return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
        }

        rv = aFMDirectory.Remove(true);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return Err(rv);
        }
      }

      connectionOrErr = OpenDatabaseAndHandleBusy(*ss, *dbFileUrl);
    }

    if (NS_WARN_IF(connectionOrErr.isErr())) {
      return connectionOrErr.propagateErr();
    }
  }

  auto connection = connectionOrErr.unwrap().unwrapBasePtr();

  rv = SetDefaultPragmas(*connection);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  rv = connection->EnableModule("filesystem"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  // Check to make sure that the database schema is correct.
  int32_t schemaVersion;
  rv = connection->GetSchemaVersion(&schemaVersion);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  // Unknown schema will fail origin initialization too.
  if (!schemaVersion && aName.IsVoid()) {
    IDB_WARNING("Unable to open IndexedDB database, schema is not set!");
    return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
  }

  if (schemaVersion > kSQLiteSchemaVersion) {
    IDB_WARNING("Unable to open IndexedDB database, schema is too high!");
    return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
  }

  bool journalModeSet = false;

  if (schemaVersion != kSQLiteSchemaVersion) {
    const bool newDatabase = !schemaVersion;

    if (newDatabase) {
      // Set the page size first.
      if (kSQLitePageSizeOverride) {
        rv = connection->ExecuteSimpleSQL(nsPrintfCString(
            "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride));
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return Err(rv);
        }
      }

      // We have to set the auto_vacuum mode before opening a transaction.
      rv = connection->ExecuteSimpleSQL(
#ifdef IDB_MOBILE
          // Turn on full auto_vacuum mode to reclaim disk space on mobile
          // devices (at the cost of some COMMIT speed).
          "PRAGMA auto_vacuum = FULL;"_ns
#else
          // Turn on incremental auto_vacuum mode on desktop builds.
          "PRAGMA auto_vacuum = INCREMENTAL;"_ns
#endif
      );
      if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) {
        // mozstorage translates SQLITE_FULL to NS_ERROR_FILE_NO_DEVICE_SPACE,
        // which we know better as NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR.
        rv = NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
      }
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      rv = SetJournalMode(*connection);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      journalModeSet = true;
    } else {
#ifdef DEBUG
      // Disable foreign key support while upgrading. This has to be done before
      // starting a transaction.
      MOZ_ALWAYS_SUCCEEDS(
          connection->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns));
#endif
    }

    bool vacuumNeeded = false;

    mozStorageTransaction transaction(
        connection.get(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);

    if (newDatabase) {
      rv = CreateTables(*connection);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      MOZ_ASSERT(NS_SUCCEEDED(connection->GetSchemaVersion(&schemaVersion)));
      MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);

      nsCOMPtr<mozIStorageStatement> stmt;
      // The parameter names are not used, parameters are bound by index only
      // locally in the same function.
      nsresult rv = connection->CreateStatement(
          nsLiteralCString("INSERT INTO database (name, origin) "
                           "VALUES (:name, :origin)"),
          getter_AddRefs(stmt));
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      rv = stmt->BindStringByIndex(0, aName);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      rv = stmt->BindUTF8StringByIndex(1, aOrigin);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      rv = stmt->Execute();
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }
    } else {
      // This logic needs to change next time we change the schema!
      static_assert(kSQLiteSchemaVersion == int32_t((26 << 4) + 0),
                    "Upgrade function needed due to schema version increase.");

      while (schemaVersion != kSQLiteSchemaVersion) {
        switch (schemaVersion) {
          case 4:
            rv = UpgradeSchemaFrom4To5(*connection);
            break;
          case 5:
            rv = UpgradeSchemaFrom5To6(*connection);
            break;
          case 6:
            rv = UpgradeSchemaFrom6To7(*connection);
            break;
          case 7:
            rv = UpgradeSchemaFrom7To8(*connection);
            break;
          case 8:
            rv = UpgradeSchemaFrom8To9_0(*connection);
            vacuumNeeded = true;
            break;
          case MakeSchemaVersion(9, 0):
            rv = UpgradeSchemaFrom9_0To10_0(*connection);
            break;
          case MakeSchemaVersion(10, 0):
            rv = UpgradeSchemaFrom10_0To11_0(*connection);
            break;
          case MakeSchemaVersion(11, 0):
            rv = UpgradeSchemaFrom11_0To12_0(*connection);
            break;
          case MakeSchemaVersion(12, 0):
            rv = UpgradeSchemaFrom12_0To13_0(*connection, &vacuumNeeded);
            break;
          case MakeSchemaVersion(13, 0):
            rv = UpgradeSchemaFrom13_0To14_0(*connection);
            break;
          case MakeSchemaVersion(14, 0):
            rv = UpgradeSchemaFrom14_0To15_0(*connection);
            break;
          case MakeSchemaVersion(15, 0):
            rv = UpgradeSchemaFrom15_0To16_0(*connection);
            break;
          case MakeSchemaVersion(16, 0):
            rv = UpgradeSchemaFrom16_0To17_0(*connection);
            break;
          case MakeSchemaVersion(17, 0):
            rv = UpgradeSchemaFrom17_0To18_0(*connection, aOrigin);
            vacuumNeeded = true;
            break;
          case MakeSchemaVersion(18, 0):
            rv = UpgradeSchemaFrom18_0To19_0(*connection);
            break;
          case MakeSchemaVersion(19, 0):
            rv = UpgradeSchemaFrom19_0To20_0(&aFMDirectory, *connection);
            break;
          case MakeSchemaVersion(20, 0):
            rv = UpgradeSchemaFrom20_0To21_0(*connection);
            break;
          case MakeSchemaVersion(21, 0):
            rv = UpgradeSchemaFrom21_0To22_0(*connection);
            break;
          case MakeSchemaVersion(22, 0):
            rv = UpgradeSchemaFrom22_0To23_0(*connection, aOrigin);
            break;
          case MakeSchemaVersion(23, 0):
            rv = UpgradeSchemaFrom23_0To24_0(*connection);
            break;
          case MakeSchemaVersion(24, 0):
            rv = UpgradeSchemaFrom24_0To25_0(*connection);
            break;
          case MakeSchemaVersion(25, 0):
            rv = UpgradeSchemaFrom25_0To26_0(*connection);
            break;
          default:
            IDB_WARNING(
                "Unable to open IndexedDB database, no upgrade path is "
                "available!");
            return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
        }

        if (NS_WARN_IF(NS_FAILED(rv))) {
          return Err(rv);
        }

        rv = connection->GetSchemaVersion(&schemaVersion);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return Err(rv);
        }
      }

      MOZ_ASSERT(schemaVersion == kSQLiteSchemaVersion);
    }

    rv = transaction.Commit();
    if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) {
      // mozstorage translates SQLITE_FULL to NS_ERROR_FILE_NO_DEVICE_SPACE,
      // which we know better as NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR.
      rv = NS_ERROR_DOM_INDEXEDDB_QUOTA_ERR;
    }
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return Err(rv);
    }

#ifdef DEBUG
    if (!newDatabase) {
      // Re-enable foreign key support after doing a foreign key check.
      nsCOMPtr<mozIStorageStatement> checkStmt;
      MOZ_ALWAYS_SUCCEEDS(connection->CreateStatement(
          "PRAGMA foreign_key_check;"_ns, getter_AddRefs(checkStmt)));

      bool hasResult;
      MOZ_ALWAYS_SUCCEEDS(checkStmt->ExecuteStep(&hasResult));
      MOZ_ASSERT(!hasResult, "Database has inconsisistent foreign keys!");

      MOZ_ALWAYS_SUCCEEDS(
          connection->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns));
    }
#endif

    if (kSQLitePageSizeOverride && !newDatabase) {
      nsCOMPtr<mozIStorageStatement> stmt;
      rv = connection->CreateStatement("PRAGMA page_size;"_ns,
                                       getter_AddRefs(stmt));
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      bool hasResult;
      rv = stmt->ExecuteStep(&hasResult);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      MOZ_ASSERT(hasResult);

      int32_t pageSize;
      rv = stmt->GetInt32(0, &pageSize);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      MOZ_ASSERT(pageSize >= 512 && pageSize <= 65536);

      if (kSQLitePageSizeOverride != uint32_t(pageSize)) {
        // We must not be in WAL journal mode to change the page size.
        rv = connection->ExecuteSimpleSQL("PRAGMA journal_mode = DELETE;"_ns);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return Err(rv);
        }

        rv = connection->CreateStatement("PRAGMA journal_mode;"_ns,
                                         getter_AddRefs(stmt));
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return Err(rv);
        }

        rv = stmt->ExecuteStep(&hasResult);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return Err(rv);
        }

        MOZ_ASSERT(hasResult);

        nsCString journalMode;
        rv = stmt->GetUTF8String(0, journalMode);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return Err(rv);
        }

        if (journalMode.EqualsLiteral("delete")) {
          // Successfully set to rollback journal mode so changing the page size
          // is possible with a VACUUM.
          rv = connection->ExecuteSimpleSQL(nsPrintfCString(
              "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride));
          if (NS_WARN_IF(NS_FAILED(rv))) {
            return Err(rv);
          }

          // We will need to VACUUM in order to change the page size.
          vacuumNeeded = true;
        } else {
          NS_WARNING(
              "Failed to set journal_mode for database, unable to "
              "change the page size!");
        }
      }
    }

    if (vacuumNeeded) {
      rv = connection->ExecuteSimpleSQL("VACUUM;"_ns);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }
    }

    if (newDatabase || vacuumNeeded) {
      if (journalModeSet) {
        // Make sure we checkpoint to get an accurate file size.
        rv = connection->ExecuteSimpleSQL("PRAGMA wal_checkpoint(FULL);"_ns);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          return Err(rv);
        }
      }

      int64_t fileSize;
      rv = aDBFile.GetFileSize(&fileSize);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      MOZ_ASSERT(fileSize > 0);

      PRTime vacuumTime = PR_Now();
      MOZ_ASSERT(vacuumTime);

      nsCOMPtr<mozIStorageStatement> vacuumTimeStmt;
      // The parameter names are not used, parameters are bound by index only
      // locally in the same function.
      rv = connection->CreateStatement(
          nsLiteralCString("UPDATE database "
                           "SET last_vacuum_time = :time"
                           ", last_vacuum_size = :size;"),
          getter_AddRefs(vacuumTimeStmt));
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      rv = vacuumTimeStmt->BindInt64ByIndex(0, vacuumTime);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      rv = vacuumTimeStmt->BindInt64ByIndex(1, fileSize);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }

      rv = vacuumTimeStmt->Execute();
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return Err(rv);
      }
    }
  }

  if (!journalModeSet) {
    rv = SetJournalMode(*connection);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return Err(rv);
    }
  }

  return WrapMovingNotNullUnchecked(std::move(connection));
}

nsCOMPtr<nsIFile> GetFileForPath(const nsAString& aPath) {
  MOZ_ASSERT(!aPath.IsEmpty());

  auto fileOrErr = QM_NewLocalFile(aPath);
  if (NS_WARN_IF(fileOrErr.isErr())) {
    return nullptr;
  }

  return fileOrErr.unwrap();
}

Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
GetStorageConnection(nsIFile& aDatabaseFile, const int64_t aDirectoryLockId,
                     const uint32_t aTelemetryId) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());
  MOZ_ASSERT(aDirectoryLockId >= 0);

  AUTO_PROFILER_LABEL("GetStorageConnection", DOM);

  bool exists;
  nsresult rv = aDatabaseFile.Exists(&exists);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  if (NS_WARN_IF(!exists)) {
    IDB_REPORT_INTERNAL_ERR();
    return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
  }

  auto dbFileUrlOrErr =
      GetDatabaseFileURL(aDatabaseFile, aDirectoryLockId, aTelemetryId);
  if (NS_WARN_IF(dbFileUrlOrErr.isErr())) {
    return dbFileUrlOrErr.propagateErr();
  }

  nsCOMPtr<mozIStorageService> ss =
      do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  auto connectionOrErr =
      OpenDatabaseAndHandleBusy(*ss, *dbFileUrlOrErr.inspect());
  if (NS_WARN_IF(connectionOrErr.isErr())) {
    return connectionOrErr.propagateErr();
  }

  auto connection = connectionOrErr.unwrap().unwrapBasePtr();

  rv = SetDefaultPragmas(*connection);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  rv = SetJournalMode(*connection);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return Err(rv);
  }

  return WrapMovingNotNullUnchecked(std::move(connection));
}

Result<MovingNotNull<nsCOMPtr<mozIStorageConnection>>, nsresult>
GetStorageConnection(const nsAString& aDatabaseFilePath,
                     const int64_t aDirectoryLockId,
                     const uint32_t aTelemetryId) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!IsOnBackgroundThread());
  MOZ_ASSERT(!aDatabaseFilePath.IsEmpty());
  MOZ_ASSERT(StringEndsWith(aDatabaseFilePath, kSQLiteSuffix));
  MOZ_ASSERT(aDirectoryLockId >= 0);

  nsCOMPtr<nsIFile> dbFile = GetFileForPath(aDatabaseFilePath);
  if (NS_WARN_IF(!dbFile)) {
    IDB_REPORT_INTERNAL_ERR();
    return Err(NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
  }

  return GetStorageConnection(*dbFile, aDirectoryLockId, aTelemetryId);
}

/*******************************************************************************
 * ConnectionPool declarations
 ******************************************************************************/

class DatabaseConnection final {
  friend class ConnectionPool;

  enum class CheckpointMode { Full, Restart, Truncate };

 public:
  class AutoSavepoint;
  class CachedStatement;
  class UpdateRefcountFunction;

 private:
  InitializedOnce<const NotNull<nsCOMPtr<mozIStorageConnection>>>
      mStorageConnection;
  InitializedOnce<const NotNull<SafeRefPtr<FileManager>>> mFileManager;
  nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement>
      mCachedStatements;
  RefPtr<UpdateRefcountFunction> mUpdateRefcountFunction;
  RefPtr<QuotaObject> mQuotaObject;
  RefPtr<QuotaObject> mJournalQuotaObject;
  bool mInReadTransaction;
  bool mInWriteTransaction;

#ifdef DEBUG
  uint32_t mDEBUGSavepointCount;
#endif

  NS_DECL_OWNINGTHREAD

 public:
  void AssertIsOnConnectionThread() const {
    NS_ASSERT_OWNINGTHREAD(DatabaseConnection);
  }

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DatabaseConnection)

  bool HasStorageConnection() const {
    return static_cast<bool>(mStorageConnection);
  }

  mozIStorageConnection& MutableStorageConnection() const {
    AssertIsOnConnectionThread();
    MOZ_ASSERT(mStorageConnection);

    return **mStorageConnection;
  }

  UpdateRefcountFunction* GetUpdateRefcountFunction() const {
    AssertIsOnConnectionThread();

    return mUpdateRefcountFunction;
  }

  nsresult GetCachedStatement(const nsACString& aQuery,
                              CachedStatement* aCachedStatement);

  template <typename BindFunctor>
  nsresult ExecuteCachedStatement(const nsACString& aQuery,
                                  const BindFunctor& aBindFunctor);

  nsresult ExecuteCachedStatement(const nsACString& aQuery);

  nsresult BeginWriteTransaction();

  nsresult CommitWriteTransaction();

  void RollbackWriteTransaction();

  void FinishWriteTransaction();

  nsresult StartSavepoint();

  nsresult ReleaseSavepoint();

  nsresult RollbackSavepoint();

  nsresult Checkpoint() {
    AssertIsOnConnectionThread();

    return CheckpointInternal(CheckpointMode::Full);
  }

  void DoIdleProcessing(bool aNeedsCheckpoint);

  void Close();

  nsresult DisableQuotaChecks();

  void EnableQuotaChecks();

 private:
  DatabaseConnection(
      MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection,
      MovingNotNull<SafeRefPtr<FileManager>> aFileManager);

  ~DatabaseConnection();

  nsresult Init();

  nsresult CheckpointInternal(CheckpointMode aMode);

  nsresult GetFreelistCount(CachedStatement& aCachedStatement,
                            uint32_t* aFreelistCount);

  nsresult ReclaimFreePagesWhileIdle(CachedStatement& aFreelistStatement,
                                     CachedStatement& aRollbackStatement,
                                     uint32_t aFreelistCount,
                                     bool aNeedsCheckpoint,
                                     bool* aFreedSomePages);

  nsresult GetFileSize(const nsAString& aPath, int64_t* aResult);
};

class MOZ_STACK_CLASS DatabaseConnection::AutoSavepoint final {
  DatabaseConnection* mConnection;
#ifdef DEBUG
  const TransactionBase* mDEBUGTransaction;
#endif

 public:
  AutoSavepoint();
  ~AutoSavepoint();

  nsresult Start(const TransactionBase& aTransaction);

  nsresult Commit();
};

class DatabaseConnection::CachedStatement final {
  friend class DatabaseConnection;

  nsCOMPtr<mozIStorageStatement> mStatement;
  Maybe<mozStorageStatementScoper> mScoper;

#ifdef DEBUG
  DatabaseConnection* mDEBUGConnection;
#endif

 public:
  CachedStatement();
  ~CachedStatement();

  void AssertIsOnConnectionThread() const {
#ifdef DEBUG
    if (mDEBUGConnection) {
      mDEBUGConnection->AssertIsOnConnectionThread();
    }
    MOZ_ASSERT(!NS_IsMainThread());
    MOZ_ASSERT(!IsOnBackgroundThread());
#endif
  }

  explicit operator bool() const;

  mozIStorageStatement& operator*() const;

  mozIStorageStatement* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN;

  void Reset();

 private:
  // Only called by DatabaseConnection.
  void Assign(DatabaseConnection* aConnection,
              nsCOMPtr<mozIStorageStatement> aStatement);

  // No funny business allowed.
  CachedStatement(const CachedStatement&) = delete;
  CachedStatement& operator=(const CachedStatement&) = delete;
};

class DatabaseConnection::UpdateRefcountFunction final
    : public mozIStorageFunction {
  class DatabaseUpdateFunction;
  class FileInfoEntry;

  enum class UpdateType { Increment, Decrement };

  DatabaseConnection* const mConnection;
  FileManager& mFileManager;
  nsClassHashtable<nsUint64HashKey, FileInfoEntry> mFileInfoEntries;
  nsDataHashtable<nsUint64HashKey, FileInfoEntry*> mSavepointEntriesIndex;

  nsTArray<int64_t> mJournalsToCreateBeforeCommit;
  nsTArray<int64_t> mJournalsToRemoveAfterCommit;
  nsTArray<int64_t> mJournalsToRemoveAfterAbort;

  bool mInSavepoint;

 public:
  NS_DECL_ISUPPORTS
  NS_DECL_MOZISTORAGEFUNCTION

  UpdateRefcountFunction(DatabaseConnection* aConnection,
                         FileManager& aFileManager);

  nsresult WillCommit();

  void DidCommit();

  void DidAbort();

  void StartSavepoint();

  void ReleaseSavepoint();

  void RollbackSavepoint();

  void Reset();

 private:
  ~UpdateRefcountFunction() = default;

  nsresult ProcessValue(mozIStorageValueArray* aValues, int32_t aIndex,
                        UpdateType aUpdateType);

  nsresult CreateJournals();

  nsresult RemoveJournals(const nsTArray<int64_t>& aJournals);
};

class DatabaseConnection::UpdateRefcountFunction::DatabaseUpdateFunction final {
  CachedStatement mUpdateStatement;
  CachedStatement mSelectStatement;
  CachedStatement mInsertStatement;

  UpdateRefcountFunction* const mFunction;

  nsresult mErrorCode;

 public:
  explicit DatabaseUpdateFunction(UpdateRefcountFunction* aFunction)
      : mFunction(aFunction), mErrorCode(NS_OK) {
    MOZ_COUNT_CTOR(
        DatabaseConnection::UpdateRefcountFunction::DatabaseUpdateFunction);
  }

  ~DatabaseUpdateFunction() {
    MOZ_COUNT_DTOR(
        DatabaseConnection::UpdateRefcountFunction::DatabaseUpdateFunction);
  }

  bool Update(int64_t aId, int32_t aDelta);

  nsresult ErrorCode() const { return mErrorCode; }

 private:
  nsresult UpdateInternal(int64_t aId, int32_t aDelta);
};

class DatabaseConnection::UpdateRefcountFunction::FileInfoEntry final {
  SafeRefPtr<FileInfo> mFileInfo;
  int32_t mDelta;
  int32_t mSavepointDelta;

 public:
  explicit FileInfoEntry(SafeRefPtr<FileInfo> aFileInfo)
      : mFileInfo(std::move(aFileInfo)), mDelta(0), mSavepointDelta(0) {
    MOZ_COUNT_CTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry);
  }

  void IncDeltas(bool aUpdateSavepointDelta) {
    ++mDelta;
    if (aUpdateSavepointDelta) {
      ++mSavepointDelta;
    }
  }
  void DecDeltas(bool aUpdateSavepointDelta) {
    --mDelta;
    if (aUpdateSavepointDelta) {
      --mSavepointDelta;
    }
  }
  void DecBySavepointDelta() { mDelta -= mSavepointDelta; }
  SafeRefPtr<FileInfo> ReleaseFileInfo() { return std::move(mFileInfo); }
  void MaybeUpdateDBRefs() {
    if (mDelta) {
      mFileInfo->UpdateDBRefs(mDelta);
    }
  }

  int32_t Delta() const { return mDelta; }
  int32_t SavepointDelta() const { return mSavepointDelta; }

  ~FileInfoEntry() {
    MOZ_COUNT_DTOR(DatabaseConnection::UpdateRefcountFunction::FileInfoEntry);
  }
};

class ConnectionPool final {
 public:
  class FinishCallback;

 private:
  class ConnectionRunnable;
  class CloseConnectionRunnable;
  struct DatabaseInfo;
  struct DatabasesCompleteCallback;
  class FinishCallbackWrapper;
  class IdleConnectionRunnable;

  class ThreadRunnable;
  class TransactionInfo;
  struct TransactionInfoPair;

  struct IdleResource {
    TimeStamp mIdleTime;

    IdleResource(const IdleResource& aOther) = delete;
    IdleResource(IdleResource&& aOther) noexcept
        : IdleResource(aOther.mIdleTime) {}
    IdleResource& operator=(const IdleResource& aOther) = delete;
    IdleResource& operator=(IdleResource&& aOther) = delete;

   protected:
    explicit IdleResource(const TimeStamp& aIdleTime);

    ~IdleResource();
  };

  struct IdleDatabaseInfo final : public IdleResource {
    InitializedOnce<const NotNull<DatabaseInfo*>> mDatabaseInfo;

   public:
    explicit IdleDatabaseInfo(DatabaseInfo& aDatabaseInfo);

    IdleDatabaseInfo(const IdleDatabaseInfo& aOther) = delete;
    IdleDatabaseInfo(IdleDatabaseInfo&& aOther) noexcept
        : IdleResource(std::move(aOther)),
          mDatabaseInfo{std::move(aOther.mDatabaseInfo)} {
      MOZ_ASSERT(mDatabaseInfo);

      MOZ_COUNT_CTOR(ConnectionPool::IdleDatabaseInfo);
    }
    IdleDatabaseInfo& operator=(const IdleDatabaseInfo& aOther) = delete;
    IdleDatabaseInfo& operator=(IdleDatabaseInfo&& aOther) = delete;

    ~IdleDatabaseInfo();

    bool operator==(const IdleDatabaseInfo& aOther) const {
      return *mDatabaseInfo == *aOther.mDatabaseInfo;
    }

    bool operator==(const DatabaseInfo* aDatabaseInfo) const {
      return *mDatabaseInfo == aDatabaseInfo;
    }

    bool operator<(const IdleDatabaseInfo& aOther) const {
      return mIdleTime < aOther.mIdleTime;
    }
  };

  class ThreadInfo {
   public:
    ThreadInfo();

    ThreadInfo(nsCOMPtr<nsIThread> aThread, RefPtr<ThreadRunnable> aRunnable)
        : mThread{std::move(aThread)}, mRunnable{std::move(aRunnable)} {
      AssertIsOnBackgroundThread();
      AssertValid();

      MOZ_COUNT_CTOR(ConnectionPool::ThreadInfo);
    }

    ThreadInfo(const ThreadInfo& aOther) = delete;
    ThreadInfo& operator=(const ThreadInfo& aOther) = delete;

    ThreadInfo(ThreadInfo&& aOther) noexcept;
    ThreadInfo& operator=(ThreadInfo&& aOther) = default;

    bool IsValid() const {
      const bool res = mThread;
      if (res) {
        AssertValid();
      } else {
        AssertEmpty();
      }
      return res;
    }

    void AssertValid() const {
      MOZ_ASSERT(mThread);
      MOZ_ASSERT(mRunnable);
    }

    void AssertEmpty() const {
      MOZ_ASSERT(!mThread);
      MOZ_ASSERT(!mRunnable);
    }

    nsIThread& ThreadRef() {
      AssertValid();
      return *mThread;
    }

    std::tuple<nsCOMPtr<nsIThread>, RefPtr<ThreadRunnable>> Forget() {
      AssertValid();

      return {std::move(mThread), std::move(mRunnable)};
    }

    ~ThreadInfo();

    bool operator==(const ThreadInfo& aOther) const {
      return mThread == aOther.mThread && mRunnable == aOther.mRunnable;
    }

   private:
    nsCOMPtr<nsIThread> mThread;
    RefPtr<ThreadRunnable> mRunnable;
  };

  struct IdleThreadInfo final : public IdleResource {
    ThreadInfo mThreadInfo;

    explicit IdleThreadInfo(ThreadInfo aThreadInfo);

    IdleThreadInfo(const IdleThreadInfo& aOther) = delete;
    IdleThreadInfo(IdleThreadInfo&& aOther) noexcept
        : IdleResource(std::move(aOther)),
          mThreadInfo(std::move(aOther.mThreadInfo)) {
      AssertIsOnBackgroundThread();
      mThreadInfo.AssertValid();

      MOZ_COUNT_CTOR(ConnectionPool::IdleThreadInfo);
    }
    IdleThreadInfo& operator=(const IdleThreadInfo& aOther) = delete;
    IdleThreadInfo& operator=(IdleThreadInfo&& aOther) = delete;

    ~IdleThreadInfo();

    bool operator==(const IdleThreadInfo& aOther) const {
      return mThreadInfo == aOther.mThreadInfo;
    }

    bool operator<(const IdleThreadInfo& aOther) const {
      return mIdleTime < aOther.mIdleTime;
    }
  };

  // This mutex guards mDatabases, see below.
  Mutex mDatabasesMutex;

  nsTArray<IdleThreadInfo> mIdleThreads;
  nsTArray<IdleDatabaseInfo> mIdleDatabases;
  nsTArray<NotNull<DatabaseInfo*>> mDatabasesPerformingIdleMaintenance;
  nsCOMPtr<nsITimer> mIdleTimer;
  TimeStamp mTargetIdleTime;

  // Only modifed on the owning thread, but read on multiple threads. Therefore
  // all modifications and all reads off the owning thread must be protected by
  // mDatabasesMutex.
  nsClassHashtable<nsCStringHashKey, DatabaseInfo> mDatabases;

  nsClassHashtable<nsUint64HashKey, TransactionInfo> mTransactions;
  nsTArray<NotNull<TransactionInfo*>> mQueuedTransactions;

  nsTArray<UniquePtr<DatabasesCompleteCallback>> mCompleteCallbacks;

  uint64_t mNextTransactionId;
  uint32_t mTotalThreadCount;
  FlippedOnce<false> mShutdownRequested;
  FlippedOnce<false> mShutdownComplete;

 public:
  ConnectionPool();

  void AssertIsOnOwningThread() const {
    NS_ASSERT_OWNINGTHREAD(ConnectionPool);
  }

  Result<RefPtr<DatabaseConnection>, nsresult> GetOrCreateConnection(
      const Database& aDatabase);

  uint64_t Start(const nsID& aBackgroundChildLoggingId,
                 const nsACString& aDatabaseId, int64_t aLoggingSerialNumber,
                 const nsTArray<nsString>& aObjectStoreNames,
                 bool aIsWriteTransaction,
                 TransactionDatabaseOperationBase* aTransactionOp);

  void Dispatch(uint64_t aTransactionId, nsIRunnable* aRunnable);

  void Finish(uint64_t aTransactionId, FinishCallback* aCallback);

  void CloseDatabaseWhenIdle(const nsACString& aDatabaseId) {
    Unused << CloseDatabaseWhenIdleInternal(aDatabaseId);
  }

  void WaitForDatabasesToComplete(nsTArray<nsCString>&& aDatabaseIds,
                                  nsIRunnable* aCallback);

  void Shutdown();

  NS_INLINE_DECL_REFCOUNTING(ConnectionPool)

 private:
  ~ConnectionPool();

  static void IdleTimerCallback(nsITimer* aTimer, void* aClosure);

  void Cleanup();

  void AdjustIdleTimer();

  void CancelIdleTimer();

  void ShutdownThread(ThreadInfo aThreadInfo);

  void CloseIdleDatabases();

  void ShutdownIdleThreads();

  bool ScheduleTransaction(TransactionInfo& aTransactionInfo,
                           bool aFromQueuedTransactions);

  void NoteFinishedTransaction(uint64_t aTransactionId);

  void ScheduleQueuedTransactions(ThreadInfo aThreadInfo);

  void NoteIdleDatabase(DatabaseInfo& aDatabaseInfo);

  void NoteClosedDatabase(DatabaseInfo& aDatabaseInfo);

  bool MaybeFireCallback(DatabasesCompleteCallback* aCallback);

  void PerformIdleDatabaseMaintenance(DatabaseInfo& aDatabaseInfo);

  void CloseDatabase(DatabaseInfo& aDatabaseInfo);

  bool CloseDatabaseWhenIdleInternal(const nsACString& aDatabaseId);
};

class ConnectionPool::ConnectionRunnable : public Runnable {
 protected:
  DatabaseInfo& mDatabaseInfo;
  nsCOMPtr<nsIEventTarget> mOwningEventTarget;

  explicit ConnectionRunnable(DatabaseInfo& aDatabaseInfo);

  ~ConnectionRunnable() override = default;
};

class ConnectionPool::IdleConnectionRunnable final : public ConnectionRunnable {
  const bool mNeedsCheckpoint;

 public:
  IdleConnectionRunnable(DatabaseInfo& aDatabaseInfo, bool aNeedsCheckpoint)
      : ConnectionRunnable(aDatabaseInfo), mNeedsCheckpoint(aNeedsCheckpoint) {}

  NS_INLINE_DECL_REFCOUNTING_INHERITED(IdleConnectionRunnable,
                                       ConnectionRunnable)

 private:
  ~IdleConnectionRunnable() override = default;

  NS_DECL_NSIRUNNABLE
};

class ConnectionPool::CloseConnectionRunnable final
    : public ConnectionRunnable {
 public:
  explicit CloseConnectionRunnable(DatabaseInfo& aDatabaseInfo)
      : ConnectionRunnable(aDatabaseInfo) {}

  NS_INLINE_DECL_REFCOUNTING_INHERITED(CloseConnectionRunnable,
                                       ConnectionRunnable)

 private:
  ~CloseConnectionRunnable() override = default;

  NS_DECL_NSIRUNNABLE
};

struct ConnectionPool::DatabaseInfo final {
  friend class mozilla::DefaultDelete<DatabaseInfo>;

  RefPtr<ConnectionPool> mConnectionPool;
  const nsCString mDatabaseId;
  RefPtr<DatabaseConnection> mConnection;
  nsClassHashtable<nsStringHashKey, TransactionInfoPair> mBlockingTransactions;
  nsTArray<NotNull<TransactionInfo*>> mTransactionsScheduledDuringClose;
  nsTArray<NotNull<TransactionInfo*>> mScheduledWriteTransactions;
  Maybe<TransactionInfo&> mRunningWriteTransaction;
  ThreadInfo mThreadInfo;
  uint32_t mReadTransactionCount;
  uint32_t mWriteTransactionCount;
  bool mNeedsCheckpoint;
  bool mIdle;
  FlippedOnce<false> mCloseOnIdle;
  bool mClosing;

#ifdef DEBUG
  PRThread* mDEBUGConnectionThread;
#endif

  DatabaseInfo(ConnectionPool* aConnectionPool, const nsACString& aDatabaseId);

  void AssertIsOnConnectionThread() const {
    MOZ_ASSERT(mDEBUGConnectionThread);
    MOZ_ASSERT(PR_GetCurrentThread() == mDEBUGConnectionThread);
  }

  uint64_t TotalTransactionCount() const {
    return mReadTransactionCount + mWriteTransactionCount;
  }

 private:
  ~DatabaseInfo();

  DatabaseInfo(const DatabaseInfo&) = delete;
  DatabaseInfo& operator=(const DatabaseInfo&) = delete;
};

struct ConnectionPool::DatabasesCompleteCallback final {
  friend class DefaultDelete<DatabasesCompleteCallback>;

  nsTArray<nsCString> mDatabaseIds;
  nsCOMPtr<nsIRunnable> mCallback;

  DatabasesCompleteCallback(nsTArray<nsCString>&& aDatabaseIds,
                            nsIRunnable* aCallback);

 private:
  ~DatabasesCompleteCallback();
};

class NS_NO_VTABLE ConnectionPool::FinishCallback : public nsIRunnable {
 public:
  // Called on the owning thread before any additional transactions are
  // unblocked.
  virtual void TransactionFinishedBeforeUnblock() = 0;

  // Called on the owning thread after additional transactions may have been
  // unblocked.
  virtual void TransactionFinishedAfterUnblock() = 0;

 protected:
  FinishCallback() = default;

  virtual ~FinishCallback() = default;
};

class ConnectionPool::FinishCallbackWrapper final : public Runnable {
  RefPtr<ConnectionPool> mConnectionPool;
  RefPtr<FinishCallback> mCallback;
  nsCOMPtr<nsIEventTarget> mOwningEventTarget;
  uint64_t mTransactionId;
  bool mHasRunOnce;

 public:
  FinishCallbackWrapper(ConnectionPool* aConnectionPool,
                        uint64_t aTransactionId, FinishCallback* aCallback);

  NS_INLINE_DECL_REFCOUNTING_INHERITED(FinishCallbackWrapper, Runnable)

 private:
  ~FinishCallbackWrapper() override;

  NS_DECL_NSIRUNNABLE
};

class ConnectionPool::ThreadRunnable final : public Runnable {
  // Only touched on the background thread.
  static uint32_t sNextSerialNumber;

  // Set at construction for logging.
  const uint32_t mSerialNumber;

  // These two values are only modified on the connection thread.
  FlippedOnce<true> mFirstRun;
  FlippedOnce<true> mContinueRunning;

 public:
  ThreadRunnable();

  NS_INLINE_DECL_REFCOUNTING_INHERITED(ThreadRunnable, Runnable)

  uint32_t SerialNumber() const { return mSerialNumber; }

  nsCString GetThreadName() const {
    return nsPrintfCString("IndexedDB #%" PRIu32, mSerialNumber);
  }

 private:
  ~ThreadRunnable() override;

  NS_DECL_NSIRUNNABLE
};

class ConnectionPool::TransactionInfo final {
  friend class mozilla::DefaultDelete<TransactionInfo>;

  nsTHashtable<nsPtrHashKey<TransactionInfo>> mBlocking;
  nsTArray<NotNull<TransactionInfo*>> mBlockingOrdered;

 public:
  DatabaseInfo& mDatabaseInfo;
  const nsID mBackgroundChildLoggingId;
  const nsCString mDatabaseId;
  const uint64_t mTransactionId;
  const int64_t mLoggingSerialNumber;
  const nsTArray<nsString> mObjectStoreNames;
  nsTHashtable<nsPtrHashKey<TransactionInfo>> mBlockedOn;
  nsTArray<nsCOMPtr<nsIRunnable>> mQueuedRunnables;
  const bool mIsWriteTransaction;
  bool mRunning;

#ifdef DEBUG
  FlippedOnce<false> mFinished;
#endif

  TransactionInfo(DatabaseInfo& aDatabaseInfo,
                  const nsID& aBackgroundChildLoggingId,
                  const nsACString& aDatabaseId, uint64_t aTransactionId,
                  int64_t aLoggingSerialNumber,
                  const nsTArray<nsString>& aObjectStoreNames,
                  bool aIsWriteTransaction,
                  TransactionDatabaseOperationBase* aTransactionOp);

  void AddBlockingTransaction(TransactionInfo& aTransactionInfo);

  void RemoveBlockingTransactions();

 private:
  ~TransactionInfo();

  void MaybeUnblock(TransactionInfo& aTransactionInfo);
};

struct ConnectionPool::TransactionInfoPair final {
  // Multiple reading transactions can block future writes.
  nsTArray<NotNull<TransactionInfo*>> mLastBlockingWrites;
  // But only a single writing transaction can block future reads.
  Maybe<TransactionInfo&> mLastBlockingReads;

#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING)
  TransactionInfoPair();
  ~TransactionInfoPair();
#endif
};

/*******************************************************************************
 * Actor class declarations
 ******************************************************************************/

template <IDBCursorType CursorType>
class CommonOpenOpHelper;
template <IDBCursorType CursorType>
class IndexOpenOpHelper;
template <IDBCursorType CursorType>
class ObjectStoreOpenOpHelper;
template <IDBCursorType CursorType>
class OpenOpHelper;

class DatabaseOperationBase : public Runnable,
                              public mozIStorageProgressHandler {
  friend class UpgradeFileIdsFunction;
  template <IDBCursorType CursorType>
  friend class OpenOpHelper;

 protected:
  class AutoSetProgressHandler;

  typedef nsDataHashtable<nsUint64HashKey, bool> UniqueIndexTable;

  const nsCOMPtr<nsIEventTarget> mOwningEventTarget;
  const nsID mBackgroundChildLoggingId;
  const uint64_t mLoggingSerialNumber;

 private:
  nsresult mResultCode = NS_OK;
  Atomic<bool> mOperationMayProceed;
  FlippedOnce<false> mActorDestroyed;

 public:
  NS_DECL_ISUPPORTS_INHERITED

  bool IsOnOwningThread() const {
    MOZ_ASSERT(mOwningEventTarget);

    bool current;
    return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(&current)) &&
           current;
  }

  void AssertIsOnOwningThread() const {
    MOZ_ASSERT(IsOnBackgroundThread());
    MOZ_ASSERT(IsOnOwningThread());
  }

  void NoteActorDestroyed() {
    AssertIsOnOwningThread();

    mActorDestroyed.Flip();
    mOperationMayProceed = false;
  }

  bool IsActorDestroyed() const {
    AssertIsOnOwningThread();

    return mActorDestroyed;
  }

  // May be called on any thread, but you should call IsActorDestroyed() if
  // you know you're on the background thread because it is slightly faster.
  bool OperationMayProceed() const { return mOperationMayProceed; }

  const nsID& BackgroundChildLoggingId() const {
    return mBackgroundChildLoggingId;
  }

  uint64_t LoggingSerialNumber() const { return mLoggingSerialNumber; }

  nsresult ResultCode() const { return mResultCode; }

  void SetFailureCode(nsresult aFailureCode) {
    MOZ_ASSERT(NS_SUCCEEDED(mResultCode));
    OverrideFailureCode(aFailureCode);
  }

  void SetFailureCodeIfUnset(nsresult aFailureCode) {
    if (NS_SUCCEEDED(mResultCode)) {
      OverrideFailureCode(aFailureCode);
    }
  }

  bool HasFailed() const { return NS_FAILED(mResultCode); }

  static Result<StructuredCloneReadInfoParent, nsresult>
  GetStructuredCloneReadInfoFromStatement(mozIStorageStatement* aStatement,
                                          uint32_t aDataIndex,
                                          uint32_t aFileIdsIndex,
                                          const FileManager& aFileManager) {
    return GetStructuredCloneReadInfoFromSource(aStatement, aDataIndex,
                                                aFileIdsIndex, aFileManager);
  }

 protected:
  DatabaseOperationBase(const nsID& aBackgroundChildLoggingId,
                        uint64_t aLoggingSerialNumber)
      : Runnable("dom::indexedDB::DatabaseOperationBase"),
        mOwningEventTarget(GetCurrentEventTarget()),
        mBackgroundChildLoggingId(aBackgroundChildLoggingId),
        mLoggingSerialNumber(aLoggingSerialNumber),
        mOperationMayProceed(true) {
    AssertIsOnOwningThread();
  }

  ~DatabaseOperationBase() override { MOZ_ASSERT(mActorDestroyed); }

  void OverrideFailureCode(nsresult aFailureCode) {
    MOZ_ASSERT(NS_FAILED(aFailureCode));

    mResultCode = aFailureCode;
  }

  static nsAutoCString MaybeGetBindingClauseForKeyRange(
      const Maybe<SerializedKeyRange>& aOptionalKeyRange,
      const nsACString& aKeyColumnName);

  static nsAutoCString GetBindingClauseForKeyRange(
      const SerializedKeyRange& aKeyRange, const nsACString& aKeyColumnName);

  static uint64_t ReinterpretDoubleAsUInt64(double aDouble);

  static Result<StructuredCloneReadInfoParent, nsresult>
  GetStructuredCloneReadInfoFromValueArray(mozIStorageValueArray* aValues,
                                           uint32_t aDataIndex,
                                           uint32_t aFileIdsIndex,
                                           const FileManager& aFileManager) {
    return GetStructuredCloneReadInfoFromSource(aValues, aDataIndex,
                                                aFileIdsIndex, aFileManager);
  }

  static nsresult BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange,
                                          mozIStorageStatement* aStatement);

  static nsresult BindKeyRangeToStatement(const SerializedKeyRange& aKeyRange,
                                          mozIStorageStatement* aStatement,
                                          const nsCString& aLocale);

  static nsresult IndexDataValuesFromUpdateInfos(
      const nsTArray<IndexUpdateInfo>& aUpdateInfos,
      const UniqueIndexTable& aUniqueIndexTable,
      nsTArray<IndexDataValue>& aIndexValues);

  static nsresult InsertIndexTableRows(
      DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId,
      const Key& aObjectStoreKey, const nsTArray<IndexDataValue>& aIndexValues);

  static nsresult DeleteIndexDataTableRows(
      DatabaseConnection* aConnection, const Key& aObjectStoreKey,
      const nsTArray<IndexDataValue>& aIndexValues);

  static nsresult DeleteObjectStoreDataTableRowsWithIndexes(
      DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId,
      const Maybe<SerializedKeyRange>& aKeyRange);

  static nsresult UpdateIndexValues(
      DatabaseConnection* aConnection, IndexOrObjectStoreId aObjectStoreId,
      const Key& aObjectStoreKey, const nsTArray<IndexDataValue>& aIndexValues);

  static nsresult ObjectStoreHasIndexes(DatabaseConnection* aConnection,

                                        IndexOrObjectStoreId aObjectStoreId,
                                        bool* aHasIndexes);

 private:
  template <typename T>
  static Result<StructuredCloneReadInfoParent, nsresult>
  GetStructuredCloneReadInfoFromSource(T* aSource, uint32_t aDataIndex,
                                       uint32_t aFileIdsIndex,
                                       const FileManager& aFileManager);

  static Result<StructuredCloneReadInfoParent, nsresult>
  GetStructuredCloneReadInfoFromBlob(const uint8_t* aBlobData,
                                     uint32_t aBlobDataLength,
                                     const FileManager& aFileManager,
                                     const nsAString& aFileIds);

  static Result<StructuredCloneReadInfoParent, nsresult>
  GetStructuredCloneReadInfoFromExternalBlob(uint64_t aIntData,
                                             const FileManager& aFileManager,
                                             const nsAString& aFileIds);

  template <typename KeyTransformation>
  static nsresult MaybeBindKeyToStatement(
      const Key& aKey, mozIStorageStatement* aStatement,
      const nsCString& aParameterName,
      const KeyTransformation& aKeyTransformation);

  template <typename KeyTransformation>
  static nsresult BindTransformedKeyRangeToStatement(
      const SerializedKeyRange& aKeyRange, mozIStorageStatement* aStatement,
      const KeyTransformation& aKeyTransformation);

  // Not to be overridden by subclasses.
  NS_DECL_MOZISTORAGEPROGRESSHANDLER
};

class MOZ_STACK_CLASS DatabaseOperationBase::AutoSetProgressHandler final {
  Maybe<mozIStorageConnection&> mConnection;
#ifdef DEBUG
  DatabaseOperationBase* mDEBUGDatabaseOp;
#endif

 public:
  AutoSetProgressHandler();

  ~AutoSetProgressHandler();

  nsresult Register(mozIStorageConnection& aConnection,
                    DatabaseOperationBase* aDatabaseOp);

  void Unregister();
};

class TransactionDatabaseOperationBase : public DatabaseOperationBase {
  enum class InternalState {
    Initial,
    DatabaseWork,
    SendingPreprocess,
    WaitingForContinue,
    SendingResults,
    Completed
  };

  InitializedOnce<const NotNull<SafeRefPtr<TransactionBase>>> mTransaction;
  InternalState mInternalState = InternalState::Initial;
  bool mWaitingForContinue = false;
  const bool mTransactionIsAborted;

 protected:
  const int64_t mTransactionLoggingSerialNumber;

#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
 protected:
  // A check only enables when the diagnostic assert turns on. It assumes the
  // mUpdateRefcountFunction is a nullptr because the previous
  // StartTransactionOp failed on the connection thread and the next write
  // operation (e.g. ObjectstoreAddOrPutRequestOp) doesn't have enough time to
  // catch up the failure information.
  bool mAssumingPreviousOperationFail = false;
#endif

 public:
  void AssertIsOnConnectionThread() const
#ifdef DEBUG
      ;
#else
  {
  }
#endif

  uint64_t StartOnConnectionPool(const nsID& aBackgroundChildLoggingId,
                                 const nsACString& aDatabaseId,
                                 int64_t aLoggingSerialNumber,
                                 const nsTArray<nsString>& aObjectStoreNames,
                                 bool aIsWriteTransaction);

  void DispatchToConnectionPool();

  TransactionBase& Transaction() { return **mTransaction; }

  const TransactionBase& Transaction() const { return **mTransaction; }

  bool IsWaitingForContinue() const {
    AssertIsOnOwningThread();

    return mWaitingForContinue;
  }

  void NoteContinueReceived();

  int64_t TransactionLoggingSerialNumber() const {
    return mTransactionLoggingSerialNumber;
  }

  // May be overridden by subclasses if they need to perform work on the
  // background thread before being dispatched. Returning false will kill the
  // child actors and prevent dispatch.
  virtual bool Init(TransactionBase& aTransaction);

  // This callback will be called on the background thread before releasing the
  // final reference to this request object. Subclasses may perform any
  // additional cleanup here but must always call the base class implementation.
  virtual void Cleanup();

 protected:
  explicit TransactionDatabaseOperationBase(
      SafeRefPtr<TransactionBase> aTransaction);

  TransactionDatabaseOperationBase(SafeRefPtr<TransactionBase> aTransaction,
                                   uint64_t aLoggingSerialNumber);

  ~TransactionDatabaseOperationBase() override;

  virtual void RunOnConnectionThread();

  // Must be overridden in subclasses. Called on the target thread to allow the
  // subclass to perform necessary database or file operations. A successful
  // return value will trigger a SendSuccessResult callback on the background
  // thread while a failure value will trigger a SendFailureResult callback.
  virtual nsresult DoDatabaseWork(DatabaseConnection* aConnection) = 0;

  // May be overriden in subclasses. Called on the background thread to decide
  // if the subclass needs to send any preprocess info to the child actor.
  virtual bool HasPreprocessInfo();

  // May be overriden in subclasses. Called on the background thread to allow
  // the subclass to serialize its preprocess info and send it to the child
  // actor. A successful return value will trigger a wait for a
  // NoteContinueReceived callback on the background thread while a failure
  // value will trigger a SendFailureResult callback.
  virtual nsresult SendPreprocessInfo();

  // Must be overridden in subclasses. Called on the background thread to allow
  // the subclass to serialize its results and send them to the child actor. A
  // failed return value will trigger a SendFailureResult callback.
  virtual nsresult SendSuccessResult() = 0;

  // Must be overridden in subclasses. Called on the background thread to allow
  // the subclass to send its failure code. Returning false will cause the
  // transaction to be aborted with aResultCode. Returning true will not cause
  // the transaction to be aborted.
  virtual bool SendFailureResult(nsresult aResultCode) = 0;

 private:
  void SendToConnectionPool();

  void SendPreprocess();

  void SendResults();

  void SendPreprocessInfoOrResults(bool aSendPreprocessInfo);

  // Not to be overridden by subclasses.
  NS_DECL_NSIRUNNABLE
};

class Factory final : public PBackgroundIDBFactoryParent,
                      public AtomicSafeRefCounted<Factory> {
  RefPtr<DatabaseLoggingInfo> mLoggingInfo;

#ifdef DEBUG
  bool mActorDestroyed;
#endif

  // Reference counted.
  ~Factory() override;

 public:
  [[nodiscard]] static SafeRefPtr<Factory> Create(
      const LoggingInfo& aLoggingInfo);

  DatabaseLoggingInfo* GetLoggingInfo() const {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mLoggingInfo);

    return mLoggingInfo;
  }

  MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::Factory)
  MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(Factory, AtomicSafeRefCounted)

  // Only constructed in Create().
  explicit Factory(RefPtr<DatabaseLoggingInfo> aLoggingInfo);

  // IPDL methods are only called by IPDL.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvDeleteMe() override;

  PBackgroundIDBFactoryRequestParent* AllocPBackgroundIDBFactoryRequestParent(
      const FactoryRequestParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBFactoryRequestConstructor(
      PBackgroundIDBFactoryRequestParent* aActor,
      const FactoryRequestParams& aParams) override;

  bool DeallocPBackgroundIDBFactoryRequestParent(
      PBackgroundIDBFactoryRequestParent* aActor) override;

  PBackgroundIDBDatabaseParent* AllocPBackgroundIDBDatabaseParent(
      const DatabaseSpec& aSpec,
      PBackgroundIDBFactoryRequestParent* aRequest) override;

  bool DeallocPBackgroundIDBDatabaseParent(
      PBackgroundIDBDatabaseParent* aActor) override;
};

class WaitForTransactionsHelper final : public Runnable {
  const nsCString mDatabaseId;
  nsCOMPtr<nsIRunnable> mCallback;

  enum class State {
    Initial = 0,
    WaitingForTransactions,
    WaitingForFileHandles,
    Complete
  } mState;

 public:
  WaitForTransactionsHelper(const nsCString& aDatabaseId,
                            nsIRunnable* aCallback)
      : Runnable("dom::indexedDB::WaitForTransactionsHelper"),
        mDatabaseId(aDatabaseId),
        mCallback(aCallback),
        mState(State::Initial) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(!aDatabaseId.IsEmpty());
    MOZ_ASSERT(aCallback);
  }

  void WaitForTransactions();

  NS_INLINE_DECL_REFCOUNTING_INHERITED(WaitForTransactionsHelper, Runnable)

 private:
  ~WaitForTransactionsHelper() override {
    MOZ_ASSERT(!mCallback);
    MOZ_ASSERT(mState == State::Complete);
  }

  void MaybeWaitForTransactions();

  void MaybeWaitForFileHandles();

  void CallCallback();

  NS_DECL_NSIRUNNABLE
};

class Database final
    : public PBackgroundIDBDatabaseParent,
      public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>>,
      public AtomicSafeRefCounted<Database> {
  friend class VersionChangeTransaction;

  class StartTransactionOp;
  class UnmapBlobCallback;

 private:
  SafeRefPtr<Factory> mFactory;
  SafeRefPtr<FullDatabaseMetadata> mMetadata;
  SafeRefPtr<FileManager> mFileManager;
  RefPtr<DirectoryLock> mDirectoryLock;
  nsTHashtable<nsPtrHashKey<TransactionBase>> mTransactions;
  nsTHashtable<nsPtrHashKey<MutableFile>> mMutableFiles;
  nsRefPtrHashtable<nsIDHashKey, FileInfo> mMappedBlobs;
  RefPtr<DatabaseConnection> mConnection;
  const PrincipalInfo mPrincipalInfo;
  const Maybe<ContentParentId> mOptionalContentParentId;
  const nsCString mGroup;
  const nsCString mOrigin;
  const nsCString mId;
  const nsString mFilePath;
  uint32_t mActiveMutableFileCount;
  uint32_t mPendingCreateFileOpCount;
  int64_t mDirectoryLockId;
  const uint32_t mTelemetryId;
  const PersistenceType mPersistenceType;
  const bool mFileHandleDisabled;
  const bool mChromeWriteAccessAllowed;
  FlippedOnce<false> mClosed;
  FlippedOnce<false> mInvalidated;
  FlippedOnce<false> mActorWasAlive;
  FlippedOnce<false> mActorDestroyed;
  nsCOMPtr<nsIEventTarget> mBackgroundThread;
#ifdef DEBUG
  bool mAllBlobsUnmapped;
#endif

 public:
  // Created by OpenDatabaseOp.
  Database(SafeRefPtr<Factory> aFactory, const PrincipalInfo& aPrincipalInfo,
           const Maybe<ContentParentId>& aOptionalContentParentId,
           const nsACString& aGroup, const nsACString& aOrigin,
           uint32_t aTelemetryId, SafeRefPtr<FullDatabaseMetadata> aMetadata,
           SafeRefPtr<FileManager> aFileManager,
           RefPtr<DirectoryLock> aDirectoryLock, bool aFileHandleDisabled,
           bool aChromeWriteAccessAllowed);

  void AssertIsOnConnectionThread() const {
#ifdef DEBUG
    if (mConnection) {
      MOZ_ASSERT(mConnection);
      mConnection->AssertIsOnConnectionThread();
    } else {
      MOZ_ASSERT(!NS_IsMainThread());
      MOZ_ASSERT(!IsOnBackgroundThread());
      MOZ_ASSERT(mInvalidated);
    }
#endif
  }

  MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::Database)

  void Invalidate();

  bool IsOwnedByProcess(ContentParentId aContentParentId) const {
    return mOptionalContentParentId &&
           mOptionalContentParentId.value() == aContentParentId;
  }

  const nsCString& Group() const { return mGroup; }

  const nsCString& Origin() const { return mOrigin; }

  const nsCString& Id() const { return mId; }

  int64_t DirectoryLockId() const { return mDirectoryLockId; }

  uint32_t TelemetryId() const { return mTelemetryId; }

  PersistenceType Type() const { return mPersistenceType; }

  const nsString& FilePath() const { return mFilePath; }

  FileManager& GetFileManager() const { return *mFileManager; }

  MovingNotNull<SafeRefPtr<FileManager>> GetFileManagerPtr() const {
    return WrapMovingNotNull(mFileManager.clonePtr());
  }

  const FullDatabaseMetadata& Metadata() const {
    MOZ_ASSERT(mMetadata);
    return *mMetadata;
  }

  SafeRefPtr<FullDatabaseMetadata> MetadataPtr() const {
    MOZ_ASSERT(mMetadata);
    return mMetadata.clonePtr();
  }

  PBackgroundParent* GetBackgroundParent() const {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(!IsActorDestroyed());

    return Manager()->Manager();
  }

  DatabaseLoggingInfo* GetLoggingInfo() const {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mFactory);

    return mFactory->GetLoggingInfo();
  }

  bool RegisterTransaction(TransactionBase& aTransaction);

  void UnregisterTransaction(TransactionBase& aTransaction);

  bool IsFileHandleDisabled() const { return mFileHandleDisabled; }

  bool RegisterMutableFile(MutableFile* aMutableFile);

  void UnregisterMutableFile(MutableFile* aMutableFile);

  void NoteActiveMutableFile();

  void NoteInactiveMutableFile();

  void NotePendingCreateFileOp();

  void NoteCompletedCreateFileOp();

  void SetActorAlive();

  void MapBlob(const IPCBlob& aIPCBlob, SafeRefPtr<FileInfo> aFileInfo);

  bool IsActorAlive() const {
    AssertIsOnBackgroundThread();

    return mActorWasAlive && !mActorDestroyed;
  }

  bool IsActorDestroyed() const {
    AssertIsOnBackgroundThread();

    return mActorWasAlive && mActorDestroyed;
  }

  bool IsClosed() const {
    AssertIsOnBackgroundThread();

    return mClosed;
  }

  bool IsInvalidated() const {
    AssertIsOnBackgroundThread();

    return mInvalidated;
  }

  nsresult EnsureConnection();

  DatabaseConnection* GetConnection() const {
#ifdef DEBUG
    if (mConnection) {
      mConnection->AssertIsOnConnectionThread();
    }
#endif

    return mConnection;
  }

  void Stringify(nsACString& aResult) const;

  ~Database() override {
    MOZ_ASSERT(mClosed);
    MOZ_ASSERT_IF(mActorWasAlive, mActorDestroyed);

    NS_ProxyRelease("ReleaseIDBFactory", mBackgroundThread.get(),
                    mFactory.forget());
  }

 private:
  [[nodiscard]] SafeRefPtr<FileInfo> GetBlob(const IPCBlob& aID);

  void UnmapBlob(const nsID& aID);

  void UnmapAllBlobs();

  bool CloseInternal();

  void MaybeCloseConnection();

  void ConnectionClosedCallback();

  void CleanupMetadata();

  bool VerifyRequestParams(const DatabaseRequestParams& aParams) const;

  // IPDL methods are only called by IPDL.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  PBackgroundIDBDatabaseFileParent* AllocPBackgroundIDBDatabaseFileParent(
      const IPCBlob& aIPCBlob) override;

  bool DeallocPBackgroundIDBDatabaseFileParent(
      PBackgroundIDBDatabaseFileParent* aActor) override;

  PBackgroundIDBDatabaseRequestParent* AllocPBackgroundIDBDatabaseRequestParent(
      const DatabaseRequestParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBDatabaseRequestConstructor(
      PBackgroundIDBDatabaseRequestParent* aActor,
      const DatabaseRequestParams& aParams) override;

  bool DeallocPBackgroundIDBDatabaseRequestParent(
      PBackgroundIDBDatabaseRequestParent* aActor) override;

  already_AddRefed<PBackgroundIDBTransactionParent>
  AllocPBackgroundIDBTransactionParent(
      const nsTArray<nsString>& aObjectStoreNames, const Mode& aMode) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBTransactionConstructor(
      PBackgroundIDBTransactionParent* aActor,
      nsTArray<nsString>&& aObjectStoreNames, const Mode& aMode) override;

  PBackgroundMutableFileParent* AllocPBackgroundMutableFileParent(
      const nsString& aName, const nsString& aType) override;

  bool DeallocPBackgroundMutableFileParent(
      PBackgroundMutableFileParent* aActor) override;

  mozilla::ipc::IPCResult RecvDeleteMe() override;

  mozilla::ipc::IPCResult RecvBlocked() override;

  mozilla::ipc::IPCResult RecvClose() override;

  template <typename T>
  static bool InvalidateAll(const nsTHashtable<nsPtrHashKey<T>>& aTable);
};

class Database::StartTransactionOp final
    : public TransactionDatabaseOperationBase {
  friend class Database;

 private:
  explicit StartTransactionOp(SafeRefPtr<TransactionBase> aTransaction)
      : TransactionDatabaseOperationBase(std::move(aTransaction),
                                         /* aLoggingSerialNumber */ 0) {}

  ~StartTransactionOp() override = default;

  void RunOnConnectionThread() override;

  nsresult DoDatabaseWork(DatabaseConnection* aConnection) override;

  nsresult SendSuccessResult() override;

  bool SendFailureResult(nsresult aResultCode) override;

  void Cleanup() override;
};

class Database::UnmapBlobCallback final
    : public RemoteLazyInputStreamParentCallback {
  SafeRefPtr<Database> mDatabase;

 public:
  explicit UnmapBlobCallback(SafeRefPtr<Database> aDatabase)
      : mDatabase(std::move(aDatabase)) {
    AssertIsOnBackgroundThread();
  }

  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Database::UnmapBlobCallback, override)

  void ActorDestroyed(const nsID& aID) override {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mDatabase);

    const SafeRefPtr<Database> database = std::move(mDatabase);
    database->UnmapBlob(aID);
  }

 private:
  ~UnmapBlobCallback() = default;
};

/**
 * In coordination with IDBDatabase's mFileActors weak-map on the child side, a
 * long-lived mapping from a child process's live Blobs to their corresponding
 * FileInfo in our owning database.  Assists in avoiding redundant IPC traffic
 * and disk storage.  This includes both:
 * - Blobs retrieved from this database and sent to the child that do not need
 *   to be written to disk because they already exist on disk in this database's
 *   files directory.
 * - Blobs retrieved from other databases or from anywhere else that will need
 *   to be written to this database's files directory.  In this case we will
 *   hold a reference to its BlobImpl in mBlobImpl until we have successfully
 *   written the Blob to disk.
 *
 * Relevant Blob context: Blobs sent from the parent process to child processes
 * are automatically linked back to their source BlobImpl when the child process
 * references the Blob via IPC. This is done using the internal IPCBlob
 * inputStream actor ID to FileInfo mapping. However, when getting an actor
 * in the child process for sending an in-child-created Blob to the parent
 * process, there is (currently) no Blob machinery to automatically establish
 * and reuse a long-lived Actor.  As a result, without IDB's weak-map
 * cleverness, a memory-backed Blob repeatedly sent from the child to the parent
 * would appear as a different Blob each time, requiring the Blob data to be
 * sent over IPC each time as well as potentially needing to be written to disk
 * each time.
 *
 * This object remains alive as long as there is an active child actor or an
 * ObjectStoreAddOrPutRequestOp::StoredFileInfo for a queued or active add/put
 * op is holding a reference to us.
 */
class DatabaseFile final : public PBackgroundIDBDatabaseFileParent {
  // mBlobImpl's ownership lifecycle:
  // - Initialized on the background thread at creation time.  Then
  //   responsibility is handed off to the connection thread.
  // - Checked and used by the connection thread to generate a stream to write
  //   the blob to disk by an add/put operation.
  // - Cleared on the connection thread once the file has successfully been
  //   written to disk.
  InitializedOnce<const RefPtr<BlobImpl>> mBlobImpl;
  const SafeRefPtr<FileInfo> mFileInfo;

 public:
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::indexedDB::DatabaseFile);

  const FileInfo& GetFileInfo() const {
    AssertIsOnBackgroundThread();

    return *mFileInfo;
  }

  SafeRefPtr<FileInfo> GetFileInfoPtr() const {
    AssertIsOnBackgroundThread();

    return mFileInfo.clonePtr();
  }

  /**
   * If mBlobImpl is non-null (implying the contents of this file have not yet
   * been written to disk), then return an input stream. Otherwise, if mBlobImpl
   * is null (because the contents have been written to disk), returns null.
   */
  [[nodiscard]] nsCOMPtr<nsIInputStream> GetInputStream(ErrorResult& rv) const;

  /**
   * To be called upon successful copying of the stream GetInputStream()
   * returned so that we won't try and redundantly write the file to disk in the
   * future.  This is a separate step from GetInputStream() because
   * the write could fail due to quota errors that happen now but that might
   * not happen in a future attempt.
   */
  void WriteSucceededClearBlobImpl() {
    MOZ_ASSERT(!IsOnBackgroundThread());

    MOZ_ASSERT(*mBlobImpl);
    mBlobImpl.destroy();
  }

 public:
  // Called when sending to the child.
  explicit DatabaseFile(SafeRefPtr<FileInfo> aFileInfo)
      : mBlobImpl{nullptr}, mFileInfo(std::move(aFileInfo)) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mFileInfo);
  }

  // Called when receiving from the child.
  DatabaseFile(RefPtr<BlobImpl> aBlobImpl, SafeRefPtr<FileInfo> aFileInfo)
      : mBlobImpl(std::move(aBlobImpl)), mFileInfo(std::move(aFileInfo)) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(*mBlobImpl);
    MOZ_ASSERT(mFileInfo);
  }

 private:
  ~DatabaseFile() override = default;

  void ActorDestroy(ActorDestroyReason aWhy) override {
    AssertIsOnBackgroundThread();
  }
};

nsCOMPtr<nsIInputStream> DatabaseFile::GetInputStream(ErrorResult& rv) const {
  // We should only be called from our DB connection thread, not the background
  // thread.
  MOZ_ASSERT(!IsOnBackgroundThread());

  // If we were constructed without a BlobImpl, or WriteSucceededClearBlobImpl
  // was already called, return nullptr.
  if (!mBlobImpl || !*mBlobImpl) {
    return nullptr;
  }

  nsCOMPtr<nsIInputStream> inputStream;
  (*mBlobImpl)->CreateInputStream(getter_AddRefs(inputStream), rv);
  if (rv.Failed()) {
    return nullptr;
  }

  return inputStream;
}

class TransactionBase : public AtomicSafeRefCounted<TransactionBase> {
  friend class CursorBase;

  template <IDBCursorType CursorType>
  friend class Cursor;

  class CommitOp;

 protected:
  typedef IDBTransaction::Mode Mode;

 private:
  const SafeRefPtr<Database> mDatabase;
  nsTArray<RefPtr<FullObjectStoreMetadata>>
      mModifiedAutoIncrementObjectStoreMetadataArray;
  LazyInitializedOnceNotNull<const uint64_t> mTransactionId;
  const nsCString mDatabaseId;
  const int64_t mLoggingSerialNumber;
  uint64_t mActiveRequestCount;
  Atomic<bool> mInvalidatedOnAnyThread;
  const Mode mMode;
  FlippedOnce<false> mInitialized;
  FlippedOnce<false> mHasBeenActiveOnConnectionThread;
  FlippedOnce<false> mActorDestroyed;
  FlippedOnce<false> mInvalidated;

 protected:
  nsresult mResultCode;
  FlippedOnce<false> mCommitOrAbortReceived;
  FlippedOnce<false> mCommittedOrAborted;
  FlippedOnce<false> mForceAborted;
  LazyInitializedOnce<const Maybe<int64_t>> mLastRequestBeforeCommit;
  Maybe<int64_t> mLastFailedRequest;

 public:
  void AssertIsOnConnectionThread() const {
    MOZ_ASSERT(mDatabase);
    mDatabase->AssertIsOnConnectionThread();
  }

  bool IsActorDestroyed() const {
    AssertIsOnBackgroundThread();

    return mActorDestroyed;
  }

  // Must be called on the background thread.
  bool IsInvalidated() const {
    MOZ_ASSERT(IsOnBackgroundThread(), "Use IsInvalidatedOnAnyThread()");
    MOZ_ASSERT_IF(mInvalidated, NS_FAILED(mResultCode));

    return mInvalidated;
  }

  // May be called on any thread, but is more expensive than IsInvalidated().
  bool IsInvalidatedOnAnyThread() const { return mInvalidatedOnAnyThread; }

  void Init(const uint64_t aTransactionId) {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(aTransactionId);

    mTransactionId.init(aTransactionId);
    mInitialized.Flip();
  }

  void SetActiveOnConnectionThread() {
    AssertIsOnConnectionThread();
    mHasBeenActiveOnConnectionThread.Flip();
  }

  MOZ_DECLARE_REFCOUNTED_TYPENAME(mozilla::dom::indexedDB::TransactionBase)

  void Abort(nsresult aResultCode, bool aForce);

  uint64_t TransactionId() const { return *mTransactionId; }

  const nsCString& DatabaseId() const { return mDatabaseId; }

  Mode GetMode() const { return mMode; }

  const Database& GetDatabase() const {
    MOZ_ASSERT(mDatabase);

    return *mDatabase;
  }

  Database& GetMutableDatabase() const {
    MOZ_ASSERT(mDatabase);

    return *mDatabase;
  }

  SafeRefPtr<Database> GetDatabasePtr() const {
    MOZ_ASSERT(mDatabase);

    return mDatabase.clonePtr();
  }

  DatabaseLoggingInfo* GetLoggingInfo() const {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(mDatabase);

    return mDatabase->GetLoggingInfo();
  }

  int64_t LoggingSerialNumber() const { return mLoggingSerialNumber; }

  bool IsAborted() const {
    AssertIsOnBackgroundThread();

    return NS_FAILED(mResultCode);
  }

  [[nodiscard]] RefPtr<FullObjectStoreMetadata> GetMetadataForObjectStoreId(
      IndexOrObjectStoreId aObjectStoreId) const;

  [[nodiscard]] RefPtr<FullIndexMetadata> GetMetadataForIndexId(
      FullObjectStoreMetadata* const aObjectStoreMetadata,
      IndexOrObjectStoreId aIndexId) const;

  PBackgroundParent* GetBackgroundParent() const {
    AssertIsOnBackgroundThread();
    MOZ_ASSERT(!IsActorDestroyed());

    return GetDatabase().GetBackgroundParent();
  }

  void NoteModifiedAutoIncrementObjectStore(FullObjectStoreMetadata* aMetadata);

  void ForgetModifiedAutoIncrementObjectStore(
      FullObjectStoreMetadata* aMetadata);

  void NoteActiveRequest();

  void NoteFinishedRequest(int64_t aRequestId, nsresult aResultCode);

  void Invalidate();

  virtual ~TransactionBase();

 protected:
  TransactionBase(SafeRefPtr<Database> aDatabase, Mode aMode);

  void NoteActorDestroyed() {
    AssertIsOnBackgroundThread();

    mActorDestroyed.Flip();
  }

#ifdef DEBUG
  // Only called by VersionChangeTransaction.
  void FakeActorDestroyed() { mActorDestroyed.EnsureFlipped(); }
#endif

  bool RecvCommit(Maybe<int64_t> aLastRequest);

  bool RecvAbort(nsresult aResultCode);

  void MaybeCommitOrAbort() {
    AssertIsOnBackgroundThread();

    // If we've already committed or aborted then there's nothing else to do.
    if (mCommittedOrAborted) {
      return;
    }

    // If there are active requests then we have to wait for those requests to
    // complete (see NoteFinishedRequest).
    if (mActiveRequestCount) {
      return;
    }

    // If we haven't yet received a commit or abort message then there could be
    // additional requests coming so we should wait unless we're being forced to
    // abort.
    if (!mCommitOrAbortReceived && !mForceAborted) {
      return;
    }

    CommitOrAbort();
  }

  PBackgroundIDBRequestParent* AllocRequest(RequestParams&& aParams,
                                            bool aTrustParams);

  bool StartRequest(PBackgroundIDBRequestParent* aActor);

  bool DeallocRequest(PBackgroundIDBRequestParent* aActor);

  PBackgroundIDBCursorParent* AllocCursor(const OpenCursorParams& aParams,
                                          bool aTrustParams);

  bool StartCursor(PBackgroundIDBCursorParent* aActor,
                   const OpenCursorParams& aParams);

  bool DeallocCursor(PBackgroundIDBCursorParent* aActor);

  virtual void UpdateMetadata(nsresult aResult) {}

  virtual void SendCompleteNotification(nsresult aResult) = 0;

 private:
  bool VerifyRequestParams(const RequestParams& aParams) const;

  bool VerifyRequestParams(const SerializedKeyRange& aKeyRange) const;

  bool VerifyRequestParams(const ObjectStoreAddPutParams& aParams) const;

  bool VerifyRequestParams(const Maybe<SerializedKeyRange>& aKeyRange) const;

  void CommitOrAbort();
};

class TransactionBase::CommitOp final : public DatabaseOperationBase,
                                        public ConnectionPool::FinishCallback {
  friend class TransactionBase;

  SafeRefPtr<TransactionBase> mTransaction;
  nsresult mResultCode;  ///< TODO: There is also a mResultCode in
                         ///< DatabaseOperationBase. Is there a reason not to
                         ///< use that? At least a more specific name should be
                         ///< given to this one.

 private:
  CommitOp(SafeRefPtr<TransactionBase> aTransaction, nsresult aResultCode);

  ~CommitOp() override = default;

  // Writes new autoIncrement counts to database.
  nsresult WriteAutoIncrementCounts();

  // Updates counts after a database activity has finished.
  void CommitOrRollbackAutoIncrementCounts();

  void AssertForeignKeyConsistency(DatabaseConnection* aConnection)
#ifdef DEBUG
      ;
#else
  {
  }
#endif

  NS_DECL_NSIRUNNABLE

  void TransactionFinishedBeforeUnblock() override;

  void TransactionFinishedAfterUnblock() override;

 public:
  // We need to declare all of nsISupports, because FinishCallback has
  // a pure-virtual nsISupports declaration.
  NS_DECL_ISUPPORTS_INHERITED
};

class NormalTransaction final : public TransactionBase,
                                public PBackgroundIDBTransactionParent {
  nsTArray<RefPtr<FullObjectStoreMetadata>> mObjectStores;

  // Reference counted.
  ~NormalTransaction() override = default;

  bool IsSameProcessActor();

  // Only called by TransactionBase.
  void SendCompleteNotification(nsresult aResult) override;

  // IPDL methods are only called by IPDL.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvDeleteMe() override;

  mozilla::ipc::IPCResult RecvCommit(
      const Maybe<int64_t>& aLastRequest) override;

  mozilla::ipc::IPCResult RecvAbort(const nsresult& aResultCode) override;

  PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent(
      const RequestParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBRequestConstructor(
      PBackgroundIDBRequestParent* aActor,
      const RequestParams& aParams) override;

  bool DeallocPBackgroundIDBRequestParent(
      PBackgroundIDBRequestParent* aActor) override;

  PBackgroundIDBCursorParent* AllocPBackgroundIDBCursorParent(
      const OpenCursorParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBCursorConstructor(
      PBackgroundIDBCursorParent* aActor,
      const OpenCursorParams& aParams) override;

  bool DeallocPBackgroundIDBCursorParent(
      PBackgroundIDBCursorParent* aActor) override;

 public:
  // This constructor is only called by Database.
  NormalTransaction(SafeRefPtr<Database> aDatabase, TransactionBase::Mode aMode,
                    nsTArray<RefPtr<FullObjectStoreMetadata>>&& aObjectStores);

  MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(NormalTransaction, TransactionBase)
};

class VersionChangeTransaction final
    : public TransactionBase,
      public PBackgroundIDBVersionChangeTransactionParent {
  friend class OpenDatabaseOp;

  RefPtr<OpenDatabaseOp> mOpenDatabaseOp;
  SafeRefPtr<FullDatabaseMetadata> mOldMetadata;

  FlippedOnce<false> mActorWasAlive;

 public:
  // Only called by OpenDatabaseOp.
  explicit VersionChangeTransaction(OpenDatabaseOp* aOpenDatabaseOp);

  MOZ_INLINE_DECL_SAFEREFCOUNTING_INHERITED(VersionChangeTransaction,
                                            TransactionBase)

 private:
  // Reference counted.
  ~VersionChangeTransaction() override;

  bool IsSameProcessActor();

  // Only called by OpenDatabaseOp.
  bool CopyDatabaseMetadata();

  void SetActorAlive();

  // Only called by TransactionBase.
  void UpdateMetadata(nsresult aResult) override;

  // Only called by TransactionBase.
  void SendCompleteNotification(nsresult aResult) override;

  // IPDL methods are only called by IPDL.
  void ActorDestroy(ActorDestroyReason aWhy) override;

  mozilla::ipc::IPCResult RecvDeleteMe() override;

  mozilla::ipc::IPCResult RecvCommit(
      const Maybe<int64_t>& aLastRequest) override;

  mozilla::ipc::IPCResult RecvAbort(const nsresult& aResultCode) override;

  mozilla::ipc::IPCResult RecvCreateObjectStore(
      const ObjectStoreMetadata& aMetadata) override;

  mozilla::ipc::IPCResult RecvDeleteObjectStore(
      const IndexOrObjectStoreId& aObjectStoreId) override;

  mozilla::ipc::IPCResult RecvRenameObjectStore(
      const IndexOrObjectStoreId& aObjectStoreId,
      const nsString& aName) override;

  mozilla::ipc::IPCResult RecvCreateIndex(
      const IndexOrObjectStoreId& aObjectStoreId,
      const IndexMetadata& aMetadata) override;

  mozilla::ipc::IPCResult RecvDeleteIndex(
      const IndexOrObjectStoreId& aObjectStoreId,
      const IndexOrObjectStoreId& aIndexId) override;

  mozilla::ipc::IPCResult RecvRenameIndex(
      const IndexOrObjectStoreId& aObjectStoreId,
      const IndexOrObjectStoreId& aIndexId, const nsString& aName) override;

  PBackgroundIDBRequestParent* AllocPBackgroundIDBRequestParent(
      const RequestParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBRequestConstructor(
      PBackgroundIDBRequestParent* aActor,
      const RequestParams& aParams) override;

  bool DeallocPBackgroundIDBRequestParent(
      PBackgroundIDBRequestParent* aActor) override;

  PBackgroundIDBCursorParent* AllocPBackgroundIDBCursorParent(
      const OpenCursorParams& aParams) override;

  mozilla::ipc::IPCResult RecvPBackgroundIDBCursorConstructor(
      PBackgroundIDBCursorParent* aActor,
      const OpenCursorParams& aParams) override;

  bool DeallocPBackgroundIDBCursorParent(
      PBackgroundIDBCursorParent* aActor) override;
};

class MutableFile : public BackgroundMutableFileParentBase {
  const SafeRefPtr<Database> mDatabase;
  const SafeRefPtr<FileInfo> mFileInfo;

 public:
  [[nodiscard]] static RefPtr<MutableFile> Create(
      nsIFile* aFile, SafeRefPtr<Database> aDatabase,
      SafeRefPtr<FileInfo> aFileInfo);

  const Database& GetDatabase() const {
    AssertIsOnBackgroundThread();

    return *mDatabase;
  }

  SafeRefPtr<FileInfo> GetFileInfoPtr() const {
    AssertIsOnBackgroundThread();

    return mFileInfo.clonePtr();
  }

  void NoteActiveState() override;

  void NoteInactiveState() override;

  PBackgroundParent* GetBackgroundParent() const override;

  already_AddRefed<nsISupports> CreateStream(bool aReadOnly) override;

  already_AddRefed<BlobImpl> CreateBlobImpl() override;

 private:
  MutableFile(nsIFile* aFile, SafeRefPtr<Database> aDatabase,
              SafeRefPtr<FileInfo> aFileInfo);

  ~MutableFile() override;

  PBackgroundFileHandleParent* AllocPBackgroundFileHandleParent(
      const FileMode& aMode) final;

  mozilla::ipc::IPCResult RecvPBackgroundFileHandleConstructor(
      PBackgroundFileHandleParent* aActor, const FileMode& aMode) final;

  mozilla::ipc::IPCResult RecvGetFileId(int64_t* aFileId) override;
};

class FactoryOp
    : public DatabaseOperationBase,
      public OpenDirectoryListener,
      public PBackgroundIDBFactoryRequestParent,
      public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
 public:
  struct MaybeBlockedDatabaseInfo final {
    SafeRefPtr<Database> mDatabase;
    bool mBlocked;

    MaybeBlockedDatabaseInfo(MaybeBlockedDatabaseInfo&&) = default;
    MaybeBlockedDatabaseInfo& operator=(MaybeBlockedDatabaseInfo&&) = default;

    MOZ_IMPLICIT MaybeBlockedDatabaseInfo(SafeRefPtr<Database> aDatabase)
        : mDatabase(std::move(aDatabase)), mBlocked(false) {
      MOZ_ASSERT(mDatabase);

      MOZ_COUNT_CTOR(FactoryOp::MaybeBlockedDatabaseInfo);
    }

    ~MaybeBlockedDatabaseInfo() {
      MOZ_COUNT_DTOR(FactoryOp::MaybeBlockedDatabaseInfo);