Sfoglia il codice sorgente

bindings: add SQLite online backup

Eric Streit 1 giorno fa
parent
commit
2c57b2be17

+ 9 - 1
bindings/sqlite3/README.md

@@ -80,6 +80,13 @@ sqlite3_blob_write
 sqlite3_blob_bytes
 sqlite3_blob_reopen
 
+(* online backup *)
+sqlite3_backup_init
+sqlite3_backup_step
+sqlite3_backup_finish
+sqlite3_backup_remaining
+sqlite3_backup_pagecount
+
 (* callbacks and user-defined scalar functions *)
 sqlite3_create_function_v2
 sqlite3_value_type
@@ -140,6 +147,7 @@ Hook-related constants include `SQLiteOp*`, `SQLiteAuth*` and
 - `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
+- `SQLiteBackup.mod` - full and incremental online backup
 - `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
@@ -209,5 +217,5 @@ without `libsqlite3-dev`.
 
 ## Still to add
 
-Backup, extension loading, WAL/checkpoint helpers and connection
+Extension loading, WAL/checkpoint helpers and connection
 configuration.

+ 14 - 0
bindings/sqlite3/SQLite.def

@@ -91,6 +91,11 @@ EXPORT UNQUALIFIED
   sqlite3_blob_write,
   sqlite3_blob_bytes,
   sqlite3_blob_reopen,
+  sqlite3_backup_init,
+  sqlite3_backup_step,
+  sqlite3_backup_finish,
+  sqlite3_backup_remaining,
+  sqlite3_backup_pagecount,
   sqlite3_create_function_v2,
   sqlite3_value_type,
   sqlite3_value_int,
@@ -353,6 +358,15 @@ PROCEDURE sqlite3_blob_bytes (blob : ADDRESS) : INTEGER;
 PROCEDURE sqlite3_blob_reopen (blob : ADDRESS;
                                rowid : LONGINT) : INTEGER;
 
