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:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user