// 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 `/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 `/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; }