Explorar o código

bindings: add SQLite aggregates and auxdata

Eric Streit hai 1 día
pai
achega
773cadae13

+ 15 - 0
bindings/sqlite3/README.md

@@ -97,6 +97,13 @@ sqlite3_value_double
 sqlite3_value_text
 sqlite3_value_blob
 sqlite3_value_bytes
+sqlite3_value_numeric_type
+sqlite3_value_dup
+sqlite3_value_free
+sqlite3_value_pointer
+sqlite3_aggregate_context
+sqlite3_get_auxdata
+sqlite3_set_auxdata
 sqlite3_result_null
 sqlite3_result_int
 sqlite3_result_int64
@@ -105,6 +112,13 @@ sqlite3_result_text
 sqlite3_result_blob
 sqlite3_result_error
 sqlite3_result_error_code
+sqlite3_result_error_toobig
+sqlite3_result_error_nomem
+sqlite3_result_value
+sqlite3_result_zeroblob
+sqlite3_result_zeroblob64
+sqlite3_result_text64
+sqlite3_result_blob64
 sqlite3_user_data
 sqlite3_context_db_handle
 
@@ -176,6 +190,7 @@ Hook-related constants include `SQLiteOp*`, `SQLiteAuth*` and
 - `SQLiteCapi3c.mod` - broad C-API integration test adapted from upstream `capi3c.test`
 - `SQLiteConnection.mod` - open_v2, close_v2, changes/rowid and deferred close
 - `SQLiteCallback.mod` - exec row callbacks and user-defined scalar functions
+- `SQLiteAggregate.mod` - aggregates, auxdata and extended UDF results
 - `SQLiteHooks.mod` - busy/progress/trace/update/commit/rollback/authorizer hooks
 - `showcase/SQLiteShowcase.mod` - prepared-statement walkthrough
 - `showcase/SQLiteFeatureShowcase.mod` - broad feature walkthrough

+ 43 - 0
bindings/sqlite3/SQLite.def

@@ -107,6 +107,13 @@ EXPORT UNQUALIFIED
   sqlite3_value_text,
   sqlite3_value_blob,
   sqlite3_value_bytes,
+  sqlite3_value_numeric_type,
+  sqlite3_value_dup,
+  sqlite3_value_free,
+  sqlite3_value_pointer,
+  sqlite3_aggregate_context,
+  sqlite3_get_auxdata,
+  sqlite3_set_auxdata,
   sqlite3_result_null,
   sqlite3_result_int,
   sqlite3_result_int64,
@@ -115,6 +122,13 @@ EXPORT UNQUALIFIED
   sqlite3_result_blob,
   sqlite3_result_error,
   sqlite3_result_error_code,
+  sqlite3_result_error_toobig,
+  sqlite3_result_error_nomem,
+  sqlite3_result_value,
+  sqlite3_result_zeroblob,
+  sqlite3_result_zeroblob64,
+  sqlite3_result_text64,
+  sqlite3_result_blob64,
   sqlite3_user_data,
   sqlite3_context_db_handle,
   sqlite3_busy_timeout,
@@ -408,6 +422,20 @@ PROCEDURE sqlite3_value_double (value : ValueHandle) : REAL;
 PROCEDURE sqlite3_value_text (value : ValueHandle) : CString;
 PROCEDURE sqlite3_value_blob (value : ValueHandle) : ADDRESS;
 PROCEDURE sqlite3_value_bytes (value : ValueHandle) : INTEGER;
+PROCEDURE sqlite3_value_numeric_type (value : ValueHandle) : INTEGER;
+PROCEDURE sqlite3_value_dup (value : ValueHandle) : ValueHandle;
+PROCEDURE sqlite3_value_free (value : ValueHandle);
+PROCEDURE sqlite3_value_pointer (value : ValueHandle;
+                                 typeName : CString) : ADDRESS;
+
+PROCEDURE sqlite3_aggregate_context (ctx : ContextHandle;
+                                     nBytes : INTEGER) : ADDRESS;
+PROCEDURE sqlite3_get_auxdata (ctx : ContextHandle;
+                               idx : INTEGER) : ADDRESS;
+PROCEDURE sqlite3_set_auxdata (ctx : ContextHandle;
+                               idx : INTEGER;
+                               data : ADDRESS;
+                               destructor : ADDRESS);
 
 PROCEDURE sqlite3_result_null (ctx : ContextHandle);
 PROCEDURE sqlite3_result_int (ctx : ContextHandle; value : INTEGER);
