Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"crates/ir",
"crates/l2",
"crates/sketch",
"crates/plan",
"crates/frontend-promql",
Expand Down
45 changes: 24 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,38 +39,40 @@ runtime coupling.

| Crate | Role | Depends on | ~LOC |
|---|---|---|---|
| **`asap-ir`** | L2 relational + L3 canonical IR + L2→L3 converter, binder, resolution, schema, expr, workload types | *(nothing — no query-language deps)* | 3,900 |
| **`asap-ir`** | **L3 canonical IR only** — `QueryExpr` + `AggIntent` + scalar expr IR + schema + names | *(nothing — no query-language deps)* | 2,300 |
| `asap-l2` | L2 per-language relational algebra + the L2→L3 converter (`convert_root`, binder, column resolution) | `asap-ir` | 1,600 |
| `asap-sketch` | L4 sketch-bound IR (`SketchExpr`) | `asap-ir` | 240 |
| `asap-plan` | optimizer layer — CSE (landed); cost-model / boundary / canonicalize (stubs) | `asap-ir` | 290 |
| `asap-frontend-promql` | PromQL L1→L2 | `asap-ir`, **promql-parser** | 1,030 |
| `asap-frontend-sql` | SQL L1→L2 | `asap-ir`, **datafusion** | 1,130 |
| `asap-frontend-promql` | PromQL L1→L2 | `asap-ir`, `asap-l2`, **promql-parser** | 1,030 |
| `asap-frontend-sql` | SQL L1→L2 | `asap-ir`, `asap-l2`, **datafusion** | 1,130 |
| `asap-lower` | facade re-exporting both front ends | the two `frontend-*` crates | 15 |
| `asap-e2e` | cross-language integration tests | `asap-frontend-promql` | 40 |

Splitting the front ends **quarantines their parsers**: a caller that needs
only PromQL depends on `asap-frontend-promql` and never compiles DataFusion,
and vice-versa (verified with `cargo tree`). `asap-lower` is the convenience
facade for callers that want both.
Two isolation wins fall out of this:
- **The front ends quarantine their parsers** — a caller that needs only PromQL depends on `asap-frontend-promql` and never compiles DataFusion, and vice-versa (verified with `cargo tree`). `asap-lower` is the facade for callers that want both.
- **L3-only consumers stay lean** — `asap-sketch` and `asap-plan` depend on `asap-ir` alone, so they never pull the L2 relational tree, the converter, or the binder (`asap-l2`). Only the front ends, which actually *lower* queries, need `asap-l2`.

### Directory structure

```
crates/
├── ir/ # asap-ir — the shared IR (largest crate)
├── ir/ # asap-ir — L3 canonical IR (the shared vocabulary)
│ └── src/
│ ├── lib.rs
│ ├── types.rs # AccuracyTarget, …
│ ├── workload.rs # QueryWorkload / QueryLanguage / SqlDialect (front-end input)
│ └── intent_algebra/
│ ├── relational.rs # L2: per-language relational tree the front ends emit
│ ├── query_expr.rs # L3: canonical QueryExpr — the IR everything pivots on
│ ├── agg_intent.rs # L3: AggIntent vocabulary (Sum/Quantile/Rate/TopK/…)
│ ├── expr_ir.rs # scalar expr IR (L2Expr / L3Expr)
│ ├── lower.rs # L2→L3 converter (convert_root)
│ ├── binder.rs # positional name-resolution seed
│ ├── column_resolution.rs
│ ├── expr_ir.rs # scalar expr IR (L2Expr / L3Expr / ColumnRef)
│ ├── schema.rs # per-edge Schema + unique-keys
│ └── names.rs
│ └── names.rs # BindingName / QueryId
├── l2/ # asap-l2 — L2 relational algebra + L2→L3 converter
│ └── src/
│ ├── relational.rs # L2: per-language relational tree the front ends emit
│ ├── lower.rs # L2→L3 converter (convert_root)
│ ├── binder.rs # positional name-resolution seed
│ └── column_resolution.rs
├── sketch/ # asap-sketch — L4 sketch IR: expr / schema / sketch
├── plan/ # asap-plan — optimizer: cse.rs (landed)
│ └── src/ # + cost_model.rs / boundary.rs / canonicalize.rs (stubs)
Expand All @@ -83,13 +85,14 @@ crates/
# deployment-model-* crates, control-proto, and the bin/ entrypoints.
```

