Browse Source

v0.1.0: GNU Modula-2 SQLite core binding, utils, tests, showcase

Eric Streit 2 weeks ago
commit
ec09d60868

+ 12 - 0
.gitignore

@@ -0,0 +1,12 @@
+# SQLite sources (reference only, not built by default).
+sqlite-master/
+# Build outputs.
+build/
+# Modula-2 compiler artefacts.
+*.o
+*.a
+*.so
+# Test databases created by examples/tests.
+*.db
+*.sqlite
+*.sqlite3

+ 79 - 0
Makefile

@@ -0,0 +1,79 @@
+# m2SQLITE — GNU Modula-2 bindings for SQLite
+GM2      ?= gm2
+# Prefer pkg-config when the dev package exists, else fall back to the
+# runtime library path (no header needed: the binding links by symbol).
+PKGLIBS  := $(shell pkg-config --libs sqlite3 2>/dev/null)
+ifeq ($(strip $(PKGLIBS)),)
+  ifneq ($(wildcard /usr/lib/x86_64-linux-gnu/libsqlite3.so.0),)
+    SQLITELIBS := /usr/lib/x86_64-linux-gnu/libsqlite3.so.0
+  else
+    SQLITELIBS := -lsqlite3
+  endif
+else
+  SQLITELIBS := $(PKGLIBS)
+endif
+SQLITECFLAGS := $(shell pkg-config --cflags sqlite3 2>/dev/null)
+GM2FLAGS ?=
+
+SRC_DIR := src
+LIB_DIR := lib
+BLD     := build
+OBJDIR  := $(BLD)/objs
+OBJS    := $(OBJDIR)/SQLiteUtils.o
+
+INCLUDES := -I$(SRC_DIR) -I$(LIB_DIR)
+
+TESTS    := test_version test_open test_exec test_prepare
+EXAMPLES := version hello_db
+SHOWCASES := showcase_all
+
+.PHONY: all tests examples showcases clean check local-sqlite
+
+all: tests examples showcases
+
+$(OBJDIR):
+	mkdir -p $(OBJDIR)
+
+$(BLD)/tests:
+	mkdir -p $(BLD)/tests
+
+$(BLD)/examples:
+	mkdir -p $(BLD)/examples
+
+$(OBJDIR)/SQLiteUtils.o: $(LIB_DIR)/SQLiteUtils.mod $(LIB_DIR)/SQLiteUtils.def $(SRC_DIR)/SQLite.def | $(OBJDIR)
+	$(GM2) $(INCLUDES) $(GM2FLAGS) -c $< -o $@
+
+tests: $(BLD)/tests $(OBJS) $(addprefix $(BLD)/tests/,$(TESTS))
+
+$(BLD)/tests/%: tests/%.mod $(OBJS) | $(BLD)/tests
+	$(GM2) $(INCLUDES) $(GM2FLAGS) $(SQLITECFLAGS) $< $(OBJS) -o $@ $(SQLITELIBS)
+
+examples: $(BLD)/examples $(OBJS) $(addprefix $(BLD)/examples/,$(EXAMPLES))
+
+$(BLD)/examples/%: examples/%.mod $(OBJS) | $(BLD)/examples
+	$(GM2) $(INCLUDES) $(GM2FLAGS) $(SQLITECFLAGS) $< $(OBJS) -o $@ $(SQLITELIBS)
+
+$(BLD)/showcases:
+	mkdir -p $(BLD)/showcases
+
+showcases: $(BLD)/showcases $(OBJS) $(addprefix $(BLD)/showcases/,$(SHOWCASES))
+
+$(BLD)/showcases/%: showcases/%.mod $(OBJS) | $(BLD)/showcases
+	$(GM2) $(INCLUDES) $(GM2FLAGS) $(SQLITECFLAGS) $< $(OBJS) -o $@ $(SQLITELIBS)
+
+check: tests
+	./tests/run_tests.sh
+
+clean:
+	rm -rf $(BLD)
+
+# Optional: build a local amalgamation from the (git-ignored)
+# sqlite-master/ reference tree into build/. Requires tclsh (present)
+# and a C compiler; takes a few minutes. After this, link with:
+#   gm2 ... build/sqlite3.o -lpthread -ldl -lm
+local-sqlite:
+	mkdir -p $(BLD)/sqlite
+	cd sqlite-master && ./configure --disable-tcl && make sqlite3.c
+	cp sqlite-master/sqlite3.c sqlite-master/sqlite3.h $(BLD)/sqlite/
+	rm -f sqlite-master/sqlite3.c sqlite-master/sqlite3.h
+	cc -O2 -DSQLITE_THREADSAFE=1 -c $(BLD)/sqlite/sqlite3.c -o $(BLD)/sqlite/sqlite3.o

+ 108 - 0
README.md

@@ -0,0 +1,108 @@
+# m2SQLITE — SQLite bindings for GNU Modula-2
+
+Thin `DEFINITION MODULE FOR "C"` bindings of SQLite for `gm2`
+(GCC 16 / GNU Modula-2), covering the core C API: open/close, exec,
+prepare/step/finalize, bind, column access and error reporting.
+
+Reference sources: `sqlite-master/` (SQLite 3.54.0, fossil checkout).
+That folder is git-ignored (see `.gitignore`); the binding only needs
+the SQLite shared library at link time.
+
+## Status
+
+Starter set, verified with gm2 16.0.1 + SQLite 3.46.1 (system
+`libsqlite3.so.0`) on Linux:
+
+| Module | C header | Contents |
+|---|---|---|
+| `SQLite` | `<sqlite3.h>` (`sqlite-master/src/sqlite.h.in`) | version, open/close, exec, errcode/errmsg, prepare/step/finalize/reset, bind_*, column_*, changes/rowid, busy_timeout, limits |
+| `SQLiteUtils` (`lib/`) | — | `CStrToM2()` + `ErrMsg()`/`ErrStr()`/`LibVersionStr()`, `StaticDestr()`/`TransientDestr()`, `BindTextCopy()`/`BindBlobCopy()`, `ExecSimple()`/`ExecWithErr()` |
+
+Custom SQL functions (`create_function`), blobs (`blob_open`),
+backups and sessions come next.
+
+## Layout
+
+```text
+m2SQLITE/
+  src/            FOR "C" .def binding (SQLite)
+  lib/            pure-M2 helpers (SQLiteUtils)
+  tests/          gm2 smoke tests + run_tests.sh
+  examples/       version, hello_db
+  showcases/      showcase_all (exercises the whole binding)
+  build/          generated binaries (git-ignored)
+  sqlite-master/  upstream SQLite sources (git-ignored)
+```
+
+## Prerequisites
+
+- `gm2` (tested: 16.0.1 experimental)
+- SQLite runtime (`/usr/lib/x86_64-linux-gnu/libsqlite3.so.0`,
+  tested: 3.46.1). The dev package (`pkg-config sqlite3`) is used
+  when present but is not required.
+
+## Build & test
+
+```sh
+make            # tests + examples + showcases -> build/
+./tests/run_tests.sh
+./build/examples/version
+./build/examples/hello_db
+./build/showcases/showcase_all
+```
+
+`make check` is `make tests` + the runner script.
+
+Optional: build a local amalgamation from the reference tree
+(takes a few minutes, needs `tclsh` + `cc`):
+
+```sh
+make local-sqlite   # -> build/sqlite/sqlite3.o
+```
+
+## Usage
+
+```modula2
+FROM SQLite IMPORT DbHandle, SQLiteOk,
+   sqlite3_open, sqlite3_close;
+FROM SQLiteUtils IMPORT ExecSimple, ErrMsg;
+```
+
+Compile your program with the binding search path and the C library:
+
+```sh
+gm2 -Isrc -Ilib myprog.mod lib/SQLiteUtils.mod -o myprog -lsqlite3
+# without the dev package, link the runtime lib directly:
+gm2 -Isrc -Ilib myprog.mod lib/SQLiteUtils.mod -o myprog \
+  /usr/lib/x86_64-linux-gnu/libsqlite3.so.0
+```
+
+Notes:
+
+- Opaque `sqlite3 *` / `sqlite3_stmt *` travel as `ADDRESS`
+  (`DbHandle` / `StmtHandle`, `NIL` == `NULL`).
+- `ARRAY OF CHAR` parameters map onto `char *`: string literals and
+  `CHAR` arrays can be passed directly (see gm2 "Interface to C" node).
+  Pass `-1` for `nByte`/`n` on NUL-terminated text.
+- `VAR` handle parameters map onto `T **`: `sqlite3_open(":memory:", db)`,
+  `sqlite3_prepare_v2(db, sql, -1, stmt, NIL)`.
+- Optional `char **` OUT params (`errmsg`, `pzTail`) are plain
+  `ADDRESS`: pass `ADR(msg)` or `NIL`.
+- `bind_text`/`bind_blob` destructors: pass `VAL(ADDRESS, -1)` for
+  `SQLITE_TRANSIENT` (copy) or use `BindTextCopy()`/`BindBlobCopy()`.
+- `INTEGER <-> int`, `LONGINT <-> sqlite3_int64`, `REAL <-> double`
+  (see gm2 "Elementary data types" node and `libc.atof`).
+  Pass `INTEGER` constants through a variable before handing them
+  to C varargs (`printf("%d")` garbles literal constants).
+
+## Showcase
+
+`showcases/showcase_all` calls every `SQLite` procedure and every
+`SQLiteUtils` helper in seven sections (info, settings, errors,
+prepare, binds, columns, close) and prints each result.
+
+## Roadmap
+
+1. `sqlite3_create_function` + value/result family (custom functions).
+2. Incremental BLOB I/O (`blob_open/read/write`) + backup API.
+3. Extended result codes as named constants + `sqlite3_get_table` wrapper.

