From 9749a9fd2c8cbc582c178b22178615db5685ca3b Mon Sep 17 00:00:00 2001 From: zz_y Date: Thu, 2 Jul 2026 10:58:35 -0600 Subject: [PATCH] refactor(lower): split front ends into asap-frontend-promql/-sql + facade (#57, phase 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 of the crate reorg (#57): split the lower crate along its real seam so each front end pulls only its own parser. - `asap-frontend-promql` (dep: promql-parser) — promql.rs + `lower_promql` / `lower_promql_batch` + a `PromqlError` with no DataFusion variant. - `asap-frontend-sql` (dep: datafusion) — sql/ + `lower_sql` / `lower_sql_batch` + `SqlCatalog` + a `SqlError` with no Parse variant. - `asap-lower` — thin facade re-exporting both language paths for callers that want one crate. - The corpora + conformance tests move to the front end they exercise; e2e (PromQL-only) now depends directly on asap-frontend-promql. The old monolithic `LoweringError` embedded a `DataFusionError`, which would have made the PromQL crate pull DataFusion. Partitioning it into `PromqlError` / `SqlError` (language-neutral variants mirrored, not shared) is what makes the quarantine real: `cargo tree` confirms the PromQL front end pulls 0 datafusion and the SQL front end pulls 0 promql-parser. No behavior change (per-module error aliased back to `LoweringError` so bodies are untouched). Full workspace suite green; clippy --all-targets clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 27 +++- Cargo.toml | 2 + crates/e2e/Cargo.toml | 2 +- crates/e2e/tests/aggregate.rs | 2 +- crates/e2e/tests/binary_op.rs | 2 +- crates/e2e/tests/nested.rs | 2 +- crates/e2e/tests/scan.rs | 2 +- crates/e2e/tests/schema.rs | 2 +- crates/e2e/tests/time_range.rs | 2 +- crates/frontend-promql/Cargo.toml | 13 ++ crates/frontend-promql/src/error.rs | 67 +++++++++ crates/frontend-promql/src/lib.rs | 61 ++++++++ .../{lower => frontend-promql}/src/promql.rs | 2 +- .../tests/awesome_prometheus_alerts.rs | 2 +- .../tests/data/awesome_prometheus_alerts.txt | 0 .../tests/data/promql_corpus_docs.txt | 0 .../tests/data/promql_corpus_testdata.txt | 0 .../tests/promql_conformance.rs | 2 +- .../tests/promql_corpus.rs | 2 +- .../tests/promql_equivalence.rs | 2 +- .../tests/promql_lowering.rs | 2 +- crates/frontend-sql/Cargo.toml | 13 ++ crates/frontend-sql/src/error.rs | 60 ++++++++ crates/frontend-sql/src/lib.rs | 80 ++++++++++ .../{lower => frontend-sql}/src/sql/expr.rs | 2 +- crates/{lower => frontend-sql}/src/sql/mod.rs | 2 +- .../{lower => frontend-sql}/src/sql/types.rs | 2 +- .../data/synthetic_packet_trace_queries.sql | 0 .../tests/sql_lowering.rs | 2 +- .../tests/synthetic_packet_trace.rs | 2 +- crates/lower/Cargo.toml | 17 ++- crates/lower/examples/topk_ir.rs | 4 +- crates/lower/src/error.rs | 86 ----------- crates/lower/src/lib.rs | 140 ++---------------- 34 files changed, 362 insertions(+), 244 deletions(-) create mode 100644 crates/frontend-promql/Cargo.toml create mode 100644 crates/frontend-promql/src/error.rs create mode 100644 crates/frontend-promql/src/lib.rs rename crates/{lower => frontend-promql}/src/promql.rs (99%) rename crates/{lower => frontend-promql}/tests/awesome_prometheus_alerts.rs (99%) rename crates/{lower => frontend-promql}/tests/data/awesome_prometheus_alerts.txt (100%) rename crates/{lower => frontend-promql}/tests/data/promql_corpus_docs.txt (100%) rename crates/{lower => frontend-promql}/tests/data/promql_corpus_testdata.txt (100%) rename crates/{lower => frontend-promql}/tests/promql_conformance.rs (99%) rename crates/{lower => frontend-promql}/tests/promql_corpus.rs (97%) rename crates/{lower => frontend-promql}/tests/promql_equivalence.rs (99%) rename crates/{lower => frontend-promql}/tests/promql_lowering.rs (99%) create mode 100644 crates/frontend-sql/Cargo.toml create mode 100644 crates/frontend-sql/src/error.rs create mode 100644 crates/frontend-sql/src/lib.rs rename crates/{lower => frontend-sql}/src/sql/expr.rs (99%) rename crates/{lower => frontend-sql}/src/sql/mod.rs (99%) rename crates/{lower => frontend-sql}/src/sql/types.rs (98%) rename crates/{lower => frontend-sql}/tests/data/synthetic_packet_trace_queries.sql (100%) rename crates/{lower => frontend-sql}/tests/sql_lowering.rs (99%) rename crates/{lower => frontend-sql}/tests/synthetic_packet_trace.rs (99%) delete mode 100644 crates/lower/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 51c06a9f..3e71391b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,21 +303,28 @@ dependencies = [ ] [[package]] -name = "asap-control-lower" +name = "asap-e2e" +version = "0.1.0" +dependencies = [ + "asap-frontend-promql", + "asap-ir", +] + +[[package]] +name = "asap-frontend-promql" version = "0.1.0" dependencies = [ "asap-ir", - "datafusion", "promql-parser", - "tokio", ] [[package]] -name = "asap-e2e" +name = "asap-frontend-sql" version = "0.1.0" dependencies = [ - "asap-control-lower", "asap-ir", + "datafusion", + "tokio", ] [[package]] @@ -329,6 +336,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "asap-lower" +version = "0.1.0" +dependencies = [ + "asap-frontend-promql", + "asap-frontend-sql", + "asap-ir", + "tokio", +] + [[package]] name = "asap-plan" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2044061e..5759b045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,8 @@ members = [ "crates/ir", "crates/sketch", "crates/plan", + "crates/frontend-promql", + "crates/frontend-sql", "crates/lower", "crates/e2e", ] diff --git a/crates/e2e/Cargo.toml b/crates/e2e/Cargo.toml index e5122940..e034dd8c 100644 --- a/crates/e2e/Cargo.toml +++ b/crates/e2e/Cargo.toml @@ -5,4 +5,4 @@ edition = "2021" [dependencies] asap-ir = { path = "../ir" } -asap-control-lower = { path = "../lower" } +asap-frontend-promql = { path = "../frontend-promql" } diff --git a/crates/e2e/tests/aggregate.rs b/crates/e2e/tests/aggregate.rs index d9bf4512..2be27dd1 100644 --- a/crates/e2e/tests/aggregate.rs +++ b/crates/e2e/tests/aggregate.rs @@ -10,7 +10,7 @@ use asap_ir::intent_algebra::{AggIntent, QueryExpr, Source}; use asap_ir::types::AccuracyTarget; -use asap_control_lower::lower_promql; +use asap_frontend_promql::lower_promql; use asap_e2e::fixtures::metric_schema; fn lower(q: &str) -> QueryExpr { diff --git a/crates/e2e/tests/binary_op.rs b/crates/e2e/tests/binary_op.rs index 812d711a..00bb4174 100644 --- a/crates/e2e/tests/binary_op.rs +++ b/crates/e2e/tests/binary_op.rs @@ -12,7 +12,7 @@ use asap_ir::intent_algebra::{ VectorMatch, VectorMatchKind, }; use asap_ir::types::AccuracyTarget; -use asap_control_lower::lower_promql; +use asap_frontend_promql::lower_promql; use asap_e2e::fixtures::metric_schema; fn lower(q: &str) -> QueryExpr { diff --git a/crates/e2e/tests/nested.rs b/crates/e2e/tests/nested.rs index 2dece873..bec96c4a 100644 --- a/crates/e2e/tests/nested.rs +++ b/crates/e2e/tests/nested.rs @@ -14,7 +14,7 @@ use asap_ir::intent_algebra::{ AggIntent, ArithOp, BinaryOpKind, CompareOp, L3Expr, L3Scalar, Predicate, QueryExpr, Source, }; use asap_ir::types::AccuracyTarget; -use asap_control_lower::lower_promql; +use asap_frontend_promql::lower_promql; use asap_e2e::fixtures::metric_schema; fn lower(q: &str) -> QueryExpr { diff --git a/crates/e2e/tests/scan.rs b/crates/e2e/tests/scan.rs index d272516f..411d6a66 100644 --- a/crates/e2e/tests/scan.rs +++ b/crates/e2e/tests/scan.rs @@ -10,7 +10,7 @@ use asap_ir::intent_algebra::{ CompareOp, L3Expr, L3Scalar, Predicate, QueryExpr, Source, }; use asap_ir::types::AccuracyTarget; -use asap_control_lower::lower_promql; +use asap_frontend_promql::lower_promql; use asap_e2e::fixtures::metric_schema; fn lower(q: &str) -> QueryExpr { diff --git a/crates/e2e/tests/schema.rs b/crates/e2e/tests/schema.rs index 22dbf0b9..b0e83038 100644 --- a/crates/e2e/tests/schema.rs +++ b/crates/e2e/tests/schema.rs @@ -10,7 +10,7 @@ //! are label-preserving and keep the schema open. use asap_ir::types::AccuracyTarget; -use asap_control_lower::lower_promql; +use asap_frontend_promql::lower_promql; fn lower(q: &str) -> asap_ir::intent_algebra::QueryExpr { lower_promql(q, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) diff --git a/crates/e2e/tests/time_range.rs b/crates/e2e/tests/time_range.rs index 77050b59..eae8e58c 100644 --- a/crates/e2e/tests/time_range.rs +++ b/crates/e2e/tests/time_range.rs @@ -11,7 +11,7 @@ use std::time::Duration; use asap_ir::intent_algebra::{AggIntent, QueryExpr, Source}; use asap_ir::types::AccuracyTarget; -use asap_control_lower::lower_promql; +use asap_frontend_promql::lower_promql; use asap_e2e::fixtures::metric_schema; fn lower(q: &str) -> QueryExpr { diff --git a/crates/frontend-promql/Cargo.toml b/crates/frontend-promql/Cargo.toml new file mode 100644 index 00000000..b5eb9d4b --- /dev/null +++ b/crates/frontend-promql/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "asap-frontend-promql" +version = "0.1.0" +edition = "2021" + +# PromQL front end: L1 (parse) → L2 relational, then the shared L2→L3 converter +# in asap-ir. Pulls the PromQL parser only — never DataFusion. +[dependencies] +asap-ir = { path = "../ir" } + +# Private mirror of GreptimeTeam/promql-parser (Apache-2.0). `main` tracks +# upstream; our `asap` branch carries the local patches. +promql-parser = { git = "https://github.com/ProjectASAP/promql-parser", branch = "asap" } diff --git a/crates/frontend-promql/src/error.rs b/crates/frontend-promql/src/error.rs new file mode 100644 index 00000000..29aeba19 --- /dev/null +++ b/crates/frontend-promql/src/error.rs @@ -0,0 +1,67 @@ +use std::fmt; + +use asap_ir::intent_algebra::ConvertError; + +/// Errors from lowering a PromQL query (L1 parse → L2 → shared L2→L3 convert). +/// +/// Carries no DataFusion type — the PromQL front end never depends on the SQL +/// stack. The language-neutral variants (`UnsupportedFeature` / `WrongLanguage` +/// / `Convert`) are mirrored by [`asap_frontend_sql::SqlError`] rather than +/// 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). + Parse(String), + /// A PromQL function (`rate`, `*_over_time`, …) not supported in this version. + UnsupportedFunction(String), + /// A PromQL aggregation operator (`sum`, `topk`, …) not supported. + UnsupportedAggregateOp(String), + /// A structural feature (offset / `@` / `without` / unary negation) not + /// supported in this version. + UnsupportedFeature(String), + /// A required function / aggregator argument was missing. + MissingArgument(String), + /// An argument had the wrong shape (e.g. a non-numeric `topk` parameter). + InvalidParameter(String), + /// The workload's query language is not PromQL. + WrongLanguage(String), + /// The L2→L3 converter failed (name resolution against the bound schema). + Convert(ConvertError), +} + +impl fmt::Display for PromqlError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Parse(e) => write!(f, "PromQL parse error: {e}"), + Self::UnsupportedFunction(n) => write!(f, "unsupported PromQL function: {n}"), + Self::UnsupportedAggregateOp(n) => write!(f, "unsupported PromQL aggregate op: {n}"), + Self::UnsupportedFeature(m) => write!(f, "unsupported feature: {m}"), + 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 conversion failed: {e}"), + } + } +} + +impl std::error::Error for PromqlError {} + +impl From for PromqlError { + fn from(e: ConvertError) -> Self { + Self::Convert(e) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unsupported_feature_label_is_language_neutral() { + // `UnsupportedFeature` shares a Display label with the SQL side, so it + // must not hardcode "PromQL". + let msg = PromqlError::UnsupportedFeature("subquery".into()).to_string(); + assert_eq!(msg, "unsupported feature: subquery"); + assert!(!msg.contains("PromQL"), "got: {msg}"); + } +} diff --git a/crates/frontend-promql/src/lib.rs b/crates/frontend-promql/src/lib.rs new file mode 100644 index 00000000..84069763 --- /dev/null +++ b/crates/frontend-promql/src/lib.rs @@ -0,0 +1,61 @@ +//! PromQL front end: L1 (parse via `promql-parser`) → L2 relational, then the +//! shared L2→L3 [`convert_root`](asap_ir::intent_algebra::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 +//! 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::types::AccuracyTarget; +use asap_ir::workload::{QueryLanguage, QueryWorkload}; + +pub use error::PromqlError; +pub use promql::PromqlLowerer; + +/// Lower a single PromQL query string to the canonical L3 `QueryExpr`. +/// +/// `accuracy` is threaded onto every approximate intent (`Count`, `Quantile`, +/// `Cardinality`, `TopK`). The returned tree carries a self-contained `Schema` +/// on its `Scan`; call [`QueryExpr::output_schema`] for any node's schema. +pub fn lower_promql(query: &str, accuracy: AccuracyTarget) -> Result { + let l2 = PromqlLowerer::lower(query)?; + let l3 = convert_root(&l2, &accuracy)?; + Ok(l3) +} + +/// Lower every PromQL batch entry in `workload` to a `QueryExpr`. +/// +/// One `Result` per entry — errors are per-query, not fatal for the batch. +/// Returns an empty `Vec` if `workload.query_batch` is absent or empty, and a +/// `WrongLanguage` error for every entry if the workload language is not PromQL. +pub fn lower_promql_batch(workload: &QueryWorkload) -> Vec> { + let entries = match &workload.query_batch { + Some(e) if !e.is_empty() => e, + _ => return vec![], + }; + + if !matches!(workload.language, QueryLanguage::PromQL) { + let lang = format!("{:?}", workload.language); + return entries + .iter() + .map(|_| Err(PromqlError::WrongLanguage(lang.clone()))) + .collect(); + } + + entries + .iter() + .map(|entry| { + let accuracy = entry + .requirements + .as_ref() + .and_then(|r| r.accuracy.clone()) + .unwrap_or(AccuracyTarget::Exact); + lower_promql(&entry.query.0, accuracy) + }) + .collect() +} diff --git a/crates/lower/src/promql.rs b/crates/frontend-promql/src/promql.rs similarity index 99% rename from crates/lower/src/promql.rs rename to crates/frontend-promql/src/promql.rs index bcf32cb1..ff35e551 100644 --- a/crates/lower/src/promql.rs +++ b/crates/frontend-promql/src/promql.rs @@ -49,7 +49,7 @@ use asap_ir::intent_algebra::relational::{ }; use asap_ir::intent_algebra::{ArithOp, ColumnRef, CompareOp, L2Expr, L3Scalar}; -use crate::error::LoweringError; +use crate::error::PromqlError as LoweringError; type Result = std::result::Result; diff --git a/crates/lower/tests/awesome_prometheus_alerts.rs b/crates/frontend-promql/tests/awesome_prometheus_alerts.rs similarity index 99% rename from crates/lower/tests/awesome_prometheus_alerts.rs rename to crates/frontend-promql/tests/awesome_prometheus_alerts.rs index 1f17c600..c1610c0b 100644 --- a/crates/lower/tests/awesome_prometheus_alerts.rs +++ b/crates/frontend-promql/tests/awesome_prometheus_alerts.rs @@ -28,7 +28,7 @@ use asap_ir::intent_algebra::{AggIntent, BinaryOpKind, CompareOp, QueryExpr}; use asap_ir::types::AccuracyTarget; -use asap_control_lower::{lower_promql, LoweringError}; +use asap_frontend_promql::{lower_promql, PromqlError as LoweringError}; const CORPUS: &str = include_str!("data/awesome_prometheus_alerts.txt"); diff --git a/crates/lower/tests/data/awesome_prometheus_alerts.txt b/crates/frontend-promql/tests/data/awesome_prometheus_alerts.txt similarity index 100% rename from crates/lower/tests/data/awesome_prometheus_alerts.txt rename to crates/frontend-promql/tests/data/awesome_prometheus_alerts.txt diff --git a/crates/lower/tests/data/promql_corpus_docs.txt b/crates/frontend-promql/tests/data/promql_corpus_docs.txt similarity index 100% rename from crates/lower/tests/data/promql_corpus_docs.txt rename to crates/frontend-promql/tests/data/promql_corpus_docs.txt diff --git a/crates/lower/tests/data/promql_corpus_testdata.txt b/crates/frontend-promql/tests/data/promql_corpus_testdata.txt similarity index 100% rename from crates/lower/tests/data/promql_corpus_testdata.txt rename to crates/frontend-promql/tests/data/promql_corpus_testdata.txt diff --git a/crates/lower/tests/promql_conformance.rs b/crates/frontend-promql/tests/promql_conformance.rs similarity index 99% rename from crates/lower/tests/promql_conformance.rs rename to crates/frontend-promql/tests/promql_conformance.rs index 361eb327..74fa2598 100644 --- a/crates/lower/tests/promql_conformance.rs +++ b/crates/frontend-promql/tests/promql_conformance.rs @@ -37,7 +37,7 @@ use asap_ir::intent_algebra::{ AggIntent, ArithOp, BinaryOpKind, CompareOp, QueryExpr, Source, }; use asap_ir::types::AccuracyTarget; -use asap_control_lower::{lower_promql, LoweringError}; +use asap_frontend_promql::{lower_promql, PromqlError as LoweringError}; // ── harness helpers ───────────────────────────────────────────────────────────── diff --git a/crates/lower/tests/promql_corpus.rs b/crates/frontend-promql/tests/promql_corpus.rs similarity index 97% rename from crates/lower/tests/promql_corpus.rs rename to crates/frontend-promql/tests/promql_corpus.rs index 40672a66..b8e9600a 100644 --- a/crates/lower/tests/promql_corpus.rs +++ b/crates/frontend-promql/tests/promql_corpus.rs @@ -14,7 +14,7 @@ //! tanking how much of the corpus we can lower. use asap_ir::types::AccuracyTarget; -use asap_control_lower::{lower_promql, LoweringError}; +use asap_frontend_promql::{lower_promql, PromqlError as LoweringError}; const DOCS: &str = include_str!("data/promql_corpus_docs.txt"); const TESTDATA: &str = include_str!("data/promql_corpus_testdata.txt"); diff --git a/crates/lower/tests/promql_equivalence.rs b/crates/frontend-promql/tests/promql_equivalence.rs similarity index 99% rename from crates/lower/tests/promql_equivalence.rs rename to crates/frontend-promql/tests/promql_equivalence.rs index aedb42b5..b097e084 100644 --- a/crates/lower/tests/promql_equivalence.rs +++ b/crates/frontend-promql/tests/promql_equivalence.rs @@ -19,7 +19,7 @@ use asap_ir::intent_algebra::QueryExpr; use asap_ir::types::AccuracyTarget; -use asap_control_lower::lower_promql; +use asap_frontend_promql::lower_promql; fn lo(q: &str) -> QueryExpr { lower_promql(q, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("{q:?} should lower: {e}")) diff --git a/crates/lower/tests/promql_lowering.rs b/crates/frontend-promql/tests/promql_lowering.rs similarity index 99% rename from crates/lower/tests/promql_lowering.rs rename to crates/frontend-promql/tests/promql_lowering.rs index 81aace5d..3a413c59 100644 --- a/crates/lower/tests/promql_lowering.rs +++ b/crates/frontend-promql/tests/promql_lowering.rs @@ -10,7 +10,7 @@ use asap_ir::workload::{ BatchEntry, Query, QueryLanguage, QueryRequirements, QueryWorkload, }; -use asap_control_lower::{lower_promql, lower_promql_batch, LoweringError}; +use asap_frontend_promql::{lower_promql, lower_promql_batch, PromqlError as LoweringError}; fn lower(q: &str) -> QueryExpr { lower_promql(q, AccuracyTarget::Exact).unwrap_or_else(|e| panic!("lower failed for {q:?}: {e}")) diff --git a/crates/frontend-sql/Cargo.toml b/crates/frontend-sql/Cargo.toml new file mode 100644 index 00000000..7dd6812e --- /dev/null +++ b/crates/frontend-sql/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "asap-frontend-sql" +version = "0.1.0" +edition = "2021" + +# SQL front end: L1 (parse + plan via DataFusion) → L2 relational, then the +# shared L2→L3 converter in asap-ir. Pulls DataFusion only — never promql-parser. +[dependencies] +asap-ir = { path = "../ir" } +datafusion = "43" + +[dev-dependencies] +tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread"] } diff --git a/crates/frontend-sql/src/error.rs b/crates/frontend-sql/src/error.rs new file mode 100644 index 00000000..558f5fa9 --- /dev/null +++ b/crates/frontend-sql/src/error.rs @@ -0,0 +1,60 @@ +use std::fmt; + +use asap_ir::intent_algebra::ConvertError; + +/// Errors from lowering a SQL query (L1 parse + plan via DataFusion → L2 → +/// shared L2→L3 convert). +/// +/// Carries no PromQL type — the SQL front end never depends on the PromQL +/// parser. The language-neutral variants (`UnsupportedFeature` / `WrongLanguage` +/// / `Convert`) are mirrored by [`asap_frontend_promql::PromqlError`] rather +/// than shared, so neither front end pulls the other's parser. +#[derive(Debug)] +pub enum SqlError { + /// DataFusion failed to parse / plan the SQL query. + DataFusion(datafusion::error::DataFusionError), + /// A table referenced by the query is absent from the catalog. + TableNotFound(String), + /// A SQL aggregate function not supported in this version. + UnsupportedAggregate(String), + /// A SQL scalar expression that could not be lowered. + InvalidExpression(String), + /// The SQL dialect is not supported (only DataFusionSQL is implemented). + UnsupportedDialect(String), + /// A structural feature (JOIN type / subquery / derived table) not + /// supported in this version. + UnsupportedFeature(String), + /// The workload's query language is not SQL. + WrongLanguage(String), + /// The L2→L3 converter failed (name resolution against the bound schema). + Convert(ConvertError), +} + +impl fmt::Display for SqlError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DataFusion(e) => write!(f, "DataFusion error: {e}"), + Self::TableNotFound(t) => write!(f, "table not found in catalog: {t}"), + Self::UnsupportedAggregate(n) => write!(f, "unsupported aggregate: {n}"), + Self::InvalidExpression(m) => write!(f, "invalid expression: {m}"), + 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 conversion failed: {e}"), + } + } +} + +impl std::error::Error for SqlError {} + +impl From for SqlError { + fn from(e: ConvertError) -> Self { + Self::Convert(e) + } +} + +impl From for SqlError { + fn from(e: datafusion::error::DataFusionError) -> Self { + Self::DataFusion(e) + } +} diff --git a/crates/frontend-sql/src/lib.rs b/crates/frontend-sql/src/lib.rs new file mode 100644 index 00000000..3ff24b0d --- /dev/null +++ b/crates/frontend-sql/src/lib.rs @@ -0,0 +1,80 @@ +//! SQL front end: L1 (parse + plan via DataFusion) → L2 relational, then the +//! shared L2→L3 [`convert_root`](asap_ir::intent_algebra::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 +//! 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::types::AccuracyTarget; +use asap_ir::workload::{QueryLanguage, QueryWorkload, SqlDialect}; + +pub use error::SqlError; +pub use sql::{SqlCatalog, SqlLowerer}; + +/// Lower a single SQL query string to the canonical L3 `QueryExpr`. +/// +/// 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. +pub async fn lower_sql( + query: &str, + catalog: &SqlCatalog, + accuracy: AccuracyTarget, +) -> Result { + let l2 = SqlLowerer::new(catalog).lower(query).await?; + let l3 = convert_root(&l2, &accuracy)?; + Ok(l3) +} + +/// Lower every SQL batch entry in `workload` to a `QueryExpr`. +/// +/// One `Result` per entry — errors are per-query, not fatal for the batch. +/// Returns `WrongLanguage` for every entry if the workload is not SQL, and +/// `UnsupportedDialect` for non-DataFusion SQL dialects. +pub async fn lower_sql_batch( + workload: &QueryWorkload, + catalog: &SqlCatalog, +) -> Vec> { + let entries = match &workload.query_batch { + Some(e) if !e.is_empty() => e, + _ => return vec![], + }; + + // `DataFusion` is a legacy alias for `SQL(DataFusionSQL)`; accept both. + if !matches!( + workload.language, + QueryLanguage::SQL(_) | QueryLanguage::DataFusion + ) { + let lang = format!("{:?}", workload.language); + return entries + .iter() + .map(|_| Err(SqlError::WrongLanguage(lang.clone()))) + .collect(); + } + if let QueryLanguage::SQL(dialect) = &workload.language { + if !matches!(dialect, SqlDialect::DataFusionSQL) { + let d = format!("{dialect:?}"); + return entries + .iter() + .map(|_| Err(SqlError::UnsupportedDialect(d.clone()))) + .collect(); + } + } + + let mut results = Vec::with_capacity(entries.len()); + for entry in entries { + let accuracy = entry + .requirements + .as_ref() + .and_then(|r| r.accuracy.clone()) + .unwrap_or(AccuracyTarget::Exact); + results.push(lower_sql(&entry.query.0, catalog, accuracy).await); + } + results +} diff --git a/crates/lower/src/sql/expr.rs b/crates/frontend-sql/src/sql/expr.rs similarity index 99% rename from crates/lower/src/sql/expr.rs rename to crates/frontend-sql/src/sql/expr.rs index 74639509..a481eade 100644 --- a/crates/lower/src/sql/expr.rs +++ b/crates/frontend-sql/src/sql/expr.rs @@ -2,7 +2,7 @@ use datafusion::logical_expr::{BinaryExpr, Expr, Operator}; use asap_ir::intent_algebra::{ArithOp, ColumnRef, CompareOp, L2Expr, L3Scalar}; -use crate::error::LoweringError; +use crate::error::SqlError as LoweringError; use super::types::{arrow_to_l3, scalar_value_to_l3}; diff --git a/crates/lower/src/sql/mod.rs b/crates/frontend-sql/src/sql/mod.rs similarity index 99% rename from crates/lower/src/sql/mod.rs rename to crates/frontend-sql/src/sql/mod.rs index e963b748..95358338 100644 --- a/crates/lower/src/sql/mod.rs +++ b/crates/frontend-sql/src/sql/mod.rs @@ -25,7 +25,7 @@ use asap_ir::intent_algebra::{ ColumnRef, CompareOp, JoinKind, L2Expr, L3Scalar, SetOpKind, WindowFuncKind, }; -use crate::error::LoweringError; +use crate::error::SqlError as LoweringError; mod expr; mod types; diff --git a/crates/lower/src/sql/types.rs b/crates/frontend-sql/src/sql/types.rs similarity index 98% rename from crates/lower/src/sql/types.rs rename to crates/frontend-sql/src/sql/types.rs index ac512668..06b1271e 100644 --- a/crates/lower/src/sql/types.rs +++ b/crates/frontend-sql/src/sql/types.rs @@ -12,7 +12,7 @@ use datafusion::common::ScalarValue; use asap_ir::intent_algebra::schema::{Column, DataType, Schema}; use asap_ir::intent_algebra::L3Scalar; -use crate::error::LoweringError; +use crate::error::SqlError as LoweringError; /// Table catalog for SQL lowering: table name → resolved L3 [`Schema`]. /// diff --git a/crates/lower/tests/data/synthetic_packet_trace_queries.sql b/crates/frontend-sql/tests/data/synthetic_packet_trace_queries.sql similarity index 100% rename from crates/lower/tests/data/synthetic_packet_trace_queries.sql rename to crates/frontend-sql/tests/data/synthetic_packet_trace_queries.sql diff --git a/crates/lower/tests/sql_lowering.rs b/crates/frontend-sql/tests/sql_lowering.rs similarity index 99% rename from crates/lower/tests/sql_lowering.rs rename to crates/frontend-sql/tests/sql_lowering.rs index fcff829a..8527e18a 100644 --- a/crates/lower/tests/sql_lowering.rs +++ b/crates/frontend-sql/tests/sql_lowering.rs @@ -9,7 +9,7 @@ use asap_ir::intent_algebra::{ AggIntent, CompareOp, GroupKeys, JoinKind, L3Expr, QueryExpr, Source, WindowFuncKind, }; use asap_ir::types::AccuracyTarget; -use asap_control_lower::{lower_sql, SqlCatalog}; +use asap_frontend_sql::{lower_sql, SqlCatalog}; fn col(name: &str, dtype: DataType) -> Column { Column::new(name, dtype, false) diff --git a/crates/lower/tests/synthetic_packet_trace.rs b/crates/frontend-sql/tests/synthetic_packet_trace.rs similarity index 99% rename from crates/lower/tests/synthetic_packet_trace.rs rename to crates/frontend-sql/tests/synthetic_packet_trace.rs index a3fee43c..6556289e 100644 --- a/crates/lower/tests/synthetic_packet_trace.rs +++ b/crates/frontend-sql/tests/synthetic_packet_trace.rs @@ -23,7 +23,7 @@ use asap_ir::intent_algebra::schema::{Column, DataType, Schema}; use asap_ir::intent_algebra::{AggIntent, GroupKeys, QueryExpr}; use asap_ir::types::AccuracyTarget; -use asap_control_lower::{lower_sql, LoweringError, SqlCatalog}; +use asap_frontend_sql::{lower_sql, SqlError as LoweringError, SqlCatalog}; const CORPUS: &str = include_str!("data/synthetic_packet_trace_queries.sql"); diff --git a/crates/lower/Cargo.toml b/crates/lower/Cargo.toml index 3fc83dbb..ff506842 100644 --- a/crates/lower/Cargo.toml +++ b/crates/lower/Cargo.toml @@ -1,16 +1,17 @@ [package] -name = "asap-control-lower" +name = "asap-lower" version = "0.1.0" edition = "2021" +# Thin facade over the two front ends — re-exports `lower_promql` / `lower_sql` +# (and batch + lowerer + error types) so a caller can depend on one crate for +# both languages. A caller that needs only one language should depend on the +# matching `asap-frontend-*` crate directly to avoid pulling the other parser. [dependencies] -asap-ir = { path = "../ir" } -# Private mirror of GreptimeTeam/promql-parser (Apache-2.0). `main` tracks -# upstream untouched; the `asap` branch carries our local grammar/function -# additions (see THIRD_PARTY.md). -promql-parser = { git = "https://github.com/ProjectASAP/promql-parser", branch = "asap" } -# SQL front end: parse + plan via DataFusion, then lower its LogicalPlan to L2. -datafusion = "43" +asap-frontend-promql = { path = "../frontend-promql" } +asap-frontend-sql = { path = "../frontend-sql" } [dev-dependencies] +# The topk_ir example builds catalog schemas + drives the async SQL path. +asap-ir = { path = "../ir" } tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread"] } diff --git a/crates/lower/examples/topk_ir.rs b/crates/lower/examples/topk_ir.rs index 70b5e91a..43a20040 100644 --- a/crates/lower/examples/topk_ir.rs +++ b/crates/lower/examples/topk_ir.rs @@ -1,11 +1,11 @@ -// cargo run -p asap-control-lower --example topk_ir +// 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. use asap_ir::intent_algebra::schema::{Column, DataType, Schema}; use asap_ir::types::AccuracyTarget; -use asap_control_lower::{lower_promql, lower_sql, SqlCatalog}; +use asap_lower::{lower_promql, lower_sql, SqlCatalog}; fn col(name: &str, dtype: DataType) -> Column { Column::new(name, dtype, false) diff --git a/crates/lower/src/error.rs b/crates/lower/src/error.rs deleted file mode 100644 index bce5095a..00000000 --- a/crates/lower/src/error.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::fmt; - -#[derive(Debug)] -pub enum LoweringError { - /// The `promql-parser` crate rejected the query string (L1 parse failure). - Parse(String), - /// A PromQL function (`rate`, `*_over_time`, …) not supported in this version. - UnsupportedFunction(String), - /// A PromQL aggregation operator (`sum`, `topk`, …) not supported. - UnsupportedAggregateOp(String), - /// A structural feature (PromQL offset/`@`/`without`; SQL JOIN/subquery/…) - /// not supported in this version. - UnsupportedFeature(String), - /// A required function / aggregator argument was missing. - MissingArgument(String), - /// An argument had the wrong shape (e.g. a non-numeric `topk` parameter). - InvalidParameter(String), - /// The workload's query language is not handled by this lowerer. - WrongLanguage(String), - /// The L2→L3 converter failed (name resolution against the bound schema). - Convert(asap_ir::intent_algebra::ConvertError), - - // ── SQL front end (DataFusion) ─────────────────────────────────────────── - /// DataFusion failed to parse / plan the SQL query. - DataFusion(datafusion::error::DataFusionError), - /// A table referenced by the query is absent from the catalog. - TableNotFound(String), - /// A SQL aggregate function not supported in this version. - UnsupportedAggregate(String), - /// A SQL scalar expression that could not be lowered. - InvalidExpression(String), - /// The SQL dialect is not supported (only DataFusionSQL is implemented). - UnsupportedDialect(String), -} - -impl fmt::Display for LoweringError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Parse(e) => write!(f, "PromQL parse error: {e}"), - Self::UnsupportedFunction(n) => write!(f, "unsupported PromQL function: {n}"), - Self::UnsupportedAggregateOp(n) => write!(f, "unsupported PromQL aggregate op: {n}"), - // Raised by both front ends (PromQL offset/`@`/`without`/negation; - // SQL join type/subquery/derived table), so keep the label neutral — - // the message string carries the specifics. - Self::UnsupportedFeature(m) => write!(f, "unsupported feature: {m}"), - 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 conversion failed: {e}"), - Self::DataFusion(e) => write!(f, "DataFusion error: {e}"), - Self::TableNotFound(t) => write!(f, "table not found in catalog: {t}"), - Self::UnsupportedAggregate(n) => write!(f, "unsupported aggregate: {n}"), - Self::InvalidExpression(m) => write!(f, "invalid expression: {m}"), - Self::UnsupportedDialect(d) => write!(f, "unsupported SQL dialect: {d}"), - } - } -} - -impl std::error::Error for LoweringError {} - -impl From for LoweringError { - fn from(e: asap_ir::intent_algebra::ConvertError) -> Self { - Self::Convert(e) - } -} - -impl From for LoweringError { - fn from(e: datafusion::error::DataFusionError) -> Self { - Self::DataFusion(e) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn unsupported_feature_label_is_language_neutral() { - // `UnsupportedFeature` is raised by both front ends, so its Display must - // not hardcode "PromQL" — a SQL user rejecting a subquery shouldn't see - // "unsupported PromQL feature: subquery". - let msg = LoweringError::UnsupportedFeature("subquery".into()).to_string(); - assert_eq!(msg, "unsupported feature: subquery"); - assert!(!msg.contains("PromQL"), "got: {msg}"); - } -} diff --git a/crates/lower/src/lib.rs b/crates/lower/src/lib.rs index 5b5dedfa..44d8177b 100644 --- a/crates/lower/src/lib.rs +++ b/crates/lower/src/lib.rs @@ -1,126 +1,16 @@ -//! L1→L3 lowering passes for the ASAP controller core. +//! `asap-lower` — thin facade over the two query front ends. //! -//! Both front ends end at the canonical intent algebra via the same L2→L3 -//! [`convert_root`]: PromQL parses with `promql-parser`, SQL parses + plans with -//! DataFusion. Each emits the per-language -//! [`relational::QueryExpr`](asap_ir::intent_algebra::relational); the -//! shared converter runs the [`Binder`](asap_ir::intent_algebra::Binder) -//! for positional name resolution and folds single-statistic sketchable -//! aggregates into canonical shapes. - -pub mod error; -pub mod promql; -pub mod sql; - -use asap_ir::intent_algebra::{convert_root, QueryExpr}; -use asap_ir::types::AccuracyTarget; -use asap_ir::workload::{QueryLanguage, QueryWorkload, SqlDialect}; - -pub use error::LoweringError; -pub use promql::PromqlLowerer; -pub use sql::{SqlCatalog, SqlLowerer}; - -/// Lower a single PromQL query string to the canonical L3 `QueryExpr`. -/// -/// `accuracy` is threaded onto every approximate intent (`Count`, `Quantile`, -/// `Cardinality`, `TopK`). The returned tree carries a self-contained `Schema` -/// on its `Scan`; call [`QueryExpr::output_schema`] for any node's schema. -pub fn lower_promql(query: &str, accuracy: AccuracyTarget) -> Result { - let l2 = PromqlLowerer::lower(query)?; - let l3 = convert_root(&l2, &accuracy)?; - Ok(l3) -} - -/// Lower every PromQL batch entry in `workload` to a `QueryExpr`. -/// -/// One `Result` per entry — errors are per-query, not fatal for the batch. -/// Returns an empty `Vec` if `workload.query_batch` is absent or empty, and a -/// `WrongLanguage` error for every entry if the workload language is not PromQL. -pub fn lower_promql_batch(workload: &QueryWorkload) -> Vec> { - let entries = match &workload.query_batch { - Some(e) if !e.is_empty() => e, - _ => return vec![], - }; - - if !matches!(workload.language, QueryLanguage::PromQL) { - let lang = format!("{:?}", workload.language); - return entries - .iter() - .map(|_| Err(LoweringError::WrongLanguage(lang.clone()))) - .collect(); - } - - entries - .iter() - .map(|entry| { - let accuracy = entry - .requirements - .as_ref() - .and_then(|r| r.accuracy.clone()) - .unwrap_or(AccuracyTarget::Exact); - lower_promql(&entry.query.0, accuracy) - }) - .collect() -} - -/// Lower a single SQL query string to the canonical L3 `QueryExpr`. -/// -/// 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. -pub async fn lower_sql( - query: &str, - catalog: &SqlCatalog, - accuracy: AccuracyTarget, -) -> Result { - let l2 = SqlLowerer::new(catalog).lower(query).await?; - let l3 = convert_root(&l2, &accuracy)?; - Ok(l3) -} - -/// Lower every SQL batch entry in `workload` to a `QueryExpr`. -/// -/// One `Result` per entry — errors are per-query, not fatal for the batch. -/// Returns `WrongLanguage` for every entry if the workload is not SQL, and -/// `UnsupportedDialect` for non-DataFusion SQL dialects. -pub async fn lower_sql_batch( - workload: &QueryWorkload, - catalog: &SqlCatalog, -) -> Vec> { - let entries = match &workload.query_batch { - Some(e) if !e.is_empty() => e, - _ => return vec![], - }; - - // `DataFusion` is a legacy alias for `SQL(DataFusionSQL)`; accept both. - if !matches!( - workload.language, - QueryLanguage::SQL(_) | QueryLanguage::DataFusion - ) { - let lang = format!("{:?}", workload.language); - return entries - .iter() - .map(|_| Err(LoweringError::WrongLanguage(lang.clone()))) - .collect(); - } - if let QueryLanguage::SQL(dialect) = &workload.language { - if !matches!(dialect, SqlDialect::DataFusionSQL) { - let d = format!("{dialect:?}"); - return entries - .iter() - .map(|_| Err(LoweringError::UnsupportedDialect(d.clone()))) - .collect(); - } - } - - let mut results = Vec::with_capacity(entries.len()); - for entry in entries { - let accuracy = entry - .requirements - .as_ref() - .and_then(|r| r.accuracy.clone()) - .unwrap_or(AccuracyTarget::Exact); - results.push(lower_sql(&entry.query.0, catalog, accuracy).await); - } - results -} +//! 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`]. +//! +//! ## Dependency isolation +//! +//! Depending on this facade pulls **both** parsers (`promql-parser` and +//! DataFusion). A caller that needs only one language should depend on the +//! matching front-end crate directly — [`asap_frontend_promql`] (PromQL parser +//! only) or [`asap_frontend_sql`] (DataFusion only) — so it never compiles the +//! other's parser. + +pub use asap_frontend_promql::{lower_promql, lower_promql_batch, PromqlError, PromqlLowerer}; +pub use asap_frontend_sql::{lower_sql, lower_sql_batch, SqlCatalog, SqlError, SqlLowerer};