Skip to main content

keelson_factory/
sequence.rs

1use std::sync::OnceLock;
2use std::sync::atomic::{AtomicI32, Ordering};
3use std::time::{SystemTime, UNIX_EPOCH};
4
5/// The uniqueness source: a process-unique, time-derived base plus an atomic
6/// counter. Generated factory modules hold one per model:
7///
8/// ```ignore
9/// static SEQ: Sequence = Sequence::new();
10/// // …
11/// id: self.id.resolve(f, |_| Set::Value(SEQ.next_i64())),
12/// ```
13///
14/// Two promises, in order of strength:
15///
16/// - **In-process, values never repeat** — the counter is atomic, so
17///   `create_many(&db, 100)` (and every factory call across every test in the
18///   binary) draws distinct values.
19/// - **Across processes, collision is improbable** — the base is taken from
20///   the clock at first use (the same shape the Layer 2 spec's `key()`
21///   pinned), so runs against a shared persistent server land in different
22///   ranges. Improbable, not impossible: this is test-data machinery, not a
23///   coordination service.
24///
25/// Values are positive and fit `i32`, so the one sequence serves `integer`
26/// (PostgreSQL/MySQL) and `INTEGER` (SQLite) primary keys alike.
27///
28/// Deliberately **outside the [`Faker`](crate::Faker) seed**: sequences exist
29/// for uniqueness, and a reproducible primary key against a shared server
30/// would reproduce a collision (crate docs, "the determinism switch").
31#[derive(Debug)]
32pub struct Sequence {
33    base: OnceLock<i32>,
34    next: AtomicI32,
35}
36
37impl Sequence {
38    /// A fresh sequence; `const`, so it can be a `static` in a generated
39    /// module. The base is drawn from the clock on first use, not here.
40    pub const fn new() -> Self {
41        Sequence {
42            base: OnceLock::new(),
43            next: AtomicI32::new(0),
44        }
45    }
46
47    /// The next value, `i32`-typed for dialects whose `integer` is 32-bit.
48    pub fn next_i32(&self) -> i32 {
49        let base = *self.base.get_or_init(|| {
50            let nanos = SystemTime::now()
51                .duration_since(UNIX_EPOCH)
52                .unwrap_or_default()
53                .as_nanos();
54            // Positive, below i32::MAX, with 2^16 of counter headroom below
55            // the next possible base.
56            ((nanos as i64) & 0x3fff_0000) as i32
57        });
58        base + self.next.fetch_add(1, Ordering::Relaxed)
59    }
60
61    /// The next value, widened — the same counter as
62    /// [`next_i32`](Sequence::next_i32), for `i64`-typed columns.
63    pub fn next_i64(&self) -> i64 {
64        i64::from(self.next_i32())
65    }
66}
67
68impl Default for Sequence {
69    fn default() -> Self {
70        Sequence::new()
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use std::collections::HashSet;
77
78    use super::*;
79
80    #[test]
81    fn a_static_sequence_yields_distinct_positive_i32_values() {
82        static SEQ: Sequence = Sequence::new();
83        let mut seen = HashSet::new();
84        for _ in 0..200 {
85            let v = SEQ.next_i32();
86            assert!(v >= 0);
87            assert!(seen.insert(v), "sequence repeated {v}");
88        }
89    }
90
91    #[test]
92    fn the_i64_getter_shares_the_counter_with_the_i32_one() {
93        let seq = Sequence::new();
94        let a = seq.next_i32();
95        let b = seq.next_i64();
96        assert_eq!(b, i64::from(a) + 1, "one counter behind both getters");
97    }
98}