gregfelice commented on PR #2435:
URL: https://github.com/apache/age/pull/2435#issuecomment-4763292009
Reviewed the full diff and the changed files in context, with two deep
passes on the executor/planner mechanics. I independently verified the two
subtle correctness traps this kind of desugaring is prone to — the
COALESCE-NULL ambiguity and PARAM_EXEC isolation — and both are genuinely
handled. Solid work; no blockers or majors. (Source-level review; I could not
run `make` here — the 38/38 installcheck is the author's.)
### Verified correct
- **The empty-list vs. fold-to-null ambiguity is correctly resolved** — the
trap I most expected here. `age_reduce_transfn` never returns SQL NULL: a null
fold result is normalized to an agtype `'null'` varlena
(`agtype.c:11730-11733`), and the accumulator is seeded the same way. So the
aggregate returns SQL NULL *only* over zero rows. The desugaring `CASE WHEN
list IS NULL THEN NULL ELSE COALESCE(<agg>, init) END` therefore reads
correctly: empty list -> SQL NULL -> `init`; a legitimate fold-to-null ->
agtype `'null'` (not SQL NULL) -> passes through COALESCE. The `[1, null, 3] ->
null` test (`age_reduce.sql:186`) exercises exactly this. The classic bug is
not present.
- **By-value Datum safety holds.** The body is normalized to agtype at
transform time, and the transfn independently re-checks `exprType(body_node) ==
AGTYPEOID` at runtime (`agtype.c:11680`) because `age_reduce` is SQL-callable,
so `datumCopy(result, false, -1)` can never misread a by-value bool/int as a
varlena. Good defense in depth.
- **Accumulator survives transitions** — `datumCopy`'d into `aggcontext`,
not the per-tuple context; `PG_ARGISNULL(0)` is a reliable first-element signal
precisely because the state is never SQL NULL.
- **PARAM_EXEC 0/1 cannot collide with planner params.** The body is
evaluated in a private standalone ExprContext whose `ecxt_param_exec_vals` is
the transfn's own 2-element array; the outer planner never sees ids 0/1, and no
node type that would need a parent (Aggref/SubLink/WindowFunc) can reach the
body — all are rejected before serialization.
- **Nested-reduce / outer-var / param rejection is enforced at transform
time** (a nested reduce lowers to a SubLink, which `reduce_body_check_walker`
rejects; the `varlevelsup == 0` guard prevents an outer RTE sharing a varno
from being rewritten into the accumulator param). The three rejection tests
confirm it.
- **Init-evaluated-once is correct.** Wrapping init in `CASE WHEN
reduce_ordinality = 1 THEN <init> ELSE NULL::agtype END` is sound because the
aggregate-level ORDER BY delivers ordinality 1 first and the transfn only reads
init when the state is NULL; `PARALLEL UNSAFE` + no combinefn prevents any
partial-aggregate reordering.
- **Serialization plumbing is complete and symmetric** — `cypher_reduce` is
wired into the tag enum, `node_names[]`/`node_methods[]`, all five fields in
copy/out/read with matching headers, and the raw-expr walker in
`cypher_analyze.c` descends all three expr fields. No field missed.
- **Backward compat preserved** — `reduce` is added to the `safe_keywords`
grammar production (like `any`/`none`/`order`), and `{reduce: 1}` still works
as a map key.
### Test coverage gaps (cheap to close, no bug implied)
The mechanisms below are correct by inspection, but the suite doesn't lock
in the exact semantics that make them correct:
1. **A fold that legitimately produces null mid-way and then recovers**
(e.g. a `coalesce`/CASE body that climbs back out of null). Only "null
propagates to a null result" is tested; the recovery path *through* the
agtype-`'null'` state is the load-bearing one and isn't directly exercised.
2. **Empty list with a NULL init** (`reduce(s = null, x IN [] | ...)`) — the
`COALESCE(NULL, null)` path is unverified.
3. **A type error / runtime failure in the body** (e.g. a non-coercible or
arithmetic-invalid body) — the `coerce_to_common_type(..., "reduce")` and
runtime error paths are unexercised.
(Large-list / interrupt-path stress and deeper nesting are nice-to-haves;
the SubLink check makes nesting depth irrelevant.)
Adding #1–#3 would be a few lines each and would pin down precisely the
behavior this implementation gets right. Nice piece of work overall — the
agtype-`'null'` state trick and the standalone-ExprContext param isolation are
the right calls.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]