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.
175 lines
6.2 KiB
Plaintext
175 lines
6.2 KiB
Plaintext
// Pinned acceptance for P5.1 — the sx-shipped SQLite (vendors/sqlite) is
|
|
// compiled and usable through its `#import c` unit.
|
|
//
|
|
// * version: sqlite3_libversion() equals the vendored amalgamation's
|
|
// version (vendor/sqlite/README.md) — a silent fallback to the OS
|
|
// libsqlite3 fails here, in both `sx run` (the unit's dylib is a
|
|
// priority symbol target ahead of the process images) and
|
|
// `sx build` (the unit's objects link into the binary).
|
|
// * round trip: create table, INSERT via bound parameters (text +
|
|
// int64 + null), SELECT back with WHERE binding, typed columns.
|
|
// * persistence: rows survive close + reopen of the same file.
|
|
// * transactions: BEGIN/ROLLBACK leaves no row behind.
|
|
// * errors: bad SQL raises, and errmsg names the problem.
|
|
#import "modules/std.sx";
|
|
#import "modules/std/fs.sx";
|
|
process :: #import "modules/std/process.sx";
|
|
sq :: #import "vendors/sqlite/sqlite.sx";
|
|
|
|
VENDORED_VERSION :: "3.53.2";
|
|
DBDIR :: ".sx-tmp/sqlite_smoke";
|
|
DBPATH :: ".sx-tmp/sqlite_smoke/t.db";
|
|
|
|
run_case :: (label: string, ok: bool) -> i32 {
|
|
if ok { print(" PASS {}\n", label); return 0; }
|
|
print(" FAIL {}\n", label);
|
|
return 1;
|
|
}
|
|
|
|
check_version :: () -> bool {
|
|
v := sq.sqlite_version();
|
|
if v != VENDORED_VERSION {
|
|
print(" loaded sqlite {} but vendor/sqlite is {}\n", v, VENDORED_VERSION);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
check_roundtrip :: () -> bool {
|
|
db, oe := sq.Sqlite.open(DBPATH);
|
|
if oe { return false; }
|
|
|
|
ee := false;
|
|
db.exec("CREATE TABLE apps (id INTEGER PRIMARY KEY, slug TEXT NOT NULL, owner TEXT, created_at INTEGER NOT NULL)") catch { ee = true; };
|
|
if ee { db.close(); return false; }
|
|
|
|
ins, pe := db.prepare("INSERT INTO apps (slug, owner, created_at) VALUES (?1, ?2, ?3)");
|
|
if pe { db.close(); return false; }
|
|
berr := false;
|
|
ins.bind_text(1, "acme-app") catch { berr = true; };
|
|
ins.bind_text(2, "ci") catch { berr = true; };
|
|
ins.bind_int64(3, 1781250000) catch { berr = true; };
|
|
se := false;
|
|
rc := ins.step() catch { se = true; 0 };
|
|
ins.finalize();
|
|
if berr or se or rc != sq.SQLITE_DONE { db.close(); return false; }
|
|
rowid := db.last_insert_rowid();
|
|
if rowid != 1 { db.close(); return false; }
|
|
|
|
// second row with a NULL owner
|
|
ins2, pe2 := db.prepare("INSERT INTO apps (slug, owner, created_at) VALUES (?1, ?2, ?3)");
|
|
if pe2 { db.close(); return false; }
|
|
ins2.bind_text(1, "other-app") catch { berr = true; };
|
|
ins2.bind_null(2) catch { berr = true; };
|
|
ins2.bind_int64(3, 1781250001) catch { berr = true; };
|
|
ins2.step() catch { se = true; 0 };
|
|
ins2.finalize();
|
|
if berr or se { db.close(); return false; }
|
|
|
|
// select the first row back through a WHERE binding
|
|
sel, pe3 := db.prepare("SELECT id, slug, owner, created_at FROM apps WHERE slug = ?1");
|
|
if pe3 { db.close(); return false; }
|
|
sel.bind_text(1, "acme-app") catch { berr = true; };
|
|
src := sel.step() catch { se = true; 0 };
|
|
ok := !berr and !se and src == sq.SQLITE_ROW;
|
|
if ok {
|
|
ok = sel.column_count() == 4
|
|
and sel.column_int64(0) == 1
|
|
and sel.column_text(1) == "acme-app"
|
|
and sel.column_text(2) == "ci"
|
|
and sel.column_int64(3) == 1781250000
|
|
and sel.column_type(1) == sq.SQLITE_TEXT;
|
|
}
|
|
if ok {
|
|
drc := sel.step() catch { se = true; 0 };
|
|
ok = !se and drc == sq.SQLITE_DONE; // exactly one matching row
|
|
}
|
|
sel.finalize();
|
|
|
|
// NULL column reads as "" with column_type NULL
|
|
if ok {
|
|
sel2, pe4 := db.prepare("SELECT owner FROM apps WHERE slug = 'other-app'");
|
|
if pe4 { db.close(); return false; }
|
|
nrc := sel2.step() catch { se = true; 0 };
|
|
ok = !se and nrc == sq.SQLITE_ROW
|
|
and sel2.column_type(0) == sq.SQLITE_NULL
|
|
and sel2.column_text(0) == "";
|
|
sel2.finalize();
|
|
}
|
|
|
|
db.close();
|
|
return ok;
|
|
}
|
|
|
|
check_persistence :: () -> bool {
|
|
db, oe := sq.Sqlite.open(DBPATH);
|
|
if oe { return false; }
|
|
sel, pe := db.prepare("SELECT COUNT(*) FROM apps");
|
|
if pe { db.close(); return false; }
|
|
se := false;
|
|
rc := sel.step() catch { se = true; 0 };
|
|
ok := !se and rc == sq.SQLITE_ROW and sel.column_int64(0) == 2;
|
|
sel.finalize();
|
|
db.close();
|
|
return ok;
|
|
}
|
|
|
|
check_transaction_rollback :: () -> bool {
|
|
db, oe := sq.Sqlite.open(DBPATH);
|
|
if oe { return false; }
|
|
ee := false;
|
|
db.exec("BEGIN") catch { ee = true; };
|
|
db.exec("INSERT INTO apps (slug, owner, created_at) VALUES ('doomed', 'x', 0)") catch { ee = true; };
|
|
db.exec("ROLLBACK") catch { ee = true; };
|
|
if ee { db.close(); return false; }
|
|
|
|
sel, pe := db.prepare("SELECT COUNT(*) FROM apps WHERE slug = 'doomed'");
|
|
if pe { db.close(); return false; }
|
|
se := false;
|
|
rc := sel.step() catch { se = true; 0 };
|
|
ok := !se and rc == sq.SQLITE_ROW and sel.column_int64(0) == 0;
|
|
sel.finalize();
|
|
db.close();
|
|
return ok;
|
|
}
|
|
|
|
check_error_path :: () -> bool {
|
|
db, oe := sq.Sqlite.open(DBPATH);
|
|
if oe { return false; }
|
|
failed := false;
|
|
db.exec("FROBNICATE THE DATABASE") catch { failed = true; };
|
|
msg := db.errmsg();
|
|
db.close();
|
|
if !failed { return false; }
|
|
// errmsg names the offending token
|
|
i := 0;
|
|
found := false;
|
|
while i + 10 <= msg.len {
|
|
v := string.{ ptr = @msg[i], len = 10 };
|
|
if v == "FROBNICATE" { found = true; break; }
|
|
i += 1;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
main :: () -> i32 {
|
|
process.run(concat("rm -rf ", DBDIR));
|
|
process.run(concat("mkdir -p ", DBDIR));
|
|
|
|
failures : i32 = 0;
|
|
failures += run_case("vendored version is loaded", check_version());
|
|
failures += run_case("create/insert/select round trip", check_roundtrip());
|
|
failures += run_case("rows persist across reopen", check_persistence());
|
|
failures += run_case("BEGIN/ROLLBACK leaves no row", check_transaction_rollback());
|
|
failures += run_case("bad SQL raises with errmsg detail", check_error_path());
|
|
|
|
process.run(concat("rm -rf ", DBDIR));
|
|
print("------------------------------------------------\n");
|
|
if failures == 0 {
|
|
print("sqlite_smoke: ALL CASES PASS\n");
|
|
return 0;
|
|
}
|
|
print("sqlite_smoke: {} CASE(S) FAILED\n", failures);
|
|
return 1;
|
|
}
|