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:
agra
2026-06-12 12:07:22 +03:00
parent aea3d62b60
commit afec94a113
8 changed files with 284184 additions and 4 deletions

201
src/db/sqlite.sx Normal file
View 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);
}
}