Skip to main content

graphforge_core/
uuid.rs

1//! UUIDv7 generation and Arrow/Parquet serialisation helpers.
2//!
3//! All write paths (CREATE, MERGE, batch ingest, provenance) mint UUIDv7
4//! identifiers for new first-class objects.  Centralising generation here keeps
5//! identity consistent across crates and provides the
6//! [`FixedSizeBinary(16)`](to_bytes) conversions used by the storage layer.
7//!
8//! UUIDv7 is time-ordered (RFC 9562): the 48 most-significant bits are a
9//! Unix-millisecond timestamp, so identifiers minted later sort after earlier
10//! ones — useful for stable, roughly-chronological row ordering.
11
12pub use uuid::Uuid;
13
14/// Generate a new UUIDv7 stamped with the current time.
15#[must_use]
16pub fn new_v7() -> Uuid {
17    Uuid::now_v7()
18}
19
20/// Namespace UUID for GraphForge provenance lineage (#604): the stable root for
21/// content-addressed derived-fact handles. A fixed constant — never regenerate
22/// it, or existing lineage `child_uuid`s stop resolving.
23pub const PROVENANCE_NAMESPACE: Uuid = Uuid::from_bytes([
24    0x9f, 0x6c, 0x2a, 0x1e, 0x7b, 0x42, 0x4d, 0x88, 0xa3, 0x10, 0x5e, 0x0c, 0x91, 0x33, 0x77, 0xd2,
25]);
26
27/// Mint a name-based (UUIDv5, SHA-1) identifier under `namespace`. The same
28/// `(namespace, name)` always yields the same UUID, so it content-addresses a
29/// derived fact: re-running a query re-derives the identical `child_uuid`,
30/// letting lineage rows dedup/accumulate idempotently rather than growing
31/// unboundedly. (#604)
32#[must_use]
33pub fn new_v5(namespace: &Uuid, name: &[u8]) -> Uuid {
34    Uuid::new_v5(namespace, name)
35}
36
37/// Convert a [`Uuid`] into its 16-byte big-endian form, suitable for an Arrow
38/// `FixedSizeBinary(16)` column.
39#[must_use]
40pub fn to_bytes(uuid: &Uuid) -> [u8; 16] {
41    *uuid.as_bytes()
42}
43
44/// Reconstruct a [`Uuid`] from its 16-byte big-endian form.
45#[must_use]
46pub fn from_bytes(bytes: &[u8; 16]) -> Uuid {
47    Uuid::from_bytes(*bytes)
48}
49
50/// Render a [`Uuid`] in canonical hyphenated form for display.
51#[must_use]
52pub fn to_string(uuid: &Uuid) -> String {
53    uuid.hyphenated().to_string()
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn uuids_are_unique() {
62        let mut seen = std::collections::HashSet::new();
63        for _ in 0..1000 {
64            assert!(seen.insert(new_v7()), "generated a duplicate UUID");
65        }
66        assert_eq!(seen.len(), 1000);
67    }
68
69    #[test]
70    fn uuids_are_time_monotone() {
71        // UUIDv7 embeds a millisecond timestamp in its high bits; successive
72        // values generated within the same process should be non-decreasing
73        // bitwise.  Comparing many in sequence keeps the test robust to
74        // multiple UUIDs landing in the same millisecond (the v7 counter then
75        // breaks the tie monotonically).
76        let mut prev = new_v7();
77        for _ in 0..1000 {
78            let next = new_v7();
79            assert!(next > prev, "UUIDv7 ordering violated: {next} !> {prev}");
80            prev = next;
81        }
82    }
83
84    #[test]
85    fn byte_round_trip_preserves_value() {
86        let original = new_v7();
87        let bytes = to_bytes(&original);
88        let restored = from_bytes(&bytes);
89        assert_eq!(original, restored);
90    }
91
92    #[test]
93    fn string_matches_rfc_format() {
94        let s = to_string(&new_v7());
95        // 8-4-4-4-12 hyphenated form, 36 chars total.
96        assert_eq!(s.len(), 36);
97        let groups: Vec<&str> = s.split('-').collect();
98        assert_eq!(groups.len(), 5);
99        assert_eq!(
100            groups.iter().map(|g| g.len()).collect::<Vec<_>>(),
101            vec![8, 4, 4, 4, 12]
102        );
103        assert!(s.chars().all(|c| c.is_ascii_hexdigit() || c == '-'));
104        // Version nibble (first char of the 3rd group) must be '7'.
105        assert_eq!(groups[2].chars().next(), Some('7'));
106    }
107}