Skip to content

Expression errors still fold silently inside aggregates, GROUP BY keys, window functions, and join probes (follow-up to #216/#226) #228

Description

@emanzx

Version / build tested against

PR #226's branch (fix/216-suppressed-expression-errors on top of origin/main); the behavior also exists on current main in its pre-#226 form (everything folds). Filed as the tracked follow-up to #226's documented deferral.

Deployment mode

Origin — single node (local), via pgwire.

Engine(s) involved

SQL execution — aggregate accumulators, GROUP BY key building, window functions, hash-join probe path.

Summary

#226 makes scalar expression contexts raise 22012/42883 instead of folding to NULL, but expressions evaluated inside certain pervasive hot paths still fold errors silently (each site carries an in-code deferral comment): aggregate arguments (an error row is skipped, so sum(10/d) over a table containing d = 0 returns a number silently missing rows), GROUP BY key expressions (zero-divisor rows group under NULL), window-function arguments / PARTITION BY / ORDER BY, and the hash-join residual-predicate probe path (matching row-pairs silently dropped). The result is an internal inconsistency with real corruption potential: SELECT 10/d FROM t errors, while SELECT sum(10/d) FROM t on the same table succeeds with a silently wrong total — the exact "silent wrong result" class #216 exists to eliminate, one context over.

Steps to reproduce

On a #226 build:

CREATE COLLECTION agg_probe (id TEXT PRIMARY KEY, d INT);
INSERT INTO agg_probe (id, d) VALUES ('a', 1), ('b', 0), ('c', 2);
SELECT 10 / d FROM agg_probe;        -- ERROR 22012 (correct, post-#226)
SELECT sum(10 / d) FROM agg_probe;   -- succeeds, returns 15 — silently missing row 'b'
SELECT 10 / d AS k FROM agg_probe GROUP BY k;  -- zero-divisor row groups under NULL

Expected behavior

Error contexts behave uniformly: an expression that raises 22012 in a scalar context also raises it when evaluated as an aggregate argument, GROUP BY key, window argument, or join predicate — matching Postgres.

Actual behavior

The four listed paths swallow the error per-row: aggregates skip the row, GROUP BY buckets it under NULL, joins drop the pair — all with green results.

What actually happened? (check all that are true)

  • Acknowledged/committed data was lost, corrupted, or silently wrong
  • The process crashed, hung, or failed to start
  • A security or isolation boundary was crossed
  • Core functionality is broken with no acceptable workaround
  • A workaround exists (pre-filter or guard divisors; move the expression to a scalar context first)

Proposed severity

SEV-2 — High: silently-wrong query results; stored data intact.

Reproducibility

Always — every attempt.

Last known-good version / commit (if a regression)

Not a regression — these paths have folded since introduction; #226 makes the inconsistency visible by fixing the scalar half.

Environment & logs

Linux x86_64, source build. Why deferred out of #226: these are infallible-by-signature hot paths spanning ~10 files each (accumulator feed, group-key builder, both window evaluators, grace-hash probe); converting them is a materially larger, perf-sensitive change than the ~19 mechanical call-site conversions #226 shipped. Each site carries an in-code comment naming this issue class. Suggested shape: same EvalError propagation, threaded through the accumulator/group-key/window/join result types, with attention to the spill/shuffle variants used by cluster mode.

Before submitting

Activity

  1. added
    type:bugA defect — broken, incorrect, or lost data
    sev:2-highMajor functionality broken; no acceptable workaround
    area:sqlParser, planner, SQL semantics
    on Jul 26, 2026
  2. self-assigned this
    on Jul 27, 2026
  3. farhan-syah commented on Jul 29, 2026

    @farhan-syah
    Member

    Fixed in 6d89563.

    The four paths named here — aggregate arguments, GROUP BY keys, window functions, and the hash-join probe — were already converted by the #226 follow-ups, so the reported repro now raises 22012 as expected. Widening the fix along the same invariant turned up the other half of the class, where the expression was not folding on error but was never evaluated at all, producing wrong answers even with no zero divisor present:

    • Window function arguments were reduced to a column name, so any non-column argument silently returned 0/NULL — SUM(n * 2) OVER (...), LAG(n * 10) OVER (...), and framed aggregates all did. Arguments are now evaluated per row, and the planner rejects a non-constant LAG/LEAD/NTILE/NTH_VALUE offset instead of quietly substituting its default.
    • ORDER BY over an expression was dropped at plan conversion, returning rows in storage order under a sort the client asked for. Sort keys now carry the expression through every engine, including the external spill and k-way merge.
    • GROUP BY referencing a SELECT alias collapsed every row into one global group. It now resolves to the aliased expression, with PostgreSQL's precedence (an input column of the same name wins).
    • HAVING dropped every group whenever the aggregate was not also projected, and never resolved output aliases. The predicate is now bound to the computed aggregate columns, and an aggregate that appears only in HAVING is computed rather than ignored.
    • A JOIN whose ON clause holds only an inequality matched nothing at all, since the probe had no key to hash. Keyless probes now consider every candidate pair and let the residual predicate decide.

    Coverage went in ahead of the fix: 20 tests across seven files, each asserting the correct behavior rather than the current one, plus value-level guards against the specific silent modes (all-zero window columns, unchanged row order, a single collapsed group, an empty join result).

    Two related gaps were flagged rather than silently folded into the fix above, and have since been completed in b9191df08109c2128b6fc89cd132283c04c7dc3a:

    • ORDER BY ... NULLS FIRST/LAST was ignored. Sort keys now carry NULL placement, with PostgreSQL's defaults (ASC → NULLS LAST, DESC → NULLS FIRST) and the placement never flipped by the sort direction. This also settled two related inconsistencies: the previous default was inverted, and the engines disagreed on whether a NULL sorted high or low.
    • A post-aggregate ORDER BY <expression> returned a typed planner error. GROUP BY cat ORDER BY 1000 / SUM(amount) and ORDER BY SUM(amount) DESC over an unprojected aggregate now work; an aggregate introduced only by the sort is computed without becoming an output column.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:sqlParser, planner, SQL semanticspriority:P2Scheduled, not urgentsev:2-highMajor functionality broken; no acceptable workaroundtype:bugA defect — broken, incorrect, or lost data

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions