Skip to main content

drizzle_seed/generator/
mod.rs

1//! Column value generators: the [`Generator`] trait, [`GeneratorKind`], and
2//! [`SeedValue`].
3
4pub mod numeric;
5pub mod special;
6pub mod string;
7pub mod temporal;
8
9/// A generated column value, before it is rendered for a dialect.
10#[derive(Debug, Clone, PartialEq)]
11pub enum SeedValue {
12    /// The SQL `DEFAULT` keyword (the column's own default).
13    Default,
14    /// SQL `NULL`.
15    Null,
16    /// Signed integer IR. Dialects may bind nonnegative values as unsigned,
17    /// but generated values remain limited to `i64::MAX`.
18    Integer(i64),
19    /// A floating-point number.
20    Float(f64),
21    /// A string.
22    Text(String),
23    /// A boolean.
24    Bool(bool),
25    /// Raw bytes.
26    Blob(Vec<u8>),
27    /// "Now", rendered as the dialect's current-timestamp expression.
28    CurrentTime,
29}
30
31/// Produces one column value per row.
32///
33/// Implement it for custom data and pass it to `SeedConfig::generator`.
34/// Use only `rng` for randomness so output stays deterministic.
35///
36/// # Examples
37///
38/// ```rust
39/// use drizzle_seed::{Generator, RngCore, SeedValue};
40///
41/// struct Sku;
42///
43/// impl Generator for Sku {
44///     fn generate(&self, _rng: &mut dyn RngCore, index: usize, _sql_type: &str) -> SeedValue {
45///         SeedValue::Text(format!("SKU-{:05}", index + 1))
46///     }
47///
48///     fn name(&self) -> &'static str {
49///         "Sku"
50///     }
51/// }
52///
53/// # use rand::SeedableRng;
54/// let mut rng = rand::rngs::StdRng::seed_from_u64(0);
55/// assert_eq!(Sku.generate(&mut rng, 0, "TEXT"), SeedValue::Text("SKU-00001".into()));
56/// ```
57pub trait Generator: Send + Sync {
58    /// Returns the value for row `index` (0-based). `sql_type` is the
59    /// column's SQL type as declared, e.g. `"INTEGER"` or `"TEXT"`.
60    fn generate(&self, rng: &mut dyn RngCore, index: usize, sql_type: &str) -> SeedValue;
61
62    /// Returns a short name for debugging. Defaults to the type name.
63    fn name(&self) -> &'static str {
64        core::any::type_name::<Self>()
65    }
66}
67
68impl<G: Generator + ?Sized> Generator for Box<G> {
69    fn generate(&self, rng: &mut dyn RngCore, index: usize, sql_type: &str) -> SeedValue {
70        (**self).generate(rng, index, sql_type)
71    }
72
73    fn name(&self) -> &'static str {
74        (**self).name()
75    }
76}
77
78impl<G: Generator + ?Sized> Generator for std::sync::Arc<G> {
79    fn generate(&self, rng: &mut dyn RngCore, index: usize, sql_type: &str) -> SeedValue {
80        (**self).generate(rng, index, sql_type)
81    }
82
83    fn name(&self) -> &'static str {
84        (**self).name()
85    }
86}
87
88/// A built-in kind is itself a generator, so it can be passed wherever a
89/// [`Generator`] is expected.
90impl Generator for GeneratorKind {
91    fn generate(&self, rng: &mut dyn RngCore, index: usize, sql_type: &str) -> SeedValue {
92        self.into_generator().generate(rng, index, sql_type)
93    }
94
95    fn name(&self) -> &'static str {
96        self.into_generator().name()
97    }
98}
99
100macro_rules! seed_value_from {
101    ($variant:ident: $($ty:ty),*) => {$(
102        impl From<$ty> for SeedValue {
103            fn from(value: $ty) -> Self {
104                Self::$variant(value.into())
105            }
106        }
107    )*};
108}
109
110seed_value_from!(Integer: i8, i16, i32, i64, u8, u16, u32);
111seed_value_from!(Float: f32, f64);
112seed_value_from!(Text: String, &str, char);
113seed_value_from!(Bool: bool);
114seed_value_from!(Blob: Vec<u8>, &[u8]);
115
116impl<T: Into<SeedValue>> From<Option<T>> for SeedValue {
117    /// `None` becomes `NULL`.
118    fn from(value: Option<T>) -> Self {
119        value.map_or(Self::Null, Into::into)
120    }
121}
122
123/// Re-export of `rand::RngCore`, the RNG passed to [`Generator::generate`].
124pub use rand::RngCore;
125
126/// A built-in generator, chosen by type/name inference or set with
127/// `SeedConfig::kind`.
128///
129/// New kinds may be added in any release, so a `match` on it needs a `_`
130/// arm.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132#[non_exhaustive]
133pub enum GeneratorKind {
134    /// Sequential integers starting at 1 (row index + 1).
135    IntPrimaryKey,
136    /// Random integer from 0 to 10 000.
137    Int,
138    /// Random number from 0 up to 10 000, rounded to 2 decimals.
139    Float,
140    /// Random boolean.
141    Bool,
142    /// 5 to 50 random lowercase letters.
143    Text,
144    /// First name from a built-in list.
145    FirstName,
146    /// Last name from a built-in list.
147    LastName,
148    /// First and last name.
149    FullName,
150    /// `first.last{row}@example.com` (or another domain reserved for
151    /// testing); the row index keeps emails unique.
152    Email,
153    /// US-style phone number, `(555) 555-5555`.
154    Phone,
155    /// City name from a built-in list.
156    City,
157    /// Country name from a built-in list.
158    Country,
159    /// Street address, `123 Name Street`.
160    Address,
161    /// Job title from a built-in list.
162    JobTitle,
163    /// A short sentence-case phrase, such as a post title: `Dolor sit amet`.
164    Title,
165    /// `https://` URL on a reserved example domain; the row index keeps
166    /// URLs unique.
167    Url,
168    /// Lowercase `first_last{row}`; the row index keeps usernames unique.
169    Username,
170    /// URL slug, `dolor-sit-amet-{row}`; the row index keeps slugs unique.
171    Slug,
172    /// Company name: a last name and a suffix, such as `Smith Inc`.
173    Company,
174    /// Ten words of lorem ipsum.
175    LoremIpsum,
176    /// Random UUID v4 as text (`8-4-4-4-12` hex).
177    Uuid,
178    /// Small JSON object as text.
179    Json,
180    /// Date between 2000 and 2030 as `YYYY-MM-DD`; for an integer column
181    /// type, its Unix milliseconds (UTC midnight) instead.
182    Date,
183    /// Timestamp between 2000 and 2030 as `YYYY-MM-DD HH:MM:SS`; for an
184    /// integer column type, its Unix milliseconds (UTC) instead.
185    Timestamp,
186    /// Time of day, `HH:MM:SS`.
187    Time,
188    /// Time of day with a UTC offset, `HH:MM:SS+HH`.
189    TimeTz,
190    /// `PostgreSQL` interval of 1 to 72 hours, such as `"12 hours"`.
191    Interval,
192    /// 32 random bytes.
193    Blob,
194    /// `PostgreSQL` INET
195    PgInet,
196    /// `PostgreSQL` CIDR
197    PgCidr,
198    /// `PostgreSQL` MACADDR
199    PgMacAddr,
200    /// `PostgreSQL` MACADDR8
201    PgMacAddr8,
202    /// `PostgreSQL` POINT
203    PgPoint,
204    /// `PostgreSQL` LINE
205    PgLine,
206    /// `PostgreSQL` LSEG
207    PgLseg,
208    /// `PostgreSQL` BOX
209    PgBox,
210    /// `PostgreSQL` PATH
211    PgPath,
212    /// `PostgreSQL` POLYGON
213    PgPolygon,
214    /// `PostgreSQL` CIRCLE
215    PgCircle,
216    /// `PostgreSQL` BIT: 8 random bits.
217    PgBit,
218    /// `PostgreSQL` VARBIT: 1 to 32 random bits.
219    PgVarBit,
220    /// `PostgreSQL` array: always the empty array literal `{}`.
221    PgArray,
222}
223
224impl GeneratorKind {
225    /// Returns the generator for this kind.
226    #[must_use]
227    pub fn into_generator(self) -> Box<dyn Generator> {
228        match self {
229            Self::IntPrimaryKey => Box::new(numeric::IntPrimaryKeyGen),
230            Self::Int => Box::new(numeric::IntGen {
231                min: 0,
232                max: 10_000,
233            }),
234            Self::Float => Box::new(numeric::FloatGen {
235                min: 0.0,
236                max: 10_000.0,
237            }),
238            Self::Bool => Box::new(numeric::BoolGen),
239            Self::Text => Box::new(string::TextGen {
240                min_len: 5,
241                max_len: 50,
242            }),
243            Self::FirstName => Box::new(string::FirstNameGen),
244            Self::LastName => Box::new(string::LastNameGen),
245            Self::FullName => Box::new(string::FullNameGen),
246            Self::Email => Box::new(string::EmailGen),
247            Self::Phone => Box::new(string::PhoneGen),
248            Self::City => Box::new(string::CityGen),
249            Self::Country => Box::new(string::CountryGen),
250            Self::Address => Box::new(string::AddressGen),
251            Self::JobTitle => Box::new(string::JobTitleGen),
252            Self::Title => Box::new(string::TitleGen),
253            Self::Url => Box::new(string::UrlGen),
254            Self::Username => Box::new(string::UsernameGen),
255            Self::Slug => Box::new(string::SlugGen),
256            Self::Company => Box::new(string::CompanyGen),
257            Self::LoremIpsum => Box::new(string::LoremGen { words: 10 }),
258            Self::Uuid => Box::new(special::UuidGen),
259            Self::Json => Box::new(special::JsonGen),
260            Self::Date => Box::new(temporal::DateGen),
261            Self::Timestamp => Box::new(temporal::TimestampGen),
262            Self::Time => Box::new(temporal::TimeGen),
263            Self::TimeTz => Box::new(temporal::TimeTzGen),
264            Self::Interval => Box::new(temporal::IntervalGen),
265            Self::Blob => Box::new(special::BlobGen { size: 32 }),
266            Self::PgInet => Box::new(special::InetGen),
267            Self::PgCidr => Box::new(special::CidrGen),
268            Self::PgMacAddr => Box::new(special::MacAddrGen),
269            Self::PgMacAddr8 => Box::new(special::MacAddr8Gen),
270            Self::PgPoint => Box::new(special::PointGen),
271            Self::PgLine => Box::new(special::LineGen),
272            Self::PgLseg => Box::new(special::LsegGen),
273            Self::PgBox => Box::new(special::BoxGen),
274            Self::PgPath => Box::new(special::PathGen),
275            Self::PgPolygon => Box::new(special::PolygonGen),
276            Self::PgCircle => Box::new(special::CircleGen),
277            Self::PgBit => Box::new(special::BitGen {
278                min_len: 8,
279                max_len: 8,
280            }),
281            Self::PgVarBit => Box::new(special::BitGen {
282                min_len: 1,
283                max_len: 32,
284            }),
285            Self::PgArray => Box::new(special::ArrayGen),
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use rand::SeedableRng;
294    use rand::rngs::StdRng;
295
296    /// Every GeneratorKind must produce identical output given identical RNG state.
297    #[test]
298    fn every_kind_is_deterministic() {
299        let kinds = [
300            GeneratorKind::IntPrimaryKey,
301            GeneratorKind::Int,
302            GeneratorKind::Float,
303            GeneratorKind::Bool,
304            GeneratorKind::Text,
305            GeneratorKind::FirstName,
306            GeneratorKind::LastName,
307            GeneratorKind::FullName,
308            GeneratorKind::Email,
309            GeneratorKind::Phone,
310            GeneratorKind::City,
311            GeneratorKind::Country,
312            GeneratorKind::Address,
313            GeneratorKind::JobTitle,
314            GeneratorKind::Title,
315            GeneratorKind::Url,
316            GeneratorKind::Username,
317            GeneratorKind::Slug,
318            GeneratorKind::Company,
319            GeneratorKind::LoremIpsum,
320            GeneratorKind::Uuid,
321            GeneratorKind::Json,
322            GeneratorKind::Date,
323            GeneratorKind::Timestamp,
324            GeneratorKind::Time,
325            GeneratorKind::TimeTz,
326            GeneratorKind::Interval,
327            GeneratorKind::Blob,
328            GeneratorKind::PgInet,
329            GeneratorKind::PgCidr,
330            GeneratorKind::PgMacAddr,
331            GeneratorKind::PgMacAddr8,
332            GeneratorKind::PgPoint,
333            GeneratorKind::PgLine,
334            GeneratorKind::PgLseg,
335            GeneratorKind::PgBox,
336            GeneratorKind::PgPath,
337            GeneratorKind::PgPolygon,
338            GeneratorKind::PgCircle,
339            GeneratorKind::PgBit,
340            GeneratorKind::PgVarBit,
341            GeneratorKind::PgArray,
342        ];
343
344        for seed in [0u64, 42, 999, u64::MAX] {
345            for kind in &kinds {
346                let g1 = kind.into_generator();
347                let g2 = kind.into_generator();
348
349                // Generate a sequence of 20 values with each RNG
350                let seq1: Vec<SeedValue> = (0..20)
351                    .map(|i| {
352                        let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
353                        g1.generate(&mut rng, i, "TEXT")
354                    })
355                    .collect();
356                let seq2: Vec<SeedValue> = (0..20)
357                    .map(|i| {
358                        let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
359                        g2.generate(&mut rng, i, "TEXT")
360                    })
361                    .collect();
362
363                assert_eq!(
364                    seq1, seq2,
365                    "Determinism violation for {:?} with seed {seed}",
366                    kind
367                );
368            }
369        }
370    }
371
372    /// Date and Timestamp generators must produce identical INTEGER values
373    /// when sql_type is INTEGER/BIGINT (SQLite timestamp_ms mode).
374    #[test]
375    fn date_timestamp_deterministic_for_integer_columns() {
376        for sql_type in ["INTEGER", "BIGINT"] {
377            for seed in [0u64, 42, 12345] {
378                let date_seq1: Vec<SeedValue> = (0..50)
379                    .map(|i| {
380                        let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
381                        temporal::DateGen.generate(&mut rng, i, sql_type)
382                    })
383                    .collect();
384                let date_seq2: Vec<SeedValue> = (0..50)
385                    .map(|i| {
386                        let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
387                        temporal::DateGen.generate(&mut rng, i, sql_type)
388                    })
389                    .collect();
390                assert_eq!(
391                    date_seq1, date_seq2,
392                    "DateGen not deterministic for {sql_type} seed={seed}"
393                );
394                // Verify they're actually integers
395                assert!(
396                    matches!(date_seq1[0], SeedValue::Integer(_)),
397                    "DateGen should produce Integer for {sql_type}"
398                );
399
400                let ts_seq1: Vec<SeedValue> = (0..50)
401                    .map(|i| {
402                        let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
403                        temporal::TimestampGen.generate(&mut rng, i, sql_type)
404                    })
405                    .collect();
406                let ts_seq2: Vec<SeedValue> = (0..50)
407                    .map(|i| {
408                        let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
409                        temporal::TimestampGen.generate(&mut rng, i, sql_type)
410                    })
411                    .collect();
412                assert_eq!(
413                    ts_seq1, ts_seq2,
414                    "TimestampGen not deterministic for {sql_type} seed={seed}"
415                );
416                assert!(
417                    matches!(ts_seq1[0], SeedValue::Integer(_)),
418                    "TimestampGen should produce Integer for {sql_type}"
419                );
420            }
421        }
422    }
423
424    /// Different seeds must produce different sequences (no degenerate RNG).
425    #[test]
426    fn different_seeds_produce_different_output() {
427        let kinds = [
428            GeneratorKind::Int,
429            GeneratorKind::Text,
430            GeneratorKind::Email,
431            GeneratorKind::Date,
432            GeneratorKind::Timestamp,
433            GeneratorKind::Uuid,
434        ];
435
436        for kind in &kinds {
437            let g = kind.into_generator();
438            let mut rng_a = StdRng::seed_from_u64(1);
439            let mut rng_b = StdRng::seed_from_u64(2);
440
441            let seq_a: Vec<SeedValue> =
442                (0..10).map(|i| g.generate(&mut rng_a, i, "TEXT")).collect();
443            let seq_b: Vec<SeedValue> =
444                (0..10).map(|i| g.generate(&mut rng_b, i, "TEXT")).collect();
445
446            assert_ne!(
447                seq_a, seq_b,
448                "{:?} produced identical output for different seeds",
449                kind
450            );
451        }
452    }
453
454    /// A continuous RNG stream must be deterministic (same seed, shared RNG across rows).
455    #[test]
456    fn continuous_rng_stream_is_deterministic() {
457        let kinds = [
458            GeneratorKind::Int,
459            GeneratorKind::Float,
460            GeneratorKind::Text,
461            GeneratorKind::Email,
462            GeneratorKind::Date,
463            GeneratorKind::Timestamp,
464        ];
465
466        for kind in &kinds {
467            let g1 = kind.into_generator();
468            let g2 = kind.into_generator();
469            let mut rng1 = StdRng::seed_from_u64(42);
470            let mut rng2 = StdRng::seed_from_u64(42);
471
472            for i in 0..100 {
473                assert_eq!(
474                    g1.generate(&mut rng1, i, "TEXT"),
475                    g2.generate(&mut rng2, i, "TEXT"),
476                    "Stream diverged at row {i} for {:?}",
477                    kind
478                );
479            }
480        }
481    }
482}