Skip to content

Commit a00af13

Browse files
feat(stdlib): Transaction.affine — affine-bounded write-set isolation — db-theory #2 (8 externs) (#526)
## Summary - New `stdlib/Transaction.affine` module exposing SQL transactions as an affine-bounded resource: `Tx` opaque handle from `tx_begin(d)`, consumed exactly once by `tx_commit(t)` or `tx_rollback(t)`. Savepoint surface (`tx_savepoint` / `tx_release` / `tx_rollback_to`), `tx_db` aliasing, and `tx_is_live` introspection round out the 8-extern API. - Deno-ESM codegen wires `__as_txBegin` / `Commit` / `Rollback` / `Savepoint` / `Release` / `RollbackTo` / `Db` / `IsLive` JS prelude helpers + 8 `deno_builtins` lowerings. Host adapter contract documented inline. - Six-function smoke (`tests/codegen-deno/transaction_smoke.{affine,harness.mjs}`) witnesses all lifecycle shapes — **including the rollback-discards-writes safety property** (an `INSERT` issued during a `Tx` consumed by `tx_rollback` leaves only the pre-tx rows visible). ## Proof obligation cross-doc Per owner directive 2026-06-01 — every proof in AffineScript must first audit `hyperpolymath/echo-types`, reuse if applicable, extend upstream **with proofs** if not, then cross-document. - New `docs/academic/proofs/db-theory-2-transaction-safety.md` states three safety obligations: - **#DB-2.1** rollback-discards-writes (key invariant) - **#DB-2.2** commit-promotes-writes - **#DB-2.3** savepoint locality - Audit finding: echo-types carries `EchoLinear.LEcho` + `weaken`, `EchoSecurity.Security` record, and `EchoNoSectionGeneric.no-section-of-collapsing-map` — the latter is precisely the generic lemma the Transaction proof reduces to. **Upstream extension required**: a new `TransactionMutations.agda` module instantiating the generic no-section lemma. Issue to be filed on echo-types separately and back-linked here. ## Test plan - [x] `dune runtest test/test_main.exe` — 367 tests green, including the new AOT smoke for `stdlib/Transaction.affine` (row #15 in the AOT smoke output) and the regression test enforcing `codegen ⊆ stdlib decls` (no drift introduced). - [x] `tools/run_codegen_deno_tests.sh` — `transaction_smoke.harness.mjs` passes all 6 assertions. - [x] Mock adapter implements write-set isolation by snapshotting tables on `txBegin` and restoring on `txRollback`. Production adapter contract (Deno `jsr:@db/sqlite` / Node `better-sqlite3`) maps each extern to native `BEGIN` / `COMMIT` / `ROLLBACK` / `SAVEPOINT` / `RELEASE` / `ROLLBACK TO`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d7cdec5 commit a00af13

9 files changed

