Преглед изворни кода

v0.4.0: extended result codes and get_table family

Eric Streit пре 2 недеља
родитељ
комит
563c4b6156
9 измењених фајлова са 335 додато и 19 уклоњено
  1. 1 1
      Makefile
  2. 3 4
      README.md
  3. 29 7
      docs/summary-v0.4.0.md
  4. 7 0
      lib/SQLiteUtils.def
  5. 42 1
      lib/SQLiteUtils.mod
  6. 26 5
      showcases/showcase_all.mod
  7. 117 0
      src/SQLite.def
  8. 1 1
      tests/run_tests.sh
  9. 109 0
      tests/test_codes.mod

+ 1 - 1
Makefile

@@ -23,7 +23,7 @@ OBJS    := $(OBJDIR)/SQLiteUtils.o
 
 INCLUDES := -I$(SRC_DIR) -I$(LIB_DIR)
 
-TESTS    := test_version test_open test_exec test_prepare test_func test_blob
+TESTS    := test_version test_open test_exec test_prepare test_func test_blob test_codes
 EXAMPLES := version hello_db
 SHOWCASES := showcase_all
 

+ 3 - 4
README.md

@@ -15,8 +15,8 @@ Starter set, verified with gm2 16.0.1 + SQLite 3.46.1 (system
 
 | 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, custom functions (create_function, value_*, result_*, aggregate/userdata/auxdata context), incremental BLOB I/O, online backup |
-| `SQLiteUtils` (`lib/`) | — | `CStrToM2()` + `ErrMsg()`/`ErrStr()`/`LibVersionStr()`, `StaticDestr()`/`TransientDestr()`, `BindTextCopy()`/`BindBlobCopy()`, `ValueText()`/`ResultTextCopy()`, `BackupToFile()`, `ExecSimple()`/`ExecWithErr()` |
+| `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, custom functions (create_function, value_*, result_*, aggregate/userdata/auxdata context), incremental BLOB I/O, online backup, all 82 extended result codes, get_table/free_table |
+| `SQLiteUtils` (`lib/`) | — | `CStrToM2()` + `ErrMsg()`/`ErrStr()`/`LibVersionStr()`, `StaticDestr()`/`TransientDestr()`, `BindTextCopy()`/`BindBlobCopy()`, `ValueText()`/`ResultTextCopy()`, `BackupToFile()`, `GetTable()`/`TableCell()`/`FreeTable()`, `ExecSimple()`/`ExecWithErr()` |
 
 Window functions (`create_window_function`), collations and sessions
 come next.
@@ -109,5 +109,4 @@ prepare, binds, columns, close) and prints each result.
 ## Roadmap
 
 1. Window functions (`create_window_function`) + collations.
-2. Extended result codes as named constants + `sqlite3_get_table` wrapper.
-3. Sessions/changesets (`ext/session`).
+2. Sessions/changesets (`ext/session`).

+ 29 - 7
docs/summary-v0.3.0.md → docs/summary-v0.4.0.md

@@ -1,8 +1,8 @@
-# m2SQLITE summary — v0.3.0 (2026-09-24)
+# m2SQLITE summary — v0.4.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 6/6 green, showcase exits 0.
+All builds verified with `make`; tests 7/7 green, showcase exits 0.
 
 ## v0.1.0 — core binding + showcase
 
@@ -56,13 +56,32 @@ All builds verified with `make`; tests 6/6 green, showcase exits 0.
   finish, close; step/finish errors propagate)
 - `tests/test_blob` — 256-byte write/read round trip with offset,
   `reopen` across rows, read-only write refusal (code 8), close
-  relaying the pending handle error (documented behavior, asserted),
-  direct backup (`step(-1)` to Done, remaining/pagecount) verified by
-  row count, `BackupToFile` to `/tmp` verified and unlinked
+  relaying the pending handle error, direct backup (`step(-1)` to
+  Done, remaining/pagecount) verified by row count, `BackupToFile`
+  to `/tmp` verified and unlinked
 - `showcases/showcase_all` — section [10]: 64-byte blob write/read,
   file backup, reopen-and-count, temp-file removal (close moves
   to [11])
 
+## v0.4.0 — extended codes and result tables
+
+- `src/SQLite.def` — all 82 extended result codes as named constants
+  (`SQLiteConstraintUnique` = 2067 etc., values generated from
+  `sqlite.h.in` so `base | (n<<8)` arithmetic cannot drift), plus
+  `get_table`/`free_table` (+2 procedures). The triple-pointer OUT
+  param needs no special support: `VAR tbl: ADDRESS` passes the
+  pointer the C side writes through.
+- `lib/SQLiteUtils` — `GetTable` (one call returning table, dims and
+  error text; frees on failure), `TableCell` (header row via row -1,
+  NULL and out-of-range read as empty), `FreeTable`
+- `tests/test_codes` — triggers UNIQUE (2067), NOTNULL (1299),
+  PRIMARYKEY (1555) and FOREIGNKEY (787) and checks each against
+  both the return code and `extended_errcode`; raw `get_table` call
+  (dims, header, NULL cell, int cell) plus the helper path incl. the
+  failing-statement branch
+- `showcases/showcase_all` — section [11]: unique-violation code
+  printout, `fx` table dump via `TableCell` (close moves to [12])
+
 ## GM2 FFI rules learned (encoded in code)
 
 1. `DEFINITION MODULE FOR "C"` needs `EXPORT UNQUALIFIED`, otherwise
@@ -92,8 +111,11 @@ All builds verified with `make`; tests 6/6 green, showcase exits 0.
     it — expect codes, not just `Ok`.
 11. `libc.unlink` takes `ADDRESS` (`ADR` of a NUL buffer) and its
     `[INTEGER]` result may be assigned or dropped as a statement.
+12. Generate large constant families from the C header with a script
+    instead of hand arithmetic; each constant keeps its C name in a
+    trailing comment for audit.
 
 ## Next
 
-Window functions (`create_window_function`) + collations, extended
-result-code constants + `get_table` wrapper, sessions/changesets.
+Window functions (`create_window_function`) + collations,
+sessions/changesets (`ext/session`).

+ 7 - 0
lib/SQLiteUtils.def

@@ -19,6 +19,7 @@ EXPORT UNQUALIFIED
    BindTextCopy, BindBlobCopy,
    ValueText, ResultTextCopy,
    BackupToFile,
+   GetTable, TableCell, FreeTable,
    ExecSimple, ExecWithErr;
 
 PROCEDURE CStrToM2 (src: ADDRESS; VAR dst: ARRAY OF CHAR);
@@ -35,6 +36,12 @@ PROCEDURE ValueText (v: ValueHandle; VAR buf: ARRAY OF CHAR);
 PROCEDURE ResultTextCopy (ctx: ContextHandle; str: ARRAY OF CHAR);
 PROCEDURE BackupToFile (srcDb: DbHandle;
                         filename: ARRAY OF CHAR) : INTEGER;
+PROCEDURE GetTable (db: DbHandle; sql: ARRAY OF CHAR;
+                    VAR tbl: ADDRESS; VAR rows, cols: INTEGER;
+                    VAR errbuf: ARRAY OF CHAR) : INTEGER;
+PROCEDURE TableCell (tbl: ADDRESS; cols, row, col: INTEGER;
+                     VAR buf: ARRAY OF CHAR);
+PROCEDURE FreeTable (tbl: ADDRESS);
 PROCEDURE ExecSimple (db: DbHandle; sql: ARRAY OF CHAR) : INTEGER;
 PROCEDURE ExecWithErr (db: DbHandle; sql: ARRAY OF CHAR;
                        VAR buf: ARRAY OF CHAR) : INTEGER;

+ 42 - 1
lib/SQLiteUtils.mod

@@ -9,7 +9,8 @@ FROM SQLite IMPORT DbHandle, StmtHandle, ContextHandle, ValueHandle,
    sqlite3_exec, sqlite3_free,
    sqlite3_bind_text, sqlite3_bind_blob,
    sqlite3_value_text, sqlite3_result_text,
-   sqlite3_backup_init, sqlite3_backup_step, sqlite3_backup_finish;
+   sqlite3_backup_init, sqlite3_backup_step, sqlite3_backup_finish,
+   sqlite3_get_table, sqlite3_free_table;
 FROM libc IMPORT strlen, strncpy;
 
 PROCEDURE CStrToM2 (src: ADDRESS; VAR dst: ARRAY OF CHAR);
@@ -97,6 +98,46 @@ BEGIN
    RETURN rc
 END BackupToFile;
 
+PROCEDURE GetTable (db: DbHandle; sql: ARRAY OF CHAR;
+                    VAR tbl: ADDRESS; VAR rows, cols: INTEGER;
+                    VAR errbuf: ARRAY OF CHAR) : INTEGER;
+VAR rc: INTEGER; msg: ADDRESS;
+BEGIN
+   tbl := NIL; rows := 0; cols := 0;
+   msg := NIL;
+   rc := sqlite3_get_table(db, sql, tbl, rows, cols, ADR(msg));
+   IF msg # NIL THEN
+      CStrToM2(msg, errbuf);
+      sqlite3_free(msg)
+   ELSIF HIGH(errbuf) >= 0 THEN
+      errbuf[0] := 0C
+   END;
+   IF rc # SQLiteOk THEN
+      IF tbl # NIL THEN sqlite3_free_table(tbl); tbl := NIL END;
+      rows := 0; cols := 0
+   END;
+   RETURN rc
+END GetTable;
+
+PROCEDURE TableCell (tbl: ADDRESS; cols, row, col: INTEGER;
+                     VAR buf: ARRAY OF CHAR);
+TYPE StrVec = POINTER TO ARRAY [0..65535] OF ADDRESS;
+VAR vec: StrVec;
+BEGIN
+   IF (tbl = NIL) OR (cols <= 0) OR (col < 0) OR (col >= cols)
+      OR (row < -1) THEN
+      CStrToM2(NIL, buf);
+      RETURN
+   END;
+   vec := VAL(StrVec, tbl);
+   CStrToM2(vec^[(row + 1) * cols + col], buf)
+END TableCell;
+
+PROCEDURE FreeTable (tbl: ADDRESS);
+BEGIN
+   IF tbl # NIL THEN sqlite3_free_table(tbl) END
+END FreeTable;
+
 PROCEDURE ExecSimple (db: DbHandle; sql: ARRAY OF CHAR) : INTEGER;
 BEGIN
    RETURN sqlite3_exec(db, sql, NIL, NIL, NIL)

+ 26 - 5
showcases/showcase_all.mod

@@ -13,7 +13,8 @@ MODULE showcase_all ;
    7. expanded sql, zeroblob, static destructor, close_v2
    8. custom functions: scalar tax plus aggregate total
    9. blob read/write plus file backup
-   10. close
+   10. extended codes plus result tables
+   11. close
 *)
 
 FROM SYSTEM IMPORT ADDRESS, ADR;
@@ -23,7 +24,7 @@ FROM SQLite IMPORT DbHandle, StmtHandle, ContextHandle, ValueHandle,
    SQLiteInteger, SQLiteFloat, SQLiteText, SQLiteBlob, SQLiteNull,
    SQLiteOpenReadWrite, SQLiteOpenCreate,
    SQLitePreparePersistent, SQLiteLimitVariableNumber,
-   SQLiteUtf8, SQLiteDeterministic,
+   SQLiteUtf8, SQLiteDeterministic, SQLiteConstraintUnique,
    sqlite3_libversion, sqlite3_libversion_number, sqlite3_sourceid,
    sqlite3_threadsafe,
    sqlite3_open, sqlite3_open_v2, sqlite3_close, sqlite3_close_v2,
@@ -54,7 +55,8 @@ FROM SQLite IMPORT DbHandle, StmtHandle, ContextHandle, ValueHandle,
    sqlite3_blob_bytes, sqlite3_blob_read, sqlite3_blob_write;
 FROM SQLiteUtils IMPORT CStrToM2, ErrMsg, ErrStr, LibVersionStr,
    StaticDestr, TransientDestr,
-   BindTextCopy, BindBlobCopy, ExecSimple, ExecWithErr, BackupToFile;
+   BindTextCopy, BindBlobCopy, ExecSimple, ExecWithErr, BackupToFile,
+   GetTable, TableCell, FreeTable;
 FROM libc IMPORT printf, strncpy, unlink;
 
 TYPE
@@ -65,6 +67,7 @@ VAR
    db, db2, db3: DbHandle;
    stmt: StmtHandle;
    bh: BlobHandle;
+   tbl: ADDRESS; trows, tcols: INTEGER;
    rc, i, ncols, ndata, busy, ro: INTEGER;
    li: LONGINT;
    d: REAL;
@@ -394,8 +397,26 @@ BEGIN
    check(sqlite3_close(db3), "close backup");
    RemoveFile("/tmp/m2sqlite_showcase_backup.db");
 
-   (* 11. close paths *)
-   section(11, "close");
+   (* 11. codes and tables *)
+   section(11, "codes and tables");
+   check(ExecSimple(db, "CREATE TABLE uniq(v TEXT UNIQUE);"),
+         "uniq create");
+   check(ExecSimple(db, "INSERT INTO uniq VALUES('a');"), "uniq seed");
+   rc := ExecSimple(db, "INSERT INTO uniq VALUES('a');");
+   printf("unique rc %d\n", rc);
+   IF rc # SQLiteConstraintUnique THEN fail("uniq code") END;
+   check(GetTable(db, "SELECT price FROM fx ORDER BY price;",
+                  tbl, trows, tcols, buf), "fx table");
+   TableCell(tbl, tcols, -1, 0, buf);
+   printf("table %dx%d head %s\n", trows, tcols, buf);
+   FOR i := 0 TO trows - 1 DO
+      TableCell(tbl, tcols, i, 0, buf2);
+      printf("cell %s\n", buf2)
+   END;
+   FreeTable(tbl);
+
+   (* 12. close paths *)
+   section(12, "close");
    check(sqlite3_close_v2(db2), "close_v2");
    check(sqlite3_close(db), "close");
    printf("PASS showcase_all\n")

+ 117 - 0
src/SQLite.def

@@ -92,6 +92,29 @@ EXPORT UNQUALIFIED
    SQLiteUtf8, SQLiteUtf16LE, SQLiteUtf16BE, SQLiteUtf16, SQLiteUtfAny,
    SQLiteDeterministic, SQLiteDirectOnly, SQLiteSubtype,
    SQLiteInnocuous, SQLiteResultSubtype, SQLiteSelfOrder1,
+   SQLiteErrorMissingCollSeq, SQLiteErrorRetry, SQLiteErrorSnapshot, SQLiteErrorReserveSize,
+   SQLiteErrorKey, SQLiteErrorUnable, SQLiteIoErrRead, SQLiteIoErrShortRead,
+   SQLiteIoErrWrite, SQLiteIoErrFsync, SQLiteIoErrDirFsync, SQLiteIoErrTruncate,
+   SQLiteIoErrFstat, SQLiteIoErrUnlock, SQLiteIoErrRdlock, SQLiteIoErrDelete,
+   SQLiteIoErrBlocked, SQLiteIoErrNomem, SQLiteIoErrAccess, SQLiteIoErrCheckReservedLock,
+   SQLiteIoErrLock, SQLiteIoErrClose, SQLiteIoErrDirClose, SQLiteIoErrShmOpen,
+   SQLiteIoErrShmSize, SQLiteIoErrShmLock, SQLiteIoErrShmMap, SQLiteIoErrSeek,
+   SQLiteIoErrDeleteNoEnt, SQLiteIoErrMmap, SQLiteIoErrGetTempPath, SQLiteIoErrConvPath,
+   SQLiteIoErrVnode, SQLiteIoErrAuth, SQLiteIoErrBeginAtomic, SQLiteIoErrCommitAtomic,
+   SQLiteIoErrRollbackAtomic, SQLiteIoErrData, SQLiteIoErrCorruptFs, SQLiteIoErrInPage,
+   SQLiteIoErrBadKey, SQLiteIoErrCodec, SQLiteLockedSharedCache, SQLiteLockedVtab,
+   SQLiteBusyRecovery, SQLiteBusySnapshot, SQLiteBusyTimeout, SQLiteCantOpenNoTempDir,
+   SQLiteCantOpenIsDir, SQLiteCantOpenFullPath, SQLiteCantOpenConvPath, SQLiteCantOpenDirtyWal,
+   SQLiteCantOpenSymlink, SQLiteCorruptVtab, SQLiteCorruptSequence, SQLiteCorruptIndex,
+   SQLiteReadOnlyRecovery, SQLiteReadOnlyCantLock, SQLiteReadOnlyRollback, SQLiteReadOnlyDbMoved,
+   SQLiteReadOnlyCantInit, SQLiteReadOnlyDirectory, SQLiteAbortRollback, SQLiteConstraintCheck,
+   SQLiteConstraintCommitHook, SQLiteConstraintForeignKey, SQLiteConstraintFunction, SQLiteConstraintNotNull,
+   SQLiteConstraintPrimaryKey, SQLiteConstraintTrigger, SQLiteConstraintUnique, SQLiteConstraintVtab,
+   SQLiteConstraintRowid, SQLiteConstraintPinned, SQLiteConstraintDatatype, SQLiteNoticeRecoverWal,
+   SQLiteNoticeRecoverRollback, SQLiteNoticeRbu, SQLiteWarningAutoIndex, SQLiteAuthUser,
+   SQLiteOkLoadPermanently, SQLiteOkSymlink,
+
+   sqlite3_get_table, sqlite3_free_table,
 
    sqlite3_create_function, sqlite3_create_function_v2,
    sqlite3_value_blob, sqlite3_value_double, sqlite3_value_int,
@@ -230,6 +253,89 @@ CONST
    SQLiteInnocuous     = 2097152;
    SQLiteResultSubtype = 16777216;
    SQLiteSelfOrder1    = 33554432;
+   (* --- extended result codes, values from sqlite.h.in --- *)
+   SQLiteErrorMissingCollSeq        = 257;   (* SQLITE_ERROR_MISSING_COLLSEQ *)
+   SQLiteErrorRetry                 = 513;   (* SQLITE_ERROR_RETRY *)
+   SQLiteErrorSnapshot              = 769;   (* SQLITE_ERROR_SNAPSHOT *)
+   SQLiteErrorReserveSize           = 1025;   (* SQLITE_ERROR_RESERVESIZE *)
+   SQLiteErrorKey                   = 1281;   (* SQLITE_ERROR_KEY *)
+   SQLiteErrorUnable                = 1537;   (* SQLITE_ERROR_UNABLE *)
+   SQLiteIoErrRead                  = 266;   (* SQLITE_IOERR_READ *)
+   SQLiteIoErrShortRead             = 522;   (* SQLITE_IOERR_SHORT_READ *)
+   SQLiteIoErrWrite                 = 778;   (* SQLITE_IOERR_WRITE *)
+   SQLiteIoErrFsync                 = 1034;   (* SQLITE_IOERR_FSYNC *)
+   SQLiteIoErrDirFsync              = 1290;   (* SQLITE_IOERR_DIR_FSYNC *)
+   SQLiteIoErrTruncate              = 1546;   (* SQLITE_IOERR_TRUNCATE *)
+   SQLiteIoErrFstat                 = 1802;   (* SQLITE_IOERR_FSTAT *)
+   SQLiteIoErrUnlock                = 2058;   (* SQLITE_IOERR_UNLOCK *)
+   SQLiteIoErrRdlock                = 2314;   (* SQLITE_IOERR_RDLOCK *)
+   SQLiteIoErrDelete                = 2570;   (* SQLITE_IOERR_DELETE *)
+   SQLiteIoErrBlocked               = 2826;   (* SQLITE_IOERR_BLOCKED *)
+   SQLiteIoErrNomem                 = 3082;   (* SQLITE_IOERR_NOMEM *)
+   SQLiteIoErrAccess                = 3338;   (* SQLITE_IOERR_ACCESS *)
+   SQLiteIoErrCheckReservedLock     = 3594;   (* SQLITE_IOERR_CHECKRESERVEDLOCK *)
+   SQLiteIoErrLock                  = 3850;   (* SQLITE_IOERR_LOCK *)
+   SQLiteIoErrClose                 = 4106;   (* SQLITE_IOERR_CLOSE *)
+   SQLiteIoErrDirClose              = 4362;   (* SQLITE_IOERR_DIR_CLOSE *)
+   SQLiteIoErrShmOpen               = 4618;   (* SQLITE_IOERR_SHMOPEN *)
+   SQLiteIoErrShmSize               = 4874;   (* SQLITE_IOERR_SHMSIZE *)
+   SQLiteIoErrShmLock               = 5130;   (* SQLITE_IOERR_SHMLOCK *)
+   SQLiteIoErrShmMap                = 5386;   (* SQLITE_IOERR_SHMMAP *)
+   SQLiteIoErrSeek                  = 5642;   (* SQLITE_IOERR_SEEK *)
+   SQLiteIoErrDeleteNoEnt           = 5898;   (* SQLITE_IOERR_DELETE_NOENT *)
+   SQLiteIoErrMmap                  = 6154;   (* SQLITE_IOERR_MMAP *)
+   SQLiteIoErrGetTempPath           = 6410;   (* SQLITE_IOERR_GETTEMPPATH *)
+   SQLiteIoErrConvPath              = 6666;   (* SQLITE_IOERR_CONVPATH *)
+   SQLiteIoErrVnode                 = 6922;   (* SQLITE_IOERR_VNODE *)
+   SQLiteIoErrAuth                  = 7178;   (* SQLITE_IOERR_AUTH *)
+   SQLiteIoErrBeginAtomic           = 7434;   (* SQLITE_IOERR_BEGIN_ATOMIC *)
+   SQLiteIoErrCommitAtomic          = 7690;   (* SQLITE_IOERR_COMMIT_ATOMIC *)
+   SQLiteIoErrRollbackAtomic        = 7946;   (* SQLITE_IOERR_ROLLBACK_ATOMIC *)
+   SQLiteIoErrData                  = 8202;   (* SQLITE_IOERR_DATA *)
+   SQLiteIoErrCorruptFs             = 8458;   (* SQLITE_IOERR_CORRUPTFS *)
+   SQLiteIoErrInPage                = 8714;   (* SQLITE_IOERR_IN_PAGE *)
+   SQLiteIoErrBadKey                = 8970;   (* SQLITE_IOERR_BADKEY *)
+   SQLiteIoErrCodec                 = 9226;   (* SQLITE_IOERR_CODEC *)
+   SQLiteLockedSharedCache          = 262;   (* SQLITE_LOCKED_SHAREDCACHE *)
+   SQLiteLockedVtab                 = 518;   (* SQLITE_LOCKED_VTAB *)
+   SQLiteBusyRecovery               = 261;   (* SQLITE_BUSY_RECOVERY *)
+   SQLiteBusySnapshot               = 517;   (* SQLITE_BUSY_SNAPSHOT *)
+   SQLiteBusyTimeout                = 773;   (* SQLITE_BUSY_TIMEOUT *)
+   SQLiteCantOpenNoTempDir          = 270;   (* SQLITE_CANTOPEN_NOTEMPDIR *)
+   SQLiteCantOpenIsDir              = 526;   (* SQLITE_CANTOPEN_ISDIR *)
+   SQLiteCantOpenFullPath           = 782;   (* SQLITE_CANTOPEN_FULLPATH *)
+   SQLiteCantOpenConvPath           = 1038;   (* SQLITE_CANTOPEN_CONVPATH *)
+   SQLiteCantOpenDirtyWal           = 1294;   (* SQLITE_CANTOPEN_DIRTYWAL *)
+   SQLiteCantOpenSymlink            = 1550;   (* SQLITE_CANTOPEN_SYMLINK *)
+   SQLiteCorruptVtab                = 267;   (* SQLITE_CORRUPT_VTAB *)
+   SQLiteCorruptSequence            = 523;   (* SQLITE_CORRUPT_SEQUENCE *)
+   SQLiteCorruptIndex               = 779;   (* SQLITE_CORRUPT_INDEX *)
+   SQLiteReadOnlyRecovery           = 264;   (* SQLITE_READONLY_RECOVERY *)
+   SQLiteReadOnlyCantLock           = 520;   (* SQLITE_READONLY_CANTLOCK *)
+   SQLiteReadOnlyRollback           = 776;   (* SQLITE_READONLY_ROLLBACK *)
+   SQLiteReadOnlyDbMoved            = 1032;   (* SQLITE_READONLY_DBMOVED *)
+   SQLiteReadOnlyCantInit           = 1288;   (* SQLITE_READONLY_CANTINIT *)
+   SQLiteReadOnlyDirectory          = 1544;   (* SQLITE_READONLY_DIRECTORY *)
+   SQLiteAbortRollback              = 516;   (* SQLITE_ABORT_ROLLBACK *)
+   SQLiteConstraintCheck            = 275;   (* SQLITE_CONSTRAINT_CHECK *)
+   SQLiteConstraintCommitHook       = 531;   (* SQLITE_CONSTRAINT_COMMITHOOK *)
+   SQLiteConstraintForeignKey       = 787;   (* SQLITE_CONSTRAINT_FOREIGNKEY *)
+   SQLiteConstraintFunction         = 1043;   (* SQLITE_CONSTRAINT_FUNCTION *)
+   SQLiteConstraintNotNull          = 1299;   (* SQLITE_CONSTRAINT_NOTNULL *)
+   SQLiteConstraintPrimaryKey       = 1555;   (* SQLITE_CONSTRAINT_PRIMARYKEY *)
+   SQLiteConstraintTrigger          = 1811;   (* SQLITE_CONSTRAINT_TRIGGER *)
+   SQLiteConstraintUnique           = 2067;   (* SQLITE_CONSTRAINT_UNIQUE *)
+   SQLiteConstraintVtab             = 2323;   (* SQLITE_CONSTRAINT_VTAB *)
+   SQLiteConstraintRowid            = 2579;   (* SQLITE_CONSTRAINT_ROWID *)
+   SQLiteConstraintPinned           = 2835;   (* SQLITE_CONSTRAINT_PINNED *)
+   SQLiteConstraintDatatype         = 3091;   (* SQLITE_CONSTRAINT_DATATYPE *)
+   SQLiteNoticeRecoverWal           = 283;   (* SQLITE_NOTICE_RECOVER_WAL *)
+   SQLiteNoticeRecoverRollback      = 539;   (* SQLITE_NOTICE_RECOVER_ROLLBACK *)
+   SQLiteNoticeRbu                  = 795;   (* SQLITE_NOTICE_RBU *)
+   SQLiteWarningAutoIndex           = 284;   (* SQLITE_WARNING_AUTOINDEX *)
+   SQLiteAuthUser                   = 279;   (* SQLITE_AUTH_USER *)
+   SQLiteOkLoadPermanently          = 256;   (* SQLITE_OK_LOAD_PERMANENTLY *)
+   SQLiteOkSymlink                  = 512;   (* SQLITE_OK_SYMLINK *)
 
 
 (* const char *sqlite3_libversion(void) *)
@@ -525,4 +631,15 @@ PROCEDURE sqlite3_backup_finish (bk: BackupHandle) : INTEGER;
 PROCEDURE sqlite3_backup_remaining (bk: BackupHandle) : INTEGER;
 PROCEDURE sqlite3_backup_pagecount (bk: BackupHandle) : INTEGER;
 
+(* One-step query into a result table. On success tbl points to
+   cols header strings followed by rows times cols value strings,
+   NULL for SQL NULL; rows and cols report the dimensions. Pass
+   ADR(msg) or NIL for the error message, free it with sqlite3_free.
+   Release the table with sqlite3_free_table. See SQLiteUtils for
+   the GetTable wrapper and TableCell reader. *)
+PROCEDURE sqlite3_get_table (db: DbHandle; sql: ARRAY OF CHAR;
+                             VAR tbl: ADDRESS; VAR rows, cols: INTEGER;
+                             errmsg: ADDRESS) : INTEGER;
+PROCEDURE sqlite3_free_table (tbl: ADDRESS);
+
 END SQLite.

+ 1 - 1
tests/run_tests.sh

@@ -20,7 +20,7 @@ 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 test_func test_blob; do
+for t in test_version test_open test_exec test_prepare test_func test_blob test_codes; do
   echo "== $t =="
   # shellcheck disable=SC2086
   $GM2 -I"$ROOT/src" -I"$ROOT/lib" $GM2FLAGS "$ROOT/tests/$t.mod" \

+ 109 - 0
tests/test_codes.mod

@@ -0,0 +1,109 @@
+MODULE test_codes ;
+
+(*
+   m2SQLITE test: extended result codes and the get_table family.
+   Triggers unique, not-null, primary-key and foreign-key violations
+   and checks the exact extended codes, then reads a result table
+   through the raw binding and the GetTable helpers.
+*)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM SQLite IMPORT DbHandle,
+   SQLiteOk,
+   SQLiteConstraintUnique, SQLiteConstraintNotNull,
+   SQLiteConstraintPrimaryKey, SQLiteConstraintForeignKey,
+   sqlite3_open, sqlite3_close,
+   sqlite3_extended_result_codes, sqlite3_extended_errcode,
+   sqlite3_get_table, sqlite3_free_table;
+FROM SQLiteUtils IMPORT ErrMsg, ExecSimple,
+   GetTable, TableCell, FreeTable;
+FROM libc IMPORT printf;
+
+VAR
+   db: DbHandle;
+   tbl: ADDRESS; msg: ADDRESS;
+   rows, cols, rc: INTEGER;
+   buf: ARRAY [0..127] OF CHAR;
+
+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 expect (sql: ARRAY OF CHAR; code: INTEGER; tag: ARRAY OF CHAR);
+VAR rc: INTEGER;
+BEGIN
+   rc := ExecSimple(db, sql);
+   printf("%s rc %d\n", tag, rc);
+   IF rc # code THEN fail(tag) END;
+   IF sqlite3_extended_errcode(db) # code THEN fail(tag) END
+END expect;
+
+BEGIN
+   check(sqlite3_open(":memory:", db), "open");
+   check(sqlite3_extended_result_codes(db, 1), "ext on");
+   check(ExecSimple(db, "CREATE TABLE u(v TEXT UNIQUE);"), "uniq tab");
+   check(ExecSimple(db, "INSERT INTO u VALUES('a');"), "uniq seed");
+   expect("INSERT INTO u VALUES('a');",
+          SQLiteConstraintUnique, "unique");
+   check(ExecSimple(db, "CREATE TABLE n(v TEXT NOT NULL);"), "nn tab");
+   expect("INSERT INTO n VALUES(NULL);",
+          SQLiteConstraintNotNull, "notnull");
+   check(ExecSimple(db, "CREATE TABLE p(id INTEGER PRIMARY KEY);"),
+         "pk tab");
+   check(ExecSimple(db, "INSERT INTO p VALUES(1);"), "pk seed");
+   expect("INSERT INTO p VALUES(1);",
+          SQLiteConstraintPrimaryKey, "primarykey");
+   check(ExecSimple(db, "PRAGMA foreign_keys=ON;"), "fk pragma");
+   check(ExecSimple(db, "CREATE TABLE ch(pid INTEGER"
+                        + " REFERENCES p(id));"), "fk tab");
+   expect("INSERT INTO ch VALUES(2);",
+          SQLiteConstraintForeignKey, "foreignkey");
+   printf("extended codes ok\n");
+
+   (* raw binding call *)
+   check(ExecSimple(db, "CREATE TABLE t(a TEXT, b INTEGER);"), "t tab");
+   check(ExecSimple(db, "INSERT INTO t VALUES('x', 1),"
+                        + "(NULL, 2), ('z', 3);"), "t seed");
+   tbl := NIL; rows := 0; cols := 0; msg := NIL;
+   rc := sqlite3_get_table(db, "SELECT a, b FROM t ORDER BY b;",
+                           tbl, rows, cols, ADR(msg));
+   IF rc # SQLiteOk THEN fail("raw get_table") END;
+   IF msg # NIL THEN fail("raw errmsg set") END;
+   IF (rows # 3) OR (cols # 2) THEN fail("raw dims") END;
+   TableCell(tbl, cols, -1, 0, buf);
+   IF buf[0] # 'a' THEN fail("raw header") END;
+   TableCell(tbl, cols, 1, 0, buf);
+   IF buf[0] # 0C THEN fail("raw null cell") END;
+   TableCell(tbl, cols, 2, 0, buf);
+   IF buf[0] # 'z' THEN fail("raw cell") END;
+   TableCell(tbl, cols, 0, 1, buf);
+   IF buf[0] # '1' THEN fail("raw int cell") END;
+   FreeTable(tbl);
+   printf("raw table ok\n");
+
+   (* helper path, including the failing-statement branch *)
+   check(GetTable(db, "SELECT a FROM t WHERE b = 3;",
+                  tbl, rows, cols, buf), "helper table");
+   IF (rows # 1) OR (cols # 1) THEN fail("helper dims") END;
+   TableCell(tbl, cols, 0, 0, buf);
+   printf("helper cell %s\n", buf);
+   IF buf[0] # 'z' THEN fail("helper cell") END;
+   FreeTable(tbl);
+   rc := GetTable(db, "SELECT * FROM bogus;", tbl, rows, cols, buf);
+   printf("helper bad rc %d err %s\n", rc, buf);
+   IF rc = SQLiteOk THEN fail("helper bad ok") END;
+   IF buf[0] = 0C THEN fail("helper bad msg") END;
+   IF tbl # NIL THEN fail("helper bad table") END;
+
+   check(sqlite3_close(db), "close");
+   printf("PASS test_codes\n")
+END test_codes.