Skip to main content

selene_core/
identity.rs

1//! Graph, catalog, and request identifier types per spec 02 section 4.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7macro_rules! identity_id {
8    ($Name:ident, $doc:literal) => {
9        #[doc = $doc]
10        ///
11        /// The raw value `0` is reserved as a tombstone sentinel. Allocators
12        /// start at `1`, and callers maintain that invariant when constructing
13        /// IDs directly.
14        #[derive(
15            Clone,
16            Copy,
17            Debug,
18            Deserialize,
19            Eq,
20            Hash,
21            Ord,
22            PartialEq,
23            PartialOrd,
24            rkyv::Archive,
25            rkyv::Deserialize,
26            rkyv::Serialize,
27            Serialize,
28        )]
29        #[repr(transparent)]
30        pub struct $Name(u64);
31
32        impl $Name {
33            #[doc = concat!("Construct a `", stringify!($Name), "` from a raw `u64`.")]
34            #[must_use]
35            pub const fn new(raw: u64) -> Self {
36                Self(raw)
37            }
38
39            #[doc = concat!("Return the raw `u64` value of this `", stringify!($Name), "`.")]
40            #[must_use]
41            pub const fn get(self) -> u64 {
42                self.0
43            }
44
45            /// The reserved tombstone sentinel value.
46            pub const TOMBSTONE: Self = Self(0);
47        }
48
49        impl fmt::Display for $Name {
50            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51                write!(f, "{}({})", stringify!($Name), self.0)
52            }
53        }
54    };
55}
56
57identity_id!(NodeId, "Graph-scoped node identifier.");
58identity_id!(EdgeId, "Graph-scoped edge identifier.");
59identity_id!(GraphId, "Catalog-scoped graph identifier.");
60identity_id!(BindingTableId, "Request-scoped binding-table identifier.");
61identity_id!(RecordTypeId, "Graph-type-scoped record-type identifier.");
62
63#[cfg(test)]
64mod tests {
65    use proptest::prelude::*;
66    use rstest::rstest;
67
68    use super::*;
69
70    #[rstest]
71    #[case(NodeId::TOMBSTONE.get())]
72    #[case(EdgeId::TOMBSTONE.get())]
73    #[case(GraphId::TOMBSTONE.get())]
74    #[case(BindingTableId::TOMBSTONE.get())]
75    #[case(RecordTypeId::TOMBSTONE.get())]
76    fn tombstone_is_zero(#[case] raw: u64) {
77        assert_eq!(raw, 0);
78    }
79
80    #[test]
81    fn identity_types_are_eight_bytes() {
82        assert_eq!(std::mem::size_of::<NodeId>(), 8);
83        assert_eq!(std::mem::size_of::<EdgeId>(), 8);
84        assert_eq!(std::mem::size_of::<GraphId>(), 8);
85        assert_eq!(std::mem::size_of::<BindingTableId>(), 8);
86        assert_eq!(std::mem::size_of::<RecordTypeId>(), 8);
87    }
88
89    #[test]
90    fn display_includes_type_name_and_value() {
91        assert_eq!(NodeId::new(42).to_string(), "NodeId(42)");
92    }
93
94    #[test]
95    fn identity_types_rkyv_round_trip() {
96        macro_rules! assert_round_trip {
97            ($ty:ident, $raw:expr) => {{
98                let value = $ty::new($raw);
99                let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&value).unwrap();
100                let round: $ty = rkyv::from_bytes::<$ty, rkyv::rancor::Error>(&bytes).unwrap();
101                assert_eq!(round, value);
102            }};
103        }
104
105        assert_round_trip!(NodeId, 1);
106        assert_round_trip!(EdgeId, 2);
107        assert_round_trip!(GraphId, 3);
108        assert_round_trip!(BindingTableId, 4);
109        assert_round_trip!(RecordTypeId, 5);
110    }
111
112    proptest! {
113        #[test]
114        fn node_id_round_trips(raw in any::<u64>()) {
115            prop_assert_eq!(NodeId::new(raw).get(), raw);
116        }
117
118        #[test]
119        fn edge_id_order_matches_raw_values(a in any::<u64>(), b in any::<u64>()) {
120            prop_assert_eq!(EdgeId::new(a).cmp(&EdgeId::new(b)), a.cmp(&b));
121        }
122    }
123}