diff --git a/src/promql/src/range_eval/function.rs b/src/promql/src/range_eval/function.rs index f48ea7a469e..6ebe3aa23ad 100644 --- a/src/promql/src/range_eval/function.rs +++ b/src/promql/src/range_eval/function.rs @@ -184,11 +184,14 @@ fn quantile(q: f64, values: &[f64], sorted: &mut Vec) -> f64 { } sorted.clear(); sorted.extend_from_slice(values); - sorted.sort_unstable_by(|a, b| match (a.is_nan(), b.is_nan()) { + // Go's `vectorByValueHeap` puts NaN first and treats +0 and -0 as equal; Go sorts short + // inputs by insertion, which keeps equal values in window order, and which zero is picked + // decides the sign of the result. + sorted.sort_by(|a, b| match (a.is_nan(), b.is_nan()) { (true, true) => std::cmp::Ordering::Equal, (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, - (false, false) => a.total_cmp(b), + (false, false) => a.partial_cmp(b).expect("neither is NaN"), }); let n = sorted.len() as f64; let rank = q * (n - 1.0); diff --git a/src/promql/src/range_eval/tests.rs b/src/promql/src/range_eval/tests.rs index 0a129f6f814..5e26eb39f23 100644 --- a/src/promql/src/range_eval/tests.rs +++ b/src/promql/src/range_eval/tests.rs @@ -27,7 +27,9 @@ use datatypes::series::histogram::{ BucketSideView, CounterResetHint, HistogramBatchBuilder, HistogramView, }; use datatypes::series::raw::{RawSeriesChunk, RawTimestamps}; -use datatypes::series::samples::{FieldSpec, Samples, Series, SeriesFields, SeriesSchema}; +use datatypes::series::samples::{ + FieldSpec, SampleRef, Samples, Series, SeriesFields, SeriesSchema, +}; use datatypes::series::time::{EvalGrid, GridSpec}; use datatypes::series::{Label, MetricName}; use rand::rngs::StdRng; @@ -986,3 +988,29 @@ fn waiting_for_a_point_drops_samples_behind_its_window() { let last = chunk(&[(100_000, vec![Some(1.0)])], 1, 0, true); assert!(evaluator.push(&last).unwrap().is_some()); } + +/// Equal values keep window order in `quantile_over_time`, so the zero a quantile lands on is +/// the one Prometheus picks: `1 / quantile_over_time(1, m[3s])` over +0, -0, -0 is -Inf. +#[test] +fn quantile_keeps_the_window_order_of_signed_zeros() { + let window = RangeWindow { + range_ms: 3_000, + offset_ms: 0, + anchor_ms: None, + }; + let rows: Vec = [(0, 0.0), (1_000, -0.0), (2_000, -0.0)] + .map(|(ts, value)| (ts, vec![Some(value)])) + .to_vec(); + let mut evaluator = RangeEvaluator::try_new( + &raw_schema(1), + grid(2_000, 1_000, 1), + window, + RangeFunction::QuantileOverTime(1.0), + ) + .unwrap(); + let series = evaluator.push(&chunk(&rows, 1, 0, true)).unwrap().unwrap(); + let SampleRef::Float(value) = series.fields.as_slice()[0].sample(0) else { + panic!("expected a float"); + }; + assert_eq!(1.0 / value, f64::NEG_INFINITY); +}