fix(promql): keep the first of equal classic bucket bounds like Prometheus

Prometheus compares bounds with < and >, so +0 and -0 are equal, keep
input order, and coalescing keeps the first; ordering -0 first returned
the other zero and flipped the sign of an infinite reciprocal.

Signed-off-by: Dennis Zhuang <killme2008@gmail.com>
This commit is contained in:
Dennis Zhuang
2026-10-17 23:21:36 +08:00
parent 642cfc7c79
commit 329fed910b
2 changed files with 41 additions and 2 deletions
+8 -2
View File
@@ -90,8 +90,14 @@ pub fn evaluate_histograms(
let mut native_buckets = Vec::new();
let mut values = vec![None; schema.fields.len()];
for mut group in groups.into_values() {
// Pre-sorting by bound keeps the buckets of every point sorted.
group.classic.sort_by(|a, b| a.0.total_cmp(&b.0));
// Pre-sorting by bound keeps the buckets of every point sorted. Like Go's comparison,
// +0 and -0 are equal and keep input order, and coalescing keeps the first bound.
group
.classic
.sort_by(|a, b| match (a.0.is_nan(), b.0.is_nan()) {
(false, false) => a.0.partial_cmp(&b.0).expect("neither is NaN"),
(nan_a, nan_b) => nan_a.cmp(&nan_b),
});
let ranges = group
.classic
.iter()
+33
View File
@@ -368,3 +368,36 @@ fn close(a: f64, b: f64, epsilon: f64) -> bool {
};
equal || (a.abs() < 1e-10 && b.abs() < 1e-10)
}
/// Bounds +0 and -0 compare equal and keep input order, so coalescing keeps the first one:
/// `1 / histogram_quantile(0, sort_desc(h))` over `le` "+Inf", "0", "-0" is +Inf.
#[test]
fn classic_bounds_keep_the_first_signed_zero() {
let grid = Arc::new(
datatypes::series::time::EvalGrid::try_new(
datatypes::series::time::GridSpec::try_new(0, 1_000, 1).unwrap(),
)
.unwrap(),
);
let schema = Arc::new(SeriesSchema {
fields: vec![FieldSpec {
name: Arc::from("value"),
}]
.into(),
eval_grid: Some(grid),
});
let buckets: Vec<_> = [("+Inf", 3.0), ("0", 2.0), ("-0", 1.0)]
.iter()
.map(|(le, count)| {
let labels = Labels::from([
("__name__".to_string(), "h".to_string()),
("le".to_string(), le.to_string()),
]);
let points = BTreeMap::from([(0, f64::to_bits(*count))]);
mixed_series(&schema, &labels, &points, &BTreeMap::new()).unwrap()
})
.collect();
let result = evaluate_histograms(HistogramFunction::Quantile(0.0), &schema, &buckets).unwrap();
let points = points_of(&result[0]);
assert_eq!(1.0 / f64::from_bits(points[&0]), f64::INFINITY);
}