**Why `asap-ir` holds the most.** L2 (the per-language relational tree) and L3
(the canonical intent algebra) live in one crate because the **L2→L3 converter
needs both** — front ends only *emit* L2, they don't own it. Its modules
(`relational` / `query_expr` / `agg_intent` / `schema` / `binder` / …) are
cleanly separated and could split into their own crates later if the crate
grows unwieldy; for now they share one compilation unit to keep the converter's
tight coupling in-crate.
**Why L2 and L3 are separate crates.** `asap-ir` is the canonical L3 IR — the
vocabulary every downstream layer pivots on. The L2 relational tree and the
L2→L3 converter live in `asap-l2` because only the *front ends* need them: they
emit L2 and call `convert_root`. Keeping them out of `asap-ir` means the
optimizer (`asap-plan`), the sketch IR (`asap-sketch`), and any future
L3-consuming layer compile against a lean core without the converter/binder
machinery. (The converter co-locates with L2 rather than L3 because it owns the
L2 tree definition and only *reads* L3.)

*(Planned.)* A new deployment model will land by adding one crate with `rules.rs` (pick L4 rules) + `topology.rs` + an emitter, plus one line in `bin/asap-controller/main.rs` — no changes to the IR crates.

Expand Down
1 change: 1 addition & 0 deletions crates/frontend-promql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"
# in asap-ir. Pulls the PromQL parser only — never DataFusion.
[dependencies]
asap-ir = { path = "../ir" }
asap-l2 = { path = "../l2" }

# Private mirror of GreptimeTeam/promql-parser (Apache-2.0). `main` tracks
# upstream; our `asap` branch carries the local patches.
Expand Down
2 changes: 1 addition & 1 deletion crates/frontend-promql/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::fmt;

use asap_ir::intent_algebra::ConvertError;
use asap_l2::ConvertError;

/// Errors from lowering a PromQL query (L1 parse → L2 → shared L2→L3 convert).
///
Expand Down
9 changes: 5 additions & 4 deletions crates/frontend-promql/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
//! PromQL front end: L1 (parse via `promql-parser`) → L2 relational, then the
//! shared L2→L3 [`convert_root`](asap_ir::intent_algebra::convert_root).
//! shared L2→L3 [`convert_root`](asap_l2::convert_root).
//!
//! Emits the per-language
//! [`relational::QueryExpr`](asap_ir::intent_algebra::relational); the shared
//! converter runs the [`Binder`](asap_ir::intent_algebra::Binder) for
//! [`relational::QueryExpr`](asap_l2::relational); the shared
//! converter runs the [`Binder`](asap_l2::Binder) for
//! positional name resolution. Depends on the PromQL parser only — never on the
//! SQL / DataFusion stack.

pub mod error;
pub mod promql;

use asap_ir::intent_algebra::{convert_root, QueryExpr};
use asap_ir::intent_algebra::QueryExpr;
use asap_l2::convert_root;
use asap_ir::types::AccuracyTarget;
use asap_ir::workload::{QueryLanguage, QueryWorkload};