+ 54 - 0
docs/summary-v0.1.0.md

@@ -0,0 +1,54 @@
+# m2SQLITE summary — v0.1.0 (2026-09-24)
+
+SQLite bindings for GNU Modula-2 (`gm2` 16.0.1, SQLite 3.46.1 system
+`libsqlite3.so.0`, Linux; API reference `sqlite-master/` 3.54.0).
+All builds verified with `make`; tests 4/4 green, showcase exits 0.
+
+## v0.1.0 — core binding + showcase
+
+- `src/SQLite.def` — thin `FOR "C"` core API (~60 procedures):
+  version/sourceid/threadsafe, `open`/`open_v2`/`close`/`close_v2`,
+  `exec`, errcode/errmsg/errstr/error-offset/free, complete,
+  busy-timeout, autocommit, changes/rowid, extended codes, limit,
+  `prepare_v2`/`prepare_v3`, step/finalize/reset/clear-bindings,
+  data/column-count, sql/expanded-sql, stmt-readonly/busy, all
+  `bind_*`, all `column_*`, parameter count/name/index; result-code,
+  open-flag, column-type, prepare-flag and limit-id constants
+- `lib/SQLiteUtils.def/.mod` — `CStrToM2`, `ErrMsg`/`ErrStr`/
+  `LibVersionStr`, `StaticDestr`/`TransientDestr` (STATIC/TRANSIENT
+  sentinels), `BindTextCopy`/`BindBlobCopy`, `ExecSimple`/`ExecWithErr`
+- `tests/` — version, open (+open_v2), exec (incl. change counters
+  and error capture), prepare/bind/column round trip + `run_tests.sh`
+- `examples/` — `version`, `hello_db` (bind-insert, select-print)
+- `showcases/showcase_all` — calls every `SQLite` procedure and every
+  `SQLiteUtils` helper in 7 sections (info, settings, errors, prepare,
+  binds, columns, close); verifies values, prints each result
+- `Makefile` — gm2 build (`-c` helper to `.o`, then link) for tests,
+  examples and showcases; `pkg-config` with runtime-lib fallback;
+  optional `local-sqlite` amalgamation target
+- `.gitignore` — ignores `sqlite-master/`, `build/`, artefacts, `*.db`
+- `README.md` — status table, layout, build/test, usage, FFI notes
+
+## GM2 FFI rules learned (encoded in code)
+
+1. `DEFINITION MODULE FOR "C"` needs `EXPORT UNQUALIFIED`, otherwise
+   the linker looks for `Module_proc` instead of the C symbol.
+2. No `*)` and no `(*` inside `(* *)` comments (nested-comment open
+   and early close) — C pointer spellings need spaces (`char *` never
+   `char *)`, `destructor` never `void (*)`).
+3. `INTEGER` <-> int, `LONGINT` <-> `sqlite3_int64`, `REAL` <-> double
+   (`libc.atof`/`strtod` return `REAL`); opaque handles = `ADDRESS`.
+4. `ARRAY OF CHAR` params = `char *` (pass literals, `-1` for lengths);
+   mandatory `**` OUT params = `VAR`, optional ones (`errmsg`, `pzTail`)
+   = plain `ADDRESS` taking `ADR(var)` or `NIL`.
+5. `SQLITE_TRANSIENT`/`STATIC` are macros, pass `VAL(ADDRESS, -1/0)`
+   (wrapped by `TransientDestr`/`StaticDestr`).
+6. `INTEGER` constants garble through C varargs (`printf("%d")`) —
+   route them via variables first.
+7. No dev package needed: link `/usr/lib/x86_64-linux-gnu/libsqlite3.so.0`
+   directly when `pkg-config sqlite3` is absent.
+
+## Next
+
+Custom SQL functions (`create_function` + value/result family),
+incremental BLOB I/O + backup API, extended result-code constants.

+ 64 - 0
examples/hello_db.mod

