Skip to content

refactor(legacy_expr): step γ1 — Aggregate bridge module + output_schema helper - #140

Merged
zzylol merged 1 commit into
mainfrom
refactor/legacy-expr-step-gamma1-aggregate
May 12, 2026
Merged

zzylol merged 1 commit into
mainfrom
refactor/legacy-expr-step-gamma1-aggregate

Conversation

@zzylol

@zzylol zzylol commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

First sub-PR of Step γ (variant-by-variant consumer migration). Adopts the (c) bridge strategy: keep legacy_expr::QueryExpr::Aggregate as the L2 emit shape; add a one-way canonical-builder helper consumers call on demand. No construction site rewritten; no bridge enum variant added to legacy QueryExpr.

This avoids the "child must be canonical" coupling that would otherwise force γ1 to migrate every other legacy variant (SketchAgg / WindowedAgg / TopK / …) in the same PR. The bridge module + output_schema_for_aggregate are the foundation γ2-γ4 will consume.

What landed

New controller/src/intent_algebra/aggregate_bridge.rs (328 lines, 8 tests)

pub fn bridge_aggregate_to_canonical(
    keys: &[ColumnRef], aggs: &[AggItem],
    having: &Option<legacy::Predicate>, schema: &Schema,
) -> Result<BridgedAggregate, BridgeError>;

pub struct BridgedAggregate {
    pub by: Vec<ColumnId>,
    pub aggs: Vec<AggIntent>,
    pub having: Option<HavingPredicate>,
}

pub enum BridgeError {
    UnresolvedKey(ResolveError),
    HavingDeferred(QueryExprError),
}

The having translation routes through from_legacy_scalar (Batch 2). E-deferred ScalarExpr variants surface as BridgeError::HavingDeferred(...) — test bridge_having_deferred_e_variant_surfaces_error is the contract.

column_resolution.rs extension (+211, +6 tests)

pub fn output_schema_for_aggregate(
    input: &Schema, by: &[ColumnId], aggs: &[AggIntent],
) -> Schema;
pub fn resolve_named_keys(keys: &[ColumnRef], schema: &Schema)
    -> Result<Vec<ColumnId>, ResolveError>;

Mirrors canonical query_expr::QueryExpr::output_schema_in's Aggregate arm. This was Step β TODO #1. γ2-γ4 consumers descending into legacy Aggregate.input should pass the output schema as the inner subtree's parent_schema.

Demo wire in physical/allocator.rs::alloc_node

The legacy Aggregate arm calls the bridge to enrich NodeAnnotation.rationale with intent kinds + group-by column count. Emit shape stays legacy; behavior unchanged. Proves the bridge is reachable.

Construction-site migration: 0 (intentional)

Per strategy (c). γ7 flips construction sites once γ2-γ4 retire legacy SketchAgg / WindowedAgg / TopK — so no legacy input subtree remains.

Build + test

  • cargo build --release -p controller clean
  • cargo build --release -p query_engine_rust clean
  • cargo test -p controller --lib688 passed (was 674; +14 new bridge + column_resolution tests)
  • cargo test -p controller --bin controller — 27 passed

