Skip to content

Commit

Permalink
Add SqliteMetadata helper class for reading/writing sqlite alarm time
Browse files Browse the repository at this point in the history
Analogous to SqliteKv helper class; not used yet.
  • Loading branch information
jclee committed Sep 4, 2024
1 parent 485a7ba commit 1bd8f75
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 0 deletions.
9 changes: 9 additions & 0 deletions src/workerd/util/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,12 @@ wd_cc_library(
srcs = [
"sqlite.c++",
"sqlite-kv.c++",
"sqlite-metadata.c++",
],
hdrs = [
"sqlite.h",
"sqlite-kv.h",
"sqlite-metadata.h",
],
implementation_deps = [
"@sqlite3",
Expand Down Expand Up @@ -202,6 +204,13 @@ kj_test(
],
)

kj_test(
src = "sqlite-metadata-test.c++",
deps = [
":sqlite",
],
)

kj_test(
src = "test-test.c++",
deps = [
Expand Down
44 changes: 44 additions & 0 deletions src/workerd/util/sqlite-metadata-test.c++
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2024 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

#include "sqlite-metadata.h"
#include <kj/test.h>

namespace workerd {
namespace {

KJ_TEST("SQLite-METADATA") {
auto dir = kj::newInMemoryDirectory(kj::nullClock());
SqliteDatabase::Vfs vfs(*dir);
SqliteDatabase db(vfs, kj::Path({"foo"}), kj::WriteMode::CREATE | kj::WriteMode::MODIFY);
SqliteMetadata metadata(db);

// Initial state has empty alarm
KJ_EXPECT(metadata.getAlarm() == kj::none);

// Can set alarm to an explicit time
constexpr kj::Date anAlarmTime1 =
kj::UNIX_EPOCH + 1734099316 * kj::SECONDS + 987654321 * kj::NANOSECONDS;
metadata.setAlarm(anAlarmTime1);

// Can get the set alarm time
KJ_EXPECT(metadata.getAlarm() == anAlarmTime1);

// Can overwrite the alarm time
constexpr kj::Date anAlarmTime2 = anAlarmTime1 + 1 * kj::NANOSECONDS;
metadata.setAlarm(anAlarmTime2);
KJ_EXPECT(metadata.getAlarm() != anAlarmTime1);
KJ_EXPECT(metadata.getAlarm() == anAlarmTime2);

// Can clear alarm
metadata.setAlarm(kj::none);
KJ_EXPECT(metadata.getAlarm() == kj::none);

// Zero alarm is distinct from unset (probably not important, but just checking)
metadata.setAlarm(kj::UNIX_EPOCH);
KJ_EXPECT(metadata.getAlarm() == kj::UNIX_EPOCH);
}

} // namespace
} // namespace workerd
64 changes: 64 additions & 0 deletions src/workerd/util/sqlite-metadata.c++
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) 2024 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

#include "sqlite-metadata.h"

namespace workerd {

SqliteMetadata::SqliteMetadata(SqliteDatabase& db) {
auto q = db.run("SELECT name FROM sqlite_master WHERE type='table' AND name='_cf_METADATA'");
if (q.isDone()) {
// The _cf_METADATA table doesn't exist. Defer initialization.
state = Uninitialized{db};
} else {
// The metadata table was initialized in the past. We can go ahead and prepare our statements.
// (We don't call ensureInitialized() here because the `CREATE TABLE IF NOT EXISTS` query it
// executes would be redundant.)
state = Initialized(db);
}
}

kj::Maybe<kj::Date> SqliteMetadata::getAlarm() {
auto& stmts = KJ_UNWRAP_OR(state.tryGet<Initialized>(), return kj::none);

auto query = stmts.stmtGetAlarm.run();
if (query.isDone() || query.isNull(0)) {
return kj::none;
} else {
return kj::UNIX_EPOCH + query.getInt64(0) * kj::NANOSECONDS;
}
}

void SqliteMetadata::setAlarm(kj::Maybe<kj::Date> currentTime) {
KJ_IF_SOME(t, currentTime) {
ensureInitialized().stmtSetAlarm.run((t - kj::UNIX_EPOCH) / kj::NANOSECONDS);
} else {
// Our getter code also allows representing an empty alarm value as a
// missing row or table, but a null-value row seems efficient and simple.
ensureInitialized().stmtSetAlarm.run(nullptr);
}
}

SqliteMetadata::Initialized& SqliteMetadata::ensureInitialized() {
KJ_SWITCH_ONEOF(state) {
KJ_CASE_ONEOF(uninitialized, Uninitialized) {
auto& db = uninitialized.db;

db.run(R"(
CREATE TABLE IF NOT EXISTS _cf_METADATA (
key INTEGER PRIMARY KEY,
value BLOB
);
)");

return state.init<Initialized>(db);
}
KJ_CASE_ONEOF(initialized, Initialized) {
return initialized;
}
}
KJ_UNREACHABLE;
}

} // namespace workerd
53 changes: 53 additions & 0 deletions src/workerd/util/sqlite-metadata.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2024 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

#pragma once

#include "sqlite.h"

namespace workerd {

// Class which implements a simple metadata kv storage on top of SQLite. Currently only used to
// store Durable Object alarm times (hardcoded as key = 1), but could later be used for other
// properties.
//
// The table is named `_cf_METADATA`. The naming is designed so that if the application is allowed to
// perform direct SQL queries, we can block it from accessing any table prefixed with `_cf_`.
class SqliteMetadata {
public:
explicit SqliteMetadata(SqliteDatabase& db);

// Return currently set alarm time, or none.
kj::Maybe<kj::Date> getAlarm();

// Sets current alarm time, or none.
void setAlarm(kj::Maybe<kj::Date> currentTime);

private:
struct Uninitialized {
SqliteDatabase& db;
};

struct Initialized {
SqliteDatabase& db;

SqliteDatabase::Statement stmtGetAlarm = db.prepare(R"(
SELECT value FROM _cf_METADATA WHERE key = 1
)");
SqliteDatabase::Statement stmtSetAlarm = db.prepare(R"(
INSERT INTO _cf_METADATA VALUES(1, ?)
ON CONFLICT DO UPDATE SET value = excluded.value;
)");

Initialized(SqliteDatabase& db): db(db) {}
};

kj::OneOf<Uninitialized, Initialized> state;

Initialized& ensureInitialized();
// Make sure the metadata table is created and prepared statements are ready. Not called until the
// first write.
};

} // namespace workerd

0 comments on commit 1bd8f75

Please sign in to comment.