diff --git a/control_plane/docs/design.md b/control_plane/docs/design.md index 40164d2f..b2771b84 100644 --- a/control_plane/docs/design.md +++ b/control_plane/docs/design.md @@ -44,8 +44,8 @@ DataCollector/controller already documents its query→sketch translation as a 5 | # | Layer | What it does | Today's locations | |---|-------|--------------|-------------------| | 1 | **Query Language** | Parse raw strings (PromQL, SQL, DataFusion, ElasticDSL, …) into a language-specific AST | DC `controller/src/query_parser/{promql,sql}.rs` (each parses straight to the L2 relational tree below); asap-planner-rs pulls `promql-parser` + `sqlparser` directly; asap-fusion consumes a pre-built DataFusion `LogicalPlan` (its L1 happens upstream) | -| 2 | **Language Logical Plan** | Per-language algebra tree (`Aggregate` / `Window` / `Filter` / `Sort` / `Limit`) preserving language semantics, **no sketch names, no sketch binding** | DC `controller/src/intent_algebra/legacy_expr.rs` — the L2 relational `QueryExpr` tree the `query_parser` front ends emit (one shared tree for PromQL + SQL today; a per-language split is future work); asap-fusion inherits DataFusion's `LogicalPlan` as its L2; asap-planner-rs has no L2 today (uses a template-pattern catalogue) — **Phase 4 builds one** | -| 3 | **Intent algebra** | Language- and deployment-independent IR: `QueryExpr` + `AggIntent`. Describes **intent only** — *what* to compute, with accuracy target. **No sketch type, no sketch parameters, no sketch-bound nodes** (`SketchAgg` / `SketchJoin` / `SketchSubtract` etc. live in the L4 IR `SketchExpr`). **No language-shaped operators** (no `HistogramQuantile`, no `PromQLSubquery` — those are PromQL L2 nodes that lower to data-model-agnostic shapes here). **One canonical form per plan** — no `WindowedAgg` (use `Window` over `Aggregate`). Heavy-hitter intents are first-class (`AggIntent::TopK`) so heavy-hitter sketches bind directly on the intent rather than on a generic `Sort + Limit` shape; generic `Sort + Limit` survives in `QueryExpr` for non-heavy-hitter cases (e.g. `ORDER BY name LIMIT 10`). Every edge carries a typed `Schema`. Data-model-agnostic — `QueryExpr::Scan` wraps a `Source` sum with `TimeSeries` / `Table` / `Join` variants so the same L3 IR covers ASAPQuery's time-series queries and asap-fusion's tabular queries. | DC `controller/src/intent_algebra/{agg_intent,query_expr,schema,lower,cse}.rs` (canonical L3) + `controller/src/intent_algebra/legacy_to_canonical.rs` (the L2→L3 converter: folds the legacy `legacy_expr` relational tree straight to the canonical IR, single-statistic sketchable `Aggregate` fusion included — no intermediate fused L3 IR); asap-fusion's 3-variant `SubPopulationAnalyticsType` maps to a subset; asap-planner-rs's 9-variant `Statistic` maps to a subset — **Phase 4 splits sketch binding out of planner's current fused L3+L4**. *Refactor 2026-05 absorbed `controller/src/algebra/expr.rs` into `intent_algebra/legacy_expr.rs`.* | +| 2 | **Language Logical Plan** | Per-language algebra tree (`Aggregate` / `Window` / `Filter` / `Sort` / `Limit`) preserving language semantics, **no sketch names, no sketch binding** | DC `controller/src/intent_algebra/relational.rs` — the L2 relational `QueryExpr` tree the `query_parser` front ends emit (one shared tree for PromQL + SQL today; a per-language split is future work); asap-fusion inherits DataFusion's `LogicalPlan` as its L2; asap-planner-rs has no L2 today (uses a template-pattern catalogue) — **Phase 4 builds one** | +| 3 | **Intent algebra** | Language- and deployment-independent IR: `QueryExpr` + `AggIntent`. Describes **intent only** — *what* to compute, with accuracy target. **No sketch type, no sketch parameters, no sketch-bound nodes** (`SketchAgg` / `SketchJoin` / `SketchSubtract` etc. live in the L4 IR `SketchExpr`). **No language-shaped operators** (no `HistogramQuantile`, no `PromQLSubquery` — those are PromQL L2 nodes that lower to data-model-agnostic shapes here). **One canonical form per plan** — no `WindowedAgg` (use `Window` over `Aggregate`). Heavy-hitter intents are first-class (`AggIntent::TopK`) so heavy-hitter sketches bind directly on the intent rather than on a generic `Sort + Limit` shape; generic `Sort + Limit` survives in `QueryExpr` for non-heavy-hitter cases (e.g. `ORDER BY name LIMIT 10`). Every edge carries a typed `Schema`. Data-model-agnostic — `QueryExpr::Scan` wraps a `Source` sum with `TimeSeries` / `Table` / `Join` variants so the same L3 IR covers ASAPQuery's time-series queries and asap-fusion's tabular queries. | DC `controller/src/intent_algebra/{agg_intent,query_expr,schema,lower,cse}.rs` (canonical L3) + `controller/src/intent_algebra/lower_to_canonical.rs` (the L2→L3 converter: folds the `relational` L2 tree straight to the canonical IR, single-statistic sketchable `Aggregate` fusion included — no intermediate fused L3 IR); asap-fusion's 3-variant `SubPopulationAnalyticsType` maps to a subset; asap-planner-rs's 9-variant `Statistic` maps to a subset — **Phase 4 splits sketch binding out of planner's current fused L3+L4**. *Refactor 2026-05 absorbed `controller/src/algebra/expr.rs` into `intent_algebra/relational.rs`.* | | 4 | **Sketch algebra + optimizer** | Cost-aware algebraic rewrite rules under deployment constraints. **This is where sketch binding happens** — L4 rules take intent-only L3 (`QueryExpr`) and emit the sketch-bound IR (`SketchExpr`). ~12 rules in DC; a smaller targeted subset in planner; `SketchConfigRule` + `HashModeRule` in fusion. | Core provides the **rule engine driver** + `OptimizerRule` trait + a shared rule library + the sketch-bound IR `core::sketch_algebra::SketchExpr`; deployment models **pick** which rules to enable + supply their own deployment constraints. DC `controller/src/sketch_algebra/` (IR + binding rules) + `controller/src/optimizer/{engine,trait_def,baseline,rules/,cost/}.rs` (rule engine + cost models); fusion `src/optimizer/rules/`; planner's `map_statistic_to_precompute_operator`. *Refactor 2026-05 absorbed `controller/src/algebra/optimizer.rs` (→ `optimizer/engine.rs`) and `controller/src/planner/{cost_model,delta_cost_model,online_cost_model,pareto,tco,wire_cost,rules,baseline_planner}.rs` (→ `optimizer/{cost/,rules/,baseline.rs}`).* | | 5 | **Physical Execution Plan** | Assign ops to pipeline stages (edge / gateway / backend / object store); produce the deployment-specific artifact (OpAMP YAML, `streaming_config.yaml`, rewritten DataFusion `LogicalPlan`). **Sketch binding is already committed by L4**; L5 is about stage allocation + emission. | Core provides the **stage allocator framework** + `PhysicalPlanner` trait + the sketch catalogue; deployment models supply their own **topology** (3-stage / 1-stage / 0-stage) + their own **emitter** for the output format. DC `controller/src/physical/{allocator,planner,plan,sketch_catalog,stage_split,topology,colored_dag/}.rs` (allocator framework + sketch catalogue + typed three-stage colouring) + `controller/src/emit/{stage_config,otap,telegraf,agent,backend,asapquery_backend,precompute,trait_def}.rs` (per-deployment-model emitters + `PlanEmitter` trait) + `controller/src/pipeline.rs` (L1→…→L5 driver, formerly `analyzer.rs`); asap-planner-rs `output/generator.rs`; asap-fusion `src/executor/`. *Refactor 2026-05 absorbed `controller/src/algebra/{physical,allocator,plan,directory}.rs` and `controller/src/planner/stage_split.rs` and the legacy `controller/src/stage_split/` framework into `physical/`; absorbed `controller/src/config/` into `emit/` + `workload.rs`; renamed `analyzer.rs` to `pipeline.rs`.* | @@ -168,8 +168,8 @@ If something must be optional (e.g. OpAMP for deployments that don't run OTel co > | Target §5 path | Current single-crate path | > |---|---| > | `crates/core/query_language/` | `controller/src/query_parser/{promql,sql}.rs` (L1 parsers — emit the L2 tree directly) | -> | `crates/core/logical_plan/` | `controller/src/intent_algebra/legacy_expr.rs` (the L2 relational `QueryExpr` tree) | -> | `crates/core/intent_algebra/` | `controller/src/intent_algebra/` (with `legacy_expr` carrying the L2 relational IR and `legacy_to_canonical` lowering it to the canonical L3 types) | +> | `crates/core/logical_plan/` | `controller/src/intent_algebra/relational.rs` (the L2 relational `QueryExpr` tree) | +> | `crates/core/intent_algebra/` | `controller/src/intent_algebra/` (with `relational` carrying the L2 relational IR and `lower_to_canonical` lowering it to the canonical L3 types) | > | `crates/core/sketch_algebra/` | `controller/src/sketch_algebra/` | > | `crates/core/optimizer/{engine,trait,rules,cost}/` | `controller/src/optimizer/{engine.rs,trait_def.rs,rules/,cost/,baseline.rs}` | > | `crates/core/physical/{planner_trait,stage_allocator,topology,executor,sketch_catalog}/` | `controller/src/physical/{planner,allocator,plan,stage_split,sketch_catalog,topology,colored_dag/}.rs` | @@ -286,10 +286,10 @@ Core is not a trait-stubs library. It ships real L1/L2/L3 code lifted from DC's > | `core::*` reference in §6 | Current single-crate path | > |---|---| > | `core::query_language` | `controller/src/query_parser/{promql,sql}.rs` | -> | `core::logical_plan` | `controller/src/intent_algebra/legacy_expr.rs` (the L2 relational `QueryExpr` tree) | -> | `core::intent_algebra` | `controller/src/intent_algebra/` (canonical `query_expr` / `agg_intent`) + `controller/src/intent_algebra/legacy_expr.rs` (the L2 relational IR) | +> | `core::logical_plan` | `controller/src/intent_algebra/relational.rs` (the L2 relational `QueryExpr` tree) | +> | `core::intent_algebra` | `controller/src/intent_algebra/` (canonical `query_expr` / `agg_intent`) + `controller/src/intent_algebra/relational.rs` (the L2 relational IR) | > | `core::sketch_algebra` | `controller/src/sketch_algebra/` | -> | `core::lower` | per-layer: `controller/src/intent_algebra/{lower,legacy_to_canonical}.rs` + `controller/src/sketch_algebra/lower.rs` | +> | `core::lower` | per-layer: `controller/src/intent_algebra/{lower,lower_to_canonical}.rs` + `controller/src/sketch_algebra/lower.rs` | > | `core::optimizer::engine` | `controller/src/optimizer/engine.rs` | > | `core::optimizer::trait` | `controller/src/optimizer/trait_def.rs` (placeholder) | > | `core::optimizer::rules` | `controller/src/optimizer/rules/` | @@ -308,7 +308,7 @@ Core is not a trait-stubs library. It ships real L1/L2/L3 code lifted from DC's > Every §6 reference below should be read with this table as the > concrete pointer. Where §6 says "lifted from DC's > `controller/src/algebra/`", read "structurally aligned to -> `intent_algebra/legacy_expr.rs` + `physical/` + `optimizer/engine.rs` +> `intent_algebra/relational.rs` + `physical/` + `optimizer/engine.rs` > per the §16 ADR." ### `core::query_language` — Layer 1 @@ -326,10 +326,10 @@ Each returns a language-flavored AST type. No sketch awareness. > the `Language`-trait / `LanguageAst` indirection the target design > sketches. L1 lives in `controller/src/query_parser/{promql,sql}.rs` — > each parser walks its language's AST and emits the **L2 relational -> tree directly** (`intent_algebra::legacy_expr::QueryExpr`: `Aggregate` +> tree directly** (`intent_algebra::relational::QueryExpr`: `Aggregate` > / `Window` / `Filter` / `Join` / `Sort` / `Limit` / …). That tree > *is* L2. `query_parser::parse_query_expr_canonical` then lowers it to -> the canonical L3 IR via `intent_algebra::legacy_to_canonical` +> the canonical L3 IR via `intent_algebra::lower_to_canonical` > (sketch-fusion + the Binder folded in); `parse_query` projects the > flat `ParsedQuery` summary the legacy analyzer / pipeline consume. > @@ -597,9 +597,9 @@ The schema source is the **`SchemaCatalog`** seam: work. Crucially, **the `Binder` pass does not change when it lands** — only the catalog impl swaps. -Placement: today the Binder runs at the L2→L3 (legacy → canonical) -conversion boundary (`legacy_to_canonical::convert_root` calls it). -Once the legacy IR is retired it moves into the `core::lower` L1→L2→L3 +Placement: today the Binder runs at the L2→L3 (relational → canonical) +conversion boundary (`lower_to_canonical::convert_root` calls it). +Once the `relational` L2 IR is retired it moves into the `core::lower` L1→L2→L3 passes proper — the `lower_*(ast, schema)` signatures below already anticipate a schema parameter at that point. @@ -787,8 +787,8 @@ The `schema` parameter on each pass is supplied by the **Binder** — see §6 "The Binder — name resolution as an explicit pass". The Binder is the single place symbolic names become positional `ColumnId`s; the `lower_*` passes consume its output and never resolve names ad-hoc. Today the -Binder runs at the L2→L3 boundary inside `legacy_to_canonical`; it folds -into these `lower_*` signatures once the legacy IR is retired. +Binder runs at the L2→L3 boundary inside `lower_to_canonical`; it folds +into these `lower_*` signatures once the `relational` L2 IR is retired. ### `core::pipeline` — orchestration @@ -1855,8 +1855,8 @@ The pre-refactor layout grew organically as the controller absorbed three legacy | Retired path | Replacement | |---|---| -| `controller/src/algebra/expr.rs` | `controller/src/intent_algebra/legacy_expr.rs` | -| `controller/src/algebra/lower.rs` | `controller/src/intent_algebra/legacy_to_canonical.rs` (the L2→L3 lowering, folded into the legacy→canonical converter) | +| `controller/src/algebra/expr.rs` | `controller/src/intent_algebra/relational.rs` | +| `controller/src/algebra/lower.rs` | `controller/src/intent_algebra/lower_to_canonical.rs` (the L2→L3 lowering + converter) | | `controller/src/algebra/directory.rs` | `controller/src/physical/sketch_catalog.rs` | | `controller/src/algebra/physical.rs` | `controller/src/physical/planner.rs` | | `controller/src/algebra/allocator.rs` | `controller/src/physical/allocator.rs` | @@ -1883,6 +1883,6 @@ The pre-refactor layout grew organically as the controller absorbed three legacy **TODOs left from the refactor:** - `controller/src/emit/stage_config.rs` (3,020 lines, formerly `config/stage_config.rs`) was moved whole rather than split into `emit/opamp.rs` + `emit/streaming_config.rs` + `emit/inference_config.rs` per design.md §5. The monolith mixes OTel-collector YAML emit, ASAPQuery-backend JSON emit, storage-routing JSON emit, and shared internals; a clean split needs ownership reorganisation, not file renames. Tracked for a follow-up. -- `controller/src/intent_algebra/legacy_expr.rs` carries the legacy **L2 relational** `QueryExpr` the `query_parser` front ends emit; `legacy_to_canonical.rs` is the single pass that converts it (sketch-fusion folded in) to the canonical `intent_algebra::{query_expr,agg_intent}` L3 types. The sketch-fused `SketchAgg` / `WindowedAgg` legacy variants and the standalone `legacy_lower` pass have been retired. Several controller modules (query_parser, physical/, optimizer/, emit/) still reference `legacy_expr` for shared leaf types; fully deleting the legacy tree is a follow-up gated on the canonical `Predicate` covering the remaining `ScalarExpr` variants. The `legacy_*` naming is itself a misnomer — `legacy_expr` is the real, current L2 IR — and an honest rename is the planned follow-up. +- `controller/src/intent_algebra/relational.rs` carries the **L2 relational** `QueryExpr` the `query_parser` front ends emit; `lower_to_canonical.rs` is the single pass that converts it (sketch-fusion folded in) to the canonical `intent_algebra::{query_expr,agg_intent}` L3 types. The sketch-fused `SketchAgg` / `WindowedAgg` variants and the standalone sketch-lowering pass that once produced them have been retired. Several controller modules (query_parser, physical/, optimizer/, emit/) still reference `relational` for shared leaf types; fully *deleting* the L2 tree is gated on the canonical `Predicate` covering the remaining `ScalarExpr` variants — but it is the real, current L2 IR (formerly misnamed `legacy_expr`), not removable debt. - `controller/src/optimizer/trait_def.rs` (`OptimizerRule`), `controller/src/emit/trait_def.rs` (`PlanEmitter`), `controller/src/deployment_model.rs` (`DeploymentModelRegistry`) ship as placeholders — the existing free-function emitters and concrete rule loops still drive behaviour. Migrating them onto the trait surfaces lands when the per-deployment-model crate split lands. diff --git a/control_plane/src/asap_tier_analysis.rs b/control_plane/src/asap_tier_analysis.rs index 19c058be..c5224dc3 100644 --- a/control_plane/src/asap_tier_analysis.rs +++ b/control_plane/src/asap_tier_analysis.rs @@ -268,7 +268,7 @@ fn collect_agg_intents(expr: &QueryExpr, out: &mut Vec) { collect_agg_intents(child, out); } QueryExpr::Scan { .. } | QueryExpr::Ref { .. } => {} - // A-variants lifted in Batch 2 of the legacy_expr migration. They + // A-variants lifted in Batch 2 of the relational migration. They // carry no AggIntent themselves — recurse into their children to // find Aggregates further down the tree. QueryExpr::Filter { child, .. } diff --git a/control_plane/src/intent_algebra/agg_intent.rs b/control_plane/src/intent_algebra/agg_intent.rs index 3a2c8af5..d7b5e00f 100644 --- a/control_plane/src/intent_algebra/agg_intent.rs +++ b/control_plane/src/intent_algebra/agg_intent.rs @@ -104,7 +104,7 @@ pub enum AggIntent { // captures the intent so the routing decision is layered above intent. // // Note: `histogram_quantile(φ, …)` is NOT an L3 intent — it's a PromQL - // /MetricsQL language-level operator. Per Step γ5 of the legacy_expr + // /MetricsQL language-level operator. Per Step γ5 of the relational // migration, the PromQL parser substitutes it directly into a plain // `Aggregate { Quantile(φ) }` (which lowers to // `AggIntent::Quantile { q, accuracy }`); bucket-aware handling is a @@ -336,10 +336,10 @@ fn quantile_suffix(q: f64) -> String { // ── AggIntent helpers ──────────────────────────────────────────────────────── // -// Step γ7: relocated from `legacy_expr.rs` (where they were free fns -// operating on the canonical re-exported `AggIntent`). `legacy_expr` +// Step γ7: relocated from `relational.rs` (where they were free fns +// operating on the canonical re-exported `AggIntent`). `relational` // re-exports them during the legacy-IR retirement; consumers migrate to -// `intent_algebra::*` paths and the re-exports drop with `legacy_expr`. +// `intent_algebra::*` paths and the re-exports drop with `relational`. /// Two instances of this aggregation can be merged /// (`agg(A ∪ B) = combine(agg(A), agg(B))`). `Avg` is the only diff --git a/control_plane/src/intent_algebra/binder.rs b/control_plane/src/intent_algebra/binder.rs index 08db8415..814bf27a 100644 --- a/control_plane/src/intent_algebra/binder.rs +++ b/control_plane/src/intent_algebra/binder.rs @@ -17,7 +17,7 @@ //! Our canonical L3 IR (`query_expr::QueryExpr`) already commits to //! positional column identity — `Aggregate.by: Vec` — exactly //! like RisingWave's `InputRef`. What was missing was the *pass* that -//! produces it: resolution was smeared into `legacy_to_canonical::convert` +//! produces it: resolution was smeared into `lower_to_canonical::convert` //! with a hardcoded synthesized `(ts, value)` schema, so any query //! referencing a real label / column name (`price`, `host`, …) errored. //! @@ -46,11 +46,11 @@ //! ## Where it sits //! //! Today the Binder runs at the L2→L3 (legacy → canonical) conversion -//! boundary — `legacy_to_canonical::convert_root` calls it. Once the +//! boundary — `lower_to_canonical::convert_root` calls it. Once the //! legacy IR is retired it moves into the `core::lower` L1→L2→L3 passes //! proper (the `lower_*(ast, schema)` signatures in design.md §6). -use crate::intent_algebra::legacy_expr::QueryExpr as LQueryExpr; +use crate::intent_algebra::relational::QueryExpr as LQueryExpr; use crate::intent_algebra::schema::{Column, DataType, Schema}; /// The DB / source-schema metadata source from design.md §6 "three @@ -192,7 +192,7 @@ fn collect_referenced_columns(tree: &LQueryExpr) -> Vec { #[cfg(test)] mod tests { use super::*; - use crate::intent_algebra::legacy_expr::{ + use crate::intent_algebra::relational::{ AggFunc, AggItem, ColumnRef as LColumnRef, PartitionKeys, QueryExpr as LQueryExpr, SourceSpec, }; diff --git a/control_plane/src/intent_algebra/column_resolution.rs b/control_plane/src/intent_algebra/column_resolution.rs index 881c24cd..6fae9016 100644 --- a/control_plane/src/intent_algebra/column_resolution.rs +++ b/control_plane/src/intent_algebra/column_resolution.rs @@ -1,9 +1,9 @@ //! Schema-driven column resolution for the legacy `QueryExpr` IR -//! (Step β of the legacy_expr migration). +//! (Step β of the relational migration). //! //! Step α (PR #138) replaced the legacy `AggIntent` enum with the canonical //! [`crate::intent_algebra::agg_intent::AggIntent`]. The legacy IR still -//! uses [`crate::intent_algebra::legacy_expr::ColumnRef::Named(String)`] for +//! uses [`crate::intent_algebra::relational::ColumnRef::Named(String)`] for //! column references; the canonical IR uses positional //! [`crate::intent_algebra::schema::ColumnId`] resolved against a per-node //! [`crate::intent_algebra::schema::Schema`]. @@ -29,7 +29,7 @@ //! //! ## Why a synthesized default //! -//! The legacy_expr migration plan's "Synthesise from metric name: +//! The relational migration plan's "Synthesise from metric name: //! `(ts, value, *labels)`" decision applies here. There is no //! `SchemaCatalog` in the controller today, so the source leaf has to //! produce a schema purely from the metric / table name. This module @@ -41,7 +41,7 @@ use thiserror::Error; use crate::intent_algebra::agg_intent::AggIntent; -use crate::intent_algebra::legacy_expr::{ColumnRef, QueryExpr, SourceSpec}; +use crate::intent_algebra::relational::{ColumnRef, QueryExpr, SourceSpec}; use crate::intent_algebra::schema::{Column, ColumnId, DataType, Schema}; /// Errors returned by [`resolve_column_ref`] / [`resolve_column_refs`]. @@ -81,7 +81,7 @@ pub enum ResolveError { /// representation for "any number of label columns whose names are /// data-dependent." That's tracked as a Step γ TODO at the module level. /// -/// Per the legacy_expr migration plan's "Synthesise from metric name: +/// Per the relational migration plan's "Synthesise from metric name: /// `(ts, value, *labels)`" decision — minus the `*labels` part the /// canonical schema model can't express today. pub fn infer_source_schema(_metric_or_table_name: &str) -> Schema { @@ -170,11 +170,11 @@ pub fn resolve_column_refs( /// Slice-flavoured variant of [`resolve_column_ref`] over a list of /// `Vec` GROUP BY keys (the shape carried by -/// `legacy_expr::QueryExpr::Aggregate.keys`). Mirrors +/// `relational::QueryExpr::Aggregate.keys`). Mirrors /// [`resolve_column_refs`] but skips the `ColumnRef::Named` wrapping — /// the legacy `Aggregate.keys` field is already a `Vec`. /// -/// Used by `legacy_to_canonical::convert` to translate a legacy +/// Used by `lower_to_canonical::convert` to translate a legacy /// `Aggregate.keys: Vec` into the canonical `by: Vec`. pub fn resolve_named_keys( keys: &[String], @@ -295,7 +295,7 @@ pub fn output_schema_for_aggregate( #[cfg(test)] mod tests { use super::*; - use crate::intent_algebra::legacy_expr::SourceSpec; + use crate::intent_algebra::relational::SourceSpec; fn src(name: &str) -> QueryExpr { QueryExpr::Source(SourceSpec { name: name.into() }) @@ -315,8 +315,8 @@ mod tests { #[test] fn root_schema_via_walk() { let expr = QueryExpr::Filter { - pred: crate::intent_algebra::legacy_expr::ScalarExpr::Literal( - crate::intent_algebra::legacy_expr::LiteralValue::Bool(true), + pred: crate::intent_algebra::relational::ScalarExpr::Literal( + crate::intent_algebra::relational::LiteralValue::Bool(true), ), input: Box::new(src("cpu_usage")), }; diff --git a/control_plane/src/intent_algebra/legacy_to_canonical.rs b/control_plane/src/intent_algebra/lower_to_canonical.rs similarity index 97% rename from control_plane/src/intent_algebra/legacy_to_canonical.rs rename to control_plane/src/intent_algebra/lower_to_canonical.rs index 5df062dd..d8f99f6f 100644 --- a/control_plane/src/intent_algebra/legacy_to_canonical.rs +++ b/control_plane/src/intent_algebra/lower_to_canonical.rs @@ -1,13 +1,13 @@ -//! The legacy Layer-2 → canonical L3 IR converter. +//! The Layer-2 → canonical L3 IR converter. //! -//! Recursively converts a *whole* `legacy_expr::QueryExpr` tree (the raw +//! Recursively converts a *whole* `relational::QueryExpr` tree (the raw //! Layer-2 relational IR the `query_parser` front ends emit) into a //! *whole* canonical `query_expr::QueryExpr` tree. This is the single //! entry the parse path routes through — [`convert_root`]. //! //! ## Variant mapping //! -//! | legacy `QueryExpr` | canonical `QueryExpr` | +//! | relational `QueryExpr` | canonical `QueryExpr` | //! |---|---| //! | `Source(spec)` | `Scan { TimeSeries, label_filters: [], schema }` | //! | `Ref(name)` | `Ref { name }` | @@ -24,15 +24,14 @@ //! | `SetOp` | `SetOp` | //! | `Sort` | `Sort` | //! | `Limit` | `Limit` | -//! | `LetBinding` | `LetBinding` (legacy `body` → canonical `child`) | +//! | `LetBinding` | `LetBinding` (relational `body` → canonical `child`)| //! | `PromQLSubquery` | `Subquery` | //! | `BinaryOp` | `BinaryOp` | //! -//! The single-statistic sketchable `Aggregate` fusion was, before the -//! legacy-IR retirement, a separate `legacy_lower` pass that produced -//! intermediate `SketchAgg` / `WindowedAgg` legacy Layer-3 nodes. Those -//! variants are gone — the fusion is now done directly in canonical -//! terms inside the [`convert`] `Aggregate` arm. +//! The single-statistic sketchable `Aggregate` fusion (`Window`-swap, +//! `Partition` wrap, `StdDev` / `Variance` fan-out) is done directly in +//! canonical terms inside the [`convert`] `Aggregate` arm — there is no +//! intermediate sketch-fused L2-or-L3 IR. //! //! ## Schema threading //! @@ -41,7 +40,7 @@ //! complete and self-contained (`(ts, value)` plus every referenced //! name), so threading the root schema down is correct except for the //! nested-schema-transform case (an `Aggregate` below another -//! `Aggregate`), which the legacy stack also doesn't handle — proper +//! `Aggregate`), which the L2→L3 lowering also doesn't handle — proper //! bottom-up schema flow lands with the canonical `output_schema_in` //! wiring downstream. @@ -56,7 +55,7 @@ use crate::intent_algebra::binder::Binder; use crate::intent_algebra::column_resolution::{ resolve_named_keys, ResolveError, }; -use crate::intent_algebra::legacy_expr::{ +use crate::intent_algebra::relational::{ AggFunc, ColumnRef as LColumnRef, PartitionKeys as LPartitionKeys, QueryExpr as LQueryExpr, ScalarExpr as LScalarExpr, }; @@ -479,7 +478,7 @@ fn convert_column_ref(c: &LColumnRef) -> CColumnRef { /// `StdDev` / `Variance` fan-out (the caller wraps the pair in a `Merge` /// of sibling sketch aggregates). fn agg_func_to_intents(func: &AggFunc) -> Vec { - use crate::intent_algebra::legacy_expr::{ + use crate::intent_algebra::relational::{ default_cardinality, default_frequency, default_quantile, }; match func { @@ -517,7 +516,7 @@ fn agg_func_to_intents(func: &AggFunc) -> Vec { #[cfg(test)] mod tests { use super::*; - use crate::intent_algebra::legacy_expr::{ + use crate::intent_algebra::relational::{ AggFunc, AggItem, ColumnRef as LColumnRef, ProjectItem as LProjectItem, SourceSpec, }; @@ -815,7 +814,7 @@ mod tests { offset: 0, input: Box::new(LQueryExpr::Filter { pred: LScalarExpr::Literal( - crate::intent_algebra::legacy_expr::LiteralValue::Bool(true), + crate::intent_algebra::relational::LiteralValue::Bool(true), ), input: Box::new(LQueryExpr::Window { duration: Duration::from_secs(60), diff --git a/control_plane/src/intent_algebra/mod.rs b/control_plane/src/intent_algebra/mod.rs index 4d0ef4fd..671dcc3b 100644 --- a/control_plane/src/intent_algebra/mod.rs +++ b/control_plane/src/intent_algebra/mod.rs @@ -79,22 +79,20 @@ pub mod lower; pub mod query_expr; pub mod schema; -// Refactor 2026-05 (`refactor/controller-layered-cleanup`): the -// pre-existing legacy L2 relational IR formerly at -// `controller/src/algebra/expr.rs` lives here while a separate follow-up -// unifies it with the canonical `query_expr` module above. It still -// carries the `QueryExpr` type the `query_parser` modules emit and the -// planner / allocator / physical planner modules consume today. +// The **Layer-2 relational IR** — the `QueryExpr` tree the `query_parser` +// front ends emit (`promql.rs` / `sql.rs`). Formerly `legacy_expr`; it is +// the real, current L2 IR, not legacy debt. The planner / allocator / +// physical planner still consume it directly while their migration onto +// the canonical L3 `query_expr` types is in progress. pub mod column_resolution; -pub mod legacy_expr; +pub mod relational; -// Step γ7 keystone: the legacy → canonical full-tree converter. The -// `query_parser` entry points emit raw Layer-2 trees and route them -// through `convert_root`, which first folds them into the sketch-fused -// legacy Layer-3 form (the former `legacy_lower` pass, now private to -// this module) and then maps that onto the canonical IR. -pub mod legacy_to_canonical; -pub use legacy_to_canonical::{convert as convert_legacy, convert_root, ConvertError}; +// The L2 → canonical-L3 lowering. The `query_parser` entry points emit a +// raw `relational::QueryExpr` tree and route it through `convert_root`, +// which lowers it (single-statistic sketchable `Aggregate` fusion folded +// in) onto the canonical IR. +pub mod lower_to_canonical; +pub use lower_to_canonical::{convert, convert_root, ConvertError}; // Step γ7: the L3 Binder — name resolution as an explicit pass. Produces // the complete self-contained `Schema` every `ColumnId` indexes into; @@ -120,22 +118,12 @@ pub use query_expr::{ }; pub use schema::{cse_reuse_is_legal, Column, ColumnId, CseError, DataType, Schema}; -// Step β plumbing: schema-driven column resolution helpers used by the -// legacy planning stack (`optimizer/engine.rs`, `physical/{allocator, -// planner, stage_split}.rs`, `query_parser/*`) to carry an inherited -// `Schema` alongside every legacy `QueryExpr` traversal. Consumers call -// `resolve_column_ref` at the point -// where they need a positional `ColumnId` — Step γ migrates variants -// one at a time onto the canonical positional form. +// Schema-driven column-resolution helpers used by the planning stack +// (`optimizer/engine.rs`, `physical/{allocator,planner,stage_split}.rs`, +// `query_parser/*`) to carry an inherited `Schema` alongside a +// `relational::QueryExpr` traversal. Consumers call `resolve_column_ref` +// at the point where they need a positional `ColumnId`. pub use column_resolution::{ infer_schema_for_root, infer_source_schema, output_schema_for_aggregate, resolve_column_ref, resolve_column_refs, resolve_named_keys, ResolveError, }; - -// Step γ7 (PR 13): the four γ1–γ4 one-way "approach (c)" bridges -// (`aggregate_bridge`, `topk_bridge`, `windowed_agg_bridge`, -// `sketch_agg_bridge`) were deleted. They returned canonical-shape data -// *minus the child* — useful only as a non-composable migration aid -// while consumers still pattern-matched legacy variants. Every consumer -// now runs on the canonical IR via the composable `legacy_to_canonical` -// converter, so the bridges had zero remaining call sites. diff --git a/control_plane/src/intent_algebra/query_expr.rs b/control_plane/src/intent_algebra/query_expr.rs index 1c805813..2a7ecf0b 100644 --- a/control_plane/src/intent_algebra/query_expr.rs +++ b/control_plane/src/intent_algebra/query_expr.rs @@ -12,18 +12,18 @@ //! [`QueryExpr::LetBinding`] + [`QueryExpr::Ref`]. //! //! Variant set. Phase B shipped `Scan`, `Window`, `Aggregate`, -//! `LetBinding`, `Ref`. Batch 2 of the legacy_expr migration adds the ten +//! `LetBinding`, `Ref`. Batch 2 of the relational migration adds the ten //! "A-classified" structurally-canonical variants from `design.md` §6: //! `Filter`, `Project`, `Partition`, `Distinct`, `Merge`, `Join`, //! `SetOp`, `Sort`, `Limit`, `BinaryOp`. Step γ7 adds `Subquery` (the -//! canonical counterpart of `legacy_expr::PromQLSubquery`). `WindowFunc` +//! canonical counterpart of `relational::PromQLSubquery`). `WindowFunc` //! remains deferred until the planner grows a consumer for it. The shape //! defined here is forward-compatible — adding more variants is purely //! additive. //! //! Single-input variants here use `child:` (matching the existing //! `Window`, `Aggregate`, `LetBinding` shape). Legacy `input:` survives in -//! `legacy_expr::QueryExpr` until its consumers redirect through here. +//! `relational::QueryExpr` until its consumers redirect through here. #![allow(dead_code)] @@ -113,11 +113,11 @@ pub struct LabelFilter { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct HavingPredicate(pub String); -// ── Supporting types lifted from legacy_expr ───────────────────────────────── +// ── Supporting types lifted from relational ───────────────────────────────── // -// Per Batch 2 of the legacy_expr migration: these are structural copies of +// Per Batch 2 of the relational migration: these are structural copies of // the legacy supporting enums so the canonical [`QueryExpr`] variants below -// can reference them without rooting the canonical IR in `legacy_expr`. +// can reference them without rooting the canonical IR in `relational`. // Field shapes mirror `design.md` §6. /// Which column / field a sketch / projection / DISTINCT operation targets. @@ -283,7 +283,7 @@ pub enum GroupSide { /// Scalar literal value. Subset of values used by the canonical /// [`Predicate`] — extended literal kinds (durations, intervals) stay in -/// `legacy_expr::LiteralValue` until the E-variants migrate. +/// `relational::LiteralValue` until the E-variants migrate. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum LiteralValue { @@ -303,7 +303,7 @@ pub struct ProjectItem { /// Projected expression. Modeled as a [`Predicate`] for the four /// covered scalar shapes (column / literal / binary op / is-null); /// the legacy E-variants (`FunctionCall`, `ScalarSubquery`, `InList`, - /// `Between`) stay in `legacy_expr::ScalarExpr` and live in + /// `Between`) stay in `relational::ScalarExpr` and live in /// [`ProjectItem::raw_expr`] until they migrate. pub expr: Predicate, } @@ -311,7 +311,7 @@ pub struct ProjectItem { // ── Typed Predicate ────────────────────────────────────────────────────────── /// Typed scalar predicate — the canonical counterpart of -/// `legacy_expr::ScalarExpr`. Covers all eight legacy scalar shapes: +/// `relational::ScalarExpr`. Covers all eight legacy scalar shapes: /// column / literal / binary-op / is-null (the structurally clean four) /// plus `FunctionCall` / `InList` / `Between` / `ScalarSubquery` (the /// E-variants). `ScalarSubquery` carries a canonical [`QueryExpr`] — @@ -405,19 +405,19 @@ pub enum QueryExpr { name: BindingName, }, - // ── A-classified variants lifted in Batch 2 of the legacy_expr migration ── + // ── A-classified variants lifted in Batch 2 of the relational migration ── // // Each is a structural copy of the legacy variant of the same name in - // `legacy_expr::QueryExpr`. Single-input variants here use `child:` to + // `relational::QueryExpr`. Single-input variants here use `child:` to // match the existing canonical `Window`/`Aggregate`/`LetBinding` shape, // whereas legacy spells them `input:`. Consumers that haven't migrated - // yet keep using `legacy_expr::QueryExpr::*` — the legacy variants stay + // yet keep using `relational::QueryExpr::*` — the legacy variants stay // in place until the consumer-side redirect lands in subsequent batches. /// σ — row-level filter (WHERE / PromQL label matchers). Uses the new /// typed [`Predicate`] (only Column / Literal / BinaryOp / IsNull at L3 /// for now; `FunctionCall` / `ScalarSubquery` / `InList` / `Between` - /// stay in `legacy_expr::ScalarExpr` until their own batch). + /// stay in `relational::ScalarExpr` until their own batch). Filter { pred: Predicate, child: Box, @@ -495,7 +495,7 @@ pub enum QueryExpr { /// PromQL sub-query (`[range:resolution]`) — re-evaluates `child` /// at `resolution`-spaced steps across the trailing `range` window, /// producing a range-vector the enclosing function consumes. The - /// canonical counterpart of `legacy_expr::QueryExpr::PromQLSubquery`. + /// canonical counterpart of `relational::QueryExpr::PromQLSubquery`. /// Logical pass-through for schema flow — the range/resolution are a /// sampling hint the L5 precompute stage reads, not a schema transform. Subquery { @@ -669,9 +669,9 @@ impl BindingScope { } } -// ── legacy_expr::ScalarExpr → canonical Predicate translation ──────────────── +// ── relational::ScalarExpr → canonical Predicate translation ──────────────── -/// Translate a [`legacy_expr::ScalarExpr`](crate::intent_algebra::legacy_expr::ScalarExpr) +/// Translate a [`relational::ScalarExpr`](crate::intent_algebra::relational::ScalarExpr) /// into the canonical typed [`Predicate`]. All scalar shapes translate /// except `ScalarSubquery`, which carries a legacy `QueryExpr` sub-tree: /// that arm still returns [`QueryExprError::UnsupportedLegacyScalar`] until @@ -682,9 +682,9 @@ impl BindingScope { /// is deliberately narrower than the legacy spelling (no `Duration` literal /// at this layer — see design.md §6 schema-flow `DataType` list). pub fn from_legacy_scalar( - se: &crate::intent_algebra::legacy_expr::ScalarExpr, + se: &crate::intent_algebra::relational::ScalarExpr, ) -> Result { - use crate::intent_algebra::legacy_expr as l; + use crate::intent_algebra::relational as l; match se { l::ScalarExpr::Column(name) => Ok(Predicate::Column(ColumnRef::Named(name.clone()))), l::ScalarExpr::Literal(lit) => Ok(Predicate::Literal(literal_from_legacy(lit))), @@ -735,8 +735,8 @@ pub fn from_legacy_scalar( } } -fn literal_from_legacy(lit: &crate::intent_algebra::legacy_expr::LiteralValue) -> LiteralValue { - use crate::intent_algebra::legacy_expr as l; +fn literal_from_legacy(lit: &crate::intent_algebra::relational::LiteralValue) -> LiteralValue { + use crate::intent_algebra::relational as l; match lit { l::LiteralValue::Null => LiteralValue::Null, l::LiteralValue::Bool(b) => LiteralValue::Bool(*b), @@ -749,8 +749,8 @@ fn literal_from_legacy(lit: &crate::intent_algebra::legacy_expr::LiteralValue) - } } -fn binary_op_from_legacy(op: &crate::intent_algebra::legacy_expr::BinaryOpKind) -> BinaryOpKind { - use crate::intent_algebra::legacy_expr as l; +fn binary_op_from_legacy(op: &crate::intent_algebra::relational::BinaryOpKind) -> BinaryOpKind { + use crate::intent_algebra::relational as l; match op { l::BinaryOpKind::Add => BinaryOpKind::Add, l::BinaryOpKind::Sub => BinaryOpKind::Sub, @@ -1090,7 +1090,7 @@ mod tests { #[test] fn from_legacy_scalar_column() { - use crate::intent_algebra::legacy_expr as l; + use crate::intent_algebra::relational as l; let s = l::ScalarExpr::Column("foo".into()); let p = from_legacy_scalar(&s).unwrap(); assert!(matches!( @@ -1101,7 +1101,7 @@ mod tests { #[test] fn from_legacy_scalar_literal_bool() { - use crate::intent_algebra::legacy_expr as l; + use crate::intent_algebra::relational as l; let s = l::ScalarExpr::Literal(l::LiteralValue::Bool(true)); let p = from_legacy_scalar(&s).unwrap(); assert!(matches!(p, Predicate::Literal(LiteralValue::Bool(true)))); @@ -1109,7 +1109,7 @@ mod tests { #[test] fn from_legacy_scalar_binary_op_and_is_null() { - use crate::intent_algebra::legacy_expr as l; + use crate::intent_algebra::relational as l; let s = l::ScalarExpr::BinaryOp { op: l::BinaryOpKind::Eq, lhs: Box::new(l::ScalarExpr::Column("a".into())), @@ -1137,7 +1137,7 @@ mod tests { #[test] fn from_legacy_scalar_e_variants_translate() { - use crate::intent_algebra::legacy_expr as l; + use crate::intent_algebra::relational as l; let f = l::ScalarExpr::FunctionCall { name: "abs".into(), @@ -1181,7 +1181,7 @@ mod tests { #[test] fn from_legacy_scalar_subquery_still_deferred() { - use crate::intent_algebra::legacy_expr as l; + use crate::intent_algebra::relational as l; // `ScalarSubquery` carries a legacy `QueryExpr` sub-tree — needs the // legacy→canonical tree converter, so it still errors for now. let sq = l::ScalarExpr::ScalarSubquery(Box::new(l::QueryExpr::Ref("cte".into()))); diff --git a/control_plane/src/intent_algebra/legacy_expr.rs b/control_plane/src/intent_algebra/relational.rs similarity index 94% rename from control_plane/src/intent_algebra/legacy_expr.rs rename to control_plane/src/intent_algebra/relational.rs index 3facfe01..a45b0e78 100644 --- a/control_plane/src/intent_algebra/legacy_expr.rs +++ b/control_plane/src/intent_algebra/relational.rs @@ -1,4 +1,6 @@ -//! General query algebra — the full IR for SQL and PromQL queries. +//! The **Layer-2 relational IR** — the per-language query algebra the +//! `query_parser` front ends emit, before lowering to the canonical L3 +//! `intent_algebra::query_expr` types. //! //! This module defines two mutually recursive expression types: //! @@ -27,9 +29,9 @@ use std::time::Duration; use crate::types_v2::AccuracyTarget; -// ── AggIntent harmonization (Step α of legacy_expr migration) ──────────────── +// ── AggIntent harmonization ────────────────────────────────────────────────── // -// The legacy `AggIntent` / `ExactAgg` enums that historically lived here have +// The `AggIntent` / `ExactAgg` enums that historically lived here have // been deleted in favor of the canonical `intent_algebra::agg_intent::AggIntent` // vocabulary. The translation table is documented in the migration spec; in // short: @@ -64,7 +66,7 @@ use crate::types_v2::AccuracyTarget; // is a typed search-and-replace rather than a semantic rewrite. /// Canonical L3 aggregation intent. Re-exported here so existing -/// `legacy_expr::AggIntent` references keep working — the type is now the +/// `relational::AggIntent` references keep working — the type is now the /// single canonical [`crate::intent_algebra::agg_intent::AggIntent`]. pub use crate::intent_algebra::agg_intent::AggIntent; @@ -131,8 +133,8 @@ pub enum ColumnRef { // ── AggIntent helpers ──────────────────────────────────────────────────────── // // Step γ7 (PR 13.5): the `AggIntent` helper free fns relocated to their -// canonical home, `intent_algebra::agg_intent`. `legacy_expr` re-exports -// the live ones so existing `legacy_expr::*` call sites keep compiling +// canonical home, `intent_algebra::agg_intent`. `relational` re-exports +// the live ones so existing `relational::*` call sites keep compiling // during the retirement; PR 13 sweeps the consumer imports to the // canonical path and drops these re-exports with the file. The dead // helpers (`agg_to_legacy_agg_type`, `agg_quantiles`, @@ -147,8 +149,8 @@ pub use crate::intent_algebra::agg_intent::{ // // The 2026-05 layered-cleanup refactor moved these helpers to // `sketch_algebra::capability` (their structural home — sketch-family -// error bounds). The thin re-exports below keep `legacy_expr::hll_accuracy` / -// `legacy_expr::countmin_accuracy` available so the in-file +// error bounds). The thin re-exports below keep `relational::hll_accuracy` / +// `relational::countmin_accuracy` available so the in-file // `default_cardinality` call site (and any external `algebra::expr::hll_accuracy` // reference resolved via the back-compat `algebra` alias in `lib.rs`) keep // compiling. @@ -197,7 +199,7 @@ pub enum FilterVal { // ── Relational algebra ──────────────────────────────────────────────────────── -/// The legacy **Layer-2 relational** query IR. +/// The **Layer-2 relational** query IR. /// /// Every variant is a *node* in the per-language logical query plan tree /// the `query_parser` front ends (`promql.rs` / `sql.rs`) emit. Leaves @@ -208,19 +210,19 @@ pub enum FilterVal { /// /// Most variants have canonical structural twins in /// [`crate::intent_algebra::query_expr::QueryExpr`]. The canonical -/// spelling uses `child:` where these legacy variants use `input:`; the +/// spelling uses `child:` where these L2 variants use `input:`; the /// typed [`crate::intent_algebra::Predicate`] replaces [`ScalarExpr`] in /// `Filter` / `Join` / `Aggregate::having` (translation via /// [`crate::intent_algebra::from_legacy_scalar`]). /// /// This is purely a Layer-2 *relational* IR — the sketch-fused /// `SketchAgg` / `WindowedAgg` variants were removed once the -/// `legacy_to_canonical` converter learned to fold the single-statistic +/// `lower_to_canonical` converter learned to fold the single-statistic /// sketchable `Aggregate` straight into canonical shapes. The remaining -/// reason the tree is not yet *deleted* outright is that the legacy -/// [`ScalarExpr`] still carries four variants (`FunctionCall`, -/// `ScalarSubquery`, `InList`, `Between`) the canonical `Predicate` -/// doesn't cover, and the parsers build `ScalarExpr` directly. +/// reason the tree is not yet *deleted* outright is that [`ScalarExpr`] +/// still carries four variants (`FunctionCall`, `ScalarSubquery`, +/// `InList`, `Between`) the canonical `Predicate` doesn't cover, and the +/// parsers build `ScalarExpr` directly. #[derive(Debug, Clone)] pub enum QueryExpr { // ── Base relations ──────────────────────────────────────────────────── @@ -269,10 +271,10 @@ pub enum QueryExpr { }, // The sketch-fused `SketchAgg` / `WindowedAgg` variants were removed: - // they were never parser output — only an intermediate the old - // `legacy_lower` pass produced — and the legacy → canonical converter - // now folds the single-statistic sketchable `Aggregate` straight into - // canonical shapes (`Aggregate { by: [] }` / `Window { Aggregate }`). + // they were never parser output — only an intermediate of an earlier + // sketch-lowering pass — and `lower_to_canonical` now folds the + // single-statistic sketchable `Aggregate` straight into canonical + // shapes (`Aggregate { by: [] }` / `Window { Aggregate }`). // ── Distributed / multi-stage operators ────────────────────────────── @@ -349,8 +351,8 @@ pub enum QueryExpr { // ── PromQL-specific operators ───────────────────────────────────────── // Note: `histogram_quantile(φ, )` is no longer a `QueryExpr` - // variant. Per Step γ5 of the legacy_expr migration, the PromQL parser - // substitutes the call with a plain `Aggregate { Quantile(φ) }` so + // variant. The PromQL parser substitutes the call with a plain + // `Aggregate { Quantile(φ) }` so // downstream code (lowerer, optimizer, physical planner) sees a single // canonical Quantile intent. @@ -499,10 +501,9 @@ impl AggFunc { /// Suggest the appropriate [`AggIntent`] for this function, if any. /// - /// Canonical Quantile is single-φ post Step α; this helper returns one - /// canonical intent. Callers that need multi-φ behaviour build the - /// merge fan-out themselves (cf. the construction sites in - /// `legacy_lower::agg_func_to_intent`). + /// Canonical Quantile is single-φ; this helper returns one canonical + /// intent. Callers that need multi-φ behaviour build the merge fan-out + /// themselves (cf. `lower_to_canonical::agg_func_to_intents`). pub fn to_sketch_op(&self) -> Option { match self { AggFunc::Quantile(phi) => Some(default_quantile(*phi)), @@ -520,10 +521,10 @@ impl AggFunc { // Leaf algebra types — `BinaryOpKind`, `JoinKind`, `SetOpKind`, // `VectorMatch` / `VectorMatchKind` / `VectorGrouping` / `GroupSide`, -// `SortKey` — are owned by the canonical `query_expr` module. The legacy +// `SortKey` — are owned by the canonical `query_expr` module. The L2 // definitions were byte-identical (modulo extra `Hash` / `serde` derives -// on the canonical side), so `legacy_expr` now re-exports them: every -// `legacy_expr::BinaryOpKind` reference resolves to the single canonical +// on the canonical side), so `relational` now re-exports them: every +// `relational::BinaryOpKind` reference resolves to the single canonical // type. `impl Display for BinaryOpKind` moved alongside the type. pub use crate::intent_algebra::query_expr::{ BinaryOpKind, GroupSide, JoinKind, SetOpKind, SortKey, VectorGrouping, VectorMatch, @@ -617,7 +618,7 @@ impl QueryExpr { // ── Predicate → ScalarExpr conversion ──────────────────────────────────────── -/// Convert a slice of legacy [`Predicate`]s (AND-list) into a single +/// Convert a slice of [`Predicate`]s (AND-list) into a single /// [`ScalarExpr`] tree. An empty slice becomes `Literal(true)`. fn scalar_from_predicates(preds: &[Predicate]) -> ScalarExpr { if preds.is_empty() { diff --git a/control_plane/src/lib.rs b/control_plane/src/lib.rs index 497c848f..c601171a 100644 --- a/control_plane/src/lib.rs +++ b/control_plane/src/lib.rs @@ -18,7 +18,7 @@ //! //! | Old path | New path | //! |---|---| -//! | `controller/src/algebra/expr.rs` | `controller/src/intent_algebra/legacy_expr.rs` | +//! | `controller/src/algebra/expr.rs` | `controller/src/intent_algebra/relational.rs` | //! | `controller/src/algebra/lower.rs` | `controller/src/intent_algebra/legacy_lower.rs` | //! | `controller/src/algebra/directory.rs` | `controller/src/physical/sketch_catalog.rs` | //! | `controller/src/algebra/physical.rs` | `controller/src/physical/planner.rs` | @@ -41,10 +41,10 @@ //! - `sketch_algebra` — `Capability` enum + `capability_for(agg_intent: &AggIntent)` //! lookup table (Phase 4 / 5 use this to route raw-name PromQL). //! - `intent_algebra` — `AggIntent` + `QueryExpr` DAG (canonical L3 IR; -//! `legacy_expr` carries the L2 relational IR the parsers emit and -//! `legacy_to_canonical` lowers it to the canonical L3 types). +//! `relational` carries the L2 relational IR the parsers emit and +//! `lower_to_canonical` lowers it to the canonical L3 types). //! - `query_parser` — front-end parsers (Layer 1): `promql.rs` / `sql.rs` -//! emit the L2 relational `legacy_expr::QueryExpr` tree. +//! emit the L2 relational `relational::QueryExpr` tree. //! - `physical` — L5 framework (allocator, planner, plan, sketch_catalog, //! colored_dag, stage_split, topology). //! - `optimizer` — L4 rule engine + cost model traits/impls + baseline @@ -82,7 +82,7 @@ pub mod workload; // `pub mod algebra { … }`, `pub use physical::colored_dag as stage_split`, // `pub mod planner { … }`) have been removed. `main.rs` and other // consumers now reference the canonical module names directly -// (`emit`, `pipeline`, `intent_algebra::legacy_expr`, `optimizer`, +// (`emit`, `pipeline`, `intent_algebra::relational`, `optimizer`, // `physical`, `physical::colored_dag`, etc.) per the layered-cleanup // follow-up task. /// PromQL → ASAP-tier candidate analyzer. Phase-9 unification of the diff --git a/control_plane/src/optimizer/cost/mod.rs b/control_plane/src/optimizer/cost/mod.rs index dae0f1dc..1428516a 100644 --- a/control_plane/src/optimizer/cost/mod.rs +++ b/control_plane/src/optimizer/cost/mod.rs @@ -514,11 +514,11 @@ fn subtree_cost_bundled( let child_cost = subtree_cost_bundled(child, &extended, &extended_scope)?; Ok(bind_cost + child_cost) } - // A-variants lifted in Batch 2 of the legacy_expr migration — no + // A-variants lifted in Batch 2 of the relational migration — no // canonical-cost consumer exercises them yet. Fall through to a // child-walk-only contribution (0 cost added at this node) until // the per-node cost primitives land alongside their consumers. - // TODO(legacy_expr-migration): add proper node_cost_* helpers for + // TODO(relational-migration): add proper node_cost_* helpers for // Filter/Project/Partition/Distinct/Merge/Join/SetOp/Sort/Limit/BinaryOp. _ => walk_children_zero_cost_bundled(expr, binding_costs, schema_scope), } diff --git a/control_plane/src/optimizer/engine.rs b/control_plane/src/optimizer/engine.rs index 0a039f20..e8a47c2b 100644 --- a/control_plane/src/optimizer/engine.rs +++ b/control_plane/src/optimizer/engine.rs @@ -31,7 +31,7 @@ use std::collections::HashMap; use crate::intent_algebra::agg_intent::AggIntent; -use crate::intent_algebra::legacy_expr::{agg_is_exact, agg_is_mergeable}; +use crate::intent_algebra::relational::{agg_is_exact, agg_is_mergeable}; use crate::intent_algebra::query_expr::{ColumnRef, Predicate, QueryExpr, SetOpKind, Source}; use crate::sketch_algebra::capability::{ default_capability_table, load_capability_overrides, SketchCapability, @@ -537,7 +537,7 @@ impl RewriteRule for TopKFusion { // ── R6 (retired): HistogramQuantileFusion ───────────────────────────────────── // -// Per Step γ5 of the legacy_expr migration, `histogram_quantile(φ, …)` is +// Per Step γ5 of the relational migration, `histogram_quantile(φ, …)` is // substituted at the PromQL parser level into a plain // `Aggregate{Quantile(φ)}`. The fusion rule that used to merge a // `HistogramQuantile` wrapper with an inner DDSketch is no longer needed. @@ -1212,7 +1212,7 @@ impl OptimizerRule for CommonSubexprElim { #[cfg(test)] mod tests { use super::*; - use crate::intent_algebra::legacy_expr::{default_cardinality, default_quantile}; + use crate::intent_algebra::relational::{default_cardinality, default_quantile}; use crate::intent_algebra::{ BinaryOpKind, LiteralValue, PartitionKeys, Schema, SortKey, Source, WindowKind, }; diff --git a/control_plane/src/optimizer/mod.rs b/control_plane/src/optimizer/mod.rs index a79bf5a2..a6f358b7 100644 --- a/control_plane/src/optimizer/mod.rs +++ b/control_plane/src/optimizer/mod.rs @@ -2,7 +2,7 @@ //! //! Per `control_plane/docs/design.md` §3/§5/§6 `core::optimizer`: this is //! the rule engine driver + cost-model trait + rule library that takes -//! L3 [`crate::intent_algebra::legacy_expr::QueryExpr`] / canonical +//! L3 [`crate::intent_algebra::relational::QueryExpr`] / canonical //! [`crate::intent_algebra::QueryExpr`] inputs and produces L4 //! sketch-bound output (in the legacy path: an annotated `QueryExpr` //! with `SketchAgg` nodes; in the canonical path: a diff --git a/control_plane/src/optimizer/trait_def.rs b/control_plane/src/optimizer/trait_def.rs index a9c930fa..880f92c5 100644 --- a/control_plane/src/optimizer/trait_def.rs +++ b/control_plane/src/optimizer/trait_def.rs @@ -8,7 +8,7 @@ //! //! * The legacy [`crate::optimizer::engine::RewriteRule`] trait drives the //! fixed-point [`crate::optimizer::engine::QueryOptimizer`] over the -//! legacy [`crate::intent_algebra::legacy_expr::QueryExpr`] IR. +//! legacy [`crate::intent_algebra::relational::QueryExpr`] IR. //! * The Phase-C bind-rule trait [`crate::sketch_algebra::rules::Rule`] //! lowers L3 → L4 [`crate::sketch_algebra::PhysicalExpr`] on the //! canonical [`crate::intent_algebra::QueryExpr`] IR. diff --git a/control_plane/src/physical/allocator.rs b/control_plane/src/physical/allocator.rs index 857ce6b3..df70b7de 100644 --- a/control_plane/src/physical/allocator.rs +++ b/control_plane/src/physical/allocator.rs @@ -40,7 +40,7 @@ use super::plan::{CostEstimate, ExecutionMode, NodeAnnotation, PipelineStage, PlanNode}; use crate::intent_algebra::agg_intent::AggIntent; -use crate::intent_algebra::legacy_expr::agg_is_exact; +use crate::intent_algebra::relational::agg_is_exact; use crate::intent_algebra::schema::ColumnId; use crate::intent_algebra::QueryExpr; use crate::types::{SketchType, StageResourceBudgets}; @@ -708,7 +708,7 @@ fn canonical_intent_kind_str(intent: &AggIntent) -> &'static str { #[cfg(test)] mod tests { use super::*; - use crate::intent_algebra::legacy_expr::{ + use crate::intent_algebra::relational::{ default_cardinality, default_frequency, default_quantile, }; use crate::intent_algebra::{ diff --git a/control_plane/src/physical/colored_dag/allocator.rs b/control_plane/src/physical/colored_dag/allocator.rs index c28fc8d1..ac7bafdb 100644 --- a/control_plane/src/physical/colored_dag/allocator.rs +++ b/control_plane/src/physical/colored_dag/allocator.rs @@ -236,7 +236,7 @@ impl ThreeStageWalker { .get(name.as_str()) .copied() .ok_or_else(|| AllocateError::UnresolvedRef(name.as_str().to_string())), - // A-variants lifted in Batch 2 of the legacy_expr migration. + // A-variants lifted in Batch 2 of the relational migration. // No colored-DAG consumer constructs them today; conservatively // route to the Edge stage (matches the per-row Scan/Window // policy) so the build is total. The proper stage-placement diff --git a/control_plane/src/physical/colored_dag/emitter.rs b/control_plane/src/physical/colored_dag/emitter.rs index 5fc09760..aa587cc9 100644 --- a/control_plane/src/physical/colored_dag/emitter.rs +++ b/control_plane/src/physical/colored_dag/emitter.rs @@ -607,7 +607,7 @@ fn extract_edge_facts(qe: &crate::intent_algebra::QueryExpr, edge: &mut EdgeStag extract_edge_facts(child, edge); } QE::Ref { .. } => {} - // A-variants lifted in Batch 2 of the legacy_expr migration. No + // A-variants lifted in Batch 2 of the relational migration. No // canonical-side consumer constructs them yet — recurse into // children so we still surface edge facts (source metric, label // filters, window size) from any leaves below. diff --git a/control_plane/src/physical/mod.rs b/control_plane/src/physical/mod.rs index fd97ae1c..1fa4b3f1 100644 --- a/control_plane/src/physical/mod.rs +++ b/control_plane/src/physical/mod.rs @@ -12,7 +12,7 @@ //! //! | File | Role | //! |---|---| -//! | [`allocator`] | `SketchAllocator` — assigns physical ops to pipeline stages over the legacy [`crate::intent_algebra::legacy_expr::QueryExpr`] IR | +//! | [`allocator`] | `SketchAllocator` — assigns physical ops to pipeline stages over the legacy [`crate::intent_algebra::relational::QueryExpr`] IR | //! | [`planner`] | `PhysicalPlanner` — `QueryExpr` → staged sub-plans (DC three-stage emission) | //! | [`plan`] | `PlanNode` / `PlanSummary` — annotated plan tree with cost estimates + stage colouring | //! | [`sketch_catalog`] | Candidate sketch types per `AggIntent`, default `SketchParams`, memory estimation | diff --git a/control_plane/src/physical/planner.rs b/control_plane/src/physical/planner.rs index 0d603cdd..702f420d 100644 --- a/control_plane/src/physical/planner.rs +++ b/control_plane/src/physical/planner.rs @@ -676,7 +676,7 @@ pub fn physical_plan_to_staged( #[cfg(test)] mod tests { use super::*; - use crate::intent_algebra::legacy_expr::{ + use crate::intent_algebra::relational::{ default_cardinality, default_frequency, default_quantile, }; use crate::intent_algebra::{Schema, Source, WindowKind}; diff --git a/control_plane/src/physical/sketch_catalog.rs b/control_plane/src/physical/sketch_catalog.rs index 5a656657..df926eeb 100644 --- a/control_plane/src/physical/sketch_catalog.rs +++ b/control_plane/src/physical/sketch_catalog.rs @@ -9,7 +9,7 @@ //! //! All callers now go through this module. -use crate::intent_algebra::legacy_expr::{agg_accuracy, AggIntent, PerPartitionWrap}; +use crate::intent_algebra::relational::{agg_accuracy, AggIntent, PerPartitionWrap}; use crate::types::{ AggType, SketchDefaults, SketchParams, SketchType, @@ -267,20 +267,20 @@ mod tests { #[test] fn op_cardinality_yields_hll_type() { - let op = crate::intent_algebra::legacy_expr::default_cardinality(); + let op = crate::intent_algebra::relational::default_cardinality(); assert_eq!(sketch_type_for_op(&op), SketchType::HLL); } #[test] fn op_frequency_yields_countsketch() { - let op = crate::intent_algebra::legacy_expr::default_frequency(); + let op = crate::intent_algebra::relational::default_frequency(); assert_eq!(sketch_type_for_op(&op), SketchType::CountSketch); } #[test] fn per_partition_delegates_to_inner() { let wrap = PerPartitionWrap { - inner: crate::intent_algebra::legacy_expr::default_cardinality(), + inner: crate::intent_algebra::relational::default_cardinality(), keys: vec!["k".into()], }; assert_eq!(sketch_type_for_per_partition(&wrap), SketchType::HLL); @@ -295,7 +295,7 @@ mod tests { #[test] fn memory_per_partition_scales_by_keys() { - let inner = crate::intent_algebra::legacy_expr::default_cardinality(); + let inner = crate::intent_algebra::relational::default_cardinality(); let base_mem = estimated_sketch_memory_bytes(&inner); let wrap = PerPartitionWrap { inner, diff --git a/control_plane/src/physical/stage_split.rs b/control_plane/src/physical/stage_split.rs index 4e5f276e..2593305e 100644 --- a/control_plane/src/physical/stage_split.rs +++ b/control_plane/src/physical/stage_split.rs @@ -44,7 +44,7 @@ use std::time::Duration; use crate::intent_algebra::agg_intent::AggIntent; -use crate::intent_algebra::legacy_expr::{agg_is_exact, agg_is_mergeable}; +use crate::intent_algebra::relational::{agg_is_exact, agg_is_mergeable}; use crate::intent_algebra::query_expr::{ BinaryOpKind, ColumnRef, GroupSide, LiteralValue, Predicate, QueryExpr, Source, VectorMatchKind, @@ -635,7 +635,7 @@ pub fn split_typed_three_stage( #[cfg(test)] mod tests { use super::*; - use crate::intent_algebra::legacy_expr::{ + use crate::intent_algebra::relational::{ default_cardinality, default_frequency, default_quantile, }; use crate::intent_algebra::{PartitionKeys, Schema, Source, WindowKind}; diff --git a/control_plane/src/physical/window_fusion.rs b/control_plane/src/physical/window_fusion.rs index a41b9c35..7437eb73 100644 --- a/control_plane/src/physical/window_fusion.rs +++ b/control_plane/src/physical/window_fusion.rs @@ -10,7 +10,7 @@ //! The canonical L3 IR keeps "one canonical form per plan" (design.md //! §6): it has no fused windowed-aggregate variant — a windowed sketch is //! the stacked `Window { child: Aggregate { aggs: [one], .. } }` shape. -//! `legacy_to_canonical` produces exactly that shape when it folds a +//! `lower_to_canonical` produces exactly that shape when it folds a //! single-statistic sketchable `Aggregate` sitting over a `Window`. This //! module is the planner-side **peephole recognizer** that puts the //! window-defines-sketch-lifecycle invariant back: it matches the stacked @@ -53,7 +53,7 @@ pub struct FusedWindowSketch<'a> { } /// Recognize the canonical `Window { child: Aggregate { aggs: [one], -/// having: None, .. } }` shape — the stacked form `legacy_to_canonical` +/// having: None, .. } }` shape — the stacked form `lower_to_canonical` /// folds a legacy `WindowedAgg` into — and return a [`FusedWindowSketch`] /// view of it. Returns `None` for any other shape (a multi-intent /// `Aggregate`, an `Aggregate` with a `having` clause, a `Window` over a @@ -172,14 +172,14 @@ pub fn fused_sketch_decision( // // Pins `recognize_windowed_sketch` + `fused_sketch_decision` against the // canonical planner: a `Window { Aggregate }` fused sketch — the shape -// `legacy_to_canonical` folds a single-statistic sketchable `Aggregate` +// `lower_to_canonical` folds a single-statistic sketchable `Aggregate` // over a `Window` into — produces a resolved (non-`None`) physical window, // and the planner's own decision matches `fused_sketch_decision`. #[cfg(test)] mod tests { use super::*; - use crate::intent_algebra::legacy_expr::{ + use crate::intent_algebra::relational::{ AggFunc, AggItem, ColumnRef as LColumnRef, QueryExpr as LQueryExpr, SourceSpec, }; use crate::intent_algebra::{ @@ -206,7 +206,7 @@ mod tests { } /// The canonical `Window { Aggregate { by: [], aggs: [agg] } }` shape — - /// the fold `legacy_to_canonical` produces for a windowed single-sketch + /// the fold `lower_to_canonical` produces for a windowed single-sketch /// aggregate. fn windowed_sketch( agg: AggIntent, diff --git a/control_plane/src/query_parser/mod.rs b/control_plane/src/query_parser/mod.rs index 3aac597b..dd26d1d2 100644 --- a/control_plane/src/query_parser/mod.rs +++ b/control_plane/src/query_parser/mod.rs @@ -90,7 +90,7 @@ pub enum QueryHint { // ── Public entry points ─────────────────────────────────────────────────────── /// Parse a raw query string (PromQL or SQL) into the **legacy Layer-2** -/// [`legacy_expr::QueryExpr`](crate::intent_algebra::legacy_expr::QueryExpr) IR. +/// [`relational::QueryExpr`](crate::intent_algebra::relational::QueryExpr) IR. /// /// Both parsers emit Layer-2 relational operators (`Aggregate { AggFunc }`, /// `Window`, `Filter`, `Join`, …). The Layer-2 → Layer-3 sketch lowering @@ -102,7 +102,7 @@ pub enum QueryHint { /// [`parse_query_expr_canonical`], which is the public canonical-IR entry. pub(crate) fn parse_query_expr( query: &str, -) -> anyhow::Result { +) -> anyhow::Result { let q = query.trim(); let upper = q.to_ascii_uppercase(); if upper.starts_with("SELECT") || upper.starts_with("WITH") { @@ -513,7 +513,7 @@ mod tests { #[test] fn legacy_entry_point_returns_raw_layer2() { - use crate::intent_algebra::legacy_expr::{AggFunc, QueryExpr as LQueryExpr}; + use crate::intent_algebra::relational::{AggFunc, QueryExpr as LQueryExpr}; // `parse_query_expr` is the crate-internal language-dispatch front // door: it returns the raw legacy Layer-2 relational tree with no // sketch lowering applied — the L2→L3 fusion now lives inside diff --git a/control_plane/src/query_parser/promql.rs b/control_plane/src/query_parser/promql.rs index 7c910c9e..fc715009 100644 --- a/control_plane/src/query_parser/promql.rs +++ b/control_plane/src/query_parser/promql.rs @@ -35,7 +35,7 @@ use std::time::Duration; use anyhow::anyhow; use promql_parser::parser::{self, AggregateExpr, Call, Expr, LabelModifier, MatrixSelector, VectorSelector}; -use crate::intent_algebra::legacy_expr::{FilterOp, FilterVal, PartitionKeys, Predicate}; +use crate::intent_algebra::relational::{FilterOp, FilterVal, PartitionKeys, Predicate}; // ── Walk context ────────────────────────────────────────────────────────────── @@ -153,7 +153,7 @@ fn modifier_to_partition(modifier: &LabelModifier) -> PartitionKeys { // | `m[5m:1m]` subquery | PromQLSubquery { 5m, Some(1m) } | // | `a op b` binary | BinaryOp { VectorMatch } | -use crate::intent_algebra::legacy_expr::{ +use crate::intent_algebra::relational::{ AggFunc, AggItem, BinaryOpKind, ColumnRef as QeColumnRef, GroupSide, PartitionKeys as QePartitionKeys, QueryExpr, @@ -165,7 +165,7 @@ use promql_parser::parser::{token::TokenType, BinaryExpr, VectorMatchCardinality /// /// This preserves `PromQLSubquery` and `BinaryOp` nodes natively; /// `histogram_quantile(φ, …)` is substituted into a plain -/// `Aggregate { Quantile(φ) }` per Step γ5 of the legacy_expr migration. +/// `Aggregate { Quantile(φ) }` per Step γ5 of the relational migration. pub fn parse_promql_expr(query: &str) -> anyhow::Result { let expr = parser::parse(query) .map_err(|e| anyhow!("PromQL parse error: {e}"))?; @@ -311,7 +311,7 @@ fn walk_call_qe(call: &Call, ctx: WalkCtx) -> anyhow::Result { match name { // histogram_quantile(φ, bucket_metric) → plain Aggregate { Quantile(φ) }. // - // Per Step γ5 of the legacy_expr migration: at the PromQL parser level + // Per Step γ5 of the relational migration: at the PromQL parser level // we substitute `histogram_quantile(φ, bucket_metric)` with the same // shape that `quantile_over_time(φ, m[w])` produces — an `Aggregate` // carrying a single `AggFunc::Quantile(φ)`. Downstream code (the @@ -494,7 +494,7 @@ fn apply_qe_filters( if filters.is_empty() { input } else { - use crate::intent_algebra::legacy_expr::{BinaryOpKind, LiteralValue, ScalarExpr}; + use crate::intent_algebra::relational::{BinaryOpKind, LiteralValue, ScalarExpr}; let pred = filters.iter().fold( ScalarExpr::Literal(LiteralValue::Bool(true)), |acc, p| { @@ -599,7 +599,7 @@ mod tests { /// multi-quantile machinery. #[test] fn histogram_quantile_lowers_to_plain_aggregate_quantile() { - use crate::intent_algebra::legacy_expr::{AggFunc, ColumnRef, QueryExpr}; + use crate::intent_algebra::relational::{AggFunc, ColumnRef, QueryExpr}; let qe = super::parse_promql_expr( r#"histogram_quantile(0.99, rate(http_requests_bucket{le="0.5"}[5m]))"#, diff --git a/control_plane/src/query_parser/sql.rs b/control_plane/src/query_parser/sql.rs index d571d5b9..cd6c9eee 100644 --- a/control_plane/src/query_parser/sql.rs +++ b/control_plane/src/query_parser/sql.rs @@ -47,7 +47,7 @@ use sqlparser::ast::{ }; use sqlparser::dialect::GenericDialect; -use crate::intent_algebra::legacy_expr::{ +use crate::intent_algebra::relational::{ AggFunc, AggItem as AlgAggItem, BinaryOpKind, ColumnRef, JoinKind, LiteralValue, ProjectItem, QueryExpr, ScalarExpr, SetOpKind, SortKey, SourceSpec, }; @@ -731,7 +731,7 @@ fn expr_to_col_name(expr: &Expr) -> Option { #[cfg(test)] mod tests { use super::parse_sql_expr; - use crate::intent_algebra::legacy_expr::QueryExpr; + use crate::intent_algebra::relational::QueryExpr; use crate::types::AggType; fn parse(sql: &str) -> QueryExpr { diff --git a/control_plane/src/sketch_algebra/capability.rs b/control_plane/src/sketch_algebra/capability.rs index 6a0536d9..925d4c4c 100644 --- a/control_plane/src/sketch_algebra/capability.rs +++ b/control_plane/src/sketch_algebra/capability.rs @@ -605,7 +605,7 @@ pub fn load_capability_overrides(path: &str) -> HashMap PhysicalExpr { QueryExpr::Aggregate { .. } | QueryExpr::Scan { .. } | QueryExpr::Window { .. } => { PhysicalExpr::Logical(expr.clone()) } - // A-variants lifted in Batch 2 of the legacy_expr migration. No + // A-variants lifted in Batch 2 of the relational migration. No // sketch binding rule applies to these shapes today — wrap as a // logical pass-through, matching the policy for `Aggregate` / // `Scan` / `Window`. Rule extensions can specialise individual