blob: 6033663757b3765aacbf054880d19e1e0b9246ac [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "storage/browser/quota/quota_database.h"
#include <stddef.h>
#include <stdint.h>
#include <tuple>
#include <vector>
#include "base/auto_reset.h"
#include "base/bind.h"
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/metrics/histogram_macros.h"
#include "sql/database.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"
#include "storage/browser/quota/special_storage_policy.h"
#include "third_party/blink/public/mojom/quota/quota_types.mojom-shared.h"
#include "url/gurl.h"
using blink::mojom::StorageType;
namespace storage {
namespace {
// Definitions for database schema.
const int kQuotaDatabaseCurrentSchemaVersion = 5;
const int kQuotaDatabaseCompatibleVersion = 2;
const char kHostQuotaTable[] = "HostQuotaTable";
const char kOriginInfoTable[] = "OriginInfoTable";
const char kEvictionInfoTable[] = "EvictionInfoTable";
const char kIsOriginTableBootstrapped[] = "IsOriginTableBootstrapped";
const int kCommitIntervalMs = 30000;
} // anonymous namespace
const QuotaDatabase::TableSchema QuotaDatabase::kTables[] = {
{kHostQuotaTable,
"(host TEXT NOT NULL,"
" type INTEGER NOT NULL,"
" quota INTEGER DEFAULT 0,"
" UNIQUE(host, type))"},
{kOriginInfoTable,
"(origin TEXT NOT NULL,"
" type INTEGER NOT NULL,"
" used_count INTEGER DEFAULT 0,"
" last_access_time INTEGER DEFAULT 0,"
" last_modified_time INTEGER DEFAULT 0,"
" UNIQUE(origin, type))"},
{kEvictionInfoTable,
"(origin TEXT NOT NULL,"
" type INTEGER NOT NULL,"
" last_eviction_time INTEGER DEFAULT 0,"
" UNIQUE(origin, type))"}};
// static
const QuotaDatabase::IndexSchema QuotaDatabase::kIndexes[] = {
{ "HostIndex",
kHostQuotaTable,
"(host)",
false },
{ "OriginInfoIndex",
kOriginInfoTable,
"(origin)",
false },
{ "OriginLastAccessTimeIndex",
kOriginInfoTable,
"(last_access_time)",
false },
{ "OriginLastModifiedTimeIndex",
kOriginInfoTable,
"(last_modified_time)",
false },
};
struct QuotaDatabase::QuotaTableImporter {
bool Append(const QuotaTableEntry& entry) {
entries.push_back(entry);
return true;
}
std::vector<QuotaTableEntry> entries;
};
// Clang requires explicit out-of-line constructors for them.
QuotaDatabase::QuotaTableEntry::QuotaTableEntry()
: type(StorageType::kUnknown), quota(0) {}
QuotaDatabase::QuotaTableEntry::QuotaTableEntry(const std::string& host,
StorageType type,
int64_t quota)
: host(host), type(type), quota(quota) {}
QuotaDatabase::OriginInfoTableEntry::OriginInfoTableEntry()
: type(StorageType::kUnknown), used_count(0) {}
QuotaDatabase::OriginInfoTableEntry::OriginInfoTableEntry(
const url::Origin& origin,
StorageType type,
int used_count,
const base::Time& last_access_time,
const base::Time& last_modified_time)
: origin(origin),
type(type),
used_count(used_count),
last_access_time(last_access_time),
last_modified_time(last_modified_time) {}
// QuotaDatabase ------------------------------------------------------------
QuotaDatabase::QuotaDatabase(const base::FilePath& path)
: db_file_path_(path),
is_recreating_(false),
is_disabled_(false) {
DETACH_FROM_SEQUENCE(sequence_checker_);
}
QuotaDatabase::~QuotaDatabase() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (db_) {
db_->CommitTransaction();
}
}
bool QuotaDatabase::GetHostQuota(const std::string& host,
StorageType type,
int64_t* quota) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(quota);
if (!LazyOpen(false))
return false;
const char* kSql =
"SELECT quota"
" FROM HostQuotaTable"
" WHERE host = ? AND type = ?";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, host);
statement.BindInt(1, static_cast<int>(type));
if (!statement.Step())
return false;
*quota = statement.ColumnInt64(0);
return true;
}
bool QuotaDatabase::SetHostQuota(const std::string& host,
StorageType type,
int64_t quota) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_GE(quota, 0);
if (!LazyOpen(true))
return false;
if (quota == 0)
return DeleteHostQuota(host, type);
if (!InsertOrReplaceHostQuota(host, type, quota))
return false;
ScheduleCommit();
return true;
}
bool QuotaDatabase::SetOriginLastAccessTime(const url::Origin& origin,
StorageType type,
base::Time last_access_time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(true))
return false;
sql::Statement statement;
OriginInfoTableEntry entry;
if (GetOriginInfo(origin, type, &entry)) {
++entry.used_count;
const char* kSql =
"UPDATE OriginInfoTable"
" SET used_count = ?, last_access_time = ?"
" WHERE origin = ? AND type = ?";
statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
} else {
entry.used_count = 1;
const char* kSql =
"INSERT INTO OriginInfoTable"
" (used_count, last_access_time, origin, type, last_modified_time)"
" VALUES (?, ?, ?, ?, ?)";
statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(4, TimeToSqlValue(last_access_time));
}
statement.BindInt(0, entry.used_count);
statement.BindInt64(1, TimeToSqlValue(last_access_time));
statement.BindString(2, origin.GetURL().spec());
statement.BindInt(3, static_cast<int>(type));
if (!statement.Run())
return false;
ScheduleCommit();
return true;
}
bool QuotaDatabase::SetOriginLastModifiedTime(const url::Origin& origin,
StorageType type,
base::Time last_modified_time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(true))
return false;
sql::Statement statement;
OriginInfoTableEntry entry;
if (GetOriginInfo(origin, type, &entry)) {
const char* kSql =
"UPDATE OriginInfoTable"
" SET last_modified_time = ?"
" WHERE origin = ? AND type = ?";
statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
} else {
const char* kSql =
"INSERT INTO OriginInfoTable"
" (last_modified_time, origin, type, last_access_time) VALUES (?, ?, ?, ?)";
statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(3, TimeToSqlValue(last_modified_time));
}
statement.BindInt64(0, TimeToSqlValue(last_modified_time));
statement.BindString(1, origin.GetURL().spec());
statement.BindInt(2, static_cast<int>(type));
if (!statement.Run())
return false;
ScheduleCommit();
return true;
}
bool QuotaDatabase::GetOriginLastEvictionTime(const url::Origin& origin,
StorageType type,
base::Time* last_modified_time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(last_modified_time);
if (!LazyOpen(false))
return false;
static const char kSql[] =
"SELECT last_eviction_time"
" FROM EvictionInfoTable"
" WHERE origin = ? AND type = ?";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, origin.GetURL().spec());
statement.BindInt(1, static_cast<int>(type));
if (!statement.Step())
return false;
*last_modified_time = TimeFromSqlValue(statement.ColumnInt64(0));
return true;
}
bool QuotaDatabase::SetOriginLastEvictionTime(const url::Origin& origin,
StorageType type,
base::Time last_modified_time) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(true))
return false;
static const char kSql[] =
"INSERT OR REPLACE INTO EvictionInfoTable"
" (last_eviction_time, origin, type)"
" VALUES (?, ?, ?)";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(0, TimeToSqlValue(last_modified_time));
statement.BindString(1, origin.GetURL().spec());
statement.BindInt(2, static_cast<int>(type));
if (!statement.Run())
return false;
ScheduleCommit();
return true;
}
bool QuotaDatabase::DeleteOriginLastEvictionTime(const url::Origin& origin,
StorageType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(false))
return false;
static const char kSql[] =
"DELETE FROM EvictionInfoTable"
" WHERE origin = ? AND type = ?";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, origin.GetURL().spec());
statement.BindInt(1, static_cast<int>(type));
if (!statement.Run())
return false;
ScheduleCommit();
return true;
}
bool QuotaDatabase::RegisterInitialOriginInfo(
const std::set<url::Origin>& origins,
StorageType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(true))
return false;
for (const auto& origin : origins) {
const char* kSql =
"INSERT OR IGNORE INTO OriginInfoTable"
" (origin, type) VALUES (?, ?)";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, origin.GetURL().spec());
statement.BindInt(1, static_cast<int>(type));
if (!statement.Run())
return false;
}
ScheduleCommit();
return true;
}
bool QuotaDatabase::GetOriginInfo(const url::Origin& origin,
StorageType type,
QuotaDatabase::OriginInfoTableEntry* entry) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(false))
return false;
const char* kSql =
"SELECT * FROM OriginInfoTable"
" WHERE origin = ? AND type = ?";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, origin.GetURL().spec());
statement.BindInt(1, static_cast<int>(type));
if (!statement.Step())
return false;
// TODO(crbug.com/889590): Use helper for url::Origin creation from string.
*entry = OriginInfoTableEntry(
url::Origin::Create(GURL(statement.ColumnString(0))),
static_cast<StorageType>(statement.ColumnInt(1)), statement.ColumnInt(2),
TimeFromSqlValue(statement.ColumnInt64(3)),
TimeFromSqlValue(statement.ColumnInt64(4)));
return true;
}
bool QuotaDatabase::DeleteHostQuota(
const std::string& host, StorageType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(false))
return false;
const char* kSql =
"DELETE FROM HostQuotaTable"
" WHERE host = ? AND type = ?";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, host);
statement.BindInt(1, static_cast<int>(type));
if (!statement.Run())
return false;
ScheduleCommit();
return true;
}
bool QuotaDatabase::DeleteOriginInfo(const url::Origin& origin,
StorageType type) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(false))
return false;
const char* kSql =
"DELETE FROM OriginInfoTable"
" WHERE origin = ? AND type = ?";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindString(0, origin.GetURL().spec());
statement.BindInt(1, static_cast<int>(type));
if (!statement.Run())
return false;
ScheduleCommit();
return true;
}
bool QuotaDatabase::GetLRUOrigin(StorageType type,
const std::set<url::Origin>& exceptions,
SpecialStoragePolicy* special_storage_policy,
base::Optional<url::Origin>* origin) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(origin);
if (!LazyOpen(false))
return false;
static const char kSql[] =
"SELECT origin FROM OriginInfoTable"
" WHERE type = ?"
" ORDER BY last_access_time ASC";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt(0, static_cast<int>(type));
while (statement.Step()) {
url::Origin read_origin =
url::Origin::Create(GURL(statement.ColumnString(0)));
if (base::Contains(exceptions, read_origin))
continue;
if (special_storage_policy &&
(special_storage_policy->IsStorageDurable(read_origin.GetURL()) ||
special_storage_policy->IsStorageUnlimited(read_origin.GetURL()))) {
continue;
}
*origin = read_origin;
return true;
}
origin->reset();
return statement.Succeeded();
}
bool QuotaDatabase::GetOriginsModifiedBetween(StorageType type,
std::set<url::Origin>* origins,
base::Time begin,
base::Time end) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(origins);
if (!LazyOpen(false))
return false;
DCHECK(!begin.is_max() && end != base::Time());
static constexpr char kSqlQuerySince[] =
"SELECT origin FROM OriginInfoTable"
" WHERE type = ? AND last_modified_time >= ?";
static constexpr char kSqlQueryBetween[] =
"SELECT origin FROM OriginInfoTable"
" WHERE type = ? AND last_modified_time >= ? AND last_modified_time < ?";
sql::Statement statement;
if (end.is_max()) {
statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE, kSqlQuerySince));
} else {
statement.Assign(db_->GetCachedStatement(SQL_FROM_HERE, kSqlQueryBetween));
}
statement.BindInt(0, static_cast<int>(type));
statement.BindInt64(1, TimeToSqlValue(begin));
if (!end.is_max())
statement.BindInt64(2, TimeToSqlValue(end));
origins->clear();
while (statement.Step())
origins->insert(url::Origin::Create(GURL(statement.ColumnString(0))));
return statement.Succeeded();
}
bool QuotaDatabase::IsOriginDatabaseBootstrapped() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(true))
return false;
int flag = 0;
return meta_table_->GetValue(kIsOriginTableBootstrapped, &flag) && flag;
}
bool QuotaDatabase::SetOriginDatabaseBootstrapped(bool bootstrap_flag) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(true))
return false;
return meta_table_->SetValue(kIsOriginTableBootstrapped, bootstrap_flag);
}
void QuotaDatabase::Commit() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!db_)
return;
if (timer_.IsRunning())
timer_.Stop();
DCHECK_EQ(1, db_->transaction_nesting());
db_->CommitTransaction();
DCHECK_EQ(0, db_->transaction_nesting());
db_->BeginTransaction();
DCHECK_EQ(1, db_->transaction_nesting());
}
void QuotaDatabase::ScheduleCommit() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (timer_.IsRunning())
return;
timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(kCommitIntervalMs),
this, &QuotaDatabase::Commit);
}
bool QuotaDatabase::LazyOpen(bool create_if_needed) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (db_)
return true;
// If we tried and failed once, don't try again in the same session
// to avoid creating an incoherent mess on disk.
if (is_disabled_)
return false;
bool in_memory_only = db_file_path_.empty();
if (!create_if_needed &&
(in_memory_only || !base::PathExists(db_file_path_))) {
return false;
}
db_.reset(new sql::Database);
meta_table_.reset(new sql::MetaTable);
db_->set_histogram_tag("Quota");
bool opened = false;
if (in_memory_only) {
opened = db_->OpenInMemory();
} else if (!base::CreateDirectory(db_file_path_.DirName())) {
LOG(ERROR) << "Failed to create quota database directory.";
} else {
opened = db_->Open(db_file_path_);
if (opened)
db_->Preload();
}
if (!opened || !EnsureDatabaseVersion()) {
LOG(ERROR) << "Could not open the quota database, resetting.";
if (!ResetSchema()) {
LOG(ERROR) << "Failed to reset the quota database.";
is_disabled_ = true;
db_.reset();
meta_table_.reset();
return false;
}
}
// Start a long-running transaction.
db_->BeginTransaction();
return true;
}
bool QuotaDatabase::EnsureDatabaseVersion() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
static const size_t kTableCount = base::size(kTables);
static const size_t kIndexCount = base::size(kIndexes);
if (!sql::MetaTable::DoesTableExist(db_.get()))
return CreateSchema(db_.get(), meta_table_.get(),
kQuotaDatabaseCurrentSchemaVersion,
kQuotaDatabaseCompatibleVersion, kTables, kTableCount,
kIndexes, kIndexCount);
if (!meta_table_->Init(db_.get(), kQuotaDatabaseCurrentSchemaVersion,
kQuotaDatabaseCompatibleVersion))
return false;
if (meta_table_->GetCompatibleVersionNumber() >
kQuotaDatabaseCurrentSchemaVersion) {
LOG(WARNING) << "Quota database is too new.";
return false;
}
if (meta_table_->GetVersionNumber() < kQuotaDatabaseCurrentSchemaVersion) {
if (!UpgradeSchema(meta_table_->GetVersionNumber()))
return ResetSchema();
}
#ifndef NDEBUG
DCHECK(sql::MetaTable::DoesTableExist(db_.get()));
for (size_t i = 0; i < kTableCount; ++i) {
DCHECK(db_->DoesTableExist(kTables[i].table_name));
}
#endif
return true;
}
// static
bool QuotaDatabase::CreateSchema(sql::Database* database,
sql::MetaTable* meta_table,
int schema_version,
int compatible_version,
const TableSchema* tables,
size_t tables_size,
const IndexSchema* indexes,
size_t indexes_size) {
// TODO(kinuko): Factor out the common code to create databases.
sql::Transaction transaction(database);
if (!transaction.Begin())
return false;
if (!meta_table->Init(database, schema_version, compatible_version))
return false;
for (size_t i = 0; i < tables_size; ++i) {
std::string sql("CREATE TABLE ");
sql += tables[i].table_name;
sql += tables[i].columns;
if (!database->Execute(sql.c_str())) {
VLOG(1) << "Failed to execute " << sql;
return false;
}
}
for (size_t i = 0; i < indexes_size; ++i) {
std::string sql;
if (indexes[i].unique)
sql += "CREATE UNIQUE INDEX ";
else
sql += "CREATE INDEX ";
sql += indexes[i].index_name;
sql += " ON ";
sql += indexes[i].table_name;
sql += indexes[i].columns;
if (!database->Execute(sql.c_str())) {
VLOG(1) << "Failed to execute " << sql;
return false;
}
}
return transaction.Commit();
}
bool QuotaDatabase::ResetSchema() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!db_file_path_.empty());
DCHECK(base::PathExists(db_file_path_));
DCHECK(!db_ || !db_->transaction_nesting());
VLOG(1) << "Deleting existing quota data and starting over.";
db_.reset();
meta_table_.reset();
if (!sql::Database::Delete(db_file_path_))
return false;
// So we can't go recursive.
if (is_recreating_)
return false;
base::AutoReset<bool> auto_reset(&is_recreating_, true);
return LazyOpen(true);
}
bool QuotaDatabase::UpgradeSchema(int current_version) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(0, db_->transaction_nesting());
if (current_version == 2) {
QuotaTableImporter importer;
if (!DumpQuotaTable(base::BindRepeating(&QuotaTableImporter::Append,
base::Unretained(&importer)))) {
return false;
}
ResetSchema();
sql::Transaction transaction(db_.get());
if (!transaction.Begin())
return false;
for (const auto& entry : importer.entries) {
if (!InsertOrReplaceHostQuota(entry.host, entry.type, entry.quota))
return false;
}
return transaction.Commit();
} else if (current_version < 5) {
sql::Transaction transaction(db_.get());
if (!transaction.Begin())
return false;
const QuotaDatabase::TableSchema& eviction_table_schema = kTables[2];
DCHECK_EQ(strcmp(kEvictionInfoTable, eviction_table_schema.table_name), 0);
std::string sql("CREATE TABLE ");
sql += eviction_table_schema.table_name;
sql += eviction_table_schema.columns;
if (!db_->Execute(sql.c_str())) {
VLOG(1) << "Failed to execute " << sql;
return false;
}
meta_table_->SetVersionNumber(5);
return transaction.Commit();
}
return false;
}
bool QuotaDatabase::InsertOrReplaceHostQuota(const std::string& host,
StorageType type,
int64_t quota) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(db_.get());
const char* kSql =
"INSERT OR REPLACE INTO HostQuotaTable"
" (quota, host, type)"
" VALUES (?, ?, ?)";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
statement.BindInt64(0, quota);
statement.BindString(1, host);
statement.BindInt(2, static_cast<int>(type));
return statement.Run();
}
bool QuotaDatabase::DumpQuotaTable(const QuotaTableCallback& callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(true))
return false;
const char* kSql = "SELECT * FROM HostQuotaTable";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
while (statement.Step()) {
QuotaTableEntry entry = QuotaTableEntry(
statement.ColumnString(0),
static_cast<StorageType>(statement.ColumnInt(1)),
statement.ColumnInt64(2));
if (!callback.Run(entry))
return true;
}
return statement.Succeeded();
}
bool QuotaDatabase::DumpOriginInfoTable(
const OriginInfoTableCallback& callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!LazyOpen(true))
return false;
const char* kSql = "SELECT * FROM OriginInfoTable";
sql::Statement statement(db_->GetCachedStatement(SQL_FROM_HERE, kSql));
while (statement.Step()) {
OriginInfoTableEntry entry(
url::Origin::Create(GURL(statement.ColumnString(0))),
static_cast<StorageType>(statement.ColumnInt(1)),
statement.ColumnInt(2), TimeFromSqlValue(statement.ColumnInt64(3)),
TimeFromSqlValue(statement.ColumnInt64(4)));
if (!callback.Run(entry))
return true;
}
return statement.Succeeded();
}
// static
base::Time QuotaDatabase::TimeFromSqlValue(int64_t time) {
return base::Time::FromDeltaSinceWindowsEpoch(
base::TimeDelta::FromMicroseconds(time));
}
// static
int64_t QuotaDatabase::TimeToSqlValue(const base::Time& time) {
return time.ToDeltaSinceWindowsEpoch().InMicroseconds();
}
bool operator<(const QuotaDatabase::QuotaTableEntry& lhs,
const QuotaDatabase::QuotaTableEntry& rhs) {
return std::tie(lhs.host, lhs.type, lhs.quota) <
std::tie(rhs.host, rhs.type, rhs.quota);
}
bool operator<(const QuotaDatabase::OriginInfoTableEntry& lhs,
const QuotaDatabase::OriginInfoTableEntry& rhs) {
return std::tie(lhs.origin, lhs.type, lhs.used_count, lhs.last_access_time) <
std::tie(rhs.origin, rhs.type, rhs.used_count, rhs.last_access_time);
}
} // namespace storage