@@ -425,6 +453,21 @@ PROCEDURE sqlite3_result_error (ctx : ContextHandle;
                                 msg : ARRAY OF CHAR;
                                 nBytes : INTEGER);
 PROCEDURE sqlite3_result_error_code (ctx : ContextHandle; code : INTEGER);
+PROCEDURE sqlite3_result_error_toobig (ctx : ContextHandle);
+PROCEDURE sqlite3_result_error_nomem (ctx : ContextHandle);
+PROCEDURE sqlite3_result_value (ctx : ContextHandle; value : ValueHandle);
+PROCEDURE sqlite3_result_zeroblob (ctx : ContextHandle; nBytes : INTEGER);
+PROCEDURE sqlite3_result_zeroblob64 (ctx : ContextHandle;
+                                     nBytes : LONGCARD) : INTEGER;
+PROCEDURE sqlite3_result_text64 (ctx : ContextHandle;
+                                 value : ARRAY OF CHAR;
+                                 nBytes : LONGCARD;
+                                 destructor : ADDRESS;
+                                 encoding : INTEGER);
+PROCEDURE sqlite3_result_blob64 (ctx : ContextHandle;
+                                 data : ADDRESS;
+                                 nBytes : LONGCARD;
+                                 destructor : ADDRESS);
 
 PROCEDURE sqlite3_user_data (ctx : ContextHandle) : ADDRESS;
 PROCEDURE sqlite3_context_db_handle (ctx : ContextHandle) : DbHandle;

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

@@ -8,11 +8,11 @@ metadata/diagnostics, BLOB error handling and extended constraint
 codes are implemented.  Covered by `tests/SQLiteParams.mod`,
 `tests/SQLiteIntrospection.mod`, `tests/SQLiteBlobErrors.mod`,
 `tests/SQLiteConstraints.mod`, `tests/SQLiteBackup.mod`,
-`tests/SQLitePrepareTail.mod`, `tests/SQLiteHelpers.mod`, the broad
-`tests/SQLiteCapi3c.mod` integration test and
-`showcase/SQLiteFeatureShowcase.mod`.  Typed handle aliases, a
-`SQLiteUtils` helper layer and portable library discovery are also in
-place.
+`tests/SQLitePrepareTail.mod`, `tests/SQLiteHelpers.mod`,
+`tests/SQLiteAggregate.mod`, the broad `tests/SQLiteCapi3c.mod`
+integration test and `showcase/SQLiteFeatureShowcase.mod`.  Typed
+handle aliases, a `SQLiteUtils` helper layer and portable library
+discovery are also in place.
 
 This note records which existing tests are useful for hardening the V3
 SQLite binding under `bindings/sqlite3`, and which APIs they would drive.
@@ -21,7 +21,7 @@ SQLite binding under `bindings/sqlite3`, and which APIs they would drive.
 
 | Source | Count | Notes |
 |---|---:|---|
-| `bindings/sqlite3/tests` | 16 | Current V3 tests: smoke, prepared, prepare-v3 tail, params, helpers, introspection, scalar, errors, BLOB, BLOB errors, constraints, backup, C API integration, connection, callback, hooks |
+| `bindings/sqlite3/tests` | 17 | Current V3 tests: smoke, prepared, prepare-v3 tail, params, helpers, introspection, scalar, errors, BLOB, BLOB errors, constraints, backup, C API integration, connection, callback, aggregate, 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 |
@@ -47,6 +47,7 @@ harness.
 - `SQLiteCapi3c.mod`
 - `SQLiteConnection.mod`
 - `SQLiteCallback.mod`
+- `SQLiteAggregate.mod`
 - `SQLiteHooks.mod`
 
 ## Original m2SQLITE tests
@@ -167,9 +168,15 @@ Ported from `m2SQLITE/tests/test_codes.mod` into `SQLiteConstraints.mod`.
 - `SQLiteConstraintCheck` = 275
 - optionally trigger, rowid and datatype constraints
 
