瀏覽代碼

bindings: add SQLite scalar and error APIs

Eric Streit 1 天之前
父節點
當前提交
5d5faa1c9a

+ 70 - 26
bindings/sqlite3/README.md

@@ -1,30 +1,68 @@
-# SQLite binding (first slice)
+# SQLite binding (scalar slice)
 
-This is the first end-to-end V3 C binding test, not a replacement for
-the full `m2SQLITE` binding.
+This is a small, hand-written V3 binding for the scalar SQLite API.  It
+is not a replacement for the full `m2SQLITE` definition.
 
-Current surface:
+## Current surface
 
 ```modula2
+(* connection *)
 sqlite3_open
 sqlite3_exec
 sqlite3_close
+
+(* prepared statements *)
 sqlite3_prepare_v2
 sqlite3_bind_int
+sqlite3_bind_int64
+sqlite3_bind_double
+sqlite3_bind_null
 sqlite3_bind_text
 sqlite3_step
+sqlite3_reset
+sqlite3_clear_bindings
+sqlite3_finalize
+
+(* column metadata and scalar results *)
+sqlite3_column_count
+sqlite3_data_count
+sqlite3_column_type
+sqlite3_column_bytes
 sqlite3_column_int
+sqlite3_column_int64
+sqlite3_column_double
 sqlite3_column_text
-sqlite3_finalize
-SQLiteOk
-SQLiteRow
-SQLiteDone
+sqlite3_column_name
+sqlite3_column_decltype
+
+(* errors and ownership *)
+sqlite3_errmsg
+sqlite3_errstr
+sqlite3_errcode
+sqlite3_extended_errcode
+sqlite3_extended_result_codes
+sqlite3_free
+
+(* binding helper *)
+SQLiteTransient
 ```
 
