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
24 changes: 24 additions & 0 deletions crates/frontend-promql/src/promql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
//! | `rate/irate(m[w])` | `Aggregate{[Rate{w}]}` (no Window) — `irate` shares the `rate` *intent*; the avg-vs-last-two-samples difference is an L4 estimation method |
//! | `increase(m[w])` | `Aggregate{[Increase{w}]}` (no Window) |
//! | `changes`/`delta`/`idelta`/`deriv`/`resets`/`predict_linear`/`double_exponential_smoothing`(`m[w]`, …) | `Aggregate{[Changes/Delta/…], Window{w}}` — per-series counter-derivative intents (issue #44); `holt_winters` is the legacy alias of `double_exponential_smoothing` |
//! | `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) |
//! | `abs`/`ceil`/`sqrt`/`ln`/`clamp*`/`round`/trig(`v`), `pi()` | `Aggregate{[Math(f)]}` element-wise transform (issue #45); `pi()` → a `Scalar` leaf |
//! | `group` / `offset` / `@` | **rejected** — distinct semantics with no intent-algebra representation yet |
//! | `OUTER by (dims) (…)` | `Aggregate.keys = dims` (→ positional `Aggregate.by` in L3; generic `topk by`/`bottomk` grouping → `Sort.partition_by`) |
//! | `count by (d) (…)` | `Aggregate{[CountDistinct], …}` (→ `Cardinality`) |
Expand Down Expand Up @@ -167,6 +169,7 @@ fn walk(expr: &Expr) -> Result<L2> {
Expr::Aggregate(agg) => walk_aggregate(agg),
Expr::Call(call) if call.func.name.starts_with("histogram_") => walk_histogram(call),
Expr::Call(call) if is_math_fn(call.func.name) => walk_math(call),
Expr::Call(call) if is_presence_fn(call.func.name) => walk_presence(call),
Expr::Call(call) => walk_call(call),
Expr::Binary(bin) => walk_binary(bin),
Expr::Paren(p) => walk(&p.expr),
Expand Down Expand Up @@ -460,6 +463,27 @@ fn walk_histogram(call: &Call) -> Result<L2> {
Ok(outer_aggregate(vec![], func, walk(arg(call, vec_idx)?)?))
}

/// The presence functions (issue #47).
fn is_presence_fn(name: &str) -> bool {
matches!(name, "absent" | "absent_over_time" | "present_over_time")
}

/// `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<L2> {
let func = match call.func.name {
"absent" => AggFunc::Absent,
"absent_over_time" => AggFunc::AbsentOverTime,
"present_over_time" => AggFunc::PresentOverTime,
other => return Err(LoweringError::UnsupportedFunction(other.to_string())),
};
// arg 0 is the instant vector (`absent`) or range vector (`*_over_time`);
// `walk` produces a `Window` for the matrix-selector forms.
Ok(outer_aggregate(vec![], func, walk(arg(call, 0)?)?))
}

