Skip to main content

rushdb_types/
ids.rs

1use serde::{Deserialize, Serialize};
2use sha2::{Digest, Sha256};
3use std::fmt;
4use uuid::Uuid;
5
6/// Resolved tenant ordinal: the 4-byte prefix of every tenant-owned key
7///. Assigned once by the tenant directory and never reused, so a purged
8/// tenant's leftovers can never become visible to a later tenant.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10pub struct TenantId(pub u32);
11
12impl TenantId {
13    /// Reserved for system rows; never assigned to a tenant.
14    pub const SYSTEM: Self = TenantId(0);
15    /// The fixed tenant of single-tenant deployments.
16    pub const SINGLE: Self = TenantId(1);
17    /// First ordinal the tenant directory assigns.
18    pub const FIRST_ASSIGNED: u32 = 2;
19    /// A scope the directory has never registered. Never assigned, so it owns
20    /// no keys: every read through it is empty.
21    pub const UNREGISTERED: Self = TenantId(u32::MAX);
22
23    /// The fixed identity used by single-tenant deployments.
24    #[must_use]
25    pub fn single() -> Self {
26        Self::SINGLE
27    }
28
29    /// A tenant id from an ordinal the caller already resolved (tests,
30    /// embedders, and the directory itself).
31    ///
32    /// # Panics
33    /// Panics if `ordinal` does not fit the 4-byte key prefix.
34    #[must_use]
35    pub const fn from_ordinal(ordinal: u64) -> Self {
36        assert!(ordinal <= u32::MAX as u64, "tenant ordinal exceeds u32");
37        #[expect(clippy::cast_possible_truncation, reason = "range checked above")]
38        TenantId(ordinal as u32)
39    }
40
41    /// Big-endian key-prefix bytes.
42    #[must_use]
43    pub const fn to_be_bytes(self) -> [u8; 4] {
44        self.0.to_be_bytes()
45    }
46}
47
48impl fmt::Display for TenantId {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(f, "{}", self.0)
51    }
52}
53
54/// Unresolved tenant identity: the SHA-256 digest of the caller's scope
55/// string. The tenant directory maps it to a [`TenantId`]; only the digest is
56/// stored, never the scope string.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
58pub struct TenantScope(pub [u8; 32]);
59
60impl TenantScope {
61    /// Digest of a scope value.
62    #[must_use]
63    pub fn of(scope: &str) -> Self {
64        TenantScope(Sha256::digest(scope.as_bytes()).into())
65    }
66
67    /// The scope of single-tenant deployments.
68    #[must_use]
69    pub fn single() -> Self {
70        Self::of("rushdb:single-tenant")
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn arbitrary_scope_values_are_stable_and_distinct() {
80        assert_ne!(TenantScope::of("scope-a"), TenantScope::of("scope-b"));
81        assert_eq!(TenantScope::of("not-a-uuid"), TenantScope::of("not-a-uuid"));
82    }
83}
84
85macro_rules! dense_id {
86    ($name:ident) => {
87        #[doc = concat!("Dense monotonic ", stringify!($name), " identifier.")]
88        #[derive(
89            Debug,
90            Clone,
91            Copy,
92            PartialEq,
93            Eq,
94            PartialOrd,
95            Ord,
96            Hash,
97            Serialize,
98            Deserialize,
99            Default,
100        )]
101        pub struct $name(#[doc = "Wrapped ordinal value."] pub u64);
102
103        impl $name {
104            /// Return the identifier that follows this one.
105            #[must_use]
106            pub fn next(self) -> Self {
107                $name(self.0 + 1)
108            }
109        }
110    };
111}
112
113dense_id!(RecordOrdinal);
114dense_id!(RelationshipOrdinal);
115dense_id!(OccurrenceOrdinal);
116dense_id!(FamilyId);
117dense_id!(PropertyId);
118dense_id!(ValueSeq);
119dense_id!(SpaceId);
120
121/// Lexical token identifier.
122pub type TokenId = u32;
123
124/// Stable 128-bit public identity of a record.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
126pub struct RecordId(pub [u8; 16]);
127
128impl RecordId {
129    /// Generate a new time-ordered (`UUIDv7`) record id.
130    #[must_use]
131    pub fn generate() -> Self {
132        RecordId(*Uuid::now_v7().as_bytes())
133    }
134
135    /// The all-zero record id, used as a sentinel.
136    #[must_use]
137    pub fn nil() -> Self {
138        RecordId([0u8; 16])
139    }
140
141    /// View this id as a [`Uuid`].
142    #[must_use]
143    pub fn to_uuid(&self) -> Uuid {
144        Uuid::from_bytes(self.0)
145    }
146}
147
148impl From<Uuid> for RecordId {
149    fn from(u: Uuid) -> Self {
150        RecordId(*u.as_bytes())
151    }
152}
153
154impl fmt::Display for RecordId {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(f, "{}", self.to_uuid().simple())
157    }
158}
159
160/// Stable 128-bit public identity of a relationship.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
162pub struct RelationshipId(pub [u8; 16]);
163
164impl RelationshipId {
165    /// Generate a new time-ordered (`UUIDv7`) relationship id.
166    #[must_use]
167    pub fn generate() -> Self {
168        RelationshipId(*Uuid::now_v7().as_bytes())
169    }
170
171    /// View this id as a [`Uuid`].
172    #[must_use]
173    pub fn to_uuid(&self) -> Uuid {
174        Uuid::from_bytes(self.0)
175    }
176}
177
178impl From<Uuid> for RelationshipId {
179    fn from(u: Uuid) -> Self {
180        RelationshipId(*u.as_bytes())
181    }
182}
183
184impl fmt::Display for RelationshipId {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(f, "{}", self.to_uuid().simple())
187    }
188}
189
190/// Whether an owned row belongs to a record or a relationship.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
192pub enum OwnerKind {
193    /// A record owner.
194    Record,
195    /// A relationship owner.
196    Relationship,
197}
198
199impl OwnerKind {
200    /// One-byte tag used in keys (`b'r'` or `b'e'`).
201    #[must_use]
202    pub fn as_byte(self) -> u8 {
203        match self {
204            OwnerKind::Record => b'r',
205            OwnerKind::Relationship => b'e',
206        }
207    }
208
209    /// Decode an owner kind from its one-byte key tag.
210    #[must_use]
211    pub fn from_byte(b: u8) -> Option<Self> {
212        match b {
213            b'r' => Some(OwnerKind::Record),
214            b'e' => Some(OwnerKind::Relationship),
215            _ => None,
216        }
217    }
218}