Skip to content

feat(sql): expand GROUPING SETS / ROLLUP / CUBE into merged per-level aggregates (#118) - #124

Merged
zzylol merged 1 commit into
mainfrom
feat/118-grouping-sets
Jul 10, 2026
Merged

zzylol merged 1 commit into
mainfrom
feat/118-grouping-sets

Conversation

@zzylol

@zzylol zzylol commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Closes #118. Takes option 1 from the issue — expand to a Merge of one Aggregate per grouping level — which needs no IR change and reuses the Merge path #109 opened.

Shape

SELECT service, SUM(bytes) FROM metrics GROUP BY ROLLUP(service)

Merge {
  Project { service := service,          sum := sum,  Aggregate{by:[service]} }
  Project { service := CAST(NULL, Utf8), sum := sum,  Aggregate{by:[]}        }
}

Three things that were not obvious

A level that omits a key must still emit it, as NULL. So each level's Aggregate is wrapped in a Project that reinstates the omitted keys and restores the canonical column order. Without it the levels are not union-compatible — and Merge derives its schema from the first child, so every other branch would be silently misdescribed. Same trap as the histogram_quantiles branches in #121.

The nulls have to be cast. infer_expr_type maps a bare L3Scalar::Null to Float64, so the () level of ROLLUP(service) would have emitted service: Float64. They are cast to the key's declared type instead, and a test asserts Utf8.

__grouping_id is safe to drop. DataFusion always adds it to the aggregate schema, but the enclosing Projection never references it. Its only purpose is to distinguish a subtotal's NULL from a data NULL, which is observable solely through GROUPING(col) — and GROUPING is an aggregate this front end rejects (unsupported aggregate: grouping). grouping_function_is_rejected pins that, since it is precisely what makes dropping the column sound.

Also verified

Cost

The scan is duplicated per level; plan::cse hoists it back into a single producer, the same trade histogram_quantiles makes. CUBE(n) is 2^n levels by definition, so the IR tree grows with it — no artificial cap added, since DataFusion builds the same power set.

Tests (8 new, 1 rewritten)

multi_level_grouping_is_rejected (added by #110) is replaced by multi_level_grouping_lowers_to_one_aggregate_per_level. The rest pin prefix expansion, the power set, mixed-form normalization, typed nulls, union compatibility, the GROUPING() rejection, the expression-key rejection, and composition with #110.

Verification

cargo test --workspace      # 387 passed, 0 failed
cargo clippy --all-targets  # clean
cargo fmt --all --check     # clean

🤖 Generated with Claude Code

… aggregates (#118)

Multi-level grouping produces several grouping levels from one scan, while
`Aggregate.by` holds a single key set. Expand each level into its own
`Aggregate` and `Merge` them — option 1 from the issue, which needs no IR change.

A level that omits a key must still emit it, as NULL, per SQL. So each level's
Aggregate is wrapped in a Project that reinstates the omitted keys and restores
the canonical column order. Without that the levels would not be
union-compatible, and `Merge` derives its schema from the first child — every
other branch would be silently misdescribed. The nulls are *cast* to the key's
declared type: a bare Null literal infers as Float64.

DataFusion's `__grouping_id` discriminator is dropped. It exists only to tell a
subtotal's NULL from a data NULL, which is observable solely through
`GROUPING(col)` — an aggregate this front end rejects as unsupported. A test
pins that rejection, since it is what makes dropping the column sound.

DataFusion normalizes every mixed form (`GROUP BY g, ROLLUP(d)`) into a single
`GroupingSets`, so one grouping expression is the only shape to handle.

Composes with #110: a derived reducer argument (`SUM(bytes * 8)`) materializes
in a Project beneath every level's Aggregate. A non-column key *inside* a
grouping set is rejected — it would also have to be reinstatable as a typed
null — with a message that says so.

The scan is duplicated per level; `plan::cse` hoists it back to one producer,
the same trade `histogram_quantiles` makes (#109). CUBE(n) is 2^n levels by
definition.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit 0c08240 into main Jul 10, 2026
1 check passed
@zzylol
zzylol deleted the feat/118-grouping-sets branch July 10, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQL: GROUPING SETS / ROLLUP / CUBE unsupported — Aggregate.by is a single key set

1 participant