P5.1: vendor SQLite 3.53.2 + sx bindings
Subplan 02 Slice 2 foundation. vendor/sqlite/ holds the amalgamation (provenance + upgrade notes in its README); make build compiles it into build/vendor/libsqlite3.a (statically linked into dist via -L) and build/vendor/jit/libsqlite3.dylib (dlopen'd by sx run via tests/run.sh's -L flag) — separate directories because the macOS linker prefers a dylib over an archive in one search dir. The sx JIT resolves #foreign symbols via dlsym(RTLD_DEFAULT), where the already-loaded OS libsqlite3 wins by load order — so the vendored build renames its API to dist_sqlite3_* (vendor/sqlite/rename.h, -include'd), making resolution unambiguous in both modes: those symbols exist only in the vendored products. src/db/sqlite.sx binds the renamed surface behind Sqlite/SqliteStmt (open/exec/prepare/bind/step/column/finalize, errmsg, last_insert_rowid, changes, libversion); opaque handles cross the FFI as usize, strings read from sqlite are copied before its buffers die. make test 20/20 (new: sqlite_smoke.sx — pins the loaded version to the vendored 3.53.2, round trip, reopen persistence, BEGIN/ROLLBACK, errmsg; also verified as an AOT binary with no libsqlite3 in otool -L).
This commit is contained in:
28
Makefile
28
Makefile
@@ -12,13 +12,35 @@ BUILD_DIR := build
|
|||||||
SMOKE := tests/smoke.sx
|
SMOKE := tests/smoke.sx
|
||||||
DIST := src/dist.sx
|
DIST := src/dist.sx
|
||||||
|
|
||||||
.PHONY: build test publish-example clean
|
# Vendored SQLite (vendor/sqlite/README.md): one amalgamation, two build
|
||||||
|
# products in two DIRECTORIES — the macOS linker prefers a dylib over an
|
||||||
|
# archive in the same -L directory, and `sx build` must link the static
|
||||||
|
# copy while `sx run` (the test runner) dlopens the dylib.
|
||||||
|
VENDOR_DIR := $(BUILD_DIR)/vendor
|
||||||
|
SQLITE_SRC := vendor/sqlite/sqlite3.c
|
||||||
|
SQLITE_DEFS := -DSQLITE_DQS=0 -DSQLITE_THREADSAFE=0 -DSQLITE_DEFAULT_MEMSTATUS=0 \
|
||||||
|
-DSQLITE_OMIT_DEPRECATED -DSQLITE_OMIT_SHARED_CACHE \
|
||||||
|
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS \
|
||||||
|
-include vendor/sqlite/rename.h
|
||||||
|
|
||||||
|
$(VENDOR_DIR)/libsqlite3.a: $(SQLITE_SRC) vendor/sqlite/sqlite3.h vendor/sqlite/rename.h
|
||||||
|
@mkdir -p $(VENDOR_DIR)
|
||||||
|
cc $(SQLITE_DEFS) -O2 -c $(SQLITE_SRC) -o $(VENDOR_DIR)/sqlite3.o
|
||||||
|
ar rcs $@ $(VENDOR_DIR)/sqlite3.o
|
||||||
|
|
||||||
|
$(VENDOR_DIR)/jit/libsqlite3.dylib: $(SQLITE_SRC) vendor/sqlite/sqlite3.h vendor/sqlite/rename.h
|
||||||
|
@mkdir -p $(VENDOR_DIR)/jit
|
||||||
|
cc $(SQLITE_DEFS) -O2 -dynamiclib $(SQLITE_SRC) -o $@
|
||||||
|
|
||||||
|
.PHONY: build test publish-example vendor clean
|
||||||
|
|
||||||
|
vendor: $(VENDOR_DIR)/libsqlite3.a $(VENDOR_DIR)/jit/libsqlite3.dylib
|
||||||
|
|
||||||
# Compile the product sources (and the smoke program) without running.
|
# Compile the product sources (and the smoke program) without running.
|
||||||
build:
|
build: vendor
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
$(SX) build -o $(BUILD_DIR)/smoke $(SMOKE)
|
$(SX) build -o $(BUILD_DIR)/smoke $(SMOKE)
|
||||||
$(SX) build -o $(BUILD_DIR)/dist $(DIST)
|
$(SX) build -o $(BUILD_DIR)/dist $(DIST) -L $(VENDOR_DIR)
|
||||||
|
|
||||||
# Run the test runner over every tests/**/*.sx. Exits non-zero on any
|
# Run the test runner over every tests/**/*.sx. Exits non-zero on any
|
||||||
# failure. Depends on `build` so the CLI acceptance test (tests/cli_*.sx)
|
# failure. Depends on `build` so the CLI acceptance test (tests/cli_*.sx)
|
||||||
|
|||||||
201
src/db/sqlite.sx
Normal file
201
src/db/sqlite.sx
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
// =====================================================================
|
||||||
|
// sqlite.sx — thin sx bindings over the VENDORED SQLite amalgamation
|
||||||
|
// (vendor/sqlite/, see its README for version + upgrade notes).
|
||||||
|
//
|
||||||
|
// `#library "sqlite3"` resolves to the vendored build products under
|
||||||
|
// build/vendor/ (never the OS copy): `sx build` links the static
|
||||||
|
// archive via `-L build/vendor`; `sx run` dlopens the dylib via
|
||||||
|
// `-L build/vendor/jit` (tests/run.sh). tests/sqlite_smoke.sx pins
|
||||||
|
// `sqlite3_libversion()` to the vendored version so a silent fallback
|
||||||
|
// to the system library fails the suite.
|
||||||
|
//
|
||||||
|
// Handles cross the FFI as `usize` (`sqlite3*` / `sqlite3_stmt*` are
|
||||||
|
// opaque), C strings as `[*]u8` + explicit length or null-termination.
|
||||||
|
// Strings READ from SQLite (column_text, errmsg, libversion) are COPIED
|
||||||
|
// into `context.allocator` before returning — sqlite's buffers die on
|
||||||
|
// the next step/finalize/close.
|
||||||
|
//
|
||||||
|
// The wrapper surface is exactly what the storage layer needs: open /
|
||||||
|
// exec / prepare / bind / step / column / finalize, last_insert_rowid,
|
||||||
|
// and errmsg for failure detail. Everything else stays behind the FFI
|
||||||
|
// line until needed.
|
||||||
|
// =====================================================================
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
sqlib :: #library "sqlite3";
|
||||||
|
|
||||||
|
sqlite3_open :: (path: [*]u8, out_db: *usize) -> i32 #foreign sqlib "dist_sqlite3_open";
|
||||||
|
sqlite3_close :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_close";
|
||||||
|
sqlite3_exec :: (db: usize, sql: [*]u8, cb: usize, arg: usize, errmsg: usize) -> i32 #foreign sqlib "dist_sqlite3_exec";
|
||||||
|
sqlite3_prepare_v2 :: (db: usize, sql: [*]u8, nbyte: i32, out_stmt: *usize, out_tail: usize) -> i32 #foreign sqlib "dist_sqlite3_prepare_v2";
|
||||||
|
sqlite3_step :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_step";
|
||||||
|
sqlite3_finalize :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_finalize";
|
||||||
|
sqlite3_reset :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_reset";
|
||||||
|
sqlite3_bind_text :: (stmt: usize, idx: i32, text: [*]u8, n: i32, destructor: isize) -> i32 #foreign sqlib "dist_sqlite3_bind_text";
|
||||||
|
sqlite3_bind_int64 :: (stmt: usize, idx: i32, v: i64) -> i32 #foreign sqlib "dist_sqlite3_bind_int64";
|
||||||
|
sqlite3_bind_null :: (stmt: usize, idx: i32) -> i32 #foreign sqlib "dist_sqlite3_bind_null";
|
||||||
|
sqlite3_column_int64 :: (stmt: usize, icol: i32) -> i64 #foreign sqlib "dist_sqlite3_column_int64";
|
||||||
|
sqlite3_column_text :: (stmt: usize, icol: i32) -> usize #foreign sqlib "dist_sqlite3_column_text";
|
||||||
|
sqlite3_column_bytes :: (stmt: usize, icol: i32) -> i32 #foreign sqlib "dist_sqlite3_column_bytes";
|
||||||
|
sqlite3_column_type :: (stmt: usize, icol: i32) -> i32 #foreign sqlib "dist_sqlite3_column_type";
|
||||||
|
sqlite3_column_count :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_column_count";
|
||||||
|
sqlite3_errmsg :: (db: usize) -> usize #foreign sqlib "dist_sqlite3_errmsg";
|
||||||
|
sqlite3_libversion :: () -> usize #foreign sqlib "dist_sqlite3_libversion";
|
||||||
|
sqlite3_last_insert_rowid :: (db: usize) -> i64 #foreign sqlib "dist_sqlite3_last_insert_rowid";
|
||||||
|
sqlite3_changes :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_changes";
|
||||||
|
|
||||||
|
// Result codes (the subset the wrappers interpret).
|
||||||
|
SQLITE_OK :: 0;
|
||||||
|
SQLITE_ROW :: 100;
|
||||||
|
SQLITE_DONE :: 101;
|
||||||
|
|
||||||
|
// sqlite3_bind_text destructor sentinel: copy the bytes NOW — sx-side
|
||||||
|
// buffers (arena strings, stack temporaries) don't outlive the call.
|
||||||
|
SQLITE_TRANSIENT : isize : -1;
|
||||||
|
|
||||||
|
// Column types (sqlite3_column_type).
|
||||||
|
SQLITE_INTEGER :: 1;
|
||||||
|
SQLITE_TEXT :: 3;
|
||||||
|
SQLITE_NULL :: 5;
|
||||||
|
|
||||||
|
SqliteErr :: error {
|
||||||
|
Open,
|
||||||
|
Exec,
|
||||||
|
Prepare,
|
||||||
|
Bind,
|
||||||
|
Step,
|
||||||
|
}
|
||||||
|
|
||||||
|
// `s` as a null-terminated heap copy (C string), from context.allocator.
|
||||||
|
sq_cstr :: (s: string) -> [*]u8 {
|
||||||
|
raw : [*]u8 = xx context.allocator.alloc_bytes(s.len + 1);
|
||||||
|
if s.len > 0 { memcpy(raw, s.ptr, s.len); }
|
||||||
|
raw[s.len] = 0;
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the C string at `p` (0 = empty) into context.allocator.
|
||||||
|
sq_from_cstr :: (p: usize) -> string {
|
||||||
|
if p == 0 { return ""; }
|
||||||
|
cp : [*]u8 = xx p;
|
||||||
|
n := 0;
|
||||||
|
while cp[n] != 0 { n += 1; }
|
||||||
|
raw : [*]u8 = xx context.allocator.alloc_bytes(n + 1);
|
||||||
|
if n > 0 { memcpy(raw, cp, xx n); }
|
||||||
|
raw[n] = 0;
|
||||||
|
return string.{ ptr = raw, len = n };
|
||||||
|
}
|
||||||
|
|
||||||
|
// The linked/loaded SQLite's version string (e.g. "3.53.2").
|
||||||
|
sqlite_version :: () -> string {
|
||||||
|
return sq_from_cstr(sqlite3_libversion());
|
||||||
|
}
|
||||||
|
|
||||||
|
// One prepared statement. `finalize` releases it; bind indexes are
|
||||||
|
// 1-based and column indexes 0-based, as in the C API.
|
||||||
|
SqliteStmt :: struct {
|
||||||
|
handle: usize;
|
||||||
|
db: usize;
|
||||||
|
|
||||||
|
bind_text :: (self: *SqliteStmt, idx: i64, s: string) -> !SqliteErr {
|
||||||
|
rc := sqlite3_bind_text(self.handle, xx idx, s.ptr, xx s.len, SQLITE_TRANSIENT);
|
||||||
|
if rc != SQLITE_OK { raise error.Bind; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bind_int64 :: (self: *SqliteStmt, idx: i64, v: i64) -> !SqliteErr {
|
||||||
|
rc := sqlite3_bind_int64(self.handle, xx idx, v);
|
||||||
|
if rc != SQLITE_OK { raise error.Bind; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bind_null :: (self: *SqliteStmt, idx: i64) -> !SqliteErr {
|
||||||
|
rc := sqlite3_bind_null(self.handle, xx idx);
|
||||||
|
if rc != SQLITE_OK { raise error.Bind; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLITE_ROW / SQLITE_DONE on success; anything else raises with the
|
||||||
|
// detail left in the connection's errmsg.
|
||||||
|
step :: (self: *SqliteStmt) -> (i64, !SqliteErr) {
|
||||||
|
rc := sqlite3_step(self.handle);
|
||||||
|
if rc != SQLITE_ROW and rc != SQLITE_DONE { raise error.Step; }
|
||||||
|
return xx rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
column_int64 :: (self: *SqliteStmt, icol: i64) -> i64 {
|
||||||
|
return sqlite3_column_int64(self.handle, xx icol);
|
||||||
|
}
|
||||||
|
// Copied into context.allocator; a NULL column reads as "".
|
||||||
|
column_text :: (self: *SqliteStmt, icol: i64) -> string {
|
||||||
|
p := sqlite3_column_text(self.handle, xx icol);
|
||||||
|
if p == 0 { return ""; }
|
||||||
|
n := sqlite3_column_bytes(self.handle, xx icol);
|
||||||
|
cp : [*]u8 = xx p;
|
||||||
|
raw : [*]u8 = xx context.allocator.alloc_bytes(xx (n + 1));
|
||||||
|
if n > 0 { memcpy(raw, cp, xx n); }
|
||||||
|
raw[n] = 0;
|
||||||
|
return string.{ ptr = raw, len = xx n };
|
||||||
|
}
|
||||||
|
column_type :: (self: *SqliteStmt, icol: i64) -> i64 {
|
||||||
|
return xx sqlite3_column_type(self.handle, xx icol);
|
||||||
|
}
|
||||||
|
column_count :: (self: *SqliteStmt) -> i64 {
|
||||||
|
return xx sqlite3_column_count(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset :: (self: *SqliteStmt) {
|
||||||
|
sqlite3_reset(self.handle);
|
||||||
|
}
|
||||||
|
finalize :: (self: *SqliteStmt) {
|
||||||
|
sqlite3_finalize(self.handle);
|
||||||
|
self.handle = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One connection. `open` creates the file (and parent-less path) per
|
||||||
|
// SQLite's defaults; `close` is safe on an already-closed handle.
|
||||||
|
Sqlite :: struct {
|
||||||
|
handle: usize;
|
||||||
|
|
||||||
|
open :: (path: string) -> (Sqlite, !SqliteErr) {
|
||||||
|
h : usize = 0;
|
||||||
|
rc := sqlite3_open(sq_cstr(path), @h);
|
||||||
|
if rc != SQLITE_OK {
|
||||||
|
if h != 0 { sqlite3_close(h); }
|
||||||
|
raise error.Open;
|
||||||
|
}
|
||||||
|
return Sqlite.{ handle = h };
|
||||||
|
}
|
||||||
|
|
||||||
|
close :: (self: *Sqlite) {
|
||||||
|
if self.handle != 0 { sqlite3_close(self.handle); }
|
||||||
|
self.handle = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run one or more ;-separated statements with no result rows
|
||||||
|
// (DDL, pragmas, BEGIN/COMMIT). Detail via errmsg on failure.
|
||||||
|
exec :: (self: *Sqlite, sql: string) -> !SqliteErr {
|
||||||
|
rc := sqlite3_exec(self.handle, sq_cstr(sql), 0, 0, 0);
|
||||||
|
if rc != SQLITE_OK { raise error.Exec; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare :: (self: *Sqlite, sql: string) -> (SqliteStmt, !SqliteErr) {
|
||||||
|
sh : usize = 0;
|
||||||
|
rc := sqlite3_prepare_v2(self.handle, sql.ptr, xx sql.len, @sh, 0);
|
||||||
|
if rc != SQLITE_OK { raise error.Prepare; }
|
||||||
|
return SqliteStmt.{ handle = sh, db = self.handle };
|
||||||
|
}
|
||||||
|
|
||||||
|
errmsg :: (self: *Sqlite) -> string {
|
||||||
|
return sq_from_cstr(sqlite3_errmsg(self.handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
last_insert_rowid :: (self: *Sqlite) -> i64 {
|
||||||
|
return sqlite3_last_insert_rowid(self.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
changes :: (self: *Sqlite) -> i64 {
|
||||||
|
return xx sqlite3_changes(self.handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,10 +7,16 @@
|
|||||||
# process.assert — and exits non-zero on failure.
|
# process.assert — and exits non-zero on failure.
|
||||||
#
|
#
|
||||||
# Locate the compiler via SX (overridable); defaults to the sibling sx repo.
|
# Locate the compiler via SX (overridable); defaults to the sibling sx repo.
|
||||||
|
#
|
||||||
|
# `-L build/vendor/jit` lets the JIT dlopen the VENDORED libsqlite3.dylib
|
||||||
|
# (built by `make build`) instead of falling back to the OS copy — the
|
||||||
|
# version assert in tests/sqlite_smoke.sx depends on it.
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
SX="${SX:-/Users/agra/projects/sx/zig-out/bin/sx}"
|
SX="${SX:-/Users/agra/projects/sx/zig-out/bin/sx}"
|
||||||
TESTS_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
TESTS_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
REPO_DIR=$(CDPATH= cd -- "$TESTS_DIR/.." && pwd)
|
||||||
|
SX_RUN_FLAGS="-L $REPO_DIR/build/vendor/jit"
|
||||||
|
|
||||||
pass=0
|
pass=0
|
||||||
fail=0
|
fail=0
|
||||||
@@ -20,7 +26,7 @@ fail=0
|
|||||||
# pass/fail counters survive).
|
# pass/fail counters survive).
|
||||||
for t in $(find "$TESTS_DIR" -name '*.sx' -type f | sort); do
|
for t in $(find "$TESTS_DIR" -name '*.sx' -type f | sort); do
|
||||||
name=${t#"$TESTS_DIR"/}
|
name=${t#"$TESTS_DIR"/}
|
||||||
if "$SX" run "$t" >/dev/null 2>&1; then
|
if "$SX" run "$t" $SX_RUN_FLAGS >/dev/null 2>&1; then
|
||||||
printf ' %-44s ok\n' "$name"
|
printf ' %-44s ok\n' "$name"
|
||||||
pass=$((pass + 1))
|
pass=$((pass + 1))
|
||||||
else
|
else
|
||||||
|
|||||||
173
tests/sqlite_smoke.sx
Normal file
173
tests/sqlite_smoke.sx
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// Pinned acceptance for P5.1 — the VENDORED SQLite is linked and usable
|
||||||
|
// through src/db/sqlite.sx.
|
||||||
|
//
|
||||||
|
// * 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` (dlopen via -L
|
||||||
|
// build/vendor/jit) and `sx build` (static link via -L build/vendor).
|
||||||
|
// * 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 "../src/db/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;
|
||||||
|
}
|
||||||
21
vendor/sqlite/README.md
vendored
Normal file
21
vendor/sqlite/README.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Vendored SQLite
|
||||||
|
|
||||||
|
- Version: **3.53.2** (`SQLITE_VERSION` in `sqlite3.h`)
|
||||||
|
- Source: <https://sqlite.org/2026/sqlite-amalgamation-3530200.zip>
|
||||||
|
- Zip sha256: `8a310d0a16c7a90cacd4c884e70faa51c902afed2a89f63aaa0126ab83558a32`
|
||||||
|
- Files kept: `sqlite3.c`, `sqlite3.h` (the amalgamation; `shell.c` and
|
||||||
|
`sqlite3ext.h` dropped — no shell, no loadable extensions)
|
||||||
|
- License: public domain (<https://sqlite.org/copyright.html>)
|
||||||
|
|
||||||
|
`make build` compiles this into `build/vendor/libsqlite3.a` (statically
|
||||||
|
linked into the `dist` binary via `-L build/vendor`) and
|
||||||
|
`build/vendor/jit/libsqlite3.dylib` (dlopen'd by `sx run`, which is how
|
||||||
|
`make test` executes the test programs). The two locations are separate
|
||||||
|
on purpose: the macOS linker prefers a dylib over an archive in the same
|
||||||
|
search directory, and the AOT binary must link the static copy.
|
||||||
|
`tests/sqlite_smoke.sx` asserts `sqlite3_libversion()` equals the version
|
||||||
|
above, so a fallback to the OS libsqlite3 fails loudly in both modes.
|
||||||
|
|
||||||
|
To upgrade: replace `sqlite3.c`/`sqlite3.h` with a newer amalgamation,
|
||||||
|
update this file and the version constant in `tests/sqlite_smoke.sx`, and
|
||||||
|
run `make clean test`.
|
||||||
34
vendor/sqlite/rename.h
vendored
Normal file
34
vendor/sqlite/rename.h
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Symbol prefix for the vendored SQLite build.
|
||||||
|
*
|
||||||
|
* The sx JIT resolves #foreign symbols via dlsym(RTLD_DEFAULT), which
|
||||||
|
* searches every image already loaded into the process — and the OS
|
||||||
|
* libsqlite3 is usually among them, so the standard names would silently
|
||||||
|
* bind to the system copy instead of this vendored one. Prefixing the
|
||||||
|
* API surface makes resolution unambiguous in both JIT (dlopen) and AOT
|
||||||
|
* (static link) modes: dist_sqlite3_* exists ONLY in the vendored build.
|
||||||
|
*
|
||||||
|
* Only the functions bound in src/db/sqlite.sx are renamed; extend BOTH
|
||||||
|
* files together when new API is needed. Injected via `-include` in the
|
||||||
|
* Makefile's SQLITE_DEFS, so sqlite3.c and its embedded sqlite3.h see
|
||||||
|
* the renames consistently.
|
||||||
|
*/
|
||||||
|
#define sqlite3_open dist_sqlite3_open
|
||||||
|
#define sqlite3_close dist_sqlite3_close
|
||||||
|
#define sqlite3_exec dist_sqlite3_exec
|
||||||
|
#define sqlite3_prepare_v2 dist_sqlite3_prepare_v2
|
||||||
|
#define sqlite3_step dist_sqlite3_step
|
||||||
|
#define sqlite3_finalize dist_sqlite3_finalize
|
||||||
|
#define sqlite3_reset dist_sqlite3_reset
|
||||||
|
#define sqlite3_bind_text dist_sqlite3_bind_text
|
||||||
|
#define sqlite3_bind_int64 dist_sqlite3_bind_int64
|
||||||
|
#define sqlite3_bind_null dist_sqlite3_bind_null
|
||||||
|
#define sqlite3_column_int64 dist_sqlite3_column_int64
|
||||||
|
#define sqlite3_column_text dist_sqlite3_column_text
|
||||||
|
#define sqlite3_column_bytes dist_sqlite3_column_bytes
|
||||||
|
#define sqlite3_column_type dist_sqlite3_column_type
|
||||||
|
#define sqlite3_column_count dist_sqlite3_column_count
|
||||||
|
#define sqlite3_errmsg dist_sqlite3_errmsg
|
||||||
|
#define sqlite3_libversion dist_sqlite3_libversion
|
||||||
|
#define sqlite3_last_insert_rowid dist_sqlite3_last_insert_rowid
|
||||||
|
#define sqlite3_changes dist_sqlite3_changes
|
||||||
269376
vendor/sqlite/sqlite3.c
vendored
Normal file
269376
vendor/sqlite/sqlite3.c
vendored
Normal file
File diff suppressed because it is too large
Load Diff
14347
vendor/sqlite/sqlite3.h
vendored
Normal file
14347
vendor/sqlite/sqlite3.h
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user