Skip to content

Commit 114c642

Browse files
ahrzbclaude
andcommitted
feat: SQLProjection — marginalization over __THIS__
Rename SQLTransform to SQLProjection (strict projections only) and implement the fit half: window aggregates over __THIS__ are marginalized into materialized params tables plus a rewritten serving_sql that LEFT JOINs them via IS NOT DISTINCT FROM. Parsing is DuckDB's own json_serialize_sql/json_deserialize_sql — the oracle's grammar and printer, no second SQL dialect. Confit is untouched. Fit is two-stage, both stages fuzz-derived: windows_sql reruns the original computation with each window re-projected (GROUP BY and solo window queries drift by an ulp — DuckDB sums floats in operator-chain- dependent order), then per-keyset SELECT DISTINCT collapses the materialized values. Fit pins threads=1: DuckDB's parallel window aggregation is not bit-deterministic (measured 1/500). The gate is differential: original SQL over train == serving_sql joined to fitted params, both executed by DuckDB, bit-exact including schema — plus a seeded 500-case fuzz, AST shape pins, and a named-refusal table. Serving (infer/infer_batch through Confit) stays NotImplementedError — the named next loop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e9df096 commit 114c642

13 files changed

Lines changed: 1379 additions & 201 deletions

README.md

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,7 @@ This repository is a workspace of two packages:
1010
| package | what it is |
1111
|---|---|
1212
| [`packages/confit`](packages/confit) | **Confit** — the serving engine. SQL plus static tables frozen at fit time are partially evaluated, once, into a native function. Serves bit-exact with DuckDB or refuses at build time. Usable on its own. |
13-
| [`packages/sql-transform`](packages/sql-transform) | The authoring surface: `SQLTransform`. **Not implemented** — signatures only, being rebuilt on Confit. |
14-
15-
> **sql-transform is a stub.** Its implementation was deliberately removed --
16-
> the DataFusion engine, the codegen backend, the batch `transform()` path and
17-
> the fit pipeline are all gone. What remains is the interface, raising
18-
> `NotImplementedError`, as the contract for a rebuild on Confit.
19-
> **Confit itself is complete and unaffected.**
13+
| [`packages/sql-transform`](packages/sql-transform) | The authoring surface: `SQLProjection`. The **fit half works**: window aggregates over `__THIS__` are marginalized into materialized params tables plus a rewritten serving SQL. The serving half (through Confit) is a later loop. |
2014

2115
## Installation
2216

@@ -67,23 +61,35 @@ fn.infer_rows([Row(age=40.0)]) # row objects in, row objects out
6761
fn.infer_arrow(pa.table({"age": [40.0]})) # pa.Table in, pa.Table out
6862
```
6963

70-
`sql_transform.SQLTransform` is the intended authoring layer on top of it --
71-
write SQL with window aggregates, `fit()` to freeze them, then serve. It is
72-
currently **not implemented**; see [packages/sql-transform](packages/sql-transform).
64+
`sql_transform.SQLProjection` is the authoring layer on top of it — write SQL
65+
with window aggregates, `fit()` to freeze them. The fit half works today; see
66+
[packages/sql-transform](packages/sql-transform).
67+
68+
```python
69+
from sql_transform import SQLProjection
70+
71+
p = SQLProjection(
72+
"SELECT (age - avg(age) OVER (PARTITION BY country)) AS d FROM __THIS__"
73+
).fit(train)
74+
p.serving_sql # the rewritten projection: params joins instead of aggregates
75+
p.params # {"__CF_PARAMS_0__": <pyarrow.Table>}
76+
```
7377

7478
## Architecture
7579

76-
The intended two-phase shape, of which **only the second phase exists today**:
80+
The two-phase shape — **fit works, the wiring between the phases is the next
81+
loop**:
7782

7883
```
7984
SQL over __THIS__
8085
8186
82-
fit(train) ── freeze each window aggregate (e.g. MEAN(age)) into a typed
83-
│ __STATE__ table and rewrite the SQL to reference it instead
84-
│ of recomputing. [NOT IMPLEMENTED]
87+
fit(train) ── marginalize each window aggregate (e.g. avg(age) OVER
88+
│ (PARTITION BY country)) into a materialized params table and
89+
│ rewrite the SQL to LEFT JOIN it instead of recomputing.
90+
│ Bit-exact with DuckDB by differential gate. [WORKS]
8591
86-
│ rewritten SQL + frozen state
92+
│ rewritten SQL + frozen params [wiring: NOT IMPLEMENTED]
8793
8894
Confit ── partially evaluates the pair into a native function: binding-time
8995
│ analysis collapses every static lookup into a prepare-time probe,
@@ -105,9 +111,10 @@ The expression surface, joins to static tables, the row-shape contract
105111
mined from DuckDB's own test suite replay bit-exact, with the remainder clean,
106112
named build-time rejections.
107113