Expand Down
4 changes: 2 additions & 2 deletions crates/frontend-promql/src/promql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! - **L2 (per-language tree)** is built here: the walk interprets PromQL
//! semantics (range vectors, aggregate operators, label matchers) and emits
//! the language-flavored [`relational::QueryExpr`] the controller's L2→L3
//! converter ([`convert_root`](asap_ir::intent_algebra::convert_root))
//! converter ([`convert_root`](asap_l2::convert_root))
//! consumes. Canonicalisation (window-over-aggregate fold, GROUP-BY →
//! positional `Aggregate.by`, positional name binding) happens in that
//! converter, not here.
Expand Down Expand Up @@ -44,7 +44,7 @@ use promql_parser::parser::{
use asap_ir::intent_algebra::query_expr::{
BinaryOpKind, GroupSide, VectorGrouping, VectorMatch, VectorMatchKind,
};
use asap_ir::intent_algebra::relational::{
use asap_l2::relational::{
AggFunc, AggItem, L2SortKey, QueryExpr as L2, SourceSpec,
};
use asap_ir::intent_algebra::{ArithOp, ColumnRef, CompareOp, L2Expr, L3Scalar};
Expand Down
1 change: 1 addition & 0 deletions crates/frontend-sql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"
# shared L2→L3 converter in asap-ir. Pulls DataFusion only — never promql-parser.
[dependencies]
asap-ir = { path = "../ir" }
asap-l2 = { path = "../l2" }
datafusion = "43"

[dev-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion crates/frontend-sql/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::fmt;

use asap_ir::intent_algebra::ConvertError;
use asap_l2::ConvertError;

/// Errors from lowering a SQL query (L1 parse + plan via DataFusion → L2 →
/// shared L2→L3 convert).
Expand Down
9 changes: 5 additions & 4 deletions crates/frontend-sql/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
//! SQL front end: L1 (parse + plan via DataFusion) → L2 relational, then the
//! shared L2→L3 [`convert_root`](asap_ir::intent_algebra::convert_root).
//! shared L2→L3 [`convert_root`](asap_l2::convert_root).
//!
//! Emits the per-language
//! [`relational::QueryExpr`](asap_ir::intent_algebra::relational); the shared
//! converter runs the [`Binder`](asap_ir::intent_algebra::Binder) for
//! [`relational::QueryExpr`](asap_l2::relational); the shared
//! converter runs the [`Binder`](asap_l2::Binder) for
//! positional name resolution. Depends on DataFusion only — never on the PromQL
//! parser.

pub mod error;
pub mod sql;

use asap_ir::intent_algebra::{convert_root, QueryExpr};
use asap_ir::intent_algebra::QueryExpr;
use asap_l2::convert_root;
use asap_ir::types::AccuracyTarget;
use asap_ir::workload::{QueryLanguage, QueryWorkload, SqlDialect};

Expand Down
8 changes: 4 additions & 4 deletions crates/frontend-sql/src/sql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
//!
//! Parses SQL via DataFusion (over the catalog's registered tables), then walks
//! the unoptimized `LogicalPlan` and emits the language-independent
//! [`relational::QueryExpr`](asap_ir::intent_algebra::relational) that
//! [`convert_root`](asap_ir::intent_algebra::convert_root) lowers to
//! [`relational::QueryExpr`](asap_l2::relational) that
//! [`convert_root`](asap_l2::convert_root) lowers to
//! canonical L3. Positional column identity, accuracy threading, and the
//! window-over-aggregate fold all happen in that converter — this front end
//! only interprets SQL semantics into the shared L2 algebra.
Expand All @@ -17,7 +17,7 @@ use datafusion::logical_expr::{
};
use datafusion::prelude::SessionContext;

use asap_ir::intent_algebra::relational::{
use asap_l2::relational::{
AggFunc, AggItem, L2ProjectItem, L2SortKey, QueryExpr as L2, SourceSpec,
};
use asap_ir::intent_algebra::schema::Schema;
Expand All @@ -36,7 +36,7 @@ use self::expr::df_expr_to_l2;
use self::types::schema_to_arrow;

/// Lowers SQL strings to the Layer-2 [`relational::QueryExpr`] over a table
/// [`SqlCatalog`]. Call [`convert_root`](asap_ir::intent_algebra::convert_root)
/// [`SqlCatalog`]. Call [`convert_root`](asap_l2::convert_root)
/// on the result for canonical L3.
pub struct SqlLowerer<'a> {
catalog: &'a SqlCatalog,
Expand Down
35 changes: 13 additions & 22 deletions crates/ir/src/intent_algebra/mod.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,33 @@
//! Layers 2–3 of the controller pipeline.
//! Layer 3 — the canonical intent algebra IR.
//!
//! - [`relational`] — the Layer-2 per-language algebra tree the parser front
//! ends emit (PromQL / SQL).
//! - [`lower`] — the L2→L3 converter ([`convert_root`]), which runs the
//! [`Binder`] for name resolution and folds single-statistic sketchable
//! aggregates into canonical shapes.
//! - [`query_expr`] — the canonical, language- and deployment-independent L3
//! intent algebra ([`QueryExpr`] + [`AggIntent`]), with positional
//! [`ColumnId`] schema flow.
//! - [`agg_intent`] — the L3 aggregation-intent vocabulary.
//! - [`expr_ir`] — the scalar expression IR ([`L2Expr`] / [`L3Expr`] /
//! [`ColumnRef`]) shared by L2 and L3.
//! - [`schema`] — the per-edge [`Schema`] every L3 node carries.
//! - [`names`] — binding / query identifiers.
//!
//! Workload-level common-sub-expression elimination over L3 lives in the
//! optimizer layer (`asap-plan`), not here — the IR crate stays free of
//! cost-aware passes.
//! The Layer-2 relational tree and the L2→L3 converter (`convert_root`, the
//! `Binder`, column resolution) live in the `asap-l2` crate — front ends need
//! them, but L3-only consumers (optimizer, sketch) do not, so they stay out of
//! this crate. Workload-level CSE lives in `asap-plan`.

pub mod agg_intent;
pub mod binder;
pub mod column_resolution;
pub mod expr_ir;
pub mod lower;
pub mod names;
pub mod query_expr;
pub mod relational;
pub mod schema;

pub use agg_intent::{
agg_accuracy, agg_is_exact, agg_is_mergeable, default_cardinality, default_quantile, AggIntent,
};
pub use binder::{Binder, SchemaCatalog, UsageDerivedCatalog};
pub use column_resolution::{
infer_schema_for_root, infer_source_schema, output_schema_for_aggregate, resolve_column_ref,
resolve_column_refs, resolve_expr, ResolveError,
};
pub use expr_ir::{ArithOp, ColumnRef, CompareOp, Expr, L2Expr, L3Expr, L3Scalar};
pub use lower::{convert, convert_root, ConvertError};
pub use names::{BindingName, QueryId};
pub use query_expr::{
BinaryOpKind, BindingScope, DataModel, GroupKeys, GroupSide, JoinKind, Predicate,
ProjectItem, QueryExpr, QueryExprError, SetOpKind, SortKey, Source, VectorGrouping,
VectorMatch, VectorMatchKind, WindowFuncKind, WindowKind,
BinaryOpKind, BindingScope, DataModel, GroupKeys, GroupSide, JoinKind, Predicate, ProjectItem,
QueryExpr, QueryExprError, SetOpKind, SortKey, Source, VectorGrouping, VectorMatch,
VectorMatchKind, WindowFuncKind, WindowKind,
};
pub use schema::{cse_reuse_is_legal, Column, ColumnId, CseError, DataType, Schema};
11 changes: 11 additions & 0 deletions crates/l2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "asap-l2"
version = "0.1.0"
edition = "2021"

# The L2 per-language relational algebra + the L2→L3 converter (convert_root,
# binder, column resolution). Front ends emit L2 and call convert_root; this
# crate owns both. Depends only on the L3 IR crate (asap-ir).
[dependencies]
asap-ir = { path = "../ir" }
thiserror = "2"
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! [`Binder::bind`] produces the complete, self-contained [`Schema`] every
//! `ColumnId` in the converted canonical tree indexes into. The converter
//! ([`super::lower::convert`]) then becomes purely structural: it threads the
//! ([`crate::lower::convert`]) then becomes purely structural: it threads the
//! Binder's schema and positional resolution downstream is **total**.
//!
//! The default [`UsageDerivedCatalog`] knows nothing — every schema is derived
Expand All @@ -11,10 +11,10 @@
//! `SchemaCatalog` is future work; the `Binder` pass does not change when it
//! lands, only the catalog impl swaps.

use crate::intent_algebra::expr_ir::ColumnRef;
use crate::intent_algebra::expr_ir::L2Expr;
use crate::intent_algebra::relational::QueryExpr as LQueryExpr;
use crate::intent_algebra::schema::{Column, DataType, Schema};
use asap_ir::intent_algebra::expr_ir::ColumnRef;
use asap_ir::intent_algebra::expr_ir::L2Expr;
use crate::relational::QueryExpr as LQueryExpr;
use asap_ir::intent_algebra::schema::{Column, DataType, Schema};

/// The DB / source-schema metadata source — resolves a source (metric /
/// table) name to its known columns.
Expand All @@ -24,7 +24,7 @@ use crate::intent_algebra::schema::{Column, DataType, Schema};
/// Distinct from `Scan.schema`, which is the *resolved* binding schema this
/// feeds — the catalog is the input, the schema is the result. Even a
/// registry-backed PromQL catalog yields an **open** schema
/// ([`Schema::closed`](super::schema::Schema::closed) `= false`): a metric's
/// ([`Schema::closed`](asap_ir::intent_algebra::schema::Schema::closed) `= false`): a metric's
/// labels are per-series and time-varying, so the registry is a superset hint,
/// not a per-row contract.
pub trait SchemaCatalog {
Expand Down Expand Up @@ -170,9 +170,9 @@ fn collect_referenced_columns(tree: &LQueryExpr) -> Vec<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::intent_algebra::expr_ir::ColumnRef;
use crate::intent_algebra::relational::{L2SortKey, QueryExpr as LQueryExpr, SourceSpec};
use crate::intent_algebra::L2Expr;
use asap_ir::intent_algebra::expr_ir::ColumnRef;
use crate::relational::{L2SortKey, QueryExpr as LQueryExpr, SourceSpec};
use asap_ir::intent_algebra::L2Expr;

fn src(name: &str) -> LQueryExpr {
LQueryExpr::Source(SourceSpec::new(name))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@
//!
//! The Layer-2 IR uses `ColumnRef` (name-based, optionally table-qualified);
//! the canonical IR uses positional [`ColumnId`] resolved against a per-node
//! [`Schema`]. These helpers bridge the two — the [`Binder`](super::binder)
//! [`Schema`]. These helpers bridge the two — the [`Binder`](crate::binder)
//! builds the schema, and [`resolve_column_refs`] turns the L2 refs (group
//! keys, dedup columns) into positional ids, qualifier-aware.

use thiserror::Error;

use crate::intent_algebra::agg_intent::AggIntent;
use crate::intent_algebra::expr_ir::ColumnRef;
use crate::intent_algebra::expr_ir::{L2Expr, L3Expr};
use crate::intent_algebra::relational::QueryExpr;
use crate::intent_algebra::schema::{Column, ColumnId, DataType, Schema};
use asap_ir::intent_algebra::agg_intent::AggIntent;
use asap_ir::intent_algebra::expr_ir::ColumnRef;
use asap_ir::intent_algebra::expr_ir::{L2Expr, L3Expr};
use crate::relational::QueryExpr;
use asap_ir::intent_algebra::schema::{Column, ColumnId, DataType, Schema};

/// Errors returned by the resolution helpers.
#[derive(Debug, Error, PartialEq, Eq)]
Expand Down
Loading
Loading