From 32a0d12e5a4079f1d510256363773133b61c99d0 Mon Sep 17 00:00:00 2001 From: parthod0x Date: Sun, 25 Oct 2026 08:46:28 +0000 Subject: [PATCH] graph: fix Timestamp::from_microseconds_since_epoch for sub-second negative values from_microseconds_since_epoch used truncating `/` and `%`, so for a negative micros with a fractional second the `ns` remainder was negative. `ns as u32` then wrapped to a value >= 1e9 and DateTime::from_timestamp rejected a valid pre-epoch instant as InvalidTimestamp. This broke the round-trip invariant from_microseconds_since_epoch(t.as_microseconds_since_epoch()) == t for any negative Timestamp - whole-second negatives happened to work, only sub-second ones failed (e.g. -1_500_000, i.e. 1.5s before the epoch). Use div_euclid/rem_euclid so the nanosecond remainder is always in [0, 1e6). Positive values are unchanged (div_euclid/rem_euclid equal / and % there). Adds a round-trip test over positive, negative and sub-second-negative micros. --- graph/src/data/store/scalar/timestamp.rs | 39 ++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/graph/src/data/store/scalar/timestamp.rs b/graph/src/data/store/scalar/timestamp.rs index 1d9026d5a72..cb4d858541f 100644 --- a/graph/src/data/store/scalar/timestamp.rs +++ b/graph/src/data/store/scalar/timestamp.rs @@ -44,8 +44,12 @@ impl Timestamp { } pub fn from_microseconds_since_epoch(micros: i64) -> Result { - let secs = micros / 1_000_000; - let ns = (micros % 1_000_000) * 1_000; + // Euclidean div/rem so the nanosecond remainder stays in [0, 1e6) for + // negative (pre-epoch) micros too. Plain `/`/`%` truncate toward zero, so a + // sub-second negative value yields a negative `ns`, which `as u32` wraps to a + // huge value and `DateTime::from_timestamp` then rejects as invalid. + let secs = micros.div_euclid(1_000_000); + let ns = micros.rem_euclid(1_000_000) * 1_000; match DateTime::from_timestamp(secs, ns as u32) { Some(dt) => Ok(Self(dt)), @@ -123,3 +127,34 @@ impl FromSql for Timestamp { .map(Timestamp) } } + +#[cfg(test)] +mod tests { + use super::Timestamp; + + #[test] + fn from_microseconds_round_trips_including_negatives() { + // Pre-epoch (negative) microsecond values, including sub-second ones, must + // decode to a valid Timestamp and round-trip. Before the Euclidean-div fix, + // sub-second negatives (e.g. -1_500_000) were wrongly rejected as invalid. + for micros in [ + 0i64, + 1, + -1, + 1_500_000, + -1_500_000, + 999_999, + -999_999, + -1_000_000, + 1_700_000_000_000_000, + ] { + let ts = Timestamp::from_microseconds_since_epoch(micros) + .unwrap_or_else(|_| panic!("{micros} should be a valid timestamp")); + assert_eq!( + ts.as_microseconds_since_epoch(), + micros, + "round-trip failed for {micros}" + ); + } + } +}