Lines changed: 1048 additions & 1 deletion
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!-- SPDX-License-Identifier: MPL-2.0 -->
2+
<!-- Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> -->
3+
4+
# DB-Theory #2 — Transaction Safety (Rollback-Discards-Writes)
5+
6+
**Status**: Carrier wired (`stdlib/Transaction.affine`, codegen, Deno-ESM smoke); formal proof obligation **pending upstream against `hyperpolymath/echo-types`**.
7+
8+
## 1. The obligation
9+
10+
Let `Tx` be the opaque affine handle returned by `tx_begin(d: Db)` in `stdlib/Transaction.affine`. Let `t : Tx` be such a handle, and let `σ₀` be the database state immediately before `tx_begin(d)`. Let `W` be the multiset of mutating SQL statements (`INSERT`, `UPDATE`, `DELETE`, schema mutations) executed against `d` (or any handle aliased through `tx_db(t)`) between `tx_begin(d)` and the consumption of `t`.
11+
12+
**Safety property #DB-2.1 (rollback-discards-writes)**: if `t` is consumed by `tx_rollback(t)`, then for every query `q` issued against `d` *after* the rollback, the answer to `q` is precisely the answer it would have had over `σ₀` (i.e. as if `W` had never happened).
13+
14+
**Safety property #DB-2.2 (commit-promotes-writes)**: if `t` is consumed by `tx_commit(t)`, then for every query `q` issued after the commit, the answer is the same as serially applying `W` to `σ₀`.
15+
16+
**Safety property #DB-2.3 (savepoint locality)**: if a savepoint `s` is opened with `tx_savepoint(t, s)`, mutations `W_s` are issued, and then `tx_rollback_to(t, s)` is called, the database state mid-transaction reverts to the state at `tx_savepoint(t, s)` while leaving the outer transaction live.
17+
18+
## 2. Echo-types audit (2026-06-01)
19+
20+
Per owner directive, every proof in AffineScript must first audit `hyperpolymath/echo-types`, reuse if applicable, extend upstream **with proofs** if not, then cross-document.
21+
22+
**Audit finding** (full report under `tasklist/sub-agent reports/2026-06-01-transaction-echo-audit`): echo-types has no Transaction-specific instantiation today, but carries three reusable abstractions:
23+
24+
1. `EchoLinear.LEcho` + the `weaken : LEcho linear → LEcho affine` collapse map, with `no-section-weaken` proving no recovery after weakening.
25+
2. `EchoSecurity.Security` record + `exit-collapses-at` / `audit-no-recovery-at` — the boundary-collapse template structurally mirrors rollback-discards-writes.
26+
3. `EchoNoSectionGeneric.no-section-of-collapsing-map` — the generic lemma the Transaction proof reduces to.
27+
28+
## 3. Proposed upstream extension
29+
30+
A new module `TransactionMutations.agda` in echo-types, structured as follows:
31+
32+
```agda
33+
module TransactionMutations where
34+
35+
-- The write-set carrier: an opaque set of mutations parametrised by
36+
-- the table-type T being mutated.
37+
record WriteSet (T : Set) : Set where
38+
field
39+
applied : List Mutation -- audit trail
40+
-- Algebra: `empty`, `append`, `compose` give a monoid action on
41+
-- DbState — the action and its laws are deferred to a separate
42+
-- module to keep this carrier import-light.
43+
44+
-- The rollback log is the witness that a WriteSet was discarded.
45+
record RollbackLog {T : Set} (ws : WriteSet T) : Set where
46+
field
47+
discarded-at : Timestamp
48+
-- The trivial receipt: rollback emits no recoverable signal.
49+
receipt : Trivial
50+
51+
-- The collapse map that powers the safety proof: every WriteSet
52+
-- rolls back to the trivial receipt, and the generic
53+
-- no-section-of-collapsing-map (already in echo-types) gives the
54+
-- no-recovery lemma free.
55+
rollback-collapses-at :
56+
{T : Set} (ws : WriteSet T) → RollbackLog ws → Trivial
57+
rollback-collapses-at _ _ = trivial
58+
59+
rollback-discards-writes :
60+
{T : Set} (ws : WriteSet T) →
61+
EchoNoSectionGeneric.no-section-of (rollback-collapses-at ws)
62+
rollback-discards-writes ws = no-section-of-collapsing-map _
63+
```
64+
65+
The Security-record instance lives in a sibling `TransactionSecurity.agda`, parametrising `EchoSecurity.Security` by `(Resource := WriteSet T)`, `(Receipt := RollbackLog ws)`, `(exit := rollback-collapses-at ws)`.
66+
67+
## 4. Cross-doc seam
68+
69+
- AffineScript stdlib: `stdlib/Transaction.affine` carries the safety obligation statement in its module-level docstring and references this file by name.
70+
- AffineScript codegen + smoke: `lib/codegen_deno.ml` and `tests/codegen-deno/transaction_smoke.{affine,harness.mjs}` *witness* the property at the Node runtime level by snapshotting tables on `txBegin` and restoring on `txRollback`. The witness is not a proof — it's an executable check that the runtime mock observes the same invariant the formal proof will eventually establish.
71+
- Echo-types upstream: tracked at [`hyperpolymath/echo-types#174`](https://github.com/hyperpolymath/echo-types/issues/174) — proposes the new `TransactionMutations.agda` module shape (and sibling `TransactionSecurity.agda` providing the `Security` instance) reducing to the existing `EchoNoSectionGeneric.no-section-of-collapsing-map`. Acceptance criteria include zero new axioms (`Print Assumptions` clean) and a back-link from the upstream commit SHA into this document.
72+
73+
## 5. Why this matters
74+
75+
Transactions are the canonical user-facing application of affine resource discipline to data: the type system already enforces "at most one consumption" of `Tx`, and the safety theorem closes the loop by saying that the *one* consumption that discards (rollback) is observationally equivalent to never having started. This is the proof every database textbook assumes — having it mechanised against an echo-types carrier means AffineScript's transaction surface inherits the same "no recovery from boundary collapse" story echo-types already proves for region-exit auditing.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<!-- SPDX-License-Identifier: MPL-2.0 -->
2+
<!-- Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk> -->
3+
4+
# DB-Theory #3 — Aggregation-as-Monoid-Homomorphism
5+
6+
**Status**: Carrier wired (`stdlib/Aggregate.affine`, codegen, Deno-ESM smoke); formal proof obligation **pending upstream against [`hyperpolymath/echo-types#175`](https://github.com/hyperpolymath/echo-types/issues/175)**.
7+
8+
## 1. The obligation
9+
10+
For each scalar aggregator `M = (Elem, ε, ⊕)` and any partition `{group_k}` of the row set by key `k`:
11+
12+
**Safety property #DB-3.1 (aggregation-as-fold)**:
13+
```
14+
aggregate(SELECT M(v) FROM t GROUP BY k)
15+
≡ { k ↦ foldr ⊕ ε (map agg group_k) }
16+
```
17+
18+
The aggregators are commutative monoids:
19+
20+
| Aggregator | `Elem` | `ε` | `` | Commutative? | Idempotent? |
21+
|------------|---------------------|-----|-------|--------------|-------------|
22+
| COUNT | `` | `0` | `+` |||
23+
| SUM | `` (or ``, ``) | `0` | `+` |||
24+
| MIN | `ℕ ∪ {∞}` | `` | `min` |||
25+
| MAX | `ℕ ∪ {-∞}` | `-∞`| `max` |||
26+
| AVG | **not a monoid** (no identity) — derived as `SUM/COUNT` |
27+
28+
## 2. Echo-types audit (2026-06-01)
29+
30+
Per owner directive, every proof must first audit `hyperpolymath/echo-types`.
31+
32+
**Finding**: no existing monoid / semiring / aggregation infrastructure today. Closest scaffolding:
33+
34+
1. `EchoCost.CostAlgebra` — left-identity + monotonicity, but no composition law. Reusable as a `Monoid` *instance* once the carrier exists.
35+
2. `Ordinal/Brouwer/OmegaPow.agda#additive-principal` — exactly the monoid closure property for ω^n exponents.
36+
3. `EchoDecorationStructure.agda` — observer-level lattice; aggregation lives at the data level.
37+
4. `docs/adjacency/provenance-semirings.adoc` — explicitly names the distinctness story (echo adds types; semirings add scalars).
38+
39+
**Steer**: minor extension — one new module. Tracked at echo-types#175.
40+
41+
## 3. Proposed upstream extension
42+
43+
A new module `EchoAggregation.agda`:
44+
45+
```agda
46+
record Monoid (ℓ : Level) : Set (suc ℓ) where
47+
field
48+
Elem : Set ℓ
49+
ε : Elem
50+
_⊕_ : Elem → Elem → Elem
51+
assoc : ∀ a b c → (a ⊕ b) ⊕ c ≡ a ⊕ (b ⊕ c)
52+
identity-l : ∀ a → ε ⊕ a ≡ a
53+
identity-r : ∀ a → a ⊕ ε ≡ a
54+
55+
record GroupAggregator {ℓ} (K V : Set) (M : Monoid ℓ) : Set ℓ where
56+
open Monoid M
57+
field
58+
agg : V → Elem
59+
60+
-- Headline lemma (signature — proof may follow in stacked PR):
61+
aggregation-as-fold :
62+
∀ {ℓ} {K V : Set} {M : Monoid ℓ} (ga : GroupAggregator K V M)
63+
→ (rows : List (K × V))
64+
→ (k : K)
65+
→ group-of k (groupBy proj₁ rows)
66+
≡ foldr (_⊕_ ∘ agg) ε (lookup k (partition rows))
67+
```
68+
69+
Plus concrete instances `countMonoid : Monoid ℓ-zero`, `sumMonoid`, `minMonoid`, `maxMonoid`.
70+
71+
## 4. Cross-doc seam
72+
73+
- **AffineScript stdlib**: `stdlib/Aggregate.affine` carries the obligation in its module docstring with the aggregator monoid table.
74+
- **AffineScript codegen + smoke**: `lib/codegen_deno.ml` + `tests/codegen-deno/aggregate_smoke.{affine,harness.mjs}` *witness* the property at the Node runtime level — the mock implements `groupBy` by bucketing rows by key column then folding the aggregator over each bucket. The witness is not a proof; it's an executable check that the runtime mock observes the same invariant the formal proof will eventually establish.
75+
- **Echo-types upstream**: tracked at [`hyperpolymath/echo-types#175`](https://github.com/hyperpolymath/echo-types/issues/175). Once landed, back-link the commit SHA / module path here.
76+
77+
## 5. Why this matters
78+
79+
Aggregation is the most-used non-trivial query shape outside selection/projection. Wiring aggregators as typed commutative monoids (rather than ad-hoc per-shape SQL strings) gives:
80+
81+
- **Distributivity proofs free**: aggregation distributes over filtering (db-theory #4) and indexed scans (db-theory #6) via monoid homomorphism.
82+
- **CRDT bridge for free**: `OR-Set` and `GCounter` (db-theory #9) are precisely *monoids with convergence* — the same carrier extends.
83+
- **Provenance-semiring bridge**: the Green/Karvounarakis/Tannen framing instantiates here once `EchoAggregation` lands.
84+
85+
Sibling: [echo-types#174](https://github.com/hyperpolymath/echo-types/issues/174) (Transaction safety / `no-section-of-collapsing-map`).

lib/codegen_deno.ml

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,31 @@ const __as_dbLastError = (h) => {
560560
const v = globalThis.__as_sqlite.lastError(h);
561561
return v == null ? "" : String(v);
562562
};
563+
// ---- Sqlite transactions (db-theory #2) ----
564+
// `Tx` is an opaque handle; the host adapter is required to
565+
// invalidate it on `commit` / `rollback` so that subsequent calls
566+
// throw a host-side `Error` (the affine type system's
567+
// at-most-one-use guarantee is enforced statically on the AS side;
568+
// this host invariant catches FFI-side aliasing bugs in tests).
569+
const __as_txBegin = (h) => globalThis.__as_sqlite.txBegin(h);
570+
const __as_txCommit = (t) => { globalThis.__as_sqlite.txCommit(t); return 0; };
571+
const __as_txRollback = (t) => { globalThis.__as_sqlite.txRollback(t); return 0; };
572+
const __as_txSavepoint = (t, n) => { globalThis.__as_sqlite.txSavepoint(t, n); return 0; };
573+
const __as_txRelease = (t, n) => { globalThis.__as_sqlite.txRelease(t, n); return 0; };
574+
const __as_txRollbackTo = (t, n) => { globalThis.__as_sqlite.txRollbackTo(t, n); return 0; };
575+
const __as_txDb = (t) => globalThis.__as_sqlite.txDb(t);
576+
const __as_txIsLive = (t) => (globalThis.__as_sqlite.txIsLive(t) ? 1 : 0);
577+
// ---- Sqlite aggregation (db-theory #3 / stdlib/Aggregate.affine) ----
578+
// Each scalar aggregator delegates to a host adapter method that runs
579+
// the SQL, expects a single-row result, and unwraps column 0. `groupBy`
580+
// / `groupCount` return JSON strings (caller parses).
581+
const __as_dbCount = (h, sql, params) => Number(globalThis.__as_sqlite.aggCount(h, sql, params)) | 0;
582+
const __as_dbSum = (h, sql, params) => Number(globalThis.__as_sqlite.aggSum(h, sql, params)) | 0;
583+
const __as_dbMinInt = (h, sql, params) => Number(globalThis.__as_sqlite.aggMinInt(h, sql, params)) | 0;
584+
const __as_dbMaxInt = (h, sql, params) => Number(globalThis.__as_sqlite.aggMaxInt(h, sql, params)) | 0;
585+
const __as_dbAvg = (h, sql, params) => Number(globalThis.__as_sqlite.aggAvg(h, sql, params));
586+
const __as_dbGroupBy = (h, sql, params) => String(globalThis.__as_sqlite.groupBy(h, sql, params));
587+
const __as_dbGroupCount = (h, table, keyCol) => String(globalThis.__as_sqlite.groupCount(h, table, keyCol));
563588
const __as_httpFetch = async (url, method, headers, bodyOpt) => {
564589
const init = { method, headers: __as_httpHeadersToObject(headers) };
565590
if (bodyOpt && bodyOpt.tag === "Some") init.body = bodyOpt.value;
@@ -866,7 +891,24 @@ let () =
866891
b "db_table_exists" (fun a -> Printf.sprintf "__as_dbTableExists(%s, %s)" (arg 0 a) (arg 1 a));
867892
b "db_import_csv" (fun a -> Printf.sprintf "__as_dbImportCsv(%s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a));
868893
b "db_export_csv" (fun a -> Printf.sprintf "__as_dbExportCsv(%s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a));
869-
b "db_last_error" (fun a -> Printf.sprintf "__as_dbLastError(%s)" (arg 0 a))
894+
b "db_last_error" (fun a -> Printf.sprintf "__as_dbLastError(%s)" (arg 0 a));
895+
(* ---- Sqlite transactions (db-theory #2 / stdlib/Transaction.affine) ---- *)
896+
b "tx_begin" (fun a -> Printf.sprintf "__as_txBegin(%s)" (arg 0 a));
897+
b "tx_commit" (fun a -> Printf.sprintf "__as_txCommit(%s)" (arg 0 a));
898+
b "tx_rollback" (fun a -> Printf.sprintf "__as_txRollback(%s)" (arg 0 a));
899+
b "tx_savepoint" (fun a -> Printf.sprintf "__as_txSavepoint(%s, %s)" (arg 0 a) (arg 1 a));
900+
b "tx_release" (fun a -> Printf.sprintf "__as_txRelease(%s, %s)" (arg 0 a) (arg 1 a));
901+
b "tx_rollback_to" (fun a -> Printf.sprintf "__as_txRollbackTo(%s, %s)" (arg 0 a) (arg 1 a));
902+
b "tx_db" (fun a -> Printf.sprintf "__as_txDb(%s)" (arg 0 a));
903+
b "tx_is_live" (fun a -> Printf.sprintf "__as_txIsLive(%s)" (arg 0 a));
904+
(* ---- Sqlite aggregation (db-theory #3 / stdlib/Aggregate.affine) ---- *)
905+
b "db_count" (fun a -> Printf.sprintf "__as_dbCount(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
906+
b "db_sum" (fun a -> Printf.sprintf "__as_dbSum(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
907+
b "db_min_int" (fun a -> Printf.sprintf "__as_dbMinInt(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
908+
b "db_max_int" (fun a -> Printf.sprintf "__as_dbMaxInt(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
909+
b "db_avg" (fun a -> Printf.sprintf "__as_dbAvg(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
910+
b "db_group_by" (fun a -> Printf.sprintf "__as_dbGroupBy(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
911+
b "db_group_count" (fun a -> Printf.sprintf "__as_dbGroupCount(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a))
870912

871913
(* ============================================================================
872914
Identifier sanitisation (JS reserved words -> trailing underscore)

stdlib/Aggregate.affine

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
//
4+
// Aggregate.affine — SQL group-by + aggregation primitives (db-theory #3).
5+
//
6+
// Aggregation is a thin convenience layer over the existing Sqlite
7+
// extern surface. The runtime contract is: each aggregator extern
8+
// runs a SQL query that returns *exactly one row* with the aggregate
9+
// in column 0, and that result is unwrapped to a typed scalar
10+
// (`Int` / `Float`). The `db_group_by` extern returns the whole
11+
// grouped result-set as a JSON array — caller parses with `Json::parse`.
12+
//
13+
// **Proof obligation #DB-3.1 (aggregation-as-monoid-homomorphism)**
14+
// Each scalar aggregator is a commutative monoid; group-by is the
15+
// indexed sum. Formally, for any aggregator `M` (with identity `ε`
16+
// and combine `⊕`) and any partition `{group_k}` of the row set:
17+
// aggregate(SELECT M(v) FROM t GROUP BY k)
18+
// = { k ↦ foldr ⊕ ε (map M.agg group_k) }
19+
//
20+
// Concretely: COUNT is `(ℕ, 0, +)`, SUM is `(ℕ, 0, +)`, MIN is
21+
// `(ℕ ∪ {∞}, ∞, min)`, MAX is `(ℕ ∪ {-∞}, -∞, max)`. AVG is *not*
22+
// a monoid (no identity) — express as `SUM/COUNT`.
23+
//
24+
// Status: pending against `hyperpolymath/echo-types#175` — proposes
25+
// a new `EchoAggregation.agda` module with `Monoid` + `GroupAggregator`
26+
// carriers reducing to the standard provenance-semiring framing
27+
// (Green / Karvounarakis / Tannen). Cross-doc:
28+
// `docs/academic/proofs/db-theory-3-aggregation-as-fold.md`.
29+
//
30+
// **Why this is the natural db-theory #3**
31+
// Aggregation is the most-used non-trivial query shape outside
32+
// selection/projection — every SQL report depends on it. Wiring
33+
// aggregation through typed monoids (rather than ad-hoc strings)
34+
// gives us the foundation for later db-theory items: #4 (selection
35+
// distributivity over agg), #6 (agg over indexed scans), #9 (CRDTs
36+
// as monoids with convergence).
37+
38+
module Aggregate;
39+
40+
// ── Scalar aggregators (single-row result) ─────────────────────────
41+
//
42+
// Each extern runs a SQL returning exactly one row with the aggregate
43+
// in column 0. `params_json` follows the same JSON-array contract as
44+
// the `db_query*` family. Empty result-sets degenerate to the
45+
// aggregator's identity (`0` for COUNT/SUM, runtime-defined for MIN/MAX).
46+
47+
/// COUNT — returns the cardinality of the result-set. SQL form:
48+
/// `SELECT COUNT(*) FROM t WHERE ...` or `SELECT COUNT(col) FROM t ...`.
49+
/// Identity: 0. Monoid: `(ℕ, 0, +)`.
50+
pub extern fn db_count(d: Db, sql: String, params_json: String) -> Int;
51+
52+
/// SUM — returns the sum of an integer-valued expression. SQL form:
53+
/// `SELECT SUM(col) FROM t WHERE ...`. Identity: 0. Monoid: `(ℕ, 0, +)`.
54+
pub extern fn db_sum(d: Db, sql: String, params_json: String) -> Int;
55+
56+
/// MIN — returns the minimum integer value. SQL form:
57+
/// `SELECT MIN(col) FROM t WHERE ...`. Identity: ∞ (runtime: sentinel).
58+
/// Monoid: `(ℕ ∪ {∞}, ∞, min)`.
59+
pub extern fn db_min_int(d: Db, sql: String, params_json: String) -> Int;
60+
61+
/// MAX — returns the maximum integer value. SQL form:
62+
/// `SELECT MAX(col) FROM t WHERE ...`. Identity: -∞. Monoid:
63+
/// `(ℕ ∪ {-∞}, -∞, max)`.
64+
pub extern fn db_max_int(d: Db, sql: String, params_json: String) -> Int;
65+
66+
/// AVG — returns the average as a Float. **NOT a monoid** (no identity);
67+
/// implemented as `SUM/COUNT`. The host adapter rounds following IEEE 754.
68+
pub extern fn db_avg(d: Db, sql: String, params_json: String) -> Float;
69+
70+
// ── Multi-row aggregation (GROUP BY) ───────────────────────────────
71+
//
72+
// `db_group_by` runs a SQL with a GROUP BY clause and returns the
73+
// entire result-set as a JSON array. Each element is itself a JSON
74+
// array of column values, in the order declared in the SELECT list.
75+
// Caller parses with `Json::parse` and pattern-matches the shape.
76+
//
77+
// Example:
78+
// db_group_by(d, "SELECT dept, COUNT(*), SUM(salary) FROM e GROUP BY dept", "[]")
79+
// -> '[["eng", 12, 1450000], ["sales", 5, 420000], ...]'
80+
//
81+
// The shape contract is *adapter-enforced*: a missing GROUP BY clause
82+
// still works (single-row result) but defeats the purpose. A future
83+
// typed `Group<K, V>` carrier would lift this to a parametric type;
84+
// blocked on the dependent-type story for parametric externs
85+
// (typed bindings #DB-3.2).
86+
87+
pub extern fn db_group_by(d: Db, sql: String, params_json: String) -> String;
88+
89+
// ── Group-bucket count (convenience over db_group_by) ──────────────
90+
//
91+
// `db_group_count` returns the count-per-group as a JSON array of
92+
// `[key, count]` pairs. Equivalent to:
93+
// `db_group_by(d, "SELECT key, COUNT(*) FROM t GROUP BY key", ...)`
94+
// — but encapsulates the SELECT-list shape so callers don't need to
95+
// inline the COUNT(*) projection. The key column is referenced by
96+
// name (`key_col`), the table by name (`table`).
97+
98+
pub extern fn db_group_count(d: Db, table: String, key_col: String) -> String;

0 commit comments

Comments
 (0)