Files
distribution/tests/sqlite_smoke.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

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