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
2 changes: 1 addition & 1 deletion crates/devtools/examples/canonical_examples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ fn bgp_catalog() -> SqlCatalog {
async fn main() {
let promql_examples: &[(&str, &str)] = &[
("Scan", "up"),
("BinaryOp + PromqlScalar", "up > 1"),
("BinaryOp + PromqlScalarBridge", "up > 1"),
("QueryTimestamp", "time()"),
("Aggregate", "sum(up)"),
(
Expand Down
6 changes: 3 additions & 3 deletions crates/devtools/src/bin/variant_coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use std::collections::BTreeSet;

const ALL_VARIANTS: &[&str] = &[
"Scan",
"PromqlScalar",
"PromqlScalarBridge",
"QueryTimestamp",
"PromqlVectorFromScalar",
"PromqlScalarFromVector",
Expand Down Expand Up @@ -42,8 +42,8 @@ fn walk(e: &QueryExpr, seen: &mut BTreeSet<&'static str>) {
QueryExpr::Scan { .. } => {
seen.insert("Scan");
}
QueryExpr::PromqlScalar(_) => {
seen.insert("PromqlScalar");
QueryExpr::PromqlScalarBridge(_) => {
seen.insert("PromqlScalarBridge");
}
QueryExpr::QueryTimestamp => {
seen.insert("QueryTimestamp");
Expand Down
24 changes: 12 additions & 12 deletions crates/frontend-promql/src/promql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
//! | `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 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 `PromqlScalar` leaf |
//! | `abs`/`ceil`/`sqrt`/`ln`/`clamp*`/`round`/trig(`v`), `pi()` | `Aggregate{[Math(f)]}` element-wise transform (issue #45); `pi()` → a `PromqlScalarBridge` leaf |
//! | `time()` / `timestamp`/`hour`/`day_of_week`/… (`v`) | `QueryTimestamp` leaf / `Aggregate{[TimeFn(f)]}` (issue #46) |
//! | `vector(s)` / `scalar(v)` | `PromqlVectorFromScalar` / `PromqlScalarFromVector` — the scalar⇄vector bridges (issue #48) |
//! | `label_replace(v,…)` / `label_join(v,…)` | `PromqlRelabel{dst, value}` — per-series label rewrite; value unchanged (issue #50) |
Expand Down Expand Up @@ -264,10 +264,10 @@ fn walk(expr: &Expr) -> Result<Unresolved> {
Expr::Call(call) if is_typeconv_fn(call.func.name) => walk_typeconv(call),
Expr::Call(call) if is_label_fn(call.func.name) => walk_label(call),
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 `PromqlScalar`
// A bare `min_of`/`max_of(consts…)` scalar query folds to a `PromqlScalarBridge`
// leaf; a non-constant argument makes `num_expr` fail → rejected (#89).
Expr::Call(call) if is_scalar_reducer_fn(call.func.name) => {
Ok(Unresolved::PromqlScalar(num_expr(expr)?))
Ok(Unresolved::promql_scalar(num_expr(expr)?))
}
Expr::Call(call) if call.func.name == "info" => walk_info(call),
Expr::Call(call) => walk_call(call),
Expand All @@ -277,15 +277,15 @@ fn walk(expr: &Expr) -> Result<Unresolved> {
// identity and `-<literal>` to a negated `NumberLiteral`, so this wraps a
// sub-expression whose samples must be sign-flipped. Now that a scalar
// operand exists (#35), express it as `x * -1` — a constant-foldable
// operand (`-(10*1024)`) collapses to a negated `PromqlScalar` leaf; anything
// else is a vector, sign-flipped by a `Mul` against `PromqlScalar(-1)`. `Mul`
// operand (`-(10*1024)`) collapses to a negated `PromqlScalarBridge` leaf; anything
// else is a vector, sign-flipped by a `Mul` against `PromqlScalarBridge(-1)`. `Mul`
// is commutative, so operand order carries no hazard (#36).
Expr::Unary(u) => match num_expr(&u.expr) {
Ok(v) => Ok(Unresolved::PromqlScalar(-v)),
Ok(v) => Ok(Unresolved::promql_scalar(-v)),
Err(_) => Ok(Unresolved::BinaryOp {
op: BinaryOpKind::Arithmetic(ArithmeticOpKind::Mul),
lhs: Rc::new(walk(&u.expr)?),
rhs: Rc::new(Unresolved::PromqlScalar(-1.0)),
rhs: Rc::new(Unresolved::promql_scalar(-1.0)),
vector_match: None,
}),
},
Expand All @@ -308,7 +308,7 @@ fn walk(expr: &Expr) -> Result<Unresolved> {
// 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(Unresolved::PromqlScalar(n.val)),
Expr::NumberLiteral(n) => Ok(Unresolved::promql_scalar(n.val)),
Expr::StringLiteral(_) => Err(LoweringError::UnsupportedFeature(
"bare string literal".into(),
)),
Expand Down Expand Up @@ -1061,10 +1061,10 @@ 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 `PromqlScalar` leaf (issue #45).
/// `pi()` is the constant π, lowered to a `PromqlScalarBridge` leaf (issue #45).
fn walk_math(call: &Call) -> Result<Unresolved> {
if call.func.name == "pi" {
return Ok(Unresolved::PromqlScalar(std::f64::consts::PI));
return Ok(Unresolved::promql_scalar(std::f64::consts::PI));
}
let func = match call.func.name {
"abs" => MathFunc::Abs,
Expand Down Expand Up @@ -1921,10 +1921,10 @@ fn is_scalar_reducer_fn(name: &str) -> bool {
}

/// A `BinaryOp` operand: fold a pure-scalar expression (`5`, `10*1024*1024`) to
/// a `PromqlScalar` leaf, otherwise walk it as a vector (issue #35).
/// a `PromqlScalarBridge` leaf, otherwise walk it as a vector (issue #35).
fn scalar_or_vector(expr: &Expr) -> Result<Unresolved> {
match num_expr(expr) {
Ok(v) => Ok(Unresolved::PromqlScalar(v)),
Ok(v) => Ok(Unresolved::promql_scalar(v)),
Err(_) => walk(expr),
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ fn intents(e: &QueryExpr) -> Vec<AggIntent> {
}
// `AggIntent` only ever lives in `Aggregate.measures`, never in a
// scalar position (issue #205) — nothing to collect there.
QueryExpr::Scan { .. } | QueryExpr::PromqlScalar(_) | QueryExpr::QueryTimestamp => {}
QueryExpr::Scan { .. }
| QueryExpr::PromqlScalarBridge(_)
| QueryExpr::QueryTimestamp => {}
QueryExpr::Column(_)
| QueryExpr::Literal(_)
| QueryExpr::Compare { .. }
Expand Down Expand Up @@ -262,8 +264,8 @@ fn all_targets_missing_core_lowers() {
#[test]
fn scalar_threshold_comparisons_lower_to_binaryop_scalar() {
// ~822/949 corpus queries are `<vector> <cmp> <scalar>`. The numeric
// threshold is now a `PromqlScalar` operand of the `BinaryOp` (issue #35) — the
// single biggest unblock for real alerts.
// threshold is now a `PromqlScalarBridge` operand of the `BinaryOp` (issue
// #35) — the single biggest unblock for real alerts.
for q in [
"prometheus_config_last_reload_successful != 1",
"increase(prometheus_tsdb_compactions_failed_total[1m]) > 0",
Expand All @@ -273,7 +275,7 @@ fn scalar_threshold_comparisons_lower_to_binaryop_scalar() {
panic!("expected a BinaryOp for {q:?}");
};
assert!(
matches!(rhs.as_ref(), QueryExpr::PromqlScalar(_)),
matches!(rhs.as_ref(), QueryExpr::PromqlScalarBridge(_)),
"scalar threshold operand for {q:?}, got {rhs:?}"
);
}
Expand Down Expand Up @@ -328,7 +330,7 @@ fn vector_literal_lowers_to_a_labelless_vector() {
let QueryExpr::PromqlVectorFromScalar(inner) = &qe else {
panic!("expected PromqlVectorFromScalar, got {qe:?}");
};
assert!(matches!(inner.as_ref(), QueryExpr::PromqlScalar(v) if *v == 1.0));
assert_eq!(inner.as_promql_scalar(), Some(1.0));
// The result is a vector: it carries a time index (unlike a bare scalar).
assert!(qe.output_schema().unwrap().time_index.is_some());
}
Expand Down
76 changes: 42 additions & 34 deletions crates/frontend-promql/tests/promql_conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ fn collect(e: &QueryExpr, out: &mut Vec<AggIntent>) {
}
// `AggIntent` only ever lives in `Aggregate.measures`, never in a
// scalar position (issue #205) — nothing to collect there.
QueryExpr::Scan { .. } | QueryExpr::PromqlScalar(_) | QueryExpr::QueryTimestamp => {}
QueryExpr::Scan { .. } | QueryExpr::PromqlScalarBridge(_) | QueryExpr::QueryTimestamp => {}
QueryExpr::Column(_)
| QueryExpr::Literal(_)
| QueryExpr::Compare { .. }
Expand Down Expand Up @@ -143,11 +143,13 @@ fn has<F: Fn(&AggIntent) -> bool>(e: &QueryExpr, pred: F) -> bool {
intents(e).iter().any(pred)
}

/// Whether the tree contains a `Mul`-by-`PromqlScalar(-1)` anywhere — the shape unary
/// Whether the tree contains a `Mul`-by-`PromqlScalarBridge(-1)` anywhere — the shape unary
/// negation lowers to (issue #36).
fn negates_via_scalar(e: &QueryExpr) -> bool {
let is_neg_one =
|q: &QueryExpr| matches!(q, QueryExpr::PromqlScalar(v) if (*v + 1.0).abs() < 1e-12);
let is_neg_one = |q: &QueryExpr| {
q.as_promql_scalar()
.is_some_and(|v| (v + 1.0).abs() < 1e-12)
};
match e {
QueryExpr::BinaryOp { op, lhs, rhs, .. } => {
(*op == BinaryOpKind::Arithmetic(ArithmeticOpKind::Mul)
Expand Down Expand Up @@ -614,7 +616,7 @@ fn vector_comparison_filters() {
fn unary_negation_lowers_as_multiply_by_minus_one() {
// SEMANTICS (PromQL, issue #36): `-expr` flips the sign of every sample.
// Now that a scalar operand exists (#35), it lowers as `expr * -1` — a `Mul`
// BinaryOp of the (label-preserving) vector against `PromqlScalar(-1)`. These are
// BinaryOp of the (label-preserving) vector against `PromqlScalarBridge(-1)`. These are
// the five cases the old `__GAP` test pinned as rejected.
for q in [
"-rate(http_errors_total[5m])",
Expand All @@ -624,14 +626,14 @@ fn unary_negation_lowers_as_multiply_by_minus_one() {
"sum(-node_cpu_seconds_total)",
] {
let qe = ok(q);
// A `Mul`-by-`-1` against a `PromqlScalar(-1)` appears somewhere in every tree.
// A `Mul`-by-`-1` against a `PromqlScalarBridge(-1)` appears somewhere in every tree.
assert!(
negates_via_scalar(&qe),
"no `* -1` negation found in {q}: {qe:?}"
);
}

// `-some_metric` at the root: `Scan * PromqlScalar(-1)`, schema follows the vector.
// `-some_metric` at the root: `Scan * PromqlScalarBridge(-1)`, schema follows the vector.
let QueryExpr::BinaryOp {
op,
lhs,
Expand All @@ -647,8 +649,9 @@ fn unary_negation_lowers_as_multiply_by_minus_one() {
"vector on the left"
);
assert!(
matches!(rhs.as_ref(), QueryExpr::PromqlScalar(v) if (*v + 1.0).abs() < 1e-12),
"negation multiplies by PromqlScalar(-1), got {rhs:?}"
rhs.as_promql_scalar()
.is_some_and(|v| (v + 1.0).abs() < 1e-12),
"negation multiplies by PromqlScalarBridge(-1), got {rhs:?}"
);
assert!(
vector_match.is_none(),
Expand Down Expand Up @@ -686,11 +689,10 @@ fn unary_negation_lowers_as_multiply_by_minus_one() {
#[test]
fn unary_negation_of_constant_folds_to_scalar() {
// `-(10*1024*1024)` — the operand is constant-foldable, so negation collapses
// to a single negated `PromqlScalar` leaf (no `BinaryOp`), just like a bare literal.
assert!(matches!(
ok("-(10*1024*1024)"),
QueryExpr::PromqlScalar(v) if (v + 10_485_760.0).abs() < 1e-6
));
// to a single negated `PromqlScalarBridge` leaf (no `BinaryOp`), just like a bare literal.
assert!(ok("-(10*1024*1024)")
.as_promql_scalar()
.is_some_and(|v| (v + 10_485_760.0).abs() < 1e-6));
}

#[test]
Expand Down Expand Up @@ -749,9 +751,9 @@ fn count_maps_to_cardinality_and_inherits_accuracy() {

#[test]
fn scalar_literal_operand_lowers_as_binaryop_scalar() {
// Issue #35: `<vector> op <scalar>` — the numeric threshold is a `PromqlScalar`
// operand of the `BinaryOp`, and constant arithmetic (`10*1024*1024`) is
// folded. The output schema is the vector side's.
// Issue #35: `<vector> op <scalar>` — the numeric threshold is a
// `PromqlScalarBridge` operand of the `BinaryOp`, and constant arithmetic
// (`10*1024*1024`) is folded. The output schema is the vector side's.
let qe = ok("node_filesystem_avail_bytes > 10*1024*1024");
let QueryExpr::BinaryOp { op, lhs, rhs, .. } = &qe else {
panic!("expected a BinaryOp, got {qe:?}");
Expand All @@ -762,7 +764,8 @@ fn scalar_literal_operand_lowers_as_binaryop_scalar() {
"vector on the left"
);
assert!(
matches!(rhs.as_ref(), QueryExpr::PromqlScalar(v) if (*v - 10_485_760.0).abs() < 1e-6),
rhs.as_promql_scalar()
.is_some_and(|v| (v - 10_485_760.0).abs() < 1e-6),
"folded scalar threshold on the right, got {rhs:?}"
);
// Schema derivation follows the vector side (a scalar contributes no labels).
Expand All @@ -772,13 +775,15 @@ fn scalar_literal_operand_lowers_as_binaryop_scalar() {
#[test]
fn scalar_arithmetic_scales_the_vector() {
// `rate(m[5m]) * 100` — a unit conversion. Arithmetic BinaryOp of the vector
// with a `PromqlScalar(100)`.
// with a `PromqlScalarBridge(100)`.
let qe = ok("rate(m[5m]) * 100");
let QueryExpr::BinaryOp { op, rhs, .. } = &qe else {
panic!("expected a BinaryOp, got {qe:?}");
};
assert_eq!(*op, BinaryOpKind::Arithmetic(ArithmeticOpKind::Mul));
assert!(matches!(rhs.as_ref(), QueryExpr::PromqlScalar(v) if (*v - 100.0).abs() < 1e-9));
assert!(rhs
.as_promql_scalar()
.is_some_and(|v| (v - 100.0).abs() < 1e-9));
}

// ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1695,10 +1700,10 @@ fn clamp_and_round_carry_their_params() {

#[test]
fn pi_lowers_to_a_scalar_constant() {
// `pi()` is the constant π — a `PromqlScalar` leaf, not a `Math` intent.
assert!(
matches!(ok("pi()"), QueryExpr::PromqlScalar(v) if (v - std::f64::consts::PI).abs() < 1e-12)
);
// `pi()` is the constant π — a `PromqlScalarBridge` leaf, not a `Math` intent.
assert!(ok("pi()")
.as_promql_scalar()
.is_some_and(|v| (v - std::f64::consts::PI).abs() < 1e-12));
}

// ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1826,7 +1831,7 @@ fn vector_promotes_a_scalar_to_a_vector() {
let QueryExpr::PromqlVectorFromScalar(inner) = &qe else {
panic!("expected PromqlVectorFromScalar, got {qe:?}");
};
assert!(matches!(inner.as_ref(), QueryExpr::PromqlScalar(v) if *v == 1.0));
assert_eq!(inner.as_promql_scalar(), Some(1.0));
// Vector-typed: schema has a time index (a scalar leaf has none).
let sch = qe.output_schema().unwrap();
assert!(sch.time_index.is_some());
Expand All @@ -1842,7 +1847,7 @@ fn scalar_collapses_a_vector_to_a_scalar() {
};
let (metric, _) = first_scan(inner);
assert_eq!(metric, "node_load1");
// PromqlScalar-typed: single `value` column, no time index.
// PromqlScalarBridge-typed: single `value` column, no time index.
let sch = qe.output_schema().unwrap();
assert!(sch.time_index.is_none());
assert_eq!(sch.columns.len(), 1);
Expand Down Expand Up @@ -2234,25 +2239,28 @@ fn sort_by_label_desc_is_descending() {
#[test]
fn min_of_max_of_fold_constant_scalars() {
// `min_of`/`max_of` are n-ary scalar reducers. When every argument is a
// constant they constant-fold to a `PromqlScalar` leaf, just like scalar
// constant they constant-fold to a `PromqlScalarBridge` leaf, just like scalar
// arithmetic (#35) — the only form the intent algebra can hold (#89).
assert!(matches!(ok("min_of(3, 5)"), QueryExpr::PromqlScalar(v) if v == 3.0));
assert!(matches!(ok("max_of(3, 5)"), QueryExpr::PromqlScalar(v) if v == 5.0));
assert!(matches!(ok("min_of(-2, -5)"), QueryExpr::PromqlScalar(v) if v == -5.0));
assert_eq!(ok("min_of(3, 5)").as_promql_scalar(), Some(3.0));
assert_eq!(ok("max_of(3, 5)").as_promql_scalar(), Some(5.0));
assert_eq!(ok("min_of(-2, -5)").as_promql_scalar(), Some(-5.0));
// Nested folds and use as a threshold operand.
assert!(matches!(ok("max_of(min_of(2, 3), 10)"), QueryExpr::PromqlScalar(v) if v == 10.0));
assert_eq!(
ok("max_of(min_of(2, 3), 10)").as_promql_scalar(),
Some(10.0)
);
let qe = ok("up > max_of(1, 2)");
let QueryExpr::BinaryOp { rhs, .. } = &qe else {
panic!("{qe:?}")
};
assert!(matches!(rhs.as_ref(), QueryExpr::PromqlScalar(v) if *v == 2.0));
assert_eq!(rhs.as_promql_scalar(), Some(2.0));
}

#[test]
fn min_of_max_of_ignore_nan_like_the_min_max_aggregators() {
// A NaN argument is skipped (Prometheus `min`/`max` NaN semantics).
assert!(matches!(ok("max_of(3, NaN)"), QueryExpr::PromqlScalar(v) if v == 3.0));
assert!(matches!(ok("min_of(NaN, 3)"), QueryExpr::PromqlScalar(v) if v == 3.0));
assert_eq!(ok("max_of(3, NaN)").as_promql_scalar(), Some(3.0));
assert_eq!(ok("min_of(NaN, 3)").as_promql_scalar(), Some(3.0));
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ fn intents(e: &QueryExpr) -> Vec<AggIntent> {
QueryExpr::PromqlVectorFromScalar(inner) | QueryExpr::PromqlScalarFromVector(inner) => {
go(inner, out)
}
QueryExpr::Scan { .. } | QueryExpr::PromqlScalar(_) | QueryExpr::QueryTimestamp => {}
QueryExpr::Scan { .. }
| QueryExpr::PromqlScalarBridge(_)
| QueryExpr::QueryTimestamp => {}
// Scalar expression variants (issue #205): `AggIntent` only ever
// lives in `Aggregate.measures`, never nested inside a scalar
// expression tree, so there's nothing to recurse into here.
Expand Down
2 changes: 1 addition & 1 deletion crates/frontend-sql/tests/netflow/netflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ fn visit(qe: &QueryExpr, f: &mut impl FnMut(&QueryExpr)) {
QueryExpr::PromqlVectorFromScalar(child) | QueryExpr::PromqlScalarFromVector(child) => {
visit(child, f)
}
QueryExpr::Scan { .. } | QueryExpr::PromqlScalar(_) | QueryExpr::QueryTimestamp => {}
QueryExpr::Scan { .. } | QueryExpr::PromqlScalarBridge(_) | QueryExpr::QueryTimestamp => {}
// Scalar expression variants (issue #205) aren't relational nodes;
// this visitor only walks the relational tree, so stop here.
QueryExpr::Column(_)
Expand Down
6 changes: 3 additions & 3 deletions crates/integration-tests/tests/binary_op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,13 @@ fn q21_div_two_sum_by_job() {
}

// #36 — unary negation lowers as `expr * -1`: a Mul BinaryOp of the vector
// against PromqlScalar(-1), no vector match. The vector side keeps its schema.
// against PromqlScalarBridge(-1), no vector match. The vector side keeps its schema.
#[test]
fn q36_unary_negation_is_multiply_by_minus_one() {
let expected = QueryExpr::BinaryOp {
op: BinaryOpKind::Arithmetic(ArithmeticOpKind::Mul),
lhs: Rc::new(scan("some_metric", &[])),
rhs: Rc::new(QueryExpr::PromqlScalar(-1.0)),
rhs: Rc::new(QueryExpr::promql_scalar(-1.0)),
vector_match: None,
};
assert_eq!(lower("-some_metric"), expected);
Expand All @@ -253,7 +253,7 @@ fn q36_sum_of_negation_nests() {
child: Rc::new(QueryExpr::BinaryOp {
op: BinaryOpKind::Arithmetic(ArithmeticOpKind::Mul),
lhs: Rc::new(scan("node_cpu_seconds_total", &[])),
rhs: Rc::new(QueryExpr::PromqlScalar(-1.0)),
rhs: Rc::new(QueryExpr::promql_scalar(-1.0)),
vector_match: None,
}),
};
Expand Down
Loading
Loading