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}