Skip to main content

renox_core/db/
key.rs

1//! Primary keys: the type of a model's `id` field.
2
3use std::fmt;
4use std::hash::Hash;
5use std::str::FromStr;
6
7use super::{DbValue, FromDb, ToDbValue};
8
9/// A type a model's `id` can have: `i64` (the database numbers the rows),
10/// [`Ulid`], `uuid::Uuid` (renox's `uuid` feature) or `String`.
11/// `#[derive(Model)]` takes the key type from the `id` field:
12///
13/// ```
14/// # use renox::prelude::*;
15/// use renox::db::Ulid;
16///
17/// #[derive(Model, serde::Serialize, Default)]
18/// #[model(table = "invoices")]
19/// struct Invoice {
20///     id: Ulid, // made on insert; TEXT PRIMARY KEY in the migration
21///     total: i64,
22/// }
23///
24/// # async fn demo(db: Db, id: Ulid) -> Result {
25/// let invoice = Invoice::find_or_404(&db, id).await?;
26/// # let _ = invoice; Ok(()) }
27/// ```
28///
29/// A key's empty value (`0`, the nil UUID, an empty string) means "not
30/// saved yet": `save` inserts such a model, making the key (a new ULID or
31/// UUID v7) unless the database does (`i64`). A `String` key is yours to
32/// set, so save a new row with [`Model::insert`](super::Model::insert) or
33/// [`Model::create`](super::Model::create).
34///
35/// The trait is sealed: other types can't be keys.
36#[diagnostic::on_unimplemented(
37    message = "`{Self}` can't be a model's primary key",
38    label = "the `id` field's type",
39    note = "use `i64`, `renox::db::Ulid`, `uuid::Uuid` (renox's `uuid` feature) or `String`"
40)]
41pub trait ModelKey:
42    sealed::Sealed
43    + ToDbValue
44    + FromDb
45    + Clone
46    + Eq
47    + Ord
48    + Hash
49    + fmt::Debug
50    + fmt::Display
51    + FromStr
52    + Default
53    + Send
54    + Sync
55    + Unpin
56    + 'static
57{
58    /// Whether this is the key of a model that isn't saved yet.
59    fn is_unsaved(&self) -> bool;
60
61    /// A new key for an insert, or `None` when the database makes it
62    /// (auto-increment) or the app must set it (`String`).
63    fn generate() -> Option<Self>;
64
65    /// The database makes the key on insert (`RETURNING id`).
66    const AUTO_INCREMENT: bool = false;
67}
68
69mod sealed {
70    /// Only Renox's key types: the trait may grow without breaking apps.
71    pub trait Sealed {}
72    impl Sealed for i64 {}
73    impl Sealed for String {}
74    impl Sealed for super::Ulid {}
75    #[cfg(feature = "uuid")]
76    impl Sealed for uuid::Uuid {}
77}
78
79impl ModelKey for i64 {
80    const AUTO_INCREMENT: bool = true;
81
82    fn is_unsaved(&self) -> bool {
83        *self == 0
84    }
85
86    fn generate() -> Option<Self> {
87        None
88    }
89}
90
91impl ModelKey for String {
92    fn is_unsaved(&self) -> bool {
93        self.is_empty()
94    }
95
96    fn generate() -> Option<Self> {
97        None
98    }
99}
100
101#[cfg(feature = "uuid")]
102impl ModelKey for uuid::Uuid {
103    fn is_unsaved(&self) -> bool {
104        self.is_nil()
105    }
106
107    /// Version 7: time-ordered, so new rows land at the end of the index and
108    /// cursor pages follow creation order.
109    fn generate() -> Option<Self> {
110        Some(uuid::Uuid::now_v7())
111    }
112}
113
114impl ModelKey for Ulid {
115    fn is_unsaved(&self) -> bool {
116        self.0.is_empty()
117    }
118
119    fn generate() -> Option<Self> {
120        Some(Ulid::new())
121    }
122}
123
124/// A [ULID](https://github.com/ulid/spec): 26 characters (`01J9Z3…`), sortable
125/// by creation time, stored as text. A good public id: short in URLs, and it
126/// doesn't reveal how many rows a table has. `Ulid::default()` is the empty
127/// value of an unsaved model.
128#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
129pub struct Ulid(String);
130
131const CROCKFORD: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
132
133impl Ulid {
134    /// A new ULID: the current time in milliseconds, then 80 random bits.
135    /// Within one millisecond the random part counts up (the spec's
136    /// monotonic mode), so ULIDs made by this process sort in the order they
137    /// were made.
138    pub fn new() -> Self {
139        static LAST: std::sync::Mutex<(u128, u128)> = std::sync::Mutex::new((0, 0));
140        const RANDOM_BITS: u128 = (1 << 80) - 1;
141        let now = u128::try_from(crate::clock::unix_millis()).unwrap_or(0) & ((1 << 48) - 1);
142        let mut last = LAST.lock().unwrap_or_else(|e| e.into_inner());
143        let (millis, random) = if now <= last.0 && last.1 < RANDOM_BITS {
144            (last.0, last.1 + 1)
145        } else {
146            let mut bytes = [0u8; 10];
147            rand::fill(&mut bytes);
148            let random = bytes
149                .iter()
150                .fold(0u128, |acc, byte| (acc << 8) | u128::from(*byte));
151            (now.max(last.0), random)
152        };
153        *last = (millis, random);
154        drop(last);
155        let value = (millis << 80) | random;
156        let text = (0..26)
157            .rev()
158            .map(|i| CROCKFORD[((value >> (i * 5)) & 31) as usize] as char)
159            .collect();
160        Self(text)
161    }
162
163    /// The ULID as text; empty for an unsaved model.
164    pub fn as_str(&self) -> &str {
165        &self.0
166    }
167}
168
169/// The text isn't a ULID (26 Crockford base-32 characters).
170#[derive(Debug)]
171#[non_exhaustive]
172pub struct InvalidUlid;
173
174impl fmt::Display for InvalidUlid {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        f.write_str("not a ULID")
177    }
178}
179
180impl std::error::Error for InvalidUlid {}
181
182impl FromStr for Ulid {
183    type Err = InvalidUlid;
184
185    /// Accepts lower case, as the spec asks; an empty string is the
186    /// unsaved value.
187    fn from_str(text: &str) -> Result<Self, Self::Err> {
188        if text.is_empty() {
189            return Ok(Self::default());
190        }
191        let upper = text.to_ascii_uppercase();
192        let valid = upper.len() == 26
193            && upper.as_bytes()[0] <= b'7'
194            && upper.bytes().all(|b| CROCKFORD.contains(&b));
195        if valid {
196            Ok(Self(upper))
197        } else {
198            Err(InvalidUlid)
199        }
200    }
201}
202
203impl fmt::Display for Ulid {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        f.write_str(&self.0)
206    }
207}
208
209impl fmt::Debug for Ulid {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(f, "Ulid({})", self.0)
212    }
213}
214
215impl serde::Serialize for Ulid {
216    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
217        serializer.serialize_str(&self.0)
218    }
219}
220
221impl<'de> serde::Deserialize<'de> for Ulid {
222    /// From a path segment, a form field or JSON: an invalid ULID is an
223    /// error (a 404 through `renox::Path`).
224    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
225        let text = String::deserialize(deserializer)?;
226        text.parse().map_err(serde::de::Error::custom)
227    }
228}
229
230impl ToDbValue for Ulid {
231    fn to_db_value(&self) -> DbValue {
232        DbValue::Text(self.0.clone())
233    }
234}
235
236crate::__db_text_type_for!(Ulid, sqlx::sqlite::Sqlite);
237#[cfg(feature = "postgres")]
238crate::__db_text_type_for!(Ulid, sqlx::postgres::Postgres);
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn ulids_are_26_sortable_characters() {
246        let first = Ulid::new();
247        assert_eq!(first.as_str().len(), 26);
248        assert!(first.as_str().bytes().all(|b| CROCKFORD.contains(&b)));
249        let second = Ulid::new(); // most likely within the same millisecond
250        assert!(second > first, "{second} after {first}");
251        let many: Vec<Ulid> = (0..1000).map(|_| Ulid::new()).collect();
252        assert!(many.windows(2).all(|w| w[0] < w[1]), "monotonic");
253        assert_eq!(first.to_string().parse::<Ulid>().unwrap(), first);
254        assert_eq!(
255            first.as_str().to_lowercase().parse::<Ulid>().unwrap(),
256            first
257        );
258        assert!("not-a-ulid".parse::<Ulid>().is_err());
259        assert!(
260            "81J9Z3ABCDEFGHJKMNPQRSTVWX".parse::<Ulid>().is_err(),
261            "past 2^48 ms"
262        );
263        assert!(Ulid::default().is_unsaved());
264        assert!(!first.is_unsaved());
265    }
266
267    // #254: an empty key is the unsaved one; Display is the text form.
268    #[test]
269    fn empty_text_is_the_unsaved_ulid() {
270        let empty: Ulid = "".parse().unwrap();
271        assert_eq!(empty, Ulid::default());
272        let id: Ulid = "01arz3ndektsv4rrffq69g5fav".parse().unwrap();
273        assert_eq!(id.to_string(), "01ARZ3NDEKTSV4RRFFQ69G5FAV");
274    }
275}