diff --git a/crates/frontend-promql/src/promql.rs b/crates/frontend-promql/src/promql.rs index 822987f2..424666aa 100644 --- a/crates/frontend-promql/src/promql.rs +++ b/crates/frontend-promql/src/promql.rs @@ -731,7 +731,7 @@ fn walk_histogram_quantiles(call: &Call) -> Result { 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, @@ -2010,10 +2010,14 @@ fn ratio_param(agg: &AggregateExpr) -> Result { 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 { + // 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 { if q.is_finite() && (0.0..=1.0).contains(&q) { Ok(q) } else { diff --git a/crates/frontend-promql/tests/promql_binding_regressions.rs b/crates/frontend-promql/tests/promql_binding_regressions.rs new file mode 100644 index 00000000..97d177f9 --- /dev/null +++ b/crates/frontend-promql/tests/promql_binding_regressions.rs @@ -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 { .. }])); +} diff --git a/crates/frontend-promql/tests/promql_lowering.rs b/crates/frontend-promql/tests/promql_lowering.rs index 539c90b9..5fc02f72 100644 --- a/crates/frontend-promql/tests/promql_lowering.rs +++ b/crates/frontend-promql/tests/promql_lowering.rs @@ -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]