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}