Repository navigation
Description
Related PR
Problem
The SQL unparser now has several aggregate-aware rendering paths that manually compose the same small set of operations:
- unproject logical column references through an
Aggregatewithunproject_agg_exprs, - detect whether the aggregate input is rendered through a derived-projection boundary with
contains_projection_before_relation, - normalize aggregate input column qualifiers with
normalize_agg_input_columns, - then render the expression into the appropriate SQL clause.
This composition is repeated in datafusion/sql/src/unparser/plan.rs for projection-over-aggregate rendering, direct LogicalPlan::Aggregate rendering, and Filter rendering for HAVING / QUALIFY. Nearby aggregate-aware paths, such as window projection and ORDER BY unprojection, have to remember which parts of the same rule apply.
The current code is mostly correct for the recently fixed SELECT / GROUP BY derived-projection shape, but the invariant is still distributed across clause-specific call sites. That makes future fixes easy to apply partially.
Why it matters
This is primarily a maintainability refactor with correctness-risk reduction.
Aggregate unparsing has a scope boundary that is easy to miss: if an aggregate input is emitted as a derived relation, outer aggregate clauses must reference the derived relation output columns, not aliases from inside that relation. When each clause manually wires unprojection and normalization, reviewers must re-check the same invariant for every new path.
A small helper would make the intended scope rule obvious and reduce drift between SELECT, GROUP BY, HAVING, QUALIFY, and aggregate-aware ORDER BY handling.
Invariant / desired behavior
For every aggregate expression rendered in one SQL SELECT block, column references should be normalized against the same visible SQL scope.
If an Aggregate reads from a child plan that the unparser renders through a derived-projection boundary, aggregate-related expressions emitted above that boundary should not retain out-of-scope base-table qualifiers for columns that belong to the aggregate input schema.
This invariant should be applied consistently where the unparser already performs aggregate unprojection or aggregate input normalization.
Proposed direction
Introduce one narrow internal helper in datafusion/sql/src/unparser/plan.rs or a nearby unparser utility module.
The helper should centralize the common aggregate expression preparation step, for example:
- optionally unproject an expression through an
Aggregatewhen the caller needs unprojection; - apply aggregate-input qualifier normalization when the aggregate input crosses a derived-projection boundary;
- return an
Exprready forexpr_to_sqlorselect_item_to_sql.
The helper should stay small and semantic. It should not become a general SQL rendering abstraction or rewrite unrelated plan-node behavior.
Call sites should become clear about intent, for example:
- projection-over-aggregate: prepare projected expressions and group expressions through the helper;
- direct aggregate rendering: prepare aggregate and group expressions through the helper;
HAVING/QUALIFY: prepare unprojected filter predicates through the helper;- aggregate-aware
ORDER BY/ window-projection paths: either use the helper where the same scope rule applies, or document why those paths are already scoped differently.
Scope
In
- Add a focused internal helper for aggregate expression unprojection plus derived-input qualifier normalization.
- Migrate existing aggregate-aware call sites in
datafusion/sql/src/unparser/plan.rswhere the helper directly matches current behavior. - Audit nearby paths that unproject aggregate expressions, especially
project_window_outputandunproject_sort_exprcallers, and either route them through the helper or leave a short comment explaining why normalization is not applicable there. - Preserve current SQL output for cases that do not cross a derived-projection boundary.
- Add focused regression coverage for the derived-projection aggregate-input shape across at least the clauses that are migrated.
Out
- Do not redesign the SQL unparser.
- Do not change optimizer behavior.
- Do not change SQL formatting, alias generation, or dialect behavior except where required to preserve aggregate scope.
- Do not claim new support for unrelated nested-query, window, or sort shapes.
- Do not add public APIs unless a very small internal helper needs to move modules.
Acceptance criteria
- Aggregate expression preparation has one canonical internal helper for unprojection plus derived-input qualifier normalization.
- Existing
SELECTandGROUP BYbehavior for derived-projection aggregate inputs is preserved. -
HAVING/QUALIFYpaths use the same helper where they unproject aggregate expressions. - Aggregate-aware
ORDER BYand window-projection paths are audited; they either use the helper or document why the helper does not apply. - Existing unparser tests pass.
- New or updated tests cover at least one derived-projection aggregate-input regression shape beyond the already-fixed
SELECT/GROUP BYpath, where practical.
Tests / verification
- Add or update tests in
datafusion/core/tests/sql/unparser.rsusing an issue_23317-style schema / plan shape. - Prefer tests that parse or execute the unparsed SQL where practical, not only string-fragment checks.
- Suggested focused command:
cargo test -p datafusion --test core_integration unparser -- --nocaptureUse narrower test filters during development as needed.
Notes / open questions
- Existing helpers such as
contains_projection_before_relation,contains_aggregate_before_relation, andnormalize_agg_input_columnsare likely sufficient building blocks. The refactor should mainly make their combined use harder to forget. - Confirm whether
ORDER BYcan still hit the same derived-projection aggregate-input shape after current optimizer rewrites. If not, document that boundary instead of adding speculative logic. - Confirm whether the
QUALIFYbranch has a stable test shape in the currently supported dialect setup. If not, still route existing code through the shared helper when behavior is unchanged.
Activity
naman-modi commentedon Jul 21, 2026
take
refactor(unparser): centralize aggregate-scope rendering in the SQL u…
refactor(unparser): centralize aggregate-scope rendering in the SQL u…