// Pinned acceptance for the FULL SQLite mapping (src/db/sqlite.sx) — // every wrapper family beyond the P5.1 smoke (which keeps owning the // vendored-version pin): // // * library: version_number, sourceid, threadsafe=false (built // THREADSAFE=0), compileoption_used, complete, errstr, strglob/ // strlike/stricmp, randomness. // * connections: open_v2 flag behavior (READONLY on a missing file // refuses; CREATE works; db_readonly reflects the mode), // db_filename, busy_timeout, autocommit/txn_state through a real // transaction, changes/total_changes, set_last_insert_rowid, // limit (prior value answered), error_offset, extended errcodes // on a UNIQUE violation (19 / 2067). // * statements: double/blob round trips (blob with interior NULs), // named parameters (count/index/name), clear_bindings + reset // reuse, column_name/decltype/origin metadata, data_count, // sql/expanded_sql, stmt readonly/isexplain, table_column_metadata. // * blob I/O: bind_zeroblob reservation, open/write/read/bytes, // reopen onto another row. // * backup: full db -> db copy via sqlite_backup_run. // * serialization: serialize -> fresh connection -> deserialize -> // query answers match. #import "modules/std.sx"; process :: #import "modules/std/process.sx"; sq :: #import "../src/db/sqlite.sx"; DBDIR :: ".sx-tmp/sqlite_api"; run_case :: (label: string, ok: bool) -> i32 { if ok { print(" PASS {}\n", label); return 0; } print(" FAIL {}\n", label); return 1; } // ── library utilities ───────────────────────────────────────────────── check_library_info :: () -> bool { if sq.sqlite_version_number() < 3053000 { return false; } if sq.sqlite_sourceid().len < 10 { return false; } if sq.sqlite_threadsafe() { return false; } // THREADSAFE=0 build if !sq.sqlite_compileoption_used("THREADSAFE=0") { return false; } if sq.sqlite_compileoption_get(0).len == 0 { return false; } return true; } check_text_utils :: () -> bool { if !sq.sqlite_complete("SELECT 1;") { return false; } if sq.sqlite_complete("SELECT 1") { return false; } // no terminator if !sq.sqlite_strglob("acme-*", "acme-app") { return false; } if sq.sqlite_strglob("acme-*", "other") { return false; } if !sq.sqlite_strlike("acme-%", "ACME-APP", 0) { return false; } // LIKE is case-folding if sq.sqlite_stricmp("AbC", "abc") != 0 { return false; } if sq.sqlite_errstr(sq.SQLITE_BUSY).len == 0 { return false; } return true; } check_randomness :: () -> bool { r := sq.sqlite_randomness(16); if r.len != 16 { return false; } all_zero := true; i := 0; while i < r.len { if r[i] != 0 { all_zero = false; break; } i += 1; } return !all_zero; } // ── connections ─────────────────────────────────────────────────────── check_open_v2_flags :: () -> bool { // READONLY on a file that doesn't exist must refuse. refused := false; nope, ne := sq.Sqlite.open_v2(".sx-tmp/sqlite_api/nope.db", sq.SQLITE_OPEN_READONLY); if ne { refused = true; } if !refused { nope.close(); return false; } // CREATE | READWRITE builds the file; db_readonly answers 0. db, oe := sq.Sqlite.open_v2(".sx-tmp/sqlite_api/v2.db", sq.SQLITE_OPEN_READWRITE | sq.SQLITE_OPEN_CREATE); if oe { return false; } ee := false; db.exec("CREATE TABLE t (x)") catch { ee = true; }; rw := db.db_readonly(); fname := db.db_filename(); db.close(); if ee or rw != 0 { return false; } // contains check: the filename carries the path we opened found := false; i := 0; while i + 5 <= fname.len { v := string.{ ptr = @fname[i], len = 5 }; if v == "v2.db" { found = true; break; } i += 1; } if !found { return false; } // Reopened READONLY: db_readonly answers 1 and writes refuse. ro, roe := sq.Sqlite.open_v2(".sx-tmp/sqlite_api/v2.db", sq.SQLITE_OPEN_READONLY); if roe { return false; } wfail := false; ro.exec("INSERT INTO t VALUES (1)") catch { wfail = true; }; is_ro := ro.db_readonly(); ro.close(); return wfail and is_ro == 1; } check_txn_state :: () -> bool { db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/txn.db"); if oe { return false; } db.busy_timeout(2000); ee := false; db.exec("CREATE TABLE t (x)") catch { ee = true; }; ok := db.get_autocommit() and db.txn_state() == sq.SQLITE_TXN_NONE; db.exec("BEGIN IMMEDIATE") catch { ee = true; }; if ok { ok = !db.get_autocommit() and db.txn_state() == sq.SQLITE_TXN_WRITE; } db.exec("COMMIT") catch { ee = true; }; if ok { ok = db.get_autocommit(); } db.close(); return ok and !ee; } check_changes_and_rowid :: () -> bool { db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/chg.db"); if oe { return false; } ee := false; db.exec("CREATE TABLE t (x); INSERT INTO t VALUES (1),(2),(3)") catch { ee = true; }; if ee { db.close(); return false; } ok := db.changes() == 3; db.exec("UPDATE t SET x = x + 1") catch { ee = true; }; if ok { ok = db.changes() == 3 and db.total_changes() >= 6; } db.set_last_insert_rowid(424242); if ok { ok = db.last_insert_rowid() == 424242; } db.close(); return ok and !ee; } check_limit :: () -> bool { db, oe := sq.Sqlite.open(":memory:"); if oe { return false; } prior := db.limit(sq.SQLITE_LIMIT_VARIABLE_NUMBER, 100); now := db.limit(sq.SQLITE_LIMIT_VARIABLE_NUMBER, -1); // read back db.close(); return prior > 0 and now == 100; } check_error_detail :: () -> bool { db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/err.db"); if oe { return false; } ee := false; db.exec("CREATE TABLE u (name TEXT UNIQUE); INSERT INTO u VALUES ('a')") catch { ee = true; }; if ee { db.close(); return false; } dup := false; db.exec("INSERT INTO u VALUES ('a')") catch { dup = true; }; ok := dup and db.errcode() == sq.SQLITE_CONSTRAINT and db.extended_errcode() == sq.SQLITE_CONSTRAINT_UNIQUE; // error_offset points at the offending token of a syntax error perr := false; bad, bade := db.prepare("SELECT * FRM u"); if bade { perr = true; } if !perr { bad.finalize(); } if ok { ok = perr and db.error_offset() >= 9; } db.close(); return ok; } // ── statements ──────────────────────────────────────────────────────── // A 6-byte blob with interior NULs. test_blob :: () -> string { raw : [*]u8 = xx context.allocator.alloc_bytes(7); raw[0] = 1; raw[1] = 0; raw[2] = 255; raw[3] = 0; raw[4] = 7; raw[5] = 42; raw[6] = 0; return string.{ ptr = raw, len = 6 }; } check_typed_round_trip :: () -> bool { db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/types.db"); if oe { return false; } ee := false; db.exec("CREATE TABLE v (d REAL, b BLOB)") catch { ee = true; }; if ee { db.close(); return false; } ins, pe := db.prepare("INSERT INTO v VALUES (?1, ?2)"); if pe { db.close(); return false; } berr := false; se := false; ins.bind_double(1, 3.5) catch { berr = true; }; ins.bind_blob(2, test_blob()) catch { berr = true; }; ins.step() catch { se = true; 0 }; ins.finalize(); if berr or se { db.close(); return false; } sel, pe2 := db.prepare("SELECT d, b FROM v"); if pe2 { db.close(); return false; } rc := sel.step() catch { se = true; 0 }; ok := !se and rc == sq.SQLITE_ROW and sel.column_type(0) == sq.SQLITE_FLOAT and sel.column_double(0) == 3.5 and sel.column_type(1) == sq.SQLITE_BLOB and sel.column_bytes(1) == 6 and sel.column_blob(1) == test_blob() and sel.data_count() == 2; sel.finalize(); db.close(); return ok; } check_named_parameters :: () -> bool { db, oe := sq.Sqlite.open(":memory:"); if oe { return false; } st, pe := db.prepare("SELECT :alpha, ?, :beta"); if pe { db.close(); return false; } ok := st.parameter_count() == 3 and st.parameter_index(":alpha") == 1 and st.parameter_index(":beta") == 3 and st.parameter_index(":nope") == 0 and st.parameter_name(1) == ":alpha" and st.parameter_name(2) == ""; // a bare ? is nameless (?N would be "?N") st.finalize(); db.close(); return ok; } check_rebind_reuse :: () -> bool { db, oe := sq.Sqlite.open(":memory:"); if oe { return false; } ee := false; db.exec("CREATE TABLE t (x)") catch { ee = true; }; if ee { db.close(); return false; } st, pe := db.prepare("INSERT INTO t VALUES (?1)"); if pe { db.close(); return false; } berr := false; se := false; st.bind_int64(1, 10) catch { berr = true; }; st.step() catch { se = true; 0 }; st.reset(); st.clear_bindings(); // NULL now bound st.step() catch { se = true; 0 }; st.reset(); st.bind_int64(1, 20) catch { berr = true; }; st.step() catch { se = true; 0 }; st.finalize(); if berr or se { db.close(); return false; } sel, pe2 := db.prepare("SELECT COUNT(*), COUNT(x), SUM(x) FROM t"); if pe2 { db.close(); return false; } rc := sel.step() catch { se = true; 0 }; ok := !se and rc == sq.SQLITE_ROW and sel.column_int64(0) == 3 // three rows and sel.column_int64(1) == 2 // one is NULL and sel.column_int64(2) == 30; sel.finalize(); db.close(); return ok; } check_statement_introspection :: () -> bool { db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/meta.db"); if oe { return false; } ee := false; db.exec("CREATE TABLE apps (id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT NOT NULL)") catch { ee = true; }; if ee { db.close(); return false; } sel, pe := db.prepare("SELECT slug FROM apps WHERE id = ?1"); if pe { db.close(); return false; } berr := false; sel.bind_int64(1, 7) catch { berr = true; }; ok := !berr and sel.readonly() and sel.isexplain() == 0 and sel.column_name(0) == "slug" and sel.column_decltype(0) == "TEXT" and sel.column_table_name(0) == "apps" and sel.column_origin_name(0) == "slug" and sel.sql() == "SELECT slug FROM apps WHERE id = ?1" and sel.expanded_sql() == "SELECT slug FROM apps WHERE id = 7"; sel.finalize(); if ok { ins, pe2 := db.prepare("INSERT INTO apps (slug) VALUES ('x')"); if pe2 { db.close(); return false; } ok = !ins.readonly(); ins.finalize(); } if ok { meta, me := db.table_column_metadata("apps", "id"); if me { db.close(); return false; } ok = meta.primary_key and meta.autoinc and meta.data_type == "INTEGER"; if ok { meta2, me2 := db.table_column_metadata("apps", "slug"); if me2 { db.close(); return false; } ok = meta2.not_null and !meta2.primary_key and meta2.data_type == "TEXT"; } } db.close(); return ok; } // ── blob I/O ────────────────────────────────────────────────────────── check_blob_io :: () -> bool { db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/blob.db"); if oe { return false; } ee := false; db.exec("CREATE TABLE files (id INTEGER PRIMARY KEY, data BLOB)") catch { ee = true; }; if ee { db.close(); return false; } // reserve 8 zero bytes in row 1, 4 in row 2 ins, pe := db.prepare("INSERT INTO files (id, data) VALUES (?1, ?2)"); if pe { db.close(); return false; } berr := false; se := false; ins.bind_int64(1, 1) catch { berr = true; }; ins.bind_zeroblob(2, 8) catch { berr = true; }; ins.step() catch { se = true; 0 }; ins.reset(); ins.bind_int64(1, 2) catch { berr = true; }; ins.bind_zeroblob(2, 4) catch { berr = true; }; ins.step() catch { se = true; 0 }; ins.finalize(); if berr or se { db.close(); return false; } bl, be := sq.SqliteBlob.open(@db, "files", "data", 1, true); if be { db.close(); return false; } werr := false; bl.write(2, "HI") catch { werr = true; }; rd, re := bl.read(0, 8); sz := bl.bytes(); if re { bl.close(); db.close(); return false; } if werr { bl.close(); db.close(); return false; } ok := sz == 8 and rd.len == 8 and rd[0] == 0 and rd[2] == 72 and rd[3] == 73 and rd[4] == 0; // reopen onto row 2 — different size, fresh content roerr := false; bl.reopen(2) catch { roerr = true; }; if ok { ok = !roerr and bl.bytes() == 4; } bl.close(); db.close(); return ok; } // ── backup + serialization ──────────────────────────────────────────── count_rows :: (db: *sq.Sqlite, table: string) -> i64 { sel, pe := db.prepare(concat("SELECT COUNT(*) FROM ", table)); if pe { return -1; } se := false; rc := sel.step() catch { se = true; 0 }; n : i64 = -1; if !se and rc == sq.SQLITE_ROW { n = sel.column_int64(0); } sel.finalize(); return n; } check_backup :: () -> bool { src, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/bk-src.db"); if oe { return false; } ee := false; src.exec("CREATE TABLE t (x); INSERT INTO t VALUES (1),(2),(3),(4)") catch { ee = true; }; if ee { src.close(); return false; } dst, oe2 := sq.Sqlite.open(".sx-tmp/sqlite_api/bk-dst.db"); if oe2 { src.close(); return false; } bke := false; sq.sqlite_backup_run(@dst, @src) catch { bke = true; }; ok := !bke and count_rows(@dst, "t") == 4; src.close(); dst.close(); return ok; } check_serialize_round_trip :: () -> bool { db, oe := sq.Sqlite.open(":memory:"); if oe { return false; } ee := false; db.exec("CREATE TABLE t (x); INSERT INTO t VALUES (10),(20)") catch { ee = true; }; if ee { db.close(); return false; } img, se := db.serialize(); db.close(); if se { return false; } if img.len < 512 { return false; } db2, oe2 := sq.Sqlite.open(":memory:"); if oe2 { return false; } de := false; db2.deserialize(img) catch { de = true; }; ok := !de and count_rows(@db2, "t") == 2; db2.close(); return ok; } main :: () -> i32 { process.run(concat("rm -rf ", DBDIR)); process.run(concat("mkdir -p ", DBDIR)); failures : i32 = 0; failures += run_case("library: version/sourceid/threadsafe/options", check_library_info()); failures += run_case("library: complete/glob/like/stricmp/errstr", check_text_utils()); failures += run_case("library: randomness yields bytes", check_randomness()); failures += run_case("conn: open_v2 flags + db_readonly/filename", check_open_v2_flags()); failures += run_case("conn: autocommit + txn_state through BEGIN", check_txn_state()); failures += run_case("conn: changes64 + set_last_insert_rowid", check_changes_and_rowid()); failures += run_case("conn: limit answers the prior value", check_limit()); failures += run_case("conn: constraint errcodes + error_offset", check_error_detail()); failures += run_case("stmt: double/blob round trip (interior NULs)", check_typed_round_trip()); failures += run_case("stmt: named parameter introspection", check_named_parameters()); failures += run_case("stmt: clear_bindings + reset reuse", check_rebind_reuse()); failures += run_case("stmt: names/decltypes/metadata/expanded_sql", check_statement_introspection()); failures += run_case("blob: zeroblob + write/read/reopen", check_blob_io()); failures += run_case("backup: full db -> db copy", check_backup()); failures += run_case("serialize: image round-trips into a fresh db", check_serialize_round_trip()); process.run(concat("rm -rf ", DBDIR)); print("------------------------------------------------\n"); if failures == 0 { print("sqlite_api: ALL CASES PASS\n"); return 0; } print("sqlite_api: {} CASE(S) FAILED\n", failures); return 1; }