Explorar el Código

bindings: add SQLite statement introspection

Eric Streit hace 1 día
padre
commit
56e4c2b3b0

+ 19 - 2
bindings/sqlite3/README.md

@@ -28,6 +28,12 @@ sqlite3_bind_parameter_count
 sqlite3_bind_parameter_name
 sqlite3_bind_parameter_index
 sqlite3_step
+sqlite3_sql
+sqlite3_expanded_sql
+sqlite3_stmt_readonly
+sqlite3_stmt_busy
+sqlite3_next_stmt
+sqlite3_stmt_status
 sqlite3_reset
 sqlite3_clear_bindings
 sqlite3_finalize
@@ -57,6 +63,14 @@ sqlite3_errcode
 sqlite3_extended_errcode
 sqlite3_extended_result_codes
 sqlite3_free
+sqlite3_libversion
+sqlite3_libversion_number
+sqlite3_sourceid
+sqlite3_threadsafe
+sqlite3_error_offset
+sqlite3_get_autocommit
+sqlite3_complete
+sqlite3_limit
 
 (* incremental BLOB I/O *)
 sqlite3_blob_open
@@ -119,6 +133,7 @@ Hook-related constants include `SQLiteOp*`, `SQLiteAuth*` and
 - `SQLiteSmoke.mod` - open / exec / close
 - `SQLitePrepared.mod` - prepared insert and select
 - `SQLiteParams.mod` - parameter names/count/index, named parameters, unbound NULL