108-
The authoring layer on top — window-aggregate `fit`, typed I/O, sklearn
109-
transformer references, and composing one transform into another — is what the
110-
rebuild has to supply.
114+
The authoring layer's window-aggregate `fit` (marginalization) works; typed
115+
I/O, serving through Confit, static tables in authored SQL, and the broader
116+
DRAFT-20 program (fitted model artifacts as params tables) are the named next
117+
loops.
111118

112119
## Reports
113120

docs/superpowers/specs/2026-07-29-sql-projection-marginalization-design.md

Lines changed: 78 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,52 @@ FROM __THIS__
2323

2424
```sql
2525
-- serving_sql (generated)
26-
SELECT (t.age - p0.__cf_a0) / p0.__cf_a1 AS age_z,
27-
t.fare - p1.__cf_a0 AS fare_c
28-
FROM __THIS__ AS t
29-
LEFT JOIN __CF_PARAMS_0__ AS p0 ON t.country IS NOT DISTINCT FROM p0.country
30-
CROSS JOIN __CF_PARAMS_1__ AS p1
26+
SELECT ((__cf_t.age - __cf_p0.__cf_a0) / __cf_p0.__cf_a1) AS age_z,
27+
(__cf_t.fare - __cf_p1.__cf_a0) AS fare_c
28+
FROM __THIS__ AS __cf_t
29+
LEFT JOIN __CF_PARAMS_0__ AS __cf_p0
30+
ON ((__cf_t.country IS NOT DISTINCT FROM __cf_p0.country))
31+
LEFT JOIN __CF_PARAMS_1__ AS __cf_p1 ON ((1 = 1))
3132
```
3233

3334
```sql
34-
-- params[0].fit_sql (keys = [country])
35-
SELECT country, avg(age) AS __cf_a0, stddev_samp(age) AS __cf_a1
36-
FROM __THIS__ GROUP BY country
35+
-- windows_sql: ONE fit-time execution — the original select list verbatim
36+
-- (chain-pinning, discarded), each distinct window re-projected, the keys:
37+
SELECT (age - avg(age) OVER (PARTITION BY country))
38+
/ stddev_samp(age) OVER (PARTITION BY country) AS __cf_o0,
39+
(fare - avg(fare) OVER ()) AS __cf_o1,
40+
avg(age) OVER (PARTITION BY country) AS __cf_w0,
41+
stddev_samp(age) OVER (PARTITION BY country) AS __cf_w1,
42+
avg(fare) OVER () AS __cf_w2,
43+
country AS __cf_k0
44+
FROM __THIS__
45+
-- params[0].fit_sql (keys = [country]) — pure value picking, no arithmetic:
46+
SELECT DISTINCT __cf_k0 AS country, __cf_w0 AS __cf_a0, __cf_w1 AS __cf_a1
47+
FROM __CF_WINDOWS__
3748
-- params[1].fit_sql (keys = [])
38-
SELECT avg(fare) AS __cf_a0 FROM __THIS__
49+
SELECT DISTINCT __cf_w2 AS __cf_a0 FROM __CF_WINDOWS__
3950
```
4051

