spacetimedb_sats/
uuid.rs

1use std::cell::Cell;
2use std::fmt;
3
4use crate::timestamp::Timestamp;
5use crate::{de::Deserialize, impl_st, ser::Serialize, AlgebraicType, AlgebraicValue};
6use uuid::{Builder, Uuid as UUID, Variant};
7
8#[derive(Clone, Copy, Debug, PartialEq)]
9#[repr(u8)]
10pub enum Version {
11    /// The "nil" UUID (all zeros).
12    Nil = 0u8,
13    /// Version 4: Random.
14    V4 = 4,
15    /// Version 7: Timestamp + counter + random.
16    V7 = 7,
17    /// The "max" (all ones) UUID.
18    Max = 0xff,
19}
20
21/// A universally unique identifier (UUID).
22///
23/// Support for UUID [`Version::Nil`], [`Version::Max`], [`Version::V4`] (random) and [`Version::V7`] (timestamp + counter + random).
24#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
25#[sats(crate = crate)]
26pub struct Uuid {
27    __uuid__: u128,
28}
29
30impl Uuid {
31    /// The nil UUID (all zeros).
32    ///
33    /// Example:
34    ///
35    /// ```
36    /// # use spacetimedb_sats::uuid::Uuid;
37    /// let uuid = Uuid::NIL;
38    ///
39    /// assert_eq!(
40    ///     "00000000-0000-0000-0000-000000000000",
41    ///     uuid.to_string(),
42    /// );
43    /// ```
44    pub const NIL: Self = Self {
45        __uuid__: UUID::nil().as_u128(),
46    };
47
48    /// The max UUID (all ones).
49    ///
50    /// Example:
51    /// ```
52    /// # use spacetimedb_sats::uuid::Uuid;
53    /// let uuid = Uuid::MAX;
54    ///
55    /// assert_eq!(
56    ///     "ffffffff-ffff-ffff-ffff-ffffffffffff",
57    ///     uuid.to_string(),
58    /// );
59    /// ```
60    pub const MAX: Self = Self {
61        __uuid__: UUID::max().as_u128(),
62    };
63
64    /// Create a UUID `v4` from explicit random bytes.
65    ///
66    /// This method assumes the bytes are already sufficiently random, it will only
67    /// set the appropriate bits for the UUID version and variant.
68    ///
69    /// # Example
70    /// ```
71    /// # use spacetimedb_sats::uuid::Uuid;
72    /// // Use the `ReducerContext::rng()` method to generate random bytes in reducers,
73    /// // or call `ReducerContext::new_uuid_v4`
74    /// let random_bytes = [0u8; 16];
75    /// let uuid = Uuid::from_random_bytes_v4(random_bytes);
76    ///
77    /// assert_eq!(
78    ///     "00000000-0000-4000-8000-000000000000",
79    ///     uuid.to_string(),
80    /// );
81    /// ```
82    pub fn from_random_bytes_v4(counter_random_bytes: [u8; 16]) -> Self {
83        Self {
84            __uuid__: Builder::from_random_bytes(counter_random_bytes).into_uuid().as_u128(),
85        }
86    }
87
88    /// Generate a UUID `v7` using a monotonic counter from 0 to 2^31-1, a timestamp, and 4 random bytes.
89    ///
90    /// The counter will wrap around on overflow.
91    ///
92    /// The UUID `v7` is structured as follows:
93    ///```ascii
94    /// ┌───────────────────────────────────────────────┬───────────────────┐
95    /// | B0  | B1  | B2  | B3  | B4  | B5              |         B6        |
96    /// ├───────────────────────────────────────────────┼───────────────────┤
97    /// |                 unix_ts_ms                    |      version 7    |
98    /// └───────────────────────────────────────────────┴───────────────────┘
99    /// ┌──────────────┬─────────┬──────────────────┬───────────────────────┐
100    /// | B7           | B8      | B9  | B10 | B11  | B12 | B13 | B14 | B15 |
101    /// ├──────────────┼─────────┼──────────────────┼───────────────────────┤
102    /// | counter_high | variant |    counter_low   |        random         |
103    /// └──────────────┴─────────┴──────────────────┴───────────────────────┘
104    /// ```
105    /// # Panics
106    ///
107    /// Panics if the counter value is negative, or the timestamp is before the Unix epoch.
108    ///
109    /// # Example
110    ///```
111    /// use spacetimedb_sats::uuid::Uuid;
112    /// use spacetimedb_sats::timestamp::Timestamp;
113    ///
114    /// let now = Timestamp::from_micros_since_unix_epoch(1_686_000_000_000);
115    /// let counter = std::cell::Cell::new(1);
116    /// // Use the `ReducerContext::rng()` | `ProcedureContext::rng()` to generate random bytes,
117    /// // or call `ReducerContext::new_uuid_v7()` / `ProcedureContext::new_uuid_v7()`
118    /// let random_bytes = [0u8; 4];
119    /// let uuid = Uuid::from_counter_v7(&counter, now, &random_bytes).unwrap();
120    ///
121    /// assert_eq!(
122    ///     "0000647e-5180-7000-8000-000200000000",
123    ///     uuid.to_string(),
124    /// );
125    /// ```
126    pub fn from_counter_v7(counter: &Cell<u32>, now: Timestamp, random_bytes: &[u8; 4]) -> anyhow::Result<Self> {
127        // Monotonic counter value (31 bits)
128        let counter_val = counter.get();
129        counter.set(counter_val.wrapping_add(1) & 0x7FFF_FFFF);
130
131        let ts_ms = now
132            .to_duration_since_unix_epoch()
133            .expect("timestamp before unix epoch")
134            .as_millis() as i64
135            & 0xFFFFFFFFFFFF;
136
137        let mut bytes = [0u8; 16];
138
139        // unix_ts_ms (48 bits)
140        bytes[0] = (ts_ms >> 40) as u8;
141        bytes[1] = (ts_ms >> 32) as u8;
142        bytes[2] = (ts_ms >> 24) as u8;
143        bytes[3] = (ts_ms >> 16) as u8;
144        bytes[4] = (ts_ms >> 8) as u8;
145        bytes[5] = ts_ms as u8;
146
147        // version & variant
148        // bytes[6] = uuid::Version::SortRand;
149        // bytes[8] = Variant::RFC4122
150
151        // Counter bits
152        bytes[7] = ((counter_val >> 23) & 0xFF) as u8;
153        bytes[9] = ((counter_val >> 15) & 0xFF) as u8;
154        bytes[10] = ((counter_val >> 7) & 0xFF) as u8;
155        bytes[11] = ((counter_val & 0x7F) << 1) as u8;
156
157        // Random bytes
158        bytes[12] |= random_bytes[0] & 0x7F;
159        bytes[13] = random_bytes[1];
160        bytes[14] = random_bytes[2];
161        bytes[15] = random_bytes[3];
162
163        let uuid = Builder::from_bytes(bytes)
164            .with_variant(Variant::RFC4122)
165            .with_version(uuid::Version::SortRand)
166            .into_uuid();
167
168        Ok(Self {
169            __uuid__: uuid.as_u128(),
170        })
171    }
172
173    /// Extract the monotonic counter from a UUIDv7.
174    #[cfg(test)]
175    fn get_counter(&self) -> i32 {
176        let bytes: [u8; 16] = self.__uuid__.to_be_bytes();
177
178        let high = bytes[7] as u32; // bits 30..23
179        let mid1 = bytes[9] as u32; // bits 22..15
180        let mid2 = bytes[10] as u32; // bits 14..7
181        let low = (bytes[11] as u32) >> 1; // bits 6..0
182
183        // reconstruct 31-bit counter
184        ((high << 23) | (mid1 << 15) | (mid2 << 7) | low) as i32
185    }
186
187    /// Parse a UUID from a string representation.
188    ///
189    /// Any of the formats generated by this module (simple, hyphenated, urn,
190    /// Microsoft GUID) are supported by this parsing function.
191    ///
192    /// # Example
193    /// ```
194    /// # use spacetimedb_sats::uuid::Uuid;
195    /// let s = "01888d6e-5c00-7000-8000-000000000000";
196    /// let uuid = Uuid::parse_str(s).unwrap();
197    ///
198    /// assert_eq!(
199    ///     s,
200    ///     uuid.to_string(),
201    /// );
202    /// ```
203    pub fn parse_str(s: &str) -> Result<Self, uuid::Error> {
204        Ok(Self {
205            __uuid__: UUID::parse_str(s)?.as_u128(),
206        })
207    }
208
209    /// Returns the version of the UUID.
210    ///
211    /// This represents the algorithm used to generate the value.
212    /// If the version field doesn't contain a recognized version then `None`
213    /// is returned.
214    pub fn get_version(&self) -> Option<Version> {
215        match self.to_uuid().get_version() {
216            Some(uuid::Version::Nil) => Some(Version::Nil),
217            Some(uuid::Version::Random) => Some(Version::V4),
218            Some(uuid::Version::SortRand) => Some(Version::V7),
219            Some(uuid::Version::Max) => Some(Version::Max),
220            _ => None,
221        }
222    }
223
224    #[cfg(test)]
225    fn get_variant(&self) -> Variant {
226        self.to_uuid().get_variant()
227    }
228
229    /// Convert to the `uuid` crate's `Uuid` type.
230    pub fn to_uuid(self) -> UUID {
231        UUID::from_u128(self.__uuid__)
232    }
233
234    pub fn from_u128(u: u128) -> Self {
235        Self { __uuid__: u }
236    }
237
238    pub fn as_u128(&self) -> u128 {
239        self.__uuid__
240    }
241}
242
243impl_st!([] Uuid, AlgebraicType::uuid());
244
245impl fmt::Display for Uuid {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        write!(f, "{}", self.to_uuid())
248    }
249}
250
251impl From<Uuid> for AlgebraicValue {
252    fn from(value: Uuid) -> Self {
253        AlgebraicValue::product([value.as_u128().into()])
254    }
255}
256
257#[cfg(test)]
258mod test {
259    use super::*;
260    use crate::timestamp::Timestamp;
261    use crate::GroundSpacetimeType;
262    use rand::RngCore;
263
264    #[test]
265    fn uuid_type_matches() {
266        assert_eq!(AlgebraicType::uuid(), Uuid::get_type());
267        assert!(Uuid::get_type().is_uuid());
268        assert!(Uuid::get_type().is_special());
269    }
270
271    #[test]
272    fn round_trip() {
273        let u1 = Uuid::NIL;
274        let s = u1.to_string();
275        let u2 = Uuid::parse_str(&s).unwrap();
276        assert_eq!(u1, u2);
277        assert_eq!(u1.as_u128(), u2.as_u128());
278        assert_eq!(u1.to_uuid(), u2.to_uuid());
279        assert_eq!(s, u2.to_string());
280    }
281
282    #[test]
283    fn to_string() {
284        for u in [
285            Uuid::NIL,
286            Uuid::from_u128(0x0102_0304_0506_0708_090a_0b0c_0d0e_0f10_u128),
287            Uuid::MAX,
288        ] {
289            let s = u.to_string();
290            let u2 = Uuid::parse_str(&s).unwrap();
291            assert_eq!(u, u2);
292        }
293    }
294
295    #[test]
296    fn version() {
297        let u_nil = Uuid::NIL;
298        assert_eq!(u_nil.get_version(), Some(Version::Nil));
299
300        let u_max = Uuid::MAX;
301        assert_eq!(u_max.get_version(), Some(Version::Max));
302
303        assert_eq!(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, u128::MAX);
304
305        let u_v4 = Uuid::from_random_bytes_v4([0u8; 16]);
306        assert_eq!(u_v4.get_version(), Some(Version::V4));
307
308        let counter = Cell::new(0);
309        let ts = Timestamp::from_micros_since_unix_epoch(1_686_000_000_000);
310        let u_v7 = Uuid::from_counter_v7(&counter, ts, &[0u8; 4]).unwrap();
311        assert_eq!(u_v7.get_version(), Some(Version::V7));
312    }
313
314    #[test]
315    fn wrap_around() {
316        // Check wraparound behavior
317        let counter = Cell::new(u32::MAX);
318        let ts = Timestamp::now();
319        let _u1 = Uuid::from_counter_v7(&counter, ts, &[0u8; 4]).unwrap();
320        assert_eq!(0, counter.get());
321    }
322
323    #[test]
324    #[should_panic(expected = "timestamp before unix epoch")]
325    fn negative_timestamp_panics() {
326        let counter = Cell::new(0);
327        let ts = Timestamp::from_micros_since_unix_epoch(-1);
328        let _u = Uuid::from_counter_v7(&counter, ts, &[0u8; 4]).unwrap();
329    }
330
331    #[test]
332    fn ordered() {
333        let u1 = Uuid::from_u128(1);
334        let u2 = Uuid::from_u128(2);
335        assert!(u1 < u2);
336        assert!(u2 > u1);
337        assert_eq!(u1, u1);
338        assert_ne!(u1, u2);
339        // Check we start from zero
340        let counter = Cell::new(0);
341        let ts = Timestamp::now();
342        let u_start = Uuid::from_counter_v7(&counter, ts, &[0u8; 4]).unwrap();
343        assert_eq!(u_start.get_counter(), 0);
344        // Check ordering over many UUIDs up to the max counter value
345        let total = 10_000_000;
346        let counter = Cell::new(u32::MAX - total);
347        let ts = Timestamp::now();
348        let uuids = (0..total)
349            .map(|_| {
350                let mut bytes = [0u8; 4];
351                rand::rng().fill_bytes(&mut bytes);
352                Uuid::from_counter_v7(&counter, ts, &bytes).unwrap()
353            })
354            .collect::<Vec<Uuid>>();
355
356        for (pos, pair) in uuids.windows(2).enumerate() {
357            assert_eq!(pair[0].get_version(), Some(Version::V7));
358            assert_eq!(pair[0].get_variant(), Variant::RFC4122);
359            assert!(
360                pair[0] < pair[1],
361                "UUIDs are not ordered at {pos}: {} !< {}",
362                pair[0],
363                pair[1]
364            );
365
366            assert!(
367                pair[0].get_counter() < pair[1].get_counter(),
368                "UUID counters are not ordered at {pos}: {} !< {}",
369                pair[0].get_counter(),
370                pair[1].get_counter()
371            );
372        }
373    }
374}