Skip to main content

grafeo_common/types/
validity.rs

1//! Reverse-ordered validity timestamp for versioned storage.
2//!
3//! When used as a key component in disk-backed storage, the reverse ordering
4//! ensures most-recent versions appear first in sorted key order, enabling
5//! efficient "get latest version at epoch X" queries with a single range scan.
6
7use std::cmp::{Ordering, Reverse};
8
9/// Reverse-ordered validity timestamp for efficient disk-storage scans.
10///
11/// Wraps a signed 64-bit timestamp with reversed ordering so that
12/// newer timestamps sort before older ones in byte-ordered storage.
13///
14/// # Key Encoding
15///
16/// Use [`versioned_key`](Self::versioned_key) to produce a 16-byte key
17/// combining an entity ID with a validity timestamp. The entity ID sorts
18/// in natural order, while the timestamp sorts in reverse order within
19/// each entity, so a forward scan returns the newest version first.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct ValidityTs(Reverse<i64>);
22
23impl ValidityTs {
24    /// Creates a new validity timestamp.
25    #[must_use]
26    pub fn new(ts: i64) -> Self {
27        Self(Reverse(ts))
28    }
29
30    /// Returns a sentinel value representing "always current."
31    ///
32    /// Uses `i64::MAX` so it sorts before every real timestamp in
33    /// reverse order.
34    #[must_use]
35    pub fn current() -> Self {
36        Self(Reverse(i64::MAX))
37    }
38
39    /// Returns the underlying timestamp value.
40    #[must_use]
41    pub fn timestamp(&self) -> i64 {
42        self.0.0
43    }
44
45    /// Encodes an entity ID and validity timestamp into a 16-byte key.
46    ///
47    /// Layout: `[entity_id: 8 bytes BE][timestamp: 8 bytes BE]`
48    ///
49    /// Because `ValidityTs` uses `Reverse<i64>`, the timestamp bytes
50    /// are the raw `i64` in big-endian. To get reverse-sorted timestamps
51    /// in byte order, callers should negate or bitwise-complement the
52    /// timestamp before creating the `ValidityTs`. For a simpler approach,
53    /// use `i64::MAX - epoch` as the timestamp value.
54    #[must_use]
55    pub fn versioned_key(entity_id: u64, ts: Self) -> [u8; 16] {
56        let mut key = [0u8; 16];
57        key[..8].copy_from_slice(&entity_id.to_be_bytes());
58        key[8..].copy_from_slice(&ts.0.0.to_be_bytes());
59        key
60    }
61
62    /// Decodes entity ID and timestamp from a 16-byte versioned key.
63    ///
64    /// Returns `(entity_id, ValidityTs)`.
65    #[must_use]
66    pub fn from_versioned_key(key: &[u8; 16]) -> (u64, Self) {
67        let entity_id = u64::from_be_bytes(key[..8].try_into().unwrap());
68        let ts = i64::from_be_bytes(key[8..].try_into().unwrap());
69        (entity_id, Self::new(ts))
70    }
71}
72
73impl Ord for ValidityTs {
74    fn cmp(&self, other: &Self) -> Ordering {
75        self.0.cmp(&other.0)
76    }
77}
78
79impl PartialOrd for ValidityTs {
80    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
81        Some(self.cmp(other))
82    }
83}
84
85impl From<i64> for ValidityTs {
86    fn from(ts: i64) -> Self {
87        Self::new(ts)
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn test_reverse_ordering() {
97        let ts1 = ValidityTs::new(10);
98        let ts2 = ValidityTs::new(20);
99
100        // Reverse ordering: higher timestamp sorts first (is "less")
101        assert!(ts2 < ts1);
102        assert!(ts1 > ts2);
103    }
104
105    #[test]
106    fn test_current_sentinel() {
107        let current = ValidityTs::current();
108        let recent = ValidityTs::new(1_000_000);
109
110        // Current sorts before everything (smallest in reverse order)
111        assert!(current < recent);
112        assert_eq!(current.timestamp(), i64::MAX);
113    }
114
115    #[test]
116    fn test_versioned_key_roundtrip() {
117        let entity_id = 42u64;
118        let ts = ValidityTs::new(12345);
119
120        let key = ValidityTs::versioned_key(entity_id, ts);
121        let (decoded_id, decoded_ts) = ValidityTs::from_versioned_key(&key);
122
123        assert_eq!(decoded_id, entity_id);
124        assert_eq!(decoded_ts, ts);
125    }
126
127    #[test]
128    fn test_versioned_key_entity_ordering() {
129        // Keys with different entity IDs: entity order is preserved
130        let key1 = ValidityTs::versioned_key(1, ValidityTs::new(100));
131        let key2 = ValidityTs::versioned_key(2, ValidityTs::new(100));
132
133        assert!(key1 < key2);
134    }
135
136    #[test]
137    fn test_equality() {
138        let ts1 = ValidityTs::new(42);
139        let ts2 = ValidityTs::new(42);
140        let ts3 = ValidityTs::new(43);
141
142        assert_eq!(ts1, ts2);
143        assert_ne!(ts1, ts3);
144    }
145
146    #[test]
147    fn test_from_i64() {
148        let ts: ValidityTs = 42i64.into();
149        assert_eq!(ts.timestamp(), 42);
150    }
151}