41-
`fit(train)` runs each `fit_sql` over the training table via DuckDB and stores
42-
the resulting tables. Serving (a later slice) hands `serving_sql` + params to
43-
Confit as static tables.
52+
`fit(train)` registers the training table, runs `windows_sql` once
53+
(single-threaded, see below), registers the materialized result as
54+
`__CF_WINDOWS__`, and collapses each params table out of it with SELECT
55+
DISTINCT — every allowlisted aggregate is deterministic per group, so the
56+
tuple is constant within a group and DISTINCT yields exactly one row per
57+
group. Serving (a later slice) hands `serving_sql` + params to Confit as
58+
static tables.
59+
60+
**Why fit is two-stage — measured, not designed.** The differential fuzz
61+
killed two simpler forms. A `GROUP BY` fit drifts from the original text by
62+
an ulp: DuckDB's group-by and window aggregates sum floats in different
63+
orders. A standalone per-keyset window query drifts too: DuckDB chains window
64+
operators, each reordering rows for the next, so a float aggregate's
65+
summation order depends on which *other* windows share the query. Keeping the
66+
original select items in `windows_sql` pins the operator chain, and
67+
re-projecting an already-present window is CSE'd — the original text's value,
68+
bit-exactly. The last ulp source is parallelism itself: DuckDB's parallel
69+
window aggregation is schedule-dependent (measured 1/500 cases), so fit pins
70+
`SET threads = 1`, making params deterministic and machine-reproducible; the
71+
gate compares both sides single-threaded.
4472

4573
## Architecture: parse with the oracle
4674

@@ -80,16 +108,23 @@ regardless of parser.
80108
normalization) within a key set dedupe to one column.
81109
2. **Join predicate is `IS NOT DISTINCT FROM`, never `USING`/`=`.** Window
82110
`PARTITION BY` groups NULL keys into one partition; equality joins would
83-
drop them. `OVER ()` (empty key set) becomes `CROSS JOIN` against a
84-
one-row table.
111+
drop them. `OVER ()` (empty key set) joins its one-row table via
112+
`LEFT JOIN … ON (1 = 1)` — never a CROSS join, because the oracle prints
113+
CROSS joins in comma form, which re-parses with different associativity
114+
once another join follows (fuzz-found).
85115
3. **Multiplicity by construction:** `fit_sql` is `GROUP BY keys` ⇒ keys are
86116
unique ⇒ LEFT JOIN matches ≤ 1 ⇒ exactly one row out per row in. Map shape
87117
is proven, not tested.
88118
4. **`__CF_` is a reserved prefix** (params tables `__CF_PARAMS_N__`, columns
89119
`__cf_aN`). Input SQL containing it (case-insensitive) is refused — same
90120
idiom as Confit's `__glob_pat` reservation.
91-
5. `__THIS__` gains the alias `t` in `serving_sql`; base-column references are
92-
qualified through it.
121+
5. `__THIS__` gains the alias `__cf_t` in `serving_sql`; base-column
122+
references are qualified through it (a short alias like `t` could collide
123+
with a user column; the reserved prefix cannot). Unaliased non-column
124+
select items get their derived name frozen as an explicit alias first —
125+
DuckDB names such columns by their own printed text, which qualification
126+
would change. Plain column refs are exempt: their name is the last path
127+
part, untouched by qualification.
93128

94129
## Accepted surface and refusals
95130

@@ -100,11 +135,13 @@ refusal raises a named, positioned error — serve-or-refuse, no third mode.
100135
`agg(expr…) OVER ()`, where:
101136

