Ver Fonte

bindings: add SQLite BLOB and connection APIs

Eric Streit há 2 dias atrás
pai
commit
0f235a1a02

+ 38 - 9
bindings/sqlite3/README.md

@@ -1,6 +1,7 @@
-# SQLite binding (scalar slice)
+# SQLite binding (core slice)
 
-This is a small, hand-written V3 binding for the scalar SQLite API.  It
+This is a small, hand-written V3 binding for the core SQLite API:
+scalar values, BLOBs, prepared statements and connection control.  It
 is not a replacement for the full `m2SQLITE` definition.
 
 ## Current surface
@@ -8,8 +9,10 @@ is not a replacement for the full `m2SQLITE` definition.
 ```modula2
 (* connection *)
 sqlite3_open
+sqlite3_open_v2
 sqlite3_exec
 sqlite3_close
+sqlite3_close_v2
 
 (* prepared statements *)
 sqlite3_prepare_v2
@@ -17,12 +20,19 @@ sqlite3_bind_int
 sqlite3_bind_int64
 sqlite3_bind_double
 sqlite3_bind_null
+sqlite3_bind_blob
+sqlite3_bind_zeroblob
 sqlite3_bind_text
 sqlite3_step
 sqlite3_reset
 sqlite3_clear_bindings
 sqlite3_finalize
 
+(* row change metadata *)
+sqlite3_changes
+sqlite3_total_changes
+sqlite3_last_insert_rowid
+
 (* column metadata and scalar results *)
 sqlite3_column_count
 sqlite3_data_count
@@ -31,6 +41,7 @@ sqlite3_column_bytes
 sqlite3_column_int
 sqlite3_column_int64
 sqlite3_column_double
+sqlite3_column_blob
 sqlite3_column_text
 sqlite3_column_name
 sqlite3_column_decltype
@@ -43,6 +54,14 @@ sqlite3_extended_errcode
 sqlite3_extended_result_codes
 sqlite3_free
 
+(* incremental BLOB I/O *)
+sqlite3_blob_open
+sqlite3_blob_close
+sqlite3_blob_read
+sqlite3_blob_write
+sqlite3_blob_bytes
+sqlite3_blob_reopen
+
 (* binding helper *)
 SQLiteTransient
 ```
@@ -50,7 +69,8 @@ SQLiteTransient
 The matching constants include the primary result codes
 (`SQLiteOk`, `SQLiteConstraint`, ...), `SQLiteRow`, `SQLiteDone`,
 storage classes (`SQLiteInteger`, `SQLiteFloat`, `SQLiteText`,
-`SQLiteBlob`, `SQLiteNull`) and a few extended constraints.
+`SQLiteBlob`, `SQLiteNull`), a few extended constraints and the
+`SQLiteOpen*` flags used by `sqlite3_open_v2` and `sqlite3_blob_open`.
 
 ## Tests and showcase
 
