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
12 changes: 8 additions & 4 deletions crates/frontend-promql/src/promql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ fn walk_histogram_quantiles(call: &Call) -> Result<Unresolved> {
let sketchable = histogram_arg_is_sketchable(vec_expr);
let branches = (2..call.args.args.len())
.map(|i| {
let phi = quantile_param(num_arg(call, i)?)?;
let phi = bounded_quantile_param(num_arg(call, i)?)?;
let intent = if sketchable {
AggIntent::Quantile {
col: None,
Expand Down Expand Up @@ -2010,10 +2010,14 @@ fn ratio_param(agg: &AggregateExpr) -> Result<f64> {
Ok(r.clamp(-1.0, 1.0))
}

/// Quantile φ — must be a finite value in `[0, 1]`. Rejects NaN/∞ and
/// out-of-range φ (which would otherwise propagate into a bogus intent and
/// output-column name like `quantile_NaN`).
/// Preserve the full Prometheus quantile parameter domain, including special values.
fn quantile_param(q: f64) -> Result<f64> {
// Prometheus returns NaN/-Inf/+Inf for these parameters at execution time.
Ok(q)
}

// The non-standard histogram_quantiles extension keeps its bounded label contract.
fn bounded_quantile_param(q: f64) -> Result<f64> {
if q.is_finite() && (0.0..=1.0).contains(&q) {
Ok(q)
} else {
Expand Down
40 changes: 40 additions & 0 deletions crates/frontend-promql/tests/promql_binding_regressions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use asap_frontend_promql::lower_promql;
use asap_types::types::AccuracyTarget;

/// Prometheus treats these quantile parameters as valid queries returning special values.
#[test]
fn quantile_parameters_retain_prometheus_special_value_semantics() {
for parameter in ["-0.1", "1.1", "NaN", "+Inf", "-Inf"] {
for query in [
format!("quantile({parameter}, smoke_gauge)"),
format!("quantile_over_time({parameter}, smoke_gauge[5m])"),
format!("histogram_quantile({parameter}, smoke_bucket)"),
] {
assert!(
lower_promql(&query, AccuracyTarget::Exact).is_ok(),
"{query}"
);
}
}
}

/// Instant rate and extrapolated rate need different execution kernels, including subqueries.
#[test]
fn irate_and_rate_have_distinct_canonical_intents() {
for input in ["smoke_counter_total[5m]", "smoke_counter_total[5m:1m]"] {
let rate = lower_promql(&format!("rate({input})"), AccuracyTarget::Exact).unwrap();
let irate = lower_promql(&format!("irate({input})"), AccuracyTarget::Exact).unwrap();
assert_ne!(rate, irate, "rate and irate must not collapse: {input}");
}
}

/// PromQL count counts series even when two sample values are equal.
#[test]
fn count_is_row_count_not_distinct_sample_value_count() {
use asap_types::pre_asap::{AggIntent, QueryExpr};
let tree = lower_promql("count(smoke_gauge)", AccuracyTarget::Exact).unwrap();
let QueryExpr::Aggregate { measures, .. } = tree else {
panic!("expected aggregate")
};
assert!(matches!(measures.as_slice(), [AggIntent::Count { .. }]));
}
19 changes: 11 additions & 8 deletions crates/frontend-promql/tests/promql_lowering.rs
Original file line number Diff line number Diff line change
Expand Up @@ -755,15 +755,18 @@ fn fractional_or_negative_topk_k_is_rejected() {
}

#[test]
fn out_of_range_quantile_phi_is_rejected() {
// φ outside [0,1] would otherwise yield a bogus `quantile_1_5` column.
assert!(lower_promql("quantile(1.5, up)", AccuracyTarget::Exact).is_err());
assert!(lower_promql("quantile_over_time(1.5, m[5m])", AccuracyTarget::Exact).is_err());
assert!(lower_promql(
fn out_of_range_quantile_phi_is_accepted() {
// Prometheus defines out-of-range phi results; lowering must preserve it.
for query in [
"quantile(1.5, up)",
"quantile_over_time(1.5, m[5m])",
"histogram_quantile(2.0, rate(b[5m]))",
AccuracyTarget::Exact
)
.is_err());
] {
assert!(
lower_promql(query, AccuracyTarget::Exact).is_ok(),
"{query}"
);
}
}

#[test]
Expand Down
Loading