Skip to content

graph: fix Timestamp::from_microseconds_since_epoch for sub-second negative values - #6747

Open
parthod0x wants to merge 1 commit into
graphprotocol:masterfrom
parthod0x:fix/timestamp-negative-micros
Open

parthod0x wants to merge 1 commit into
graphprotocol:masterfrom
parthod0x:fix/timestamp-negative-micros

Conversation

@parthod0x

Copy link
Copy Markdown

What

Timestamp::from_microseconds_since_epoch rejects valid sub-second negative (pre-epoch) timestamps.

The bug

graph/src/data/store/scalar/timestamp.rs:

let secs = micros / 1_000_000;
let ns = (micros % 1_000_000) * 1_000;
match DateTime::from_timestamp(secs, ns as u32) { ... }

Rust's % truncates toward zero, so for a negative micros with a fractional second the remainder is negative → ns is negative → ns as u32 wraps to a huge value (≥ 1e9) → DateTime::from_timestamp returns None → the instant is wrongly rejected as InvalidTimestamp.

This breaks the round-trip invariant from_microseconds_since_epoch(t.as_microseconds_since_epoch()) == t for any negative Timestamp. Whole-second negatives (e.g. -1_000_000) happen to work; only sub-second negatives (e.g. -1_500_000, 1.5s before epoch) fail. It's reachable from subgraph mappings (Timestamp.fromMicroseconds) and GraphQL arg coercion — the symptom is a spurious parse/coercion error (no panic, no corruption).

The fix

Use div_euclid/rem_euclid so the nanosecond remainder is always in [0, 1e6). Positive values are unchanged.

Tests

Adds a round-trip test over positive, negative and sub-second-negative micros.

Verification note

The function is self-contained (chrono + integer arithmetic). I verified the exact arithmetic against the pinned chrono = 0.4.45 in a standalone crate: the old //% form returns None for -1_500_000, and the div_euclid/rem_euclid form round-trips {0, ±1, ±1_500_000, ±999_999, -1_000_000, 1.7e15}. I couldn't run the in-crate cargo test -p graph locally (the graph crate needs system OpenSSL, unavailable in my environment), so CI will confirm the added test in situ.

…gative 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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant