Files
distribution/tests/db_import.sx
agra 7ec1e10f6e sqlite moves into the sx library: import vendors/sqlite/sqlite.sx
The amalgamation and the bindings now ship with sx itself
(sx library/vendors/sqlite/ — bindings + c/ amalgamation); every
import flips from ../src/db/sqlite.sx to vendors/sqlite/sqlite.sx,
resolved through the compiler's stdlib search paths. vendor/ and
src/db/ leave this repo entirely. make test 22/22 — the object cache
keys on content, not path, so the relocated source still hits the
existing cache entries.
2026-06-12 17:41:26 +03:00

148 lines
9.2 KiB
Plaintext

// Pinned acceptance for P5.2 — one-time import of a pre-SQLite store.
//
// A store laid down by the db.json era (no dist.db) must keep working:
// the FIRST load imports every entity into `<store>/dist.db` and renames
// the JSON file to `db.json.imported`, after which SQLite is the only
// authority. Drives the BUILT `build/dist` binary:
//
// 1. A store holding ONLY an old-layout db.json (app + bundle id, two
// releases, artifact, channel, token, audit event) answers
// `token list` with the minted token → the import ran: dist.db
// exists, db.json is GONE, db.json.imported holds the original.
// 2. Every entity survived the import field-for-field where it counts
// (queried from dist.db via the SQLite bindings).
// 3. A follow-up write op (`release promote`) works against the
// imported store and does NOT re-import (db.json.imported stays).
// 4. A store with NO database at all refuses ops that need one
// (`release promote` → exit 1, store.load).
#import "modules/std.sx";
#import "modules/std/json.sx";
process :: #import "modules/std/process.sx";
fs :: #import "modules/std/fs.sx";
sq :: #import "vendors/sqlite/sqlite.sx";
STORE :: ".sx-tmp/db_import";
EMPTY :: ".sx-tmp/db_import_empty";
// An old-layout db.json covering every persisted table.
OLD_DB :: "{\"apps\":[{\"id\":\"app-old\",\"slug\":\"old-app\",\"display_name\":\"Old App\",\"bundle_ids\":[{\"platform\":\"ios\",\"value\":\"co.old.app\"}],\"owner\":\"ci\",\"visibility\":\"private\",\"created_at\":1700000000,\"updated_at\":1700000000}],\"releases\":[{\"id\":\"rel-1\",\"app_id\":\"app-old\",\"version\":\"1.0.0\",\"build\":1,\"channel\":\"beta\",\"notes\":\"\",\"created_by\":\"ci\",\"created_at\":1700000100,\"published_at\":1700000100},{\"id\":\"rel-2\",\"app_id\":\"app-old\",\"version\":\"1.0.1\",\"build\":2,\"channel\":\"beta\",\"notes\":\"\",\"created_by\":\"ci\",\"created_at\":1700000200,\"published_at\":1700000200}],\"artifacts\":[{\"id\":\"rel-1-android_apk\",\"app_id\":\"app-old\",\"release_id\":\"rel-1\",\"platform\":\"android_apk\",\"filename\":\"old.apk\",\"content_type\":\"application/vnd.android.package-archive\",\"size_bytes\":5,\"sha256\":\"55a008aa634d45313ef0a758624e0d2a356c156e507f28a2c60d19d38893af09\",\"storage_key\":\"55a008aa634d45313ef0a758624e0d2a356c156e507f28a2c60d19d38893af09\",\"metadata\":\"\",\"validation_status\":\"valid\"}],\"channels\":[{\"app_id\":\"app-old\",\"name\":\"beta\",\"current_release_id\":\"rel-2\",\"policy\":\"manual\",\"rollout_percent\":100}],\"tokens\":[{\"id\":\"tok-abcdefabcdef\",\"name\":\"legacy-ci\",\"token_hash\":\"55a008aa634d45313ef0a758624e0d2a356c156e507f28a2c60d19d38893af09\",\"scopes\":\"publish\",\"app_slug\":\"\",\"channel\":\"\",\"created_at\":1700000300,\"expires_at\":0,\"last_used_at\":0,\"revoked_at\":0}],\"audit_events\":[{\"id\":\"evt-publish-rel-1\",\"actor\":\"ci\",\"action\":\"release.publish\",\"target_type\":\"release\",\"target_id\":\"rel-1\",\"metadata\":\"\",\"created_at\":1700000100}]}";
get :: (o: Object, key: string) -> Value {
i := 0;
while i < o.len {
if o.items[i].key == key { return o.items[i].val; }
i += 1;
}
process.assert(false, concat("missing json key: ", key));
dummy : Value = .null_;
return dummy;
}
get_str :: (o: Object, key: string) -> string { return get(o, key).str; }
get_obj :: (o: Object, key: string) -> Object { return get(o, key).object; }
get_arr :: (o: Object, key: string) -> Array { return get(o, key).array; }
// 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;
}
main :: () -> i32 {
gpa := GPA.init();
arena := Arena.init(xx gpa, 1 << 20);
defer arena.deinit();
process.run(concat("rm -rf ", STORE));
process.run(concat("rm -rf ", EMPTY));
process.run(concat("mkdir -p ", STORE));
process.run(concat("mkdir -p ", EMPTY));
process.run(concat(concat(concat("printf '%s' '", OLD_DB), "' > "), path_join(STORE, "db.json")));
// ── 1. First load imports: db.json -> dist.db + db.json.imported ──
tl := process.run("build/dist token list --local-store .sx-tmp/db_import --json 2>/dev/null");
process.assert(tl != null and tl!.exit_code == 0, "token list over a db.json-only store must exit 0");
lv, le := parse(tl!.stdout, xx arena);
if le { process.assert(false, "token list stdout must be one JSON object"); return 1; }
toks := get_arr(lv.object, "tokens");
process.assert(toks.len == 1, "the legacy token is listed");
process.assert(get_str(toks.items[0].object, "id") == "tok-abcdefabcdef", "legacy token id survives");
process.assert(fs.exists(path_join(STORE, "dist.db")), "import created dist.db");
process.assert(!fs.exists(path_join(STORE, "db.json")), "import consumed db.json");
process.assert(fs.exists(path_join(STORE, "db.json.imported")), "the original is kept as db.json.imported");
print(" first load imported db.json into dist.db\n");
// ── 2. Every entity survived the import ──────────────────────────
process.assert(q_int("SELECT COUNT(*) FROM apps", "") == 1, "imported: one app");
process.assert(q_text("SELECT slug FROM apps", "") == "old-app", "imported: app slug");
process.assert(q_int("SELECT COUNT(*) FROM app_bundle_ids", "") == 1, "imported: one bundle id");
process.assert(q_text("SELECT value FROM app_bundle_ids WHERE platform = ?1", "ios") == "co.old.app",
"imported: iOS bundle id value");
process.assert(q_int("SELECT COUNT(*) FROM releases", "") == 2, "imported: both releases");
process.assert(q_int("SELECT COUNT(*) FROM artifacts", "") == 1, "imported: the artifact");
process.assert(q_text("SELECT validation_status FROM artifacts", "") == "valid",
"imported: artifact status");
process.assert(q_text("SELECT current_release_id FROM channels WHERE name = ?1", "beta") == "rel-2",
"imported: channel pointer");
process.assert(q_text("SELECT token_hash FROM tokens WHERE id = ?1", "tok-abcdefabcdef")
== "55a008aa634d45313ef0a758624e0d2a356c156e507f28a2c60d19d38893af09",
"imported: token hash at rest");
process.assert(q_int("SELECT COUNT(*) FROM audit_events WHERE action = ?1", "release.publish") == 1,
"imported: the audit event");
print(" imported store carries every entity\n");
// ── 3. A write op works on the imported store, no re-import ──────
pr := process.run("build/dist release promote --app old-app --channel beta --release rel-1 --local-store .sx-tmp/db_import --json 2>/dev/null");
process.assert(pr != null and pr!.exit_code == 0, "promote on the imported store must exit 0");
process.assert(q_text("SELECT current_release_id FROM channels WHERE name = ?1", "beta") == "rel-1",
"promote moved the imported channel pointer");
process.assert(fs.exists(path_join(STORE, "db.json.imported")), "db.json.imported is not consumed again");
process.assert(!fs.exists(path_join(STORE, "db.json")), "no db.json reappears");
print(" imported store accepts writes; import ran exactly once\n");
// ── 4. A store with NO database refuses ops that need one ────────
pn := process.run("build/dist release promote --app old-app --channel beta --release rel-1 --local-store .sx-tmp/db_import_empty --json 2>/dev/null");
process.assert(pn != null and pn!.exit_code == 1, "promote on an empty store must exit 1");
nv, ne := parse(pn!.stdout, xx arena);
if ne { process.assert(false, "empty-store promote stdout must be one JSON object"); return 1; }
process.assert(get_str(get_obj(nv.object, "error"), "code") == "store.load",
"empty-store promote names store.load");
process.assert(!fs.exists(path_join(EMPTY, "dist.db")), "a refused op creates no database");
print(" empty store: ops that need state refuse with store.load\n");
process.run(concat("rm -rf ", STORE));
process.run(concat("rm -rf ", EMPTY));
print("db_import: ALL CASES PASS\n");
return 0;
}