sqlite persistence: the store moves from db.json to dist.db (P5.2)
src/repo/db.sx persists the whole Repo to <store>/dist.db through the vendored SQLite bindings, keeping the load-whole/save-whole call shape. One table per entity; enums as lowercase variant names; list order round-trips via rowid. Enforced uniqueness: apps.slug, channels(app_id, name), tokens.token_hash; lookup indexes on releases(app_id) and artifacts(sha256) (non-unique - identical bytes may ship in several releases). save is DELETE-all + INSERT-all inside BEGIN IMMEDIATE...COMMIT with rollback on failure; every connection sets busy_timeout so the CLI and a running distd interleave safely. A store holding only a pre-SQLite db.json imports once on first load, then the file is renamed db.json.imported; a store with neither starts empty. Consumers gate on db.store_exists instead of probing db.json. The JSON read-back stays for the import path; the entity->json writers stay for distd's /api responses. Tests that parsed db.json directly now assert by querying dist.db through the SQLite bindings; tests/db_import.sx pins the import path; tests/repo_roundtrip.sx pins the SQLite round-trip. make test 22/22.
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
// * happy path: exit 0; stdout is the publish JSON (status published,
|
||||
// release id, per-artifact sha256 equal to an independent digest of
|
||||
// the fixture); the SERVER's store gained objects/<sha>, the release,
|
||||
// the channel pointer, and `token:<name>` audit actors.
|
||||
// the channel pointer, and `token:<name>` audit actors (store state
|
||||
// queried from `<store>/dist.db` via the SQLite bindings).
|
||||
// * duplicate version: exit 1, the server's transaction.integrity code
|
||||
// passed through verbatim.
|
||||
// * wrong secret: exit 1, auth.unknown_token.
|
||||
@@ -21,6 +22,7 @@
|
||||
process :: #import "modules/std/process.sx";
|
||||
fs :: #import "modules/std/fs.sx";
|
||||
hash :: #import "modules/std/hash.sx";
|
||||
sq :: #import "../src/db/sqlite.sx";
|
||||
|
||||
STORE :: ".sx-tmp/remote_publish";
|
||||
MDIR :: ".sx-tmp/remote_publish_m";
|
||||
@@ -54,6 +56,42 @@ parse_body :: (body: string, what: string, scratch: Allocator) -> Object {
|
||||
return v.object;
|
||||
}
|
||||
|
||||
// One-row scalar queries over `<STORE>/dist.db` ("" = unbound binding).
|
||||
db_open_ro :: () -> sq.Sqlite {
|
||||
c, oe := sq.Sqlite.open_v2(path_join(STORE, "dist.db"), sq.SQLITE_OPEN_READONLY);
|
||||
process.assert(!oe, "dist.db must open as a SQLite database");
|
||||
c.busy_timeout(2000);
|
||||
return c;
|
||||
}
|
||||
|
||||
q_text :: (sql: string, p1: string) -> string {
|
||||
c := db_open_ro();
|
||||
st, pe := c.prepare(sql);
|
||||
process.assert(!pe, concat("prepare must succeed: ", sql));
|
||||
if p1.len > 0 { st.bind_text(1, p1) catch { process.assert(false, "bind 1 failed"); }; }
|
||||
rc, se := st.step();
|
||||
process.assert(!se, concat("step must succeed: ", sql));
|
||||
process.assert(rc == sq.SQLITE_ROW, concat("query must return a row: ", sql));
|
||||
out := st.column_text(0);
|
||||
st.finalize();
|
||||
c.close();
|
||||
return out;
|
||||
}
|
||||
|
||||
q_int :: (sql: string, p1: string) -> i64 {
|
||||
c := db_open_ro();
|
||||
st, pe := c.prepare(sql);
|
||||
process.assert(!pe, concat("prepare must succeed: ", sql));
|
||||
if p1.len > 0 { st.bind_text(1, p1) catch { process.assert(false, "bind 1 failed"); }; }
|
||||
rc, se := st.step();
|
||||
process.assert(!se, concat("step must succeed: ", sql));
|
||||
process.assert(rc == sq.SQLITE_ROW, concat("query must return a row: ", sql));
|
||||
out := st.column_int64(0);
|
||||
st.finalize();
|
||||
c.close();
|
||||
return out;
|
||||
}
|
||||
|
||||
// Run a remote publish with `server` and `token`; returns the run result.
|
||||
remote_publish :: (server: string, token: string) -> ?process.ProcessResult {
|
||||
cmd := concat("build/dist ci publish --manifest ", path_join(MDIR, "m.json"));
|
||||
@@ -124,14 +162,11 @@ main :: () -> i32 {
|
||||
|
||||
process.assert(fs.exists(path_join(STORE, concat("objects/", expect_sha))),
|
||||
"the SERVER's store holds the uploaded object");
|
||||
dbo := parse_body(fs.read_file(path_join(STORE, "db.json"))!, "db.json", xx arena);
|
||||
rels := get_arr(dbo, "releases");
|
||||
process.assert(rels.len == 1, "server db records the release");
|
||||
process.assert(get_str(rels.items[0].object, "created_by") == "token:ci-remote",
|
||||
process.assert(q_int("SELECT COUNT(*) FROM releases", "") == 1, "server db records the release");
|
||||
process.assert(q_text("SELECT created_by FROM releases", "") == "token:ci-remote",
|
||||
"release created_by carries the token actor");
|
||||
chans := get_arr(dbo, "channels");
|
||||
process.assert(chans.len == 1, "server db records the channel");
|
||||
process.assert(get_str(chans.items[0].object, "current_release_id") == "rel-acme-app-2.0.0",
|
||||
process.assert(q_int("SELECT COUNT(*) FROM channels", "") == 1, "server db records the channel");
|
||||
process.assert(q_text("SELECT current_release_id FROM channels", "") == "rel-acme-app-2.0.0",
|
||||
"beta points at the published release");
|
||||
print(" remote publish: CI contract round trip ok\n");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user