-The smoke tests open an in-memory database, execute SQL, bind
-parameters, read a row and close the handle.  The showcase
-(`showcase/SQLiteShowcase.mod`) exercises the complete current surface
-and prints:
+The matching constants include the primary result codes
+(`SQLiteOk`, `SQLiteConstraint`, ...), `SQLiteRow`, `SQLiteDone`,
+storage classes (`SQLiteInteger`, `SQLiteFloat`, `SQLiteText`,
+`SQLiteBlob`, `SQLiteNull`) and a few extended constraints.
+
+## Tests and showcase
+
+`run.sh` builds and runs:
+
+- `SQLiteSmoke.mod` - open / exec / close
+- `SQLitePrepared.mod` - prepared insert and select
+- `SQLiteScalar.mod` - int, int64, double, text, NULL and column metadata
+- `SQLiteErrors.mod` - error codes, errmsg/errstr and `sqlite3_free`
+- `showcase/SQLiteShowcase.mod` - prepared-statement walkthrough
+
+The showcase prints:
 
 ```text
 1: Ada
@@ -47,17 +85,23 @@ links the versioned runtime library directly:
 That fallback matters on systems with the SQLite runtime installed but
 without `libsqlite3-dev`.
 
-## Notes
-
-- `sqlite3_open`, `sqlite3_exec`, `prepare` and `bind_text` exercise the
-  C string ABI.
-- `VAR db : ADDRESS` / `VAR stmt : ADDRESS` exercise opaque C handles.
-- `sqlite3_column_text` returns `CStr.CString`, converted with
-  `CStr.Copy`.
-- `SQLiteTransient()` supplies the SQLITE_TRANSIENT sentinel so SQLite
-  copies bound text; passing `NIL` means SQLITE_STATIC and requires the
-  caller's buffer to remain alive.
-- The prepared-statement test is `SQLitePrepared.mod`.
-- The full showcase is `showcase/SQLiteShowcase.mod`.
-- The full `m2SQLITE` definition remains the source for later API
-  coverage; error strings, column types, resets and callbacks are next.
+## Ownership notes
+
+- `sqlite3_exec` now takes `VAR errmsg : ADDRESS`.  A non-NIL message is
+  allocated by SQLite and must be released with `sqlite3_free`.
+- `sqlite3_errmsg` and `sqlite3_errstr` return borrowed C strings.  Copy
+  them with `CStr.Copy` if they need to outlive the next SQLite call.
+- `sqlite3_column_text` is valid only until the next `step`, `reset` or
+  `finalize`; copy it immediately.
+- `SQLiteTransient()` supplies the `SQLITE_TRANSIENT` sentinel so SQLite
+  copies bound text.  Passing `NIL` means `SQLITE_STATIC`; the caller's
+  buffer must then stay alive until the statement is reset or finalized.
+- When extended result codes are enabled, `sqlite3_exec` and
+  `sqlite3_errcode` can report an extended code such as
+  `SQLiteConstraintPrimaryKey` (1555) rather than only
+  `SQLiteConstraint` (19).
+
+## Still to add
+
+BLOB binding/column access, `open_v2` flags, `close_v2`, changes/rowid
+helpers, callbacks, BLOB I/O, backup and extension loading.

+ 103 - 4
bindings/sqlite3/SQLite.def

@@ -1,34 +1,100 @@
 DEFINITION MODULE FOR "C" SQLite;
-(* Minimal first SQLite binding: open an in-memory database, execute SQL,
-   and close it.  The full binding stays in m2SQLITE for later porting. *)
+(* Scalar SQLite binding: connection setup, prepared statements,
+   scalar binding/results, column metadata and error reporting.
+   The full binding stays in m2SQLITE for later porting. *)
 
 FROM SYSTEM IMPORT ADDRESS;
 FROM CStr IMPORT CString;
 
 EXPORT UNQUALIFIED
-  SQLiteOk, SQLiteRow, SQLiteDone,
+  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,
+  SQLiteInteger, SQLiteFloat, SQLiteText, SQLiteBlob, SQLiteNull,
+  SQLiteConstraintPrimaryKey, SQLiteConstraintUnique,
   sqlite3_open,
   sqlite3_exec,
   sqlite3_close,
   sqlite3_prepare_v2,
   sqlite3_bind_int,
+  sqlite3_bind_int64,
+  sqlite3_bind_double,
+  sqlite3_bind_null,
   sqlite3_bind_text,
   sqlite3_step,
+  sqlite3_reset,
+  sqlite3_clear_bindings,
+  sqlite3_column_count,
+  sqlite3_data_count,
+  sqlite3_column_type,
+  sqlite3_column_bytes,
   sqlite3_column_int,
+  sqlite3_column_int64,
+  sqlite3_column_double,
   sqlite3_column_text,
+  sqlite3_column_name,
+  sqlite3_column_decltype,
   sqlite3_finalize,
+  sqlite3_errmsg,
+  sqlite3_errstr,
+  sqlite3_errcode,
+  sqlite3_extended_errcode,
+  sqlite3_extended_result_codes,
+  sqlite3_free,
   SQLiteTransient;
 
 CONST
   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;
 
+  SQLiteInteger = 1;
+  SQLiteFloat = 2;
+  SQLiteText = 3;
+  SQLiteBlob = 4;
+  SQLiteNull = 5;
+
+  SQLiteConstraintPrimaryKey = 1555;
+  SQLiteConstraintUnique = 2067;
+
 PROCEDURE sqlite3_open (filename : ARRAY OF CHAR;
                         VAR db : ADDRESS) : INTEGER;
 PROCEDURE sqlite3_exec (db : ADDRESS;
                         sql : ARRAY OF CHAR;
-                        callback, arg, errmsg : ADDRESS) : INTEGER;
+                        callback, arg : ADDRESS;
+                        VAR errmsg : ADDRESS) : INTEGER;
 PROCEDURE sqlite3_close (db : ADDRESS) : INTEGER;
 
 PROCEDURE sqlite3_prepare_v2 (db : ADDRESS;
@@ -39,18 +105,51 @@ PROCEDURE sqlite3_prepare_v2 (db : ADDRESS;
 PROCEDURE sqlite3_bind_int (stmt : ADDRESS;
                             idx : INTEGER;
                             value : INTEGER) : INTEGER;
+PROCEDURE sqlite3_bind_int64 (stmt : ADDRESS;
+                              idx : INTEGER;
+                              value : LONGINT) : INTEGER;
+PROCEDURE sqlite3_bind_double (stmt : ADDRESS;
+                               idx : INTEGER;
+                               value : REAL) : INTEGER;
+PROCEDURE sqlite3_bind_null (stmt : ADDRESS;
+                             idx : INTEGER) : INTEGER;
 PROCEDURE sqlite3_bind_text (stmt : ADDRESS;
                              idx : INTEGER;
                              value : ARRAY OF CHAR;
                              nBytes : INTEGER;
                              destructor : ADDRESS) : INTEGER;
 PROCEDURE sqlite3_step (stmt : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_reset (stmt : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_clear_bindings (stmt : ADDRESS) : INTEGER;
+
+PROCEDURE sqlite3_column_count (stmt : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_data_count (stmt : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_column_type (stmt : ADDRESS;
+                               col : INTEGER) : INTEGER;
+PROCEDURE sqlite3_column_bytes (stmt : ADDRESS;
+                                col : INTEGER) : INTEGER;
 PROCEDURE sqlite3_column_int (stmt : ADDRESS;
                               col : INTEGER) : INTEGER;
+PROCEDURE sqlite3_column_int64 (stmt : ADDRESS;
+                                col : INTEGER) : LONGINT;
+PROCEDURE sqlite3_column_double (stmt : ADDRESS;
+                                 col : INTEGER) : REAL;
 PROCEDURE sqlite3_column_text (stmt : ADDRESS;
                                col : INTEGER) : CString;
+PROCEDURE sqlite3_column_name (stmt : ADDRESS;
+                               col : INTEGER) : CString;
+PROCEDURE sqlite3_column_decltype (stmt : ADDRESS;
+                                   col : INTEGER) : CString;
 PROCEDURE sqlite3_finalize (stmt : ADDRESS) : INTEGER;
 
+PROCEDURE sqlite3_errmsg (db : ADDRESS) : CString;
+PROCEDURE sqlite3_errstr (code : INTEGER) : CString;
+PROCEDURE sqlite3_errcode (db : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_extended_errcode (db : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_extended_result_codes (db : ADDRESS;
+                                         onoff : INTEGER) : INTEGER;
+PROCEDURE sqlite3_free (p : ADDRESS);
+
 PROCEDURE SQLiteTransient () : ADDRESS;
   EXTERNAL "m2_sqlite_transient";
 (* The SQLITE_TRANSIENT destructor sentinel: SQLite copies bound text. *)

+ 2 - 0
bindings/sqlite3/run.sh

@@ -38,4 +38,6 @@ build_and_run() {
 
 build_and_run SQLiteSmoke
 build_and_run SQLitePrepared
+build_and_run SQLiteScalar
+build_and_run SQLiteErrors
 build_and_run SQLiteShowcase showcase/SQLiteShowcase.mod

+ 11 - 3
bindings/sqlite3/showcase/SQLiteShowcase.mod

@@ -1,5 +1,5 @@
 MODULE SQLiteShowcase;
-(* Showcase of the current SQLite binding surface:
+(* Prepared-statement walkthrough:
    open/exec/close, prepare, bind int/text, step, column int/text,
    finalize.  Prints the two stored rows. *)
 
@@ -11,15 +11,23 @@ FROM SQLite IMPORT
   sqlite3_open, sqlite3_exec, sqlite3_close,
   sqlite3_prepare_v2, sqlite3_bind_int, sqlite3_bind_text,
   sqlite3_step, sqlite3_column_int, sqlite3_column_text,
-  sqlite3_finalize, SQLiteTransient;
+  sqlite3_finalize, sqlite3_free, SQLiteTransient;
 
 VAR
   ExitCode : INTEGER;
   db : ADDRESS;
 
 PROCEDURE ExecSQL (sql : ARRAY OF CHAR) : BOOLEAN;
+VAR
+  errmsg : ADDRESS;
+  ok : BOOLEAN;
 BEGIN
-  RETURN sqlite3_exec(db, sql, NIL, NIL, NIL) = SQLiteOk
+  errmsg := NIL;
+  ok := sqlite3_exec(db, sql, NIL, NIL, errmsg) = SQLiteOk;
+  IF errmsg # NIL THEN
+    sqlite3_free(errmsg)
+  END;
+  RETURN ok
 END ExecSQL;
 
 PROCEDURE InsertPerson (id : INTEGER; name : ARRAY OF CHAR) : BOOLEAN;

+ 63 - 0
bindings/sqlite3/tests/SQLiteErrors.mod

@@ -0,0 +1,63 @@
+MODULE SQLiteErrors;
+(* Error codes, errmsg/errstr, extended codes and sqlite3_free. *)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM CStr IMPORT CString, Copy;
+FROM SQLite IMPORT
+  SQLiteOk, SQLiteConstraint, SQLiteConstraintPrimaryKey,
+  sqlite3_open, sqlite3_exec, sqlite3_close,
+  sqlite3_errcode, sqlite3_extended_errcode,
+  sqlite3_extended_result_codes, sqlite3_errmsg, sqlite3_errstr,
+  sqlite3_free;
+
+VAR
+  ExitCode : INTEGER;
+  db : ADDRESS;
+  errmsg : ADDRESS;
+  svalue : CString;
+  buf : ARRAY [0..127] OF CHAR;
+  rc : INTEGER;
+  ok : BOOLEAN;
+
+BEGIN
+  ExitCode := 1;
+  db := NIL; errmsg := NIL; ok := FALSE;
+  IF sqlite3_open(":memory:", db) = SQLiteOk THEN
+    IF (sqlite3_extended_result_codes(db, 1) = SQLiteOk)
+       AND (sqlite3_exec(db, "CREATE TABLE u(x INTEGER PRIMARY KEY);",
+                         NIL, NIL, errmsg) = SQLiteOk) THEN
+      IF sqlite3_exec(db, "INSERT INTO u VALUES (1);",
+                      NIL, NIL, errmsg) = SQLiteOk THEN
+        errmsg := NIL;
+        rc := sqlite3_exec(db, "INSERT INTO u VALUES (1);",
+                           NIL, NIL, errmsg);
+        IF (rc = SQLiteConstraintPrimaryKey)
+           AND (sqlite3_errcode(db) = SQLiteConstraintPrimaryKey)
+           AND (sqlite3_extended_errcode(db) = SQLiteConstraintPrimaryKey) THEN
+          svalue := sqlite3_errmsg(db);
+          IF (svalue # NIL) AND (Copy(svalue, buf) > 0) AND (buf[0] # CHR(0)) THEN
+            svalue := sqlite3_errstr(SQLiteConstraint);
+            IF (svalue # NIL) AND (Copy(svalue, buf) > 0)
+               AND (buf[0] # CHR(0)) THEN
+              ok := TRUE
+            END
+          END
+        END;
+        IF errmsg # NIL THEN
+          svalue := errmsg;
+          IF (Copy(svalue, buf) = 0) OR (buf[0] = CHR(0)) THEN
+            ok := FALSE
+          END;
+          sqlite3_free(errmsg);
+          errmsg := NIL
+        ELSE
+          ok := FALSE
+        END
+      END
+    END
+  END;
+  IF db # NIL THEN
+    IF sqlite3_close(db) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF ok THEN ExitCode := 42 END
+END SQLiteErrors.

+ 132 - 0
bindings/sqlite3/tests/SQLiteScalar.mod

@@ -0,0 +1,132 @@
+MODULE SQLiteScalar;
+(* Scalar binding/result metadata: int, int64, double, text, NULL. *)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM CStr IMPORT CString, Copy;
+FROM SQLite IMPORT
+  SQLiteOk, SQLiteRow, SQLiteDone,
+  SQLiteInteger, SQLiteFloat, SQLiteText, SQLiteNull,
+  sqlite3_open, sqlite3_exec, sqlite3_close,
+  sqlite3_prepare_v2, sqlite3_bind_int, sqlite3_bind_int64,
+  sqlite3_bind_double, sqlite3_bind_null, sqlite3_bind_text,
+  sqlite3_step, sqlite3_reset, sqlite3_clear_bindings,
+  sqlite3_column_count, sqlite3_data_count, sqlite3_column_type,
+  sqlite3_column_bytes, sqlite3_column_int, sqlite3_column_int64,
+  sqlite3_column_double, sqlite3_column_text, sqlite3_column_name,
+  sqlite3_column_decltype, sqlite3_finalize, sqlite3_free,
+  sqlite3_extended_result_codes, SQLiteTransient;
+
+VAR
+  ExitCode : INTEGER;
+  db : ADDRESS;
+
+PROCEDURE ExecSQL (sql : ARRAY OF CHAR) : BOOLEAN;
+VAR
+  errmsg : ADDRESS;
+  ok : BOOLEAN;
+BEGIN
+  errmsg := NIL;
+  ok := sqlite3_exec(db, sql, NIL, NIL, errmsg) = SQLiteOk;
+  IF errmsg # NIL THEN
+    sqlite3_free(errmsg)
+  END;
+  RETURN ok
+END ExecSQL;
+
+VAR
+  stmt : ADDRESS;
+  rc : INTEGER;
+  ok : BOOLEAN;
+  svalue : CString;
+  name : ARRAY [0..63] OF CHAR;
+  text : ARRAY [0..63] OF CHAR;
+  decl : ARRAY [0..63] OF CHAR;
+
+BEGIN
+  ExitCode := 1;
+  db := NIL;
+  stmt := NIL;
+  ok := FALSE;
+  IF (sqlite3_open(":memory:", db) = SQLiteOk) AND (db # NIL) THEN
+    IF sqlite3_extended_result_codes(db, 1) = SQLiteOk THEN
+      IF ExecSQL("CREATE TABLE t(i INTEGER, j INTEGER, r REAL, s TEXT, n TEXT);") THEN
+        rc := sqlite3_prepare_v2(db,
+          "INSERT INTO t VALUES (?, ?, ?, ?, ?);", -1, stmt, NIL);
+        ok := (rc = SQLiteOk) AND (stmt # NIL);
+        IF ok THEN
+          ok := (sqlite3_bind_int(stmt, 1, 42) = SQLiteOk)
+                AND (sqlite3_bind_int64(stmt, 2, 43) = SQLiteOk)
+                AND (sqlite3_bind_double(stmt, 3, 3.5) = SQLiteOk)
+                AND (sqlite3_bind_text(stmt, 4, "hello", -1,
+                                       SQLiteTransient()) = SQLiteOk)
+                AND (sqlite3_bind_null(stmt, 5) = SQLiteOk)
+        END;
+        IF ok THEN
+          ok := sqlite3_step(stmt) = SQLiteDone
+        END;
+        IF stmt # NIL THEN
+          IF sqlite3_reset(stmt) # SQLiteOk THEN ok := FALSE END;
+          IF sqlite3_clear_bindings(stmt) # SQLiteOk THEN ok := FALSE END;
+          IF sqlite3_finalize(stmt) # SQLiteOk THEN ok := FALSE END;
+          stmt := NIL
+        END
+      END
+    END
+  END;
+
+  IF ok THEN
+    rc := sqlite3_prepare_v2(db,
+      "SELECT i, j, r, s, n FROM t;", -1, stmt, NIL);
+    ok := (rc = SQLiteOk) AND (stmt # NIL);
+    IF ok THEN
+      IF sqlite3_column_count(stmt) = 5 THEN
+        rc := sqlite3_step(stmt);
+        IF rc = SQLiteRow THEN
+          ok := (sqlite3_data_count(stmt) = 5)
+                AND (sqlite3_column_type(stmt, 0) = SQLiteInteger)
+                AND (sqlite3_column_type(stmt, 1) = SQLiteInteger)
+                AND (sqlite3_column_type(stmt, 2) = SQLiteFloat)
+                AND (sqlite3_column_type(stmt, 3) = SQLiteText)
+                AND (sqlite3_column_type(stmt, 4) = SQLiteNull)
+                AND (sqlite3_column_int(stmt, 0) = 42)
+                AND (sqlite3_column_int64(stmt, 1) = 43)
+                AND (sqlite3_column_double(stmt, 2) = 3.5)
+                AND (sqlite3_column_bytes(stmt, 3) = 5);
+          svalue := sqlite3_column_text(stmt, 3);
+          IF (svalue = NIL) OR (Copy(svalue, text) # 5) THEN
+            ok := FALSE
+          ELSIF (text[0] # 'h') OR (text[4] # 'o') THEN
+            ok := FALSE
+          END;
+          svalue := sqlite3_column_name(stmt, 3);
+          IF (svalue = NIL) OR (Copy(svalue, name) # 1)
+             OR (name[0] # 's') THEN
+            ok := FALSE
+          END;
+          svalue := sqlite3_column_decltype(stmt, 3);
+          IF (svalue = NIL) OR (Copy(svalue, decl) # 4)
+             OR (decl[0] # 'T') OR (decl[3] # 'T') THEN
+            ok := FALSE
+          END;
+          IF sqlite3_step(stmt) # SQLiteDone THEN ok := FALSE END
+        ELSE
+          ok := FALSE
+        END
+      ELSE
+        ok := FALSE
+      END;
+      IF stmt # NIL THEN
+        IF sqlite3_finalize(stmt) # SQLiteOk THEN ok := FALSE END;
+        stmt := NIL
+      END
+    END
+  END;
+
+  IF stmt # NIL THEN
+    IF sqlite3_finalize(stmt) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF db # NIL THEN
+    IF sqlite3_close(db) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF ok THEN ExitCode := 42 END
+END SQLiteScalar.

+ 8 - 5
bindings/sqlite3/tests/SQLiteSmoke.mod

@@ -2,22 +2,25 @@ MODULE SQLiteSmoke;
 (* First end-to-end SQLite binding test: :memory: open/exec/close. *)
 
 FROM SYSTEM IMPORT ADDRESS;
-FROM SQLite IMPORT SQLiteOk, sqlite3_open, sqlite3_exec, sqlite3_close;
+FROM SQLite IMPORT SQLiteOk, sqlite3_open, sqlite3_exec, sqlite3_close,
+  sqlite3_free;
 
 VAR
   ExitCode : INTEGER;
-  db : ADDRESS;
+  db, errmsg : ADDRESS;
   rc : INTEGER;
 
 BEGIN
-  db := NIL;
+  db := NIL; errmsg := NIL;
   rc := sqlite3_open(":memory:", db);
   IF (rc = SQLiteOk) AND (db # NIL) THEN
     rc := sqlite3_exec(db, "CREATE TABLE t(x INTEGER);",
-                       NIL, NIL, NIL);
+                       NIL, NIL, errmsg);
+    IF errmsg # NIL THEN sqlite3_free(errmsg); errmsg := NIL END;
     IF rc = SQLiteOk THEN
       rc := sqlite3_exec(db, "INSERT INTO t VALUES (42);",
-                         NIL, NIL, NIL)
+                         NIL, NIL, errmsg);
+      IF errmsg # NIL THEN sqlite3_free(errmsg); errmsg := NIL END
     END
   END;
   IF db # NIL THEN