From 45a4725433a427a90e4d979cf415d716f9532c44 Mon Sep 17 00:00:00 2001 From: zz_y Date: Tue, 18 Aug 2026 17:19:39 -0600 Subject: [PATCH] rename: drop stale L1/L2/L3/L4/L5 layer-numbering jargon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `pre-asap-ir.md`/`post-asap-ir.md` design docs don't define an L1–L5 layer numbering (#211 explicitly removed it from the docs as undefined jargon, in favor of the terms the docs actually define: "pre-ASAP IR" / "post-ASAP IR"). The source code still had it all over — both as real type names and as prose shorthand — surfaced during #214's review ("what is L3Scalar?" → "does L2 refer to pre-asap-ir?"). This drops it from the code too. Type renames (mechanical, no behavior change): - `L2QueryExpr` -> `UnresolvedQueryExpr` (`QueryExpr`, the front-end-emitted, name-based tree) - `L3QueryExpr` -> `ResolvedQueryExpr` (`QueryExpr`, the positional, resolved tree) - `L4Node` -> `SummaryNode`, `L4Schema` -> `SummarySchema`, `L4DataType` -> `SummaryDataType`, `L4Field` -> `SummaryField` (all four already sat next to `SummaryExpr`/`SummaryKind`/`SummaryParams` in `post_asap` — this makes them consistent with their own module's naming instead of a leftover number) The two front ends' local `L2` construction alias (`UnresolvedQueryExpr as L2`) becomes `Unresolved`, matching the same treatment. A few incidentally-adjacent stale names got the same cleanup while touching their call sites: `df_expr_to_l2` -> `df_expr_to_unresolved`, `scalar_value_to_l3` -> `scalar_value_to_asap`, `arrow_to_l3`/`l3_to_arrow` -> `arrow_to_dtype`/`dtype_to_arrow`, `matcher_to_l3expr` -> `matcher_to_compare`, plus local variables named `l2`/`l3`/`l4` (`tree`, `resolved`, `bound`, `logical_leaf`, …) and two test-local `as L2`/`as L3` aliases (dropped — the bare names already read fine at those call sites). Two real name collisions surfaced while doing this: `sql/mod.rs` and `sql/types.rs` both already import `datafusion::common::ScalarValue` unqualified — aliased that import to `DfScalarValue` in both (same `DfColumn`/`ArrowDataType`-style convention those files already use), rather than picking a different name for the `asap-types` type. The much larger remainder was prose: module docs, doc comments, and `//` explanatory comments across nearly every crate describing a step as "L1 parse", "the L2 tree", "an L4 concern", etc. Replaced with either the descriptive phrase already used elsewhere in this repo ("pre-ASAP" / "post-ASAP" / "canonical" / "unresolved" / "resolved"/ "a deployment's own physical stage" for the L5 placeholder that was never modeled in this crate to begin with) or, where the sentence just meant "the canonical tree", dropped the label entirely. A few doubly-stale spots got fixed at the same time since they were in the exact text being rewritten: `bind.rs`'s/`show_post_asap_ir.rs`'s references to the deleted `asap-ir`/`asap-lower` crate names, and `devtools/src/lib.rs`'s reference to a "shared L2→L3 converter" that issue #179 removed (each front end now calls `resolve_root` directly). Left alone: `boundary.rs`'s "L2-norm"/"L1-norm" (real math, Count- Sketch's error bound), and two literal citations of an external design doc's own section titles ("§6 ... L3 edge", "§'L4 — sketch algebra'") in `schema.rs`/`l4_binding.rs` — that doc's own numbering, not this crate's. Verified: cargo build / clippy(--all-targets --all-features --locked -- -D warnings) / fmt --all -- --check / test(--locked) / doc --workspace --no-deps all clean. Same test counts as before, all green. `cargo doc` warnings are one *fewer* than baseline (fixed the `asap_ir` link along the way) — no new warnings introduced. Pure rename — no behavior change. Co-Authored-By: Claude Sonnet 5 --- crates/asap-aware-mapping/src/bind.rs | 104 +++--- crates/asap-aware-mapping/src/boundary.rs | 14 +- crates/asap-aware-mapping/src/lib.rs | 26 +- crates/devtools/examples/topk_ir.rs | 2 +- crates/devtools/src/bin/dag_export.rs | 2 +- crates/devtools/src/bin/show_post_asap_ir.rs | 8 +- crates/devtools/src/bin/show_pre_asap_ir.rs | 7 +- crates/devtools/src/lib.rs | 4 +- crates/devtools/tests/cross_language.rs | 9 +- crates/frontend-promql/src/error.rs | 15 +- crates/frontend-promql/src/histogram.rs | 3 +- crates/frontend-promql/src/lib.rs | 14 +- crates/frontend-promql/src/promql.rs | 300 ++++++++++-------- .../awesome_prometheus_alerts.rs | 11 +- .../tests/observability/o11y_bench_promql.rs | 2 +- .../tests/observability/promql_corpus.rs | 23 +- .../tests/promql_conformance.rs | 18 +- .../tests/promql_equivalence.rs | 33 +- .../frontend-promql/tests/promql_lowering.rs | 12 +- crates/frontend-sql/src/error.rs | 14 +- crates/frontend-sql/src/lib.rs | 20 +- crates/frontend-sql/src/sql/expr.rs | 106 ++++--- crates/frontend-sql/src/sql/mod.rs | 206 ++++++------ crates/frontend-sql/src/sql/types.rs | 22 +- crates/frontend-sql/tests/sql_lowering.rs | 45 +-- crates/integration-tests/src/lib.rs | 11 +- crates/integration-tests/tests/l4_binding.rs | 33 +- crates/types/src/dag_export.rs | 2 +- crates/types/src/lib.rs | 11 +- crates/types/src/post_asap/expr.rs | 77 ++--- crates/types/src/post_asap/mod.rs | 12 +- crates/types/src/post_asap/schema.rs | 36 ++- crates/types/src/post_asap/sketch.rs | 5 +- crates/types/src/pre_asap/agg_intent.rs | 37 +-- crates/types/src/pre_asap/binder.rs | 32 +- crates/types/src/pre_asap/canonicalize.rs | 8 +- .../types/src/pre_asap/column_resolution.rs | 25 +- crates/types/src/pre_asap/mod.rs | 20 +- crates/types/src/pre_asap/query_expr.rs | 55 ++-- crates/types/src/pre_asap/resolve.rs | 29 +- crates/types/src/pre_asap/schema.rs | 27 +- crates/types/src/workload.rs | 2 +- 42 files changed, 770 insertions(+), 672 deletions(-) diff --git a/crates/asap-aware-mapping/src/bind.rs b/crates/asap-aware-mapping/src/bind.rs index 6fbf515d..6417ab22 100644 --- a/crates/asap-aware-mapping/src/bind.rs +++ b/crates/asap-aware-mapping/src/bind.rs @@ -1,8 +1,8 @@ -//! The L3→L4 binding pass (issue #98). +//! The pre-ASAP → post-ASAP binding pass (issue #98). //! -//! Walks a canonical L3 [`QueryExpr`] and emits the sketch-bound L4 IR -//! ([`SummaryExpr`] / [`L4Node`] in `asap-sketch`). Per node, the -//! [`boundary`](crate::boundary) decision picks the realization: +//! Walks a canonical pre-ASAP [`QueryExpr`] and emits the sketch-bound +//! post-ASAP IR ([`SummaryExpr`] / [`SummaryNode`] in `asap-sketch`). Per +//! node, the [`boundary`](crate::boundary) decision picks the realization: //! //! - **Sketch** — the `Aggregate` becomes a [`SummaryExpr::SummaryAgg`] //! carrying the committed `(SummaryKind, SummaryParams)`, wrapped in a @@ -10,10 +10,11 @@ //! `Sketch(…)` column type does not propagate past the estimate); //! - **Exact accumulator** — a `SummaryAgg` with an exact kind //! (`Sum`/`Count`/`MinMax`/`Rate`/`Increase`) and no estimate: the partial -//! state *is* the value, so L5 finalization is the identity; -//! - **Pass-through** — the whole L3 subtree is wrapped as +//! state *is* the value, so a deployment's later finalization step is the +//! identity; +//! - **Pass-through** — the whole pre-ASAP subtree is wrapped as //! [`SummaryExpr::Logical`], schema lifted with every column -//! `L4DataType::Primitive`. +//! `SummaryDataType::Primitive`. //! //! Binding recurses through the `Aggregate` spine, so nested aggregates each //! get their own decision (`quantile(0.9, sum by (svc) (rate(m[5m]))))` binds @@ -21,18 +22,20 @@ //! //! ## Conservative fallbacks //! -//! [`SummaryExpr::Logical`] boxes a whole L3 subtree — it has no L4 children — -//! so a *logical* operator above a bindable aggregate (`Filter`/`BinaryOp`/… -//! over a quantile) subsumes the aggregate into the logical wrapper unbound. -//! Rewriting through logical parents is the L4 rule engine's job (#6/#33), -//! not this pass's. Similarly conservative: multi-intent `Aggregate` nodes -//! (SQL `SELECT SUM(a), AVG(b)`) and aggregates with a `HAVING` predicate -//! (the filter would need the estimate first) stay logical. +//! [`SummaryExpr::Logical`] boxes a whole pre-ASAP subtree — it has no +//! post-ASAP children — so a *logical* operator above a bindable aggregate +//! (`Filter`/`BinaryOp`/… over a quantile) subsumes the aggregate into the +//! logical wrapper unbound. Rewriting through logical parents is the +//! post-ASAP rule engine's job (#6/#33), not this pass's. Similarly +//! conservative: multi-intent `Aggregate` nodes (SQL `SELECT SUM(a), AVG(b)`) +//! and aggregates with a `HAVING` predicate (the filter would need the +//! estimate first) stay logical. use std::rc::Rc; use asap_types::post_asap::{ - L4DataType, L4Field, L4Node, L4Schema, SketchQuery, SummaryExpr, SummaryKind, SummaryParams, + SketchQuery, SummaryDataType, SummaryExpr, SummaryField, SummaryKind, SummaryNode, + SummaryParams, SummarySchema, }; use asap_types::pre_asap::agg_intent::AggIntent; use asap_types::pre_asap::expr_ir::ColumnRef; @@ -43,19 +46,19 @@ use thiserror::Error; use crate::boundary::{implementation_for_with, Implementation}; use crate::cost_model::{CostModel, DefaultCostModel}; -/// Errors from the L3→L4 binding pass. +/// Errors from the pre-ASAP → post-ASAP binding pass. #[derive(Debug, Error)] pub enum ImplementError { - /// L3 schema derivation failed while lifting an edge to `L4Schema`. - #[error("schema derivation failed during L3→L4 binding: {0}")] + /// Schema derivation failed while lifting an edge to `SummarySchema`. + #[error("schema derivation failed during pre-ASAP → post-ASAP binding: {0}")] Schema(#[from] QueryExprError), } -/// Bind a single query to the L4 IR. Ranks candidate summaries via +/// Bind a single query to the post-ASAP IR. Ranks candidate summaries via /// [`DefaultCostModel`] (`asap-plan`'s built-in static preference order, /// unchanged); use [`implement_tree_with`] to plug in a deployment-specific /// [`CostModel`] instead. -pub fn implement_tree(expr: &QueryExpr) -> Result, ImplementError> { +pub fn implement_tree(expr: &QueryExpr) -> Result, ImplementError> { implement_tree_with(expr, &DefaultCostModel) } @@ -64,7 +67,7 @@ pub fn implement_tree(expr: &QueryExpr) -> Result, ImplementError> { pub fn implement_tree_with( expr: &QueryExpr, cost_model: &dyn CostModel, -) -> Result, ImplementError> { +) -> Result, ImplementError> { if let QueryExpr::Aggregate { reduction, measures, @@ -102,11 +105,11 @@ fn bind_summary_agg( params: SummaryParams, estimate: bool, cost_model: &dyn CostModel, -) -> Result, ImplementError> { +) -> Result, ImplementError> { let child_schema = child.output_schema()?; - // The single canonical L3 derivation (per-series vs cross-series, name - // overrides) already computes the row shape; L4 only retypes the summary - // state column. + // The single canonical pre-ASAP derivation (per-series vs cross-series, + // name overrides) already computes the row shape; binding only retypes + // the summary state column. let per_series = matches!(reduction, Reduction::PerEntity); let by: Vec = reduction .group_keys() @@ -120,7 +123,7 @@ fn bind_summary_agg( let mut state_schema = lift(&out_schema); if let Some(field) = state_schema.fields.get_mut(state_idx) { - field.dtype = L4DataType::Sketch(kind.clone(), params.clone()); + field.dtype = SummaryDataType::Sketch(kind.clone(), params.clone()); } // `reduction` is carried onto `SummaryAgg` verbatim — not flattened to a @@ -128,7 +131,7 @@ fn bind_summary_agg( // a genuine empty-`by` reduction apart from a per-entity shape with no // grouping concept at all (issue #163). `bind_summary_agg` is the single // place that decides this; nothing downstream re-derives it. - let agg = Rc::new(L4Node { + let agg = Rc::new(SummaryNode { expr: SummaryExpr::SummaryAgg { child: implement_tree_with(child, cost_model)?, summary: kind, @@ -139,9 +142,9 @@ fn bind_summary_agg( schema: state_schema, }); match query { - // The readout: downstream of the estimate the schema is the plain L3 - // row shape again (the `Sketch(…)` type does not propagate). - Some(query) => Ok(Rc::new(L4Node { + // The readout: downstream of the estimate the schema is the plain + // pre-ASAP row shape again (the `Sketch(…)` type does not propagate). + Some(query) => Ok(Rc::new(SummaryNode { expr: SummaryExpr::SummaryEstimate { summary_input: agg, query, @@ -209,29 +212,29 @@ fn readout(intent: &AggIntent, col: &ColumnRef, cost_model: &dyn CostModel) -> S } } -/// Wrap an unrewritten L3 subtree, lifting its schema with every column -/// `L4DataType::Primitive`. Public so a deployment can force a node it +/// Wrap an unrewritten pre-ASAP subtree, lifting its schema with every column +/// `SummaryDataType::Primitive`. Public so a deployment can force a node it /// knows `implement_tree_in_with` would otherwise actively (mis)bind — /// e.g. an intent this crate's `boundary::implementation_for` maps to an /// accumulator kind the deployment's runtime doesn't actually implement — /// through the same fallback this crate's own dispatch uses, without /// duplicating the schema-lift logic. -pub fn logical(expr: &QueryExpr) -> Result, ImplementError> { +pub fn logical(expr: &QueryExpr) -> Result, ImplementError> { let schema = expr.output_schema()?; - Ok(Rc::new(L4Node { + Ok(Rc::new(SummaryNode { expr: SummaryExpr::Logical(Box::new(expr.clone())), schema: lift(&schema), })) } -fn lift(schema: &Schema) -> L4Schema { - L4Schema { +fn lift(schema: &Schema) -> SummarySchema { + SummarySchema { fields: schema .columns .iter() - .map(|c| L4Field { + .map(|c| SummaryField { name: c.name.clone(), - dtype: L4DataType::Primitive(c.dtype.clone()), + dtype: SummaryDataType::Primitive(c.dtype.clone()), nullable: c.nullable, }) .collect(), @@ -285,7 +288,7 @@ mod tests { } } - fn field<'a>(schema: &'a L4Schema, name: &str) -> &'a L4Field { + fn field<'a>(schema: &'a SummarySchema, name: &str) -> &'a SummaryField { schema .fields .iter() @@ -311,11 +314,11 @@ mod tests { // Estimate edge: plain row shape — group key + Float64 answer. assert_eq!( field(&root.schema, "quantile_0_99").dtype, - L4DataType::Primitive(DataType::Float64) + SummaryDataType::Primitive(DataType::Float64) ); assert_eq!( field(&root.schema, "job").dtype, - L4DataType::Primitive(DataType::Utf8) + SummaryDataType::Primitive(DataType::Utf8) ); let SummaryExpr::SummaryAgg { @@ -335,7 +338,7 @@ mod tests { // SummaryAgg edge: the state column carries the committed (kind, params). assert_eq!( field(&summary_input.schema, "quantile_0_99").dtype, - L4DataType::Sketch(SummaryKind::Kll, SummaryParams::Kll { k: 200 }) + SummaryDataType::Sketch(SummaryKind::Kll, SummaryParams::Kll { k: 200 }) ); assert!(matches!(child.expr, SummaryExpr::Logical(ref e) if matches!(**e, QueryExpr::Scan { .. }))); @@ -508,7 +511,7 @@ mod tests { assert_eq!(params, &SummaryParams::Sum); assert_eq!( field(&root.schema, "sum").dtype, - L4DataType::Sketch(SummaryKind::Sum, SummaryParams::Sum) + SummaryDataType::Sketch(SummaryKind::Sum, SummaryParams::Sum) ); } @@ -538,16 +541,16 @@ mod tests { ); assert_eq!( field(&root.schema, "value").dtype, - L4DataType::Sketch(SummaryKind::Rate, SummaryParams::Rate) + SummaryDataType::Sketch(SummaryKind::Rate, SummaryParams::Rate) ); assert_eq!(root.schema.time_index, Some(0)); } /// Issue #163, case 1: a bare per-series range function (e.g. /// `quantile_over_time(...)`) binds to `SummaryAgg { reduction: - /// PerEntity, .. }` — proving the L3 `Reduction` this crate already - /// computes (issue #165) is carried onto the L4 node verbatim, not - /// flattened back into an ambiguous bare `Vec`. + /// PerEntity, .. }` — proving the pre-ASAP `Reduction` this crate + /// already computes (issue #165) is carried onto the post-ASAP node + /// verbatim, not flattened back into an ambiguous bare `Vec`. #[test] fn bare_per_series_aggregate_binds_summary_agg_with_per_entity_reduction() { let q = agg_per_entity( @@ -642,7 +645,7 @@ mod tests { } /// The `col` of the first `SummaryAgg` in the tree. - fn find_summary_col(node: &L4Node) -> Option { + fn find_summary_col(node: &SummaryNode) -> Option { match &node.expr { SummaryExpr::SummaryAgg { col, .. } => Some(col.clone()), SummaryExpr::SummaryEstimate { summary_input, .. } => find_summary_col(summary_input), @@ -675,8 +678,9 @@ mod tests { #[test] fn logical_parent_subsumes_bindable_child() { - // Filter over a bindable quantile: `Logical` has no L4 children, so - // the conservative fallback keeps the whole subtree logical. + // Filter over a bindable quantile: `Logical` has no post-ASAP + // children, so the conservative fallback keeps the whole subtree + // logical. let q = QueryExpr::Filter { pred: Predicate(Box::new(QueryExpr::Compare { left: Box::new(QueryExpr::Column(0)), diff --git a/crates/asap-aware-mapping/src/boundary.rs b/crates/asap-aware-mapping/src/boundary.rs index b59ab5d3..8139b93c 100644 --- a/crates/asap-aware-mapping/src/boundary.rs +++ b/crates/asap-aware-mapping/src/boundary.rs @@ -2,8 +2,8 @@ //! //! The per-node choice of how an [`AggIntent`] is *realised*: by an //! approximate sketch, by an exact mergeable accumulator, or by an ordinary -//! exact operator (pass-through). This is an L4 concern: L3 carries only the -//! intent + accuracy target, never the realization. +//! exact operator (pass-through). This is a post-ASAP concern: the pre-ASAP +//! IR carries only the intent + accuracy target, never the realization. //! //! The decision consumes three inputs: //! @@ -30,7 +30,7 @@ use asap_types::types::AccuracyTarget; use crate::cost_model::{CostModel, DefaultCostModel}; -/// How an [`AggIntent`] is realised at L4. +/// How an [`AggIntent`] is realised at post-ASAP binding time. #[derive(Debug, Clone, PartialEq)] pub enum Implementation { /// A summary — either an approximate sketch sized to the intent's @@ -45,8 +45,8 @@ pub enum Implementation { kind: SummaryKind, params: SummaryParams, }, - /// No summary form — the node stays a logical L3 operator and is executed - /// exactly (per-series transforms, non-mergeable reducers, exact + /// No summary form — the node stays a logical pre-ASAP operator and is + /// executed exactly (per-series transforms, non-mergeable reducers, exact /// quantile/top-k/cardinality, classic-bucket `HistogramQuantile`, …). PassThrough, } @@ -79,8 +79,8 @@ pub enum Implementation { /// single-population query via re-aggregation is a fact about that /// deployment's storage layout, not about `SummaryKind` at all — /// `SummaryKind`'s exact accumulators don't encode grouping (grouping -/// lives on the L4 node's `by` instead), so there is nothing in this -/// crate's own vocabulary to subsume. +/// lives on the post-ASAP node's `by` instead), so there is nothing in +/// this crate's own vocabulary to subsume. /// /// Implementations are expected to consult `required`/`available`'s /// `kind` (and whatever grouping/placement context the deployment tracks diff --git a/crates/asap-aware-mapping/src/lib.rs b/crates/asap-aware-mapping/src/lib.rs index 5f89e03b..d963d81e 100644 --- a/crates/asap-aware-mapping/src/lib.rs +++ b/crates/asap-aware-mapping/src/lib.rs @@ -1,18 +1,18 @@ -//! `asap-plan` — the cost-aware optimizer layer over the L3 intent algebra. +//! `asap-plan` — the cost-aware optimizer layer over the pre-ASAP intent algebra. //! //! This crate sits between the language-agnostic IR ([`asap_ir`]) and -//! any runtime: it consumes L3 [`QueryExpr`](asap_types::pre_asap::QueryExpr) -//! trees and makes the cost-aware decisions that L3 deliberately leaves open — -//! which shared sub-expressions to hoist and which sketch (if any) realises -//! each approximate intent. +//! any runtime: it consumes pre-ASAP [`QueryExpr`](asap_types::pre_asap::QueryExpr) +//! trees and makes the cost-aware decisions the pre-ASAP IR deliberately +//! leaves open — which shared sub-expressions to hoist and which sketch (if +//! any) realises each approximate intent. //! //! It depends only on the IR crate, never on a front end — the layering //! invariant (arrows point up) holds here too. //! //! Post-lowering **canonicalization** is *not* here: it landed in //! `asap_types::pre_asap::canonicalize`, run inside the shared `resolve_root` -//! so every front end normalizes before L3 leaves resolution (issue #34, -//! closed). +//! so every front end normalizes before the pre-ASAP IR leaves resolution +//! (issue #34, closed). //! //! ## Status //! @@ -43,13 +43,13 @@ //! distinct from a *transformation rule*, logical → logical — see Graefe, //! *The Cascades Framework for Query Optimization*): //! -//! | Term | Layer | Meaning | Lives in | +//! | Term | Stage | Meaning | Lives in | //! |---|---|---|---| -//! | **Parse** | L1 | text (PromQL/SQL) → AST | `asap-frontend-promql` / `asap-frontend-sql` | -//! | **Bind #1** | L2 | *name resolution*: `ColumnRef` (a name) → `ColumnId` (a concrete schema column) — the classic RDBMS "Parse → **Bind** → Optimize" pipeline sense (e.g. SQL Server's query-processor terminology) | [`asap_types::pre_asap::binder::Binder`](https://docs.rs/asap-types) | -//! | **Implementation** — [`boundary::implementation_for`] | L3→L4, *one node* | choosing a concrete physical realization (a sketch family, an exact accumulator, or pass-through) for one [`AggIntent`](asap_types::pre_asap::agg_intent::AggIntent) | [`boundary`] | -//! | **`implement_tree`** — [`bind::implement_tree`] | L3→L4, *whole tree* | walk a whole `QueryExpr` tree, calling [`boundary::implementation_for`] per node, and emit the complete L4 [`SummaryExpr`](asap_types::post_asap::SummaryExpr)/`L4Node` DAG — named after "implementation" too rather than reusing "bind" a second time | [`bind`] | -//! | **Bind #2** (downstream, not in this crate) | L4→L5 | a *deployment's* own physical binder, additionally deciding **placement** (edge vs. backend, wire format, …) — a genuinely different, deployment-specific decision this crate doesn't model at all | e.g. `control_plane::sketch_algebra::rules::bind_*` (as of this writing; expected to fold into that deployment's cost-model layer rather than stay a separate "bind" concept) | +//! | **Parse** | parse | text (PromQL/SQL) → AST | `asap-frontend-promql` / `asap-frontend-sql` | +//! | **Bind #1** | name resolution | `ColumnRef` (a name) → `ColumnId` (a concrete schema column) — the classic RDBMS "Parse → **Bind** → Optimize" pipeline sense (e.g. SQL Server's query-processor terminology) | [`asap_types::pre_asap::binder::Binder`](https://docs.rs/asap-types) | +//! | **Implementation** — [`boundary::implementation_for`] | pre-ASAP → post-ASAP, *one node* | choosing a concrete physical realization (a sketch family, an exact accumulator, or pass-through) for one [`AggIntent`](asap_types::pre_asap::agg_intent::AggIntent) | [`boundary`] | +//! | **`implement_tree`** — [`bind::implement_tree`] | pre-ASAP → post-ASAP, *whole tree* | walk a whole `QueryExpr` tree, calling [`boundary::implementation_for`] per node, and emit the complete post-ASAP [`SummaryExpr`](asap_types::post_asap::SummaryExpr)/`SummaryNode` DAG — named after "implementation" too rather than reusing "bind" a second time | [`bind`] | +//! | **Bind #2** (downstream, not in this crate) | post-ASAP → deployment placement | a *deployment's* own physical binder, additionally deciding **placement** (edge vs. backend, wire format, …) — a genuinely different, deployment-specific decision this crate doesn't model at all | e.g. `control_plane::sketch_algebra::rules::bind_*` (as of this writing; expected to fold into that deployment's cost-model layer rather than stay a separate "bind" concept) | //! //! A related question (tracked alongside issues #6/#33): whether this //! crate should also own a **matching** predicate — "does an already diff --git a/crates/devtools/examples/topk_ir.rs b/crates/devtools/examples/topk_ir.rs index 32bf327f..a44d2501 100644 --- a/crates/devtools/examples/topk_ir.rs +++ b/crates/devtools/examples/topk_ir.rs @@ -1,7 +1,7 @@ // cargo run -p asap-lower --example topk_ir // // Lowers every topk-shaped query from the design discussion and prints the -// resulting L3 IR. Used for interactive exploration; not a test. +// resulting pre-ASAP IR. Used for interactive exploration; not a test. use asap_devtools::{lower_promql, lower_sql, SqlCatalog}; use asap_types::pre_asap::schema::{Column, DataType, Schema}; diff --git a/crates/devtools/src/bin/dag_export.rs b/crates/devtools/src/bin/dag_export.rs index 2edf14aa..7c71eb68 100644 --- a/crates/devtools/src/bin/dag_export.rs +++ b/crates/devtools/src/bin/dag_export.rs @@ -2,7 +2,7 @@ // --sql "SELECT service, COUNT(*) FROM metrics GROUP BY service" --name q1 \ // --promql "topk(5, rate(http_requests_total[5m]))" --name q2 // -// Lowers each given SQL/PromQL query to L3 IR and prints a single +// Lowers each given SQL/PromQL query to pre-ASAP IR and prints a single // `asap_types::dag_export::WorkloadGraph` as JSON on stdout — the input format // for `tools/dag-viewer` (issue #133). Redirect to a file and load it there: // cargo run -p asap-lower --bin dag_export -- --sql "..." --name q1 > /tmp/dag.json diff --git a/crates/devtools/src/bin/show_post_asap_ir.rs b/crates/devtools/src/bin/show_post_asap_ir.rs index 8edaca7d..1092a662 100644 --- a/crates/devtools/src/bin/show_post_asap_ir.rs +++ b/crates/devtools/src/bin/show_post_asap_ir.rs @@ -1,10 +1,10 @@ // cargo run -p asap-devtools --bin show_post_asap_ir -- queries.txt // (or pipe via stdin: cargo run -p asap-devtools --bin show_post_asap_ir < queries.txt) // -// Lowers a batch of ad-hoc SQL/PromQL queries to pre-ASAP IR (L3), then runs -// the `asap-aware-mapping` L3→L4 binding pass and prints the resulting -// **post-ASAP IR** (L4, the sketch-bound IR: `SummaryExpr`/`L4Node` — the -// concrete `SummaryKind`/`SummaryParams` committed per aggregate, or +// Lowers a batch of ad-hoc SQL/PromQL queries to pre-ASAP IR, then runs the +// `asap-aware-mapping` pre-ASAP → post-ASAP binding pass and prints the +// resulting **post-ASAP IR** (the sketch-bound IR: `SummaryExpr`/`SummaryNode` +// — the concrete `SummaryKind`/`SummaryParams` committed per aggregate, or // `Logical` for whatever the pass left untouched). See `show_pre_asap_ir` // for the sketch-agnostic IR one layer upstream. // diff --git a/crates/devtools/src/bin/show_pre_asap_ir.rs b/crates/devtools/src/bin/show_pre_asap_ir.rs index 95432793..c2a3c9ac 100644 --- a/crates/devtools/src/bin/show_pre_asap_ir.rs +++ b/crates/devtools/src/bin/show_pre_asap_ir.rs @@ -1,10 +1,11 @@ // cargo run -p asap-devtools --bin show_pre_asap_ir -- queries.txt // (or pipe via stdin: cargo run -p asap-devtools --bin show_pre_asap_ir < queries.txt) // -// Lowers a batch of ad-hoc SQL/PromQL queries to **pre-ASAP IR** (L3, the +// Lowers a batch of ad-hoc SQL/PromQL queries to **pre-ASAP IR** (the // sketch-agnostic intent algebra: `QueryExpr`/`AggIntent`) and prints them. -// See `show_post_asap_ir` for the L4 sketch-bound IR one layer downstream — -// this tool never picks a sketch, it only shows what a query means. +// See `show_post_asap_ir` for the post-ASAP sketch-bound IR one layer +// downstream — this tool never picks a sketch, it only shows what a query +// means. // // File format: one query per line, prefixed with "sql>" or "promql>". // Blank lines and lines starting with '#' are ignored. diff --git a/crates/devtools/src/lib.rs b/crates/devtools/src/lib.rs index 44d8177b..f9feca6c 100644 --- a/crates/devtools/src/lib.rs +++ b/crates/devtools/src/lib.rs @@ -1,8 +1,8 @@ //! `asap-lower` — thin facade over the two query front ends. //! //! Re-exports both language paths so a caller can depend on a single crate for -//! PromQL *and* SQL. Both front ends end at the canonical intent algebra via the -//! same shared L2→L3 converter in [`asap_ir`]. +//! PromQL *and* SQL. Both front ends end at the canonical intent algebra via +//! the same shared [`resolve_root`](asap_types::pre_asap::resolve_root). //! //! ## Dependency isolation //! diff --git a/crates/devtools/tests/cross_language.rs b/crates/devtools/tests/cross_language.rs index a6a502ac..0a4c664b 100644 --- a/crates/devtools/tests/cross_language.rs +++ b/crates/devtools/tests/cross_language.rs @@ -1,8 +1,9 @@ -//! Cross-language L3 equivalence (issue #34). +//! Cross-language pre-ASAP-IR equivalence (issue #34). //! //! Semantically equivalent SQL and PromQL queries must lower to the **same -//! canonical intent algebra**, so an L4 rule matching on `AggIntent` sees one -//! spelling regardless of source language. These tests are the executable spec +//! canonical intent algebra**, so a post-ASAP binding rule matching on +//! `AggIntent` sees one spelling regardless of source language. These tests +//! are the executable spec //! for the shared [`canonicalize`](asap_types::pre_asap::canonicalize) pass: they pin the //! canonical heavy-hitter shape and assert both front ends reach it. //! @@ -97,7 +98,7 @@ async fn sql_and_promql_heavy_hitter_share_the_canonical_shape() { #[tokio::test] async fn sql_aliased_and_inline_count_topk_are_identical() { - // #20: aliasing the COUNT in the ORDER BY must not change the L3. + // #20: aliasing the COUNT in the ORDER BY must not change the canonical shape. let inline = sql( "SELECT service, COUNT(*) FROM metrics GROUP BY service ORDER BY COUNT(*) DESC LIMIT 5", ) diff --git a/crates/frontend-promql/src/error.rs b/crates/frontend-promql/src/error.rs index 06a6270a..932f77b8 100644 --- a/crates/frontend-promql/src/error.rs +++ b/crates/frontend-promql/src/error.rs @@ -2,9 +2,10 @@ use std::fmt; use asap_types::pre_asap::ResolveTreeError; -/// Errors from lowering a PromQL query (L1 parse → canonical L2, built -/// directly → [`resolve_root`](asap_types::pre_asap::resolve_root) binds it -/// to L3, issue #179). +/// Errors from lowering a PromQL query (parse → the canonical, unresolved +/// tree, built directly → +/// [`resolve_root`](asap_types::pre_asap::resolve_root) binds it to the +/// resolved tree, issue #179). /// /// Carries no DataFusion type — the PromQL front end never depends on the SQL /// stack. The language-neutral variants (`UnsupportedFeature` / `WrongLanguage` @@ -12,7 +13,7 @@ use asap_types::pre_asap::ResolveTreeError; /// shared, so neither front end pulls the other's parser. #[derive(Debug)] pub enum PromqlError { - /// The `promql-parser` crate rejected the query string (L1 parse failure). + /// The `promql-parser` crate rejected the query string (parse failure). Parse(String), /// A PromQL function (`rate`, `*_over_time`, …) not supported in this version. UnsupportedFunction(String), @@ -27,8 +28,8 @@ pub enum PromqlError { InvalidParameter(String), /// The workload's query language is not PromQL. WrongLanguage(String), - /// Resolving the canonical L2 tree to L3 failed (name resolution against - /// the bound schema). + /// Resolving the canonical unresolved tree failed (name resolution + /// against the bound schema). Convert(ResolveTreeError), } @@ -42,7 +43,7 @@ impl fmt::Display for PromqlError { Self::MissingArgument(m) => write!(f, "missing argument: {m}"), Self::InvalidParameter(m) => write!(f, "invalid parameter: {m}"), Self::WrongLanguage(l) => write!(f, "unsupported query language: {l}"), - Self::Convert(e) => write!(f, "L2→L3 resolution failed: {e}"), + Self::Convert(e) => write!(f, "column resolution failed: {e}"), } } } diff --git a/crates/frontend-promql/src/histogram.rs b/crates/frontend-promql/src/histogram.rs index e73b0670..f3f971a5 100644 --- a/crates/frontend-promql/src/histogram.rs +++ b/crates/frontend-promql/src/histogram.rs @@ -3,7 +3,8 @@ //! `histogram_quantile(φ, m)` has two lowerings: exact interpolation over //! classic cumulative `le` buckets (`AggIntent::HistogramQuantile`, **not** //! sketch-able) versus the generic sketch-able `Quantile` (native histograms / -//! raw samples, which L4 can approximate to an accuracy target). The true +//! raw samples, which post-ASAP binding can approximate to an accuracy +//! target). The true //! signal is the argument's **sample type**, which query structure only //! *proxies* — see the structural `is_classic_bucket_arg` heuristic, whose //! false-positive (`…_bucket`-named non-histogram) and false-negative diff --git a/crates/frontend-promql/src/lib.rs b/crates/frontend-promql/src/lib.rs index 6c2c8870..08fec09a 100644 --- a/crates/frontend-promql/src/lib.rs +++ b/crates/frontend-promql/src/lib.rs @@ -1,7 +1,7 @@ -//! PromQL front end: L1 (parse via `promql-parser`) → the canonical L2 shape, -//! built directly (issue #179) → [`resolve_root`]. +//! PromQL front end: parse (via `promql-parser`) → the canonical, unresolved +//! shape, built directly (issue #179) → [`resolve_root`]. //! -//! Emits [`L2QueryExpr`](asap_types::pre_asap::L2QueryExpr) itself — the +//! Emits [`UnresolvedQueryExpr`](asap_types::pre_asap::UnresolvedQueryExpr) itself — the //! canonical `QueryExpr`, generic over an unresolved //! [`ColumnRef`](asap_types::pre_asap::ColumnRef) — directly, rather than a //! separate per-language relational tree; `resolve_root` runs the @@ -21,7 +21,7 @@ pub use error::PromqlError; pub use histogram::{HistogramCatalog, HistogramKind}; pub use promql::PromqlLowerer; -/// Lower a single PromQL query string to the canonical L3 `QueryExpr`. +/// Lower a single PromQL query string to the canonical, resolved `QueryExpr`. /// /// `accuracy` is threaded onto every approximate intent (`Count`, `Quantile`, /// `Cardinality`, `TopK`). The returned tree carries a self-contained `Schema` @@ -30,9 +30,9 @@ pub use promql::PromqlLowerer; /// `histogram_quantile` discrimination uses the structural heuristic; to drive /// it from declared sample types instead, use [`lower_promql_with_histograms`]. pub fn lower_promql(query: &str, accuracy: AccuracyTarget) -> Result { - let l2 = PromqlLowerer::lower(query, &accuracy)?; - let l3 = resolve_root(&l2)?; - Ok(l3) + let unresolved = PromqlLowerer::lower(query, &accuracy)?; + let resolved = resolve_root(&unresolved)?; + Ok(resolved) } /// Like [`lower_promql`], but consults `histograms` to decide whether a diff --git a/crates/frontend-promql/src/promql.rs b/crates/frontend-promql/src/promql.rs index e59984a2..18c82d3e 100644 --- a/crates/frontend-promql/src/promql.rs +++ b/crates/frontend-promql/src/promql.rs @@ -1,22 +1,23 @@ -//! Layers 1→2 lowering: PromQL string → the canonical, unresolved -//! [`L2QueryExpr`](asap_types::pre_asap::query_expr::L2QueryExpr) +//! PromQL string → the canonical, unresolved +//! [`UnresolvedQueryExpr`](asap_types::pre_asap::query_expr::UnresolvedQueryExpr) //! (`QueryExpr`). //! -//! - **L1 (parse)** is delegated to `promql-parser` 0.8. -//! - **L2** is built *directly in canonical shape* here (issue #179): the walk -//! interprets PromQL semantics (range vectors, aggregate operators, label -//! matchers) and emits `L2QueryExpr` nodes with unresolved `ColumnRef`s — -//! the same tree shape [`resolve_root`](asap_types::pre_asap::resolve_root) -//! later binds to canonical, positional `QueryExpr`. The -//! structural decisions a two-layer design would otherwise defer to a -//! separate converter (heavy-hitter `topk` recognition, the -//! `PerEntity`/`Reduce` reduction choice, `without(...)` grouping) are made -//! right here, since a front end building this shape already knows the -//! answer at parse time — see `reduction_for` and `mark_without`. -//! `resolve_root` is left with exactly the schema-*dependent* work: binding -//! every `ColumnRef` to its positional `ColumnId`. +//! - **Parsing** is delegated to `promql-parser` 0.8. +//! - **Lowering** builds *directly in canonical shape* here (issue #179): the +//! walk interprets PromQL semantics (range vectors, aggregate operators, +//! label matchers) and emits `UnresolvedQueryExpr` nodes with unresolved +//! `ColumnRef`s — the same tree shape +//! [`resolve_root`](asap_types::pre_asap::resolve_root) later binds to +//! canonical, positional `QueryExpr`. The structural decisions a +//! separate converter stage would otherwise have to make (heavy-hitter +//! `topk` recognition, the `PerEntity`/`Reduce` reduction choice, +//! `without(...)` grouping) are made right here, since a front end +//! building this shape already knows the answer at parse time — see +//! `reduction_for` and `mark_without`. `resolve_root` is left with exactly +//! the schema-*dependent* work: binding every `ColumnRef` to its +//! positional `ColumnId`. //! -//! # PromQL → canonical L2 mapping (summary) +//! # PromQL → canonical unresolved-tree mapping (summary) //! //! | PromQL | Canonical shape | //! |---|---| @@ -33,15 +34,15 @@ //! | `count_over_time(m[w])` | `Aggregate{[Count], TimeRange{w}}` | //! | `last/first/mad/ts_of_min/ts_of_max/ts_of_first/ts_of_last_over_time(m[w])` | `Aggregate{[Last/First/Mad/TsOf…OverTime], TimeRange{w}}` — per-series range reducers (issue #51) | //! | `sort`/`sort_desc(v)`, `sort_by_label[_desc](v,"l"…)` | `Sort{value \| label…}` (no `Limit`) — row-preserving reorder (issue #51); `min_of`/`max_of` scalar reducers → #89 | -//! | `rate/irate(m[w])` | `Aggregate{[Rate], TimeRange{w}}` — `irate` shares the `rate` *intent*; the avg-vs-last-two-samples difference is an L4 estimation method | +//! | `rate/irate(m[w])` | `Aggregate{[Rate], TimeRange{w}}` — `irate` shares the `rate` *intent*; the avg-vs-last-two-samples difference is a post-ASAP estimation method | //! | `increase(m[w])` | `Aggregate{[Increase], TimeRange{w}}` | //! | `changes`/`delta`/`idelta`/`deriv`/`resets`/`predict_linear`/`double_exponential_smoothing`(`m[w]`, …) | `Aggregate{[Changes/Delta/…], TimeRange{w}}` — per-series counter-derivative intents (issue #44) | -//! | `absent(v)` / `absent_over_time(m[w])` / `present_over_time(m[w])` | `Aggregate{[Absent/AbsentOverTime/PresentOverTime]}` — presence intents; the empty→synthesized-sample logic is L4 (issue #47) | +//! | `absent(v)` / `absent_over_time(m[w])` / `present_over_time(m[w])` | `Aggregate{[Absent/AbsentOverTime/PresentOverTime]}` — presence intents; the empty→synthesized-sample logic is a post-ASAP concern (issue #47) | //! | `abs`/`ceil`/`sqrt`/`ln`/`clamp*`/`round`/trig(`v`), `pi()` | `Aggregate{[Math(f)]}` element-wise transform (issue #45); `pi()` → a `Scalar` leaf | //! | `time()` / `timestamp`/`hour`/`day_of_week`/… (`v`) | `EvalTime` leaf / `Aggregate{[TimeFn(f)]}` (issue #46) | //! | `vector(s)` / `scalar(v)` | `VectorFromScalar` / `ScalarFromVector` — the scalar⇄vector bridges (issue #48) | //! | `label_replace(v,…)` / `label_join(v,…)` | `Relabel{dst, value}` — per-series label rewrite; value unchanged (issue #50) | -//! | `info(v, [selector])` | `InfoJoin{selector}` — label-enrichment join against the info metric(s); join keys resolved at L4 (issue #84) | +//! | `info(v, [selector])` | `InfoJoin{selector}` — label-enrichment join against the info metric(s); join keys resolved during post-ASAP binding (issue #84) | //! | `group` / `offset` / `@` / `info` | **rejected** — distinct semantics with no intent-algebra representation yet (`info` label-join → #84) | //! | `OUTER by (dims) (…)` | `Aggregate.reduction = Reduce(by = dims)` (generic `topk by`/`bottomk` grouping → `Sort.partition_by`) | //! | `count by (d) (…)` | `Aggregate{[Cardinality], …}` | @@ -58,16 +59,16 @@ use std::time::{Duration, SystemTime}; use promql_parser::label::{MatchOp, Matcher}; use promql_parser::parser::{ - self, token, AggregateExpr, AtModifier, BinaryExpr, Call, Expr, LabelModifier, Offset, - VectorMatchCardinality, VectorSelector, + self, token, AggregateExpr, AtModifier as ParserAtModifier, BinaryExpr, Call, Expr, + LabelModifier, Offset, VectorMatchCardinality, VectorSelector, }; use asap_types::pre_asap::agg_intent::{ is_frequency_heavy_hitter, AggIntent, MathFunc, RankingMeasure, TimeFunc, }; use asap_types::pre_asap::query_expr::{ - AtModifier as L3AtModifier, BinaryOpKind, GroupKeys, GroupSide, L2QueryExpr as L2, Predicate, - Reduction, SortKey, Source, TimeShift, VectorGrouping, VectorMatch, VectorMatchKind, + AtModifier, BinaryOpKind, GroupKeys, GroupSide, Predicate, Reduction, SortKey, Source, + TimeShift, UnresolvedQueryExpr as Unresolved, VectorGrouping, VectorMatch, VectorMatchKind, }; use asap_types::pre_asap::{ArithOp, ColumnRef, CompareOp, InfoMatcher, SampleKind, ScalarValue}; use asap_types::types::AccuracyTarget; @@ -76,7 +77,7 @@ use crate::error::PromqlError as LoweringError; type Result = std::result::Result; -/// Parses (L1) and lowers (→ L2 relational) a PromQL query string. +/// Parses and lowers (→ the canonical, unresolved tree) a PromQL query string. pub struct PromqlLowerer; #[derive(Debug, Clone)] @@ -122,7 +123,7 @@ enum InnerFunc { StdDev, Variance, Count, - // `Rate`/`Increase` carry no window of their own — unlike the old L2 + // `Rate`/`Increase` carry no window of their own — unlike the old Unresolved // `AggFunc::Rate{window}`, canonical `AggIntent::Rate`/`Increase` have no // window field either; `windowed_aggregate` reads `Inner.window` // uniformly for every intent, so it would be a redundant duplicate here. @@ -139,7 +140,7 @@ enum InnerFunc { PredictLinear(f64), DoubleExp { smoothing: f64, trend: f64 }, // Additional range-vector reducers (issue #51). Per-series over the window - // (like `*_over_time`); the window rides on the enclosing L2 `Window`. + // (like `*_over_time`); the window rides on the enclosing Unresolved `Window`. LastOverTime, FirstOverTime, MadOverTime, @@ -151,7 +152,7 @@ enum InnerFunc { struct Inner { metric: String, - matchers: Vec, + matchers: Vec, window: Option, func: Option, /// `offset` / `@` on the selector, carried to the `Source` (issue #40). @@ -165,7 +166,7 @@ struct Inner { const MAX_DEPTH: usize = 256; impl PromqlLowerer { - /// Lower `query` to the canonical L2 tree, threading `accuracy` onto every + /// Lower `query` to the canonical Unresolved tree, threading `accuracy` onto every /// approximate intent (`Count`, `Quantile`, `Cardinality`, `TopK`) as it is /// built — this front end constructs the canonical shape directly (issue /// #179), so accuracy is baked in here rather than threaded through a @@ -175,7 +176,7 @@ impl PromqlLowerer { /// `walk` recursion without a parameter on every one of its ~30 mutually /// recursive signatures; consulted only at the handful of sites that build /// an accuracy-bearing `AggIntent`. - pub fn lower(query: &str, accuracy: &AccuracyTarget) -> Result { + pub fn lower(query: &str, accuracy: &AccuracyTarget) -> Result { let _guard = AccuracyGuard::install(accuracy.clone()); let ast = parser::parse(query).map_err(LoweringError::Parse)?; // Reject over-deep nesting up front, so the (mutually-recursive) walk @@ -250,7 +251,7 @@ fn check_depth(expr: &Expr, budget: usize) -> Result<()> { Ok(()) } -fn walk(expr: &Expr) -> Result { +fn walk(expr: &Expr) -> Result { match expr { Expr::Aggregate(agg) => walk_aggregate(agg), Expr::Call(call) if call.func.name.starts_with("histogram_") => walk_histogram(call), @@ -262,7 +263,9 @@ fn walk(expr: &Expr) -> Result { Expr::Call(call) if is_sort_fn(call.func.name) => walk_sort(call), // A bare `min_of`/`max_of(consts…)` scalar query folds to a `Scalar` // leaf; a non-constant argument makes `num_expr` fail → rejected (#89). - Expr::Call(call) if is_scalar_reducer_fn(call.func.name) => Ok(L2::Scalar(num_expr(expr)?)), + Expr::Call(call) if is_scalar_reducer_fn(call.func.name) => { + Ok(Unresolved::Scalar(num_expr(expr)?)) + } Expr::Call(call) if call.func.name == "info" => walk_info(call), Expr::Call(call) => walk_call(call), Expr::Binary(bin) => walk_binary(bin), @@ -275,15 +278,15 @@ fn walk(expr: &Expr) -> Result { // else is a vector, sign-flipped by a `Mul` against `Scalar(-1)`. `Mul` // is commutative, so operand order carries no hazard (#36). Expr::Unary(u) => match num_expr(&u.expr) { - Ok(v) => Ok(L2::Scalar(-v)), - Err(_) => Ok(L2::BinaryOp { + Ok(v) => Ok(Unresolved::Scalar(-v)), + Err(_) => Ok(Unresolved::BinaryOp { op: BinaryOpKind::Arith(ArithOp::Mul), lhs: Box::new(walk(&u.expr)?), - rhs: Box::new(L2::Scalar(-1.0)), + rhs: Box::new(Unresolved::Scalar(-1.0)), vector_match: None, }), }, - Expr::Subquery(sq) => Ok(L2::Subquery { + Expr::Subquery(sq) => Ok(Unresolved::Subquery { range: sq.range, resolution: sq.step, child: Box::new(walk(&sq.expr)?), @@ -294,7 +297,7 @@ fn walk(expr: &Expr) -> Result { } Expr::MatrixSelector(ms) => { let (metric, matchers, shift) = vs_parts(&ms.vs)?; - Ok(L2::TimeRange { + Ok(Unresolved::TimeRange { range: ms.range, child: Box::new(filtered_source(metric, matchers, shift)), }) @@ -302,7 +305,7 @@ fn walk(expr: &Expr) -> Result { // A number literal is a scalar leaf (`v > 5`, or a bare scalar query // `5`). String literals only appear as function args (`label_replace`, // …), which are not supported, so reject them (issue #35). - Expr::NumberLiteral(n) => Ok(L2::Scalar(n.val)), + Expr::NumberLiteral(n) => Ok(Unresolved::Scalar(n.val)), Expr::StringLiteral(_) => Err(LoweringError::UnsupportedFeature( "bare string literal".into(), )), @@ -320,9 +323,9 @@ fn walk(expr: &Expr) -> Result { /// `PromQLSubquery`, not a matrix selector, so the flat template's /// `extract_matrix` can't accept it. Lower the sub-query recursively and reduce /// it per series (issue #27). -fn walk_call(call: &Call) -> Result { - if let Some(l2) = range_fn_over_subquery(call)? { - return Ok(l2); +fn walk_call(call: &Call) -> Result { + if let Some(tree) = range_fn_over_subquery(call)? { + return Ok(tree); } build(lower_inner_call(call)?, vec![], Outer::None) } @@ -334,11 +337,11 @@ fn walk_call(call: &Call) -> Result { /// (`changes`/`delta`/`idelta`/`deriv`/`resets`/`predict_linear`/ /// `double_exponential_smoothing`). Each lowers to a per-series `Aggregate{[f]}` /// directly over the `PromQLSubquery` — the sub-query is the range context, so -/// there is no separate `Window`/`TimeRange` (the L2→L3 converter treats the -/// `Subquery` as the range marker). Returns `None` when `call` isn't a range +/// there is no separate `Window`/`TimeRange` (this walk treats the `Subquery` +/// node itself as the range marker). Returns `None` when `call` isn't a range /// function or its argument isn't a sub-query, so the flat matrix-selector /// template still handles `f(m[w])` (issues #42, #55). -fn range_fn_over_subquery(call: &Call) -> Result> { +fn range_fn_over_subquery(call: &Call) -> Result> { // `rate`/`increase`/`irate` carry their window in the `AggFunc`; over a // sub-query that window is the sub-query's own range. if let "rate" | "irate" | "increase" = call.func.name { @@ -417,7 +420,7 @@ fn subquery_range(expr: &Expr) -> Option { } } -fn walk_aggregate(agg: &AggregateExpr) -> Result { +fn walk_aggregate(agg: &AggregateExpr) -> Result { let (keys, without) = resolve_group(agg)?; let outer = outer_kind(agg)?; @@ -515,7 +518,7 @@ fn outer_kind(agg: &AggregateExpr) -> Result { }) } -/// Wrap an already-lowered L2 subtree in the outer aggregation. This is the +/// Wrap an already-lowered Unresolved subtree in the outer aggregation. This is the /// general-nesting counterpart to [`build`]: where `build` assembles the /// two-level shape from a flat [`Inner`], this composes the outer operator over /// an arbitrary child (`max(sum by (job) (…))`, `sum(a + b)`, …). @@ -535,20 +538,21 @@ fn outer_kind(agg: &AggregateExpr) -> Result { /// `reduction_for` (used by [`windowed_aggregate`]/[`outer_aggregate`] to /// build this node) decides `PerEntity` vs `Reduce(by)` *without* knowing /// about `without` yet — it only ever sees `by`-mode keys, since `without`'s -/// excluded-labels list is applied here, after the fact, exactly like the old -/// L2 `relational` tree's `mark_without` did (the L2→L3 converter read -/// `without` only after this front-end step had already set it). Whether +/// excluded-labels list is applied here, after the fact, exactly like the +/// pre-#179 legacy `relational::QueryExpr` tree's own `mark_without` did (its +/// converter read `without` only after this front-end step had already set +/// it). Whether /// `reduction_for` picked `PerEntity` (only possible when `keys` was empty) /// or `Reduce(by)`, the correct answer under `without(...)` is always /// `Reduce(without(keys))`: a `without` grouping is never label-preserving — /// per-entity requires `!by.is_without()` — so this both re-tags an existing /// `Reduce` and upgrades a wrongly-early `PerEntity` guess, uniformly. -fn mark_without(l2: L2, without: bool) -> L2 { +fn mark_without(tree: Unresolved, without: bool) -> Unresolved { if !without { - return l2; + return tree; } - match l2 { - L2::Aggregate { + match tree { + Unresolved::Aggregate { reduction, measures, output_names, @@ -559,7 +563,7 @@ fn mark_without(l2: L2, without: bool) -> L2 { Reduction::Reduce(by) => by.keys().to_vec(), Reduction::PerEntity => vec![], }; - L2::Aggregate { + Unresolved::Aggregate { reduction: Reduction::Reduce(GroupKeys::without(keys)), measures, output_names, @@ -571,7 +575,7 @@ fn mark_without(l2: L2, without: bool) -> L2 { } } -fn build_over_subtree(outer: Outer, keys: Vec, child: L2) -> Result { +fn build_over_subtree(outer: Outer, keys: Vec, child: Unresolved) -> Result { Ok(match outer { // `walk_aggregate` always passes a real aggregator; `None` can't occur. Outer::None => child, @@ -587,22 +591,22 @@ fn build_over_subtree(outer: Outer, keys: Vec, child: L2) -> Result { outer_aggregate(keys, AggIntent::CountValues { label }, child) } - Outer::Sample { kind } => L2::Sample { + Outer::Sample { kind } => Unresolved::Sample { by: keys.into(), kind, child: Box::new(child), }, Outer::TopK { k, descending } => { - let sorted = L2::Sort { + let sorted = Unresolved::Sort { keys: vec![SortKey { - expr: L2::Column(ColumnRef::SampleValue), + expr: Unresolved::Column(ColumnRef::SampleValue), ascending: !descending, nulls_first: false, }], partition_by: keys.into(), child: Box::new(child), }; - L2::Limit { + Unresolved::Limit { n: k as usize, offset: 0, child: Box::new(sorted), @@ -628,7 +632,7 @@ fn build_over_subtree(outer: Outer, keys: Vec, child: L2) -> Result Result { +fn walk_histogram(call: &Call) -> Result { if call.func.name == "histogram_quantiles" { return walk_histogram_quantiles(call); } @@ -692,7 +696,7 @@ fn walk_histogram(call: &Call) -> Result { /// would make the merged schema silently misdescribe every branch but one. The /// quantile is carried by the `label` column, which is exactly where Prometheus /// puts it. -fn walk_histogram_quantiles(call: &Call) -> Result { +fn walk_histogram_quantiles(call: &Call) -> Result { let vec_expr = arg(call, 0)?; let label = str_arg(call, 1)?; if call.args.args.len() < 3 { @@ -716,7 +720,7 @@ fn walk_histogram_quantiles(call: &Call) -> Result { }; let child = walk(vec_expr)?; let reduction = reduction_for(&[], &intent, &child); - let quantile = L2::Aggregate { + let quantile = Unresolved::Aggregate { reduction, measures: vec![intent], // Each branch aliases its value column to "value" (not the @@ -726,14 +730,16 @@ fn walk_histogram_quantiles(call: &Call) -> Result { having: None, child: Box::new(child), }; - Ok(L2::Relabel { + Ok(Unresolved::Relabel { dst: label.clone(), - value: Box::new(L2::Literal(ScalarValue::Utf8(open_metrics_float(phi)))), + value: Box::new(Unresolved::Literal(ScalarValue::Utf8(open_metrics_float( + phi, + )))), child: Box::new(quantile), }) }) .collect::>>()?; - Ok(L2::Merge { children: branches }) + Ok(Unresolved::Merge { children: branches }) } /// Prometheus's `labels.FormatOpenMetricsFloat` — how `histogram_quantiles` @@ -796,9 +802,9 @@ fn is_time_fn(name: &str) -> bool { /// `time()` → the `EvalTime` leaf. `timestamp(v)` and the calendar accessors → /// `Aggregate{[TimeFn(f)]}` over the argument vector, or over `EvalTime` for the /// no-argument calendar forms (`hour()`, `day_of_week()`, …). Issue #46. -fn walk_time(call: &Call) -> Result { +fn walk_time(call: &Call) -> Result { if call.func.name == "time" { - return Ok(L2::EvalTime); + return Ok(Unresolved::EvalTime); } let func = match call.func.name { "timestamp" => TimeFunc::Timestamp, @@ -815,7 +821,7 @@ fn walk_time(call: &Call) -> Result { // A calendar function with no argument reads the evaluation time; otherwise // it maps over each sample's timestamp in the argument vector. let inner = if call.args.args.is_empty() { - L2::EvalTime + Unresolved::EvalTime } else { walk(arg(call, 0)?)? }; @@ -829,9 +835,9 @@ fn is_presence_fn(name: &str) -> bool { /// `absent(v)` / `absent_over_time(m[w])` / `present_over_time(m[w])` — lowered /// to an `Aggregate{[Absent/…]}` over the (instant or range) argument. The -/// empty-result → synthesized-1-sample logic is an L4/runtime concern; L3 only -/// marks the operation (issue #47). -fn walk_presence(call: &Call) -> Result { +/// empty-result → synthesized-1-sample logic is a post-ASAP/runtime concern; +/// the canonical tree only marks the operation (issue #47). +fn walk_presence(call: &Call) -> Result { let func = match call.func.name { "absent" => AggIntent::Absent, "absent_over_time" => AggIntent::AbsentOverTime, @@ -853,12 +859,12 @@ fn is_typeconv_fn(name: &str) -> bool { /// `vector(s)` — promote a scalar to a label-less instant vector. `scalar(v)` /// — collapse a single-element vector to its value. Both are honest bridge /// nodes in the IR; the "exactly one element → NaN otherwise" runtime rule of -/// `scalar` is an L4/runtime concern (issue #48). -fn walk_typeconv(call: &Call) -> Result { +/// `scalar` is a post-ASAP/runtime concern (issue #48). +fn walk_typeconv(call: &Call) -> Result { let inner = walk(arg(call, 0)?)?; Ok(match call.func.name { - "vector" => L2::VectorFromScalar(Box::new(inner)), - "scalar" => L2::ScalarFromVector(Box::new(inner)), + "vector" => Unresolved::VectorFromScalar(Box::new(inner)), + "scalar" => Unresolved::ScalarFromVector(Box::new(inner)), other => return Err(LoweringError::UnsupportedFunction(other.to_string())), }) } @@ -875,7 +881,7 @@ fn is_sort_fn(name: &str) -> bool { /// `sort_by_label`/`sort_by_label_desc(v, "l"…)` reorder by label values. All /// lower to a bare `Sort` (no `Limit`) over the vector argument — a faithful, /// row-preserving reordering (issue #51). -fn walk_sort(call: &Call) -> Result { +fn walk_sort(call: &Call) -> Result { let child = Box::new(walk(arg(call, 0)?)?); let (by_value, ascending) = match call.func.name { "sort" => (true, true), @@ -890,7 +896,7 @@ fn walk_sort(call: &Call) -> Result { nulls_first: false, }; let keys = if by_value { - vec![sort_key(L2::Column(ColumnRef::SampleValue))] + vec![sort_key(Unresolved::Column(ColumnRef::SampleValue))] } else { // `sort_by_label(v, "l1", "l2", …)` — one key per label arg, in order. if call.args.args.len() < 2 { @@ -899,10 +905,14 @@ fn walk_sort(call: &Call) -> Result { )); } (1..call.args.args.len()) - .map(|i| Ok(sort_key(L2::Column(ColumnRef::Named(str_arg(call, i)?))))) + .map(|i| { + Ok(sort_key(Unresolved::Column(ColumnRef::Named(str_arg( + call, i, + )?)))) + }) .collect::>>()? }; - Ok(L2::Sort { + Ok(Unresolved::Sort { keys, partition_by: GroupKeys::none(), child, @@ -912,14 +922,14 @@ fn walk_sort(call: &Call) -> Result { /// `info(v, [selector])` — a label-enrichment join. Lowers the input vector and /// wraps it in an `InfoJoin` carrying the (optional) data-label selector's /// matchers; the actual join against the info metric — on shared identifying -/// labels — is resolved at L4 (issue #84). -fn walk_info(call: &Call) -> Result { +/// labels — is resolved during post-ASAP binding (issue #84). +fn walk_info(call: &Call) -> Result { let child = Box::new(walk(arg(call, 0)?)?); let selector = match call.args.args.get(1) { Some(sel) => info_selector(sel)?, None => Vec::new(), // default: enrich from `target_info` }; - Ok(L2::InfoJoin { selector, child }) + Ok(Unresolved::InfoJoin { selector, child }) } /// Extract the `info` data-label selector's matchers. Unlike an ordinary @@ -962,8 +972,8 @@ fn is_label_fn(name: &str) -> bool { /// expression that computes the destination label: `label_replace` a regex /// capture-expansion, `label_join` a separator-joined concatenation. Sample /// values are untouched; the regex-match-or-passthrough and capture-expansion -/// are L4/runtime concerns (issue #50). -fn walk_label(call: &Call) -> Result { +/// are post-ASAP/runtime concerns (issue #50). +fn walk_label(call: &Call) -> Result { let child = Box::new(walk(arg(call, 0)?)?); match call.func.name { "label_replace" => { @@ -971,15 +981,15 @@ fn walk_label(call: &Call) -> Result { let replacement = str_arg(call, 2)?; let src = str_arg(call, 3)?; let regex = str_arg(call, 4)?; - let value = L2::FunctionCall { + let value = Unresolved::FunctionCall { name: "label_replace".into(), args: vec![ - L2::Column(ColumnRef::Named(src)), - L2::Literal(ScalarValue::Utf8(regex)), - L2::Literal(ScalarValue::Utf8(replacement)), + Unresolved::Column(ColumnRef::Named(src)), + Unresolved::Literal(ScalarValue::Utf8(regex)), + Unresolved::Literal(ScalarValue::Utf8(replacement)), ], }; - Ok(L2::Relabel { + Ok(Unresolved::Relabel { dst, value: Box::new(value), child, @@ -994,15 +1004,15 @@ fn walk_label(call: &Call) -> Result { } let dst = str_arg(call, 1)?; let sep = str_arg(call, 2)?; - let mut args = vec![L2::Literal(ScalarValue::Utf8(sep))]; + let mut args = vec![Unresolved::Literal(ScalarValue::Utf8(sep))]; for i in 3..call.args.args.len() { - args.push(L2::Column(ColumnRef::Named(str_arg(call, i)?))); + args.push(Unresolved::Column(ColumnRef::Named(str_arg(call, i)?))); } - let value = L2::FunctionCall { + let value = Unresolved::FunctionCall { name: "label_join".into(), args, }; - Ok(L2::Relabel { + Ok(Unresolved::Relabel { dst, value: Box::new(value), child, @@ -1050,9 +1060,9 @@ fn is_math_fn(name: &str) -> bool { /// A math / trig function — a per-series element-wise value transform, lowered /// to a per-series `Aggregate{[Math(f)]}` over the (instant) argument vector. /// `pi()` is the constant π, lowered to a `Scalar` leaf (issue #45). -fn walk_math(call: &Call) -> Result { +fn walk_math(call: &Call) -> Result { if call.func.name == "pi" { - return Ok(L2::Scalar(std::f64::consts::PI)); + return Ok(Unresolved::Scalar(std::f64::consts::PI)); } let func = match call.func.name { "abs" => MathFunc::Abs, @@ -1203,7 +1213,7 @@ fn selector_is_bucket(vs: &VectorSelector) -> bool { || vs.matchers.matchers.iter().any(|m| m.name == "le") } -fn walk_binary(bin: &BinaryExpr) -> Result { +fn walk_binary(bin: &BinaryExpr) -> Result { let lhs = scalar_or_vector(&bin.lhs)?; let rhs = scalar_or_vector(&bin.rhs)?; let op = binop(bin.op.id())?; @@ -1237,7 +1247,7 @@ fn walk_binary(bin: &BinaryExpr) -> Result { grouping, } }); - Ok(L2::BinaryOp { + Ok(Unresolved::BinaryOp { op, lhs: Box::new(lhs), rhs: Box::new(rhs), @@ -1374,7 +1384,7 @@ fn lower_inner_call(call: &Call) -> Result { /// Assemble the Layer-2 tree from a lowered inner vector, the resolved group /// keys, and the enclosing aggregator shape. -fn build(inner: Inner, keys: Vec, outer: Outer) -> Result { +fn build(inner: Inner, keys: Vec, outer: Outer) -> Result { match outer { Outer::None => match &inner.func { None => Ok(filtered_source(inner.metric, inner.matchers, inner.shift)), @@ -1421,7 +1431,7 @@ fn build(inner: Inner, keys: Vec, outer: Outer) -> Result { Some(intent) => windowed_aggregate(inner, vec![], intent), None => filtered_source(inner.metric, inner.matchers, inner.shift), }; - Ok(L2::Sample { + Ok(Unresolved::Sample { by: keys.into(), kind, child: Box::new(base), @@ -1433,20 +1443,21 @@ fn build(inner: Inner, keys: Vec, outer: Outer) -> Result { // over avg/quantile/rate, a bare selector's raw value, all bottomk) // is a generic order-by-value + limit and stays as the `Sort + Limit` // operator pair. The descending-plus-measure rule is shared with the - // L3 canonicalize promotion so the two cannot drift (issue #38). + // canonicalize-pass promotion so the two cannot drift (issue #38). let measure = match inner.func { Some(InnerFunc::Count) => RankingMeasure::Frequency, _ => RankingMeasure::NonAdditive, }; let heavy_hitter = is_frequency_heavy_hitter(descending, measure); if heavy_hitter { - // Preserve the Count intent in L3 so the intent algebra is - // explicit about what is being computed. L4 may fuse the Count - // and TopK into a single-pass heavy-hitter sketch (SpaceSaving / - // CMS-with-heap), but that is a cost-model decision, not an L3 - // concern. + // Preserve the Count intent in the canonical tree so the + // intent algebra is explicit about what is being computed. + // Post-ASAP binding may fuse the Count and TopK into a + // single-pass heavy-hitter sketch (SpaceSaving / + // CMS-with-heap), but that is a cost-model decision, not a + // canonical-IR concern. let count_agg = windowed_aggregate(inner, vec![], inner_intent(&InnerFunc::Count)); - Ok(L2::Aggregate { + Ok(Unresolved::Aggregate { // A ranking always reduces (a `by`-empty TopK ranks the // whole input into one ordering, never per-entity). reduction: Reduction::Reduce(keys.into()), @@ -1468,22 +1479,22 @@ fn build(inner: Inner, keys: Vec, outer: Outer) -> Result { // (PromQL `topk` ranks the raw samples, it does not sum them) and // destructive — the cross-series `Sum` collapses every label, // including the `by (…)` partition keys, so they no longer - // resolve at L3 (issue #30). Keep the selector label-preserving so + // resolve (issue #30). Keep the selector label-preserving so // `Sort.partition_by` can rank within each group (issue #12). let base = match inner.func.as_ref().map(inner_intent) { Some(intent) => windowed_aggregate(inner, vec![], intent), None => filtered_source(inner.metric, inner.matchers, inner.shift), }; - let sorted = L2::Sort { + let sorted = Unresolved::Sort { keys: vec![SortKey { - expr: L2::Column(ColumnRef::SampleValue), + expr: Unresolved::Column(ColumnRef::SampleValue), ascending: !descending, nulls_first: false, }], partition_by: keys.into(), child: Box::new(base), }; - Ok(L2::Limit { + Ok(Unresolved::Limit { n: k as usize, offset: 0, child: Box::new(sorted), @@ -1503,9 +1514,12 @@ fn build(inner: Inner, keys: Vec, outer: Outer) -> Result { fn reduction_for( keys: &[ColumnRef], intent: &AggIntent, - child: &L2, + child: &Unresolved, ) -> Reduction { - let is_range_child = matches!(child, L2::TimeRange { .. } | L2::Subquery { .. }); + let is_range_child = matches!( + child, + Unresolved::TimeRange { .. } | Unresolved::Subquery { .. } + ); if keys.is_empty() && (intent.is_per_series() || is_range_child) { Reduction::PerEntity } else { @@ -1517,20 +1531,24 @@ fn reduction_for( /// in `TimeRange` when there's a window — including for `Rate`/`Increase`, /// whose window rides on `inner.window` too (set redundantly alongside the /// intent itself): canonical `AggIntent::Rate`/`Increase` carry no window -/// field of their own, unlike the old L2 `AggFunc::Rate{window}` — "the range +/// field of their own, unlike the old Unresolved `AggFunc::Rate{window}` — "the range /// is on the enclosing `TimeRange` node" is now true unconditionally, so /// there's no more `skip_window` special case. -fn windowed_aggregate(inner: Inner, keys: Vec, intent: AggIntent) -> L2 { +fn windowed_aggregate( + inner: Inner, + keys: Vec, + intent: AggIntent, +) -> Unresolved { let base = filtered_source(inner.metric, inner.matchers, inner.shift); let child = match inner.window { - Some(w) => L2::TimeRange { + Some(w) => Unresolved::TimeRange { range: w, child: Box::new(base), }, None => base, }; let reduction = reduction_for(&keys, &intent, &child); - L2::Aggregate { + Unresolved::Aggregate { reduction, measures: vec![intent], // A single empty entry — never an override — so the resolver keeps @@ -1542,12 +1560,16 @@ fn windowed_aggregate(inner: Inner, keys: Vec, intent: AggIntent, intent: AggIntent, child: L2) -> L2 { +fn outer_aggregate( + keys: Vec, + intent: AggIntent, + child: Unresolved, +) -> Unresolved { let reduction = reduction_for(&keys, &intent, &child); - L2::Aggregate { + Unresolved::Aggregate { reduction, measures: vec![intent], output_names: vec![String::new()], @@ -1556,8 +1578,8 @@ fn outer_aggregate(keys: Vec, intent: AggIntent, child: L2 } } -fn filtered_source(metric: String, matchers: Vec, shift: TimeShift) -> L2 { - let scan = L2::Scan { +fn filtered_source(metric: String, matchers: Vec, shift: TimeShift) -> Unresolved { + let scan = Unresolved::Scan { source: Source::TimeSeries { metric }, predicates: matchers .into_iter() @@ -1569,7 +1591,7 @@ fn filtered_source(metric: String, matchers: Vec, shift: TimeShift) -> L2 { if shift.is_identity() { scan } else { - L2::TimeShift { + Unresolved::TimeShift { shift, child: Box::new(scan), } @@ -1705,12 +1727,12 @@ fn resolve_group(agg: &AggregateExpr) -> Result<(Vec, bool)> { // ── Free helpers ────────────────────────────────────────────────────────────── -fn vs_parts(vs: &VectorSelector) -> Result<(String, Vec, TimeShift)> { +fn vs_parts(vs: &VectorSelector) -> Result<(String, Vec, TimeShift)> { // A non-equality `__name__` matcher (`=~` / `!~` / `!=`) selects *across* - // metric names. The L3 `Source::TimeSeries { metric }` carries a single - // concrete metric name, so there is no representation for a regex/negated - // name match — reject rather than mislower it to a literal metric named - // after the pattern (issue #67). An equality `__name__` (`{__name__="up"}`) + // metric names. `Source::TimeSeries { metric }` carries a single concrete + // metric name, so there is no representation for a regex/negated name + // match — reject rather than mislower it to a literal metric named after + // the pattern (issue #67). An equality `__name__` (`{__name__="up"}`) // still names the metric below. if let Some(m) = vs .matchers @@ -1720,7 +1742,7 @@ fn vs_parts(vs: &VectorSelector) -> Result<(String, Vec, TimeShift)> { { return Err(LoweringError::UnsupportedFeature(format!( "non-equality `__name__` matcher ({}{:?}) selects across metric names, \ - which has no single-metric L3 representation", + which has no single-metric canonical representation", m.name, m.op ))); } @@ -1742,7 +1764,7 @@ fn vs_parts(vs: &VectorSelector) -> Result<(String, Vec, TimeShift)> { .filter(|m| m.name != "__name__") .collect(); ms.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.value.cmp(&b.value))); - let matchers = ms.into_iter().map(matcher_to_l3expr).collect(); + let matchers = ms.into_iter().map(matcher_to_compare).collect(); let shift = time_shift(vs.offset.as_ref(), vs.at.as_ref())?; Ok((metric, matchers, shift)) } @@ -1750,7 +1772,7 @@ fn vs_parts(vs: &VectorSelector) -> Result<(String, Vec, TimeShift)> { /// Convert the parser's `offset` / `@` modifiers into a [`TimeShift`] (issue /// #40). Offset is signed milliseconds; `@ ` (parser seconds → ms) becomes /// an absolute anchor, `@ start()`/`@ end()` the range bounds. -fn time_shift(offset: Option<&Offset>, at: Option<&AtModifier>) -> Result { +fn time_shift(offset: Option<&Offset>, at: Option<&ParserAtModifier>) -> Result { let offset_ms = match offset { None => 0, Some(Offset::Pos(d)) => duration_ms(*d)?, @@ -1758,9 +1780,9 @@ fn time_shift(offset: Option<&Offset>, at: Option<&AtModifier>) -> Result None, - Some(AtModifier::Start) => Some(L3AtModifier::Start), - Some(AtModifier::End) => Some(L3AtModifier::End), - Some(AtModifier::At(t)) => Some(L3AtModifier::Timestamp(system_time_ms(*t)?)), + Some(ParserAtModifier::Start) => Some(AtModifier::Start), + Some(ParserAtModifier::End) => Some(AtModifier::End), + Some(ParserAtModifier::At(t)) => Some(AtModifier::Timestamp(system_time_ms(*t)?)), }; Ok(TimeShift { offset_ms, at }) } @@ -1785,21 +1807,21 @@ fn system_time_ms(t: SystemTime) -> Result { }) } -fn matcher_to_l3expr(m: &Matcher) -> L2 { +fn matcher_to_compare(m: &Matcher) -> Unresolved { let op = match &m.op { MatchOp::Equal => CompareOp::Eq, MatchOp::NotEqual => CompareOp::Ne, MatchOp::Re(_) => CompareOp::Regex, MatchOp::NotRe(_) => CompareOp::NotRegex, }; - L2::Compare { - left: Box::new(L2::Column(ColumnRef::Named(m.name.clone()))), + Unresolved::Compare { + left: Box::new(Unresolved::Column(ColumnRef::Named(m.name.clone()))), op, - right: Box::new(L2::Literal(ScalarValue::Utf8(m.value.clone()))), + right: Box::new(Unresolved::Literal(ScalarValue::Utf8(m.value.clone()))), } } -fn extract_matrix(expr: &Expr) -> Result<(String, Vec, Duration, TimeShift)> { +fn extract_matrix(expr: &Expr) -> Result<(String, Vec, Duration, TimeShift)> { match expr { Expr::MatrixSelector(ms) => { let (metric, matchers, shift) = vs_parts(&ms.vs)?; @@ -1902,9 +1924,9 @@ fn is_scalar_reducer_fn(name: &str) -> bool { /// A `BinaryOp` operand: fold a pure-scalar expression (`5`, `10*1024*1024`) to /// a `Scalar` leaf, otherwise walk it as a vector (issue #35). -fn scalar_or_vector(expr: &Expr) -> Result { +fn scalar_or_vector(expr: &Expr) -> Result { match num_expr(expr) { - Ok(v) => Ok(L2::Scalar(v)), + Ok(v) => Ok(Unresolved::Scalar(v)), Err(_) => walk(expr), } } diff --git a/crates/frontend-promql/tests/observability/awesome_prometheus_alerts.rs b/crates/frontend-promql/tests/observability/awesome_prometheus_alerts.rs index 95719d94..3cd933ec 100644 --- a/crates/frontend-promql/tests/observability/awesome_prometheus_alerts.rs +++ b/crates/frontend-promql/tests/observability/awesome_prometheus_alerts.rs @@ -5,7 +5,7 @@ //! host/hardware, node-exporter, databases, message brokers, Kubernetes, and //! more (`tests/data/awesome_prometheus_alerts.txt`). //! -//! We *lower* (parse → L2 → L3), we do not execute. Two guarantees: +//! We *lower* (parse → the canonical tree), we do not execute. Two guarantees: //! 1. **Totality** — every real-world query returns `Ok` or a clean //! `LoweringError` and never panics. //! 2. **Parseability** — none of them fail at the *parse* stage; the private @@ -18,8 +18,8 @@ //! //! NOTE: the headline finding is that real alerting PromQL is overwhelmingly //! ` ` (e.g. `… > 0`, `… != 1`): ~822/949 queries are -//! rejected *only* because a bare numeric threshold operand has no L2 scalar -//! node yet (see `scalar_threshold_*__GAP`). The query bodies underneath +//! rejected *only* because a bare numeric threshold operand has no canonical +//! scalar-comparison node yet (see `scalar_threshold_*__GAP`). The query bodies underneath //! (`rate`, `*_over_time`, `histogram_quantile`, `sum by (…)`) lower fine on //! their own — see the `*_core_lowers` tests. @@ -165,7 +165,7 @@ fn corpus_lowering_is_total_and_fully_parseable() { } // ───────────────────────────────────────────────────────────────────────────── -// Patterns we lower (verbatim corpus queries) — pin the L3 shape +// Patterns we lower (verbatim corpus queries) — pin the canonical shape // ───────────────────────────────────────────────────────────────────────────── #[test] @@ -282,7 +282,8 @@ fn scalar_threshold_comparisons_lower_to_binaryop_scalar() { #[test] fn absent_function_lowers_to_absent_intent() { // `absent(up{job="prometheus"})` — "job missing" alerts. Lowers to the - // `Absent` intent (issue #47); the empty→synthesized-sample logic is L4. + // `Absent` intent (issue #47); the empty→synthesized-sample logic is a + // post-ASAP concern. let qe = ok(r#"absent(up{job="prometheus"})"#); assert!(intents(&qe).iter().any(|i| matches!(i, AggIntent::Absent))); } diff --git a/crates/frontend-promql/tests/observability/o11y_bench_promql.rs b/crates/frontend-promql/tests/observability/o11y_bench_promql.rs index 3fb6d43d..9895d0c4 100644 --- a/crates/frontend-promql/tests/observability/o11y_bench_promql.rs +++ b/crates/frontend-promql/tests/observability/o11y_bench_promql.rs @@ -12,7 +12,7 @@ //! verbatim into a differently-licensed test suite is fine as-is is an open //! question — tracked in issue #135, not resolved by this file's existence. //! -//! We *lower* (parse → L2 → L3), we do not execute. Totality: every query +//! We *lower* (parse → the canonical tree), we do not execute. Totality: every query //! returns `Ok` or a clean `LoweringError` and never panics. Given the corpus //! is small and hand-picked from realistic incident-response queries, we also //! assert full lowering coverage — a regression here means a real pattern diff --git a/crates/frontend-promql/tests/observability/promql_corpus.rs b/crates/frontend-promql/tests/observability/promql_corpus.rs index 69687fb9..526c02bf 100644 --- a/crates/frontend-promql/tests/observability/promql_corpus.rs +++ b/crates/frontend-promql/tests/observability/promql_corpus.rs @@ -55,10 +55,11 @@ fn tally(corpus: &str) -> Tally { t } -/// Every query that lowers, additionally run through the L3→L4 -/// `asap-aware-mapping` binding pass (issue #98), at an approximate -/// accuracy target so the sketch-selection boundary actually fires (an -/// `Exact` target would only ever exercise the exact-accumulator arm). +/// Every query that lowers, additionally run through the pre-ASAP → +/// post-ASAP `asap-aware-mapping` binding pass (issue #98), at an +/// approximate accuracy target so the sketch-selection boundary actually +/// fires (an `Exact` target would only ever exercise the exact-accumulator +/// arm). #[derive(Default, Debug)] struct BindTally { /// Root bound to `SummaryAgg`/`SummaryEstimate` — the pass did something. @@ -72,11 +73,11 @@ struct BindTally { fn bind_tally(corpus: &str, accuracy: AccuracyTarget) -> BindTally { let mut t = BindTally::default(); for q in queries(corpus) { - let Ok(l3) = lower_promql(q, accuracy.clone()) else { + let Ok(tree) = lower_promql(q, accuracy.clone()) else { continue; }; - match implement_tree(&l3) { - Ok(l4) if matches!(l4.expr, SummaryExpr::Logical(_)) => t.unchanged += 1, + match implement_tree(&tree) { + Ok(bound) if matches!(bound.expr, SummaryExpr::Logical(_)) => t.unchanged += 1, Ok(_) => t.transformed += 1, Err(_) => t.errored += 1, } @@ -89,13 +90,13 @@ fn binding_is_total_over_the_entire_corpus() { let accuracy = AccuracyTarget::Epsilon(0.01); let docs = bind_tally(DOCS, accuracy.clone()); let td = bind_tally(TESTDATA, accuracy); - eprintln!("docs corpus L4 binding: {docs:?}"); - eprintln!("testdata corpus L4 binding: {td:?}"); + eprintln!("docs corpus post-ASAP binding: {docs:?}"); + eprintln!("testdata corpus post-ASAP binding: {td:?}"); // Same totality guarantee as lowering: reaching here means `implement_tree` // never panicked over any lowerable query in the corpus. - assert_eq!(docs.errored, 0, "L3->L4 binding errored: {docs:?}"); - assert_eq!(td.errored, 0, "L3->L4 binding errored: {td:?}"); + assert_eq!(docs.errored, 0, "post-ASAP binding errored: {docs:?}"); + assert_eq!(td.errored, 0, "post-ASAP binding errored: {td:?}"); } #[test] diff --git a/crates/frontend-promql/tests/promql_conformance.rs b/crates/frontend-promql/tests/promql_conformance.rs index 4814e07e..574c9930 100644 --- a/crates/frontend-promql/tests/promql_conformance.rs +++ b/crates/frontend-promql/tests/promql_conformance.rs @@ -1,8 +1,8 @@ -//! PromQL **semantic conformance** for the L1→L3 lowering. +//! PromQL **semantic conformance** for the parse-to-canonical-tree lowering. //! //! We *lower* PromQL to the intent algebra; we do not *execute* it. So "same //! semantic job as Prometheus" here means: for each canonical query, does the -//! L3 tree encode the **documented PromQL meaning** — and where we knowingly +//! canonical tree encode the **documented PromQL meaning** — and where we knowingly //! diverge (reject, approximate, or drop a modifier), is that pinned by a test //! so it stays visible? //! @@ -19,7 +19,7 @@ //! subquery.test, at_modifier.test, literals.test, limit.test //! //! Legend used in test names: -//! - (no suffix) — we lower it and the L3 intent matches PromQL. +//! - (no suffix) — we lower it and the canonical intent matches PromQL. //! - `__GAP` — a PromQL capability we don't *yet* support. It is **cleanly //! rejected** (never silently mislowered), and pinned here so adding support //! later flips the assertion deliberately. @@ -227,7 +227,7 @@ fn name_regex_matcher_is_rejected__GAP() { #[test] fn range_vector_selector_is_time_range() { // SEMANTICS: `[5m]` turns an instant vector into a range vector, - // represented in L3 as a dedicated `TimeRange` node. + // represented in the canonical tree as a dedicated `TimeRange` node. let qe = ok("node_cpu_seconds_total[5m]"); let QueryExpr::TimeRange { range, .. } = &qe else { panic!("expected TimeRange for a range-vector selector, got {qe:?}"); @@ -535,7 +535,8 @@ fn histogram_quantile_over_rate() { #[test] fn histogram_quantile_over_sum_by_le_preserves_le_grouping() { // SEMANTICS: the standard pattern — bucket rates summed by `le`, then the - // quantile. The `sum by (le)` aggregation must survive into L3. + // quantile. The `sum by (le)` aggregation must survive into the + // canonical tree. let qe = ok( "histogram_quantile(0.99, sum by(le) (rate(demo_api_request_duration_seconds_bucket[5m])))", ); @@ -715,8 +716,8 @@ fn count_maps_to_cardinality_and_inherits_accuracy() { // SEMANTICS (review #2): PromQL `count by (...)` counts distinct series → the // `Cardinality` intent. The workload's AccuracyTarget threads onto it: // `Exact` stays exact (no silent HLL substitution); an approximate target is - // carried through for L4 to honor. This pins the intentional count→Cardinality - // mapping and its accuracy gating. + // carried through for post-ASAP binding to honor. This pins the + // intentional count→Cardinality mapping and its accuracy gating. let exact = lower_promql("count by (job) (up)", AccuracyTarget::Exact).unwrap(); assert!( has(&exact, |i| matches!( @@ -845,7 +846,8 @@ fn topk_over_nested_aggregate_is_generic_sort_limit() { fn outer_aggregate_over_nested_aggregate_nests() { // `max(sum by (job) (rate(m[5m])))` — an outer cross-series reduction over a // nested per-group reduction over a per-series rate: three stacked levels the - // flat two-level template rejected. Each level survives into L3 (issue #27). + // flat two-level template rejected. Each level survives into the + // canonical tree (issue #27). let qe = ok("max(sum by (job) (rate(http_requests_total[5m])))"); let QueryExpr::Aggregate { measures, child, .. diff --git a/crates/frontend-promql/tests/promql_equivalence.rs b/crates/frontend-promql/tests/promql_equivalence.rs index 0e4a5f11..3d03fc64 100644 --- a/crates/frontend-promql/tests/promql_equivalence.rs +++ b/crates/frontend-promql/tests/promql_equivalence.rs @@ -1,10 +1,10 @@ -//! PromQL **semantic-equivalence proving** for the L1→L3 lowering. +//! PromQL **semantic-equivalence proving** for the parse-to-canonical-tree lowering. //! //! The lowering is a *normalizer*: it should map a whole class of -//! semantically-equivalent PromQL strings to **one** canonical L3 tree, and +//! semantically-equivalent PromQL strings to **one** canonical tree, and //! must keep semantically-*distinct* queries distinct. This suite proves: //! -//! 1. Equivalence classes collapse to identical L3 (`assert_equiv`). +//! 1. Equivalence classes collapse to an identical canonical tree (`assert_equiv`). //! 2. Distinct meanings stay distinct (`assert_distinct`). //! 3. The lowering never *wrongly* equates distinct semantics — the cases it //! cannot faithfully distinguish are **rejected**, not silently merged. @@ -25,30 +25,30 @@ fn lo(q: &str) -> QueryExpr { lower_promql(q, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("{q:?} should lower: {e}")) } -/// Every member of an equivalence class must lower to the *same* L3 tree. +/// Every member of an equivalence class must lower to the *same* canonical tree. fn assert_equiv(class: &[&str]) { let first = lo(class[0]); for q in &class[1..] { assert_eq!( lo(q), first, - "expected {q:?} ≡ {:?}, but they lowered to different L3", + "expected {q:?} ≡ {:?}, but they lowered to different trees", class[0] ); } } -/// Two semantically-distinct queries must lower to *different* L3 trees. +/// Two semantically-distinct queries must lower to *different* canonical trees. fn assert_distinct(a: &str, b: &str) { assert_ne!( lo(a), lo(b), - "{a:?} and {b:?} must not collapse to the same L3" + "{a:?} and {b:?} must not collapse to the same tree" ); } // ───────────────────────────────────────────────────────────────────────────── -// 1. Equivalence classes the lowering canonicalises to one L3. +// 1. Equivalence classes the lowering canonicalises to one shape. // ───────────────────────────────────────────────────────────────────────────── #[test] @@ -78,8 +78,8 @@ fn whitespace_is_irrelevant() { #[test] fn label_matcher_order_is_equivalent() { - // A matcher set is unordered: same series, so same L3 (FIX: predicates are - // now canonicalised by (name, value) at lowering time). + // A matcher set is unordered: same series, so same canonical tree (FIX: + // predicates are now canonicalised by (name, value) at lowering time). assert_equiv(&[r#"up{job="a",env="prod"}"#, r#"up{env="prod",job="a"}"#]); } @@ -125,10 +125,11 @@ fn distinct_semantics_stay_distinct() { #[test] fn rate_and_irate_share_the_same_intent() { - // L3 captures *intent* ("per-second rate of a counter"), not the estimation - // method. `rate` (windowed average) and `irate` (last two samples) differ - // only in HOW the rate is estimated — an L4/execution concern — so they - // share one L3 intent by design. + // The canonical tree captures *intent* ("per-second rate of a counter"), + // not the estimation method. `rate` (windowed average) and `irate` (last + // two samples) differ only in HOW the rate is estimated — a + // post-ASAP/execution concern — so they share one canonical intent by + // design. assert_equiv(&["rate(m[5m])", "irate(m[5m])"]); } @@ -156,7 +157,7 @@ fn changes_and_resets_are_not_count_over_time() { // PromQL: count_over_time = #samples, changes = #value-changes, // resets = #counter-resets. They previously all collapsed to `Count`; now // each lowers to its own intent (issue #44), so all three are pairwise - // distinct L3 rather than being rejected or merged. + // distinct canonical trees rather than being rejected or merged. assert_distinct("changes(m[5m])", "count_over_time(m[5m])"); assert_distinct("resets(m[5m])", "count_over_time(m[5m])"); assert_distinct("changes(m[5m])", "resets(m[5m])"); @@ -166,7 +167,7 @@ fn changes_and_resets_are_not_count_over_time() { fn group_is_not_sum() { // PromQL `group` returns a constant 1 per group; it previously collapsed // onto `sum` (sum of values). It now lowers to its own `Group` intent - // (issue #49) — distinct L3 from `sum`, not merged. + // (issue #49) — a distinct canonical tree from `sum`, not merged. assert_distinct("group(up)", "sum(up)"); assert_distinct("group by (job) (up)", "sum by (job) (up)"); } diff --git a/crates/frontend-promql/tests/promql_lowering.rs b/crates/frontend-promql/tests/promql_lowering.rs index 0cc40d65..8603dafc 100644 --- a/crates/frontend-promql/tests/promql_lowering.rs +++ b/crates/frontend-promql/tests/promql_lowering.rs @@ -1,4 +1,4 @@ -//! End-to-end tests for PromQL → L2 → canonical L3 lowering. +//! End-to-end tests for PromQL → unresolved → canonical tree lowering. use std::time::Duration; @@ -199,7 +199,8 @@ fn histogram_quantile_wraps_inner_in_quantile() { fn histogram_quantile_over_sum_by_le_preserves_grouping() { // The canonical Prometheus histogram pattern. Previously returned // UnsupportedFeature because `extract_matrix` couldn't see through the - // `sum by (le)` aggregate; now the `le` grouping survives into L3. + // `sum by (le)` aggregate; now the `le` grouping survives into the + // canonical tree. let qe = lower(r#"histogram_quantile(0.99, sum by (le) (rate(http_requests_bucket[5m])))"#); let QueryExpr::Aggregate { measures, child, .. @@ -792,12 +793,13 @@ fn batch_rejects_non_promql_language() { assert!(matches!(results[0], Err(LoweringError::WrongLanguage(_)))); } -// ── #12: one home per grouping concept (the L3 `Partition` node is removed) ── +// ── #12: one home per grouping concept (the canonical `Partition` node is removed) ── // // `Partition` and `Aggregate.by` were two ways to express grouping. #12 collapses // them: a reducing GROUP BY → `Aggregate.by`; per-group *ranking* (split without -// reduce) → `Sort.partition_by`; parallel sharding → L5-physical. There is no -// longer an L3 `Partition` node. These tests pin both surviving L3 homes. +// reduce) → `Sort.partition_by`; parallel sharding → a deployment's own +// physical stage. There is no longer a canonical `Partition` node. These +// tests pin both surviving canonical homes. #[test] fn reducing_group_by_lowers_to_aggregate_by() { diff --git a/crates/frontend-sql/src/error.rs b/crates/frontend-sql/src/error.rs index 48c51e38..5342f5a3 100644 --- a/crates/frontend-sql/src/error.rs +++ b/crates/frontend-sql/src/error.rs @@ -2,10 +2,10 @@ use std::fmt; use asap_types::pre_asap::ResolveTreeError; -/// Errors from lowering a SQL query (L1 parse + plan via DataFusion → -/// canonical L2, built directly → -/// [`resolve_root`](asap_types::pre_asap::resolve_root) binds it to L3, issue -/// #179). +/// Errors from lowering a SQL query (parse + plan via DataFusion → the +/// canonical, unresolved tree, built directly → +/// [`resolve_root`](asap_types::pre_asap::resolve_root) binds it to the +/// resolved tree, issue #179). /// /// Carries no PromQL type — the SQL front end never depends on the PromQL /// parser. The language-neutral variants (`UnsupportedFeature` / `WrongLanguage` @@ -28,8 +28,8 @@ pub enum SqlError { UnsupportedFeature(String), /// The workload's query language is not SQL. WrongLanguage(String), - /// Resolving the canonical L2 tree to L3 failed (name resolution against - /// the bound schema). + /// Resolving the canonical unresolved tree failed (name resolution + /// against the bound schema). Convert(ResolveTreeError), } @@ -43,7 +43,7 @@ impl fmt::Display for SqlError { Self::UnsupportedDialect(d) => write!(f, "unsupported SQL dialect: {d}"), Self::UnsupportedFeature(m) => write!(f, "unsupported feature: {m}"), Self::WrongLanguage(l) => write!(f, "unsupported query language: {l}"), - Self::Convert(e) => write!(f, "L2→L3 resolution failed: {e}"), + Self::Convert(e) => write!(f, "column resolution failed: {e}"), } } } diff --git a/crates/frontend-sql/src/lib.rs b/crates/frontend-sql/src/lib.rs index 188e0cf1..7f961535 100644 --- a/crates/frontend-sql/src/lib.rs +++ b/crates/frontend-sql/src/lib.rs @@ -1,7 +1,7 @@ -//! SQL front end: L1 (parse + plan via DataFusion) → the canonical L2 shape, -//! built directly (issue #179) → [`resolve_root`]. +//! SQL front end: parse + plan (via DataFusion) → the canonical, unresolved +//! shape, built directly (issue #179) → [`resolve_root`]. //! -//! Emits [`L2QueryExpr`](asap_types::pre_asap::L2QueryExpr) itself — the +//! Emits [`UnresolvedQueryExpr`](asap_types::pre_asap::UnresolvedQueryExpr) itself — the //! canonical `QueryExpr`, generic over an unresolved //! [`ColumnRef`](asap_types::pre_asap::ColumnRef) — directly, rather than a //! separate per-language relational tree; `resolve_root` runs the @@ -19,12 +19,12 @@ use asap_types::workload::{QueryLanguage, QueryWorkload, SqlDialect}; pub use error::SqlError; pub use sql::{SqlCatalog, SqlLowerer}; -/// Lower a single SQL query string to the canonical L3 `QueryExpr`, parsed as -/// `SqlDialect::DataFusionSQL`. +/// Lower a single SQL query string to the canonical, resolved `QueryExpr`, +/// parsed as `SqlDialect::DataFusionSQL`. /// /// The `catalog` supplies table schemas (used both to plan the SQL with -/// DataFusion and to carry positional column identity into L3). `accuracy` is -/// threaded onto every approximate intent by the shared converter. +/// DataFusion and to carry positional column identity into the resolved +/// tree). `accuracy` is threaded onto every approximate intent as it's built. pub async fn lower_sql( query: &str, catalog: &SqlCatalog, @@ -45,11 +45,11 @@ pub async fn lower_sql_dialect( dialect: SqlDialect, accuracy: AccuracyTarget, ) -> Result { - let l2 = SqlLowerer::with_dialect(catalog, dialect) + let unresolved = SqlLowerer::with_dialect(catalog, dialect) .lower(query, &accuracy) .await?; - let l3 = resolve_root(&l2)?; - Ok(l3) + let resolved = resolve_root(&unresolved)?; + Ok(resolved) } /// Lower every SQL batch entry in `workload` to a `QueryExpr`. diff --git a/crates/frontend-sql/src/sql/expr.rs b/crates/frontend-sql/src/sql/expr.rs index f877f14a..78518172 100644 --- a/crates/frontend-sql/src/sql/expr.rs +++ b/crates/frontend-sql/src/sql/expr.rs @@ -4,8 +4,8 @@ use asap_types::pre_asap::{ArithOp, ColumnRef, CompareOp, ScalarValue}; use crate::error::SqlError as LoweringError; -use super::types::{arrow_to_l3, scalar_value_to_l3}; -use super::L2; +use super::types::{arrow_to_dtype, scalar_value_to_asap}; +use super::Unresolved; pub(super) fn split_conjuncts(expr: &Expr) -> Vec<&Expr> { match expr { @@ -22,13 +22,13 @@ pub(super) fn split_conjuncts(expr: &Expr) -> Vec<&Expr> { } } -/// Translate a DataFusion `Expr` to an `L2`. +/// Translate a DataFusion `Expr` to the canonical, unresolved tree. /// Returns `UnsupportedFeature` for anything not needed in v1. -pub(super) fn df_expr_to_l2(expr: &Expr) -> Result { +pub(super) fn df_expr_to_unresolved(expr: &Expr) -> Result { match expr { // Preserve DataFusion's relation qualifier so a column name shared // across a join (`a.k` vs `b.k`) resolves to the correct side. - Expr::Column(col) => Ok(L2::Column(match &col.relation { + Expr::Column(col) => Ok(Unresolved::Column(match &col.relation { Some(rel) => ColumnRef::Qualified { table: rel.to_string(), name: col.name.clone(), @@ -36,20 +36,22 @@ pub(super) fn df_expr_to_l2(expr: &Expr) -> Result { None => ColumnRef::Named(col.name.clone()), })), - Expr::Literal(sv) => scalar_value_to_l3(sv).map(L2::Literal), + Expr::Literal(sv) => scalar_value_to_asap(sv).map(Unresolved::Literal), - Expr::Alias(a) => df_expr_to_l2(&a.expr), + Expr::Alias(a) => df_expr_to_unresolved(&a.expr), Expr::BinaryExpr(BinaryExpr { left, op, right }) => match op { Operator::And => { let parts = split_conjuncts(expr); - let l3_parts: Result, _> = parts.iter().map(|e| df_expr_to_l2(e)).collect(); - Ok(L2::BoolAnd(l3_parts?)) + let lowered: Result, _> = + parts.iter().map(|e| df_expr_to_unresolved(e)).collect(); + Ok(Unresolved::BoolAnd(lowered?)) } Operator::Or => { let parts = split_disjuncts(expr); - let l3_parts: Result, _> = parts.iter().map(|e| df_expr_to_l2(e)).collect(); - Ok(L2::BoolOr(l3_parts?)) + let lowered: Result, _> = + parts.iter().map(|e| df_expr_to_unresolved(e)).collect(); + Ok(Unresolved::BoolOr(lowered?)) } Operator::Eq => compare(left, CompareOp::Eq, right), Operator::NotEq => compare(left, CompareOp::Ne, right), @@ -86,13 +88,17 @@ pub(super) fn df_expr_to_l2(expr: &Expr) -> Result { // Unary minus: negate literals directly; wrap others in -1 * x. Expr::Negative(inner) => { - let inner_l3 = df_expr_to_l2(inner)?; - match inner_l3 { - L2::Literal(ScalarValue::Int64(v)) => Ok(L2::Literal(ScalarValue::Int64(-v))), - L2::Literal(ScalarValue::Float64(v)) => Ok(L2::Literal(ScalarValue::Float64(-v))), - other => Ok(L2::Arith { + let inner = df_expr_to_unresolved(inner)?; + match inner { + Unresolved::Literal(ScalarValue::Int64(v)) => { + Ok(Unresolved::Literal(ScalarValue::Int64(-v))) + } + Unresolved::Literal(ScalarValue::Float64(v)) => { + Ok(Unresolved::Literal(ScalarValue::Float64(-v))) + } + other => Ok(Unresolved::Arith { op: ArithOp::Mul, - left: Box::new(L2::Literal(ScalarValue::Int64(-1))), + left: Box::new(Unresolved::Literal(ScalarValue::Int64(-1))), right: Box::new(other), }), } @@ -103,35 +109,39 @@ pub(super) fn df_expr_to_l2(expr: &Expr) -> Result { let operand = c .expr .as_ref() - .map(|e| df_expr_to_l2(e).map(Box::new)) + .map(|e| df_expr_to_unresolved(e).map(Box::new)) .transpose()?; let branches = c .when_then_expr .iter() - .map(|(when, then)| Ok((df_expr_to_l2(when)?, df_expr_to_l2(then)?))) + .map(|(when, then)| { + Ok((df_expr_to_unresolved(when)?, df_expr_to_unresolved(then)?)) + }) .collect::, LoweringError>>()?; let else_expr = c .else_expr .as_ref() - .map(|e| df_expr_to_l2(e).map(Box::new)) + .map(|e| df_expr_to_unresolved(e).map(Box::new)) .transpose()?; - Ok(L2::Case { + Ok(Unresolved::Case { operand, branches, else_expr, }) } - Expr::Not(inner) => Ok(L2::Not(Box::new(df_expr_to_l2(inner)?))), + Expr::Not(inner) => Ok(Unresolved::Not(Box::new(df_expr_to_unresolved(inner)?))), - Expr::IsNull(inner) => Ok(L2::IsNull(Box::new(df_expr_to_l2(inner)?))), + Expr::IsNull(inner) => Ok(Unresolved::IsNull(Box::new(df_expr_to_unresolved(inner)?))), - Expr::IsNotNull(inner) => Ok(L2::IsNotNull(Box::new(df_expr_to_l2(inner)?))), + Expr::IsNotNull(inner) => Ok(Unresolved::IsNotNull(Box::new(df_expr_to_unresolved( + inner, + )?))), Expr::Cast(c) => { - let inner = df_expr_to_l2(&c.expr)?; - let to = arrow_to_l3(&c.data_type)?; - Ok(L2::Cast { + let inner = df_expr_to_unresolved(&c.expr)?; + let to = arrow_to_dtype(&c.data_type)?; + Ok(Unresolved::Cast { expr: Box::new(inner), to, try_cast: false, @@ -140,9 +150,9 @@ pub(super) fn df_expr_to_l2(expr: &Expr) -> Result { // TRY_CAST returns NULL on conversion failure; preserve that semantic. Expr::TryCast(c) => { - let inner = df_expr_to_l2(&c.expr)?; - let to = arrow_to_l3(&c.data_type)?; - Ok(L2::Cast { + let inner = df_expr_to_unresolved(&c.expr)?; + let to = arrow_to_dtype(&c.data_type)?; + Ok(Unresolved::Cast { expr: Box::new(inner), to, try_cast: true, @@ -150,9 +160,9 @@ pub(super) fn df_expr_to_l2(expr: &Expr) -> Result { } Expr::InList(il) => { - let expr = df_expr_to_l2(&il.expr)?; - let list: Result, _> = il.list.iter().map(df_expr_to_l2).collect(); - Ok(L2::InList { + let expr = df_expr_to_unresolved(&il.expr)?; + let list: Result, _> = il.list.iter().map(df_expr_to_unresolved).collect(); + Ok(Unresolved::InList { expr: Box::new(expr), list: list?, negated: il.negated, @@ -168,15 +178,15 @@ pub(super) fn df_expr_to_l2(expr: &Expr) -> Result { // NOT BETWEEN: invert each side let lt = compare(&b.expr, CompareOp::Lt, &b.low)?; let gt = compare(&b.expr, CompareOp::Gt, &b.high)?; - Ok(L2::BoolOr(vec![lt, gt])) + Ok(Unresolved::BoolOr(vec![lt, gt])) } else { - Ok(L2::BoolAnd(vec![x_low, x_high])) + Ok(Unresolved::BoolAnd(vec![x_low, x_high])) } } Expr::ScalarFunction(sf) => { - let args: Result, _> = sf.args.iter().map(df_expr_to_l2).collect(); - Ok(L2::FunctionCall { + let args: Result, _> = sf.args.iter().map(df_expr_to_unresolved).collect(); + Ok(Unresolved::FunctionCall { name: sf.func.name().to_string(), args: args?, }) @@ -184,7 +194,7 @@ pub(super) fn df_expr_to_l2(expr: &Expr) -> Result { // Subquery-valued expressions in a predicate/projection — `x > (SELECT // …)`, `x IN (SELECT …)`, `EXISTS (SELECT …)`. These need a subquery - // node in the L2 expression IR (and a correlated-vs-uncorrelated + // node in the unresolved expression IR (and a correlated-vs-uncorrelated // decision); rejected cleanly until that lands rather than mislowered. // Derived tables in `FROM` (the common nesting shape) ARE supported — // see `lower_plan`'s `SubqueryAlias` arm. @@ -199,19 +209,23 @@ pub(super) fn df_expr_to_l2(expr: &Expr) -> Result { } } -pub(super) fn compare(left: &Expr, op: CompareOp, right: &Expr) -> Result { - Ok(L2::Compare { - left: Box::new(df_expr_to_l2(left)?), +pub(super) fn compare( + left: &Expr, + op: CompareOp, + right: &Expr, +) -> Result { + Ok(Unresolved::Compare { + left: Box::new(df_expr_to_unresolved(left)?), op, - right: Box::new(df_expr_to_l2(right)?), + right: Box::new(df_expr_to_unresolved(right)?), }) } -pub(super) fn arith(left: &Expr, op: ArithOp, right: &Expr) -> Result { - Ok(L2::Arith { +pub(super) fn arith(left: &Expr, op: ArithOp, right: &Expr) -> Result { + Ok(Unresolved::Arith { op, - left: Box::new(df_expr_to_l2(left)?), - right: Box::new(df_expr_to_l2(right)?), + left: Box::new(df_expr_to_unresolved(left)?), + right: Box::new(df_expr_to_unresolved(right)?), }) } diff --git a/crates/frontend-sql/src/sql/mod.rs b/crates/frontend-sql/src/sql/mod.rs index dc44d51f..0bf063e3 100644 --- a/crates/frontend-sql/src/sql/mod.rs +++ b/crates/frontend-sql/src/sql/mod.rs @@ -1,9 +1,9 @@ //! SQL → the canonical, unresolved -//! [`L2QueryExpr`](asap_types::pre_asap::query_expr::L2QueryExpr) +//! [`UnresolvedQueryExpr`](asap_types::pre_asap::query_expr::UnresolvedQueryExpr) //! (`QueryExpr`). //! //! Parses SQL via DataFusion (over the catalog's registered tables), then -//! walks the unoptimized `LogicalPlan` and emits `L2QueryExpr` nodes with +//! walks the unoptimized `LogicalPlan` and emits `UnresolvedQueryExpr` nodes with //! unresolved `ColumnRef`s directly (issue #179) — the same tree shape //! [`resolve_root`](asap_types::pre_asap::resolve_root) binds to canonical, //! positional `QueryExpr`. Unlike PromQL's front end, SQL's @@ -17,7 +17,7 @@ //! this shape are responsible for it now, not a converter. //! //! Heavy-hitter `topk` recognition (`ORDER BY count(...) DESC LIMIT k`) is -//! *not* done here: SQL emits a plain `Sort`/`Limit`, and the shared L3 +//! *not* done here: SQL emits a plain `Sort`/`Limit`, and the shared //! `canonicalize` pass (issue #34, run by `resolve_root`) recognises the //! count-ranked shape positionally, so a SQL `ORDER BY`/`LIMIT` and a PromQL //! `topk(...)` converge without either front end special-casing the other's @@ -36,7 +36,8 @@ use datafusion::prelude::{SessionConfig, SessionContext}; use asap_types::pre_asap::agg_intent::AggIntent; use asap_types::pre_asap::query_expr::{ - GroupKeys, L2QueryExpr as L2, Predicate, ProjectItem, Reduction, SortKey, Source, + GroupKeys, Predicate, ProjectItem, Reduction, SortKey, Source, + UnresolvedQueryExpr as Unresolved, }; use asap_types::pre_asap::schema::{DataType, Schema}; use asap_types::pre_asap::{ @@ -52,8 +53,8 @@ mod types; pub use types::SqlCatalog; -use self::expr::df_expr_to_l2; -use self::types::{arrow_to_l3, schema_to_arrow}; +use self::expr::df_expr_to_unresolved; +use self::types::{arrow_to_dtype, schema_to_arrow}; std::thread_local! { static ACCURACY: std::cell::RefCell = @@ -86,10 +87,10 @@ fn current_accuracy() -> AccuracyTarget { ACCURACY.with(|a| a.borrow().clone()) } -/// Lowers SQL strings to the canonical [`L2QueryExpr`](asap_types::pre_asap::L2QueryExpr) +/// Lowers SQL strings to the canonical [`UnresolvedQueryExpr`](asap_types::pre_asap::UnresolvedQueryExpr) /// over a table [`SqlCatalog`]. Call /// [`resolve_root`](asap_types::pre_asap::resolve_root) on the result for -/// canonical L3. +/// the canonical, resolved tree. pub struct SqlLowerer<'a> { catalog: &'a SqlCatalog, dialect: SqlDialect, @@ -113,7 +114,7 @@ impl<'a> SqlLowerer<'a> { Self { catalog, dialect } } - /// Parse + lower a SQL query to the canonical L2 shape, threading + /// Parse + lower a SQL query to the canonical, unresolved shape, threading /// `accuracy` onto every approximate intent (`Count`, `Quantile`, /// `Cardinality`) as it is built. /// @@ -121,7 +122,11 @@ impl<'a> SqlLowerer<'a> { /// (`ctx.sql`) — `lower_plan` itself is synchronous, so once it starts /// there is no further suspension point that could move this task to a /// different OS thread out from under a thread-local set beforehand. - pub async fn lower(&self, sql: &str, accuracy: &AccuracyTarget) -> Result { + pub async fn lower( + &self, + sql: &str, + accuracy: &AccuracyTarget, + ) -> Result { let ctx = self.build_context()?; let df = ctx.sql(sql).await?; let plan = df.into_unoptimized_plan(); @@ -162,7 +167,7 @@ impl<'a> SqlLowerer<'a> { Ok(ctx) } - fn lower_plan(&self, plan: &LogicalPlan) -> Result { + fn lower_plan(&self, plan: &LogicalPlan) -> Result { match plan { LogicalPlan::TableScan(scan) => self.lower_table_scan(scan), LogicalPlan::Filter(filter) => self.lower_filter(filter), @@ -172,7 +177,7 @@ impl<'a> SqlLowerer<'a> { LogicalPlan::Limit(limit) => self.lower_limit(limit), LogicalPlan::Distinct(d) => match d { Distinct::On(_) => Err(LoweringError::UnsupportedFeature("DISTINCT ON".into())), - Distinct::All(input) => Ok(L2::Distinct { + Distinct::All(input) => Ok(Unresolved::Distinct { cols: vec![], child: Box::new(self.lower_plan(input)?), }), @@ -185,7 +190,7 @@ impl<'a> SqlLowerer<'a> { .ok_or_else(|| LoweringError::InvalidExpression("empty union".into()))?; let first_expr = self.lower_plan(first)?; iter.try_fold(first_expr, |left, right_plan| { - Ok(L2::SetOp { + Ok(Unresolved::SetOp { kind: SetOpKind::Union, all: true, left: Box::new(left), @@ -217,7 +222,7 @@ impl<'a> SqlLowerer<'a> { match self.lower_plan(other)? { // The derived SELECT list already lowered to a // Projection — stamp the alias onto it, no extra node. - L2::Project { cols, child, .. } => Ok(L2::Project { + Unresolved::Project { cols, child, .. } => Ok(Unresolved::Project { cols, qualifier: Some(alias_name), child, @@ -233,10 +238,12 @@ impl<'a> SqlLowerer<'a> { .iter() .map(|f| ProjectItem { alias: Some(f.name().clone()), - expr: L2::Column(ColumnRef::Named(f.name().clone())), + expr: Unresolved::Column(ColumnRef::Named( + f.name().clone(), + )), }) .collect(); - Ok(L2::Project { + Ok(Unresolved::Project { cols, qualifier: Some(alias_name), child: Box::new(inner), @@ -264,7 +271,7 @@ impl<'a> SqlLowerer<'a> { /// before: a semi-join only ever drops left rows, so the two orders agree — /// and keeping the fold-onto-`Scan` (`filter_or_fold`) below the joins /// matches where the old converter folded it too. - fn lower_filter(&self, filter: &logical_expr::Filter) -> Result { + fn lower_filter(&self, filter: &logical_expr::Filter) -> Result { let mut conjuncts = Vec::new(); split_conjunction(&filter.predicate, &mut conjuncts); let (subqueries, residual): (Vec<_>, Vec<_>) = conjuncts @@ -273,7 +280,7 @@ impl<'a> SqlLowerer<'a> { let input = self.lower_plan(&filter.input)?; let mut node = match rebuild_conjunction(&residual) { - Some(pred) => filter_or_fold(df_expr_to_l2(&pred)?, input), + Some(pred) => filter_or_fold(df_expr_to_unresolved(&pred)?, input), None => input, }; for sq in subqueries { @@ -290,8 +297,8 @@ impl<'a> SqlLowerer<'a> { fn lower_in_subquery( &self, is: &logical_expr::expr::InSubquery, - left: L2, - ) -> Result { + left: Unresolved, + ) -> Result { if is.negated { // `NOT IN` is not an anti-join. Under three-valued logic a single // NULL among the subquery's rows makes `c NOT IN (…)` UNKNOWN for @@ -324,29 +331,31 @@ impl<'a> SqlLowerer<'a> { // Rebuild the subquery's projection with the synthetic alias, so a // computed key (`SELECT bytes + 1 …`) is named rather than becoming // the anonymous `col_0` that nothing can reference. - LogicalPlan::Projection(p) if p.expr.len() == 1 => L2::Project { + LogicalPlan::Projection(p) if p.expr.len() == 1 => Unresolved::Project { cols: vec![ProjectItem { alias: Some(IN_SUBQUERY_KEY.to_string()), - expr: df_expr_to_l2(unalias(&p.expr[0]))?, + expr: df_expr_to_unresolved(unalias(&p.expr[0]))?, }], qualifier: None, child: Box::new(self.lower_plan(&p.input)?), }, - other => L2::Project { + other => Unresolved::Project { cols: vec![ProjectItem { alias: Some(IN_SUBQUERY_KEY.to_string()), - expr: L2::Column(ColumnRef::Named(key.name().clone())), + expr: Unresolved::Column(ColumnRef::Named(key.name().clone())), }], qualifier: None, child: Box::new(self.lower_plan(other)?), }, }; - Ok(L2::Join { + Ok(Unresolved::Join { kind: JoinKind::Semi, - pred: Predicate(Box::new(L2::Compare { - left: Box::new(df_expr_to_l2(&is.expr)?), + pred: Predicate(Box::new(Unresolved::Compare { + left: Box::new(df_expr_to_unresolved(&is.expr)?), op: CompareOp::Eq, - right: Box::new(L2::Column(ColumnRef::Named(IN_SUBQUERY_KEY.to_string()))), + right: Box::new(Unresolved::Column(ColumnRef::Named( + IN_SUBQUERY_KEY.to_string(), + ))), })), left: Box::new(left), right: Box::new(right), @@ -355,7 +364,11 @@ impl<'a> SqlLowerer<'a> { /// `[NOT] EXISTS (SELECT … WHERE inner.k = outer.k)` → a semi- / anti-join /// on the correlation predicate (issue #111). - fn lower_exists(&self, ex: &logical_expr::expr::Exists, left: L2) -> Result { + fn lower_exists( + &self, + ex: &logical_expr::expr::Exists, + left: Unresolved, + ) -> Result { let kind = if ex.negated { JoinKind::Anti } else { @@ -376,10 +389,10 @@ impl<'a> SqlLowerer<'a> { // the join condition is unconditionally true — same convention as an // unconditional `JOIN` (`lower_join`, below). let pred = match correlation { - Some(e) => Predicate(Box::new(df_expr_to_l2(&e)?)), - None => Predicate(Box::new(L2::Literal(ScalarValue::Boolean(true)))), + Some(e) => Predicate(Box::new(df_expr_to_unresolved(&e)?)), + None => Predicate(Box::new(Unresolved::Literal(ScalarValue::Boolean(true)))), }; - Ok(L2::Join { + Ok(Unresolved::Join { kind, pred, left: Box::new(left), @@ -392,7 +405,10 @@ impl<'a> SqlLowerer<'a> { /// usage-derive it (SQL is never schemaless). Projection pushdown is left /// to the enclosing `Project` (DataFusion's unoptimized plan sets no /// projection). - fn lower_table_scan(&self, scan: &logical_expr::TableScan) -> Result { + fn lower_table_scan( + &self, + scan: &logical_expr::TableScan, + ) -> Result { let table = scan.table_name.to_string(); self.scan_source(&table, &table) } @@ -400,7 +416,7 @@ impl<'a> SqlLowerer<'a> { /// A `Scan` over catalog table `table`, with its columns qualified by /// `qualifier` (the table name, or an alias from a `SubqueryAlias`) so /// `Qualified` column refs resolve to the right side across a join. - fn scan_source(&self, table: &str, qualifier: &str) -> Result { + fn scan_source(&self, table: &str, qualifier: &str) -> Result { let schema = self .catalog .tables @@ -418,7 +434,7 @@ impl<'a> SqlLowerer<'a> { // Catalog-backed: the table's columns are fully declared → closed. closed: true, }; - Ok(L2::Scan { + Ok(Unresolved::Scan { source: Source::Table { table_ref: table.to_string(), }, @@ -428,11 +444,11 @@ impl<'a> SqlLowerer<'a> { } /// ⋈ — equijoin. The `on` key pairs become `left = right` comparisons, - /// AND-ed with any non-equi `filter`, into the L2 join predicate. The L2→L3 - /// converter derives the concatenated output schema; the join predicate - /// stays name-based (like a `WHERE`). Semi/anti/mark joins have no L3 - /// counterpart yet and are rejected. - fn lower_join(&self, join: &logical_expr::Join) -> Result { + /// AND-ed with any non-equi `filter`, into the join predicate — still + /// name-based here (like a `WHERE`); `resolve_root` derives the + /// concatenated output schema downstream. Semi/anti/mark joins have no + /// canonical counterpart yet and are rejected. + fn lower_join(&self, join: &logical_expr::Join) -> Result { let kind = match join.join_type { JoinType::Inner => JoinKind::Inner, JoinType::Left => JoinKind::Left, @@ -448,23 +464,23 @@ impl<'a> SqlLowerer<'a> { .on .iter() .map(|(l, r)| { - Ok(L2::Compare { - left: Box::new(df_expr_to_l2(l)?), + Ok(Unresolved::Compare { + left: Box::new(df_expr_to_unresolved(l)?), op: CompareOp::Eq, - right: Box::new(df_expr_to_l2(r)?), + right: Box::new(df_expr_to_unresolved(r)?), }) }) .collect::, LoweringError>>()?; if let Some(filter) = &join.filter { - conjuncts.push(df_expr_to_l2(filter)?); + conjuncts.push(df_expr_to_unresolved(filter)?); } let pred = Predicate(Box::new(match conjuncts.len() { // No condition (a CROSS JOIN) is unconditionally true. - 0 => L2::Literal(ScalarValue::Boolean(true)), + 0 => Unresolved::Literal(ScalarValue::Boolean(true)), 1 => conjuncts.pop().unwrap(), - _ => L2::BoolAnd(conjuncts), + _ => Unresolved::BoolAnd(conjuncts), })); - Ok(L2::Join { + Ok(Unresolved::Join { kind, pred, left: Box::new(self.lower_plan(&join.left)?), @@ -474,7 +490,7 @@ impl<'a> SqlLowerer<'a> { /// `func(args) OVER (PARTITION BY … ORDER BY …)`. One window function per /// plan node; window frames are not modelled yet (default frame assumed). - fn lower_window(&self, window: &logical_expr::Window) -> Result { + fn lower_window(&self, window: &logical_expr::Window) -> Result { if window.window_expr.len() > 1 { return Err(LoweringError::UnsupportedFeature(format!( "multiple window functions in one plan node (got {}); split them", @@ -495,12 +511,12 @@ impl<'a> SqlLowerer<'a> { let mut args = wf .args .iter() - .map(df_expr_to_l2) + .map(df_expr_to_unresolved) .collect::, _>>()?; // Nth_value: lift N from the (literal) 2nd arg, keep only the column. let func = if matches!(func, WindowFuncKind::NthValue(None)) { let n = match args.get(1) { - Some(L2::Literal(ScalarValue::Int64(n))) if *n > 0 => *n as u64, + Some(Unresolved::Literal(ScalarValue::Int64(n))) if *n > 0 => *n as u64, other => { return Err(LoweringError::InvalidExpression(format!( "NTH_VALUE requires a positive integer literal 2nd arg, got {other:?}" @@ -521,7 +537,7 @@ impl<'a> SqlLowerer<'a> { .order_by .iter() .map(|s| { - df_expr_to_l2(&s.expr).map(|expr| SortKey { + df_expr_to_unresolved(&s.expr).map(|expr| SortKey { expr, ascending: s.asc, nulls_first: s.nulls_first, @@ -536,7 +552,7 @@ impl<'a> SqlLowerer<'a> { .last() .map(|f| f.name().clone()) .unwrap_or_else(|| "window".into()); - Ok(L2::WindowFunc { + Ok(Unresolved::WindowFunc { func, args, partition_by: partition_by.into(), @@ -546,7 +562,10 @@ impl<'a> SqlLowerer<'a> { }) } - fn lower_projection(&self, proj: &logical_expr::Projection) -> Result { + fn lower_projection( + &self, + proj: &logical_expr::Projection, + ) -> Result { // SELECT * — no column constraint; pass through without a Project. if proj.expr.iter().any(|e| matches!(e, Expr::Wildcard { .. })) { return self.lower_plan(&proj.input); @@ -556,21 +575,21 @@ impl<'a> SqlLowerer<'a> { .expr .iter() .map(|e| match e { - Expr::Alias(a) => df_expr_to_l2(&a.expr).map(|expr| ProjectItem { + Expr::Alias(a) => df_expr_to_unresolved(&a.expr).map(|expr| ProjectItem { expr, alias: Some(a.name.clone()), }), - _ => df_expr_to_l2(e).map(|expr| ProjectItem { expr, alias: None }), + _ => df_expr_to_unresolved(e).map(|expr| ProjectItem { expr, alias: None }), }) .collect::, _>>()?; - Ok(L2::Project { + Ok(Unresolved::Project { cols, qualifier: None, child, }) } - fn lower_aggregate(&self, agg: &logical_expr::Aggregate) -> Result { + fn lower_aggregate(&self, agg: &logical_expr::Aggregate) -> Result { let input = self.lower_plan(&agg.input)?; // `GROUPING SETS`/`ROLLUP`/`CUBE` emit several grouping levels from one @@ -609,7 +628,7 @@ impl<'a> SqlLowerer<'a> { .get(i) .cloned() .unwrap_or_else(|| other.to_string()); - derived.materialize(name.clone(), df_expr_to_l2(other)?)?; + derived.materialize(name.clone(), df_expr_to_unresolved(other)?)?; keys.push(ColumnRef::Named(name)); } } @@ -640,7 +659,7 @@ impl<'a> SqlLowerer<'a> { .iter() .map(lower_agg_intent) .collect::, LoweringError>>()?; - Ok(L2::Aggregate { + Ok(Unresolved::Aggregate { // SQL `GROUP BY` is always an inclusion list, never PromQL's // `without(...)` exclusion form — and always a genuine reduction, // never `PerEntity` (there's no windowed/subquery-child concept @@ -674,8 +693,8 @@ impl<'a> SqlLowerer<'a> { &self, agg: &logical_expr::Aggregate, gs: &logical_expr::GroupingSet, - input: L2, - ) -> Result { + input: Unresolved, + ) -> Result { // DataFusion normalizes every mixed form (`GROUP BY g, ROLLUP(d)`) into a // single `GroupingSets`, so one grouping expression is the only shape. if agg.group_expr.len() != 1 { @@ -700,7 +719,7 @@ impl<'a> SqlLowerer<'a> { .fields() .iter() .take(distinct.len()) - .map(|f| Ok((f.name().to_string(), arrow_to_l3(f.data_type())?))) + .map(|f| Ok((f.name().to_string(), arrow_to_dtype(f.data_type())?))) .collect::>()?; let output_names: Vec = agg @@ -736,7 +755,7 @@ impl<'a> SqlLowerer<'a> { .filter(|e| level.contains(e)) .map(|e| expr_to_group_ref(e)) .collect::, LoweringError>>()?; - let aggregate = L2::Aggregate { + let aggregate = Unresolved::Aggregate { reduction: Reduction::Reduce(GroupKeys::by(level_keys)), measures: measures.clone(), output_names: output_names.clone(), @@ -750,10 +769,10 @@ impl<'a> SqlLowerer<'a> { .map(|((name, dtype), e)| ProjectItem { alias: Some(name.clone()), expr: if level.contains(e) { - L2::Column(ColumnRef::Named(name.clone())) + Unresolved::Column(ColumnRef::Named(name.clone())) } else { - L2::Cast { - expr: Box::new(L2::Literal(ScalarValue::Null)), + Unresolved::Cast { + expr: Box::new(Unresolved::Literal(ScalarValue::Null)), to: dtype.clone(), try_cast: false, } @@ -761,10 +780,10 @@ impl<'a> SqlLowerer<'a> { }) .chain(output_names.iter().map(|n| ProjectItem { alias: Some(n.clone()), - expr: L2::Column(ColumnRef::Named(n.clone())), + expr: Unresolved::Column(ColumnRef::Named(n.clone())), })) .collect(); - Ok(L2::Project { + Ok(Unresolved::Project { cols, qualifier: None, child: Box::new(aggregate), @@ -772,13 +791,13 @@ impl<'a> SqlLowerer<'a> { }) .collect::, LoweringError>>()?; - Ok(L2::Merge { children: branches }) + Ok(Unresolved::Merge { children: branches }) } - fn lower_sort(&self, sort: &logical_expr::Sort) -> Result { + fn lower_sort(&self, sort: &logical_expr::Sort) -> Result { // A count-ranked `ORDER BY … LIMIT k` is the frequency heavy-hitter the // `TopK` intent represents, but that promotion now happens in the shared - // L3 `canonicalize` pass (issue #34) — the same one both front ends run — + // `canonicalize` pass (issue #34) — the same one both front ends run — // so SQL emits a plain `Sort` (+ `Limit`) here and lets canonicalization // recognise the count-ranked shape positionally. This removes the gate's // alias blind spot (#20). @@ -786,14 +805,14 @@ impl<'a> SqlLowerer<'a> { .expr .iter() .map(|s| { - df_expr_to_l2(&s.expr).map(|expr| SortKey { + df_expr_to_unresolved(&s.expr).map(|expr| SortKey { expr, ascending: s.asc, nulls_first: s.nulls_first, }) }) .collect::, _>>()?; - Ok(L2::Sort { + Ok(Unresolved::Sort { keys, // SQL `ORDER BY` is a global sort; per-group ranking would come from a // window function (`WindowFunc`), not a bare Sort. @@ -802,10 +821,10 @@ impl<'a> SqlLowerer<'a> { }) } - fn lower_limit(&self, limit: &logical_expr::Limit) -> Result { + fn lower_limit(&self, limit: &logical_expr::Limit) -> Result { // Count-ranked `LIMIT k` over a `Sort` is promoted to the heavy-hitter - // `TopK` by the shared L3 `canonicalize` pass (issue #34), not here. - Ok(L2::Limit { + // `TopK` by the shared `canonicalize` pass (issue #34), not here. + Ok(Unresolved::Limit { n: eval_fetch(&limit.fetch).unwrap_or(usize::MAX), offset: eval_fetch(&limit.skip).unwrap_or(0), child: Box::new(self.lower_plan(&limit.input)?), @@ -826,7 +845,8 @@ fn lower_agg_intent(expr: &Expr) -> Result, LoweringError> Expr::Alias(a) => lower_agg_intent(&a.expr), Expr::AggregateFunction(agg_fn) => { let name = agg_fn.func.name().to_lowercase(); - // L3 has no DISTINCT modifier for the value reducers; only + // The canonical intent algebra has no DISTINCT modifier for the + // value reducers; only // COUNT(DISTINCT) maps (to Cardinality). Reject DISTINCT elsewhere // rather than silently lowering `SUM(DISTINCT x)` as `SUM(x)`. if agg_fn.distinct && name != "count" { @@ -913,21 +933,21 @@ const IN_SUBQUERY_KEY: &str = "__asap_in_key"; /// `Filter` — canonical's invariant that a `Filter` never sits directly over a /// `Scan`. A front end emitting the canonical shape directly is responsible /// for maintaining that invariant itself (issue #179). -fn filter_or_fold(pred: L2, child: L2) -> L2 { +fn filter_or_fold(pred: Unresolved, child: Unresolved) -> Unresolved { match child { - L2::Scan { + Unresolved::Scan { source, mut predicates, schema, } => { predicates.push(Predicate(Box::new(pred))); - L2::Scan { + Unresolved::Scan { source, predicates, schema, } } - other => L2::Filter { + other => Unresolved::Filter { pred: Predicate(Box::new(pred)), child: Box::new(other), }, @@ -1109,7 +1129,7 @@ impl DerivedCols { /// something else. `Project` carries one relation qualifier for all its /// columns, so `a.k` and `b.k` cannot both survive it — but that only /// matters when a projection gets inserted at all. - fn push(&mut self, alias: String, expr: L2) { + fn push(&mut self, alias: String, expr: Unresolved) { let existing = self .cols .iter() @@ -1132,12 +1152,12 @@ impl DerivedCols { let Expr::Column(c) = unalias(expr) else { return Ok(()); }; - self.push(c.name.clone(), df_expr_to_l2(expr)?); + self.push(c.name.clone(), df_expr_to_unresolved(expr)?); Ok(()) } /// A genuinely derived column: `alias` now names `expr`'s value. - fn materialize(&mut self, alias: String, expr: L2) -> Result<(), LoweringError> { + fn materialize(&mut self, alias: String, expr: Unresolved) -> Result<(), LoweringError> { self.any = true; self.push(alias, expr); Ok(()) @@ -1160,12 +1180,12 @@ impl DerivedCols { } match agg_col_name(&agg_fn.args) { Some(name) => { - self.push(name, df_expr_to_l2(arg)?); + self.push(name, df_expr_to_unresolved(arg)?); Ok(expr.clone()) } None => { let alias = unalias(arg).to_string(); - self.materialize(alias.clone(), df_expr_to_l2(arg)?)?; + self.materialize(alias.clone(), df_expr_to_unresolved(arg)?)?; let mut agg_fn = agg_fn.clone(); agg_fn.args[0] = Expr::Column(DfColumn::new_unqualified(alias)); Ok(Expr::AggregateFunction(agg_fn)) @@ -1177,7 +1197,7 @@ impl DerivedCols { /// nothing needed deriving — so a query that lowers today keeps its exact /// tree, and a name collision that the projection would have flattened only /// matters once the projection exists. - fn wrap(self, input: L2) -> Result { + fn wrap(self, input: Unresolved) -> Result { if !self.any { return Ok(input); } @@ -1187,7 +1207,7 @@ impl DerivedCols { aggregate — alias the relations apart" ))); } - Ok(L2::Project { + Ok(Unresolved::Project { cols: self.cols, qualifier: None, child: Box::new(input), @@ -1211,8 +1231,9 @@ fn agg_col_name(args: &[Expr]) -> Option { /// The single input column of a value reducer (`SUM`/`MIN`/`MAX`/`AVG`/stddev/ /// variance/quantile/count-distinct). Errors if the argument is not a column: -/// L3 reduces a column, not an arbitrary expression (`SUM(a*b)`), so silently -/// picking a probe column would compute the wrong result. +/// the canonical `AggIntent` reduces a column, not an arbitrary expression +/// (`SUM(a*b)`), so silently picking a probe column would compute the wrong +/// result. fn reducer_col(name: &str, args: &[Expr]) -> Result { agg_col_name(args).map(ColumnRef::Named).ok_or_else(|| { LoweringError::UnsupportedAggregate(format!("{name} over a non-column expression")) @@ -1223,7 +1244,7 @@ fn expr_to_group_ref(expr: &Expr) -> Result { match expr { // Preserve the relation qualifier so a GROUP BY / PARTITION BY key over a // join (`b.k` vs `a.k`) resolves to the correct side — the same rule the - // scalar predicate path uses (`df_expr_to_l2`). + // scalar predicate path uses (`df_expr_to_unresolved`). Expr::Column(col) => Ok(match &col.relation { Some(rel) => ColumnRef::Qualified { table: rel.to_string(), @@ -1268,7 +1289,8 @@ fn eval_fetch(expr_opt: &Option>) -> Option { }) } -/// Map a DataFusion window-function definition to the L3 [`WindowFuncKind`]. +/// Map a DataFusion window-function definition to the canonical +/// [`WindowFuncKind`]. /// `NthValue` is returned with `None`; `lower_window` fills in `n` from args. fn lower_window_func_kind(fun: &WindowFunctionDefinition) -> Result { let unsupported = |what: &str, name: &str| { diff --git a/crates/frontend-sql/src/sql/types.rs b/crates/frontend-sql/src/sql/types.rs index 3afca01b..36dd4eed 100644 --- a/crates/frontend-sql/src/sql/types.rs +++ b/crates/frontend-sql/src/sql/types.rs @@ -1,6 +1,6 @@ -//! Type bridges between DataFusion's Arrow types and the L3 `DataType`, plus +//! Type bridges between DataFusion's Arrow types and the canonical `DataType`, plus //! the SQL table catalog used to register tables with DataFusion and to carry -//! resolved leaf schemas into the relational L2 tree. +//! resolved leaf schemas into the canonical, unresolved tree. use std::collections::HashMap; @@ -14,7 +14,7 @@ use asap_types::pre_asap::ScalarValue; use crate::error::SqlError as LoweringError; -/// Table catalog for SQL lowering: table name → resolved L3 [`Schema`]. +/// Table catalog for SQL lowering: table name → resolved canonical [`Schema`]. /// /// Used twice: to register Arrow-backed `MemTable`s so DataFusion can resolve /// `SELECT … FROM t`, and to attach each table's schema directly onto the @@ -30,14 +30,14 @@ impl SqlCatalog { Self::default() } - /// Builder: register `name` with its resolved L3 schema. + /// Builder: register `name` with its resolved canonical schema. pub fn with_table(mut self, name: impl Into, schema: Schema) -> Self { self.tables.insert(name.into(), schema); self } } -pub(super) fn scalar_value_to_l3(sv: &DfScalarValue) -> Result { +pub(super) fn scalar_value_to_asap(sv: &DfScalarValue) -> Result { match sv { DfScalarValue::Int64(Some(v)) => Ok(ScalarValue::Int64(*v)), DfScalarValue::Int32(Some(v)) => Ok(ScalarValue::Int64(*v as i64)), @@ -60,8 +60,8 @@ pub(super) fn scalar_value_to_l3(sv: &DfScalarValue) -> Result Result { +/// Arrow → the canonical `DataType` (used for `CAST` targets). Deliberately narrow. +pub(super) fn arrow_to_dtype(dt: &ArrowDataType) -> Result { match dt { ArrowDataType::Int64 | ArrowDataType::Int32 @@ -77,8 +77,8 @@ pub(super) fn arrow_to_l3(dt: &ArrowDataType) -> Result } } -/// L3 `DataType` → Arrow (for registering catalog tables with DataFusion). -pub(super) fn l3_to_arrow(dt: &DataType) -> ArrowDataType { +/// The canonical `DataType` → Arrow (for registering catalog tables with DataFusion). +pub(super) fn dtype_to_arrow(dt: &DataType) -> ArrowDataType { match dt { DataType::Int64 => ArrowDataType::Int64, DataType::Float64 => ArrowDataType::Float64, @@ -90,12 +90,12 @@ pub(super) fn l3_to_arrow(dt: &DataType) -> ArrowDataType { } } -/// Build an Arrow schema from an L3 [`Schema`] (column name + type + nullability). +/// Build an Arrow schema from a canonical [`Schema`] (column name + type + nullability). pub(super) fn schema_to_arrow(schema: &Schema) -> ArrowSchema { let fields: Fields = schema .columns .iter() - .map(|c: &Column| Field::new(&c.name, l3_to_arrow(&c.dtype), c.nullable)) + .map(|c: &Column| Field::new(&c.name, dtype_to_arrow(&c.dtype), c.nullable)) .collect(); ArrowSchema::new(fields) } diff --git a/crates/frontend-sql/tests/sql_lowering.rs b/crates/frontend-sql/tests/sql_lowering.rs index 594cc372..4f0dd1dd 100644 --- a/crates/frontend-sql/tests/sql_lowering.rs +++ b/crates/frontend-sql/tests/sql_lowering.rs @@ -1,9 +1,9 @@ -//! End-to-end SQL → L2 → canonical L3 lowering tests (positional IR). +//! End-to-end SQL → unresolved → canonical tree lowering tests (positional IR). //! //! Validates the DataFusion front end: SQL parses + plans, lowers directly to -//! the canonical L2 shape (`QueryExpr`, issue #179), and the shared -//! `resolve_root` produces positional canonical L3 (the same resolver the -//! PromQL path uses). +//! the canonical, unresolved shape (`QueryExpr`, issue #179), and +//! the shared `resolve_root` produces the positional, resolved canonical +//! tree (the same resolver the PromQL path uses). use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_types::pre_asap::schema::{Column, DataType, Schema}; @@ -166,8 +166,8 @@ async fn multi_aggregate_group_by_binds_columns_positionally() { async fn projection_over_aggregate_resolves_output_types_via_output_names() { // The enclosing Projection references the aggregates by DataFusion's // generated names (e.g. "sum(metrics.bytes)"); output_names threads those - // onto the L3 Aggregate so the Project resolves real types — not the Utf8 - // fallback that an unresolved column would get. + // onto the canonical Aggregate so the Project resolves real types — not + // the Utf8 fallback that an unresolved column would get. let qe = lower("SELECT SUM(bytes), AVG(latency) FROM metrics").await; let schema = qe .output_schema() @@ -212,7 +212,7 @@ async fn single_agg_group_by_keeps_key_in_output_schema() { #[tokio::test] async fn count_ranked_topk_is_heavy_hitter() { // `ORDER BY COUNT(*) DESC LIMIT k` over a single COUNT aggregate is the one - // case the heavy-hitter (frequency) sketch is correct for. The shared L3 + // case the heavy-hitter (frequency) sketch is correct for. The shared // `canonicalize` pass (issue #34) promotes it to the canonical two-level // form: an outer global `TopK` (by: []) over the explicit inner `Count` // grouped by `service`. @@ -245,7 +245,7 @@ async fn count_ranked_topk_is_heavy_hitter() { async fn count_ranked_topk_via_alias_is_also_heavy_hitter() { // Regression for #20: aliasing `COUNT(*)` in the ORDER BY used to defeat the // SQL front-end gate. The positional `canonicalize` pass now promotes it too, - // so the aliased and inline forms produce identical L3. + // so the aliased and inline forms produce an identical canonical tree. let inline = lower( "SELECT service, COUNT(*) FROM metrics GROUP BY service ORDER BY COUNT(*) DESC LIMIT 10", ) @@ -286,8 +286,8 @@ async fn non_count_ranked_limit_keeps_the_aggregate() { #[tokio::test] async fn distinct_value_reducer_is_rejected_not_dropped() { - // L3 has no distinct-Sum; SUM(DISTINCT x) must be rejected, not silently - // lowered as SUM(x). + // The canonical intent algebra has no distinct-Sum; SUM(DISTINCT x) must + // be rejected, not silently lowered as SUM(x). let res = lower_sql( "SELECT SUM(DISTINCT bytes) FROM metrics", &catalog(), @@ -299,9 +299,10 @@ async fn distinct_value_reducer_is_rejected_not_dropped() { #[tokio::test] async fn aggregate_over_an_expression_reduces_a_derived_column() { - // L3 reduces a column, not an arbitrary expression. `SUM(bytes + 1)` used to - // be rejected for that reason; since #110 the expression is materialized as - // a derived column in a `Project` beneath the aggregate, and reduced there. + // The canonical `AggIntent` reduces a column, not an arbitrary expression. + // `SUM(bytes + 1)` used to be rejected for that reason; since #110 the + // expression is materialized as a derived column in a `Project` beneath + // the aggregate, and reduced there. let qe = lower("SELECT SUM(bytes + 1) FROM metrics").await; let (_, measures) = find_aggregate(&qe).expect("expected an Aggregate"); assert!( @@ -349,7 +350,7 @@ async fn select_distinct_lowers_to_distinct_with_positional_cols() { #[tokio::test] async fn inner_join_lowers_to_join_over_two_scans() { - // INNER JOIN over two distinct tables → L3 Join with both leaves as Scans. + // INNER JOIN over two distinct tables → a canonical Join with both leaves as Scans. let qe = lower( "SELECT metrics.bytes, hosts.region \ FROM metrics JOIN hosts ON metrics.service = hosts.service", @@ -494,7 +495,7 @@ async fn qualified_where_over_join_resolves_to_right_side() { #[tokio::test] async fn self_join_group_by_disambiguates_via_qualifier() { - // L2 group-key qualifier fix: GROUP BY on the *duplicated* column over a + // Group-key qualifier fix: GROUP BY on the *duplicated* column over a // self-join must bind to the qualified side, not first-match. metrics ⋈ // metrics → a.service = col 1, b.service = col 5. (Without qualified keys, // both `GROUP BY a.service` and `GROUP BY b.service` collapsed to col 1.) @@ -765,7 +766,8 @@ fn all_intents(qe: &QueryExpr) -> Vec { async fn derived_table_aggregate_over_aggregate_nests() { // `MAX(s)` over a derived table `(SELECT service, SUM(bytes) AS s … GROUP BY // service)` — the SQL counterpart of PromQL function nesting (issue #27). - // Both reductions survive into L3: an outer `Max` over the inner `Sum`. + // Both reductions survive into the canonical tree: an outer `Max` over + // the inner `Sum`. let qe = lower( "SELECT MAX(s) FROM \ (SELECT service, SUM(bytes) AS s FROM metrics GROUP BY service) t", @@ -788,7 +790,7 @@ async fn derived_table_aggregate_over_aggregate_nests() { #[tokio::test] async fn derived_table_outer_avg_over_inner_percentile() { // Outer exact `AVG` over an inner approximate `Quantile` — each layer keeps - // its own intent (the per-node sketch-vs-exact choice is an L4 decision). + // its own intent (the per-node sketch-vs-exact choice is a post-ASAP decision). let qe = lower( "SELECT AVG(p) FROM \ (SELECT service, approx_percentile_cont(latency, 0.9) AS p \ @@ -827,8 +829,8 @@ async fn filter_over_derived_aggregate_resolves_alias_column() { #[tokio::test] async fn scalar_subquery_in_predicate_is_rejected() { // A subquery-*valued* expression (`x > (SELECT …)`) needs a subquery node in - // the L2 expression IR (and a correlated/uncorrelated decision); rejected - // cleanly until that lands. Derived tables in FROM (the common nesting + // the unresolved expression IR (and a correlated/uncorrelated decision); + // rejected cleanly until that lands. Derived tables in FROM (the common nesting // shape) ARE supported — see the tests above. let res = lower_sql( "SELECT service FROM metrics WHERE bytes > (SELECT AVG(bytes) FROM metrics)", @@ -968,8 +970,9 @@ async fn count_distinct_carries_its_input_column() { #[tokio::test] async fn quantile_and_count_distinct_over_an_expression_bind_the_derived_column() { // A SQL aggregate has no "sample value" to fall back on, so an expression - // argument must never reach L3 as `col: None` (#115). Since #110 it reaches - // L3 as `col: Some(derived)` instead of being rejected. + // argument must never reach the canonical tree as `col: None` (#115). + // Since #110 it reaches the canonical tree as `col: Some(derived)` + // instead of being rejected. for q in [ "SELECT approx_percentile_cont(bytes * 8, 0.95) FROM metrics", "SELECT COUNT(DISTINCT bytes * 8) FROM metrics", diff --git a/crates/integration-tests/src/lib.rs b/crates/integration-tests/src/lib.rs index e742ed63..5a96aa07 100644 --- a/crates/integration-tests/src/lib.rs +++ b/crates/integration-tests/src/lib.rs @@ -1,10 +1,11 @@ //! Shared test fixtures for the ASAP e2e test suite. //! -//! This crate owns the integration tests that verify correct L3 IR output for -//! each input **PromQL** query workload (it depends on the PromQL front end -//! only). The *cross-language* equivalence tests — semantically equivalent SQL -//! and PromQL mapping to the same canonical L3 — live in -//! `crates/lower/tests/cross_language.rs`, where both front ends are in scope. +//! This crate owns the integration tests that verify correct pre-ASAP IR +//! output for each input **PromQL** query workload (it depends on the PromQL +//! front end only). The *cross-language* equivalence tests — semantically +//! equivalent SQL and PromQL mapping to the same canonical pre-ASAP IR — +//! live in `crates/devtools/tests/cross_language.rs`, where both front ends +//! are in scope. //! //! `fixtures` provides column/schema constructors used across test files. //! Expected IR trees are always hand-constructed inside each test — nothing diff --git a/crates/integration-tests/tests/l4_binding.rs b/crates/integration-tests/tests/l4_binding.rs index 79fe9c3a..907d9011 100644 --- a/crates/integration-tests/tests/l4_binding.rs +++ b/crates/integration-tests/tests/l4_binding.rs @@ -1,22 +1,23 @@ -//! End-to-end query-string → L4 pin (issue #98). +//! End-to-end query-string → post-ASAP IR pin (issue #98). //! -//! Drives the full pipeline — PromQL text → L3 `QueryExpr` (`lower_promql`) -//! → L4 `SummaryExpr` DAG (`asap_aware_mapping::implement_tree`) — and pins the sketch-bound -//! shape node by node, including the `(SummaryKind, SummaryParams)` committed -//! on each edge's schema. This is the design doc's §"L4 — sketch algebra" +//! Drives the full pipeline — PromQL text → pre-ASAP `QueryExpr` +//! (`lower_promql`) → post-ASAP `SummaryExpr` DAG +//! (`asap_aware_mapping::implement_tree`) — and pins the sketch-bound shape +//! node by node, including the `(SummaryKind, SummaryParams)` committed on +//! each edge's schema. This is the design doc's §"L4 — sketch algebra" //! worked example, running for real. use asap_aware_mapping::implement_tree; use asap_frontend_promql::lower_promql; use asap_types::post_asap::{ - L4DataType, L4Schema, SketchQuery, SummaryExpr, SummaryKind, SummaryParams, + SketchQuery, SummaryDataType, SummaryExpr, SummaryKind, SummaryParams, SummarySchema, }; use asap_types::pre_asap::expr_ir::ColumnRef; use asap_types::pre_asap::query_expr::{QueryExpr, Reduction}; use asap_types::pre_asap::schema::DataType; use asap_types::types::AccuracyTarget; -fn dtype<'a>(schema: &'a L4Schema, name: &str) -> &'a L4DataType { +fn dtype<'a>(schema: &'a SummarySchema, name: &str) -> &'a SummaryDataType { &schema .fields .iter() @@ -57,7 +58,7 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() { assert!(matches!(query, SketchQuery::Quantile { q } if *q == 0.99)); assert_eq!( dtype(&root.schema, "quantile_0_99"), - &L4DataType::Primitive(DataType::Float64), + &SummaryDataType::Primitive(DataType::Float64), "the Sketch(…) type must not propagate past the estimate" ); @@ -86,7 +87,7 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() { ); assert_eq!( dtype(&summary_input.schema, "quantile_0_99"), - &L4DataType::Sketch(SummaryKind::Kll, SummaryParams::Kll { k: 200 }) + &SummaryDataType::Sketch(SummaryKind::Kll, SummaryParams::Kll { k: 200 }) ); // The rate: exact counter-reset-aware accumulator, per-series (labels @@ -107,7 +108,7 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() { assert_eq!(reduction, &Reduction::PerEntity); assert_eq!( dtype(&child.schema, "value"), - &L4DataType::Sketch(SummaryKind::Rate, SummaryParams::Rate) + &SummaryDataType::Sketch(SummaryKind::Rate, SummaryParams::Rate) ); assert_eq!( child.schema.time_index, @@ -115,12 +116,12 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() { "per-series keeps the time axis" ); - // The leaf: unrewritten L3 pass-through — TimeRange marker over the Scan. - let SummaryExpr::Logical(l3_leaf) = &leaf.expr else { + // The leaf: unrewritten pass-through — TimeRange marker over the Scan. + let SummaryExpr::Logical(logical_leaf) = &leaf.expr else { panic!("expected Logical leaf, got {:?}", leaf.expr); }; - let QueryExpr::TimeRange { range, child: scan } = l3_leaf.as_ref() else { - panic!("expected TimeRange leaf, got {l3_leaf:?}"); + let QueryExpr::TimeRange { range, child: scan } = logical_leaf.as_ref() else { + panic!("expected TimeRange leaf, got {logical_leaf:?}"); }; assert_eq!(range.as_secs(), 300); assert!(matches!(scan.as_ref(), QueryExpr::Scan { .. })); @@ -128,7 +129,7 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() { leaf.schema .fields .iter() - .all(|f| matches!(f.dtype, L4DataType::Primitive(_))), + .all(|f| matches!(f.dtype, SummaryDataType::Primitive(_))), "logical edges carry only primitive columns" ); } @@ -159,7 +160,7 @@ fn promql_exact_workload_binds_accumulators_not_sketches() { ); assert_eq!( dtype(&root.schema, "job"), - &L4DataType::Primitive(DataType::Utf8), + &SummaryDataType::Primitive(DataType::Utf8), "group keys pass through verbatim" ); diff --git a/crates/types/src/dag_export.rs b/crates/types/src/dag_export.rs index f938a2a4..850eea01 100644 --- a/crates/types/src/dag_export.rs +++ b/crates/types/src/dag_export.rs @@ -1,4 +1,4 @@ -//! Export the L3 [`QueryExpr`] tree as a generic node/edge graph, for tools +//! Export the pre-ASAP [`QueryExpr`] tree as a generic node/edge graph, for tools //! that need to render or diff the IR (the `dag_export` example + the //! `tools/dag-viewer` viewer — see issue #133) rather than walk it in Rust. //! diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 2b00e4d8..6c3fff43 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -1,13 +1,14 @@ //! `asap-types` — shared vocabulary for the whole workspace. //! -//! Merges the former `asap-ir` crate (the L3 intent algebra, workload/batch -//! types, and DAG export) with the data-type-only modules of the former -//! `asap-sketch` crate (the L4 sketch-bound IR types, under [`post_asap`]). +//! Merges the former `asap-ir` crate (the pre-ASAP intent algebra, +//! workload/batch types, and DAG export) with the data-type-only modules of +//! the former `asap-sketch` crate (the post-ASAP sketch-bound IR types, +//! under [`post_asap`]). //! //! - [`pre_asap`] / [`types`] / [`workload`] / [`dag_export`] — the -//! pre-ASAP L3 IR: language-agnostic query intent, independent of any +//! pre-ASAP IR: language-agnostic query intent, independent of any //! sketch decision. -//! - [`post_asap`] — the post-ASAP L4 IR: sketch-bound types +//! - [`post_asap`] — the post-ASAP IR: sketch-bound types //! ([`post_asap::sketch`], [`post_asap::expr`], [`post_asap::schema`]) //! that commit to a concrete `SummaryKind`/`SummaryParams` realization. //! No execution logic lives in this workspace (see issue #190) — a diff --git a/crates/types/src/post_asap/expr.rs b/crates/types/src/post_asap/expr.rs index f5448e73..e7c97eff 100644 --- a/crates/types/src/post_asap/expr.rs +++ b/crates/types/src/post_asap/expr.rs @@ -1,56 +1,57 @@ use std::rc::Rc; -use super::schema::L4Schema; +use super::schema::SummarySchema; use super::sketch::{SketchQuery, SummaryKind, SummaryParams}; use crate::pre_asap::{ColumnRef, QueryExpr, Reduction}; -// ── L4 DAG node ─────────────────────────────────────────────────────────────── +// ── Post-ASAP DAG node ─────────────────────────────────────────────────────── -/// A node in the L4 DAG. Mirrors `L3Node`: wraps the expression and its -/// derived output schema so every edge carries a typed schema. -/// `L4Schema` may contain `L4DataType::Sketch` columns; `L3Schema` cannot. +/// A node in the post-ASAP DAG: wraps the expression and its derived output +/// schema so every edge carries a typed schema. `SummarySchema` may contain +/// `SummaryDataType::Sketch` columns; the pre-ASAP `Schema` cannot. #[derive(Debug, Clone)] -pub struct L4Node { +pub struct SummaryNode { pub expr: SummaryExpr, /// Output schema of `expr` — the schema of the data flowing on the edge /// leading *from* this node to its parent(s). - pub schema: L4Schema, + pub schema: SummarySchema, } -// ── L4 sketch-bound IR ──────────────────────────────────────────────────────── +// ── Post-ASAP sketch-bound IR ──────────────────────────────────────────────── -/// Sketch-bound IR produced by L4 optimizer rules. L4 rules selectively -/// replace logical aggregates and joins in the L3 `QueryExpr` with their -/// sketch-bound counterparts; everything not rewritten passes through as -/// `Logical(Box)`. +/// Sketch-bound IR produced by post-ASAP binding rules. Those rules +/// selectively replace logical aggregates and joins in the pre-ASAP +/// `QueryExpr` with their sketch-bound counterparts; everything not +/// rewritten passes through as `Logical(Box)`. /// /// Traversing from the root node yields a DAG; shared sub-expressions appear -/// as multiple `Rc` references to the same `L4Node`. +/// as multiple `Rc` references to the same `SummaryNode`. #[derive(Debug, Clone)] pub enum SummaryExpr { - /// Any L3 node that no L4 rule rewrote (e.g. `Filter`, `Project`, `Sort`). - /// Output schema is the inner L3 node's schema, lifted to `L4Schema` - /// with all fields as `L4DataType::Primitive`. + /// Any pre-ASAP node no binding rule rewrote (e.g. `Filter`, `Project`, + /// `Sort`). Output schema is the inner node's schema, lifted to + /// `SummarySchema` with all fields as `SummaryDataType::Primitive`. Logical(Box), - /// Summary aggregation. L4 chose `summary` + `params` from the catalog - /// for `AggIntent` under `DeploymentConstraints`. + /// Summary aggregation. Post-ASAP binding chose `summary` + `params` + /// from the catalog for `AggIntent` under `DeploymentConstraints`. /// Output schema: grouping columns (verbatim) + one `Sketch(summary, /// params)` field carrying partial summary state per group. SummaryAgg { - child: Rc, + child: Rc, summary: SummaryKind, params: SummaryParams, /// The column being summarised (fed into the sketch). col: ColumnRef, /// How this aggregation's output rows relate to `child`'s — the - /// same [`Reduction`] the L3 `Aggregate` node it was bound from - /// carried (issue #165), reused verbatim rather than flattened to - /// a bare `Vec`. `Reduction::Reduce(by)` with an empty - /// `by` is a genuine full reduction (merge every candidate into - /// one group); `Reduction::PerEntity` has no grouping concept at - /// all (never merge across entities) — the two collapsed to the - /// same ambiguous `by: []` before this field existed (issue #163). + /// same [`Reduction`] the pre-ASAP `Aggregate` node it was bound + /// from carried (issue #165), reused verbatim rather than + /// flattened to a bare `Vec`. `Reduction::Reduce(by)` + /// with an empty `by` is a genuine full reduction (merge every + /// candidate into one group); `Reduction::PerEntity` has no + /// grouping concept at all (never merge across entities) — the + /// two collapsed to the same ambiguous `by: []` before this field + /// existed (issue #163). reduction: Reduction, }, @@ -59,8 +60,8 @@ pub enum SummaryExpr { /// Output schema: one `Sketch(sketch, params)` field read by a downstream /// `SummaryEstimate`. SummaryJoin { - outer: Rc, - inner: Rc, + outer: Rc, + inner: Rc, key: ColumnRef, summary: SummaryKind, params: SummaryParams, @@ -70,13 +71,16 @@ pub enum SummaryExpr { /// linear-inverse property (CMS, theta, count-based). Catalog flag /// `subtractable` must be true for the sketch family. /// Output schema: one `Sketch(s, p)` field (same family + params as inputs). - SummarySubtract { left: Rc, right: Rc }, + SummarySubtract { + left: Rc, + right: Rc, + }, /// Delete a key from a sketch (CMS update with −1, deletable Bloom /// filter). Catalog flag `deletable` must be true. Output schema = /// input schema unchanged in type (same `Sketch(s, p)` field). SummaryDelete { - summary_input: Rc, + summary_input: Rc, key: ColumnRef, }, @@ -85,14 +89,15 @@ pub enum SummaryExpr { /// schema is a regular row-shaped schema (Float64 for quantile, Int64 /// for count/cardinality, `[(key, count)]` for top-k). SummaryEstimate { - summary_input: Rc, + summary_input: Rc, query: SketchQuery, }, - /// ⊕ — union of sketches across stages / shards. Distinct from L3 - /// `Merge` because sketch union has type constraints: all inputs must - /// agree on `(sketch, params)` and the catalog flag `mergeable` must - /// be true. Inserted by the L5 stage allocator on cut edges. + /// ⊕ — union of sketches across stages / shards. Distinct from the + /// pre-ASAP `Merge` because sketch union has type constraints: all + /// inputs must agree on `(sketch, params)` and the catalog flag + /// `mergeable` must be true. Inserted by a deployment's own stage + /// allocator (not modeled in this crate) on cut edges. /// Output schema: one `Sketch(s, p)` field (same family + params as inputs). - SummaryMerge { children: Vec> }, + SummaryMerge { children: Vec> }, } diff --git a/crates/types/src/post_asap/mod.rs b/crates/types/src/post_asap/mod.rs index a28d5e0b..4097e6b8 100644 --- a/crates/types/src/post_asap/mod.rs +++ b/crates/types/src/post_asap/mod.rs @@ -1,16 +1,16 @@ -//! The post-ASAP L4 IR: sketch-bound types, distinct from -//! [`crate::pre_asap`]'s pre-ASAP L3 IR. +//! The post-ASAP IR: sketch-bound types, distinct from +//! [`crate::pre_asap`]'s pre-ASAP IR. //! -//! Where L3 ([`crate::pre_asap`]) carries *intent* only ("compute a +//! Where [`crate::pre_asap`] carries *intent* only ("compute a //! quantile to ε accuracy"), this module is the sketch-bound IR: the sketch //! kind + parameters are committed ([`sketch::SummaryKind`] / -//! [`sketch::SummaryParams`]), and [`expr::L4Node`] / [`expr::SummaryExpr`] +//! [`sketch::SummaryParams`]), and [`expr::SummaryNode`] / [`expr::SummaryExpr`] //! describe the summary computation. pub mod expr; pub mod schema; pub mod sketch; -pub use expr::{L4Node, SummaryExpr}; -pub use schema::{L4DataType, L4Field, L4Schema}; +pub use expr::{SummaryExpr, SummaryNode}; +pub use schema::{SummaryDataType, SummaryField, SummarySchema}; pub use sketch::{SketchQuery, SummaryKind, SummaryParams}; diff --git a/crates/types/src/post_asap/schema.rs b/crates/types/src/post_asap/schema.rs index 0e4e116e..c4099376 100644 --- a/crates/types/src/post_asap/schema.rs +++ b/crates/types/src/post_asap/schema.rs @@ -1,18 +1,20 @@ use super::sketch::{SummaryKind, SummaryParams}; use crate::pre_asap::DataType; -// ── L4 data types ───────────────────────────────────────────────────────────── +// ── Post-ASAP data types ──────────────────────────────────────────────────── -/// Column types that may appear on an L4 DAG edge. A strict superset of -/// `L3DataType`: L4 adds `Sketch` for edges that carry partial sketch state -/// between a `SummaryAgg` and a downstream `SummaryEstimate` or `SummaryMerge`. +/// Column types that may appear on a post-ASAP DAG edge. A strict superset of +/// the pre-ASAP [`DataType`]: adds `Sketch` for edges that carry partial +/// sketch state between a `SummaryAgg` and a downstream `SummaryEstimate` or +/// `SummaryMerge`. /// /// The `Sketch` variant carries `(kind, params)` so the type system can reject /// merges of incompatible sketches at plan construction time — a `SummaryMerge` /// over `Sketch(Kll, …)` and `Sketch(Cms, …)` inputs is a plan-time error. #[derive(Debug, Clone, PartialEq)] -pub enum L4DataType { - /// Any base L3 column type — passed through unchanged from L3 edges. +pub enum SummaryDataType { + /// Any base pre-ASAP column type — passed through unchanged from + /// pre-ASAP edges. Primitive(DataType), /// Opaque summary state (exact accumulator or approximate sketch). /// The `(kind, params)` pair is the type identity: two summary columns @@ -20,23 +22,23 @@ pub enum L4DataType { Sketch(SummaryKind, SummaryParams), } -// ── L4 schema ───────────────────────────────────────────────────────────────── +// ── Post-ASAP schema ───────────────────────────────────────────────────────── #[derive(Debug, Clone)] -pub struct L4Field { +pub struct SummaryField { pub name: String, - pub dtype: L4DataType, + pub dtype: SummaryDataType, pub nullable: bool, } -/// Schema carried on every edge of the L4 DAG. Extends `L3Schema` with the -/// ability to express sketch-state columns. L3 and L4 schemas are separate -/// types so L3 nodes structurally cannot carry `Sketch`-typed columns — any -/// attempt to do so is a compile-time type error. +/// Schema carried on every edge of the post-ASAP DAG. Extends the pre-ASAP +/// `Schema` with the ability to express sketch-state columns. The two are +/// separate types so a pre-ASAP node structurally cannot carry a +/// `Sketch`-typed column — any attempt to do so is a compile-time type error. #[derive(Debug, Clone)] -pub struct L4Schema { - pub fields: Vec, - /// Index into `fields` for the time axis, if any (same semantics as - /// `L3Schema::time_index`). +pub struct SummarySchema { + pub fields: Vec, + /// Index into `fields` for the time axis, if any (same semantics as the + /// pre-ASAP `Schema::time_index`). pub time_index: Option, } diff --git a/crates/types/src/post_asap/sketch.rs b/crates/types/src/post_asap/sketch.rs index 9b706a03..555e9825 100644 --- a/crates/types/src/post_asap/sketch.rs +++ b/crates/types/src/post_asap/sketch.rs @@ -3,7 +3,7 @@ use crate::pre_asap::ColumnRef; // ── Summary kind identifiers ─────────────────────────────────────────────────── /// Identifies a precomputed aggregation family — either an exact accumulator -/// or an approximate sketch. Used as a type tag in `L4DataType` and as the +/// or an approximate sketch. Used as a type tag in `SummaryDataType` and as the /// binding choice recorded in `SummaryExpr` nodes. /// /// `PartialOrd`/`Ord` (derived, by variant declaration order below): a @@ -67,7 +67,8 @@ impl SummaryKind { /// Concrete, catalog-validated parameters for a specific summary instance. /// The variant must correspond to the associated `SummaryKind`; mismatches -/// are caught at L4 bind time before L5 ever sees the plan. +/// are caught at post-ASAP bind time, before any later, deployment-specific +/// stage ever sees the plan. /// Exact-accumulator variants carry no parameters (their semantics are fixed). #[derive(Debug, Clone, PartialEq)] pub enum SummaryParams { diff --git a/crates/types/src/pre_asap/agg_intent.rs b/crates/types/src/pre_asap/agg_intent.rs index d7701f5f..359f589c 100644 --- a/crates/types/src/pre_asap/agg_intent.rs +++ b/crates/types/src/pre_asap/agg_intent.rs @@ -1,14 +1,14 @@ -//! Layer 3 aggregation-intent vocabulary — "what to compute, not how". +//! The pre-ASAP aggregation-intent vocabulary — "what to compute, not how". //! -//! L3 carries intent ("compute a quantile to ε=0.01 accuracy"); the choice -//! between `HashAgg` / `SortAgg` / `SketchAgg(KLL{k=200})` is an L4 cost-aware -//! decision, not encoded here. +//! This IR carries intent ("compute a quantile to ε=0.01 accuracy"); the +//! choice between `HashAgg` / `SortAgg` / `SketchAgg(KLL{k=200})` is a +//! post-ASAP cost-aware decision, not encoded here. //! //! `AggIntent::TopK` is a first-class *intent*: "the k most frequent keys by //! value, to accuracy ε." Like `Quantile`, the exact-vs-approximate //! realisation — an exact heap / sort+limit when `accuracy: Exact`, a -//! heavy-hitter sketch when approximate — is an L4 cost-aware decision, not -//! encoded here. The semantic distinction that *is* made at lowering is +//! heavy-hitter sketch when approximate — is a post-ASAP cost-aware decision, +//! not encoded here. The semantic distinction that *is* made at lowering is //! intent vs operator: a heavy-hitter aggregate becomes `TopK`, whereas a //! generic `ORDER BY value LIMIT k` stays as the `QueryExpr::Sort + Limit` //! operator pair. @@ -19,7 +19,7 @@ use crate::pre_asap::query_expr::DataModel; use crate::pre_asap::schema::{Column, ColumnId, DataType}; use crate::types::AccuracyTarget; -/// "What to compute" at L3 — the vocabulary the planner pivots on. +/// "What to compute" — the vocabulary the planner pivots on. /// /// Grouping for `TopK` rides on the enclosing `QueryExpr::Aggregate.by` /// (positional `ColumnId`s), like every other aggregate; the intent itself @@ -177,8 +177,8 @@ pub enum AggIntent { // ── Presence functions (issue #47) ─────────────────────────────────── // `absent`/`present_over_time` — the value/emptiness of the argument // determines the output. The empty-result → synthesized-1-sample logic is - // an L4/runtime concern; L3 only marks the operation. Modelled as - // label-preserving so the argument's (matcher-derived) labels — which + // a post-ASAP/runtime concern; this IR only marks the operation. + // Modelled as label-preserving so the argument's (matcher-derived) labels — which // `absent` synthesizes onto its output — stay in the schema. /// PromQL `absent(v)` — a 1-sample vector when the instant vector `v` has no /// matching series, else empty. @@ -234,7 +234,7 @@ pub enum AggIntent { /// A deployment-model-specific intent this core vocabulary doesn't /// (and shouldn't) know the shape of. First step toward issue #131 /// ("add an explicit registration API for extending query / sketch / - /// primitive / data types") applied to L3: rather than growing + /// primitive / data types") applied to this IR: rather than growing /// `AggIntent` for every capability a single deployment model needs /// (which would turn this shared vocabulary into a dumping ground — /// core only grows for intents ≥2 deployment models actually use), @@ -338,8 +338,9 @@ pub enum MathFunc { // `asap_frontend_promql::promql::reduction_for`) — so they stay generic // alongside `input_col`, in one `impl` block. impl AggIntent { - /// Which data model this intent semantically requires. L4 rules consult - /// this to skip non-applicable intents (e.g. `Rate` over a tabular source). + /// Which data model this intent semantically requires. Post-ASAP binding + /// rules consult this to skip non-applicable intents (e.g. `Rate` over a + /// tabular source). pub fn requires(&self) -> DataModel { match self { Self::Rate @@ -451,8 +452,8 @@ impl AggIntent { DataType::Float64, false, ), - // TopK output is a per-row struct/list; modeled as Utf8 at L3 - // (the L4 sketch-bound IR upgrades the dtype). + // TopK output is a per-row struct/list; modeled as Utf8 here + // (the post-ASAP sketch-bound IR upgrades the dtype). AggIntent::TopK { k, .. } => col(&format!("topk_{k}"), DataType::Utf8, false), AggIntent::Cardinality { .. } => col("cardinality", DataType::Int64, false), AggIntent::Rate => col("rate", DataType::Float64, false), @@ -541,7 +542,7 @@ pub enum RankingMeasure { /// Sketchable in principle (weighted SpaceSaving), but no weighted /// heavy-hitter sketch is realised yet, so a `sum`-ranked top-k currently /// stays a generic `Sort + Limit`. Reserved so the axis is explicit; the - /// realisation is L4 sketch selection (issues #6/#33). + /// realisation is post-ASAP sketch selection (issues #6/#33). WeightedSum, /// A non-additive measure (`avg` / `quantile` / `min` / `max`) or a raw, /// un-aggregated value. Never a heavy-hitter — always generic. @@ -583,7 +584,7 @@ pub fn ranking_measure(agg: &AggIntent) -> RankingMeasure { /// - the PromQL front-end gate, on `topk(k, count_over_time(…))` — `descending` /// is the `topk`-vs-`bottomk` choice, `measure` is the inner range function /// (`count_over_time` → `Frequency`, else `NonAdditive`); -/// - the shared L3 canonicalize promotion, on a +/// - the shared canonicalize promotion, on a /// `Limit { Sort { … Aggregate([agg]) } }` — `descending` is the sort key's /// direction, `measure` is [`ranking_measure`] of the ranked aggregate. /// @@ -790,8 +791,8 @@ mod tests { } } - /// `col` is `#[serde(default)]`, so L3 serialized before issue #115 — with - /// no `col` key — still deserializes, as the sample-value convention `None`. + /// `col` is `#[serde(default)]`, so a tree serialized before issue #115 — + /// with no `col` key — still deserializes, as the sample-value convention `None`. #[test] fn agg_intent_serde_reads_pre_115_payloads() { let legacy = r#"{"kind":"quantile","q":0.99,"accuracy":"Exact"}"#; diff --git a/crates/types/src/pre_asap/binder.rs b/crates/types/src/pre_asap/binder.rs index eb8802e2..6feee64d 100644 --- a/crates/types/src/pre_asap/binder.rs +++ b/crates/types/src/pre_asap/binder.rs @@ -1,4 +1,4 @@ -//! The L3 **Binder** — name resolution as an explicit pass. +//! The **Binder** — name resolution as an explicit pass. //! //! [`Binder::bind`] produces the complete, self-contained [`Schema`] every //! `ColumnId` in the canonical tree indexes into. [`resolve`](super::resolve) @@ -12,7 +12,7 @@ //! lands, only the catalog impl swaps. use super::expr_ir::ColumnRef; -use super::query_expr::L2QueryExpr; +use super::query_expr::UnresolvedQueryExpr; use super::schema::{Column, DataType, Schema}; /// The DB / source-schema metadata source — resolves a source (metric / @@ -42,7 +42,7 @@ impl SchemaCatalog for UsageDerivedCatalog { } } -/// The L3 Binder — the explicit name-resolution pass. +/// The explicit name-resolution pass. pub struct Binder { catalog: C, } @@ -71,7 +71,7 @@ impl Binder { /// Contains the time axis, the synthetic `value` column, and one column /// per distinct name referenced anywhere in the tree — so positional /// `ColumnId` resolution downstream is total. - pub fn bind(&self, tree: &L2QueryExpr) -> Schema { + pub fn bind(&self, tree: &UnresolvedQueryExpr) -> Schema { self.bind_with_inherited(tree, &[]) } @@ -81,7 +81,7 @@ impl Binder { /// own sub-tree) still sees an outer aggregate's group keys — e.g. the /// `__name__` / `job` in `sum by (__name__)(a or b)`, which appear in neither /// side's own matchers (issue #52). - pub fn bind_with_inherited(&self, tree: &L2QueryExpr, inherited: &[String]) -> Schema { + pub fn bind_with_inherited(&self, tree: &UnresolvedQueryExpr, inherited: &[String]) -> Schema { let mut columns: Vec = leftmost_scan_name(tree) .and_then(|name| self.catalog.columns_for(name)) .unwrap_or_else(default_leaf_columns); @@ -132,12 +132,12 @@ fn push_ref_name(c: &ColumnRef, out: &mut Vec) { } } -/// The leftmost `Scan`'s source name in a canonical (`L2QueryExpr`) tree — +/// The leftmost `Scan`'s source name in a canonical (`UnresolvedQueryExpr`) tree — /// the [`collect_referenced_columns`] counterpart to what a dedicated /// `Source` leaf type would carry as a method; the canonical tree's `Scan` /// leaf needs this walk written out instead. -fn leftmost_scan_name(tree: &L2QueryExpr) -> Option<&str> { - use L2QueryExpr as QE; +fn leftmost_scan_name(tree: &UnresolvedQueryExpr) -> Option<&str> { + use UnresolvedQueryExpr as QE; match tree { QE::Scan { source, .. } => Some(match source { super::query_expr::Source::TimeSeries { metric } => metric.as_str(), @@ -191,9 +191,9 @@ fn leftmost_scan_name(tree: &L2QueryExpr) -> Option<&str> { /// `WindowFunc.args`/`partition_by`/`order_by`, `Join.pred`, `Relabel.value`. /// The Binder seeds these into the usage-derived leaf so positional /// resolution downstream is total. -pub(crate) fn collect_referenced_columns(tree: &L2QueryExpr) -> Vec { - use L2QueryExpr as QE; - fn named(expr: &L2QueryExpr, out: &mut Vec) { +pub(crate) fn collect_referenced_columns(tree: &UnresolvedQueryExpr) -> Vec { + use UnresolvedQueryExpr as QE; + fn named(expr: &UnresolvedQueryExpr, out: &mut Vec) { for c in expr.columns_referenced() { push_ref_name(c, out); } @@ -211,7 +211,7 @@ pub(crate) fn collect_referenced_columns(tree: &L2QueryExpr) -> Vec { opt_ref(&m.input_col(), out); } } - fn walk(node: &L2QueryExpr, out: &mut Vec) { + fn walk(node: &UnresolvedQueryExpr, out: &mut Vec) { match node { QE::Scan { predicates, .. } => { for super::query_expr::Predicate(p) in predicates { @@ -339,8 +339,8 @@ mod tests { use super::super::query_expr::{GroupKeys, Source}; use super::*; - fn src(name: &str) -> L2QueryExpr { - L2QueryExpr::Scan { + fn src(name: &str) -> UnresolvedQueryExpr { + UnresolvedQueryExpr::Scan { source: Source::TimeSeries { metric: name.into(), }, @@ -362,9 +362,9 @@ mod tests { fn sort_partition_keys_land_in_schema() { // Per-group ranking keys (`topk by (host)` → `Sort.partition_by`) must be // seeded into the usage-derived leaf so they resolve positionally. - let tree = L2QueryExpr::Sort { + let tree = UnresolvedQueryExpr::Sort { keys: vec![super::super::query_expr::SortKey { - expr: L2QueryExpr::Column(ColumnRef::SampleValue), + expr: UnresolvedQueryExpr::Column(ColumnRef::SampleValue), ascending: false, nulls_first: false, }], diff --git a/crates/types/src/pre_asap/canonicalize.rs b/crates/types/src/pre_asap/canonicalize.rs index 125d55e4..216ea803 100644 --- a/crates/types/src/pre_asap/canonicalize.rs +++ b/crates/types/src/pre_asap/canonicalize.rs @@ -1,10 +1,10 @@ -//! Shared post-lowering canonicalization of the L3 [`QueryExpr`]. +//! Shared post-lowering canonicalization of the resolved [`QueryExpr`]. //! //! Both language front ends funnel through [`resolve_root`](super::resolve::resolve_root), //! which runs this pass over the resolved tree. Its job is to erase -//! *structural* differences between semantically identical queries so an L4 rule -//! matching on the intent algebra sees one canonical spelling regardless of -//! source language (issue #34). +//! *structural* differences between semantically identical queries so a +//! post-ASAP binding rule matching on the intent algebra sees one canonical +//! spelling regardless of source language (issue #34). //! //! ## Heavy-hitter promotion //! diff --git a/crates/types/src/pre_asap/column_resolution.rs b/crates/types/src/pre_asap/column_resolution.rs index 35fbc149..70113042 100644 --- a/crates/types/src/pre_asap/column_resolution.rs +++ b/crates/types/src/pre_asap/column_resolution.rs @@ -12,8 +12,8 @@ use thiserror::Error; use super::agg_intent::AggIntent; use super::expr_ir::ColumnRef; use super::query_expr::{ - aggregate_output_schema, GroupKeys, L2QueryExpr, L3QueryExpr, QueryExpr, QueryExprError, - Reduction, + aggregate_output_schema, GroupKeys, QueryExpr, QueryExprError, Reduction, ResolvedQueryExpr, + UnresolvedQueryExpr, }; use super::schema::{ColumnId, DataType, Schema}; @@ -110,16 +110,19 @@ pub fn resolve_group_keys_promql( .collect() } -/// Resolve a name-based scalar [`L2QueryExpr`] (one of `QueryExpr`'s scalar -/// variants, issue #205) into a positional [`L3QueryExpr`] by resolving every +/// Resolve a name-based scalar [`UnresolvedQueryExpr`] (one of `QueryExpr`'s scalar +/// variants, issue #205) into a positional [`ResolvedQueryExpr`] by resolving every /// column reference against `schema`. Structural otherwise. `expr` must be /// one of the scalar variants — an operator variant here is a construction /// bug, not a shape this needs to handle silently. -pub fn resolve_expr(expr: &L2QueryExpr, schema: &Schema) -> Result { - let boxed = |e: &L2QueryExpr| -> Result, ResolveError> { +pub fn resolve_expr( + expr: &UnresolvedQueryExpr, + schema: &Schema, +) -> Result { + let boxed = |e: &UnresolvedQueryExpr| -> Result, ResolveError> { Ok(Box::new(resolve_expr(e, schema)?)) }; - let each = |es: &[L2QueryExpr]| -> Result, ResolveError> { + let each = |es: &[UnresolvedQueryExpr]| -> Result, ResolveError> { es.iter().map(|e| resolve_expr(e, schema)).collect() }; Ok(match expr { @@ -344,7 +347,7 @@ mod tests { // for the same aggregate. Before the dedup this diverged on a per-series // reduction — the HAVING mirror lacked the per-series branch and would // collapse `[ts, value]` to a single `rate` column. - use crate::pre_asap::query_expr::{QueryExpr as L3, Source}; + use crate::pre_asap::query_expr::Source; use std::time::Duration; let leaf_schema = Schema::with_time_index( @@ -355,19 +358,19 @@ mod tests { 0, vec![], ); - let scan = L3::Scan { + let scan = QueryExpr::Scan { source: Source::TimeSeries { metric: "m".into() }, predicates: vec![], schema: leaf_schema.clone(), }; // Aggregate{ reduction: PerEntity, [Rate], child: TimeRange{ Scan } } — // a per-series reduction (label-preserving). - let agg = L3::Aggregate { + let agg = QueryExpr::Aggregate { reduction: Reduction::PerEntity, measures: vec![AggIntent::Rate], output_names: vec![], having: None, - child: Box::new(L3::TimeRange { + child: Box::new(QueryExpr::TimeRange { range: Duration::from_secs(300), child: Box::new(scan), }), diff --git a/crates/types/src/pre_asap/mod.rs b/crates/types/src/pre_asap/mod.rs index 93eefd14..dd5c849a 100644 --- a/crates/types/src/pre_asap/mod.rs +++ b/crates/types/src/pre_asap/mod.rs @@ -1,20 +1,20 @@ -//! Layer 3 — the canonical intent algebra IR. +//! The canonical pre-ASAP intent algebra IR. //! -//! - [`query_expr`] — the canonical, language- and deployment-independent L3 +//! - [`query_expr`] — the canonical, language- and deployment-independent //! intent algebra: one recursive [`QueryExpr`] tree (relational operators //! *and* scalar expression shapes both, since issue #205) + [`AggIntent`], //! generic over the column-reference state (positional [`ColumnId`] once //! bound, name-based [`ColumnRef`] before). -//! - [`agg_intent`] — the L3 aggregation-intent vocabulary. +//! - [`agg_intent`] — the aggregation-intent vocabulary. //! - [`expr_ir`] — the [`ColumnRef`] column-reference type and the scalar //! operator/literal vocabulary ([`ScalarValue`], [`CompareOp`], [`ArithOp`]) //! [`QueryExpr`]'s scalar variants are built from. -//! - [`schema`] — the per-edge [`Schema`] every L3 node carries. +//! - [`schema`] — the per-edge [`Schema`] every node carries. //! - [`binder`] / [`column_resolution`] — name resolution: turn a `ColumnRef` //! into a positional `ColumnId` against an in-scope [`Schema`]. -//! - [`resolve`] — binds a whole front-end-emitted [`L2QueryExpr`] tree to -//! canonical [`L3QueryExpr`] (issue #179): both front ends -//! (`asap-frontend-promql`, `asap-frontend-sql`) construct `L2QueryExpr` +//! - [`resolve`] — binds a whole front-end-emitted [`UnresolvedQueryExpr`] tree to +//! canonical [`ResolvedQueryExpr`] (issue #179): both front ends +//! (`asap-frontend-promql`, `asap-frontend-sql`) construct `UnresolvedQueryExpr` //! directly during their own `interpret` step and call //! [`resolve_root`] on the result — there is no separate per-language //! relational tree or converter anymore. @@ -48,9 +48,9 @@ pub use column_resolution::{ pub use expr_ir::{ArithOp, ColumnRef, CompareOp, ScalarValue}; pub use query_expr::{ aggregate_output_schema, AtModifier, BinaryOpKind, ColState, DataModel, GroupKeys, GroupSide, - InfoMatcher, JoinKind, L2QueryExpr, L3QueryExpr, Predicate, ProjectItem, QueryExpr, - QueryExprError, Reduction, SampleKind, SetOpKind, SortKey, Source, TimeShift, VectorGrouping, - VectorMatch, VectorMatchKind, WindowFuncKind, + InfoMatcher, JoinKind, Predicate, ProjectItem, QueryExpr, QueryExprError, Reduction, + ResolvedQueryExpr, SampleKind, SetOpKind, SortKey, Source, TimeShift, UnresolvedQueryExpr, + VectorGrouping, VectorMatch, VectorMatchKind, WindowFuncKind, }; pub use resolve::{resolve_root, ResolveTreeError}; pub use schema::{Column, ColumnId, DataType, Schema}; diff --git a/crates/types/src/pre_asap/query_expr.rs b/crates/types/src/pre_asap/query_expr.rs index 37b224a7..5ee9addf 100644 --- a/crates/types/src/pre_asap/query_expr.rs +++ b/crates/types/src/pre_asap/query_expr.rs @@ -1,4 +1,4 @@ -//! The canonical Layer-3 intent algebra IR. +//! The canonical pre-ASAP intent algebra IR. //! //! Language- and deployment-independent. Box-owned tree; column identity is **positional** //! (`Aggregate.reduction: Reduction`, wrapping `GroupKeys` for the @@ -58,7 +58,7 @@ pub enum QueryExprError { // ── Leaf / supporting types ─────────────────────────────────────────────────── -/// Positional grouping keys, shared by every "operate per group" L3 operator: +/// Positional grouping keys, shared by every "operate per group" operator: /// `Aggregate.by` (reduce per group), `Sort.partition_by` (rank per group — /// including generic `topk`/`bottomk`), and `WindowFunc.partition_by` (window /// per group). One spelling so grouping has a single home to evolve. Empty @@ -327,7 +327,7 @@ pub enum WindowFuncKind { /// [`QueryExpr::InfoJoin`] (issue #84). Unlike a `Scan` predicate it is not /// resolved positionally — it references the info metric's labels (`__name__` /// picks the metric, the rest constrain data labels), which aren't in the input -/// vector's schema; L4 applies it against the info metric. +/// vector's schema; the post-ASAP binder applies it against the info metric. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct InfoMatcher { pub label: String, @@ -437,12 +437,12 @@ pub struct ProjectItem { pub expr: QueryExpr, } -// ── L3 intent algebra IR ────────────────────────────────────────────────────── +// ── Intent algebra IR ──────────────────────────────────────────────────────── /// What kind of computation an `Aggregate` node performs — orthogonal to /// *which* columns it groups by (that's still [`GroupKeys`], inside /// `Reduce`). Explicit, decided once by whichever pass constructs the node -/// (structural, at L2→L3 lowering), rather than inferred downstream from +/// (structural, at front-end lowering time), rather than inferred downstream from /// whether a grouping-key list happens to be empty or from a neighboring /// node's shape. See design proposal #165. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -555,8 +555,9 @@ pub enum QueryExpr { /// metric(s), the rest constrain the data labels), joined on their shared /// identifying labels. Those join keys are the info metric's identifying /// labels — runtime/metadata-resolved, since an open PromQL schema can't - /// enumerate them — so they are NOT carried here; L4 resolves them from the - /// info metric's schema. The output keeps `child`'s (open) schema: the + /// enumerate them — so they are NOT carried here; the post-ASAP binder + /// resolves them from the info metric's schema. The output keeps + /// `child`'s (open) schema: the /// grafted labels appear at runtime. InfoJoin { #[serde(default)] @@ -607,8 +608,8 @@ pub enum QueryExpr { child: Box>, }, - /// δ — SQL `DISTINCT` / row deduplication. Positional like every other L3 - /// column reference; empty = dedup on all columns (`SELECT DISTINCT *`). + /// δ — SQL `DISTINCT` / row deduplication. Positional like every other + /// column reference here; empty = dedup on all columns (`SELECT DISTINCT *`). Distinct { cols: Vec, child: Box>, @@ -635,7 +636,7 @@ pub enum QueryExpr { /// empty relation: there would be no schema to derive. Merge { children: Vec> }, - /// Logical join. L4 picks the physical alternative. + /// Logical join. Post-ASAP binding picks the physical alternative. Join { kind: JoinKind, pred: Predicate, @@ -657,7 +658,8 @@ pub enum QueryExpr { /// row-preserving (schema pass-through) and is where the grouping of a /// generic (non-heavy-hitter) ranking lives, so there is no separate /// `Partition` node (issue #12: reducing GROUP BY → `Aggregate.by`, per-group - /// ranking → here, parallel sharding → L5). Empty = a global order-by. + /// ranking → here, parallel sharding → a deployment's own physical + /// stage). Empty = a global order-by. Sort { keys: Vec>, #[serde(default)] @@ -821,7 +823,8 @@ impl QueryExpr { /// Recursively collect every column reference in a **scalar** subtree — /// used by the [`Binder`](super::binder::Binder) to seed usage-derived - /// leaf schemas, and available to L4 for column-lineage / selectivity. + /// leaf schemas, and available to post-ASAP binding for column-lineage / + /// selectivity. /// `self` must be one of the scalar variants (see the module doc on /// [`QueryExpr`]'s scalar shapes) — every caller already only reaches /// this through a scalar-typed position (`Predicate`, `ProjectItem.expr`, @@ -876,18 +879,18 @@ impl QueryExpr { } } -/// The canonical, positional Layer-3 tree — what the bare `QueryExpr` name has -/// always meant (the default `C = ColumnId`). Every existing consumer keeps -/// using `QueryExpr` unparameterized; this alias exists only to name the -/// resolved state explicitly at a use site that also wants to name -/// [`L2QueryExpr`] nearby. -pub type L3QueryExpr = QueryExpr; +/// The canonical, positional, resolved tree — what the bare `QueryExpr` name +/// has always meant (the default `C = ColumnId`). Every existing consumer +/// keeps using `QueryExpr` unparameterized; this alias exists only to name +/// the resolved state explicitly at a use site that also wants to name +/// [`UnresolvedQueryExpr`] nearby. +pub type ResolvedQueryExpr = QueryExpr; -/// The front-end-emitted, name-based Layer-2 tree — `QueryExpr`, -/// unresolved: front ends construct this directly during their own `interpret` -/// step (issue #179), and the [`Binder`](super::binder) resolves it into -/// [`L3QueryExpr`]. -pub type L2QueryExpr = QueryExpr; +/// The front-end-emitted, name-based, unresolved tree — +/// `QueryExpr`: front ends construct this directly during their +/// own `interpret` step (issue #179), and the [`Binder`](super::binder) +/// resolves it into [`ResolvedQueryExpr`]. +pub type UnresolvedQueryExpr = QueryExpr; // `output_schema` needs a fully bound tree — it reads `Scan.schema` as a plain // `Schema` and resolves every scalar `Expr::Column` positionally — so it lives @@ -1346,9 +1349,9 @@ fn without_output_schema( } /// Infer the `(DataType, nullable)` a scalar [`QueryExpr`] produces against an -/// input [`Schema`]. Used by `Project` schema derivation. Approximate at L3: +/// input [`Schema`]. Used by `Project` schema derivation. Approximate here: /// unknown columns and bare `FunctionCall`s fall back to a permissive default -/// (the L4/emit layer refines with a real function/type registry). `expr` +/// (post-ASAP binding refines with a real function/type registry). `expr` /// must be one of the scalar variants (issue #205) — an operator variant here /// is a construction bug, not a shape this needs to handle silently. fn infer_expr_type(expr: &QueryExpr, schema: &Schema) -> (DataType, bool) { @@ -1387,7 +1390,7 @@ fn infer_expr_type(expr: &QueryExpr, schema: &Schema) -> (DataType, bo let (_, nullable) = infer_expr_type(expr, schema); (to.clone(), *try_cast || nullable) } - // No function/type registry at L3 — default permissive. + // No function/type registry here — default permissive. QueryExpr::FunctionCall { .. } => (DataType::Float64, true), QueryExpr::Case { branches, diff --git a/crates/types/src/pre_asap/resolve.rs b/crates/types/src/pre_asap/resolve.rs index a477e0f0..5ca7c5a8 100644 --- a/crates/types/src/pre_asap/resolve.rs +++ b/crates/types/src/pre_asap/resolve.rs @@ -1,5 +1,5 @@ -//! Resolve a front-end-emitted, unresolved [`L2QueryExpr`] (`QueryExpr`) -//! into the canonical, positional [`L3QueryExpr`] (`QueryExpr`). +//! Resolve a front-end-emitted, unresolved [`UnresolvedQueryExpr`] (`QueryExpr`) +//! into the canonical, positional [`ResolvedQueryExpr`] (`QueryExpr`). //! //! Both front ends (`asap-frontend-promql`, `asap-frontend-sql`) construct //! canonical `QueryExpr` shapes directly during their own `interpret` step @@ -8,8 +8,8 @@ //! *structural* decision happen right there, since a front end already knows //! the answer at parse time. What's left for [`resolve_root`] is exactly the //! "mechanical, schema-dependent substitution" #179 describes: a single -//! generic, shape-preserving walk — every [`L2QueryExpr`] variant maps to the -//! identical [`L3QueryExpr`] variant — that resolves every [`ColumnRef`] to +//! generic, shape-preserving walk — every [`UnresolvedQueryExpr`] variant maps to the +//! identical [`ResolvedQueryExpr`] variant — that resolves every [`ColumnRef`] to //! the [`Binder`](super::binder::Binder)-computed positional [`ColumnId`]. use thiserror::Error; @@ -21,12 +21,12 @@ use super::column_resolution::{ }; use super::expr_ir::ColumnRef; use super::query_expr::{ - aggregate_output_schema, GroupKeys, L2QueryExpr, L3QueryExpr, Predicate, ProjectItem, - QueryExprError, Reduction, SortKey, + aggregate_output_schema, GroupKeys, Predicate, ProjectItem, QueryExprError, Reduction, + ResolvedQueryExpr, SortKey, UnresolvedQueryExpr, }; use super::schema::{ColumnId, Schema}; -/// Errors from resolving a canonical, unresolved [`L2QueryExpr`] tree. +/// Errors from resolving a canonical, unresolved [`UnresolvedQueryExpr`] tree. #[derive(Debug, Error)] pub enum ResolveTreeError { /// A column reference did not resolve against its in-scope schema. @@ -38,20 +38,20 @@ pub enum ResolveTreeError { Schema(#[from] QueryExprError), } -/// Resolve a whole [`L2QueryExpr`] tree rooted at `tree` into canonical -/// [`L3QueryExpr`]: binds every `ColumnRef` to a `ColumnId` via the +/// Resolve a whole [`UnresolvedQueryExpr`] tree rooted at `tree` into canonical +/// [`ResolvedQueryExpr`]: binds every `ColumnRef` to a `ColumnId` via the /// [`Binder`], then [`canonicalize`](super::canonicalize::canonicalize)s the /// result. -pub fn resolve_root(tree: &L2QueryExpr) -> Result { +pub fn resolve_root(tree: &UnresolvedQueryExpr) -> Result { resolve_root_with_inherited(tree, &[]) } /// [`resolve_root`] with label names inherited from an enclosing scope seeded /// into the leaf schema, used when re-binding a `BinaryOp` side (issue #52). fn resolve_root_with_inherited( - tree: &L2QueryExpr, + tree: &UnresolvedQueryExpr, inherited: &[String], -) -> Result { +) -> Result { let fallback = Binder::new().bind_with_inherited(tree, inherited); let l3 = resolve(tree, &fallback)?; Ok(super::canonicalize::canonicalize(l3)) @@ -61,7 +61,10 @@ fn resolve_root_with_inherited( /// resolves this node's own `ColumnRef`s against the *converted child's* /// derived output schema — so a `JOIN`'s concatenated schema and a cross- /// series aggregate's frozen-closed output bind to the right positions. -fn resolve(tree: &L2QueryExpr, fallback: &Schema) -> Result { +fn resolve( + tree: &UnresolvedQueryExpr, + fallback: &Schema, +) -> Result { use super::query_expr::QueryExpr as QE; Ok(match tree { QE::Scan { diff --git a/crates/types/src/pre_asap/schema.rs b/crates/types/src/pre_asap/schema.rs index 46db0eb7..2cf8bb5c 100644 --- a/crates/types/src/pre_asap/schema.rs +++ b/crates/types/src/pre_asap/schema.rs @@ -1,9 +1,10 @@ -//! Layer 3 schema flow — every L3 edge carries a typed `Schema`. +//! Pre-ASAP IR schema flow — every edge carries a typed `Schema`. //! //! Per `control_plane/docs/design.md` §6 "Schema flow — every L3 edge carries -//! a typed schema". The DAG is type-checked: a node's output schema is a -//! function of its inputs and parameters and is verifiable independently -//! of the surrounding context. +//! a typed schema" (that doc's own layer numbering; this crate no longer uses +//! it). The DAG is type-checked: a node's output schema is a function of its +//! inputs and parameters and is verifiable independently of the surrounding +//! context. //! //! `Schema::unique_keys` is metadata for reuse-aware planning: a producer's //! output can only be safely shared across consumers when its row identity @@ -11,7 +12,7 @@ //! //! Single-query plans don't read this field; it lives here so the metadata //! is available the moment workload-aware planning lands without requiring -//! an L3-wide schema change. +//! a pre-ASAP-IR-wide schema change. #![allow(dead_code)] @@ -33,9 +34,9 @@ pub struct Column { /// produce label-name + the synthetic `value` / `timestamp` columns; /// SQL leaves carry their `information_schema` names. pub name: String, - /// Column data type. Kept narrow at L3 (`Int64` / `Float64` / `Utf8` - /// / `Bool` / `Timestamp`); `Sketch(...)` is an L4-only addition per - /// design.md §6.4 and is intentionally absent here. + /// Column data type. Kept narrow here (`Int64` / `Float64` / `Utf8` / + /// `Bool` / `Timestamp`); `Sketch(...)` is a post-ASAP-only addition + /// (design.md §6.4) and is intentionally absent here. pub dtype: DataType, /// Whether NULL values are allowed in this column. PromQL value /// columns are non-nullable; SQL columns inherit their DDL nullability. @@ -66,8 +67,8 @@ impl Column { } } -/// L3 column data types. Deliberately narrow: no sketch state at this -/// layer (see `design.md` §6.4 for the L4 `DataType::Sketch(...)` +/// Pre-ASAP IR column data types. Deliberately narrow: no sketch state at +/// this layer (see `design.md` §6.4 for the post-ASAP `DataType::Sketch(...)` /// extension). #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -85,7 +86,7 @@ pub enum DataType { Timestamp, } -/// Per-edge L3 schema. Flowing between any two L3 operators, on every +/// Per-edge pre-ASAP IR schema. Flowing between any two operators, on every /// node's input and output. /// /// `unique_keys` is metadata for reuse-aware planning: each inner `Vec` @@ -96,8 +97,8 @@ pub enum DataType { /// adds `cols`; most other nodes pass through. /// /// **Consumed by**: a future workload-level reuse pass (not yet shipped). -/// The single-query path, the `Bind*` rules, push-down, and L5 emitters do -/// not read this field. +/// The single-query path, the `Bind*` rules, push-down, and a deployment's +/// own physical emitters do not read this field. #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] pub struct Schema { /// Columns flowing on this edge, in positional order. diff --git a/crates/types/src/workload.rs b/crates/types/src/workload.rs index eabae324..9fbc98f8 100644 --- a/crates/types/src/workload.rs +++ b/crates/types/src/workload.rs @@ -11,7 +11,7 @@ pub struct Query(pub String); pub struct RepetitionInterval(pub u32); /// SQL dialect variant — different dialects have different syntax and -/// function sets that affect how the query string is parsed at L1. +/// function sets that affect how the query string is parsed. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SqlDialect { DataFusionSQL,