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