102137
- `agg` is on the **allowlist** (grown one measured entry at a time, like the
103-
corpus): `avg`, `sum`, `count`, `min`, `max`, `stddev`, `stddev_pop`,
104-
`stddev_samp`, `var_pop`, `var_samp`, `variance`, `median`. Order-sensitive
105-
aggregates (`first`, `last`, `string_agg`, `array_agg`, …) are refused by
138+
corpus): `avg`, `sum`, `count`, `count_star`, `min`, `max`, `stddev`,
139+
`stddev_pop`, `stddev_samp`, `var_pop`, `var_samp`, `variance`, `median`.
140+
Order-sensitive aggregates (`string_agg`, `array_agg`, …) are refused by
106141
absence: their per-group value is nondeterministic, which would make both
107-
fit and the differential gate flaky.
142+
fit and the differential gate flaky. (`first`/`last` never reach the
143+
allowlist check — the oracle classifies them as their own window types, so
144+
they are refused as position-dependent window functions.)
108145
- Partition keys are plain `__THIS__` columns (expressions: next loop).
109146
- Aggregate arguments are arbitrary expressions over `__THIS__` columns —
110147
computed training-side, no restriction beyond what DuckDB accepts.
@@ -148,14 +185,23 @@ class SQLProjection:
148185
def boundary(self) -> str: ...
149186
```
150187

151-
Internally, `marginalize(sql) -> Marginalized(serving_sql, params_specs)` is a
152-
pure function (parse + rewrite + plan, no execution); `fit` = marginalize +
153-
materialize. `this_model` is accepted for signature stability but unused this
154-
slice (the training table brings its own schema).
188+
Internally, `marginalize(sql) -> Marginalized(serving_sql, windows_sql,
189+
params)` is a pure function (parse + rewrite + plan, no execution); `fit` =
190+
marginalize + materialize (two stages, see above). `this_model` is accepted
191+
for signature stability but unused this slice (the training table brings its
192+
own schema).
193+
194+
The raw AST dicts are handled under a two-tier typing discipline: subtrees
195+
merely carried pass through as opaque `Node` dicts, while every node the
196+
module *interprets* is read through a pydantic view that validates shape at
197+
the read site — a DuckDB format change fails as one named "AST shape drift"
198+
error, not a `KeyError` mid-walk.
155199

156200
## The bulletproof gate
157201

158-
DuckDB-vs-DuckDB differential — no inference code anywhere:
202+
DuckDB-vs-DuckDB differential — no inference code anywhere, both sides at
203+
`threads = 1` (the only setting where the oracle's own float window
204+
aggregation is bit-deterministic):
159205

160206
```python
161207
original = duck(sql, __THIS__=train)
@@ -172,6 +218,13 @@ unicode/quoted identifiers. Plus:
172218
- **AST pins:** executed `json_serialize_sql` examples for every node shape the
173219
walker relies on, so a DuckDB bump that moves the format fails loudly.
174220
- Column-order and dtype equality in the differential, not just values.
221+
- **Seeded differential fuzz** (`MARGINALIZE_FUZZ_N`, default 25; deep runs at
222+
500+): random typed tables with NULLs everywhere × random projections. The
223+
fuzz found every deep bug in this slice: the CROSS-join comma-form
224+
associativity trap, both float summation-order drifts, and the degenerate
225+
`pa.null()`-typed-column coercion (an untyped training column is rejected
226+
territory for a later loop — the generator now types its columns
227+
explicitly).
175228

176229
## Out of scope (named next loops)
177230

packages/sql-transform/README.md

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,47 @@
11
# sql-transform
22

3-
The authoring surface for SQL feature transforms — **not implemented**.
3+
`SQLProjection` — projections over `__THIS__`, fit once, serve row-at-a-time.
4+
5+
The **fit half works today**: window aggregates over `__THIS__` are
6+
*marginalized* — computed once over training data, materialized into params
7+
tables, and the SQL rewritten to join them instead of recomputing:
48

59
```python
6-
from sql_transform import SQLTransform
10+
from sql_transform import SQLProjection
11+
12+
p = SQLProjection(
13+
"SELECT (age - avg(age) OVER (PARTITION BY country))"
14+
" / stddev_samp(age) OVER (PARTITION BY country) AS age_z FROM __THIS__"
15+
).fit(train) # train: pyarrow.Table
716

8-
t = SQLTransform(sql) # NotImplementedError
17+
p.serving_sql
18+
# SELECT (__cf_t.age - __cf_p0.__cf_a0) / __cf_p0.__cf_a1 AS age_z
19+
# FROM __THIS__ AS __cf_t
20+
# LEFT JOIN __CF_PARAMS_0__ AS __cf_p0
21+
# ON (__cf_t.country IS NOT DISTINCT FROM __cf_p0.country)
22+
p.params
23+
# {"__CF_PARAMS_0__": <pyarrow.Table: country, __cf_a0, __cf_a1>}
924
```
1025