@@ -60,6 +80,8 @@ storage classes (`SQLiteInteger`, `SQLiteFloat`, `SQLiteText`,
 - `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`
+- `SQLiteBlob.mod` - bind_blob, bind_zeroblob, column_blob and incremental BLOB I/O
+- `SQLiteConnection.mod` - open_v2, close_v2, changes/rowid and deferred close
 - `showcase/SQLiteShowcase.mod` - prepared-statement walkthrough
 
 The showcase prints:
@@ -91,11 +113,18 @@ without `libsqlite3-dev`.
   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.
+- `sqlite3_column_text` and `sqlite3_column_blob` return pointers valid
+  only until the next `step`, `reset` or `finalize`; copy or consume them
+  immediately.
+- For a raw C data pointer to a V3 array, use `ADR(a[0])`, not `ADR(a)`:
+  `ADR(a)` points at the V3 array descriptor.
 - `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.
+  copies bound text or BLOB data.  Passing `NIL` means `SQLITE_STATIC`;
+  the caller's buffer must then stay alive until the statement is reset
+  or finalized.
+- `sqlite3_blob_open` needs an explicit `SQLiteOpenReadOnly` or
+  `SQLiteOpenReadWrite` flag.  A BLOB handle holds a read/write lock on
+  its row until `sqlite3_blob_close`.
 - When extended result codes are enabled, `sqlite3_exec` and
   `sqlite3_errcode` can report an extended code such as
   `SQLiteConstraintPrimaryKey` (1555) rather than only
@@ -103,5 +132,5 @@ without `libsqlite3-dev`.
 
 ## Still to add
 
-BLOB binding/column access, `open_v2` flags, `close_v2`, changes/rowid
-helpers, callbacks, BLOB I/O, backup and extension loading.
+Callbacks, backup, extension loading, WAL/checkpoint helpers, connection
+configuration and statement parameter introspection.

+ 69 - 2
bindings/sqlite3/SQLite.def

@@ -1,6 +1,7 @@
 DEFINITION MODULE FOR "C" SQLite;
-(* Scalar SQLite binding: connection setup, prepared statements,
-   scalar binding/results, column metadata and error reporting.
+(* Core SQLite binding: connection setup, prepared statements,
+   scalar/BLOB binding and results, column metadata, row changes,
+   incremental BLOB I/O and error reporting.
    The full binding stays in m2SQLITE for later porting. *)
 
 FROM SYSTEM IMPORT ADDRESS;
@@ -16,14 +17,22 @@ EXPORT UNQUALIFIED
   SQLiteNotADb, SQLiteNotice, SQLiteWarning, SQLiteRow, SQLiteDone,
   SQLiteInteger, SQLiteFloat, SQLiteText, SQLiteBlob, SQLiteNull,
   SQLiteConstraintPrimaryKey, SQLiteConstraintUnique,
+  SQLiteOpenReadOnly, SQLiteOpenReadWrite, SQLiteOpenCreate,
+  SQLiteOpenURI, SQLiteOpenMemory, SQLiteOpenNoMutex,
+  SQLiteOpenFullMutex, SQLiteOpenSharedCache, SQLiteOpenPrivateCache,
+  SQLiteOpenWal,
   sqlite3_open,
+  sqlite3_open_v2,
   sqlite3_exec,
   sqlite3_close,
+  sqlite3_close_v2,
   sqlite3_prepare_v2,
   sqlite3_bind_int,
   sqlite3_bind_int64,
   sqlite3_bind_double,
   sqlite3_bind_null,
+  sqlite3_bind_blob,
+  sqlite3_bind_zeroblob,
   sqlite3_bind_text,
   sqlite3_step,
   sqlite3_reset,
@@ -35,10 +44,20 @@ EXPORT UNQUALIFIED
   sqlite3_column_int,
   sqlite3_column_int64,
   sqlite3_column_double,
+  sqlite3_column_blob,
   sqlite3_column_text,
   sqlite3_column_name,
   sqlite3_column_decltype,
   sqlite3_finalize,
+  sqlite3_changes,
+  sqlite3_total_changes,
+  sqlite3_last_insert_rowid,
+  sqlite3_blob_open,
+  sqlite3_blob_close,
+  sqlite3_blob_read,
+  sqlite3_blob_write,
+  sqlite3_blob_bytes,
+  sqlite3_blob_reopen,
   sqlite3_errmsg,
   sqlite3_errstr,
   sqlite3_errcode,
@@ -89,13 +108,29 @@ CONST
   SQLiteConstraintPrimaryKey = 1555;
   SQLiteConstraintUnique = 2067;
 
+  SQLiteOpenReadOnly = 1;
+  SQLiteOpenReadWrite = 2;
+  SQLiteOpenCreate = 4;
+  SQLiteOpenURI = 64;
+  SQLiteOpenMemory = 128;
+  SQLiteOpenNoMutex = 32768;
+  SQLiteOpenFullMutex = 65536;
+  SQLiteOpenSharedCache = 131072;
+  SQLiteOpenPrivateCache = 262144;
+  SQLiteOpenWal = 524288;
+
 PROCEDURE sqlite3_open (filename : ARRAY OF CHAR;
                         VAR db : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_open_v2 (filename : ARRAY OF CHAR;
+                           VAR db : ADDRESS;
+                           flags : INTEGER;
+                           vfs : CString) : INTEGER;
 PROCEDURE sqlite3_exec (db : ADDRESS;
                         sql : ARRAY OF CHAR;
                         callback, arg : ADDRESS;
                         VAR errmsg : ADDRESS) : INTEGER;
 PROCEDURE sqlite3_close (db : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_close_v2 (db : ADDRESS) : INTEGER;
 
 PROCEDURE sqlite3_prepare_v2 (db : ADDRESS;
                               sql : ARRAY OF CHAR;
@@ -113,6 +148,14 @@ PROCEDURE sqlite3_bind_double (stmt : ADDRESS;
                                value : REAL) : INTEGER;
 PROCEDURE sqlite3_bind_null (stmt : ADDRESS;
                              idx : INTEGER) : INTEGER;
+PROCEDURE sqlite3_bind_blob (stmt : ADDRESS;
+                             idx : INTEGER;
+                             data : ADDRESS;
+                             nBytes : INTEGER;
+                             destructor : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_bind_zeroblob (stmt : ADDRESS;
+                                 idx : INTEGER;
+                                 nBytes : INTEGER) : INTEGER;
 PROCEDURE sqlite3_bind_text (stmt : ADDRESS;
                              idx : INTEGER;
                              value : ARRAY OF CHAR;
@@ -134,6 +177,8 @@ PROCEDURE sqlite3_column_int64 (stmt : ADDRESS;
                                 col : INTEGER) : LONGINT;
 PROCEDURE sqlite3_column_double (stmt : ADDRESS;
                                  col : INTEGER) : REAL;
+PROCEDURE sqlite3_column_blob (stmt : ADDRESS;
+                               col : INTEGER) : ADDRESS;
 PROCEDURE sqlite3_column_text (stmt : ADDRESS;
                                col : INTEGER) : CString;
 PROCEDURE sqlite3_column_name (stmt : ADDRESS;
@@ -142,6 +187,28 @@ PROCEDURE sqlite3_column_decltype (stmt : ADDRESS;
                                    col : INTEGER) : CString;
 PROCEDURE sqlite3_finalize (stmt : ADDRESS) : INTEGER;
 
+PROCEDURE sqlite3_changes (db : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_total_changes (db : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_last_insert_rowid (db : ADDRESS) : LONGINT;
+
+PROCEDURE sqlite3_blob_open (db : ADDRESS;
+                             dbName, tableName, columnName : ARRAY OF CHAR;
+                             rowid : LONGINT;
+                             flags : INTEGER;
+                             VAR blob : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_blob_close (blob : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_blob_read (blob : ADDRESS;
+                             data : ADDRESS;
+                             nBytes : INTEGER;
+                             offset : INTEGER) : INTEGER;
+PROCEDURE sqlite3_blob_write (blob : ADDRESS;
+                              data : ADDRESS;
+                              nBytes : INTEGER;
+                              offset : INTEGER) : INTEGER;
+PROCEDURE sqlite3_blob_bytes (blob : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_blob_reopen (blob : ADDRESS;
+                               rowid : LONGINT) : INTEGER;
+
 PROCEDURE sqlite3_errmsg (db : ADDRESS) : CString;
 PROCEDURE sqlite3_errstr (code : INTEGER) : CString;
 PROCEDURE sqlite3_errcode (db : ADDRESS) : INTEGER;

+ 3 - 1
bindings/sqlite3/run.sh

@@ -1,5 +1,5 @@
 #!/bin/sh
-# Minimal SQLite binding smoke tests.
+# SQLite binding tests and showcase.
 set -e
 cd "$(dirname "$0")"
 ROOT="$(cd ../.. && pwd)"
@@ -40,4 +40,6 @@ build_and_run SQLiteSmoke
 build_and_run SQLitePrepared
 build_and_run SQLiteScalar
 build_and_run SQLiteErrors
+build_and_run SQLiteBlob
+build_and_run SQLiteConnection
 build_and_run SQLiteShowcase showcase/SQLiteShowcase.mod

+ 201 - 0
bindings/sqlite3/tests/SQLiteBlob.mod

@@ -0,0 +1,201 @@
+MODULE SQLiteBlob;
+(* BLOB storage and access:
+   bind_blob, bind_zeroblob, column_blob, column_bytes,
+   incremental blob_open/read/write/bytes/reopen/close. *)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM SQLite IMPORT
+  SQLiteOk, SQLiteRow, SQLiteDone, SQLiteBlob,
+  SQLiteOpenReadOnly, SQLiteOpenReadWrite, SQLiteOpenCreate,
+  SQLiteOpenMemory,
+  sqlite3_open_v2, sqlite3_exec, sqlite3_close_v2,
+  sqlite3_prepare_v2, sqlite3_bind_blob, sqlite3_bind_zeroblob,
+  sqlite3_step, sqlite3_reset, sqlite3_clear_bindings,
+  sqlite3_column_type, sqlite3_column_bytes, sqlite3_column_blob,
+  sqlite3_finalize,
+  sqlite3_changes, sqlite3_total_changes, sqlite3_last_insert_rowid,
+  sqlite3_blob_open, sqlite3_blob_close, sqlite3_blob_read,
+  sqlite3_blob_write, sqlite3_blob_bytes, sqlite3_blob_reopen,
+  sqlite3_free, SQLiteTransient;
+
+VAR
+  ExitCode : INTEGER;
+  db, stmt, blob, p : ADDRESS;
+  bin, out : ARRAY [0..7] OF CHAR;
+  rc : INTEGER;
+  ok : BOOLEAN;
+
+PROCEDURE ExecSQL (sql : ARRAY OF CHAR) : BOOLEAN;
+VAR
+  errmsg : ADDRESS;
+  good : BOOLEAN;
+BEGIN
+  errmsg := NIL;
+  good := sqlite3_exec(db, sql, NIL, NIL, errmsg) = SQLiteOk;
+  IF errmsg # NIL THEN
+    sqlite3_free(errmsg)
+  END;
+  RETURN good
+END ExecSQL;
+
+PROCEDURE SameBytes (a, b : ARRAY OF CHAR; n : INTEGER) : BOOLEAN;
+VAR
+  i : INTEGER;
+BEGIN
+  i := 0;
+  WHILE (i < n) AND (a[i] = b[i]) DO
+    i := i + 1
+  END;
+  RETURN i = n
+END SameBytes;
+
+PROCEDURE FinishStmt;
+BEGIN
+  IF stmt # NIL THEN
+    IF sqlite3_finalize(stmt) # SQLiteOk THEN ok := FALSE END;
+    stmt := NIL
+  END
+END FinishStmt;
+
+BEGIN
+  ExitCode := 1;
+  db := NIL; stmt := NIL; blob := NIL; p := NIL;
+  ok := FALSE;
+  bin[0] := CHR(0); bin[1] := CHR(1); bin[2] := CHR(2);
+  bin[3] := CHR(0); bin[4] := CHR(255);
+
+  IF (sqlite3_open_v2(":memory:", db,
+        SQLiteOpenReadWrite + SQLiteOpenCreate + SQLiteOpenMemory,
+        NIL) = SQLiteOk) AND (db # NIL) THEN
+    IF ExecSQL("CREATE TABLE b(id INTEGER PRIMARY KEY, data BLOB);") THEN
+      rc := sqlite3_prepare_v2(db,
+        "INSERT INTO b(data) VALUES (?);", -1, stmt, NIL);
+      ok := (rc = SQLiteOk) AND (stmt # NIL);
+
+      (* 5-byte binary value, including an embedded NUL.  ADR(bin[0])
+         skips the V3 array descriptor and passes the first data byte. *)
+      IF ok THEN
+        ok := sqlite3_bind_blob(stmt, 1, ADR(bin[0]), 5,
+                                SQLiteTransient()) = SQLiteOk
+      END;
+      IF ok THEN ok := sqlite3_step(stmt) = SQLiteDone END;
+      IF ok THEN ok := sqlite3_reset(stmt) = SQLiteOk END;
+      IF ok THEN ok := sqlite3_clear_bindings(stmt) = SQLiteOk END;
+
+      (* Legitimate zero-length BLOB, distinct from NULL. *)
+      IF ok THEN
+        ok := sqlite3_bind_blob(stmt, 1, ADR(bin[0]), 0,
+                                SQLiteTransient()) = SQLiteOk
+      END;
+      IF ok THEN ok := sqlite3_step(stmt) = SQLiteDone END;
+      IF ok THEN ok := sqlite3_reset(stmt) = SQLiteOk END;
+      IF ok THEN ok := sqlite3_clear_bindings(stmt) = SQLiteOk END;
+
+      (* Eight zero bytes for incremental BLOB I/O. *)
+      IF ok THEN ok := sqlite3_bind_zeroblob(stmt, 1, 8) = SQLiteOk END;
+      IF ok THEN ok := sqlite3_step(stmt) = SQLiteDone END;
+      FinishStmt;
+
+      IF ok THEN
+        ok := (sqlite3_changes(db) = 1)
+              AND (sqlite3_total_changes(db) = 3)
+              AND (sqlite3_last_insert_rowid(db) = 3)
+      END
+    END
+  END;
+
+  (* Read the binary BLOB column metadata. *)
+  IF ok THEN
+    rc := sqlite3_prepare_v2(db,
+      "SELECT data FROM b WHERE id = 1;", -1, stmt, NIL);
+    ok := (rc = SQLiteOk) AND (stmt # NIL);
+    IF ok THEN
+      ok := sqlite3_step(stmt) = SQLiteRow;
+      IF ok THEN
+        ok := (sqlite3_column_type(stmt, 0) = SQLiteBlob)
+              AND (sqlite3_column_bytes(stmt, 0) = 5);
+        p := sqlite3_column_blob(stmt, 0);
+        IF p = NIL THEN ok := FALSE END;
+        IF sqlite3_step(stmt) # SQLiteDone THEN ok := FALSE END
+      END
+    END;
+    FinishStmt
+  END;
+
+  (* Verify the bound bytes through a read-only incremental BLOB. *)
+  IF ok THEN
+    rc := sqlite3_blob_open(db, "main", "b", "data", 1,
+                            SQLiteOpenReadOnly, blob);
+    ok := (rc = SQLiteOk) AND (blob # NIL)
+  END;
+  IF ok THEN ok := sqlite3_blob_bytes(blob) = 5 END;
+  IF ok THEN
+    ok := sqlite3_blob_read(blob, ADR(out[0]), 5, 0) = SQLiteOk
+  END;
+  IF ok THEN ok := SameBytes(bin, out, 5) END;
+  IF blob # NIL THEN
+    IF sqlite3_blob_close(blob) # SQLiteOk THEN ok := FALSE END;
+    blob := NIL
+  END;
+
+  (* A zero-length BLOB reports SQLiteBlob with zero bytes. *)
+  IF ok THEN
+    rc := sqlite3_prepare_v2(db,
+      "SELECT data FROM b WHERE id = 2;", -1, stmt, NIL);
+    ok := (rc = SQLiteOk) AND (stmt # NIL);
+    IF ok THEN
+      ok := sqlite3_step(stmt) = SQLiteRow;
+      IF ok THEN
+        ok := (sqlite3_column_type(stmt, 0) = SQLiteBlob)
+              AND (sqlite3_column_bytes(stmt, 0) = 0);
+        IF sqlite3_step(stmt) # SQLiteDone THEN ok := FALSE END
+      END
+    END;
+    FinishStmt
+  END;
+
+  (* Incremental BLOB I/O on the zeroblob row. *)
+  IF ok THEN
+    rc := sqlite3_blob_open(db, "main", "b", "data", 3,
+                            SQLiteOpenReadWrite, blob);
+    ok := (rc = SQLiteOk) AND (blob # NIL)
+  END;
+  IF ok THEN ok := sqlite3_blob_bytes(blob) = 8 END;
+  IF ok THEN
+    ok := sqlite3_blob_write(blob, ADR(bin[0]), 5, 0) = SQLiteOk
+  END;
+  IF ok THEN
+    ok := sqlite3_blob_read(blob, ADR(out[0]), 5, 0) = SQLiteOk
+  END;
+  IF ok THEN ok := SameBytes(bin, out, 5) END;
+  IF ok THEN ok := sqlite3_blob_reopen(blob, 3) = SQLiteOk END;
+  IF blob # NIL THEN
+    IF sqlite3_blob_close(blob) # SQLiteOk THEN ok := FALSE END;
+    blob := NIL
+  END;
+
+  (* The write survives closing the incremental BLOB handle. *)
+  IF ok THEN
+    rc := sqlite3_prepare_v2(db,
+      "SELECT data FROM b WHERE id = 3;", -1, stmt, NIL);
+    ok := (rc = SQLiteOk) AND (stmt # NIL);
+    IF ok THEN
+      ok := sqlite3_step(stmt) = SQLiteRow;
+      IF ok THEN
+        ok := (sqlite3_column_type(stmt, 0) = SQLiteBlob)
+              AND (sqlite3_column_bytes(stmt, 0) = 8);
+        IF sqlite3_step(stmt) # SQLiteDone THEN ok := FALSE END
+      END
+    END;
+    FinishStmt
+  END;
+
+  FinishStmt;
+  IF blob # NIL THEN
+    IF sqlite3_blob_close(blob) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF db # NIL THEN
+    IF sqlite3_close_v2(db) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF ok THEN ExitCode := 42 END
+END SQLiteBlob.

+ 106 - 0
bindings/sqlite3/tests/SQLiteConnection.mod

@@ -0,0 +1,106 @@
+MODULE SQLiteConnection;
+(* Connection flags, close_v2, changes and last_insert_rowid. *)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM SQLite IMPORT
+  SQLiteOk,
+  SQLiteOpenReadOnly, SQLiteOpenReadWrite, SQLiteOpenCreate,
+  SQLiteOpenMemory, SQLiteReadOnly,
+  sqlite3_open_v2, sqlite3_exec, sqlite3_close_v2,
+  sqlite3_prepare_v2, sqlite3_finalize,
+  sqlite3_changes, sqlite3_total_changes, sqlite3_last_insert_rowid,
+  sqlite3_free;
+
+VAR
+  ExitCode : INTEGER;
+  db, stmt, errmsg : ADDRESS;
+  rc : INTEGER;
+  ok : BOOLEAN;
+
+PROCEDURE ExecSQL (sql : ARRAY OF CHAR) : BOOLEAN;
+VAR
+  errmsg : ADDRESS;
+  good : BOOLEAN;
+BEGIN
+  errmsg := NIL;
+  good := sqlite3_exec(db, sql, NIL, NIL, errmsg) = SQLiteOk;
+  IF errmsg # NIL THEN
+    sqlite3_free(errmsg)
+  END;
+  RETURN good
+END ExecSQL;
+
+BEGIN
+  ExitCode := 1;
+  db := NIL; stmt := NIL;
+  ok := FALSE;
+
+  IF (sqlite3_open_v2(":memory:", db,
+        SQLiteOpenReadWrite + SQLiteOpenCreate + SQLiteOpenMemory,
+        NIL) = SQLiteOk) AND (db # NIL) THEN
+    IF ExecSQL("CREATE TABLE items(id INTEGER PRIMARY KEY, v TEXT);") THEN
+      ok := TRUE
+    END;
+
+    IF ok THEN
+      ok := ExecSQL("INSERT INTO items(v) VALUES ('a');")
+            AND (sqlite3_changes(db) = 1)
+            AND (sqlite3_total_changes(db) = 1)
+            AND (sqlite3_last_insert_rowid(db) = 1)
+    END;
+
+    IF ok THEN
+      ok := ExecSQL("INSERT INTO items(v) VALUES ('b');")
+            AND (sqlite3_changes(db) = 1)
+            AND (sqlite3_total_changes(db) = 2)
+            AND (sqlite3_last_insert_rowid(db) = 2)
+    END;
+
+    IF ok THEN
+      ok := ExecSQL("UPDATE items SET v = v || '!';")
+            AND (sqlite3_changes(db) = 2)
+            AND (sqlite3_total_changes(db) = 4)
+            AND (sqlite3_last_insert_rowid(db) = 2)
+    END;
+
+    IF ok THEN
+      ok := ExecSQL("DELETE FROM items WHERE id = 1;")
+            AND (sqlite3_changes(db) = 1)
+            AND (sqlite3_total_changes(db) = 5)
+    END;
+
+    (* Leave a statement open across close_v2 to exercise deferred close. *)
+    IF ok THEN
+      rc := sqlite3_prepare_v2(db, "SELECT 1;", -1, stmt, NIL);
+      ok := (rc = SQLiteOk) AND (stmt # NIL)
+    END;
+    IF ok THEN
+      ok := sqlite3_close_v2(db) = SQLiteOk
+    END;
+    IF ok THEN
+      ok := sqlite3_finalize(stmt) = SQLiteOk
+    END;
+    stmt := NIL;
+    db := NIL
+  END;
+
+  (* A read-only connection must reject writes. *)
+  IF ok THEN
+    IF sqlite3_open_v2(":memory:", db, SQLiteOpenReadOnly,
+                       NIL) = SQLiteOk THEN
+      errmsg := NIL;
+      ok := sqlite3_exec(db, "CREATE TABLE ro(x INTEGER);",
+                         NIL, NIL, errmsg) = SQLiteReadOnly;
+      IF errmsg # NIL THEN
+        sqlite3_free(errmsg);
+        errmsg := NIL
+      END;
+      IF sqlite3_close_v2(db) # SQLiteOk THEN ok := FALSE END;
+      db := NIL
+    ELSE
+      ok := FALSE
+    END
+  END;
+
+  IF ok THEN ExitCode := 42 END
+END SQLiteConnection.