sqlite: map the full practical C API
src/db/sqlite.sx grows from the P5.1 subset (~19 fns) to the complete practical surface (~100): open_v2 + flags, extended errcodes + error_offset, txn_state/autocommit, changes64/total_changes64, limits, the full bind/column families (double/blob/zeroblob), parameter and column introspection (built with SQLITE_ENABLE_COLUMN_METADATA), table_column_metadata, statement introspection (sql/expanded_sql/ readonly/busy/isexplain/status), incremental blob I/O (SqliteBlob), online backup (SqliteBackup + sqlite_backup_run), serialize/ deserialize, and library utilities (complete, strglob/strlike/stricmp, randomness, memory, compileoptions). One variant per duplicate family (modern/64-bit preferred; bind_text/blob keep the 32-bit length forms that skip text64's encoding arg). Not bound, by design: callback-taking APIs (hooks/UDFs/collations need C->sx callbacks), sqlite3_value_* (UDF-coupled), varargs config, UTF-16, and subsystems this build omits — the boundary list lives in the module header and vendor README. rename.h is now GENERATED by make into build/vendor/ from the bindings' #foreign names — src/db/sqlite.sx is the single source of truth and the rename list cannot drift (checked-in vendor/sqlite/rename.h removed). make test 21/21 (new: sqlite_api.sx — 15 cases over every wrapper family, including a blob round trip with interior NULs, UNIQUE constraint extended errcodes, txn_state through BEGIN IMMEDIATE, backup db->db, and a serialize->deserialize round trip). KNOWN sx BOUNDARY (filed as a followup): 'if !e' on an error binding evaluates true even when the error is set — negated error logic in tests routes through plain bools.
This commit is contained in:
18
Makefile
18
Makefile
@@ -16,19 +16,29 @@ DIST := src/dist.sx
|
||||
# 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.
|
||||
#
|
||||
# rename.h is GENERATED from the bindings: every `dist_sqlite3_*` symbol
|
||||
# named in src/db/sqlite.sx gets a #define, so the rename list and the
|
||||
# bound surface cannot drift (see the README for why renaming exists).
|
||||
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
|
||||
-DSQLITE_LIKE_DOESNT_MATCH_BLOBS -DSQLITE_ENABLE_COLUMN_METADATA \
|
||||
-include $(VENDOR_DIR)/rename.h
|
||||
|
||||
$(VENDOR_DIR)/libsqlite3.a: $(SQLITE_SRC) vendor/sqlite/sqlite3.h vendor/sqlite/rename.h
|
||||
$(VENDOR_DIR)/rename.h: src/db/sqlite.sx
|
||||
@mkdir -p $(VENDOR_DIR)
|
||||
@{ echo '/* GENERATED by make from src/db/sqlite.sx — do not edit. */'; \
|
||||
grep -o '"dist_sqlite3_[a-z0-9_]*"' src/db/sqlite.sx | tr -d '"' | sort -u | \
|
||||
sed 's/^dist_\(.*\)/#define \1 dist_\1/'; } > $@
|
||||
|
||||
$(VENDOR_DIR)/libsqlite3.a: $(SQLITE_SRC) vendor/sqlite/sqlite3.h $(VENDOR_DIR)/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
|
||||
$(VENDOR_DIR)/jit/libsqlite3.dylib: $(SQLITE_SRC) vendor/sqlite/sqlite3.h $(VENDOR_DIR)/rename.h
|
||||
@mkdir -p $(VENDOR_DIR)/jit
|
||||
cc $(SQLITE_DEFS) -O2 -dynamiclib $(SQLITE_SRC) -o $@
|
||||
|
||||
|
||||
735
src/db/sqlite.sx
735
src/db/sqlite.sx
@@ -1,77 +1,301 @@
|
||||
// =====================================================================
|
||||
// sqlite.sx — thin sx bindings over the VENDORED SQLite amalgamation
|
||||
// sqlite.sx — 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
|
||||
// `-L build/vendor/jit` (tests/run.sh). Every bound symbol is renamed
|
||||
// `dist_sqlite3_*` in the vendored build — `build/vendor/rename.h` is
|
||||
// GENERATED by the Makefile from THIS FILE's `#foreign` names, so the
|
||||
// rename list and the bindings cannot drift. tests/sqlite_smoke.sx pins
|
||||
// `sqlite3_libversion()` to the vendored version so a silent fallback
|
||||
// to the system library fails the suite.
|
||||
//
|
||||
// FFI type choices: handles cross as `usize` (`sqlite3*` /
|
||||
// `sqlite3_stmt*` are opaque and never dereferenced; `usize` keeps
|
||||
// `sqlite3_open`'s out-param a single-level `*usize` instead of
|
||||
// `**void`). Whole C strings IN are `[:0]u8` (the std FFI convention —
|
||||
// build terminated copies with `cstring`); `prepare_v2`/`bind_text`
|
||||
// take `[*]u8` + explicit byte length instead, so un-terminated sx
|
||||
// string views pass without copying. Nullable C strings OUT are `*u8`
|
||||
// + a cast null-check (the `getenv` convention — `[:0]u8` has no null),
|
||||
// and are COPIED into `context.allocator` before returning — sqlite's
|
||||
// buffers die on the next step/finalize/close.
|
||||
// COVERAGE — the full practical C API, ONE variant per function (the
|
||||
// modern/64-bit form where duplicates exist; `bind_text`/`bind_blob`
|
||||
// keep the 32-bit length forms, which skip text64's encoding arg —
|
||||
// per-value sizes beyond 2 GiB are out of scope). Deliberately NOT
|
||||
// bound, each a real boundary rather than an omission:
|
||||
// * callback-taking APIs (exec's row callback, busy_handler,
|
||||
// hooks, trace/progress, authorizer, create_function/collation)
|
||||
// — they need C->sx function-pointer callbacks; busy_timeout and
|
||||
// callback-less exec cover the storage layer's needs;
|
||||
// * the sqlite3_value_* family + bind/column_value — only
|
||||
// meaningful with user-defined functions (callbacks, above);
|
||||
// * varargs APIs (sqlite3_config, db_config, log, mprintf) —
|
||||
// configuration happens via compile-time defines in the Makefile;
|
||||
// * UTF-16 variants (we are UTF-8 only), the mutex/VFS layer
|
||||
// (built SQLITE_THREADSAFE=0), sessions/snapshots/vtabs/loadable
|
||||
// extensions (not compiled in), deprecated API (compiled out via
|
||||
// SQLITE_OMIT_DEPRECATED).
|
||||
//
|
||||
// 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.
|
||||
// FFI type choices: handles cross as `usize` (`sqlite3*`,
|
||||
// `sqlite3_stmt*`, `sqlite3_blob*`, `sqlite3_backup*` are opaque and
|
||||
// never dereferenced; `usize` keeps out-params single-level `*usize`
|
||||
// instead of `**void`). Whole C strings IN are `[:0]u8` (the std FFI
|
||||
// convention — build terminated copies with `cstring`);
|
||||
// `prepare_v2/v3` and `bind_text/blob` take `[*]u8` + explicit byte
|
||||
// length instead, so un-terminated sx string views pass without
|
||||
// copying. Nullable C strings OUT are `*u8` + a cast null-check (the
|
||||
// `getenv` convention — `[:0]u8` has no null), and are COPIED into
|
||||
// `context.allocator` before returning — sqlite's buffers die on the
|
||||
// next step/finalize/close.
|
||||
// =====================================================================
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
sqlib :: #library "sqlite3";
|
||||
|
||||
// ── FFI: connection lifecycle ─────────────────────────────────────────
|
||||
sqlite3_open :: (path: [:0]u8, out_db: *usize) -> i32 #foreign sqlib "dist_sqlite3_open";
|
||||
sqlite3_open_v2 :: (path: [:0]u8, out_db: *usize, flags: i32, vfs: usize) -> i32 #foreign sqlib "dist_sqlite3_open_v2";
|
||||
sqlite3_close :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_close";
|
||||
sqlite3_exec :: (db: usize, sql: [:0]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) -> *u8 #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) -> *u8 #foreign sqlib "dist_sqlite3_errmsg";
|
||||
sqlite3_libversion :: () -> *u8 #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";
|
||||
sqlite3_close_v2 :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_close_v2";
|
||||
|
||||
// Result codes (the subset the wrappers interpret).
|
||||
SQLITE_OK :: 0;
|
||||
SQLITE_ROW :: 100;
|
||||
SQLITE_DONE :: 101;
|
||||
// ── FFI: errors ───────────────────────────────────────────────────────
|
||||
sqlite3_errcode :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_errcode";
|
||||
sqlite3_extended_errcode :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_extended_errcode";
|
||||
sqlite3_errmsg :: (db: usize) -> *u8 #foreign sqlib "dist_sqlite3_errmsg";
|
||||
sqlite3_errstr :: (code: i32) -> *u8 #foreign sqlib "dist_sqlite3_errstr";
|
||||
sqlite3_error_offset :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_error_offset";
|
||||
sqlite3_extended_result_codes :: (db: usize, onoff: i32) -> i32 #foreign sqlib "dist_sqlite3_extended_result_codes";
|
||||
|
||||
// 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;
|
||||
// ── FFI: connection state & control ───────────────────────────────────
|
||||
sqlite3_busy_timeout :: (db: usize, ms: i32) -> i32 #foreign sqlib "dist_sqlite3_busy_timeout";
|
||||
sqlite3_interrupt :: (db: usize) #foreign sqlib "dist_sqlite3_interrupt";
|
||||
sqlite3_is_interrupted :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_is_interrupted";
|
||||
sqlite3_get_autocommit :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_get_autocommit";
|
||||
sqlite3_txn_state :: (db: usize, schema: [:0]u8) -> i32 #foreign sqlib "dist_sqlite3_txn_state";
|
||||
sqlite3_db_filename :: (db: usize, db_name: [:0]u8) -> *u8 #foreign sqlib "dist_sqlite3_db_filename";
|
||||
sqlite3_db_readonly :: (db: usize, db_name: [:0]u8) -> i32 #foreign sqlib "dist_sqlite3_db_readonly";
|
||||
sqlite3_db_cacheflush :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_db_cacheflush";
|
||||
sqlite3_db_release_memory :: (db: usize) -> i32 #foreign sqlib "dist_sqlite3_db_release_memory";
|
||||
sqlite3_last_insert_rowid :: (db: usize) -> i64 #foreign sqlib "dist_sqlite3_last_insert_rowid";
|
||||
sqlite3_set_last_insert_rowid :: (db: usize, rowid: i64) #foreign sqlib "dist_sqlite3_set_last_insert_rowid";
|
||||
sqlite3_changes64 :: (db: usize) -> i64 #foreign sqlib "dist_sqlite3_changes64";
|
||||
sqlite3_total_changes64 :: (db: usize) -> i64 #foreign sqlib "dist_sqlite3_total_changes64";
|
||||
sqlite3_limit :: (db: usize, id: i32, new_val: i32) -> i32 #foreign sqlib "dist_sqlite3_limit";
|
||||
sqlite3_exec :: (db: usize, sql: [:0]u8, cb: usize, arg: usize, errmsg: usize) -> i32 #foreign sqlib "dist_sqlite3_exec";
|
||||
sqlite3_table_column_metadata :: (db: usize, db_name: [:0]u8, table: [:0]u8, column: [:0]u8, out_data_type: *usize, out_coll_seq: *usize, out_not_null: *i32, out_primary_key: *i32, out_autoinc: *i32) -> i32 #foreign sqlib "dist_sqlite3_table_column_metadata";
|
||||
|
||||
// Column types (sqlite3_column_type).
|
||||
// ── FFI: statements ───────────────────────────────────────────────────
|
||||
sqlite3_prepare_v2 :: (db: usize, sql: [*]u8, nbyte: i32, out_stmt: *usize, out_tail: usize) -> i32 #foreign sqlib "dist_sqlite3_prepare_v2";
|
||||
sqlite3_prepare_v3 :: (db: usize, sql: [*]u8, nbyte: i32, prep_flags: u32, out_stmt: *usize, out_tail: usize) -> i32 #foreign sqlib "dist_sqlite3_prepare_v3";
|
||||
sqlite3_step :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_step";
|
||||
sqlite3_reset :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_reset";
|
||||
sqlite3_finalize :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_finalize";
|
||||
sqlite3_clear_bindings :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_clear_bindings";
|
||||
sqlite3_sql :: (stmt: usize) -> *u8 #foreign sqlib "dist_sqlite3_sql";
|
||||
sqlite3_expanded_sql :: (stmt: usize) -> *u8 #foreign sqlib "dist_sqlite3_expanded_sql";
|
||||
sqlite3_stmt_busy :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_stmt_busy";
|
||||
sqlite3_stmt_readonly :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_stmt_readonly";
|
||||
sqlite3_stmt_isexplain :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_stmt_isexplain";
|
||||
sqlite3_stmt_explain :: (stmt: usize, mode: i32) -> i32 #foreign sqlib "dist_sqlite3_stmt_explain";
|
||||
sqlite3_stmt_status :: (stmt: usize, op: i32, reset: i32) -> i32 #foreign sqlib "dist_sqlite3_stmt_status";
|
||||
sqlite3_db_handle :: (stmt: usize) -> usize #foreign sqlib "dist_sqlite3_db_handle";
|
||||
sqlite3_next_stmt :: (db: usize, stmt: usize) -> usize #foreign sqlib "dist_sqlite3_next_stmt";
|
||||
|
||||
// ── FFI: binding ──────────────────────────────────────────────────────
|
||||
sqlite3_bind_parameter_count :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_bind_parameter_count";
|
||||
sqlite3_bind_parameter_index :: (stmt: usize, name: [:0]u8) -> i32 #foreign sqlib "dist_sqlite3_bind_parameter_index";
|
||||
sqlite3_bind_parameter_name :: (stmt: usize, idx: i32) -> *u8 #foreign sqlib "dist_sqlite3_bind_parameter_name";
|
||||
sqlite3_bind_text :: (stmt: usize, idx: i32, text: [*]u8, n: i32, destructor: isize) -> i32 #foreign sqlib "dist_sqlite3_bind_text";
|
||||
sqlite3_bind_blob :: (stmt: usize, idx: i32, data: [*]u8, n: i32, destructor: isize) -> i32 #foreign sqlib "dist_sqlite3_bind_blob";
|
||||
sqlite3_bind_double :: (stmt: usize, idx: i32, v: f64) -> i32 #foreign sqlib "dist_sqlite3_bind_double";
|
||||
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_bind_zeroblob64 :: (stmt: usize, idx: i32, n: u64) -> i32 #foreign sqlib "dist_sqlite3_bind_zeroblob64";
|
||||
|
||||
// ── FFI: result columns ───────────────────────────────────────────────
|
||||
sqlite3_column_blob :: (stmt: usize, icol: i32) -> *u8 #foreign sqlib "dist_sqlite3_column_blob";
|
||||
sqlite3_column_double :: (stmt: usize, icol: i32) -> f64 #foreign sqlib "dist_sqlite3_column_double";
|
||||
sqlite3_column_int64 :: (stmt: usize, icol: i32) -> i64 #foreign sqlib "dist_sqlite3_column_int64";
|
||||
sqlite3_column_text :: (stmt: usize, icol: i32) -> *u8 #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_column_name :: (stmt: usize, icol: i32) -> *u8 #foreign sqlib "dist_sqlite3_column_name";
|
||||
sqlite3_column_decltype :: (stmt: usize, icol: i32) -> *u8 #foreign sqlib "dist_sqlite3_column_decltype";
|
||||
sqlite3_column_database_name :: (stmt: usize, icol: i32) -> *u8 #foreign sqlib "dist_sqlite3_column_database_name";
|
||||
sqlite3_column_table_name :: (stmt: usize, icol: i32) -> *u8 #foreign sqlib "dist_sqlite3_column_table_name";
|
||||
sqlite3_column_origin_name :: (stmt: usize, icol: i32) -> *u8 #foreign sqlib "dist_sqlite3_column_origin_name";
|
||||
sqlite3_data_count :: (stmt: usize) -> i32 #foreign sqlib "dist_sqlite3_data_count";
|
||||
|
||||
// ── FFI: incremental blob I/O ─────────────────────────────────────────
|
||||
sqlite3_blob_open :: (db: usize, db_name: [:0]u8, table: [:0]u8, column: [:0]u8, rowid: i64, flags: i32, out_blob: *usize) -> i32 #foreign sqlib "dist_sqlite3_blob_open";
|
||||
sqlite3_blob_reopen :: (blob: usize, rowid: i64) -> i32 #foreign sqlib "dist_sqlite3_blob_reopen";
|
||||
sqlite3_blob_close :: (blob: usize) -> i32 #foreign sqlib "dist_sqlite3_blob_close";
|
||||
sqlite3_blob_bytes :: (blob: usize) -> i32 #foreign sqlib "dist_sqlite3_blob_bytes";
|
||||
sqlite3_blob_read :: (blob: usize, buf: [*]u8, n: i32, offset: i32) -> i32 #foreign sqlib "dist_sqlite3_blob_read";
|
||||
sqlite3_blob_write :: (blob: usize, buf: [*]u8, n: i32, offset: i32) -> i32 #foreign sqlib "dist_sqlite3_blob_write";
|
||||
|
||||
// ── FFI: online backup ────────────────────────────────────────────────
|
||||
sqlite3_backup_init :: (dst: usize, dst_name: [:0]u8, src: usize, src_name: [:0]u8) -> usize #foreign sqlib "dist_sqlite3_backup_init";
|
||||
sqlite3_backup_step :: (bk: usize, n_page: i32) -> i32 #foreign sqlib "dist_sqlite3_backup_step";
|
||||
sqlite3_backup_finish :: (bk: usize) -> i32 #foreign sqlib "dist_sqlite3_backup_finish";
|
||||
sqlite3_backup_remaining :: (bk: usize) -> i32 #foreign sqlib "dist_sqlite3_backup_remaining";
|
||||
sqlite3_backup_pagecount :: (bk: usize) -> i32 #foreign sqlib "dist_sqlite3_backup_pagecount";
|
||||
|
||||
// ── FFI: serialization ────────────────────────────────────────────────
|
||||
sqlite3_serialize :: (db: usize, schema: [:0]u8, out_size: *i64, flags: u32) -> usize #foreign sqlib "dist_sqlite3_serialize";
|
||||
sqlite3_deserialize :: (db: usize, schema: [:0]u8, data: usize, sz_db: i64, sz_buf: i64, flags: u32) -> i32 #foreign sqlib "dist_sqlite3_deserialize";
|
||||
|
||||
// ── FFI: library utilities ────────────────────────────────────────────
|
||||
sqlite3_libversion :: () -> *u8 #foreign sqlib "dist_sqlite3_libversion";
|
||||
sqlite3_libversion_number :: () -> i32 #foreign sqlib "dist_sqlite3_libversion_number";
|
||||
sqlite3_sourceid :: () -> *u8 #foreign sqlib "dist_sqlite3_sourceid";
|
||||
sqlite3_threadsafe :: () -> i32 #foreign sqlib "dist_sqlite3_threadsafe";
|
||||
sqlite3_compileoption_used :: (name: [:0]u8) -> i32 #foreign sqlib "dist_sqlite3_compileoption_used";
|
||||
sqlite3_compileoption_get :: (n: i32) -> *u8 #foreign sqlib "dist_sqlite3_compileoption_get";
|
||||
sqlite3_complete :: (sql: [:0]u8) -> i32 #foreign sqlib "dist_sqlite3_complete";
|
||||
sqlite3_free :: (p: usize) #foreign sqlib "dist_sqlite3_free";
|
||||
sqlite3_malloc64 :: (n: u64) -> usize #foreign sqlib "dist_sqlite3_malloc64";
|
||||
sqlite3_msize :: (p: usize) -> u64 #foreign sqlib "dist_sqlite3_msize";
|
||||
sqlite3_memory_used :: () -> i64 #foreign sqlib "dist_sqlite3_memory_used";
|
||||
sqlite3_memory_highwater :: (reset: i32) -> i64 #foreign sqlib "dist_sqlite3_memory_highwater";
|
||||
sqlite3_release_memory :: (n: i32) -> i32 #foreign sqlib "dist_sqlite3_release_memory";
|
||||
sqlite3_soft_heap_limit64 :: (n: i64) -> i64 #foreign sqlib "dist_sqlite3_soft_heap_limit64";
|
||||
sqlite3_hard_heap_limit64 :: (n: i64) -> i64 #foreign sqlib "dist_sqlite3_hard_heap_limit64";
|
||||
sqlite3_randomness :: (n: i32, buf: [*]u8) #foreign sqlib "dist_sqlite3_randomness";
|
||||
sqlite3_sleep :: (ms: i32) -> i32 #foreign sqlib "dist_sqlite3_sleep";
|
||||
sqlite3_stricmp :: (a: [:0]u8, b: [:0]u8) -> i32 #foreign sqlib "dist_sqlite3_stricmp";
|
||||
sqlite3_strnicmp :: (a: [:0]u8, b: [:0]u8, n: i32) -> i32 #foreign sqlib "dist_sqlite3_strnicmp";
|
||||
sqlite3_strglob :: (glob: [:0]u8, s: [:0]u8) -> i32 #foreign sqlib "dist_sqlite3_strglob";
|
||||
sqlite3_strlike :: (like: [:0]u8, s: [:0]u8, esc: u32) -> i32 #foreign sqlib "dist_sqlite3_strlike";
|
||||
|
||||
// ── Result codes (primary; full set) ──────────────────────────────────
|
||||
SQLITE_OK :: 0;
|
||||
SQLITE_ERROR :: 1;
|
||||
SQLITE_INTERNAL :: 2;
|
||||
SQLITE_PERM :: 3;
|
||||
SQLITE_ABORT :: 4;
|
||||
SQLITE_BUSY :: 5;
|
||||
SQLITE_LOCKED :: 6;
|
||||
SQLITE_NOMEM :: 7;
|
||||
SQLITE_READONLY :: 8;
|
||||
SQLITE_INTERRUPT :: 9;
|
||||
SQLITE_IOERR :: 10;
|
||||
SQLITE_CORRUPT :: 11;
|
||||
SQLITE_NOTFOUND :: 12;
|
||||
SQLITE_FULL :: 13;
|
||||
SQLITE_CANTOPEN :: 14;
|
||||
SQLITE_PROTOCOL :: 15;
|
||||
SQLITE_EMPTY :: 16;
|
||||
SQLITE_SCHEMA :: 17;
|
||||
SQLITE_TOOBIG :: 18;
|
||||
SQLITE_CONSTRAINT :: 19;
|
||||
SQLITE_MISMATCH :: 20;
|
||||
SQLITE_MISUSE :: 21;
|
||||
SQLITE_NOLFS :: 22;
|
||||
SQLITE_AUTH :: 23;
|
||||
SQLITE_FORMAT :: 24;
|
||||
SQLITE_RANGE :: 25;
|
||||
SQLITE_NOTADB :: 26;
|
||||
SQLITE_NOTICE :: 27;
|
||||
SQLITE_WARNING :: 28;
|
||||
SQLITE_ROW :: 100;
|
||||
SQLITE_DONE :: 101;
|
||||
|
||||
// Extended result codes are `primary | (n << 8)`; the families a storage
|
||||
// layer actually branches on are spelled out, others compute by formula.
|
||||
SQLITE_ABORT_ROLLBACK :: 516; // ABORT | (2<<8)
|
||||
SQLITE_BUSY_RECOVERY :: 261; // BUSY | (1<<8)
|
||||
SQLITE_BUSY_SNAPSHOT :: 517; // BUSY | (2<<8)
|
||||
SQLITE_BUSY_TIMEOUT :: 773; // BUSY | (3<<8)
|
||||
SQLITE_LOCKED_SHAREDCACHE :: 262; // LOCKED | (1<<8)
|
||||
SQLITE_LOCKED_VTAB :: 518; // LOCKED | (2<<8)
|
||||
SQLITE_CONSTRAINT_CHECK :: 275; // CONSTRAINT | (1<<8)
|
||||
SQLITE_CONSTRAINT_COMMITHOOK :: 531;
|
||||
SQLITE_CONSTRAINT_FOREIGNKEY :: 787;
|
||||
SQLITE_CONSTRAINT_FUNCTION :: 1043;
|
||||
SQLITE_CONSTRAINT_NOTNULL :: 1299;
|
||||
SQLITE_CONSTRAINT_PRIMARYKEY :: 1555;
|
||||
SQLITE_CONSTRAINT_TRIGGER :: 1811;
|
||||
SQLITE_CONSTRAINT_UNIQUE :: 2067;
|
||||
SQLITE_CONSTRAINT_VTAB :: 2323;
|
||||
SQLITE_CONSTRAINT_ROWID :: 2579;
|
||||
SQLITE_CONSTRAINT_PINNED :: 2835;
|
||||
SQLITE_CONSTRAINT_DATATYPE :: 3091;
|
||||
|
||||
// ── Fundamental datatypes (column_type) ───────────────────────────────
|
||||
SQLITE_INTEGER :: 1;
|
||||
SQLITE_FLOAT :: 2;
|
||||
SQLITE_TEXT :: 3;
|
||||
SQLITE_BLOB :: 4;
|
||||
SQLITE_NULL :: 5;
|
||||
|
||||
// ── open_v2 flags ─────────────────────────────────────────────────────
|
||||
SQLITE_OPEN_READONLY :: 0x00000001;
|
||||
SQLITE_OPEN_READWRITE :: 0x00000002;
|
||||
SQLITE_OPEN_CREATE :: 0x00000004;
|
||||
SQLITE_OPEN_URI :: 0x00000040;
|
||||
SQLITE_OPEN_MEMORY :: 0x00000080;
|
||||
SQLITE_OPEN_NOMUTEX :: 0x00008000;
|
||||
SQLITE_OPEN_FULLMUTEX :: 0x00010000;
|
||||
SQLITE_OPEN_NOFOLLOW :: 0x01000000;
|
||||
SQLITE_OPEN_EXRESCODE :: 0x02000000;
|
||||
|
||||
// ── prepare_v3 flags ──────────────────────────────────────────────────
|
||||
SQLITE_PREPARE_PERSISTENT :: 0x01;
|
||||
SQLITE_PREPARE_NO_VTAB :: 0x04;
|
||||
|
||||
// ── txn_state values ──────────────────────────────────────────────────
|
||||
SQLITE_TXN_NONE :: 0;
|
||||
SQLITE_TXN_READ :: 1;
|
||||
SQLITE_TXN_WRITE :: 2;
|
||||
|
||||
// ── limit ids ─────────────────────────────────────────────────────────
|
||||
SQLITE_LIMIT_LENGTH :: 0;
|
||||
SQLITE_LIMIT_SQL_LENGTH :: 1;
|
||||
SQLITE_LIMIT_COLUMN :: 2;
|
||||
SQLITE_LIMIT_EXPR_DEPTH :: 3;
|
||||
SQLITE_LIMIT_COMPOUND_SELECT :: 4;
|
||||
SQLITE_LIMIT_VDBE_OP :: 5;
|
||||
SQLITE_LIMIT_FUNCTION_ARG :: 6;
|
||||
SQLITE_LIMIT_ATTACHED :: 7;
|
||||
SQLITE_LIMIT_LIKE_PATTERN_LENGTH :: 8;
|
||||
SQLITE_LIMIT_VARIABLE_NUMBER :: 9;
|
||||
SQLITE_LIMIT_TRIGGER_DEPTH :: 10;
|
||||
SQLITE_LIMIT_WORKER_THREADS :: 11;
|
||||
|
||||
// ── stmt_status ops ───────────────────────────────────────────────────
|
||||
SQLITE_STMTSTATUS_FULLSCAN_STEP :: 1;
|
||||
SQLITE_STMTSTATUS_SORT :: 2;
|
||||
SQLITE_STMTSTATUS_AUTOINDEX :: 3;
|
||||
SQLITE_STMTSTATUS_VM_STEP :: 4;
|
||||
SQLITE_STMTSTATUS_REPREPARE :: 5;
|
||||
SQLITE_STMTSTATUS_RUN :: 6;
|
||||
SQLITE_STMTSTATUS_FILTER_MISS :: 7;
|
||||
SQLITE_STMTSTATUS_FILTER_HIT :: 8;
|
||||
SQLITE_STMTSTATUS_MEMUSED :: 99;
|
||||
|
||||
// ── serialize / deserialize flags ─────────────────────────────────────
|
||||
SQLITE_SERIALIZE_NOCOPY :: 1;
|
||||
SQLITE_DESERIALIZE_FREEONCLOSE :: 1;
|
||||
SQLITE_DESERIALIZE_RESIZEABLE :: 2;
|
||||
SQLITE_DESERIALIZE_READONLY :: 4;
|
||||
|
||||
// bind destructor sentinels: TRANSIENT = copy the bytes NOW (sx-side
|
||||
// buffers — arena strings, stack temporaries — don't outlive the call);
|
||||
// STATIC = the caller guarantees the bytes outlive the statement.
|
||||
SQLITE_STATIC : isize : 0;
|
||||
SQLITE_TRANSIENT : isize : -1;
|
||||
|
||||
SqliteErr :: error {
|
||||
Open,
|
||||
Exec,
|
||||
Prepare,
|
||||
Bind,
|
||||
Step,
|
||||
Blob,
|
||||
Backup,
|
||||
Serialize,
|
||||
Metadata,
|
||||
}
|
||||
|
||||
// ── string helpers ────────────────────────────────────────────────────
|
||||
|
||||
// `s` as a null-terminated heap copy, passable where `[:0]u8` is expected.
|
||||
sq_cstr :: (s: string) -> string {
|
||||
z := cstring(s.len);
|
||||
@@ -92,10 +316,86 @@ sq_from_cstr :: (p: *u8) -> string {
|
||||
return string.{ ptr = raw, len = n };
|
||||
}
|
||||
|
||||
// `n` raw bytes at `p` as a context.allocator copy ("" when p is null).
|
||||
sq_copy_bytes :: (p: *u8, n: i64) -> string {
|
||||
addr : i64 = xx p;
|
||||
if addr == 0 or n <= 0 { return ""; }
|
||||
cp : [*]u8 = xx p;
|
||||
raw : [*]u8 = xx context.allocator.alloc_bytes(n + 1);
|
||||
memcpy(raw, cp, n);
|
||||
raw[n] = 0;
|
||||
return string.{ ptr = raw, len = n };
|
||||
}
|
||||
|
||||
// ── library-level utilities ───────────────────────────────────────────
|
||||
|
||||
// The linked/loaded SQLite's version string (e.g. "3.53.2").
|
||||
sqlite_version :: () -> string {
|
||||
return sq_from_cstr(sqlite3_libversion());
|
||||
}
|
||||
sqlite_version_number :: () -> i64 {
|
||||
return xx sqlite3_libversion_number();
|
||||
}
|
||||
sqlite_sourceid :: () -> string {
|
||||
return sq_from_cstr(sqlite3_sourceid());
|
||||
}
|
||||
sqlite_threadsafe :: () -> bool {
|
||||
return sqlite3_threadsafe() != 0;
|
||||
}
|
||||
sqlite_compileoption_used :: (name: string) -> bool {
|
||||
return sqlite3_compileoption_used(sq_cstr(name)) != 0;
|
||||
}
|
||||
// The n'th compile option ("" past the end).
|
||||
sqlite_compileoption_get :: (n: i64) -> string {
|
||||
return sq_from_cstr(sqlite3_compileoption_get(xx n));
|
||||
}
|
||||
// Human text for a result code (static storage; copied anyway).
|
||||
sqlite_errstr :: (code: i64) -> string {
|
||||
return sq_from_cstr(sqlite3_errstr(xx code));
|
||||
}
|
||||
// True iff `sql` ends in a complete SQL statement.
|
||||
sqlite_complete :: (sql: string) -> bool {
|
||||
return sqlite3_complete(sq_cstr(sql)) != 0;
|
||||
}
|
||||
// `n` bytes from SQLite's CSPRNG.
|
||||
sqlite_randomness :: (n: i64) -> string {
|
||||
raw : [*]u8 = xx context.allocator.alloc_bytes(n + 1);
|
||||
sqlite3_randomness(xx n, raw);
|
||||
raw[n] = 0;
|
||||
return string.{ ptr = raw, len = n };
|
||||
}
|
||||
sqlite_sleep :: (ms: i64) -> i64 {
|
||||
return xx sqlite3_sleep(xx ms);
|
||||
}
|
||||
// memory_used/highwater read 0 in this build: SQLITE_DEFAULT_MEMSTATUS=0.
|
||||
sqlite_memory_used :: () -> i64 {
|
||||
return sqlite3_memory_used();
|
||||
}
|
||||
sqlite_memory_highwater :: (reset: bool) -> i64 {
|
||||
return sqlite3_memory_highwater(if reset then 1 else 0);
|
||||
}
|
||||
sqlite_release_memory :: (n: i64) -> i64 {
|
||||
return xx sqlite3_release_memory(xx n);
|
||||
}
|
||||
sqlite_soft_heap_limit :: (n: i64) -> i64 {
|
||||
return sqlite3_soft_heap_limit64(n);
|
||||
}
|
||||
sqlite_hard_heap_limit :: (n: i64) -> i64 {
|
||||
return sqlite3_hard_heap_limit64(n);
|
||||
}
|
||||
// GLOB / LIKE / case-insensitive compare, sqlite's own semantics.
|
||||
// strglob/strlike answer 0 on MATCH (C convention) — exposed as bools.
|
||||
sqlite_strglob :: (glob: string, s: string) -> bool {
|
||||
return sqlite3_strglob(sq_cstr(glob), sq_cstr(s)) == 0;
|
||||
}
|
||||
sqlite_strlike :: (like: string, s: string, esc: i64) -> bool {
|
||||
return sqlite3_strlike(sq_cstr(like), sq_cstr(s), xx esc) == 0;
|
||||
}
|
||||
sqlite_stricmp :: (a: string, b: string) -> i64 {
|
||||
return xx sqlite3_stricmp(sq_cstr(a), sq_cstr(b));
|
||||
}
|
||||
|
||||
// ── prepared statements ───────────────────────────────────────────────
|
||||
|
||||
// One prepared statement. `finalize` releases it; bind indexes are
|
||||
// 1-based and column indexes 0-based, as in the C API.
|
||||
@@ -103,11 +403,22 @@ SqliteStmt :: struct {
|
||||
handle: usize;
|
||||
db: usize;
|
||||
|
||||
// ── binding ──
|
||||
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_blob :: (self: *SqliteStmt, idx: i64, bytes: string) -> !SqliteErr {
|
||||
rc := sqlite3_bind_blob(self.handle, xx idx, bytes.ptr, xx bytes.len, SQLITE_TRANSIENT);
|
||||
if rc != SQLITE_OK { raise error.Bind; }
|
||||
return;
|
||||
}
|
||||
bind_double :: (self: *SqliteStmt, idx: i64, v: f64) -> !SqliteErr {
|
||||
rc := sqlite3_bind_double(self.handle, xx idx, v);
|
||||
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; }
|
||||
@@ -118,7 +429,30 @@ SqliteStmt :: struct {
|
||||
if rc != SQLITE_OK { raise error.Bind; }
|
||||
return;
|
||||
}
|
||||
// Reserve an `n`-byte zero-filled blob (fill it via SqliteBlob I/O).
|
||||
bind_zeroblob :: (self: *SqliteStmt, idx: i64, n: i64) -> !SqliteErr {
|
||||
rc := sqlite3_bind_zeroblob64(self.handle, xx idx, xx n);
|
||||
if rc != SQLITE_OK { raise error.Bind; }
|
||||
return;
|
||||
}
|
||||
clear_bindings :: (self: *SqliteStmt) {
|
||||
sqlite3_clear_bindings(self.handle);
|
||||
}
|
||||
|
||||
// ── parameters ──
|
||||
parameter_count :: (self: *SqliteStmt) -> i64 {
|
||||
return xx sqlite3_bind_parameter_count(self.handle);
|
||||
}
|
||||
// 1-based index of a named parameter (":name" / "@name" / "?N"), 0 if absent.
|
||||
parameter_index :: (self: *SqliteStmt, name: string) -> i64 {
|
||||
return xx sqlite3_bind_parameter_index(self.handle, sq_cstr(name));
|
||||
}
|
||||
// The name of parameter `idx` ("" for nameless `?` parameters).
|
||||
parameter_name :: (self: *SqliteStmt, idx: i64) -> string {
|
||||
return sq_from_cstr(sqlite3_bind_parameter_name(self.handle, xx idx));
|
||||
}
|
||||
|
||||
// ── execution ──
|
||||
// SQLITE_ROW / SQLITE_DONE on success; anything else raises with the
|
||||
// detail left in the connection's errmsg.
|
||||
step :: (self: *SqliteStmt) -> (i64, !SqliteErr) {
|
||||
@@ -126,29 +460,6 @@ SqliteStmt :: struct {
|
||||
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);
|
||||
addr : i64 = xx p;
|
||||
if addr == 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);
|
||||
}
|
||||
@@ -156,10 +467,95 @@ SqliteStmt :: struct {
|
||||
sqlite3_finalize(self.handle);
|
||||
self.handle = 0;
|
||||
}
|
||||
|
||||
// ── columns (copies into context.allocator; NULL reads as ""/0) ──
|
||||
column_int64 :: (self: *SqliteStmt, icol: i64) -> i64 {
|
||||
return sqlite3_column_int64(self.handle, xx icol);
|
||||
}
|
||||
column_double :: (self: *SqliteStmt, icol: i64) -> f64 {
|
||||
return sqlite3_column_double(self.handle, xx icol);
|
||||
}
|
||||
column_text :: (self: *SqliteStmt, icol: i64) -> string {
|
||||
p := sqlite3_column_text(self.handle, xx icol);
|
||||
n := sqlite3_column_bytes(self.handle, xx icol);
|
||||
return sq_copy_bytes(p, xx n);
|
||||
}
|
||||
column_blob :: (self: *SqliteStmt, icol: i64) -> string {
|
||||
p := sqlite3_column_blob(self.handle, xx icol);
|
||||
n := sqlite3_column_bytes(self.handle, xx icol);
|
||||
return sq_copy_bytes(p, xx n);
|
||||
}
|
||||
column_bytes :: (self: *SqliteStmt, icol: i64) -> i64 {
|
||||
return xx sqlite3_column_bytes(self.handle, xx icol);
|
||||
}
|
||||
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);
|
||||
}
|
||||
column_name :: (self: *SqliteStmt, icol: i64) -> string {
|
||||
return sq_from_cstr(sqlite3_column_name(self.handle, xx icol));
|
||||
}
|
||||
// The declared type from the schema ("" for expressions).
|
||||
column_decltype :: (self: *SqliteStmt, icol: i64) -> string {
|
||||
return sq_from_cstr(sqlite3_column_decltype(self.handle, xx icol));
|
||||
}
|
||||
column_database_name :: (self: *SqliteStmt, icol: i64) -> string {
|
||||
return sq_from_cstr(sqlite3_column_database_name(self.handle, xx icol));
|
||||
}
|
||||
column_table_name :: (self: *SqliteStmt, icol: i64) -> string {
|
||||
return sq_from_cstr(sqlite3_column_table_name(self.handle, xx icol));
|
||||
}
|
||||
column_origin_name :: (self: *SqliteStmt, icol: i64) -> string {
|
||||
return sq_from_cstr(sqlite3_column_origin_name(self.handle, xx icol));
|
||||
}
|
||||
data_count :: (self: *SqliteStmt) -> i64 {
|
||||
return xx sqlite3_data_count(self.handle);
|
||||
}
|
||||
|
||||
// ── introspection ──
|
||||
sql :: (self: *SqliteStmt) -> string {
|
||||
return sq_from_cstr(sqlite3_sql(self.handle));
|
||||
}
|
||||
// The SQL with bound parameters substituted; sqlite allocates, we
|
||||
// copy + free.
|
||||
expanded_sql :: (self: *SqliteStmt) -> string {
|
||||
p := sqlite3_expanded_sql(self.handle);
|
||||
s := sq_from_cstr(p);
|
||||
addr : i64 = xx p;
|
||||
if addr != 0 { sqlite3_free(xx addr); }
|
||||
return s;
|
||||
}
|
||||
busy :: (self: *SqliteStmt) -> bool {
|
||||
return sqlite3_stmt_busy(self.handle) != 0;
|
||||
}
|
||||
readonly :: (self: *SqliteStmt) -> bool {
|
||||
return sqlite3_stmt_readonly(self.handle) != 0;
|
||||
}
|
||||
// 0 = normal, 1 = EXPLAIN, 2 = EXPLAIN QUERY PLAN.
|
||||
isexplain :: (self: *SqliteStmt) -> i64 {
|
||||
return xx sqlite3_stmt_isexplain(self.handle);
|
||||
}
|
||||
status :: (self: *SqliteStmt, op: i64, reset: bool) -> i64 {
|
||||
return xx sqlite3_stmt_status(self.handle, xx op, if reset then 1 else 0);
|
||||
}
|
||||
}
|
||||
|
||||
// One connection. `open` creates the file (and parent-less path) per
|
||||
// SQLite's defaults; `close` is safe on an already-closed handle.
|
||||
// ── table column metadata (ENABLE_COLUMN_METADATA build) ─────────────
|
||||
ColumnMeta :: struct {
|
||||
data_type: string;
|
||||
coll_seq: string;
|
||||
not_null: bool;
|
||||
primary_key: bool;
|
||||
autoinc: bool;
|
||||
}
|
||||
|
||||
// ── connections ───────────────────────────────────────────────────────
|
||||
|
||||
// One connection. `open` creates the file per SQLite's defaults;
|
||||
// `open_v2` takes SQLITE_OPEN_* flags; `close` is safe on an
|
||||
// already-closed handle. Schema-qualified queries default to "main".
|
||||
Sqlite :: struct {
|
||||
handle: usize;
|
||||
|
||||
@@ -173,10 +569,26 @@ Sqlite :: struct {
|
||||
return Sqlite.{ handle = h };
|
||||
}
|
||||
|
||||
open_v2 :: (path: string, flags: i64) -> (Sqlite, !SqliteErr) {
|
||||
h : usize = 0;
|
||||
rc := sqlite3_open_v2(sq_cstr(path), @h, xx flags, 0);
|
||||
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;
|
||||
}
|
||||
// Deferred close: succeeds even with unfinalized statements, the
|
||||
// handle dies once the last one is finalized.
|
||||
close_v2 :: (self: *Sqlite) {
|
||||
if self.handle != 0 { sqlite3_close_v2(self.handle); }
|
||||
self.handle = 0;
|
||||
}
|
||||
|
||||
// Run one or more ;-separated statements with no result rows
|
||||
// (DDL, pragmas, BEGIN/COMMIT). Detail via errmsg on failure.
|
||||
@@ -192,16 +604,203 @@ Sqlite :: struct {
|
||||
if rc != SQLITE_OK { raise error.Prepare; }
|
||||
return SqliteStmt.{ handle = sh, db = self.handle };
|
||||
}
|
||||
// prepare with SQLITE_PREPARE_* flags (e.g. PERSISTENT for the
|
||||
// statement cache a storage layer keeps hot).
|
||||
prepare_v3 :: (self: *Sqlite, sql: string, flags: i64) -> (SqliteStmt, !SqliteErr) {
|
||||
sh : usize = 0;
|
||||
rc := sqlite3_prepare_v3(self.handle, sql.ptr, xx sql.len, xx flags, @sh, 0);
|
||||
if rc != SQLITE_OK { raise error.Prepare; }
|
||||
return SqliteStmt.{ handle = sh, db = self.handle };
|
||||
}
|
||||
|
||||
// ── errors ──
|
||||
errmsg :: (self: *Sqlite) -> string {
|
||||
return sq_from_cstr(sqlite3_errmsg(self.handle));
|
||||
}
|
||||
errcode :: (self: *Sqlite) -> i64 {
|
||||
return xx sqlite3_errcode(self.handle);
|
||||
}
|
||||
extended_errcode :: (self: *Sqlite) -> i64 {
|
||||
return xx sqlite3_extended_errcode(self.handle);
|
||||
}
|
||||
// Byte offset of the most recent error's token in its SQL, -1 if n/a.
|
||||
error_offset :: (self: *Sqlite) -> i64 {
|
||||
return xx sqlite3_error_offset(self.handle);
|
||||
}
|
||||
extended_result_codes :: (self: *Sqlite, on: bool) {
|
||||
sqlite3_extended_result_codes(self.handle, if on then 1 else 0);
|
||||
}
|
||||
|
||||
// ── state & control ──
|
||||
busy_timeout :: (self: *Sqlite, ms: i64) {
|
||||
sqlite3_busy_timeout(self.handle, xx ms);
|
||||
}
|
||||
interrupt :: (self: *Sqlite) {
|
||||
sqlite3_interrupt(self.handle);
|
||||
}
|
||||
is_interrupted :: (self: *Sqlite) -> bool {
|
||||
return sqlite3_is_interrupted(self.handle) != 0;
|
||||
}
|
||||
get_autocommit :: (self: *Sqlite) -> bool {
|
||||
return sqlite3_get_autocommit(self.handle) != 0;
|
||||
}
|
||||
// SQLITE_TXN_NONE / _READ / _WRITE for the "main" schema.
|
||||
txn_state :: (self: *Sqlite) -> i64 {
|
||||
return xx sqlite3_txn_state(self.handle, sq_cstr("main"));
|
||||
}
|
||||
db_filename :: (self: *Sqlite) -> string {
|
||||
return sq_from_cstr(sqlite3_db_filename(self.handle, sq_cstr("main")));
|
||||
}
|
||||
// 1 readonly, 0 read-write, -1 no such database name.
|
||||
db_readonly :: (self: *Sqlite) -> i64 {
|
||||
return xx sqlite3_db_readonly(self.handle, sq_cstr("main"));
|
||||
}
|
||||
cacheflush :: (self: *Sqlite) {
|
||||
sqlite3_db_cacheflush(self.handle);
|
||||
}
|
||||
db_release_memory :: (self: *Sqlite) {
|
||||
sqlite3_db_release_memory(self.handle);
|
||||
}
|
||||
last_insert_rowid :: (self: *Sqlite) -> i64 {
|
||||
return sqlite3_last_insert_rowid(self.handle);
|
||||
}
|
||||
|
||||
set_last_insert_rowid :: (self: *Sqlite, rowid: i64) {
|
||||
sqlite3_set_last_insert_rowid(self.handle, rowid);
|
||||
}
|
||||
changes :: (self: *Sqlite) -> i64 {
|
||||
return xx sqlite3_changes(self.handle);
|
||||
return sqlite3_changes64(self.handle);
|
||||
}
|
||||
total_changes :: (self: *Sqlite) -> i64 {
|
||||
return sqlite3_total_changes64(self.handle);
|
||||
}
|
||||
// Set a SQLITE_LIMIT_* runtime limit; answers the PRIOR value.
|
||||
// new_val < 0 reads without changing.
|
||||
limit :: (self: *Sqlite, id: i64, new_val: i64) -> i64 {
|
||||
return xx sqlite3_limit(self.handle, xx id, xx new_val);
|
||||
}
|
||||
|
||||
// Schema introspection for one column of "main".`table`.
|
||||
table_column_metadata :: (self: *Sqlite, table: string, column: string) -> (ColumnMeta, !SqliteErr) {
|
||||
dt : usize = 0;
|
||||
cs : usize = 0;
|
||||
nn : i32 = 0;
|
||||
pk : i32 = 0;
|
||||
ai : i32 = 0;
|
||||
rc := sqlite3_table_column_metadata(self.handle, sq_cstr("main"), sq_cstr(table), sq_cstr(column), @dt, @cs, @nn, @pk, @ai);
|
||||
if rc != SQLITE_OK { raise error.Metadata; }
|
||||
dtp : *u8 = xx dt;
|
||||
csp : *u8 = xx cs;
|
||||
return ColumnMeta.{
|
||||
data_type = sq_from_cstr(dtp), coll_seq = sq_from_cstr(csp),
|
||||
not_null = nn != 0, primary_key = pk != 0, autoinc = ai != 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ── serialization ──
|
||||
// The whole "main" database as bytes (a valid database image).
|
||||
serialize :: (self: *Sqlite) -> (string, !SqliteErr) {
|
||||
size : i64 = 0;
|
||||
p := sqlite3_serialize(self.handle, sq_cstr("main"), @size, 0);
|
||||
if p == 0 { raise error.Serialize; }
|
||||
bp : *u8 = xx p;
|
||||
out := sq_copy_bytes(bp, size);
|
||||
sqlite3_free(p);
|
||||
return out;
|
||||
}
|
||||
// Replace "main" with the database image in `bytes`. The image is
|
||||
// copied into sqlite-owned memory (FREEONCLOSE+RESIZEABLE), so
|
||||
// `bytes` may die immediately after.
|
||||
deserialize :: (self: *Sqlite, bytes: string) -> !SqliteErr {
|
||||
buf := sqlite3_malloc64(xx (bytes.len));
|
||||
if buf == 0 { raise error.Serialize; }
|
||||
bp : [*]u8 = xx buf;
|
||||
memcpy(bp, bytes.ptr, bytes.len);
|
||||
rc := sqlite3_deserialize(self.handle, sq_cstr("main"), buf, bytes.len, bytes.len,
|
||||
SQLITE_DESERIALIZE_FREEONCLOSE | SQLITE_DESERIALIZE_RESIZEABLE);
|
||||
if rc != SQLITE_OK { raise error.Serialize; }
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ── incremental blob I/O ──────────────────────────────────────────────
|
||||
|
||||
// An open handle onto one blob cell ("main".`table`.`column` at `rowid`).
|
||||
// Reserve space with bind_zeroblob, then stream through this. The handle
|
||||
// cannot change a blob's SIZE — that's an UPDATE.
|
||||
SqliteBlob :: struct {
|
||||
handle: usize;
|
||||
|
||||
open :: (db: *Sqlite, table: string, column: string, rowid: i64, writable: bool) -> (SqliteBlob, !SqliteErr) {
|
||||
h : usize = 0;
|
||||
rc := sqlite3_blob_open(db.handle, sq_cstr("main"), sq_cstr(table), sq_cstr(column),
|
||||
rowid, if writable then 1 else 0, @h);
|
||||
if rc != SQLITE_OK { raise error.Blob; }
|
||||
return SqliteBlob.{ handle = h };
|
||||
}
|
||||
// Point this handle at the same column in a DIFFERENT row (cheaper
|
||||
// than close + open).
|
||||
reopen :: (self: *SqliteBlob, rowid: i64) -> !SqliteErr {
|
||||
rc := sqlite3_blob_reopen(self.handle, rowid);
|
||||
if rc != SQLITE_OK { raise error.Blob; }
|
||||
return;
|
||||
}
|
||||
bytes :: (self: *SqliteBlob) -> i64 {
|
||||
return xx sqlite3_blob_bytes(self.handle);
|
||||
}
|
||||
read :: (self: *SqliteBlob, offset: i64, n: i64) -> (string, !SqliteErr) {
|
||||
raw : [*]u8 = xx context.allocator.alloc_bytes(n + 1);
|
||||
rc := sqlite3_blob_read(self.handle, raw, xx n, xx offset);
|
||||
if rc != SQLITE_OK { raise error.Blob; }
|
||||
raw[n] = 0;
|
||||
return string.{ ptr = raw, len = n };
|
||||
}
|
||||
write :: (self: *SqliteBlob, offset: i64, data: string) -> !SqliteErr {
|
||||
rc := sqlite3_blob_write(self.handle, data.ptr, xx data.len, xx offset);
|
||||
if rc != SQLITE_OK { raise error.Blob; }
|
||||
return;
|
||||
}
|
||||
close :: (self: *SqliteBlob) {
|
||||
if self.handle != 0 { sqlite3_blob_close(self.handle); }
|
||||
self.handle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── online backup ─────────────────────────────────────────────────────
|
||||
|
||||
// Copy `src`'s "main" into `dst`'s "main" page by page while both stay
|
||||
// usable. `run` drives it to completion in one call; `step` exposes the
|
||||
// incremental form.
|
||||
SqliteBackup :: struct {
|
||||
handle: usize;
|
||||
|
||||
init :: (dst: *Sqlite, src: *Sqlite) -> (SqliteBackup, !SqliteErr) {
|
||||
h := sqlite3_backup_init(dst.handle, sq_cstr("main"), src.handle, sq_cstr("main"));
|
||||
if h == 0 { raise error.Backup; }
|
||||
return SqliteBackup.{ handle = h };
|
||||
}
|
||||
// Copy up to `n` pages (-1 = all remaining). SQLITE_OK = more to do,
|
||||
// SQLITE_DONE = complete; BUSY/LOCKED are retryable per the C API.
|
||||
step :: (self: *SqliteBackup, n: i64) -> i64 {
|
||||
return xx sqlite3_backup_step(self.handle, xx n);
|
||||
}
|
||||
remaining :: (self: *SqliteBackup) -> i64 {
|
||||
return xx sqlite3_backup_remaining(self.handle);
|
||||
}
|
||||
pagecount :: (self: *SqliteBackup) -> i64 {
|
||||
return xx sqlite3_backup_pagecount(self.handle);
|
||||
}
|
||||
finish :: (self: *SqliteBackup) -> i64 {
|
||||
rc := sqlite3_backup_finish(self.handle);
|
||||
self.handle = 0;
|
||||
return xx rc;
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot full copy of src."main" into dst."main".
|
||||
sqlite_backup_run :: (dst: *Sqlite, src: *Sqlite) -> !SqliteErr {
|
||||
bk := try SqliteBackup.init(dst, src);
|
||||
rc := bk.step(-1);
|
||||
frc := bk.finish();
|
||||
if rc != SQLITE_DONE or frc != SQLITE_OK { raise error.Backup; }
|
||||
return;
|
||||
}
|
||||
|
||||
425
tests/sqlite_api.sx
Normal file
425
tests/sqlite_api.sx
Normal file
@@ -0,0 +1,425 @@
|
||||
// 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;
|
||||
}
|
||||
25
vendor/sqlite/README.md
vendored
25
vendor/sqlite/README.md
vendored
@@ -16,6 +16,31 @@ 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.
|
||||
|
||||
## Symbol renaming
|
||||
|
||||
The sx JIT resolves `#foreign` symbols via `dlsym(RTLD_DEFAULT)`, which
|
||||
searches every image already loaded into the process — the OS libsqlite3
|
||||
is usually among them and wins by load order. So every API function the
|
||||
bindings use is renamed `dist_sqlite3_*` in the vendored build: those
|
||||
symbols exist ONLY here, making resolution unambiguous in both JIT and
|
||||
AOT modes. The rename header is GENERATED by `make` into
|
||||
`build/vendor/rename.h` from the `#foreign` names in
|
||||
`src/db/sqlite.sx` — the bindings file is the single source of truth,
|
||||
and the rename list cannot drift from it.
|
||||
|
||||
## Bound surface
|
||||
|
||||
`src/db/sqlite.sx` maps the full practical C API (~100 functions):
|
||||
connection lifecycle + open_v2 flags, errors (extended codes included),
|
||||
statements with the complete bind/column families, parameter and column
|
||||
introspection (built with `SQLITE_ENABLE_COLUMN_METADATA`), incremental
|
||||
blob I/O, the online backup API, serialize/deserialize, and the library
|
||||
utilities. Not bound, by design: callback-taking APIs (hooks, UDFs,
|
||||
collations, authorizers — they need C→sx callbacks), the
|
||||
`sqlite3_value_*` family (UDF-coupled), varargs configuration, UTF-16
|
||||
variants, and subsystems this build omits (mutex/VFS under
|
||||
`SQLITE_THREADSAFE=0`, sessions/snapshots/vtabs, deprecated API).
|
||||
|
||||
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
34
vendor/sqlite/rename.h
vendored
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
Reference in New Issue
Block a user