Procházet zdrojové kódy

bindings: add SQLite connection hooks

Eric Streit před 1 dnem
rodič
revize
9dd2a80225

+ 22 - 3
bindings/sqlite3/README.md

@@ -83,6 +83,18 @@ sqlite3_result_error_code
 sqlite3_user_data
 sqlite3_context_db_handle
 
+(* connection hooks *)
+sqlite3_busy_timeout
+sqlite3_busy_handler
+sqlite3_interrupt
+sqlite3_progress_handler
+sqlite3_trace_v2
+sqlite3_update_hook
+sqlite3_commit_hook
+sqlite3_rollback_hook
+sqlite3_set_authorizer
+sqlite3_db_handle
+
 (* binding helpers *)
 SQLiteArrayAt
 SQLiteTransient
@@ -94,6 +106,8 @@ storage classes (`SQLiteInteger`, `SQLiteFloat`, `SQLiteText`,
 `SQLiteBlob`, `SQLiteNull`), a few extended constraints, the
 `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*`.
 
 ## Tests and showcase
 
@@ -106,6 +120,7 @@ and `SQLiteUtf*` / `SQLiteDeterministic` for function registration.
 - `SQLiteBlob.mod` - bind_blob, bind_zeroblob, column_blob and incremental BLOB I/O
 - `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
 - `showcase/SQLiteShowcase.mod` - prepared-statement walkthrough
 
 The showcase prints:
@@ -157,6 +172,11 @@ without `libsqlite3-dev`.
 - User-defined functions receive `sqlite3_value **argv` as `ADDRESS`;
   pass each `SQLiteArrayAt(argv, i)` result to `sqlite3_value_*` or
   `sqlite3_result_*`.
+- `sqlite3_update_hook` and `sqlite3_commit_hook` return the previous
+  hook pointer.  Assign the result to an `ADDRESS`; pass `NIL` as the
+  callback to unregister.
+- `sqlite3_set_authorizer` must return `SQLiteAuthOk`,
+  `SQLiteAuthDeny` or `SQLiteAuthIgnore`.
 - When extended result codes are enabled, `sqlite3_exec` and
   `sqlite3_errcode` can report an extended code such as
   `SQLiteConstraintPrimaryKey` (1555) rather than only
@@ -164,6 +184,5 @@ without `libsqlite3-dev`.
 
 ## Still to add
 
-Remaining callback hooks (busy handler, authorizer, progress handler,
-trace/update hooks), backup, extension loading, WAL/checkpoint helpers,
-connection configuration and statement parameter introspection.
+Backup, extension loading, WAL/checkpoint helpers, connection
+configuration and statement parameter introspection.

+ 52 - 2
bindings/sqlite3/SQLite.def

@@ -1,8 +1,8 @@
 DEFINITION MODULE FOR "C" SQLite;
 (* Core SQLite binding: connection setup, prepared statements,
    scalar/BLOB binding and results, column metadata, row changes,
-   incremental BLOB I/O, error reporting, exec callbacks and
-   user-defined scalar functions.
+   incremental BLOB I/O, error reporting, exec and connection hooks,
+   and user-defined scalar functions.
    The full binding stays in m2SQLITE for later porting. *)
 
 FROM SYSTEM IMPORT ADDRESS;
@@ -24,6 +24,10 @@ EXPORT UNQUALIFIED
   SQLiteOpenWal,
   SQLiteUtf8, SQLiteUtf16Le, SQLiteUtf16Be, SQLiteUtf16, SQLiteAny,
   SQLiteDeterministic,
+  SQLiteOpInsert, SQLiteOpUpdate, SQLiteOpDelete,
+  SQLiteAuthOk, SQLiteAuthDeny, SQLiteAuthIgnore,
+  SQLiteAuthDelete, SQLiteAuthSelect,
+  SQLiteTraceStmt, SQLiteTraceProfile, SQLiteTraceRow, SQLiteTraceClose,
   sqlite3_open,
   sqlite3_open_v2,
   sqlite3_exec,
@@ -79,6 +83,16 @@ EXPORT UNQUALIFIED
   sqlite3_result_error_code,
   sqlite3_user_data,
   sqlite3_context_db_handle,
+  sqlite3_busy_timeout,
+  sqlite3_busy_handler,
+  sqlite3_interrupt,
+  sqlite3_progress_handler,
+  sqlite3_trace_v2,
+  sqlite3_update_hook,
+  sqlite3_commit_hook,
+  sqlite3_rollback_hook,
+  sqlite3_set_authorizer,
+  sqlite3_db_handle,
   sqlite3_errmsg,
   sqlite3_errstr,
   sqlite3_errcode,
@@ -148,6 +162,21 @@ CONST
   SQLiteAny = 5;
   SQLiteDeterministic = 2048;
 
+  SQLiteOpDelete = 9;
+  SQLiteOpInsert = 18;
+  SQLiteOpUpdate = 23;
+
+  SQLiteAuthOk = 0;
+  SQLiteAuthDeny = 1;
+  SQLiteAuthIgnore = 2;
+  SQLiteAuthDelete = 9;
+  SQLiteAuthSelect = 21;
+
+  SQLiteTraceStmt = 1;
+  SQLiteTraceProfile = 2;
+  SQLiteTraceRow = 4;
+  SQLiteTraceClose = 8;
+
 PROCEDURE sqlite3_open (filename : ARRAY OF CHAR;
                         VAR db : ADDRESS) : INTEGER;
 PROCEDURE sqlite3_open_v2 (filename : ARRAY OF CHAR;
@@ -277,6 +306,27 @@ PROCEDURE sqlite3_result_error_code (ctx : ADDRESS; code : INTEGER);
 PROCEDURE sqlite3_user_data (ctx : ADDRESS) : ADDRESS;
 PROCEDURE sqlite3_context_db_handle (ctx : ADDRESS) : ADDRESS;
 
+(* Connection hooks.  Hooks must be top-level V3 procedures. *)
+PROCEDURE sqlite3_busy_timeout (db : ADDRESS; milliseconds : INTEGER) : INTEGER;
+PROCEDURE sqlite3_busy_handler (db : ADDRESS;
+                                callback, arg : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_interrupt (db : ADDRESS);
+PROCEDURE sqlite3_progress_handler (db : ADDRESS;
+                                    nOps : INTEGER;
+                                    callback, arg : ADDRESS);
+PROCEDURE sqlite3_trace_v2 (db : ADDRESS;
+                            mask : CARDINAL;
+                            callback, arg : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_update_hook (db : ADDRESS;
+                               callback, arg : ADDRESS) : ADDRESS;
+PROCEDURE sqlite3_commit_hook (db : ADDRESS;
+                               callback, arg : ADDRESS) : ADDRESS;
+PROCEDURE sqlite3_rollback_hook (db : ADDRESS;
+                                 callback, arg : ADDRESS);
+PROCEDURE sqlite3_set_authorizer (db : ADDRESS;
+                                  callback, arg : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_db_handle (stmt : ADDRESS) : ADDRESS;
+
 PROCEDURE sqlite3_errmsg (db : ADDRESS) : CString;
 PROCEDURE sqlite3_errstr (code : INTEGER) : CString;
 PROCEDURE sqlite3_errcode (db : ADDRESS) : INTEGER;

+ 1 - 0
bindings/sqlite3/run.sh

@@ -43,4 +43,5 @@ build_and_run SQLiteErrors
 build_and_run SQLiteBlob
 build_and_run SQLiteConnection
 build_and_run SQLiteCallback
+build_and_run SQLiteHooks
 build_and_run SQLiteShowcase showcase/SQLiteShowcase.mod

+ 240 - 0
bindings/sqlite3/tests/SQLiteHooks.mod

@@ -0,0 +1,240 @@
+MODULE SQLiteHooks;
+(* Connection hooks:
+   busy handler/timeout, progress, trace_v2, update/commit/rollback,
+   authorizer and sqlite3_db_handle. *)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM CStr IMPORT CString, Copy;
+FROM SQLite IMPORT
+  SQLiteOk, SQLiteRow, SQLiteDone, SQLiteAuth,
+  SQLiteOpenReadWrite, SQLiteOpenCreate, SQLiteOpenMemory,
+  SQLiteOpInsert, SQLiteOpUpdate, SQLiteOpDelete,
+  SQLiteAuthOk, SQLiteAuthDeny, SQLiteAuthDelete,
+  SQLiteTraceStmt,
+  sqlite3_open_v2, sqlite3_exec, sqlite3_close_v2,
+  sqlite3_prepare_v2, sqlite3_step, sqlite3_column_int,
+  sqlite3_finalize, sqlite3_free,
+  sqlite3_busy_timeout, sqlite3_busy_handler, sqlite3_interrupt,
+  sqlite3_progress_handler, sqlite3_trace_v2,
+  sqlite3_update_hook, sqlite3_commit_hook, sqlite3_rollback_hook,
+  sqlite3_set_authorizer, sqlite3_db_handle;
+
+VAR
+  ExitCode : INTEGER;
+  db, stmt, prev : ADDRESS;
+  rc : INTEGER;
+  ok : BOOLEAN;
+  progressCalls, traceCalls, updateCalls : INTEGER;
+  commitCalls, rollbackCalls, authCalls : INTEGER;
+  lastOp : INTEGER;
+  lastRowid : LONGINT;
+  hookBad : BOOLEAN;
+  tableBuf : ARRAY [0..31] 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 QueryInt (sql : ARRAY OF CHAR; want : INTEGER) : BOOLEAN;
+VAR
+  s : ADDRESS;
+  code, got : INTEGER;
+  good : BOOLEAN;
+BEGIN
+  s := NIL; got := -777; good := FALSE;
+  code := sqlite3_prepare_v2(db, sql, -1, s, NIL);
+  IF (code = SQLiteOk) AND (s # NIL) THEN
+    IF sqlite3_step(s) = SQLiteRow THEN
+      got := sqlite3_column_int(s, 0);
+      IF sqlite3_step(s) = SQLiteDone THEN good := TRUE END
+    END;
+    IF sqlite3_finalize(s) # SQLiteOk THEN good := FALSE END
+  END;
+  RETURN good AND (got = want)
+END QueryInt;
+
+PROCEDURE BusyFn (arg : ADDRESS; count : INTEGER) : INTEGER;
+BEGIN
+  RETURN 0
+END BusyFn;
+
+PROCEDURE ProgressFn (arg : ADDRESS) : INTEGER;
+BEGIN
+  progressCalls := progressCalls + 1;
+  RETURN 0
+END ProgressFn;
+
+PROCEDURE TraceFn (mask : CARDINAL; ctx, sql, expanded : ADDRESS) : INTEGER;
+BEGIN
+  traceCalls := traceCalls + 1;
+  RETURN 0
+END TraceFn;
+
+PROCEDURE UpdateFn (arg : ADDRESS; op : INTEGER;
+                    dbName, tableName : ADDRESS; rowid : LONGINT);
+VAR
+  s : CString;
+BEGIN
+  updateCalls := updateCalls + 1;
+  lastOp := op;
+  lastRowid := rowid;
+  s := tableName;
+  IF (s = NIL) OR (Copy(s, tableBuf) = 0) OR (tableBuf[0] # 'h') THEN
+    hookBad := TRUE
+  END
+END UpdateFn;
+
+PROCEDURE CommitFn (arg : ADDRESS) : INTEGER;
+BEGIN
+  commitCalls := commitCalls + 1;
+  RETURN 0
+END CommitFn;
+
+PROCEDURE RollbackFn (arg : ADDRESS);
+BEGIN
+  rollbackCalls := rollbackCalls + 1
+END RollbackFn;
+
+PROCEDURE AuthFn (arg : ADDRESS; action : INTEGER;
+                  dbName, tableName, colName, triggerName : ADDRESS) : INTEGER;
+BEGIN
+  authCalls := authCalls + 1;
+  IF action = SQLiteAuthDelete THEN
+    RETURN SQLiteAuthDeny
+  ELSE
+    RETURN SQLiteAuthOk
+  END
+END AuthFn;
+
+BEGIN
+  ExitCode := 1;
+  db := NIL; stmt := NIL; prev := NIL;
+  progressCalls := 0; traceCalls := 0; updateCalls := 0;
+  commitCalls := 0; rollbackCalls := 0; authCalls := 0;
+  lastOp := 0; lastRowid := 0; hookBad := FALSE;
+  ok := FALSE;
+
+  IF (sqlite3_open_v2(":memory:", db,
+        SQLiteOpenReadWrite + SQLiteOpenCreate + SQLiteOpenMemory,
+        NIL) = SQLiteOk) AND (db # NIL) THEN
+    IF ExecSQL("CREATE TABLE h(id INTEGER PRIMARY KEY, v TEXT);") THEN
+      ok := TRUE
+    END;
+
+    IF ok THEN
+      ok := (sqlite3_busy_timeout(db, 100) = SQLiteOk)
+            AND (sqlite3_busy_handler(db, ADR(BusyFn), NIL) = SQLiteOk)
+            AND (sqlite3_busy_handler(db, NIL, NIL) = SQLiteOk)
+    END;
+
+    (* sqlite3_db_handle returns the owning connection. *)
+    IF ok THEN
+      stmt := NIL;
+      rc := sqlite3_prepare_v2(db, "SELECT 1;", -1, stmt, NIL);
+      ok := (rc = SQLiteOk) AND (stmt # NIL)
+            AND (sqlite3_db_handle(stmt) = db);
+      IF stmt # NIL THEN
+        IF sqlite3_finalize(stmt) # SQLiteOk THEN ok := FALSE END;
+        stmt := NIL
+      END
+    END;
+
+    (* Progress handler runs during a recursive query. *)
+    IF ok THEN
+      progressCalls := 0;
+      sqlite3_progress_handler(db, 1, ADR(ProgressFn), NIL);
+      ok := QueryInt("SELECT 1;", 1) AND (progressCalls > 0);
+      sqlite3_progress_handler(db, 0, NIL, NIL)
+    END;
+
+    (* Trace callback sees prepared statements. *)
+    IF ok THEN
+      traceCalls := 0;
+      ok := sqlite3_trace_v2(db, SQLiteTraceStmt,
+                             ADR(TraceFn), NIL) = SQLiteOk
+    END;
+    IF ok THEN ok := QueryInt("SELECT 1;", 1) AND (traceCalls > 0) END;
+    IF ok THEN
+      ok := sqlite3_trace_v2(db, 0, NIL, NIL) = SQLiteOk
+    END;
+
+    (* Update hook fires for INSERT, UPDATE and DELETE. *)
+    IF ok THEN
+      updateCalls := 0; hookBad := FALSE;
+      prev := sqlite3_update_hook(db, ADR(UpdateFn), NIL)
+    END;
+    IF ok THEN
+      ok := ExecSQL("INSERT INTO h(v) VALUES ('a');")
+            AND (updateCalls = 1) AND (lastOp = SQLiteOpInsert)
+    END;
+    IF ok THEN
+      ok := ExecSQL("UPDATE h SET v = 'b' WHERE id = 1;")
+            AND (updateCalls = 2) AND (lastOp = SQLiteOpUpdate)
+    END;
+    IF ok THEN
+      ok := ExecSQL("DELETE FROM h WHERE id = 1;")
+            AND (updateCalls = 3) AND (lastOp = SQLiteOpDelete)
+            AND (NOT hookBad)
+    END;
+    IF ok THEN prev := sqlite3_update_hook(db, NIL, NIL) END;
+
+    (* Commit and rollback hooks. *)
+    IF ok THEN
+      commitCalls := 0; rollbackCalls := 0;
+      prev := sqlite3_commit_hook(db, ADR(CommitFn), NIL);
+      sqlite3_rollback_hook(db, ADR(RollbackFn), NIL)
+    END;
+    IF ok THEN
+      ok := ExecSQL("BEGIN; INSERT INTO h(v) VALUES ('c'); COMMIT;")
+            AND (commitCalls = 1)
+    END;
+    IF ok THEN
+      ok := ExecSQL("BEGIN; INSERT INTO h(v) VALUES ('d'); ROLLBACK;")
+            AND (rollbackCalls = 1)
+    END;
+    IF ok THEN
+      prev := sqlite3_commit_hook(db, NIL, NIL);
+      sqlite3_rollback_hook(db, NIL, NIL)
+    END;
+
+    (* Authorizer: allow SELECT, deny DELETE. *)
+    IF ok THEN
+      authCalls := 0;
+      ok := sqlite3_set_authorizer(db, ADR(AuthFn), NIL) = SQLiteOk
+    END;
+    IF ok THEN
+      stmt := NIL;
+      rc := sqlite3_prepare_v2(db, "DELETE FROM h;", -1, stmt, NIL);
+      ok := (rc = SQLiteAuth) AND (stmt = NIL) AND (authCalls > 0);
+      IF stmt # NIL THEN
+        IF sqlite3_finalize(stmt) # SQLiteOk THEN ok := FALSE END;
+        stmt := NIL
+      END
+    END;
+    IF ok THEN ok := QueryInt("SELECT count(*) FROM h;", 1) END;
+    IF ok THEN
+      ok := sqlite3_set_authorizer(db, NIL, NIL) = SQLiteOk
+    END;
+    IF ok THEN ok := ExecSQL("DELETE FROM h;") END;
+
+    IF ok THEN sqlite3_interrupt(db) END;
+    IF stmt # NIL THEN
+      IF sqlite3_finalize(stmt) # SQLiteOk THEN ok := FALSE END;
+      stmt := NIL
+    END
+  END;
+
+  IF db # NIL THEN
+    IF sqlite3_close_v2(db) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF ok THEN ExitCode := 42 END
+END SQLiteHooks.