+- `SQLiteIntrospection.mod` - sql/expanded_sql, statement state, version, limits
 - `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
@@ -181,6 +196,8 @@ without `libsqlite3-dev`.
   callback to unregister.
 - `sqlite3_set_authorizer` must return `SQLiteAuthOk`,
   `SQLiteAuthDeny` or `SQLiteAuthIgnore`.
+- `sqlite3_sql` returns borrowed SQL text; `sqlite3_expanded_sql`
+  returns allocated text and must be released with `sqlite3_free`.
 - When extended result codes are enabled, `sqlite3_exec` and
   `sqlite3_errcode` can report an extended code such as
   `SQLiteConstraintPrimaryKey` (1555) rather than only
@@ -188,5 +205,5 @@ without `libsqlite3-dev`.
 
 ## Still to add
 
-Backup, extension loading, WAL/checkpoint helpers, connection
-configuration and statement metadata.
+Backup, extension loading, WAL/checkpoint helpers and connection
+configuration.

+ 69 - 0
bindings/sqlite3/SQLite.def

@@ -28,6 +28,17 @@ EXPORT UNQUALIFIED
   SQLiteAuthOk, SQLiteAuthDeny, SQLiteAuthIgnore,
   SQLiteAuthDelete, SQLiteAuthSelect,
   SQLiteTraceStmt, SQLiteTraceProfile, SQLiteTraceRow, SQLiteTraceClose,
+  SQLiteLimitLength, SQLiteLimitSqlLength, SQLiteLimitColumn,
+  SQLiteLimitExprDepth, SQLiteLimitCompoundSelect, SQLiteLimitVdbeOp,
+  SQLiteLimitFunctionArg, SQLiteLimitAttached,
+  SQLiteLimitLikePatternLength, SQLiteLimitVariableNumber,
+  SQLiteLimitTriggerDepth, SQLiteLimitWorkerThreads,
+  SQLiteLimitParserDepth, SQLiteLimitSchema, SQLiteLimitTriggerSteps,
+  SQLiteStmtStatusFullscanStep, SQLiteStmtStatusSort,
+  SQLiteStmtStatusAutoindex, SQLiteStmtStatusVmStep,
+  SQLiteStmtStatusReprepare, SQLiteStmtStatusRun,
+  SQLiteStmtStatusFilterMiss, SQLiteStmtStatusFilterHit,
+  SQLiteStmtStatusMemused,
   sqlite3_open,
   sqlite3_open_v2,
   sqlite3_exec,
@@ -45,6 +56,12 @@ EXPORT UNQUALIFIED
   sqlite3_bind_parameter_name,
   sqlite3_bind_parameter_index,
   sqlite3_step,
+  sqlite3_sql,
+  sqlite3_expanded_sql,
+  sqlite3_stmt_readonly,
+  sqlite3_stmt_busy,
+  sqlite3_next_stmt,
+  sqlite3_stmt_status,
   sqlite3_reset,
   sqlite3_clear_bindings,
   sqlite3_column_count,
@@ -96,6 +113,14 @@ EXPORT UNQUALIFIED
   sqlite3_rollback_hook,
   sqlite3_set_authorizer,
   sqlite3_db_handle,
+  sqlite3_libversion,
+  sqlite3_libversion_number,
+  sqlite3_sourceid,
+  sqlite3_threadsafe,
+  sqlite3_error_offset,
+  sqlite3_get_autocommit,
+  sqlite3_complete,
+  sqlite3_limit,
   sqlite3_errmsg,
   sqlite3_errstr,
   sqlite3_errcode,
@@ -180,6 +205,32 @@ CONST
   SQLiteTraceRow = 4;
   SQLiteTraceClose = 8;
 
+  SQLiteLimitLength = 0;
+  SQLiteLimitSqlLength = 1;
+  SQLiteLimitColumn = 2;
+  SQLiteLimitExprDepth = 3;
+  SQLiteLimitCompoundSelect = 4;
+  SQLiteLimitVdbeOp = 5;
+  SQLiteLimitFunctionArg = 6;
+  SQLiteLimitAttached = 7;
+  SQLiteLimitLikePatternLength = 8;
+  SQLiteLimitVariableNumber = 9;
+  SQLiteLimitTriggerDepth = 10;
+  SQLiteLimitWorkerThreads = 11;
+  SQLiteLimitParserDepth = 12;
+  SQLiteLimitSchema = 13;
+  SQLiteLimitTriggerSteps = 14;
+
+  SQLiteStmtStatusFullscanStep = 1;
+  SQLiteStmtStatusSort = 2;
+  SQLiteStmtStatusAutoindex = 3;
+  SQLiteStmtStatusVmStep = 4;
+  SQLiteStmtStatusReprepare = 5;
+  SQLiteStmtStatusRun = 6;
+  SQLiteStmtStatusFilterMiss = 7;
+  SQLiteStmtStatusFilterHit = 8;
+  SQLiteStmtStatusMemused = 99;
+
 PROCEDURE sqlite3_open (filename : ARRAY OF CHAR;
                         VAR db : ADDRESS) : INTEGER;
 PROCEDURE sqlite3_open_v2 (filename : ARRAY OF CHAR;
@@ -228,6 +279,14 @@ PROCEDURE sqlite3_bind_parameter_name (stmt : ADDRESS;
 PROCEDURE sqlite3_bind_parameter_index (stmt : ADDRESS;
                                         name : ARRAY OF CHAR) : INTEGER;
 PROCEDURE sqlite3_step (stmt : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_sql (stmt : ADDRESS) : CString;
+PROCEDURE sqlite3_expanded_sql (stmt : ADDRESS) : CString;
+PROCEDURE sqlite3_stmt_readonly (stmt : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_stmt_busy (stmt : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_next_stmt (db, stmt : ADDRESS) : ADDRESS;
+PROCEDURE sqlite3_stmt_status (stmt : ADDRESS;
+                               op : INTEGER;
+                               resetFlag : INTEGER) : INTEGER;
 PROCEDURE sqlite3_reset (stmt : ADDRESS) : INTEGER;
 PROCEDURE sqlite3_clear_bindings (stmt : ADDRESS) : INTEGER;
 
@@ -334,6 +393,16 @@ PROCEDURE sqlite3_rollback_hook (db : ADDRESS;
 PROCEDURE sqlite3_set_authorizer (db : ADDRESS;
                                   callback, arg : ADDRESS) : INTEGER;
 PROCEDURE sqlite3_db_handle (stmt : ADDRESS) : ADDRESS;
+PROCEDURE sqlite3_libversion () : CString;
+PROCEDURE sqlite3_libversion_number () : INTEGER;
+PROCEDURE sqlite3_sourceid () : CString;
+PROCEDURE sqlite3_threadsafe () : INTEGER;
+PROCEDURE sqlite3_error_offset (db : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_get_autocommit (db : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_complete (sql : ARRAY OF CHAR) : INTEGER;
+PROCEDURE sqlite3_limit (db : ADDRESS;
+                         id : INTEGER;
+                         newVal : INTEGER) : INTEGER;
 
 PROCEDURE sqlite3_errmsg (db : ADDRESS) : CString;
 PROCEDURE sqlite3_errstr (code : INTEGER) : CString;

+ 3 - 2
bindings/sqlite3/TEST-INVENTORY.md

@@ -3,8 +3,9 @@
 Date: 2026-10-26
 
 Status update 2026-10-27: parameter introspection
-(`sqlite3_bind_parameter_count/name/index`) is now implemented and
-covered by `tests/SQLiteParams.mod`.
+(`sqlite3_bind_parameter_count/name/index`) and statement
+metadata/diagnostics are implemented and covered by
+`tests/SQLiteParams.mod` and `tests/SQLiteIntrospection.mod`.
 
 This note records which existing tests are useful for hardening the V3
 SQLite binding under `bindings/sqlite3`, and which APIs they would drive.

+ 1 - 0
bindings/sqlite3/run.sh

@@ -39,6 +39,7 @@ build_and_run() {
 build_and_run SQLiteSmoke
 build_and_run SQLitePrepared
 build_and_run SQLiteParams
+build_and_run SQLiteIntrospection
 build_and_run SQLiteScalar
 build_and_run SQLiteErrors
 build_and_run SQLiteBlob

+ 194 - 0
bindings/sqlite3/tests/SQLiteIntrospection.mod

@@ -0,0 +1,194 @@
+MODULE SQLiteIntrospection;
+(* Statement metadata and library diagnostics:
+   sql, expanded_sql, readonly, busy, next_stmt, stmt_status,
+   version/sourceid, error_offset, autocommit, complete and limits. *)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM CStr IMPORT CString, Copy;
+FROM SQLite IMPORT
+  SQLiteOk, SQLiteRow, SQLiteDone,
+  SQLiteOpenReadWrite, SQLiteOpenCreate, SQLiteOpenMemory,
+  SQLiteLimitLikePatternLength, SQLiteStmtStatusVmStep,
+  sqlite3_open_v2, sqlite3_exec, sqlite3_close_v2,
+  sqlite3_prepare_v2, sqlite3_step, sqlite3_finalize,
+  sqlite3_bind_int, sqlite3_sql, sqlite3_expanded_sql,
+  sqlite3_stmt_readonly, sqlite3_stmt_busy, sqlite3_next_stmt,
+  sqlite3_stmt_status, sqlite3_libversion,
+  sqlite3_libversion_number, sqlite3_sourceid, sqlite3_threadsafe,
+  sqlite3_error_offset, sqlite3_get_autocommit, sqlite3_complete,
+  sqlite3_limit, sqlite3_free;
+
+VAR
+  ExitCode : INTEGER;
+  db, stmt, s1, s2, p, q : ADDRESS;
+  rc : INTEGER;
+  ok : BOOLEAN;
+
+PROCEDURE SameText (a, b : ARRAY OF CHAR) : BOOLEAN;
+VAR
+  i : INTEGER;
+BEGIN
+  i := 0;
+  WHILE (a[i] # CHR(0)) AND (a[i] = b[i]) DO
+    i := i + 1
+  END;
+  RETURN (a[i] = CHR(0)) AND (b[i] = CHR(0))
+END SameText;
+
+PROCEDURE TextIs (s : CString; want : ARRAY OF CHAR) : BOOLEAN;
+VAR
+  buf : ARRAY [0..127] OF CHAR;
+BEGIN
+  IF s = NIL THEN RETURN FALSE END;
+  IF Copy(s, buf) = 0 THEN RETURN FALSE END;
+  RETURN SameText(buf, want)
+END TextIs;
+
+PROCEDURE TextNonEmpty (s : CString) : BOOLEAN;
+VAR
+  buf : ARRAY [0..127] OF CHAR;
+BEGIN
+  IF s = NIL THEN RETURN FALSE END;
+  RETURN Copy(s, buf) > 0
+END TextNonEmpty;
+
+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 Finish (VAR s : ADDRESS) : BOOLEAN;
+VAR
+  good : BOOLEAN;
+BEGIN
+  good := TRUE;
+  IF s # NIL THEN
+    IF sqlite3_finalize(s) # SQLiteOk THEN good := FALSE END;
+    s := NIL
+  END;
+  RETURN good
+END Finish;
+
+BEGIN
+  ExitCode := 1;
+  db := NIL; stmt := NIL; s1 := NIL; s2 := NIL;
+  p := NIL; q := NIL; ok := FALSE;
+
+  IF (sqlite3_open_v2(":memory:", db,
+        SQLiteOpenReadWrite + SQLiteOpenCreate + SQLiteOpenMemory,
+        NIL) = SQLiteOk) AND (db # NIL) THEN
+    ok := (sqlite3_libversion_number() > 0)
+          AND TextNonEmpty(sqlite3_libversion())
+          AND TextNonEmpty(sqlite3_sourceid())
+          AND (sqlite3_threadsafe() > 0);
+
+    IF ok THEN
+      ok := (sqlite3_complete("SELECT 1;") = 1)
+            AND (sqlite3_complete("SELECT 1") = 0)
+    END;
+
+    IF ok THEN
+      ok := (sqlite3_get_autocommit(db) = 1)
+            AND ExecSQL("BEGIN;")
+            AND (sqlite3_get_autocommit(db) = 0)
+            AND ExecSQL("COMMIT;")
+            AND (sqlite3_get_autocommit(db) = 1)
+    END;
+
+    IF ok THEN
+      rc := sqlite3_limit(db, SQLiteLimitLikePatternLength, -1);
+      ok := (rc > 0)
+            AND (sqlite3_limit(db, SQLiteLimitLikePatternLength, 1000) = rc)
+            AND (sqlite3_limit(db, SQLiteLimitLikePatternLength, -1) = 1000)
+            AND (sqlite3_limit(db, SQLiteLimitLikePatternLength, rc) = 1000)
+    END;
+
+    IF ok THEN ok := ExecSQL("CREATE TABLE t(x INTEGER);") END;
+
+    (* Original SQL, expanded SQL, readonly/busy and VM counters. *)
+    IF ok THEN
+      stmt := NIL;
+      rc := sqlite3_prepare_v2(db, "SELECT ?;", -1, stmt, NIL);
+      ok := (rc = SQLiteOk) AND (stmt # NIL)
+            AND (sqlite3_stmt_readonly(stmt) = 1)
+            AND TextIs(sqlite3_sql(stmt), "SELECT ?;")
+            AND (sqlite3_stmt_busy(stmt) = 0);
+      IF ok THEN
+        ok := sqlite3_bind_int(stmt, 1, 42) = SQLiteOk
+      END;
+      IF ok THEN
+        p := sqlite3_expanded_sql(stmt);
+        ok := (p # NIL) AND TextIs(p, "SELECT 42;");
+        IF p # NIL THEN
+          sqlite3_free(p);
+          p := NIL
+        END
+      END;
+      IF ok THEN ok := sqlite3_step(stmt) = SQLiteRow END;
+      IF ok THEN ok := sqlite3_stmt_busy(stmt) = 1 END;
+      IF ok THEN
+        ok := sqlite3_stmt_status(stmt, SQLiteStmtStatusVmStep, 0) > 0
+      END;
+      IF ok THEN ok := sqlite3_step(stmt) = SQLiteDone END;
+      IF ok THEN ok := sqlite3_stmt_busy(stmt) = 0 END;
+      IF ok THEN
+        ok := (sqlite3_stmt_status(stmt, SQLiteStmtStatusVmStep, 1) > 0)
+              AND (sqlite3_stmt_status(stmt, SQLiteStmtStatusVmStep, 0) = 0)
+      END;
+      IF NOT Finish(stmt) THEN ok := FALSE END
+    END;
+
+    (* A write statement is not readonly. *)
+    IF ok THEN
+      stmt := NIL;
+      rc := sqlite3_prepare_v2(db, "INSERT INTO t(x) VALUES(1);",
+                               -1, stmt, NIL);
+      ok := (rc = SQLiteOk) AND (stmt # NIL)
+            AND (sqlite3_stmt_readonly(stmt) = 0);
+      IF NOT Finish(stmt) THEN ok := FALSE END
+    END;
+
+    (* Syntax errors expose a byte offset. *)
+    IF ok THEN
+      stmt := NIL;
+      rc := sqlite3_prepare_v2(db, "SELECT * FROM;", -1, stmt, NIL);
+      ok := (rc # SQLiteOk) AND (stmt = NIL)
+            AND (sqlite3_error_offset(db) >= 0);
+      IF NOT Finish(stmt) THEN ok := FALSE END
+    END;
+
+    (* Statement iteration: exactly the two prepared statements. *)
+    IF ok THEN
+      rc := sqlite3_prepare_v2(db, "SELECT 1;", -1, s1, NIL);
+      ok := (rc = SQLiteOk) AND (s1 # NIL);
+      IF ok THEN
+        rc := sqlite3_prepare_v2(db, "SELECT 2;", -1, s2, NIL);
+        ok := (rc = SQLiteOk) AND (s2 # NIL)
+      END;
+      IF ok THEN
+        p := sqlite3_next_stmt(db, NIL);
+        q := sqlite3_next_stmt(db, p);
+        ok := (p # NIL) AND (q # NIL) AND (p # q)
+              AND (sqlite3_next_stmt(db, q) = NIL)
+      END;
+      IF NOT Finish(s1) THEN ok := FALSE END;
+      IF NOT Finish(s2) THEN ok := FALSE END
+    END
+  END;
+
+  IF NOT Finish(stmt) THEN ok := FALSE END;
+  IF NOT Finish(s1) THEN ok := FALSE END;
+  IF NOT Finish(s2) 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 SQLiteIntrospection.