Caveats

  • Canonical QueryExpr::Aggregate.having is Option<HavingPredicate> (HavingPredicate(pub String)), NOT typed Option<Predicate> as spec suggested. Bridge renders converted Predicate via format!("{pred:?}"). Roundtripping is a follow-up.
  • BridgeError can't derive PartialEq (QueryExprError doesn't); tests use matches!.
  • optimizer::engine::HydraConversion (Step β TODO test: e2e test for modified-OTLP CountMin sketch hot path (PR D) #7) NOT migrated — deferred.

Diff: 5 files, +618 / -4

🤖 Generated with Claude Code

…ema helper

First sub-PR of Step γ (variant-by-variant consumer migration). Adopts
the (c) bridge strategy: keep `legacy_expr::QueryExpr::Aggregate` as
the L2 emit shape; add a one-way canonical-builder helper consumers
call on demand. No construction site rewritten; no bridge enum
variant added to legacy QueryExpr.

This avoids the "child must be canonical" coupling that would otherwise
force γ1 to migrate every other legacy variant (SketchAgg, WindowedAgg,
TopK, etc.) in the same PR.

## What landed

### New: `controller/src/intent_algebra/aggregate_bridge.rs` (328 lines, 8 tests)

```rust
pub fn bridge_aggregate_to_canonical(
    keys: &[ColumnRef],
    aggs: &[AggItem],
    having: &Option<legacy::Predicate>,
    schema: &Schema,
) -> Result<BridgedAggregate, BridgeError>;

pub struct BridgedAggregate {
    pub by: Vec<ColumnId>,
    pub aggs: Vec<AggIntent>,
    pub having: Option<HavingPredicate>,
}

pub enum BridgeError {
    UnresolvedKey(ResolveError),
    HavingDeferred(QueryExprError),
}
```

The `having` translation routes through `from_legacy_scalar` (Batch 2).
E-deferred ScalarExpr variants (`FunctionCall`, `ScalarSubquery`,
`InList`, `Between`) surface as `BridgeError::HavingDeferred(...)`.
Test `bridge_having_deferred_e_variant_surfaces_error` is the contract;
no in-tree construction site builds a HAVING with E-variants today.

### `column_resolution.rs` extension (+211 lines, +6 tests)

```rust
pub fn output_schema_for_aggregate(
    input: &Schema,
    by: &[ColumnId],
    aggs: &[AggIntent],
) -> Schema;

pub fn resolve_named_keys(keys: &[ColumnRef], schema: &Schema)
    -> Result<Vec<ColumnId>, ResolveError>;
```

Mirrors canonical `query_expr::QueryExpr::output_schema_in`'s Aggregate
arm: outputs `by`-columns positionally + one column per
`AggIntent::output_column(probe)`, strips `time_index`, sets
`unique_keys = [by]`. This was Step β TODO #1.

Step γ2-γ4 consumers descending into legacy `Aggregate.input` should
pass `output_schema_for_aggregate(parent_schema, &bridged.by, &bridged.aggs)`
as the inner subtree's `parent_schema`.

### Demo wire in `physical/allocator.rs::alloc_node`

The legacy `QueryExpr::Aggregate { keys, aggs, having, input }` arm
now calls the bridge to derive canonical-shape data and enrich
`NodeAnnotation.rationale` with intent kinds + group-by column count.
Emit shape stays legacy; behavior unchanged (only the rationale string
carries extra info). Proves the bridge is reachable.

## Construction-site migration: 0 (intentional)

Per strategy (c), all ~10 construction sites in
`query_parser/{promql,sql}.rs`, `legacy_lower::lower_aggregate`, and
the optimizer rewrite path still emit `legacy_expr::QueryExpr::Aggregate`.
They migrate in γ7 once no legacy `input` subtree remains (SketchAgg,
WindowedAgg, TopK migrations land first in γ2-γ4).

## Build + test

- `cargo build --release -p controller` — clean
- `cargo build --release -p query_engine_rust` — clean
- `cargo test -p controller --lib` — **688 passed** (was 674; +14 new
  bridge + column_resolution tests)
- `cargo test -p controller --bin controller` — 27 passed

## Known caveats

- Canonical `QueryExpr::Aggregate.having` field is `Option<HavingPredicate>`
  where `HavingPredicate(pub String)`, NOT typed `Option<Predicate>` as
  the spec text suggested. Bridge renders the converted `Predicate` via
  `format!("{pred:?}")`. Round-tripping from the string is a follow-up
  (when canonical `having` upgrades to typed `Predicate`).
- `BridgeError` can't derive `PartialEq` (QueryExprError doesn't); tests
  use `matches!`. Non-blocking.
- `optimizer::engine::HydraConversion` (Step β TODO #7) NOT migrated —
  out of γ1's stated scope, deferred to a later γ sub-PR.

## Diff: 5 files, +618 / -4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zzylol
zzylol merged commit f5468a9 into main May 12, 2026
@zzylol
zzylol deleted the refactor/legacy-expr-step-gamma1-aggregate branch July 17, 2026 20:06
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.

1 participant