11-
Every method raises `NotImplementedError`. The signatures, defaults and return
12-
types are all that remain, and they are the contract a rebuild has to satisfy:
13-
14-
| method | intended behaviour |
15-
|---|---|
16-
| `SQLTransform(sql)` | accept SQL (or a t-string) over `__THIS__` |
17-
| `from_file(path)` | same, read from a file |
18-
| `fit(table, this_model=None)` | freeze window-aggregate state into static tables, rewrite the SQL to reference them, specialize via Confit; returns self |
19-
| `infer(row)` / `infer_batch(rows)` | serve rows through the specialized function |
20-
| `backend` / `boundary` | report which engine and boundary the fitted function uses |
21-
22-
The serving half already exists and is unaffected: [Confit](../confit) takes SQL
23-
plus frozen static tables and partially evaluates them into a native function,
24-
bit-exact with DuckDB or refusing at build time. What was removed here is the
25-
fit half, the DataFusion batch engine, the codegen backend, and transform
26-
composition.
27-
28-
Tests in this package assert only that the surface exists and refuses honestly —
29-
they fail if a method is dropped, renamed, or quietly starts returning something.
26+
The serving half (`infer`/`infer_batch` through [Confit](../confit)) is a later
27+
loop and still raises `NotImplementedError`.
28+
29+
## The contract
30+
31+
Strict projection: `SELECT <exprs> FROM __THIS__`. Everything else — WHERE,
32+
GROUP BY, joins, subqueries, CTEs, running windows, order-sensitive
33+
aggregates — is refused at construction with a named error. Serve-or-refuse,
34+
no third mode.
35+
36+
Parsing is DuckDB's own (`json_serialize_sql` / `json_deserialize_sql`): the
37+
oracle's grammar and the oracle's printer, no second SQL dialect anywhere.
38+
39+
The correctness gate is differential: for every accepted projection, the
40+
original SQL over training data must be **bit-exact** with `serving_sql`
41+
joined against the fitted params — both executed by DuckDB (single-threaded:
42+
DuckDB's parallel float window aggregation is not bit-deterministic even
43+
against itself). NULL partition keys are one partition and join back via
44+
`IS NOT DISTINCT FROM`; join multiplicity is provable, not tested.
45+
46+
See the design spec:
47+
[2026-07-29-sql-projection-marginalization-design.md](../../docs/superpowers/specs/2026-07-29-sql-projection-marginalization-design.md).

packages/sql-transform/pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
[project]
22
name = "sql-transform"
33
version = "0.1.0"
4-
description = "SQL feature transforms — authoring surface, being rebuilt on Confit"
4+
description = "SQL projections over __THIS__ — fit marginalizes aggregates via DuckDB, serving via Confit"
55
readme = "README.md"
66
requires-python = ">=3.14"
7-
# Only what the surface itself references. The fit-time engine (datafusion,
8-
# sqlglot, numpy) went with the implementation; add back what the rebuild needs.
7+
# duckdb is the fit-time engine and the parser (json_serialize_sql); serving
8+
# will come through confit and never needs it.
99
dependencies = [
10+
"duckdb>=1.5.5",
1011
"pyarrow>=19.0",
1112
"pydantic>=2.0,<3.0",
1213
]
Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
1-
"""SQLTransformthe authoring surface, awaiting reimplementation.
1+
"""SQLProjectionprojections over ``__THIS__``, fit once, serve row-at-a-time.
22
3-
Every method raises `NotImplementedError`. The signatures are the contract a
4-
rebuild has to satisfy; the previous implementation (a DataFusion batch engine,
5-
a codegen backend, and transform composition) was deleted deliberately.
6-
7-
The serving half already exists and is unaffected: confit takes SQL plus static
8-
tables frozen at fit time and partially evaluates them into a native function.
3+
The fit half works today: ``fit(table)`` marginalizes every window aggregate
4+
into materialized params tables plus a rewritten ``serving_sql``. The serving
5+
half (``infer``/``infer_batch`` through Confit) is a later loop and raises
6+
``NotImplementedError``.
97
"""
108

119
from __future__ import annotations
1210

13-
from sql_transform._transform import SQLTransform
11+
from sql_transform._marginalize import (
12+
Marginalized,
13+
MarginalizeError,
14+
ParamsSpec,
15+
marginalize,
16+
)
17+
from sql_transform._projection import SQLProjection
1418

15-
__all__ = ["SQLTransform"]
19+
__all__ = [
20+
"Marginalized",
21+
"MarginalizeError",
22+
"ParamsSpec",
23+
"SQLProjection",
24+
"marginalize",
25+
]

0 commit comments

Comments
 (0)