Add @summarize (alias @summarise) for grouped and whole-table aggregation - #356
Open
davidanthoff wants to merge 3 commits into
Open
Add @summarize (alias @summarise) for grouped and whole-table aggregation#356davidanthoff wants to merge 3 commits into
davidanthoff wants to merge 3 commits into
Conversation
Adds a dplyr-style @summarize standalone macro (with @summarise alias) that aggregates a grouped source into one row per group, prepending the grouping key columns, or an ungrouped source into a single-row stream with no key columns. Pure Query.jl-level sugar: the grouped path expands to a lazy map over the groups, the ungrouped path wraps the source rows in an internal keyless Grouping so column access works identically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The @summarize macro now expands to QueryOperators.summarize (added in queryverse/QueryOperators.jl#54); the EnumerableSummarizeAll type, the keyless-Grouping sentinel and the key normalization helpers move there. Requires QueryOperators 1.1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a dplyr-style
@summarizestandalone macro, closing the biggest ergonomic gap identified in the recent API audit: grouped aggregation currently requires the two-step@groupby(_.k) |> @map({k = key(_), m = mean(_.x)})idiom with the explicitkey(_)dance.Depends on queryverse/QueryOperators.jl#54 — the operator layer lives there, and this PR sets
QueryOperators = "1.1"compat, so CI here stays red until QueryOperators 1.1.0 is merged and registered.Syntax
Each argument must have the form
name = expression. Inside each expression_is the collection of rows being aggregated, somean(_.x),length(_)andkey(_)all work. Both the pipe form and the experimental source-first form (@summarize(source, name = expr, ...)) are supported, mirroring every other standalone macro.Semantics (dplyr's, in both cases)
<: Grouping): one output row per group, with the grouping key columns prepended and the aggregates after. The result stays a lazy stream — the grouped path is literally aQueryOperators.mapover the groups, so it works on every source@mapworks on and remains representable in backend query plans as groupby+map.@summarizestays a table→table arrow that composes with everything downstream (|> @mutate(...),|> DataFrame, ...); users who want the bare named tuple write|> first. (The terminal-scalar design already exists as@countand is deliberately not duplicated.)key(rename afterwards with@renameif desired). A multi-column key from@groupby({_.a, _.b})splats its fields as columnsa, b, ...in order.mergeis left-to-right, so the aggregate wins — documented and tested, not an error.@summarize()with no aggregates, or any argument that is notname = expression, throws a macro-expansion-time error naming the problem (deliberately stricter than@mutate).Implementation
The operator layer —
QueryOperators.summarizewith its eltype dispatch, the lazy one-rowEnumerableSummarizeAllfor the ungrouped path, the keyless-Grouping sentinel and the_key_namedtuplekey normalization — lives in QueryOperators (queryverse/QueryOperators.jl#54), alongside the other enumerables and with the generic function open for backend methods. This PR contributes the macro layer: it emits one lambda via the existinghelper_replace_anon_func_syntaxpass (including@mutate's closure-escaping approach) and expands toQueryOperators.summarize(QueryOperators.query(source), f, f_expr).QueryOperators.map(source, f, f_expr)(lazy, unchanged rows-per-group semantics);Grouping(keyed by a private sentinel type, so a legitimatenothinggrouping key stays unambiguous), then yields the single aggregated row. Eager materialization on first iterate has precedent in orderby/groupby.Reusing
Groupingfor the ungrouped path means_.xcolumn access goes through the existingGroupColumnArrayViewmachinery identically in both paths, and plain dispatch on the key type keeps both paths type-stable by construction, soBase._return_type-based eltype inference downstream still yields concrete row types.A representative macroexpansion of
@summarize(m = mean(_.x), n = length(_))(gensyms renamed for readability;QO=Query.QueryOperators, reached through theQuerymodule because the expansion is fully escaped and users loadQuery, notQueryOperators):where the key normalization in QueryOperators is:
Testing and docs
test/test_summarize.jltestitems covering: scalar and multi-column keys,length(_)/key(_)in aggregates, both collision cases, chaining after@summarize, ungrouped single-row semantics (including empty sources and composability), closures over locals, concrete-eltype type stability for both paths, DataFrame collection, the source-first form, and the new error messages. Full testitem suites of both this PR and the QueryOperators PR pass (28/28 combined), anddoctest(Query)passes on Julia 1.12 against the dev QueryOperators.@summarizesection indocs/src/standalonequerycommands.mdwith jldoctest examples (grouped and ungrouped), plus a NEWS.md entry under a new v1.2.0 heading.Backend pushdown follow-up (not in this PR)
SQL pushdown of
@summarizeis a separate follow-up in the backend repos:@groupby(_.k)without an element selector emits the simple 3-arg groupby, and QuerySQLite only registers the 5-arg form; QueryDuckDB would additionally need a translation branch for_key_namedtuple/key. The grouped path is deliberately just aQueryableMapafter a groupby, so it stays fully representable in QueryableBackend plans; the ungrouped path can later be exposed to plan backends by overloadingQueryOperators.summarize(::Queryable, ...)(SQL's aggregate without GROUP BY) — now possible because the generic function lives in QueryOperators. The fused group-and-reduce fast path is likewise out of scope: this sugar emits exactly the groupby+map pattern a future optimizing backend can pattern-match.🤖 Generated with Claude Code