From 09cb32a3e36155f14f315e5676572900a4e69864 Mon Sep 17 00:00:00 2001 From: Dennis Zhuang Date: Sat, 17 Oct 2026 23:07:11 +0800 Subject: [PATCH] fix(promql): report 0 from ts_of_last_over_time for negative timestamps Prometheus 3.15 takes the max of the last float timestamp and the histogram one, which defaults to 0 without histograms. Signed-off-by: Dennis Zhuang --- src/promql/src/range_eval/function.rs | 7 ++++--- src/promql/src/range_eval/tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/promql/src/range_eval/function.rs b/src/promql/src/range_eval/function.rs index 8b73d67c073..578630e898b 100644 --- a/src/promql/src/range_eval/function.rs +++ b/src/promql/src/range_eval/function.rs @@ -125,10 +125,11 @@ impl RangeFunction { Self::TsOfFirstOverTime => { Box::new(move |window| Some(seconds(window.timestamps[0] - offset_ms))) } + // Prometheus takes the max with the histogram timestamp, 0 when there is none, so a + // negative timestamp reports 0. Self::TsOfLastOverTime => Box::new(move |window| { - Some(seconds( - window.timestamps[window.timestamps.len() - 1] - offset_ms, - )) + let last = window.timestamps[window.timestamps.len() - 1] - offset_ms; + Some(seconds(last.max(0))) }), Self::TsOfMinOverTime => Box::new(move |window| { Some(seconds(timestamp_of_extremum(&window, false) - offset_ms)) diff --git a/src/promql/src/range_eval/tests.rs b/src/promql/src/range_eval/tests.rs index 981c659a309..af3688ed8a7 100644 --- a/src/promql/src/range_eval/tests.rs +++ b/src/promql/src/range_eval/tests.rs @@ -1026,3 +1026,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()); } + +/// Prometheus 3.15 reports `ts_of_last_over_time` as the max of the last float timestamp and +/// a histogram timestamp that defaults to 0, so a window of negative timestamps reports 0; +/// `ts_of_first_over_time` takes the min and keeps them. +#[test] +fn ts_of_last_over_time_reports_zero_for_negative_timestamps() { + let window = RangeWindow { + range_ms: 2_000, + offset_ms: 0, + anchor_ms: None, + }; + let rows: Vec = vec![(-1_000, vec![Some(7.0)])]; + for (function, expected) in [ + (RangeFunction::TsOfLastOverTime, 0.0), + (RangeFunction::TsOfFirstOverTime, -1.0), + ] { + let mut evaluator = + RangeEvaluator::try_new(&raw_schema(1), grid(0, 1_000, 1), window, function).unwrap(); + let series = evaluator.push(&chunk(&rows, 1, 0, true)).unwrap().unwrap(); + assert_series_eq( + Some(&series), + &vec![vec![Some(expected)]], + &output_metric(function, 0), + ); + } +}