Bladeren bron

bindings: harden SQLite BLOB and constraint errors

Eric Streit 1 dag geleden
bovenliggende
commit
06b064441d

+ 8 - 4
bindings/sqlite3/README.md

@@ -124,7 +124,8 @@ storage classes (`SQLiteInteger`, `SQLiteFloat`, `SQLiteText`,
 `SQLiteOpen*` flags used by `sqlite3_open_v2` and `sqlite3_blob_open`,
 and `SQLiteUtf*` / `SQLiteDeterministic` for function registration.
 Hook-related constants include `SQLiteOp*`, `SQLiteAuth*` and
-`SQLiteTrace*`.
+`SQLiteTrace*`.  Extended constraint codes include `SQLiteConstraintCheck`,
+`NotNull`, `ForeignKey`, `Trigger`, `Rowid` and `Datatype`.
 
 ## Tests and showcase
 
@@ -137,6 +138,8 @@ Hook-related constants include `SQLiteOp*`, `SQLiteAuth*` and
 - `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
+- `SQLiteBlobErrors.mod` - read-only/bounds/invalid/expired BLOB handles
+- `SQLiteConstraints.mod` - unique/not-null/PK/check/foreign-key extended codes
 - `SQLiteConnection.mod` - open_v2, close_v2, changes/rowid and deferred close
 - `SQLiteCallback.mod` - exec row callbacks and user-defined scalar functions
 - `SQLiteHooks.mod` - busy/progress/trace/update/commit/rollback/authorizer hooks
@@ -180,9 +183,10 @@ without `libsqlite3-dev`.
   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`.
+- `sqlite3_blob_open` uses `SQLiteBlobReadOnly` (0) or
+  `SQLiteBlobReadWrite` (1).  These are not the `SQLiteOpen*` flags.
+  A BLOB handle holds a read/write lock on its row until
+  `sqlite3_blob_close`.
 - SQLite callbacks must be top-level V3 procedures: they are C-callable,
   while nested procedures still carry a hidden frame link.
 - `sqlite3_exec` callbacks receive `char **argv` and `char **colNames`

+ 19 - 0
bindings/sqlite3/SQLite.def

@@ -18,6 +18,12 @@ EXPORT UNQUALIFIED
   SQLiteNotADb, SQLiteNotice, SQLiteWarning, SQLiteRow, SQLiteDone,
   SQLiteInteger, SQLiteFloat, SQLiteText, SQLiteBlob, SQLiteNull,
   SQLiteConstraintPrimaryKey, SQLiteConstraintUnique,
+  SQLiteConstraintCheck, SQLiteConstraintCommitHook,
+  SQLiteConstraintForeignKey, SQLiteConstraintFunction,
+  SQLiteConstraintNotNull, SQLiteConstraintTrigger,
+  SQLiteConstraintVtab, SQLiteConstraintRowid,
+  SQLiteConstraintPinned, SQLiteConstraintDatatype,
+  SQLiteBlobReadOnly, SQLiteBlobReadWrite,
   SQLiteOpenReadOnly, SQLiteOpenReadWrite, SQLiteOpenCreate,
   SQLiteOpenURI, SQLiteOpenMemory, SQLiteOpenNoMutex,
   SQLiteOpenFullMutex, SQLiteOpenSharedCache, SQLiteOpenPrivateCache,
@@ -169,8 +175,21 @@ CONST
   SQLiteBlob = 4;
   SQLiteNull = 5;
 
+  SQLiteConstraintCheck = 275;
+  SQLiteConstraintCommitHook = 531;
+  SQLiteConstraintForeignKey = 787;
+  SQLiteConstraintFunction = 1043;
+  SQLiteConstraintNotNull = 1299;
   SQLiteConstraintPrimaryKey = 1555;
+  SQLiteConstraintTrigger = 1811;
   SQLiteConstraintUnique = 2067;
+  SQLiteConstraintVtab = 2323;
+  SQLiteConstraintRowid = 2579;
+  SQLiteConstraintPinned = 2835;
+  SQLiteConstraintDatatype = 3091;
+
+  SQLiteBlobReadOnly = 0;
+  SQLiteBlobReadWrite = 1;
 
   SQLiteOpenReadOnly = 1;
   SQLiteOpenReadWrite = 2;

+ 8 - 6
bindings/sqlite3/TEST-INVENTORY.md

@@ -3,9 +3,11 @@
 Date: 2026-10-26
 
 Status update 2026-10-27: parameter introspection
-(`sqlite3_bind_parameter_count/name/index`) and statement
-metadata/diagnostics are implemented and covered by
-`tests/SQLiteParams.mod` and `tests/SQLiteIntrospection.mod`.
+(`sqlite3_bind_parameter_count/name/index`), statement
+metadata/diagnostics, BLOB error handling and extended constraint
+codes are implemented.  Covered by `tests/SQLiteParams.mod`,
+`tests/SQLiteIntrospection.mod`, `tests/SQLiteBlobErrors.mod` and
+`tests/SQLiteConstraints.mod`.
 
 This note records which existing tests are useful for hardening the V3
 SQLite binding under `bindings/sqlite3`, and which APIs they would drive.
@@ -40,8 +42,8 @@ harness.
 |---|---|---|
 | `test_prepare.mod` | `bind_parameter_count/index`, named parameters, 64-bit bind, scalar round trip | Implemented in `SQLiteParams.mod` |
 | `test_func.mod` | scalar/aggregate UDFs, auxdata, numeric type, `result_value`, 64-bit text/blob, zeroblob, error paths | Missing several callback/result APIs |
-| `test_blob.mod` | read-only BLOB write error, reopen, backup API, backup to file | BLOB happy path covered; read-only/error and backup not covered |
-| `test_codes.mod` | extended constraint codes (unique/not-null/PK/FK), `get_table` | Only PK/unique constants; missing not-null/FK constants and `get_table` |
+| `test_blob.mod` | read-only BLOB write error, reopen, backup API, backup to file | Read-only/bounds/expiry covered; backup remains |
+| `test_codes.mod` | extended constraint codes (unique/not-null/PK/FK), `get_table` | Extended codes implemented; `get_table` remains |
 | `test_exec.mod` | exec, change counters, rowid, error capture | Already covered by current V3 tests |
 | `test_open.mod` | `open` / `open_v2` / close and `errcode` | Already covered by current V3 tests |
 | `test_version.mod` | `sqlite3_libversion*` | Missing version APIs |
@@ -53,7 +55,7 @@ harness.
 | File | Interesting cases | What it drives |
 |---|---|---|
 | `bind.test` | `bind-1.*` named params/unbound NULL/reset; `bind-9.*` high parameter numbers; `bind-10.*` parameter index/name; 32/64/double/NULL/text/UTF-16/clear sections | Basic coverage added; high parameter numbers and UTF-16 remain |
-| `incrblob.test` | sections `incrblob-3.*` read-only write, `4.*` bad row/table/column/type opens, `7.*` handle invalidation | BLOB error paths and boundary behavior |
+| `incrblob.test` | sections `incrblob-3.*` read-only write, `4.*` bad row/table/column/type opens, `7.*` handle invalidation | Covered by `SQLiteBlobErrors.mod` |
 | `types.test`, `types2.test`, `types3.test` | storage classes, affinity, integer/real/text conversions | `column_type`, numeric columns, bound values |
 | `null.test`, `nulls1.test`, `nulls2.test` | NULL in expressions, comparisons, result types | `bind_null`, `column_type`, `value_type` |
 | `auth.test` | deny at prepare, `SQLITE_IGNORE` for reads, action arguments, authorizer replacement | authorizer arguments and return semantics |

+ 2 - 0
bindings/sqlite3/run.sh

@@ -43,6 +43,8 @@ build_and_run SQLiteIntrospection
 build_and_run SQLiteScalar
 build_and_run SQLiteErrors
 build_and_run SQLiteBlob
+build_and_run SQLiteBlobErrors
+build_and_run SQLiteConstraints
 build_and_run SQLiteConnection
 build_and_run SQLiteCallback
 build_and_run SQLiteHooks

+ 4 - 4
bindings/sqlite3/tests/SQLiteBlob.mod

@@ -6,8 +6,8 @@ MODULE SQLiteBlob;
 FROM SYSTEM IMPORT ADDRESS, ADR;
 FROM SQLite IMPORT
   SQLiteOk, SQLiteRow, SQLiteDone, SQLiteBlob,
-  SQLiteOpenReadOnly, SQLiteOpenReadWrite, SQLiteOpenCreate,
-  SQLiteOpenMemory,
+  SQLiteBlobReadOnly, SQLiteBlobReadWrite,
+  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,
@@ -125,7 +125,7 @@ BEGIN
   (* Verify the bound bytes through a read-only incremental BLOB. *)
   IF ok THEN
     rc := sqlite3_blob_open(db, "main", "b", "data", 1,
-                            SQLiteOpenReadOnly, blob);
+                            SQLiteBlobReadOnly, blob);
     ok := (rc = SQLiteOk) AND (blob # NIL)
   END;
   IF ok THEN ok := sqlite3_blob_bytes(blob) = 5 END;
@@ -157,7 +157,7 @@ BEGIN
   (* Incremental BLOB I/O on the zeroblob row. *)
   IF ok THEN
     rc := sqlite3_blob_open(db, "main", "b", "data", 3,
-                            SQLiteOpenReadWrite, blob);
+                            SQLiteBlobReadWrite, blob);
     ok := (rc = SQLiteOk) AND (blob # NIL)
   END;
   IF ok THEN ok := sqlite3_blob_bytes(blob) = 8 END;

+ 133 - 0
bindings/sqlite3/tests/SQLiteBlobErrors.mod

@@ -0,0 +1,133 @@
+MODULE SQLiteBlobErrors;
+(* BLOB error paths:
+   read-only writes, bounds, invalid opens and expired handles. *)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM SQLite IMPORT
+  SQLiteOk, SQLiteError, SQLiteReadOnly, SQLiteAbort,
+  SQLiteBlobReadOnly, SQLiteBlobReadWrite,
+  SQLiteOpenReadWrite, SQLiteOpenCreate, SQLiteOpenMemory,
+  sqlite3_open_v2, sqlite3_exec, sqlite3_close_v2,
+  sqlite3_blob_open, sqlite3_blob_close, sqlite3_blob_read,
+  sqlite3_blob_write, sqlite3_free;
+
+VAR
+  ExitCode : INTEGER;
+  db, blob : ADDRESS;
+  rc : INTEGER;
+  ok : BOOLEAN;
+  buf : ARRAY [0..7] OF CHAR;
+
+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 CloseBlob (VAR b : ADDRESS; want : INTEGER) : BOOLEAN;
+VAR
+  got : INTEGER;
+BEGIN
+  IF b = NIL THEN RETURN FALSE END;
+  got := sqlite3_blob_close(b);
+  b := NIL;
+  RETURN got = want
+END CloseBlob;
+
+BEGIN
+  ExitCode := 1;
+  db := NIL; blob := NIL; ok := FALSE;
+  buf[0] := 'X'; buf[1] := 'Y';
+
+  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);")
+       AND ExecSQL("INSERT INTO b(data) VALUES(zeroblob(4));")
+       AND ExecSQL("INSERT INTO b(data) VALUES(zeroblob(4));") THEN
+      ok := TRUE
+    END
+  END;
+
+  (* Flags 0 = read-only; a write must fail and remain pending at close. *)
+  IF ok THEN
+    rc := sqlite3_blob_open(db, "main", "b", "data", 1,
+                            SQLiteBlobReadOnly, blob);
+    ok := (rc = SQLiteOk) AND (blob # NIL)
+  END;
+  IF ok THEN
+    ok := sqlite3_blob_write(blob, ADR(buf[0]), 1, 0) = SQLiteReadOnly
+  END;
+  IF NOT CloseBlob(blob, SQLiteReadOnly) THEN ok := FALSE END;
+
+  (* Bounds checks on a read-write handle. *)
+  IF ok THEN
+    rc := sqlite3_blob_open(db, "main", "b", "data", 1,
+                            SQLiteBlobReadWrite, blob);
+    ok := (rc = SQLiteOk) AND (blob # NIL)
+  END;
+  IF ok THEN
+    ok := sqlite3_blob_read(blob, ADR(buf[0]), 2, 3) = SQLiteError
+  END;
+  IF ok THEN
+    ok := sqlite3_blob_write(blob, ADR(buf[0]), 2, 3) = SQLiteError
+  END;
+  IF ok THEN
+    ok := sqlite3_blob_read(blob, ADR(buf[0]), 1, -1) = SQLiteError
+  END;
+  IF ok THEN
+    ok := sqlite3_blob_write(blob, ADR(buf[0]), 1, -1) = SQLiteError
+  END;
+  IF NOT CloseBlob(blob, SQLiteOk) THEN ok := FALSE END;
+
+  (* Invalid row/table/column and a non-BLOB column. *)
+  IF ok THEN
+    rc := sqlite3_blob_open(db, "main", "b", "data", 99,
+                            SQLiteBlobReadOnly, blob);
+    ok := (rc # SQLiteOk) AND (blob = NIL)
+  END;
+  IF ok THEN
+    rc := sqlite3_blob_open(db, "main", "b", "nope", 1,
+                            SQLiteBlobReadOnly, blob);
+    ok := (rc # SQLiteOk) AND (blob = NIL)
+  END;
+  IF ok THEN
+    rc := sqlite3_blob_open(db, "main", "nope", "data", 1,
+                            SQLiteBlobReadOnly, blob);
+    ok := (rc # SQLiteOk) AND (blob = NIL)
+  END;
+  IF ok THEN
+    rc := sqlite3_blob_open(db, "main", "b", "id", 1,
+                            SQLiteBlobReadOnly, blob);
+    ok := (rc # SQLiteOk) AND (blob = NIL)
+  END;
+
+  (* An UPDATE expires an open BLOB handle. *)
+  IF ok THEN
+    rc := sqlite3_blob_open(db, "main", "b", "data", 2,
+                            SQLiteBlobReadWrite, blob);
+    ok := (rc = SQLiteOk) AND (blob # NIL)
+  END;
+  IF ok THEN
+    ok := sqlite3_blob_read(blob, ADR(buf[0]), 1, 0) = SQLiteOk
+  END;
+  IF ok THEN
+    ok := ExecSQL("UPDATE b SET data = zeroblob(4) WHERE id = 2;")
+  END;
+  IF ok THEN
+    ok := sqlite3_blob_read(blob, ADR(buf[0]), 1, 0) = SQLiteAbort
+  END;
+  IF NOT CloseBlob(blob, SQLiteOk) THEN ok := FALSE END;
+
+  IF db # NIL THEN
+    IF sqlite3_close_v2(db) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF ok THEN ExitCode := 42 END
+END SQLiteBlobErrors.

+ 99 - 0
bindings/sqlite3/tests/SQLiteConstraints.mod

@@ -0,0 +1,99 @@
+MODULE SQLiteConstraints;
+(* Extended constraint result codes:
+   unique, not-null, primary-key, check and foreign-key failures. *)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM SQLite IMPORT
+  SQLiteOk, SQLiteConstraint,
+  SQLiteConstraintUnique, SQLiteConstraintNotNull,
+  SQLiteConstraintPrimaryKey, SQLiteConstraintCheck,
+  SQLiteConstraintForeignKey,
+  SQLiteOpenReadWrite, SQLiteOpenCreate, SQLiteOpenMemory,
+  sqlite3_open_v2, sqlite3_exec, sqlite3_close_v2,
+  sqlite3_extended_result_codes, sqlite3_extended_errcode,
+  sqlite3_free;
+
+VAR
+  ExitCode : INTEGER;
+  db : 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;
+
+PROCEDURE Expect (sql : ARRAY OF CHAR; code : INTEGER) : BOOLEAN;
+VAR
+  errmsg : ADDRESS;
+  got : INTEGER;
+  good : BOOLEAN;
+BEGIN
+  errmsg := NIL;
+  got := sqlite3_exec(db, sql, NIL, NIL, errmsg);
+  good := ((got = code) OR (got = SQLiteConstraint))
+          AND (sqlite3_extended_errcode(db) = code);
+  IF errmsg # NIL THEN
+    sqlite3_free(errmsg)
+  END;
+  RETURN good
+END Expect;
+
+BEGIN
+  ExitCode := 1;
+  db := NIL; ok := FALSE;
+
+  IF (sqlite3_open_v2(":memory:", db,
+        SQLiteOpenReadWrite + SQLiteOpenCreate + SQLiteOpenMemory,
+        NIL) = SQLiteOk) AND (db # NIL)
+     AND (sqlite3_extended_result_codes(db, 1) = SQLiteOk) THEN
+    ok := TRUE
+  END;
+
+  IF ok THEN
+    ok := ExecSQL("CREATE TABLE u(v TEXT UNIQUE);")
+          AND ExecSQL("INSERT INTO u VALUES('a');")
+          AND Expect("INSERT INTO u VALUES('a');",
+                     SQLiteConstraintUnique)
+  END;
+
+  IF ok THEN
+    ok := ExecSQL("CREATE TABLE n(v TEXT NOT NULL);")
+          AND Expect("INSERT INTO n VALUES(NULL);",
+                     SQLiteConstraintNotNull)
+  END;
+
+  IF ok THEN
+    ok := ExecSQL("CREATE TABLE p(id INTEGER PRIMARY KEY);")
+          AND ExecSQL("INSERT INTO p VALUES(1);")
+          AND Expect("INSERT INTO p VALUES(1);",
+                     SQLiteConstraintPrimaryKey)
+  END;
+
+  IF ok THEN
+    ok := ExecSQL("CREATE TABLE c(v INTEGER CHECK(v > 0));")
+          AND Expect("INSERT INTO c VALUES(0);",
+                     SQLiteConstraintCheck)
+  END;
+
+  IF ok THEN
+    ok := ExecSQL("PRAGMA foreign_keys=ON;")
+          AND ExecSQL("CREATE TABLE f(pid INTEGER REFERENCES p(id));")
+          AND Expect("INSERT INTO f VALUES(99);",
+                     SQLiteConstraintForeignKey)
+  END;
+
+  IF db # NIL THEN
+    IF sqlite3_close_v2(db) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF ok THEN ExitCode := 42 END
+END SQLiteConstraints.