+PROCEDURE sqlite3_backup_init (dst : ADDRESS;
+                               dstName : ARRAY OF CHAR;
+                               src : ADDRESS;
+                               srcName : ARRAY OF CHAR) : ADDRESS;
+PROCEDURE sqlite3_backup_step (backup : ADDRESS; pages : INTEGER) : INTEGER;
+PROCEDURE sqlite3_backup_finish (backup : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_backup_remaining (backup : ADDRESS) : INTEGER;
+PROCEDURE sqlite3_backup_pagecount (backup : ADDRESS) : INTEGER;
+
 (* User-defined scalar functions.  xFunc receives
    (sqlite3_context*, int argc, sqlite3_value** argv); use
    SQLiteArrayAt(argv, i) to reach each value. *)

+ 19 - 17
bindings/sqlite3/TEST-INVENTORY.md

@@ -6,8 +6,8 @@ Status update 2026-10-27: parameter introspection
 (`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`.
+`tests/SQLiteIntrospection.mod`, `tests/SQLiteBlobErrors.mod`,
+`tests/SQLiteConstraints.mod` and `tests/SQLiteBackup.mod`.
 
 This note records which existing tests are useful for hardening the V3
 SQLite binding under `bindings/sqlite3`, and which APIs they would drive.
@@ -16,7 +16,7 @@ SQLite binding under `bindings/sqlite3`, and which APIs they would drive.
 
 | Source | Count | Notes |
 |---|---:|---|
-| `bindings/sqlite3/tests` | 8 | Current V3 tests: smoke, prepared, scalar, errors, BLOB, connection, callback, hooks |
+| `bindings/sqlite3/tests` | 13 | Current V3 tests: smoke, prepared, params, introspection, scalar, errors, BLOB, BLOB errors, constraints, backup, connection, callback, hooks |
 | sibling `m2SQLITE/tests` | 7 | Original GNU Modula-2 binding tests; closest existing coverage |
 | sibling `m2SQLITE/sqlite-master/test` | 1,195 `.test` files | Upstream SQLite TCL regression suite; useful as a specification |
 | `compiler/tests` | 806 files | Compiler/ABI tests; not SQLite-specific, but several guard binding prerequisites |
@@ -29,9 +29,14 @@ harness.
 
 - `SQLiteSmoke.mod`
 - `SQLitePrepared.mod`
+- `SQLiteParams.mod`
+- `SQLiteIntrospection.mod`
 - `SQLiteScalar.mod`
 - `SQLiteErrors.mod`
 - `SQLiteBlob.mod`
+- `SQLiteBlobErrors.mod`
+- `SQLiteConstraints.mod`
+- `SQLiteBackup.mod`
 - `SQLiteConnection.mod`
 - `SQLiteCallback.mod`
 - `SQLiteHooks.mod`
@@ -42,11 +47,11 @@ 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 | Read-only/bounds/expiry covered; backup remains |
+| `test_blob.mod` | read-only BLOB write error, reopen, backup API, backup to file | Read-only/bounds/expiry and memory backup covered; file 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 |
+| `test_version.mod` | `sqlite3_libversion*` | Implemented in `SQLiteIntrospection.mod` |
 
 ## Upstream SQLite tests worth mining
 
@@ -69,11 +74,11 @@ harness.
 |---|---|---|
 | `func.test` … `func9.test` | function arg counts, NULLs, destructors, auxdata, aggregates, result types | aggregate/auxdata APIs, more result writers |
 | `errors`/`errmsg.test` | error text and error offsets | `sqlite3_error_offset`, error-message behavior |
-| `fkey1.test` … `fkey9.test` | foreign-key and not-null constraint codes | missing extended-code constants |
+| `fkey1.test` … `fkey9.test` | foreign-key and not-null constraint codes | Implemented in `SQLiteConstraints.mod` |
 | `openv2.test`, `uri.test`, `uri2.test` | open flags, URI filenames, read-only opens | `open_v2` flags and URI behavior |
 | `limit.test`, `limit2.test` | `sqlite3_limit` | connection limits |
 | `trans.test`, `trans2.test`, `trans3.test`, `rollback.test` | transaction boundaries and hooks | commit/rollback hook ordering |
-| `backup.test`, `backup2.test`, `backup4.test`, `backup5.test` | online backup and incremental backup | backup API |
+| `backup.test`, `backup2.test`, `backup4.test`, `backup5.test` | online backup and incremental backup | Core API implemented; file backup remains |
 | `attach.test`, `attach2.test` | multiple attached databases | BLOB `zDb` values, future helpers |
 
 ### Later / not useful yet
@@ -119,9 +124,9 @@ Cases:
 - `clear_bindings`
 - `LONGINT` value such as `9000000001`
 
-### 2. Extend `SQLiteBlob.mod`
+### 2. `SQLiteBlobErrors.mod` — implemented 2026-10-27
 
-Port from `incrblob-3.*`, `incrblob-4.*` and `test_blob.mod`.
+Ported from `incrblob-3.*`, `incrblob-4.*` and `test_blob.mod`.
 
 Cases:
 
@@ -145,11 +150,9 @@ Cases:
 - authorizer action arguments
 - busy handler actually invoked by a second connection
 
-### 4. Extended-code tests
+### 4. Extended-code tests — implemented 2026-10-27
 
-Port from `m2SQLITE/tests/test_codes.mod`.
-
-Add constants and tests for:
+Ported from `m2SQLITE/tests/test_codes.mod` into `SQLiteConstraints.mod`.
 
 - `SQLiteConstraintNotNull` = 1299
 - `SQLiteConstraintForeignKey` = 787
@@ -159,11 +162,10 @@ Add constants and tests for:
 ### 5. Later slices
 
 - aggregate functions + auxdata (`test_func.mod`, `func*.test`)
-- backup API (`test_blob.mod`, `backup*.test`)
-- version/sourceid/threadsafe (`test_version.mod`)
+- file-to-file backup coverage (`backup*.test`)
 - `sqlite3_get_table` / `free_table`
-- statement metadata (`sqlite3_sql`, `expanded_sql`, `stmt_readonly`, `stmt_busy`)
-- limits, autocommit and error offsets
+- extension loading and WAL/checkpoint helpers
+- deeper progress/authorizer/busy cases from upstream tests
 
 ## Related session note
 

+ 1 - 0
bindings/sqlite3/run.sh

@@ -45,6 +45,7 @@ build_and_run SQLiteErrors
 build_and_run SQLiteBlob
 build_and_run SQLiteBlobErrors
 build_and_run SQLiteConstraints
+build_and_run SQLiteBackup
 build_and_run SQLiteConnection
 build_and_run SQLiteCallback
 build_and_run SQLiteHooks

+ 111 - 0
bindings/sqlite3/tests/SQLiteBackup.mod

@@ -0,0 +1,111 @@
+MODULE SQLiteBackup;
+(* Online backup API:
+   backup_init/step/finish/remaining/pagecount. *)
+
+FROM SYSTEM IMPORT ADDRESS;
+FROM SQLite IMPORT
+  SQLiteOk, SQLiteRow, SQLiteDone,
+  sqlite3_open, sqlite3_close, sqlite3_exec,
+  sqlite3_prepare_v2, sqlite3_step, sqlite3_column_int,
+  sqlite3_finalize,
+  sqlite3_backup_init, sqlite3_backup_step, sqlite3_backup_finish,
+  sqlite3_backup_remaining, sqlite3_backup_pagecount,
+  sqlite3_free;
+
+VAR
+  ExitCode : INTEGER;
+  src, dst, dst2, bk : ADDRESS;
+  rc, pages : INTEGER;
+  ok : BOOLEAN;
+
+PROCEDURE ExecSQL (h : ADDRESS; sql : ARRAY OF CHAR) : BOOLEAN;
+VAR
+  errmsg : ADDRESS;
+  good : BOOLEAN;
+BEGIN
+  errmsg := NIL;
+  good := sqlite3_exec(h, sql, NIL, NIL, errmsg) = SQLiteOk;
+  IF errmsg # NIL THEN
+    sqlite3_free(errmsg)
+  END;
+  RETURN good
+END ExecSQL;
+
+PROCEDURE CountRows (h : ADDRESS; want : INTEGER) : BOOLEAN;
+VAR
+  s : ADDRESS;
+  code, got : INTEGER;
+  good : BOOLEAN;
+BEGIN
+  s := NIL; got := -1; good := FALSE;
+  code := sqlite3_prepare_v2(h, "SELECT count(*) FROM t;",
+                             -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 CountRows;
+
+BEGIN
+  ExitCode := 1;
+  src := NIL; dst := NIL; dst2 := NIL; bk := NIL;
+  ok := FALSE;
+
+  IF (sqlite3_open(":memory:", src) = SQLiteOk) AND (src # NIL)
+     AND (sqlite3_open(":memory:", dst) = SQLiteOk) AND (dst # NIL)
+     AND (sqlite3_open(":memory:", dst2) = SQLiteOk) AND (dst2 # NIL) THEN
+    ok := ExecSQL(src, "CREATE TABLE t(x INTEGER);")
+          AND ExecSQL(src, "INSERT INTO t VALUES(1);")
+          AND ExecSQL(src, "INSERT INTO t VALUES(2);")
+  END;
+
+  (* Full backup into another in-memory database. *)
+  IF ok THEN
+    bk := sqlite3_backup_init(dst, "main", src, "main");
+    ok := bk # NIL;
+    IF ok THEN
+      ok := sqlite3_backup_step(bk, -1) = SQLiteDone
+    END;
+    IF ok THEN
+      pages := sqlite3_backup_pagecount(bk);
+      ok := (pages > 0) AND (sqlite3_backup_remaining(bk) = 0)
+    END;
+    IF sqlite3_backup_finish(bk) # SQLiteOk THEN ok := FALSE END;
+    bk := NIL
+  END;
+  IF ok THEN ok := CountRows(dst, 2) END;
+
+  (* Incremental backup, one page at a time. *)
+  IF ok THEN
+    bk := sqlite3_backup_init(dst2, "main", src, "main");
+    ok := bk # NIL;
+    IF ok THEN
+      rc := sqlite3_backup_step(bk, 1);
+      WHILE (rc = SQLiteOk) DO
+        rc := sqlite3_backup_step(bk, 1)
+      END;
+      ok := (rc = SQLiteDone) AND (sqlite3_backup_remaining(bk) = 0);
+      IF sqlite3_backup_finish(bk) # SQLiteOk THEN ok := FALSE END;
+      bk := NIL
+    END
+  END;
+  IF ok THEN ok := CountRows(dst2, 2) END;
+
+  IF bk # NIL THEN
+    IF sqlite3_backup_finish(bk) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF src # NIL THEN
+    IF sqlite3_close(src) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF dst # NIL THEN
+    IF sqlite3_close(dst) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF dst2 # NIL THEN
+    IF sqlite3_close(dst2) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF ok THEN ExitCode := 42 END
+END SQLiteBackup.