feat(core)!: collapse L3 grouping — remove Partition node, unify on GroupKeys (#12, #13) - #18
Conversation
…Y), not a 2nd GROUP BY (#12) `Partition` and `Aggregate.by` are different operations, not two ways to do one. `Aggregate` groups-and-reduces (N rows → 1, schema rewritten to `by ++ aggs`, closed); `Partition` is row-preserving (schema pass-through) — it's `PARTITION BY`, not `GROUP BY`. The #12 duplication is already resolved: after #8 + the tabular-GROUP-BY convergence, every reducing GROUP BY (SQL and PromQL) lowers to `Aggregate.by`. No query emits a `Partition` wrapping a reducing aggregate. The surviving `Partition` only records per-group structure over a per-series (label- preserving) reduction — e.g. `topk by (host) (avg_over_time(cpu[5m]))`. This commit reconciles the docs to that reality and pins it with a test; it does NOT remove the node (deferred — see below). - docs/design.md: kill the stale "Partition … (`GROUP BY`)" framing that contradicted the schema-flow table's "logical-only marker / sharding hint". - query_expr.rs / relational.rs / lower.rs: document `Partition` as transitional split-without-reduce, slated to move to a rank `partition_by` (L3) / L5 stage-allocation artifact per the layering contract (L3/L4 symbolic, L5 commits placement). Note the removal blocker: per-series schema detection keys off `Aggregate.by.is_empty()`, so the windowed-reduction keys can't simply move into `by` without flipping the node to a cross-series reduce and breaking the open, label-preserving schema. - promql_lowering.rs: regression test — reducing GROUP BY (ungrouped, grouped, and over a label-preserving `rate`) lowers to `Aggregate.by` with no `Partition` anywhere in the tree. No behavior change. Full node removal tracked as the #12 follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion_by (#12) Completes issue #12: there are no longer two ways to express grouping. The `Partition` node — which conflated a semantic per-group split with a physical sharding hint — is removed from both L2 and L3. Grouping now has exactly one home per concept: - reducing GROUP BY → `Aggregate.by` (already true) - per-group *ranking* → `Sort.partition_by` (new; this commit) - parallel/sharding split → L5 stage allocator (physical, not in the IR) The only producer of the old `Partition` node was the generic (non-heavy-hitter) `topk by (…)` / `bottomk` path: its grouping couldn't go in `Aggregate.by` without flipping the windowed reduction from per-series (label-preserving) to a cross-series reduce — per-series detection keys off `Aggregate.by.is_empty()`. That grouping is semantic (it changes which rows rank within which group), so it belongs in L3 — but on the operator that consumes it, not as an ambiguous node. Changes: - L2 (`relational.rs`): drop the `Partition` variant; add `partition_by: Vec<ColumnRef>` to `Sort`. - L3 (`query_expr.rs`): drop the `Partition` variant and the `PartitionKeys` type; add `partition_by: Vec<ColumnId>` (`#[serde(default)]`) to `Sort` (row-preserving, schema pass-through). - `promql.rs`: generic `topk by (…)` routes its keys to `Sort.partition_by`; the windowed reduction beneath stays `by: []`. - `lower.rs`: resolve `Sort.partition_by` positionally; the windowed-reduction- with-keys path is now a hard `ConvertError::WindowedReductionKeys` (it can no longer be reached by valid PromQL) instead of a `Partition` wrap. - `binder.rs`: seed `Sort.partition_by` names into the usage-derived leaf schema. - `sql/mod.rs`: SQL `ORDER BY` is a global sort (`partition_by: []`). - docs/design.md: remove the `Partition` node + reconcile the schema-flow table. - tests: `topk_over_avg` now asserts the `Sort.partition_by` shape; `generic_topk_grouping_lowers_to_sort_partition_by` pins the new routing; helper match arms updated. Verified: `cargo build` clean; `cargo test -p asap-control-core -p asap-control-lower -p asap-e2e` — all suites pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
, #13) The three "operate per group" L3 operators carried their grouping as three ad-hoc `Vec<ColumnId>` fields — `Aggregate.by`, `Sort.partition_by`, `WindowFunc.partition_by`. Replace all three with one `GroupKeys` newtype, so "per-group keys" has a single spelling and a single home to evolve (e.g. a future qualified-key or `without(...)` representation). - `GroupKeys(pub Vec<ColumnId>)` — `#[serde(transparent)]` (wire-compatible with the bare arrays it replaces); `Deref<Target=[ColumnId]>`, `IntoIterator for &GroupKeys`, `From<Vec<ColumnId>>`, `FromIterator`, and `PartialEq<Vec<…>>` so existing read/compare/iterate sites are unchanged. - This is the unification #12 called for (grouping is a *parameter type* shared across operators, not a standalone node) and it ties off #13: heavy-hitter `topk` groups via `Aggregate.by`, generic `topk`/`bottomk` via `Sort.partition_by` — both now `GroupKeys`. No behavior change. `cargo test -p asap-control-core -p asap-control-lower -p asap-e2e` — 193 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AggIntent::TopK { k, accuracy } is a pure L3 intent (k most frequent by
value, to accuracy ε), built from the lowering accuracy context exactly
like Quantile/Cardinality. Its doc claimed it "is served by a dedicated
sketch (SpaceSaving, CMS-with-heap) in one pass" and "never materialises-
then-sorts" — physical-strategy language that belongs to L4 and is false
for accuracy: Exact. Reframe the docs as intent-only and defer the
exact-vs-sketch realisation to L4, matching the module's own rule that the
HashAgg/SortAgg/SketchAgg choice is an L4 cost-aware decision. Keep the
legitimate lowering-time split (heavy-hitter intent vs generic Sort+Limit).
Doc/comment-only; no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
31bf28b to
5687733
Compare
|
Answers to the review questions on this PR: 1. "Partition is removed, and now Sort has an optional partition key?" Yes. The L3 2. "What about aggregate and group by?" Reducing GROUP BY stays on 3. "Can we represent Filed as #30 and fixed in #31 — |
…ng by-labels (#30) `topk(k, <bare instant selector>) by (labels)` — e.g. `topk(3, http_requests_total) by (job)` ("top-3 series per job") — mislowered. The non-heavy-hitter path in `build`'s `Outer::TopK` else-branch defaulted a bare selector argument (`inner.func == None`) to an implicit cross-series `AggFunc::Sum`. That reducing `Sum` collapsed every label (including the `by` partition keys) into a single `sum` column, so `Sort.partition_by = [job]` no longer resolved at L3 — a regression surfaced reviewing the Partition → `Sort.partition_by` reframe (#12, PR #18). It was also semantically wrong: PromQL `topk` ranks the raw instant-vector samples, it does not sum them. A bare selector now ranks over the `filtered_source` directly (label- preserving), so `Sort.partition_by` ranks within each group. A range-vector- function argument (`topk(k, rate(m[5m]))`) still reduces per series first — also label-preserving — so those paths are unchanged, as are the heavy-hitter `count_over_time` and `bottomk` cases. Expected: `topk(3, http_requests_total) by (job)` → `Limit{3} → Sort{value desc, partition_by:[job]} → Scan`. Tests: bare-selector topk by-label (ranks per group, no implicit Sum) and bare-selector topk without `by` (ranks raw samples). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng by-labels (#30) (#31) `topk(k, <bare instant selector>) by (labels)` — e.g. `topk(3, http_requests_total) by (job)` ("top-3 series per job") — mislowered. The non-heavy-hitter path in `build`'s `Outer::TopK` else-branch defaulted a bare selector argument (`inner.func == None`) to an implicit cross-series `AggFunc::Sum`. That reducing `Sum` collapsed every label (including the `by` partition keys) into a single `sum` column, so `Sort.partition_by = [job]` no longer resolved at L3 — a regression surfaced reviewing the Partition → `Sort.partition_by` reframe (#12, PR #18). It was also semantically wrong: PromQL `topk` ranks the raw instant-vector samples, it does not sum them. A bare selector now ranks over the `filtered_source` directly (label- preserving), so `Sort.partition_by` ranks within each group. A range-vector- function argument (`topk(k, rate(m[5m]))`) still reduces per series first — also label-preserving — so those paths are unchanged, as are the heavy-hitter `count_over_time` and `bottomk` cases. Expected: `topk(3, http_requests_total) by (job)` → `Limit{3} → Sort{value desc, partition_by:[job]} → Scan`. Tests: bare-selector topk by-label (ranks per group, no implicit Sum) and bare-selector topk without `by` (ranks raw samples). Co-authored-by: zz_y <zz_y@node0.zz-y-308294.softmeasure-pg0.clemson.cloudlab.us> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restructure the flat 19-section developer guide into the three-part structure the doc owner asked for: Part 1 - Code Architecture, Part 2 - Interfaces and Definitions, Part 3 - How to Add X, Y, Z (each ending in how to verify). Content is moved, not rewritten: Part 1 (Mental model first, per doc-owner follow-up, then a new whole-PR architecture diagram, then "How the current pieces fit together"): - old #1 Mental model -> Part 1 #1 - new: whole-PR architecture diagram (TargetSubDAG's two entry points through ReplacementStrategy, PlanSpace/cost_sorted, explanation.rs, to a downstream consumer) -> Part 1 #2 - old #3 How the current pieces fit together -> Part 1 #3 Part 2: - old Terminology's "Implementation" definition merged into the Glossary as one more entry (### Implementation), next to ReplacementStrategy - old #2 Glossary -> Part 2 #1 (plus the merged Implementation entry and old #10 Matcher, retitled to match glossary-entry style) - old #10 Matcher (implementation.rs) -> ### Matcher inside the Glossary; implementation.rs no longer exists, so the stale title is fixed - old #19's definitional content (ReplacementExplanation/ ExplanationKind shapes, node_hash, why there's no ExplanationRule trait, location-text ownership) -> Part 2 #2 Part 3: - old #4, #5, #6, #7, #13, #14 -> Part 3 #1, Adding a new ReplacementStrategy (ending in Testing a new strategy) - old #8, #9, #15 -> Part 3 #2, Adding or customizing a CostModel (ending in Testing a new cost model) - old #12 -> Part 3 #3, Adding a new sketch algorithm, with its stale implementation.rs/binder references fixed to replacement.rs/ construct_summary vocabulary, plus a new "Verifying a new sketch algorithm" close grounded in the existing coverage-matrix tests - old #11, #16, #17, #18 -> Part 3 #4-#7 (capstone + closing reference material); #18's extension-map table's implementation.rs row fixed to replacement.rs - old #19's "Using it"/"Adding a new kind" content -> Part 3 #8, Using and extending explanation.rs cargo build --workspace --all-targets is clean (docs-only change). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Collapses L3 grouping to one representation, closing #12 and tying off the #13 discussion. Grouping had two problems: a
Partitionnode that duplicatedAggregate.by, and — once that was resolved — grouping keys living in three unrelatedVec<ColumnId>fields. This PR fixes both.End state — grouping has one home per concept, all sharing one type:
Aggregate.byGroupKeystopk by,bottomk)Sort.partition_byGroupKeysOVER (PARTITION BY …))WindowFunc.partition_byGroupKeysCommits
docs(core)— reframe:Partitionis split-without-reduce (PARTITION BY), not a 2ndGROUP BY; reconcile the contradictorydesign.mdtext; add a regression test pinning that reducing GROUP BY →Aggregate.by.feat(core)!— remove the L3 (and L2)Partitionnode +PartitionKeys. The sole producer was generictopk by (…); its grouping is semantic (changes which rows rank within which group), so it moves onto the operator that consumes it — a newSort.partition_by— rather than an ambiguous standalone node. The windowed reduction beneath stays label-preserving (by: []).refactor(core)— unify the three grouping-key fields into a sharedGroupKeysnewtype overVec<ColumnId>.Algebra changes
New type (L3,
query_expr.rs):L3
QueryExprnodes (query_expr.rs):L2
relational::QueryExpr(relational.rs):New lowering error (
lower.rs):Why a node was the wrong fix (and a shared type is the right one)
A standalone group/partition node is exactly the
Partitionwe removed: with nothing consuming it, it's a row-preserving pass-through whose meaning (semantic grouping vs physical sharding) is ambiguous — and grouping's semantics are defined by the consumer (reduce vs rank vs window), so the keys belong on the operator. The real duplication (keys in three places) is fixed by a shared parameter type, not a node.GroupKeysand positionalColumnIdGroupKeys(pub Vec<ColumnId>)is a transparent newtype — grouping stays positional (an index into theScanschema), unchanged.#[serde(transparent)]keeps it wire-compatible with the bare arrays it replaces, andDeref<[ColumnId]>+IntoIterator for &GroupKeys+From<Vec<…>>+PartialEq<Vec<…>>keep all read/iterate/compare/construct sites working — hence no behavior change.Ties off #13
AggIntent::TopKstays a first-class intent (it's the sketchable heavy-hitter operator, distinct from exactSort + Limit). Its grouping rides onAggregate.by; generictopk/bottomkgrouping rides onSort.partition_by— both nowGroupKeys. So every grouping concept has one spelling, and the lowering remains one-canonical-form-per-query.Compatibility
Breaking IR change (
feat!):QueryExpr::Partition/PartitionKeysare gone,Sortgainspartition_by, and the three grouping fields are nowGroupKeys.partition_byis#[serde(default)]andGroupKeysis#[serde(transparent)], so older serialized forms still deserialize. No front end emittedPartitiondirectly (converter-only fallback), so PromQL/SQL surface behavior is unchanged —topk bynow lowers toLimit{Sort{partition_by,…}}.Test plan
cargo build— clean, no warningscargo test -p asap-control-core -p asap-control-lower -p asap-e2e— 193 tests pass, incl. the rewrittentopk_over_avg_is_generic_sort_limit, newgeneric_topk_grouping_lowers_to_sort_partition_by,reducing_group_by_lowers_to_aggregate_by, and the SQLcount_ranked_topk_is_heavy_hitterPartition/PartitionKeysreferencesCloses #12. Resolves the #13 discussion (closed: keep
TopK).🤖 Generated with Claude Code