-### 5. Later slices
+### 5. Aggregate/UDF extensions — implemented 2026-10-27
 
-- aggregate functions + auxdata (`test_func.mod`, `func*.test`)
+Ported into `SQLiteAggregate.mod`: aggregate context, auxdata,
+`value_numeric_type`, `value_dup/free`, `result_value`, zeroblob64,
+text64/blob64 and error-result APIs.
+
+### 6. Later slices
+
+- deeper aggregate/window-function coverage (`func*.test`, `window*.test`)
 - file-to-file backup coverage (`backup*.test`)
 - `sqlite3_get_table` / `free_table`
 - extension loading and WAL/checkpoint helpers

+ 1 - 0
bindings/sqlite3/run.sh

@@ -89,5 +89,6 @@ build_and_run SQLiteCapi3c
 build_and_run SQLiteConnection
 build_and_run SQLiteCallback
 build_and_run SQLiteHooks
+build_and_run SQLiteAggregate
 build_and_run SQLiteShowcase showcase/SQLiteShowcase.mod
 build_and_run SQLiteFeatureShowcase showcase/SQLiteFeatureShowcase.mod

+ 283 - 0
bindings/sqlite3/tests/SQLiteAggregate.mod

@@ -0,0 +1,283 @@
+MODULE SQLiteAggregate;
+(* Aggregate functions, auxdata and the remaining UDF value/result APIs. *)
+
+FROM SYSTEM IMPORT ADDRESS, ADR;
+FROM CStr IMPORT CString, Copy;
+FROM SQLite IMPORT
+  DbHandle, StmtHandle, ContextHandle, ValueHandle,
+  SQLiteOk, SQLiteRow, SQLiteDone, SQLiteNull,
+  SQLiteInteger, SQLiteText, SQLiteTooBig, SQLiteNoMem,
+  SQLiteOpenReadWrite, SQLiteOpenCreate, SQLiteOpenMemory, SQLiteUtf8,
+  sqlite3_open_v2, sqlite3_close, sqlite3_exec,
+  sqlite3_prepare_v2, sqlite3_step, sqlite3_column_int,
+  sqlite3_column_text, sqlite3_finalize,
+  sqlite3_create_function_v2,
+  sqlite3_value_type, sqlite3_value_int64, sqlite3_value_numeric_type,
+  sqlite3_value_dup, sqlite3_value_free,
+  sqlite3_aggregate_context, sqlite3_get_auxdata, sqlite3_set_auxdata,
+  sqlite3_result_int, sqlite3_result_int64, sqlite3_result_null,
+  sqlite3_result_error, sqlite3_result_value, sqlite3_result_zeroblob,
+  sqlite3_result_zeroblob64,
+  sqlite3_result_text64, sqlite3_result_blob64,
+  sqlite3_result_error_toobig, sqlite3_result_error_nomem,
+  sqlite3_free, SQLiteArrayAt, SQLiteTransient;
+
+TYPE
+  SumPtr = POINTER TO LONGINT;
+
+VAR
+  ExitCode : INTEGER;
+  db : DbHandle;
+  stmt : StmtHandle;
+  rc : INTEGER;
+  ok : BOOLEAN;
+  bin, auxMarker : ARRAY [0..7] OF CHAR;
+
+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 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 : StmtHandle;
+  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 QueryText (sql : ARRAY OF CHAR; want : ARRAY OF CHAR) : BOOLEAN;
+VAR
+  s : StmtHandle;
+  code : INTEGER;
+  p : CString;
+  buf : ARRAY [0..63] OF CHAR;
+  good : BOOLEAN;
+BEGIN
+  s := NIL; good := FALSE;
+  code := sqlite3_prepare_v2(db, sql, -1, s, NIL);
+  IF (code = SQLiteOk) AND (s # NIL) THEN
+    IF sqlite3_step(s) = SQLiteRow THEN
+      p := sqlite3_column_text(s, 0);
+      IF (p # NIL) AND (Copy(p, buf) > 0) THEN
+        good := SameText(buf, want)
+      END;
+      IF sqlite3_step(s) # SQLiteDone THEN good := FALSE END
+    END;
+    IF sqlite3_finalize(s) # SQLiteOk THEN good := FALSE END
+  END;
+  RETURN good
+END QueryText;
+
+PROCEDURE QueryError (sql : ARRAY OF CHAR; want : INTEGER) : BOOLEAN;
+VAR
+  s : StmtHandle;
+  code : INTEGER;
+  good : BOOLEAN;
+BEGIN
+  s := NIL; good := FALSE;
+  code := sqlite3_prepare_v2(db, sql, -1, s, NIL);
+  IF (code = SQLiteOk) AND (s # NIL) THEN
+    good := sqlite3_step(s) = want;
+    rc := sqlite3_finalize(s)
+  END;
+  RETURN good
+END QueryError;
+
+PROCEDURE Register (name : ARRAY OF CHAR; n : INTEGER;
+                    xFunc, xStep, xFinal : ADDRESS) : BOOLEAN;
+BEGIN
+  RETURN sqlite3_create_function_v2(db, name, n, SQLiteUtf8,
+    NIL, xFunc, xStep, xFinal, NIL) = SQLiteOk
+END Register;
+
+PROCEDURE SumStep (ctx : ContextHandle; argc : INTEGER; argv : ADDRESS);
+VAR
+  p : SumPtr;
+  v : ValueHandle;
+BEGIN
+  p := VAL(SumPtr, sqlite3_aggregate_context(ctx, 8));
+  IF p # NIL THEN
+    v := SQLiteArrayAt(argv, 0);
+    IF sqlite3_value_type(v) # SQLiteNull THEN
+      p^ := p^ + sqlite3_value_int64(v)
+    END
+  END
+END SumStep;
+
+PROCEDURE SumFinal (ctx : ContextHandle);
+VAR
+  p : SumPtr;
+BEGIN
+  p := VAL(SumPtr, sqlite3_aggregate_context(ctx, 0));
+  IF p = NIL THEN
+    sqlite3_result_null(ctx)
+  ELSE
+    sqlite3_result_int64(ctx, p^)
+  END
+END SumFinal;
+
+PROCEDURE AuxFn (ctx : ContextHandle; argc : INTEGER; argv : ADDRESS);
+VAR
+  p, q : ADDRESS;
+BEGIN
+  p := ADR(auxMarker[0]);
+  sqlite3_set_auxdata(ctx, 0, p, NIL);
+  q := sqlite3_get_auxdata(ctx, 0);
+  IF q = p THEN
+    sqlite3_result_int(ctx, 1)
+  ELSE
+    sqlite3_result_int(ctx, 0)
+  END
+END AuxFn;
+
+PROCEDURE DupFn (ctx : ContextHandle; argc : INTEGER; argv : ADDRESS);
+VAR
+  v, d : ValueHandle;
+BEGIN
+  IF argc = 1 THEN
+    v := SQLiteArrayAt(argv, 0);
+    d := sqlite3_value_dup(v);
+    IF d = NIL THEN
+      sqlite3_result_error(ctx, "dup failed", -1)
+    ELSE
+      sqlite3_result_value(ctx, d);
+      sqlite3_value_free(d)
+    END
+  ELSE
+    sqlite3_result_error(ctx, "m2_dup expects 1 argument", -1)
+  END
+END DupFn;
+
+PROCEDURE NumTypeFn (ctx : ContextHandle; argc : INTEGER; argv : ADDRESS);
+BEGIN
+  IF argc = 1 THEN
+    sqlite3_result_int(ctx,
+      sqlite3_value_numeric_type(SQLiteArrayAt(argv, 0)))
+  ELSE
+    sqlite3_result_error(ctx, "m2_numtype expects 1 argument", -1)
+  END
+END NumTypeFn;
+
+PROCEDURE Zero32Fn (ctx : ContextHandle; argc : INTEGER; argv : ADDRESS);
+BEGIN
+  sqlite3_result_zeroblob(ctx, 4)
+END Zero32Fn;
+
+PROCEDURE Zero64Fn (ctx : ContextHandle; argc : INTEGER; argv : ADDRESS);
+VAR
+  code : INTEGER;
+BEGIN
+  code := sqlite3_result_zeroblob64(ctx, 4);
+  IF code # SQLiteOk THEN
+    sqlite3_result_error(ctx, "zeroblob64 failed", -1)
+  END
+END Zero64Fn;
+
+PROCEDURE Text64Fn (ctx : ContextHandle; argc : INTEGER; argv : ADDRESS);
+BEGIN
+  sqlite3_result_text64(ctx, "hi", 2, SQLiteTransient(), SQLiteUtf8)
+END Text64Fn;
+
+PROCEDURE Blob64Fn (ctx : ContextHandle; argc : INTEGER; argv : ADDRESS);
+BEGIN
+  sqlite3_result_blob64(ctx, ADR(bin[0]), 3, SQLiteTransient())
+END Blob64Fn;
+
+PROCEDURE TooBigFn (ctx : ContextHandle; argc : INTEGER; argv : ADDRESS);
+BEGIN
+  sqlite3_result_error_toobig(ctx)
+END TooBigFn;
+
+PROCEDURE NoMemFn (ctx : ContextHandle; argc : INTEGER; argv : ADDRESS);
+BEGIN
+  sqlite3_result_error_nomem(ctx)
+END NoMemFn;
+
+BEGIN
+  ExitCode := 1;
+  db := NIL; stmt := NIL; ok := FALSE;
+  bin[0] := CHR(0); bin[1] := CHR(1); bin[2] := CHR(2);
+  auxMarker[0] := 'A';
+
+  IF (sqlite3_open_v2(":memory:", db,
+        SQLiteOpenReadWrite + SQLiteOpenCreate + SQLiteOpenMemory,
+        NIL) = SQLiteOk) AND (db # NIL)
+     AND ExecSQL("CREATE TABLE nums(x INTEGER);")
+     AND ExecSQL("INSERT INTO nums VALUES(1);")
+     AND ExecSQL("INSERT INTO nums VALUES(2);")
+     AND ExecSQL("INSERT INTO nums VALUES(3);") THEN
+    ok := TRUE
+  END;
+
+  IF ok THEN
+    ok := Register("m2_sum", 1, NIL, ADR(SumStep), ADR(SumFinal))
+          AND QueryInt("SELECT m2_sum(x) FROM nums;", 6)
+  END;
+  IF ok THEN
+    ok := Register("m2_aux", 0, ADR(AuxFn), NIL, NIL)
+          AND QueryInt("SELECT m2_aux();", 1)
+  END;
+  IF ok THEN
+    ok := Register("m2_dup", 1, ADR(DupFn), NIL, NIL)
+          AND QueryText("SELECT m2_dup('abc');", "abc")
+  END;
+  IF ok THEN
+    ok := Register("m2_numtype", 1, ADR(NumTypeFn), NIL, NIL)
+          AND QueryInt("SELECT m2_numtype('42');", SQLiteInteger)
+  END;
+  IF ok THEN
+    ok := Register("m2_zero32", 0, ADR(Zero32Fn), NIL, NIL)
+          AND QueryText("SELECT hex(m2_zero32());", "00000000")
+  END;
+  IF ok THEN
+    ok := Register("m2_zero64", 0, ADR(Zero64Fn), NIL, NIL)
+          AND QueryText("SELECT hex(m2_zero64());", "00000000")
+  END;
+  IF ok THEN
+    ok := Register("m2_text64", 0, ADR(Text64Fn), NIL, NIL)
+          AND QueryText("SELECT m2_text64();", "hi")
+  END;
+  IF ok THEN
+    ok := Register("m2_blob64", 0, ADR(Blob64Fn), NIL, NIL)
+          AND QueryText("SELECT hex(m2_blob64());", "000102")
+  END;
+  IF ok THEN
+    ok := Register("m2_toobig", 0, ADR(TooBigFn), NIL, NIL)
+          AND QueryError("SELECT m2_toobig();", SQLiteTooBig)
+  END;
+  IF ok THEN
+    ok := Register("m2_nomem", 0, ADR(NoMemFn), NIL, NIL)
+          AND QueryError("SELECT m2_nomem();", SQLiteNoMem)
+  END;
+
+  IF db # NIL THEN
+    IF sqlite3_close(db) # SQLiteOk THEN ok := FALSE END
+  END;
+  IF ok THEN ExitCode := 42 END
+END SQLiteAggregate.