Skip to content

Commit f31fcc3

Browse files
ahrzbclaude
andcommitted
feat: widen marginalization to almost all window queries
One rule decides everything: a window is marginalizable iff its value is a function of row-visible values — the partition keys plus, when ORDER BY discriminates, the order values (RANGE/GROUPS peers share values). Newly admitted: running aggregates and RANGE/GROUPS frames (order values join the key set), whole-partition frames, the rank family, first_value/last_value/nth_value, FILTER, DISTINCT and ordered arguments inside aggregates, IGNORE NULLS on value functions, expression partition/order keys (joined by the qualified expression), COLLATE order keys (stripped to the raw child so multiplicity holds), and named windows (inlined by the oracle's parser for free). The aggregate allowlist is gone: the two-stage fit reruns the original computation, so any WINDOW_AGGREGATE — including order-sensitive ones — freezes exactly the value the original text produced. Still refused, each with a named error: row_number, ntile, lag, lead, bounded ROWS frames, EXCLUDE, non-constant frame bounds — physical position is the one thing a join key cannot carry. Gate: training-set round-trip invariant over the widened surface, 2000-case seeded fuzz bit-exact; the gate comparison is now NaN-aware and signed-zero-strict (corr produces NaN, and pyarrow equals says NaN != NaN). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e227a0d commit f31fcc3

5 files changed

Lines changed: 458 additions & 120 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Widening marginalization: almost all window queries
2+
3+
**Date:** 2026-07-29
4+
**Status:** loop 2 of SQLProjection marginalization; extends
5+
[loop 1](2026-07-29-sql-projection-marginalization-design.md), whose
6+
architecture (oracle parser, two-stage fit, training-set round-trip
7+
invariant) is unchanged.
8+
9+
## The one rule
10+
11+
A window expression is marginalizable iff its value is a **function of
12+
row-visible values**: the partition keys, plus — once ORDER BY enters — the
13+
order values. RANGE/GROUPS frame semantics make all order-peers share a
14+
value, so the value is constant within (partition keys ∪ order values) and a
15+
params table keyed on that tuple reproduces it exactly. What can never be
16+
carried by a join key is **physical position**: row_number, ntile, lag/lead,
17+
and bounded ROWS frames distinguish tied rows, so they stay refused.
18+
19+
Every window is assigned a discriminator set D:
20+
21+
| window | D (extra join keys beyond PARTITION BY) |
22+
|---|---|
23+
| aggregate, no ORDER BY (default frame) ||
24+
| aggregate, any frame `UNBOUNDED PRECEDING..UNBOUNDED FOLLOWING` ||
25+
| aggregate, ORDER BY + default / RANGE / GROUPS frame (constant bounds) | order exprs |
26+
| `rank`, `dense_rank`, `percent_rank`, `cume_dist` | order exprs |
27+
| `first_value`, default or whole-partition frame ||
28+
| `first_value`/`last_value`/`nth_value` (constant n), RANGE/GROUPS frames | order exprs |
29+
| **refused:** `row_number`, `ntile`, `lag`, `lead`; bounded ROWS frames; `EXCLUDE …`; non-constant frame bounds ||
30+
31+
Join keys = partition entries ∪ D, and both may now be **expressions**
32+
(`PARTITION BY lower(c)`, `ORDER BY o % 10`): validated row-wise (no
33+
windows/aggregates/subqueries inside), projected in `windows_sql`, joined
34+
back by the qualified expression `ON (lower(__cf_t.c) IS NOT DISTINCT FROM
35+
__cf_p0.__cf_x0)`. Plain-column keys keep their natural name in the params
36+
table; expression keys are named `__cf_xJ`. `COLLATE` on an order key is
37+
stripped to its child for keying: raw-equal implies collated-equal implies
38+
peer, so joining on the raw value preserves multiplicity (collated-equal but
39+
raw-distinct values get their own params rows carrying the same value —
40+
denormalized, still exact; joining on the collated value would match several
41+
rows and break multiplicity).
42+
43+
## The aggregate allowlist is gone
44+
45+
Loop 1's allowlist encoded "deterministic under GROUP BY refit". The
46+
two-stage fit removed that dependency: `windows_sql` reruns the original
47+
computation and the collapse picks values, so *any* function the oracle
48+
classifies as `WINDOW_AGGREGATE` is per-key-constant within the execution —
49+
including order-sensitive ones (`string_agg`, `array_agg`, `first`) and
50+
ordered-argument aggregates (`string_agg(x ORDER BY o)`), whose frozen value
51+
is precisely the fitted semantics. `FILTER (WHERE …)` and `DISTINCT` inside
52+
aggregates are likewise admitted (the filter expression validated row-wise).
53+
`IGNORE NULLS` is admitted on value functions, refused on aggregates.
54+
The bare-aggregate-without-OVER refusal (checked against
55+
`duckdb_functions()`) is unchanged.
56+
57+
Named windows (`WINDOW w AS (…)` + `OVER w`) are inlined by the oracle's
58+
parser — supported with zero code.
59+
60+
## Multiplicity, restated
61+
62+
For every admitted window, the value is constant within its key tuple (the
63+
table above is exactly that claim, case by case), so `SELECT DISTINCT keys,
64+
aggs FROM __CF_WINDOWS__` collapses to one row per key tuple, keys are
65+
unique, and the LEFT JOIN matches exactly one params row per input row. The
66+
training-set round-trip invariant remains the gate, now over the widened
67+
surface: the fuzz generator gains ORDER BY windows, rank/value functions,
68+
frames, FILTER, DISTINCT aggregates, expression keys, and order-sensitive
69+
aggregates.
70+
71+
## Still refused after this loop
72+
73+
Position-dependent windows (`row_number`, `ntile`, `lag`, `lead`, bounded
74+
ROWS frames, `EXCLUDE`) — a join key cannot carry physical position;
75+
non-constant frame bounds and non-constant `nth_value` n; nested
76+
windows/aggregates/subqueries inside window arguments, keys, or filters; and
77+
every loop-1 structural refusal (WHERE, GROUP BY, joins, CTEs, …).

packages/sql-transform/README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@ loop and still raises `NotImplementedError`.
2828

2929
## The contract
3030

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.
31+
Strict projection: `SELECT <exprs> FROM __THIS__`, where the window surface is
32+
wide: any aggregate (including `FILTER`, `DISTINCT`, ordered arguments, and
33+
order-sensitive ones), running windows and RANGE/GROUPS frames (the order
34+
values join the key set), the rank family, `first_value`/`last_value`/
35+
`nth_value`, expression partition keys, and named windows. The rule deciding
36+
it all: a window is marginalizable iff its value is a function of row-visible
37+
values. What depends on **physical row position**`row_number`, `ntile`,
38+
`lag`/`lead`, bounded ROWS frames, `EXCLUDE` — is refused, as is everything
39+
non-projection (WHERE, GROUP BY, joins, subqueries, CTEs), each with a named
40+
error. Serve-or-refuse, no third mode.
3541

3642
Parsing is DuckDB's own (`json_serialize_sql` / `json_deserialize_sql`): the
3743
oracle's grammar and the oracle's printer, no second SQL dialect anywhere.

0 commit comments

Comments
 (0)