@@ -0,0 +1,64 @@
+MODULE hello_db ;
+
+(*
+   m2SQLITE example: create a table, insert two rows with bound
+   parameters, then read them back with prepare/step/column.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM SQLite IMPORT DbHandle, StmtHandle,
+   SQLiteOk, SQLiteRow, SQLiteDone,
+   sqlite3_open, sqlite3_close,
+   sqlite3_prepare_v2, sqlite3_step, sqlite3_finalize,
+   sqlite3_bind_int, sqlite3_column_int, sqlite3_column_text;
+FROM SQLiteUtils IMPORT ExecSimple, BindTextCopy, CStrToM2, ErrMsg;
+FROM libc IMPORT printf;
+
+VAR db: DbHandle; stmt: StmtHandle;
+    rc: INTEGER; buf: ARRAY [0..63] OF CHAR;
+    name: ARRAY [0..63] OF CHAR; id: INTEGER;
+
+PROCEDURE check (rc: INTEGER; what: ARRAY OF CHAR);
+VAR e: ARRAY [0..255] OF CHAR;
+BEGIN
+   IF rc # SQLiteOk THEN
+      ErrMsg(db, e);
+      printf("FAIL %s: %s\n", what, e);
+      HALT(1)
+   END
+END check;
+
+BEGIN
+   check(sqlite3_open(":memory:", db), "open");
+   check(ExecSimple(db, "CREATE TABLE hi(id INTEGER, name TEXT);"),
+         "create");
+   check(sqlite3_prepare_v2(db, "INSERT INTO hi(id,name) VALUES(?,?);",
+                            -1, stmt, NIL), "prep insert");
+   check(sqlite3_bind_int(stmt, 1, 1), "bind1");
+   check(BindTextCopy(stmt, 2, "modula"), "bind2");
+   IF sqlite3_step(stmt) # SQLiteDone THEN HALT(1) END;
+   check(sqlite3_finalize(stmt), "fin1");
+   check(sqlite3_prepare_v2(db, "INSERT INTO hi(id,name) VALUES(?,?);",
+                            -1, stmt, NIL), "prep insert2");
+   check(sqlite3_bind_int(stmt, 1, 2), "bind3");
+   check(BindTextCopy(stmt, 2, "sqlite"), "bind4");
+   IF sqlite3_step(stmt) # SQLiteDone THEN HALT(1) END;
+   check(sqlite3_finalize(stmt), "fin2");
+
+   check(sqlite3_prepare_v2(db, "SELECT id, name FROM hi ORDER BY id;",
+                            -1, stmt, NIL), "prep select");
+   LOOP
+      rc := sqlite3_step(stmt);
+      IF rc = SQLiteDone THEN EXIT END;
+      IF rc # SQLiteRow THEN
+         printf("FAIL step rc=%d\n", rc);
+         HALT(1)
+      END;
+      id := sqlite3_column_int(stmt, 0);
+      CStrToM2(sqlite3_column_text(stmt, 1), name);
+      printf("row id=%d name=%s\n", id, name)
+   END;
+   check(sqlite3_finalize(stmt), "fin3");
+   check(sqlite3_close(db), "close");
+   printf("hello_db OK\n")
+END hello_db.

+ 15 - 0
examples/version.mod

@@ -0,0 +1,15 @@
+MODULE version ;
+
+(* m2SQLITE example: print the linked SQLite version. *)
+
+FROM SQLite IMPORT sqlite3_libversion_number;
+FROM SQLiteUtils IMPORT LibVersionStr;
+FROM libc IMPORT printf;
+
+VAR buf: ARRAY [0..63] OF CHAR;
+
+BEGIN
+   LibVersionStr(buf);
+   printf("sqlite %s (%d)\n",
+          buf, sqlite3_libversion_number())
+END version.

+ 35 - 0
lib/SQLiteUtils.def

@@ -0,0 +1,35 @@
+DEFINITION MODULE SQLiteUtils ;
+
+(*
+   m2SQLITE - small pure-Modula-2 helpers over the FOR "C" SQLite
+   binding. CStrToM2 copies NUL-terminated C strings (ADDRESS)
+   into Modula-2 CHAR arrays (NUL-terminated, truncated to fit).
+   StaticDestr/TransientDestr return the SQLITE_STATIC and
+   SQLITE_TRANSIENT destructor sentinels; BindTextCopy/BindBlobCopy
+   bind with SQLITE_TRANSIENT so SQLite copies the value.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM SQLite IMPORT DbHandle, StmtHandle;
+
+EXPORT UNQUALIFIED
+   CStrToM2, ErrMsg, ErrStr, LibVersionStr,
+   StaticDestr, TransientDestr,
+   BindTextCopy, BindBlobCopy,
+   ExecSimple, ExecWithErr;
+
+PROCEDURE CStrToM2 (src: ADDRESS; VAR dst: ARRAY OF CHAR);
+PROCEDURE ErrMsg (db: DbHandle; VAR buf: ARRAY OF CHAR);
+PROCEDURE ErrStr (code: INTEGER; VAR buf: ARRAY OF CHAR);
+PROCEDURE LibVersionStr (VAR buf: ARRAY OF CHAR);
+PROCEDURE StaticDestr () : ADDRESS;
+PROCEDURE TransientDestr () : ADDRESS;
+PROCEDURE BindTextCopy (stmt: StmtHandle; idx: INTEGER;
+                        v: ARRAY OF CHAR) : INTEGER;
+PROCEDURE BindBlobCopy (stmt: StmtHandle; idx: INTEGER;
+                        data: ADDRESS; n: INTEGER) : INTEGER;
+PROCEDURE ExecSimple (db: DbHandle; sql: ARRAY OF CHAR) : INTEGER;
+PROCEDURE ExecWithErr (db: DbHandle; sql: ARRAY OF CHAR;
+                       VAR buf: ARRAY OF CHAR) : INTEGER;
+
+END SQLiteUtils.

+ 78 - 0
lib/SQLiteUtils.mod

@@ -0,0 +1,78 @@
+IMPLEMENTATION MODULE SQLiteUtils ;
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM SQLite IMPORT DbHandle, StmtHandle,
+   sqlite3_libversion, sqlite3_errmsg, sqlite3_errstr,
+   sqlite3_exec, sqlite3_free,
+   sqlite3_bind_text, sqlite3_bind_blob;
+FROM libc IMPORT strlen, strncpy;
+
+PROCEDURE CStrToM2 (src: ADDRESS; VAR dst: ARRAY OF CHAR);
+VAR n: CARDINAL;
+BEGIN
+   IF HIGH(dst) < 0 THEN RETURN END;
+   IF src = NIL THEN dst[0] := 0C; RETURN END;
+   n := VAL(CARDINAL, strlen(src));
+   IF n > HIGH(dst) THEN n := HIGH(dst) END;
+   strncpy(ADR(dst), src, n);
+   IF n <= HIGH(dst) THEN dst[n] := 0C END
+END CStrToM2;
+
+PROCEDURE ErrMsg (db: DbHandle; VAR buf: ARRAY OF CHAR);
+BEGIN
+   CStrToM2(sqlite3_errmsg(db), buf)
+END ErrMsg;
+
+PROCEDURE ErrStr (code: INTEGER; VAR buf: ARRAY OF CHAR);
+BEGIN
+   CStrToM2(sqlite3_errstr(code), buf)
+END ErrStr;
+
+PROCEDURE LibVersionStr (VAR buf: ARRAY OF CHAR);
+BEGIN
+   CStrToM2(sqlite3_libversion(), buf)
+END LibVersionStr;
+
+PROCEDURE StaticDestr () : ADDRESS;
+BEGIN
+   RETURN VAL(ADDRESS, 0)
+END StaticDestr;
+
+PROCEDURE TransientDestr () : ADDRESS;
+BEGIN
+   RETURN VAL(ADDRESS, -1)
+END TransientDestr;
+
+PROCEDURE BindTextCopy (stmt: StmtHandle; idx: INTEGER;
+                        v: ARRAY OF CHAR) : INTEGER;
+BEGIN
+   RETURN sqlite3_bind_text(stmt, idx, v, -1, TransientDestr())
+END BindTextCopy;
+
+PROCEDURE BindBlobCopy (stmt: StmtHandle; idx: INTEGER;
+                        data: ADDRESS; n: INTEGER) : INTEGER;
+BEGIN
+   RETURN sqlite3_bind_blob(stmt, idx, data, n, TransientDestr())
+END BindBlobCopy;
+
+PROCEDURE ExecSimple (db: DbHandle; sql: ARRAY OF CHAR) : INTEGER;
+BEGIN
+   RETURN sqlite3_exec(db, sql, NIL, NIL, NIL)
+END ExecSimple;
+
+PROCEDURE ExecWithErr (db: DbHandle; sql: ARRAY OF CHAR;
+                       VAR buf: ARRAY OF CHAR) : INTEGER;
+VAR rc: INTEGER; msg: ADDRESS;
+BEGIN
+   msg := NIL;
+   rc := sqlite3_exec(db, sql, NIL, NIL, ADR(msg));
+   IF msg # NIL THEN
+      CStrToM2(msg, buf);
+      sqlite3_free(msg)
+   ELSIF HIGH(buf) >= 0 THEN
+      buf[0] := 0C
+   END;
+   RETURN rc
+END ExecWithErr;
+
+END SQLiteUtils.

+ 287 - 0
showcases/showcase_all.mod

@@ -0,0 +1,287 @@
+MODULE showcase_all ;
+
+(*
+   m2SQLITE showcase: exercises every procedure of the SQLite
+   binding plus all SQLiteUtils helpers end to end.
+
+   1. library info: version, source id, threadsafe, errstr, complete
+   2. open, autocommit, busy timeout, extended codes, limit, open_v2
+   3. exec with raw errmsg capture, ExecWithErr failure, errcode paths
+   4. prepare_v2 tail capture, prepare_v3 flags, sql text, parameter ids
+   5. all bind kinds, step, changes counters, rowid
+   6. all column kinds, metadata, busy/reset/rebind, transactions
+   7. expanded sql, zeroblob, static destructor, close_v2, close
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM SQLite IMPORT DbHandle, StmtHandle,
+   SQLiteOk, SQLiteRow, SQLiteDone,
+   SQLiteInteger, SQLiteFloat, SQLiteText, SQLiteBlob, SQLiteNull,
+   SQLiteOpenReadWrite, SQLiteOpenCreate,
+   SQLitePreparePersistent, SQLiteLimitVariableNumber,
+   sqlite3_libversion, sqlite3_libversion_number, sqlite3_sourceid,
+   sqlite3_threadsafe,
+   sqlite3_open, sqlite3_open_v2, sqlite3_close, sqlite3_close_v2,
+   sqlite3_exec,
+   sqlite3_errcode, sqlite3_extended_errcode, sqlite3_errmsg,
+   sqlite3_errstr, sqlite3_error_offset, sqlite3_free,
+   sqlite3_complete, sqlite3_busy_timeout, sqlite3_get_autocommit,
+   sqlite3_changes, sqlite3_total_changes, sqlite3_last_insert_rowid,
+   sqlite3_extended_result_codes, sqlite3_limit,
+   sqlite3_prepare_v2, sqlite3_prepare_v3,
+   sqlite3_step, sqlite3_finalize, sqlite3_reset, sqlite3_clear_bindings,
+   sqlite3_data_count, sqlite3_column_count,
+   sqlite3_sql, sqlite3_expanded_sql,
+   sqlite3_stmt_readonly, sqlite3_stmt_busy,
+   sqlite3_bind_blob, sqlite3_bind_zeroblob,
+   sqlite3_bind_double, sqlite3_bind_int, sqlite3_bind_int64,
+   sqlite3_bind_null, sqlite3_bind_text,
+   sqlite3_bind_parameter_count, sqlite3_bind_parameter_name,
+   sqlite3_bind_parameter_index,
+   sqlite3_column_blob, sqlite3_column_double, sqlite3_column_int,
+   sqlite3_column_int64, sqlite3_column_text, sqlite3_column_bytes,
+   sqlite3_column_type, sqlite3_column_name, sqlite3_column_decltype;
+FROM SQLiteUtils IMPORT CStrToM2, ErrMsg, ErrStr, LibVersionStr,
+   StaticDestr, TransientDestr,
+   BindTextCopy, BindBlobCopy, ExecSimple, ExecWithErr;
+FROM libc IMPORT printf, strncpy;
+
+VAR
+   db, db2: DbHandle;
+   stmt: StmtHandle;
+   rc, i, ncols, ndata, busy, ro: INTEGER;
+   li: LONGINT;
+   d: REAL;
+   tp: INTEGER;
+   tail, msg, p, xsql: ADDRESS;
+   buf, buf2: ARRAY [0..127] OF CHAR;
+   blobIn: ARRAY [0..3] OF CHAR;
+   staticBlob: ARRAY [0..3] OF CHAR;
+   out: ARRAY [0..15] OF CHAR;
+   showInt, showFloat, showText, showBlob, showNull: INTEGER;
+
+PROCEDURE fail (what: ARRAY OF CHAR);
+VAR e: ARRAY [0..255] OF CHAR;
+BEGIN
+   ErrMsg(db, e);
+   printf("FAIL %s: %s\n", what, e);
+   HALT(1)
+END fail;
+
+PROCEDURE check (rc: INTEGER; what: ARRAY OF CHAR);
+BEGIN
+   IF rc # SQLiteOk THEN fail(what) END
+END check;
+
+PROCEDURE section (n: INTEGER; title: ARRAY OF CHAR);
+BEGIN
+   printf("--- [%d] %s ---\n", n, title)
+END section;
+
+BEGIN
+   staticBlob[0] := 'W'; staticBlob[1] := 'X';
+   staticBlob[2] := 'Y'; staticBlob[3] := 'Z';
+   blobIn[0] := 'A'; blobIn[1] := 'B';
+   blobIn[2] := 'C'; blobIn[3] := 'D';
+
+   (* 1. library info *)
+   section(1, "library info");
+   LibVersionStr(buf);
+   printf("version %s number %d\n", buf, sqlite3_libversion_number());
+   IF sqlite3_libversion_number() <= 0 THEN HALT(1) END;
+   CStrToM2(sqlite3_libversion(), buf);
+   CStrToM2(sqlite3_sourceid(), buf2);
+   printf("sourceid %s\n", buf2);
+   printf("threadsafe %d\n", sqlite3_threadsafe());
+   ErrStr(SQLiteOk, buf);
+   printf("errstr ok %s\n", buf);
+   CStrToM2(sqlite3_errstr(SQLiteOk), buf);
+   printf("raw errstr %s\n", buf);
+   IF sqlite3_complete("SELECT 1;") = 0 THEN
+      printf("FAIL complete full\n"); HALT(1)
+   END;
+   IF sqlite3_complete("SELECT") # 0 THEN
+      printf("FAIL complete partial\n"); HALT(1)
+   END;
+   printf("complete ok\n");
+
+   (* 2. open and connection settings *)
+   section(2, "open and settings");
+   check(sqlite3_open(":memory:", db), "open");
+   printf("autocommit %d\n", sqlite3_get_autocommit(db));
+   check(sqlite3_busy_timeout(db, 500), "busy timeout");
+   check(sqlite3_extended_result_codes(db, 1), "ext codes");
+   i := sqlite3_limit(db, SQLiteLimitVariableNumber, -1);
+   printf("limit variables %d\n", i);
+   IF i <= 0 THEN printf("FAIL limit\n"); HALT(1) END;
+   check(sqlite3_open_v2(":memory:", db2,
+                         SQLiteOpenReadWrite + SQLiteOpenCreate,
+                         NIL), "open_v2");
+
+   (* 3. exec, including raw errmsg capture and failure paths *)
+   section(3, "exec and errors");
+   check(ExecSimple(db, "CREATE TABLE demo(id INTEGER PRIMARY KEY,"
+                        + " small INTEGER, big INTEGER,"
+                        + " score REAL, label TEXT,"
+                        + " payload BLOB, code TEXT);"),
+         "create");
+   check(ExecSimple(db, "INSERT INTO demo(small) VALUES(1);"), "seed");
+   printf("changes %d total %d rowid %ld\n",
+          sqlite3_changes(db), sqlite3_total_changes(db),
+          sqlite3_last_insert_rowid(db));
+   msg := NIL;
+   rc := sqlite3_exec(db, "INSERT INTO demo(small) VALUES(2);",
+                      NIL, NIL, ADR(msg));
+   IF rc # SQLiteOk THEN fail("raw exec") END;
+   IF msg # NIL THEN sqlite3_free(msg) END;
+   rc := ExecWithErr(db, "INSERT INTO bogus(x) VALUES(1);", buf);
+   printf("expected failure rc %d err %s\n", rc, buf);
+   IF rc = SQLiteOk THEN printf("FAIL bad sql ok\n"); HALT(1) END;
+   printf("errcode %d ext %d offset %d\n",
+          sqlite3_errcode(db), sqlite3_extended_errcode(db),
+          sqlite3_error_offset(db));
+   CStrToM2(sqlite3_errmsg(db), buf);
+   printf("raw errmsg %s\n", buf);
+
+   (* 4. prepare variants, tail, sql text, parameters *)
+   section(4, "prepare and parameters");
+   tail := NIL;
+   check(sqlite3_prepare_v2(db, "SELECT small FROM demo; SELECT 99;",
+                            -1, stmt, ADR(tail)), "prep tail");
+   IF tail = NIL THEN printf("FAIL tail nil\n"); HALT(1) END;
+   CStrToM2(tail, buf);
+   printf("tail %s\n", buf);
+   CStrToM2(sqlite3_sql(stmt), buf);
+   printf("sql %s\n", buf);
+   check(sqlite3_finalize(stmt), "fin tail");
+   check(sqlite3_prepare_v3(db, "SELECT :a, ?, :b;",
+                            -1, SQLitePreparePersistent,
+                            stmt, NIL), "prep v3");
+   printf("param count %d\n", sqlite3_bind_parameter_count(stmt));
+   IF sqlite3_bind_parameter_count(stmt) # 3 THEN HALT(1) END;
+   CStrToM2(sqlite3_bind_parameter_name(stmt, 1), buf);
+   printf("param1 %s\n", buf);
+   IF sqlite3_bind_parameter_name(stmt, 2) # NIL THEN
+      printf("FAIL anon param named\n"); HALT(1)
+   END;
+   i := sqlite3_bind_parameter_index(stmt, ":b");
+   printf("param :b index %d\n", i);
+   IF i # 3 THEN HALT(1) END;
+   ro := sqlite3_stmt_readonly(stmt);
+   printf("readonly select %d\n", ro);
+   IF ro = 0 THEN HALT(1) END;
+   check(sqlite3_finalize(stmt), "fin v3");
+   check(sqlite3_prepare_v2(db, "INSERT INTO demo(small) VALUES(1);",
+                            -1, stmt, NIL), "prep ins");
+   IF sqlite3_stmt_readonly(stmt) # 0 THEN
+      printf("FAIL readonly insert\n"); HALT(1)
+   END;
+   check(sqlite3_finalize(stmt), "fin ins");
+
+   (* 5. all bind kinds *)
+   section(5, "binds and step");
+   check(sqlite3_prepare_v2(db, "DELETE FROM demo;", -1, stmt, NIL),
+         "prep delete");
+   IF sqlite3_step(stmt) # SQLiteDone THEN HALT(1) END;
+   check(sqlite3_finalize(stmt), "fin delete");
+   check(sqlite3_prepare_v2(db, "INSERT INTO demo"
+                                + "(small,big,score,label,payload,code)"
+                                + " VALUES(?,?,?,?,?,?);",
+                            -1, stmt, NIL), "prep full insert");
+   check(sqlite3_bind_int(stmt, 1, 42), "bind int");
+   check(sqlite3_bind_int64(stmt, 2, VAL(LONGINT, 9000000001)),
+         "bind int64");
+   check(sqlite3_bind_double(stmt, 3, 2.5), "bind double");
+   check(BindTextCopy(stmt, 4, "hello"), "bind text copy");
+   check(BindBlobCopy(stmt, 5, ADR(blobIn), 4), "bind blob copy");
+   check(sqlite3_bind_null(stmt, 6), "bind null");
+   IF sqlite3_step(stmt) # SQLiteDone THEN fail("step insert") END;
+   printf("insert changes %d rowid %ld\n",
+          sqlite3_changes(db), sqlite3_last_insert_rowid(db));
+   xsql := sqlite3_expanded_sql(stmt);
+   CStrToM2(xsql, buf);
+   printf("expanded %s\n", buf);
+   IF xsql # NIL THEN sqlite3_free(xsql) END;
+   check(sqlite3_finalize(stmt), "fin full insert");
+
+   (* 6. all column kinds, metadata, reuse, transactions *)
+   section(6, "columns and reuse");
+   check(sqlite3_prepare_v2(db, "SELECT small,big,score,label,"
+                                + "payload,code FROM demo;",
+                            -1, stmt, NIL), "prep select");
+   rc := sqlite3_step(stmt);
+   IF rc # SQLiteRow THEN fail("step row") END;
+   ncols := sqlite3_column_count(stmt);
+   ndata := sqlite3_data_count(stmt);
+   printf("cols %d data %d\n", ncols, ndata);
+   IF (ncols # 6) OR (ndata # 6) THEN HALT(1) END;
+   busy := sqlite3_stmt_busy(stmt);
+   printf("busy %d\n", busy);
+   IF busy = 0 THEN HALT(1) END;
+   i := sqlite3_column_int(stmt, 0);
+   li := sqlite3_column_int64(stmt, 1);
+   d := sqlite3_column_double(stmt, 2);
+   CStrToM2(sqlite3_column_text(stmt, 3), buf);
+   ncols := sqlite3_column_bytes(stmt, 3);
+   p := sqlite3_column_blob(stmt, 4);
+   tp := sqlite3_column_type(stmt, 4);
+   printf("row %d %ld %f %s bytes %d blobtype %d\n",
+          i, li, d, buf, ncols, tp);
+   IF (i # 42) OR (li # VAL(LONGINT, 9000000001)) THEN HALT(1) END;
+   IF ncols # 5 THEN HALT(1) END;
+   IF (p = NIL) OR (sqlite3_column_bytes(stmt, 4) # 4) THEN HALT(1) END;
+   strncpy(ADR(out), p, 4);
+   out[4] := 0C;
+   printf("blob %s\n", out);
+   IF sqlite3_column_type(stmt, 5) # SQLiteNull THEN HALT(1) END;
+   CStrToM2(sqlite3_column_name(stmt, 0), buf);
+   CStrToM2(sqlite3_column_decltype(stmt, 0), buf2);
+   printf("col0 %s decl %s\n", buf, buf2);
+   showInt := SQLiteInteger; showFloat := SQLiteFloat;
+   showText := SQLiteText; showBlob := SQLiteBlob; showNull := SQLiteNull;
+   printf("type ids int %d float %d text %d blob %d null %d\n",
+          showInt, showFloat, showText, showBlob, showNull);
+   check(sqlite3_reset(stmt), "reset");
+   IF sqlite3_stmt_busy(stmt) # 0 THEN
+      printf("FAIL busy after reset\n"); HALT(1)
+   END;
+   check(sqlite3_clear_bindings(stmt), "clear");
+   check(sqlite3_finalize(stmt), "fin select");
+
+   (* statement reuse with raw transient and static destructors *)
+   check(sqlite3_prepare_v2(db, "INSERT INTO demo(label,payload)"
+                                + " VALUES(?,?);",
+                            -1, stmt, NIL), "prep reuse");
+   check(sqlite3_bind_text(stmt, 1, "raw", -1, TransientDestr()),
+         "bind raw transient");
+   check(sqlite3_bind_blob(stmt, 2, ADR(staticBlob), 4, StaticDestr()),
+         "bind static");
+   IF sqlite3_step(stmt) # SQLiteDone THEN HALT(1) END;
+   check(sqlite3_reset(stmt), "reset reuse");
+   check(sqlite3_clear_bindings(stmt), "clear reuse");
+   check(BindTextCopy(stmt, 1, "second"), "rebind text");
+   check(sqlite3_bind_zeroblob(stmt, 2, 16), "bind zeroblob");
+   IF sqlite3_step(stmt) # SQLiteDone THEN HALT(1) END;
+   check(sqlite3_finalize(stmt), "fin reuse");
+   check(sqlite3_prepare_v2(db, "SELECT COUNT(*),"
+                                + " SUM(LENGTH(payload)) FROM demo;",
+                            -1, stmt, NIL), "prep agg");
+   IF sqlite3_step(stmt) # SQLiteRow THEN HALT(1) END;
+   printf("rows %d blobs %d\n",
+          sqlite3_column_int(stmt, 0), sqlite3_column_int(stmt, 1));
+   IF sqlite3_column_int(stmt, 0) # 3 THEN HALT(1) END;
+   check(sqlite3_finalize(stmt), "fin agg");
+
+   check(ExecSimple(db, "BEGIN;"), "begin");
+   IF sqlite3_get_autocommit(db) # 0 THEN HALT(1) END;
+   check(ExecSimple(db, "INSERT INTO demo(small) VALUES(7);"), "txn ins");
+   check(ExecSimple(db, "COMMIT;"), "commit");
+   IF sqlite3_get_autocommit(db) = 0 THEN HALT(1) END;
+   printf("total changes %d\n", sqlite3_total_changes(db));
+
+   (* 7. close paths *)
+   section(7, "close");
+   check(sqlite3_close_v2(db2), "close_v2");
+   check(sqlite3_close(db), "close");
+   printf("PASS showcase_all\n")
+END showcase_all.

+ 371 - 0
src/SQLite.def

@@ -0,0 +1,371 @@
+DEFINITION MODULE FOR "C" SQLite ;
+
+(*
+   m2SQLITE - thin GNU Modula-2 binding of the SQLite 3 C API.
+
+   Reference headers in this workspace: sqlite-master/src/sqlite.h.in
+   (SQLite 3.54.0). The binding links against the system SQLite
+   shared library; the sqlite-master/ tree is only used as API
+   documentation and is git-ignored (see .gitignore).
+
+   Mapping notes
+   -------------
+   * DbHandle / StmtHandle are opaque (sqlite3 ptr, sqlite3_stmt ptr),
+     they travel as ADDRESS (NIL = NULL).
+   * INTEGER <-> C int (result codes, flags, indexes, byte counts).
+   * LONGINT <-> sqlite3_int64 (use for rowids, int64 columns).
+   * REAL    <-> C double (libc.atof/strtod also return REAL, so
+     REAL is the 64-bit float on this target).
+   * ARRAY OF CHAR parameters map onto (char * ): pass string
+     literals or NUL-terminated CHAR arrays directly, as documented
+     in the GM2 "Interface to C" node (same idiom as m2SDL).
+   * OUT (char * * ) / (sqlite3 * * ) parameters:
+       - mandatory single OUT (ppDb, ppStmt): VAR parameter.
+       - optional OUT (pzTail, errmsg): plain ADDRESS parameter,
+         pass ADR(var) or NIL.
+   * Destructor arguments (bind_text/bind_blob 5th parameter):
+     C macros SQLITE_STATIC ((void * )0) and SQLITE_TRANSIENT
+     ((void * )-1) are not functions, so pass VAL(ADDRESS, 0) and
+     VAL(ADDRESS, -1). SQLiteUtils.StaticDestr()/TransientDestr()
+     and BindTextCopy()/BindBlobCopy() wrap this for you.
+   * nByte / n parameters: pass -1 for NUL-terminated text so
+     SQLite measures with strlen.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+
+EXPORT UNQUALIFIED
+   DbHandle, StmtHandle,
+
+   SQLiteOk, SQLiteError, SQLiteInternal, SQLitePerm, SQLiteAbort,
+   SQLiteBusy, SQLiteLocked, SQLiteNoMem, SQLiteReadOnly,
+   SQLiteInterrupt, SQLiteIoErr, SQLiteCorrupt, SQLiteNotFound,
+   SQLiteFull, SQLiteCantOpen, SQLiteProtocol, SQLiteEmpty,
+   SQLiteSchema, SQLiteTooBig, SQLiteConstraint, SQLiteMismatch,
+   SQLiteMisuse, SQLiteNoLfs, SQLiteAuth, SQLiteFormat,
+   SQLiteRange, SQLiteNotADb, SQLiteNotice, SQLiteWarning,
+   SQLiteRow, SQLiteDone,
+
+   SQLiteOpenReadOnly, SQLiteOpenReadWrite, SQLiteOpenCreate,
+   SQLiteOpenDeleteOnClose, SQLiteOpenExclusive, SQLiteOpenAutoProxy,
+   SQLiteOpenUri, SQLiteOpenMemory, SQLiteOpenMainDb,
+   SQLiteOpenTempDb, SQLiteOpenTransientDb, SQLiteOpenMainJournal,
+   SQLiteOpenTempJournal, SQLiteOpenSubJournal, SQLiteOpenSuperJournal,
+   SQLiteOpenNoMutex, SQLiteOpenFullMutex, SQLiteOpenSharedCache,
+   SQLiteOpenPrivateCache, SQLiteOpenWal, SQLiteOpenNoFollow,
+   SQLiteOpenExResCode,
+
+   SQLiteInteger, SQLiteFloat, SQLiteText, SQLiteBlob, SQLiteNull,
+
+   SQLitePreparePersistent, SQLitePrepareNormalize, SQLitePrepareNoVtab,
+
+   SQLiteLimitLength, SQLiteLimitSqlLength, SQLiteLimitColumn,
+   SQLiteLimitExprDepth, SQLiteLimitCompoundSelect, SQLiteLimitVdbeOp,
+   SQLiteLimitFunctionArg, SQLiteLimitAttached,
+   SQLiteLimitLikePatternLength, SQLiteLimitVariableNumber,
+   SQLiteLimitTriggerDepth, SQLiteLimitWorkerThreads,
+
+   sqlite3_libversion, sqlite3_libversion_number, sqlite3_sourceid,
+   sqlite3_threadsafe,
+   sqlite3_open, sqlite3_open_v2, sqlite3_close, sqlite3_close_v2,
+   sqlite3_exec,
+   sqlite3_errcode, sqlite3_extended_errcode, sqlite3_errmsg,
+   sqlite3_errstr, sqlite3_error_offset, sqlite3_free,
+   sqlite3_complete, sqlite3_busy_timeout, sqlite3_get_autocommit,
+   sqlite3_changes, sqlite3_total_changes, sqlite3_last_insert_rowid,
+   sqlite3_extended_result_codes, sqlite3_limit,
+   sqlite3_prepare_v2, sqlite3_prepare_v3,
+   sqlite3_step, sqlite3_finalize, sqlite3_reset, sqlite3_clear_bindings,
+   sqlite3_data_count, sqlite3_column_count,
+   sqlite3_sql, sqlite3_expanded_sql,
+   sqlite3_stmt_readonly, sqlite3_stmt_busy,
+   sqlite3_bind_blob, sqlite3_bind_zeroblob,
+   sqlite3_bind_double, sqlite3_bind_int, sqlite3_bind_int64,
+   sqlite3_bind_null, sqlite3_bind_text,
+   sqlite3_bind_parameter_count, sqlite3_bind_parameter_name,
+   sqlite3_bind_parameter_index,
+   sqlite3_column_blob, sqlite3_column_double, sqlite3_column_int,
+   sqlite3_column_int64, sqlite3_column_text, sqlite3_column_bytes,
+   sqlite3_column_type, sqlite3_column_name, sqlite3_column_decltype;
+
+
+TYPE
+   DbHandle   = ADDRESS;   (* sqlite3 handle *)
+   StmtHandle = ADDRESS;   (* sqlite3_stmt handle *)
+
+CONST
+   (* --- primary result codes (sqlite.h.in) --- *)
+   SQLiteOk         = 0;
+   SQLiteError      = 1;
+   SQLiteInternal   = 2;
+   SQLitePerm       = 3;
+   SQLiteAbort      = 4;
+   SQLiteBusy       = 5;
+   SQLiteLocked     = 6;
+   SQLiteNoMem      = 7;
+   SQLiteReadOnly   = 8;
+   SQLiteInterrupt  = 9;
+   SQLiteIoErr      = 10;
+   SQLiteCorrupt    = 11;
+   SQLiteNotFound   = 12;
+   SQLiteFull       = 13;
+   SQLiteCantOpen   = 14;
+   SQLiteProtocol   = 15;
+   SQLiteEmpty      = 16;
+   SQLiteSchema     = 17;
+   SQLiteTooBig     = 18;
+   SQLiteConstraint = 19;
+   SQLiteMismatch   = 20;
+   SQLiteMisuse     = 21;
+   SQLiteNoLfs      = 22;
+   SQLiteAuth       = 23;
+   SQLiteFormat     = 24;
+   SQLiteRange      = 25;
+   SQLiteNotADb     = 26;
+   SQLiteNotice     = 27;
+   SQLiteWarning    = 28;
+   SQLiteRow        = 100;
+   SQLiteDone       = 101;
+
+   (* --- sqlite3_open_v2 flags --- *)
+   SQLiteOpenReadOnly      = 1;
+   SQLiteOpenReadWrite     = 2;
+   SQLiteOpenCreate        = 4;
+   SQLiteOpenDeleteOnClose = 8;
+   SQLiteOpenExclusive     = 16;
+   SQLiteOpenAutoProxy     = 32;
+   SQLiteOpenUri           = 64;
+   SQLiteOpenMemory        = 128;
+   SQLiteOpenMainDb        = 256;
+   SQLiteOpenTempDb        = 512;
+   SQLiteOpenTransientDb   = 1024;
+   SQLiteOpenMainJournal   = 2048;
+   SQLiteOpenTempJournal   = 4096;
+   SQLiteOpenSubJournal    = 8192;
+   SQLiteOpenSuperJournal  = 16384;
+   SQLiteOpenNoMutex       = 32768;
+   SQLiteOpenFullMutex     = 65536;
+   SQLiteOpenSharedCache   = 131072;
+   SQLiteOpenPrivateCache  = 262144;
+   SQLiteOpenWal           = 524288;
+   SQLiteOpenNoFollow      = 16777216;
+   SQLiteOpenExResCode     = 33554432;
+
+   (* --- column / value types --- *)
+   SQLiteInteger = 1;
+   SQLiteFloat   = 2;
+   SQLiteText    = 3;
+   SQLiteBlob    = 4;
+   SQLiteNull    = 5;
+
+   (* --- sqlite3_prepare_v3 flags --- *)
+   SQLitePreparePersistent = 1;
+   SQLitePrepareNormalize  = 2;
+   SQLitePrepareNoVtab     = 4;
+
+   (* --- sqlite3_limit ids (subset) --- *)
+   SQLiteLimitLength            = 0;
+   SQLiteLimitSqlLength         = 1;
+   SQLiteLimitColumn            = 2;
+   SQLiteLimitExprDepth         = 3;
+   SQLiteLimitCompoundSelect    = 4;
+   SQLiteLimitVdbeOp            = 5;
+   SQLiteLimitFunctionArg       = 6;
+   SQLiteLimitAttached          = 7;
+   SQLiteLimitLikePatternLength = 8;
+   SQLiteLimitVariableNumber    = 9;
+   SQLiteLimitTriggerDepth      = 10;
+   SQLiteLimitWorkerThreads     = 11;
+
+
+(* const char *sqlite3_libversion(void) *)
+PROCEDURE sqlite3_libversion () : ADDRESS;
+
+(* int sqlite3_libversion_number(void) *)
+PROCEDURE sqlite3_libversion_number () : INTEGER;
+
+(* const char *sqlite3_sourceid(void) *)
+PROCEDURE sqlite3_sourceid () : ADDRESS;
+
+(* int sqlite3_threadsafe(void) *)
+PROCEDURE sqlite3_threadsafe () : INTEGER;
+
+(* int sqlite3_open(const char *filename, sqlite3 * * ppDb) *)
+PROCEDURE sqlite3_open (filename: ARRAY OF CHAR; VAR db: DbHandle) : INTEGER;
+
+(* int sqlite3_open_v2(const char *filename, sqlite3 * * ppDb,
+                       int flags, const char *zVfs)
+   Pass NIL for vfs to select the default VFS. *)
+PROCEDURE sqlite3_open_v2 (filename: ARRAY OF CHAR; VAR db: DbHandle;
+                           flags: INTEGER; vfs: ADDRESS) : INTEGER;
+
+(* int sqlite3_close(sqlite3 * ) *)
+PROCEDURE sqlite3_close (db: DbHandle) : INTEGER;
+
+(* int sqlite3_close_v2(sqlite3 * ) *)
+PROCEDURE sqlite3_close_v2 (db: DbHandle) : INTEGER;
+
+(* int sqlite3_exec: db, SQL text, row callback, callback arg,
+                    errmsg OUT; free a returned message
+                    with sqlite3_free. *)
+PROCEDURE sqlite3_exec (db: DbHandle; sql: ARRAY OF CHAR;
+                        callback, arg, errmsg: ADDRESS) : INTEGER;
+
+(* int sqlite3_errcode(sqlite3 *db) *)
+PROCEDURE sqlite3_errcode (db: DbHandle) : INTEGER;
+
+(* int sqlite3_extended_errcode(sqlite3 *db) *)
+PROCEDURE sqlite3_extended_errcode (db: DbHandle) : INTEGER;
+
+(* const char *sqlite3_errmsg(sqlite3 * ) *)
+PROCEDURE sqlite3_errmsg (db: DbHandle) : ADDRESS;
+
+(* const char *sqlite3_errstr(int) *)
+PROCEDURE sqlite3_errstr (code: INTEGER) : ADDRESS;
+
+(* int sqlite3_error_offset(sqlite3 *db) *)
+PROCEDURE sqlite3_error_offset (db: DbHandle) : INTEGER;
+
+(* void sqlite3_free(void * ) *)
+PROCEDURE sqlite3_free (p: ADDRESS);
+
+(* int sqlite3_complete(const char *sql) *)
+PROCEDURE sqlite3_complete (sql: ARRAY OF CHAR) : INTEGER;
+
+(* int sqlite3_busy_timeout(sqlite3 * , int ms) *)
+PROCEDURE sqlite3_busy_timeout (db: DbHandle; ms: INTEGER) : INTEGER;
+
+(* int sqlite3_get_autocommit(sqlite3 * ) *)
+PROCEDURE sqlite3_get_autocommit (db: DbHandle) : INTEGER;
+
+(* int sqlite3_changes(sqlite3 * ) *)
+PROCEDURE sqlite3_changes (db: DbHandle) : INTEGER;
+
+(* int sqlite3_total_changes(sqlite3 * ) *)
+PROCEDURE sqlite3_total_changes (db: DbHandle) : INTEGER;
+
+(* sqlite3_int64 sqlite3_last_insert_rowid(sqlite3 * ) *)
+PROCEDURE sqlite3_last_insert_rowid (db: DbHandle) : LONGINT;
+
+(* int sqlite3_extended_result_codes(sqlite3 * , int onoff) *)
+PROCEDURE sqlite3_extended_result_codes (db: DbHandle;
+                                         onoff: INTEGER) : INTEGER;
+
+(* int sqlite3_limit(sqlite3 * , int id, int newVal) *)
+PROCEDURE sqlite3_limit (db: DbHandle; id, newVal: INTEGER) : INTEGER;
+
+(* int sqlite3_prepare_v2(sqlite3 *db, const char *zSql, int nByte,
+                          sqlite3_stmt * * ppStmt, const char * * pzTail)
+   Pass -1 for nByte on NUL-terminated SQL, NIL or ADR(tail) for tail. *)
+PROCEDURE sqlite3_prepare_v2 (db: DbHandle; sql: ARRAY OF CHAR;
+                              nBytes: INTEGER; VAR stmt: StmtHandle;
+                              tail: ADDRESS) : INTEGER;
+
+(* int sqlite3_prepare_v3(sqlite3 *db, const char *zSql, int nByte,
+                          unsigned int prepFlags,
+                          sqlite3_stmt * * ppStmt, const char * * pzTail) *)
+PROCEDURE sqlite3_prepare_v3 (db: DbHandle; sql: ARRAY OF CHAR;
+                              nBytes: INTEGER; prepFlags: INTEGER;
+                              VAR stmt: StmtHandle;
+                              tail: ADDRESS) : INTEGER;
+
+(* int sqlite3_step(sqlite3_stmt * ) *)
+PROCEDURE sqlite3_step (stmt: StmtHandle) : INTEGER;
+
+(* int sqlite3_finalize(sqlite3_stmt *pStmt) *)
+PROCEDURE sqlite3_finalize (stmt: StmtHandle) : INTEGER;
+
+(* int sqlite3_reset(sqlite3_stmt *pStmt) *)
+PROCEDURE sqlite3_reset (stmt: StmtHandle) : INTEGER;
+
+(* int sqlite3_clear_bindings(sqlite3_stmt * ) *)
+PROCEDURE sqlite3_clear_bindings (stmt: StmtHandle) : INTEGER;
+
+(* int sqlite3_data_count(sqlite3_stmt *pStmt) *)
+PROCEDURE sqlite3_data_count (stmt: StmtHandle) : INTEGER;
+
+(* int sqlite3_column_count(sqlite3_stmt *pStmt) *)
+PROCEDURE sqlite3_column_count (stmt: StmtHandle) : INTEGER;
+
+(* const char *sqlite3_sql(sqlite3_stmt *pStmt) *)
+PROCEDURE sqlite3_sql (stmt: StmtHandle) : ADDRESS;
+
+(* char *sqlite3_expanded_sql(sqlite3_stmt *pStmt) - free with sqlite3_free *)
+PROCEDURE sqlite3_expanded_sql (stmt: StmtHandle) : ADDRESS;
+
+(* int sqlite3_stmt_readonly(sqlite3_stmt *pStmt) *)
+PROCEDURE sqlite3_stmt_readonly (stmt: StmtHandle) : INTEGER;
+
+(* int sqlite3_stmt_busy(sqlite3_stmt * ) *)
+PROCEDURE sqlite3_stmt_busy (stmt: StmtHandle) : INTEGER;
+
+(* int sqlite3_bind_blob(sqlite3_stmt * , int, const void * , int n,
+                         destructor) *)
+PROCEDURE sqlite3_bind_blob (stmt: StmtHandle; idx: INTEGER;
+                             data: ADDRESS; n: INTEGER;
+                             destr: ADDRESS) : INTEGER;
+
+(* int sqlite3_bind_zeroblob(sqlite3_stmt * , int, int n) *)
+PROCEDURE sqlite3_bind_zeroblob (stmt: StmtHandle; idx, n: INTEGER) : INTEGER;
+
+(* int sqlite3_bind_double(sqlite3_stmt * , int, double) *)
+PROCEDURE sqlite3_bind_double (stmt: StmtHandle; idx: INTEGER;
+                               v: REAL) : INTEGER;
+
+(* int sqlite3_bind_int(sqlite3_stmt * , int, int) *)
+PROCEDURE sqlite3_bind_int (stmt: StmtHandle; idx: INTEGER;
+                            v: INTEGER) : INTEGER;
+
+(* int sqlite3_bind_int64(sqlite3_stmt * , int, sqlite3_int64) *)
+PROCEDURE sqlite3_bind_int64 (stmt: StmtHandle; idx: INTEGER;
+                              v: LONGINT) : INTEGER;
+
+(* int sqlite3_bind_null(sqlite3_stmt * , int) *)
+PROCEDURE sqlite3_bind_null (stmt: StmtHandle; idx: INTEGER) : INTEGER;
+
+(* int sqlite3_bind_text(sqlite3_stmt*,int,const char * ,int,destructor)
+   Pass -1 for n on NUL-terminated text. *)
+PROCEDURE sqlite3_bind_text (stmt: StmtHandle; idx: INTEGER;
+                             v: ARRAY OF CHAR; n: INTEGER;
+                             destr: ADDRESS) : INTEGER;
+
+(* int sqlite3_bind_parameter_count(sqlite3_stmt * ) *)
+PROCEDURE sqlite3_bind_parameter_count (stmt: StmtHandle) : INTEGER;
+
+(* const char *sqlite3_bind_parameter_name(sqlite3_stmt * , int) *)
+PROCEDURE sqlite3_bind_parameter_name (stmt: StmtHandle;
+                                       idx: INTEGER) : ADDRESS;
+
+(* int sqlite3_bind_parameter_index(sqlite3_stmt*, const char *zName) *)
+PROCEDURE sqlite3_bind_parameter_index (stmt: StmtHandle;
+                                        name: ARRAY OF CHAR) : INTEGER;
+
+(* const void *sqlite3_column_blob(sqlite3_stmt * , int iCol) *)
+PROCEDURE sqlite3_column_blob (stmt: StmtHandle; col: INTEGER) : ADDRESS;
+
+(* double sqlite3_column_double(sqlite3_stmt * , int iCol) *)
+PROCEDURE sqlite3_column_double (stmt: StmtHandle; col: INTEGER) : REAL;
+
+(* int sqlite3_column_int(sqlite3_stmt * , int iCol) *)
+PROCEDURE sqlite3_column_int (stmt: StmtHandle; col: INTEGER) : INTEGER;
+
+(* sqlite3_int64 sqlite3_column_int64(sqlite3_stmt * , int iCol) *)
+PROCEDURE sqlite3_column_int64 (stmt: StmtHandle; col: INTEGER) : LONGINT;
+
+(* const unsigned char *sqlite3_column_text(sqlite3_stmt * , int iCol) *)
+PROCEDURE sqlite3_column_text (stmt: StmtHandle; col: INTEGER) : ADDRESS;
+
+(* int sqlite3_column_bytes(sqlite3_stmt * , int iCol) *)
+PROCEDURE sqlite3_column_bytes (stmt: StmtHandle; col: INTEGER) : INTEGER;
+
+(* int sqlite3_column_type(sqlite3_stmt * , int iCol) *)
+PROCEDURE sqlite3_column_type (stmt: StmtHandle; col: INTEGER) : INTEGER;
+
+(* const char *sqlite3_column_name(sqlite3_stmt * , int N) *)
+PROCEDURE sqlite3_column_name (stmt: StmtHandle; col: INTEGER) : ADDRESS;
+
+(* const char *sqlite3_column_decltype(sqlite3_stmt*,int) *)
+PROCEDURE sqlite3_column_decltype (stmt: StmtHandle;
+                                   col: INTEGER) : ADDRESS;
+
+END SQLite.

+ 32 - 0
tests/run_tests.sh

@@ -0,0 +1,32 @@
+#!/bin/sh
+# m2SQLITE test runner - builds lib helper then each tests/*.mod with gm2.
+set -e
+CDIR=$(dirname "$0")
+ROOT=$(cd "$CDIR/.." && pwd)
+BIN="$ROOT/build/tests"
+OBJ="$ROOT/build/objs"
+GM2="${GM2:-gm2}"
+
+mkdir -p "$BIN" "$OBJ"
+
+if command -v pkg-config >/dev/null 2>&1 && pkg-config --libs sqlite3 >/dev/null 2>&1; then
+  SQLITELIBS="$(pkg-config --libs sqlite3)"
+elif [ -f /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 ]; then
+  SQLITELIBS="/usr/lib/x86_64-linux-gnu/libsqlite3.so.0"
+else
+  SQLITELIBS="-lsqlite3"
+fi
+
+# shellcheck disable=SC2086
+$GM2 -I"$ROOT/src" -I"$ROOT/lib" $GM2FLAGS -c "$ROOT/lib/SQLiteUtils.mod" -o "$OBJ/SQLiteUtils.o"
+pass=0; fail=0
+for t in test_version test_open test_exec test_prepare; do
+  echo "== $t =="
+  # shellcheck disable=SC2086
+  $GM2 -I"$ROOT/src" -I"$ROOT/lib" $GM2FLAGS "$ROOT/tests/$t.mod" \
+       "$OBJ/SQLiteUtils.o" \
+       -o "$BIN/$t" $SQLITELIBS
+  "$BIN/$t"
+  pass=$((pass+1))
+done
+echo "OK: $pass passed, $fail failed"

+ 64 - 0
tests/test_exec.mod

@@ -0,0 +1,64 @@
+MODULE test_exec ;
+
+(*
+   m2SQLITE smoke test: one-step exec interface (CREATE/INSERT),
+   error capture via ExecWithErr, and change counters.
+*)
+
+FROM SQLite IMPORT DbHandle, SQLiteOk, SQLiteDone, SQLiteRow,
+   sqlite3_open, sqlite3_close,
+   sqlite3_changes, sqlite3_total_changes, sqlite3_last_insert_rowid;
+FROM SQLiteUtils IMPORT ExecSimple, ExecWithErr;
+FROM libc IMPORT printf;
+
+VAR db: DbHandle; rc: INTEGER; buf: ARRAY [0..255] OF CHAR;
+    rowid: LONGINT;
+
+BEGIN
+   IF sqlite3_open(":memory:", db) # SQLiteOk THEN
+      printf("FAIL test_exec: open\n");
+      HALT(1)
+   END;
+
+   rc := ExecSimple(db, "CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT);");
+   IF rc # SQLiteOk THEN
+      printf("FAIL test_exec: create rc=%d\n", rc);
+      HALT(1)
+   END;
+
+   rc := ExecSimple(db, "INSERT INTO t(name) VALUES('alice');");
+   IF rc # SQLiteOk THEN
+      printf("FAIL test_exec: insert rc=%d\n", rc);
+      HALT(1)
+   END;
+   IF sqlite3_changes(db) # 1 THEN
+      printf("FAIL test_exec: changes=%d want 1\n", sqlite3_changes(db));
+      HALT(1)
+   END;
+   rowid := sqlite3_last_insert_rowid(db);
+   printf("last_rowid=%ld\n", rowid);
+   IF rowid # 1 THEN
+      printf("FAIL test_exec: rowid=%ld want 1\n", rowid);
+      HALT(1)
+   END;
+
+   (* failing statement must report its message *)
+   rc := ExecWithErr(db, "INSERT INTO bogus(x) VALUES(1);", buf);
+   printf("bad exec rc=%d err=%s\n", rc, buf);
+   IF rc = SQLiteOk THEN
+      printf("FAIL test_exec: bad SQL succeeded\n");
+      HALT(1)
+   END;
+   IF buf[0] = 0C THEN
+      printf("FAIL test_exec: empty error message\n");
+      HALT(1)
+   END;
+
+   printf("total_changes=%d\n", sqlite3_total_changes(db));
+   rc := sqlite3_close(db);
+   IF rc # SQLiteOk THEN
+      printf("FAIL test_exec: close rc=%d\n", rc);
+      HALT(1)
+   END;
+   printf("PASS test_exec\n")
+END test_exec.

+ 53 - 0
tests/test_open.mod

@@ -0,0 +1,53 @@
+MODULE test_open ;
+
+(*
+   m2SQLITE smoke test: open/close an in-memory database, including
+   the open_v2 path with a NIL (default) VFS.
+*)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM SQLite IMPORT DbHandle,
+   SQLiteOk, SQLiteOpenReadWrite, SQLiteOpenCreate,
+   sqlite3_open, sqlite3_open_v2, sqlite3_close,
+   sqlite3_errcode;
+FROM libc IMPORT printf;
+
+VAR db: DbHandle; rc: INTEGER;
+
+BEGIN
+   db := NIL;
+   rc := sqlite3_open(":memory:", db);
+   printf("open rc=%d\n", rc);
+   IF rc # SQLiteOk THEN
+      printf("FAIL test_open: sqlite3_open rc=%d\n", rc);
+      HALT(1)
+   END;
+   IF db = NIL THEN
+      printf("FAIL test_open: NIL handle\n");
+      HALT(1)
+   END;
+   rc := sqlite3_close(db);
+   IF rc # SQLiteOk THEN
+      printf("FAIL test_open: close rc=%d\n", rc);
+      HALT(1)
+   END;
+
+   db := NIL;
+   rc := sqlite3_open_v2(":memory:", db,
+                         SQLiteOpenReadWrite + SQLiteOpenCreate, NIL);
+   printf("open_v2 rc=%d\n", rc);
+   IF rc # SQLiteOk THEN
+      printf("FAIL test_open: open_v2 rc=%d\n", rc);
+      HALT(1)
+   END;
+   IF sqlite3_errcode(db) # SQLiteOk THEN
+      printf("FAIL test_open: errcode after open\n");
+      HALT(1)
+   END;
+   rc := sqlite3_close(db);
+   IF rc # SQLiteOk THEN
+      printf("FAIL test_open: close2 rc=%d\n", rc);
+      HALT(1)
+   END;
+   printf("PASS test_open\n")
+END test_open.

+ 124 - 0
tests/test_prepare.mod

@@ -0,0 +1,124 @@
+MODULE test_prepare ;
+
+(*
+   m2SQLITE smoke test: prepare/bind/step/column/finalize round trip,
+   covering int, int64, double and text plus the SQLiteUtils
+   TRANSIENT bind helpers.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM SQLite IMPORT DbHandle, StmtHandle,
+   SQLiteOk, SQLiteRow, SQLiteDone, SQLiteText,
+   sqlite3_open, sqlite3_close,
+   sqlite3_prepare_v2, sqlite3_step, sqlite3_finalize,
+   sqlite3_bind_int, sqlite3_bind_int64, sqlite3_bind_double,
+   sqlite3_bind_null,
+   sqlite3_bind_parameter_count, sqlite3_bind_parameter_index,
+   sqlite3_column_int, sqlite3_column_int64, sqlite3_column_double,
+   sqlite3_column_text, sqlite3_column_type, sqlite3_column_count;
+FROM SQLiteUtils IMPORT ExecSimple, BindTextCopy, CStrToM2, ErrMsg;
+FROM libc IMPORT printf;
+
+VAR db: DbHandle; stmt: StmtHandle;
+    rc, n: INTEGER; buf: ARRAY [0..63] OF CHAR;
+    i: INTEGER; li: LONGINT; d: REAL;
+    tp: INTEGER;
+
+BEGIN
+   IF sqlite3_open(":memory:", db) # SQLiteOk THEN
+      printf("FAIL test_prepare: open\n");
+      HALT(1)
+   END;
+   IF ExecSimple(db, "CREATE TABLE t(a INTEGER, b INTEGER, c REAL, d TEXT);")
+      # SQLiteOk THEN
+      ErrMsg(db, buf);
+      printf("FAIL test_prepare: create %s\n", buf);
+      HALT(1)
+   END;
+
+   rc := sqlite3_prepare_v2(db, "INSERT INTO t(a,b,c,d) VALUES(?,?,?,?);",
+                            -1, stmt, NIL);
+   IF rc # SQLiteOk THEN
+      ErrMsg(db, buf);
+      printf("FAIL test_prepare: prep insert %s\n", buf);
+      HALT(1)
+   END;
+   IF sqlite3_bind_parameter_count(stmt) # 4 THEN
+      printf("FAIL test_prepare: param count\n");
+      HALT(1)
+   END;
+   IF sqlite3_bind_int(stmt, 1, 42) # SQLiteOk THEN HALT(1) END;
+   IF sqlite3_bind_int64(stmt, 2, VAL(LONGINT, 9000000001)) # SQLiteOk THEN
+      printf("FAIL test_prepare: bind int64\n");
+      HALT(1)
+   END;
+   IF sqlite3_bind_double(stmt, 3, 2.5) # SQLiteOk THEN HALT(1) END;
+   IF BindTextCopy(stmt, 4, "hello") # SQLiteOk THEN HALT(1) END;
+   rc := sqlite3_step(stmt);
+   IF rc # SQLiteDone THEN
+      printf("FAIL test_prepare: step insert rc=%d\n", rc);
+      HALT(1)
+   END;
+   rc := sqlite3_finalize(stmt);
+   IF rc # SQLiteOk THEN HALT(1) END;
+
+   (* named parameter lookup *)
+   rc := sqlite3_prepare_v2(db, "SELECT * FROM t WHERE a = :v;", -1,
+                            stmt, NIL);
+   IF rc # SQLiteOk THEN HALT(1) END;
+   n := sqlite3_bind_parameter_index(stmt, ":v");
+   printf("param :v index=%d\n", n);
+   IF n # 1 THEN
+      printf("FAIL test_prepare: param index=%d\n", n);
+      HALT(1)
+   END;
+   IF sqlite3_bind_int(stmt, n, 42) # SQLiteOk THEN HALT(1) END;
+   rc := sqlite3_step(stmt);
+   IF rc # SQLiteRow THEN
+      printf("FAIL test_prepare: step select rc=%d\n", rc);
+      HALT(1)
+   END;
+   IF sqlite3_column_count(stmt) # 4 THEN
+      printf("FAIL test_prepare: column count\n");
+      HALT(1)
+   END;
+   i := sqlite3_column_int(stmt, 0);
+   li := sqlite3_column_int64(stmt, 1);
+   d := sqlite3_column_double(stmt, 2);
+   tp := sqlite3_column_type(stmt, 3);
+   CStrToM2(sqlite3_column_text(stmt, 3), buf);
+   printf("row a=%d b=%ld c=%f d=%s t=%d\n", i, li, d, buf, tp);
+   IF (i # 42) OR (li # VAL(LONGINT, 9000000001)) THEN
+      printf("FAIL test_prepare: int values\n");
+      HALT(1)
+   END;
+   IF (d < 2.49) OR (d > 2.51) THEN
+      printf("FAIL test_prepare: double value\n");
+      HALT(1)
+   END;
+   IF tp # SQLiteText THEN
+      printf("FAIL test_prepare: text type=%d\n", tp);
+      HALT(1)
+   END;
+   IF (buf[0] # 'h') THEN
+      printf("FAIL test_prepare: text value\n");
+      HALT(1)
+   END;
+   rc := sqlite3_step(stmt);
+   IF rc # SQLiteDone THEN
+      printf("FAIL test_prepare: drain rc=%d\n", rc);
+      HALT(1)
+   END;
+   IF sqlite3_finalize(stmt) # SQLiteOk THEN HALT(1) END;
+
+   (* NULL bind *)
+   IF sqlite3_prepare_v2(db, "SELECT ?;", -1, stmt, NIL) # SQLiteOk THEN
+      HALT(1)
+   END;
+   IF sqlite3_bind_null(stmt, 1) # SQLiteOk THEN HALT(1) END;
+   IF sqlite3_step(stmt) # SQLiteRow THEN HALT(1) END;
+   IF sqlite3_finalize(stmt) # SQLiteOk THEN HALT(1) END;
+
+   IF sqlite3_close(db) # SQLiteOk THEN HALT(1) END;
+   printf("PASS test_prepare\n")
+END test_prepare.

+ 26 - 0
tests/test_version.mod

@@ -0,0 +1,26 @@
+MODULE test_version ;
+
+(*
+   m2SQLITE smoke test: library version number and string are sane.
+*)
+
+FROM SQLite IMPORT sqlite3_libversion_number, sqlite3_libversion;
+FROM SQLiteUtils IMPORT LibVersionStr;
+FROM libc IMPORT printf;
+
+VAR vn: INTEGER; buf: ARRAY [0..63] OF CHAR;
+
+BEGIN
+   vn := sqlite3_libversion_number();
+   LibVersionStr(buf);
+   printf("libversion_number=%d version=%s\n", vn, buf);
+   IF vn <= 0 THEN
+      printf("FAIL test_version: bad version number\n");
+      HALT(1)
+   END;
+   IF buf[0] = 0C THEN
+      printf("FAIL test_version: empty version string\n");
+      HALT(1)
+   END;
+   printf("PASS test_version\n")
+END test_version.