/// The element-wise math / trig functions (issue #45).
fn is_math_fn(name: &str) -> bool {
matches!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,10 +260,11 @@ fn scalar_threshold_comparisons_lower_to_binaryop_scalar() {
}

#[test]
fn absent_function_is_rejected__GAP() {
// `absent(up{job="prometheus"})` — "job missing" alerts. `absent` has no
// intent-algebra representation yet.
let _ = rejected(r#"absent(up{job="prometheus"})"#);
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.
let qe = ok(r#"absent(up{job="prometheus"})"#);
assert!(intents(&qe).iter().any(|i| matches!(i, AggIntent::Absent)));
}

#[test]
Expand Down
31 changes: 29 additions & 2 deletions crates/frontend-promql/tests/promql_conformance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,8 +773,6 @@ fn unsupported_functions_are_rejected() {
for q in [
"time()",
"timestamp(up)",
"absent(up)",
"absent_over_time(up[5m])",
r#"label_replace(up, "host", "$1", "instance", "(.+):.*")"#,
// NOTE: counter-derivatives (#44) now lower — see section M; the math /
// trig family (`abs`/`clamp*`/`ln`/…, #45) lowers — see section O.
Expand Down Expand Up @@ -1099,3 +1097,32 @@ fn pi_lowers_to_a_scalar_constant() {
// `pi()` is the constant π — a `Scalar` leaf, not a `Math` intent.
assert!(matches!(ok("pi()"), QueryExpr::Scalar(v) if (v - std::f64::consts::PI).abs() < 1e-12));
}

// ─────────────────────────────────────────────────────────────────────────────
// P. Presence functions (functions.test; issue #47)
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn presence_functions_lower_to_presence_intents() {
for (q, want) in [
(r#"absent(up{job="x"})"#, AggIntent::Absent),
("absent_over_time(m[1h])", AggIntent::AbsentOverTime),
("present_over_time(m[5m])", AggIntent::PresentOverTime),
] {
let qe = ok(q);
assert!(intents(&qe).contains(&want), "{q}: got {:?}", intents(&qe));
}
}

#[test]
fn absent_keeps_matcher_labels_for_the_synthesized_output() {
// `absent(v)` synthesizes its output labels from `v`'s equality matchers, so
// those labels must survive into the schema — here `job` from `{job="x"}`.
let qe = ok(r#"absent(up{job="x"})"#);
let cols = qe.output_schema().unwrap();
assert!(
cols.columns.iter().any(|c| c.name == "job"),
"matcher label `job` kept, got {:?}",
cols.columns.iter().map(|c| &c.name).collect::<Vec<_>>()
);
}
26 changes: 25 additions & 1 deletion crates/ir/src/intent_algebra/agg_intent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,21 @@ pub enum AggIntent {
/// one value out per input sample. (`pi()` is a constant, lowered to a
/// scalar leaf, not this.)
Math(MathFunc),

// ── 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
// `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.
Absent,
/// PromQL `absent_over_time(v[w])` — `absent` over a range vector.
AbsentOverTime,
/// PromQL `present_over_time(v[w])` — value 1 per series that has any sample
/// in the range (per-series).
PresentOverTime,
}

/// The element-wise math / trig functions (issue #45). Unary over the sample
Expand Down Expand Up @@ -215,7 +230,10 @@ impl AggIntent {
| Self::HistogramAvg
| Self::HistogramStdDev
| Self::HistogramStdVar
| Self::HistogramFraction { .. } => DataModel::TimeSeries,
| Self::HistogramFraction { .. }
| Self::Absent
| Self::AbsentOverTime
| Self::PresentOverTime => DataModel::TimeSeries,
_ => DataModel::Any,
}
}
Expand Down Expand Up @@ -244,6 +262,9 @@ impl AggIntent {
| Self::HistogramStdVar
| Self::HistogramFraction { .. }
| Self::Math(_)
| Self::Absent
| Self::AbsentOverTime
| Self::PresentOverTime
)
}

Expand Down Expand Up @@ -313,6 +334,9 @@ impl AggIntent {
col("histogram_quantile", DataType::Float64, false)
}
AggIntent::Math(_) => col("value", DataType::Float64, false),
AggIntent::Absent => col("absent", DataType::Float64, false),
AggIntent::AbsentOverTime => col("absent_over_time", DataType::Float64, false),
AggIntent::PresentOverTime => col("present_over_time", DataType::Float64, false),
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/l2/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,9 @@ fn agg_func_to_intent(func: &AggFunc, acc: &AccuracyTarget, col: Option<ColumnId
},
AggFunc::HistogramQuantile(q) => AggIntent::HistogramQuantile { q: *q },
AggFunc::Math(m) => AggIntent::Math(m.clone()),
AggFunc::Absent => AggIntent::Absent,
AggFunc::AbsentOverTime => AggIntent::AbsentOverTime,
AggFunc::PresentOverTime => AggIntent::PresentOverTime,
}
}

Expand Down
6 changes: 6 additions & 0 deletions crates/l2/src/relational.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ pub enum AggFunc {
/// PromQL element-wise math / trig transform (`abs`/`sqrt`/`clamp_max`/…) →
/// `AggIntent::Math` (issue #45).
Math(MathFunc),
/// PromQL `absent` → `AggIntent::Absent` (issue #47).
Absent,
/// PromQL `absent_over_time` → `AggIntent::AbsentOverTime`.
AbsentOverTime,
/// PromQL `present_over_time` → `AggIntent::PresentOverTime`.
PresentOverTime,
}

/// The Layer-2 relational query IR.
Expand Down
Loading