pub mod numeric;
pub mod special;
pub mod string;
pub mod temporal;
#[derive(Debug, Clone, PartialEq)]
pub enum SeedValue {
Default,
Null,
Integer(i64),
Float(f64),
Text(String),
Bool(bool),
Blob(Vec<u8>),
CurrentTime,
}
pub trait Generator: Send + Sync {
fn generate(&self, rng: &mut dyn RngCore, index: usize, sql_type: &str) -> SeedValue;
fn name(&self) -> &'static str {
core::any::type_name::<Self>()
}
}
impl<G: Generator + ?Sized> Generator for Box<G> {
fn generate(&self, rng: &mut dyn RngCore, index: usize, sql_type: &str) -> SeedValue {
(**self).generate(rng, index, sql_type)
}
fn name(&self) -> &'static str {
(**self).name()
}
}
impl<G: Generator + ?Sized> Generator for std::sync::Arc<G> {
fn generate(&self, rng: &mut dyn RngCore, index: usize, sql_type: &str) -> SeedValue {
(**self).generate(rng, index, sql_type)
}
fn name(&self) -> &'static str {
(**self).name()
}
}
impl Generator for GeneratorKind {
fn generate(&self, rng: &mut dyn RngCore, index: usize, sql_type: &str) -> SeedValue {
self.into_generator().generate(rng, index, sql_type)
}
fn name(&self) -> &'static str {
self.into_generator().name()
}
}
macro_rules! seed_value_from {
($variant:ident: $($ty:ty),*) => {$(
impl From<$ty> for SeedValue {
fn from(value: $ty) -> Self {
Self::$variant(value.into())
}
}
)*};
}
seed_value_from!(Integer: i8, i16, i32, i64, u8, u16, u32);
seed_value_from!(Float: f32, f64);
seed_value_from!(Text: String, &str, char);
seed_value_from!(Bool: bool);
seed_value_from!(Blob: Vec<u8>, &[u8]);
impl<T: Into<SeedValue>> From<Option<T>> for SeedValue {
fn from(value: Option<T>) -> Self {
value.map_or(Self::Null, Into::into)
}
}
pub use rand::RngCore;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum GeneratorKind {
IntPrimaryKey,
Int,
Float,
Bool,
Text,
FirstName,
LastName,
FullName,
Email,
Phone,
City,
Country,
Address,
JobTitle,
Title,
Url,
Username,
Slug,
Company,
LoremIpsum,
Uuid,
Json,
Date,
Timestamp,
Time,
TimeTz,
Interval,
Blob,
PgInet,
PgCidr,
PgMacAddr,
PgMacAddr8,
PgPoint,
PgLine,
PgLseg,
PgBox,
PgPath,
PgPolygon,
PgCircle,
PgBit,
PgVarBit,
PgArray,
}
impl GeneratorKind {
#[must_use]
pub fn into_generator(self) -> Box<dyn Generator> {
match self {
Self::IntPrimaryKey => Box::new(numeric::IntPrimaryKeyGen),
Self::Int => Box::new(numeric::IntGen {
min: 0,
max: 10_000,
}),
Self::Float => Box::new(numeric::FloatGen {
min: 0.0,
max: 10_000.0,
}),
Self::Bool => Box::new(numeric::BoolGen),
Self::Text => Box::new(string::TextGen {
min_len: 5,
max_len: 50,
}),
Self::FirstName => Box::new(string::FirstNameGen),
Self::LastName => Box::new(string::LastNameGen),
Self::FullName => Box::new(string::FullNameGen),
Self::Email => Box::new(string::EmailGen),
Self::Phone => Box::new(string::PhoneGen),
Self::City => Box::new(string::CityGen),
Self::Country => Box::new(string::CountryGen),
Self::Address => Box::new(string::AddressGen),
Self::JobTitle => Box::new(string::JobTitleGen),
Self::Title => Box::new(string::TitleGen),
Self::Url => Box::new(string::UrlGen),
Self::Username => Box::new(string::UsernameGen),
Self::Slug => Box::new(string::SlugGen),
Self::Company => Box::new(string::CompanyGen),
Self::LoremIpsum => Box::new(string::LoremGen { words: 10 }),
Self::Uuid => Box::new(special::UuidGen),
Self::Json => Box::new(special::JsonGen),
Self::Date => Box::new(temporal::DateGen),
Self::Timestamp => Box::new(temporal::TimestampGen),
Self::Time => Box::new(temporal::TimeGen),
Self::TimeTz => Box::new(temporal::TimeTzGen),
Self::Interval => Box::new(temporal::IntervalGen),
Self::Blob => Box::new(special::BlobGen { size: 32 }),
Self::PgInet => Box::new(special::InetGen),
Self::PgCidr => Box::new(special::CidrGen),
Self::PgMacAddr => Box::new(special::MacAddrGen),
Self::PgMacAddr8 => Box::new(special::MacAddr8Gen),
Self::PgPoint => Box::new(special::PointGen),
Self::PgLine => Box::new(special::LineGen),
Self::PgLseg => Box::new(special::LsegGen),
Self::PgBox => Box::new(special::BoxGen),
Self::PgPath => Box::new(special::PathGen),
Self::PgPolygon => Box::new(special::PolygonGen),
Self::PgCircle => Box::new(special::CircleGen),
Self::PgBit => Box::new(special::BitGen {
min_len: 8,
max_len: 8,
}),
Self::PgVarBit => Box::new(special::BitGen {
min_len: 1,
max_len: 32,
}),
Self::PgArray => Box::new(special::ArrayGen),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
use rand::rngs::StdRng;
#[test]
fn every_kind_is_deterministic() {
let kinds = [
GeneratorKind::IntPrimaryKey,
GeneratorKind::Int,
GeneratorKind::Float,
GeneratorKind::Bool,
GeneratorKind::Text,
GeneratorKind::FirstName,
GeneratorKind::LastName,
GeneratorKind::FullName,
GeneratorKind::Email,
GeneratorKind::Phone,
GeneratorKind::City,
GeneratorKind::Country,
GeneratorKind::Address,
GeneratorKind::JobTitle,
GeneratorKind::Title,
GeneratorKind::Url,
GeneratorKind::Username,
GeneratorKind::Slug,
GeneratorKind::Company,
GeneratorKind::LoremIpsum,
GeneratorKind::Uuid,
GeneratorKind::Json,
GeneratorKind::Date,
GeneratorKind::Timestamp,
GeneratorKind::Time,
GeneratorKind::TimeTz,
GeneratorKind::Interval,
GeneratorKind::Blob,
GeneratorKind::PgInet,
GeneratorKind::PgCidr,
GeneratorKind::PgMacAddr,
GeneratorKind::PgMacAddr8,
GeneratorKind::PgPoint,
GeneratorKind::PgLine,
GeneratorKind::PgLseg,
GeneratorKind::PgBox,
GeneratorKind::PgPath,
GeneratorKind::PgPolygon,
GeneratorKind::PgCircle,
GeneratorKind::PgBit,
GeneratorKind::PgVarBit,
GeneratorKind::PgArray,
];
for seed in [0u64, 42, 999, u64::MAX] {
for kind in &kinds {
let g1 = kind.into_generator();
let g2 = kind.into_generator();
let seq1: Vec<SeedValue> = (0..20)
.map(|i| {
let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
g1.generate(&mut rng, i, "TEXT")
})
.collect();
let seq2: Vec<SeedValue> = (0..20)
.map(|i| {
let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
g2.generate(&mut rng, i, "TEXT")
})
.collect();
assert_eq!(
seq1, seq2,
"Determinism violation for {:?} with seed {seed}",
kind
);
}
}
}
#[test]
fn date_timestamp_deterministic_for_integer_columns() {
for sql_type in ["INTEGER", "BIGINT"] {
for seed in [0u64, 42, 12345] {
let date_seq1: Vec<SeedValue> = (0..50)
.map(|i| {
let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
temporal::DateGen.generate(&mut rng, i, sql_type)
})
.collect();
let date_seq2: Vec<SeedValue> = (0..50)
.map(|i| {
let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
temporal::DateGen.generate(&mut rng, i, sql_type)
})
.collect();
assert_eq!(
date_seq1, date_seq2,
"DateGen not deterministic for {sql_type} seed={seed}"
);
assert!(
matches!(date_seq1[0], SeedValue::Integer(_)),
"DateGen should produce Integer for {sql_type}"
);
let ts_seq1: Vec<SeedValue> = (0..50)
.map(|i| {
let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
temporal::TimestampGen.generate(&mut rng, i, sql_type)
})
.collect();
let ts_seq2: Vec<SeedValue> = (0..50)
.map(|i| {
let mut rng = StdRng::seed_from_u64(seed.wrapping_add(i as u64));
temporal::TimestampGen.generate(&mut rng, i, sql_type)
})
.collect();
assert_eq!(
ts_seq1, ts_seq2,
"TimestampGen not deterministic for {sql_type} seed={seed}"
);
assert!(
matches!(ts_seq1[0], SeedValue::Integer(_)),
"TimestampGen should produce Integer for {sql_type}"
);
}
}
}
#[test]
fn different_seeds_produce_different_output() {
let kinds = [
GeneratorKind::Int,
GeneratorKind::Text,
GeneratorKind::Email,
GeneratorKind::Date,
GeneratorKind::Timestamp,
GeneratorKind::Uuid,
];
for kind in &kinds {
let g = kind.into_generator();
let mut rng_a = StdRng::seed_from_u64(1);
let mut rng_b = StdRng::seed_from_u64(2);
let seq_a: Vec<SeedValue> =
(0..10).map(|i| g.generate(&mut rng_a, i, "TEXT")).collect();
let seq_b: Vec<SeedValue> =
(0..10).map(|i| g.generate(&mut rng_b, i, "TEXT")).collect();
assert_ne!(
seq_a, seq_b,
"{:?} produced identical output for different seeds",
kind
);
}
}
#[test]
fn continuous_rng_stream_is_deterministic() {
let kinds = [
GeneratorKind::Int,
GeneratorKind::Float,
GeneratorKind::Text,
GeneratorKind::Email,
GeneratorKind::Date,
GeneratorKind::Timestamp,
];
for kind in &kinds {
let g1 = kind.into_generator();
let g2 = kind.into_generator();
let mut rng1 = StdRng::seed_from_u64(42);
let mut rng2 = StdRng::seed_from_u64(42);
for i in 0..100 {
assert_eq!(
g1.generate(&mut rng1, i, "TEXT"),
g2.generate(&mut rng2, i, "TEXT"),
"Stream diverged at row {i} for {:?}",
kind
);
}
}
}
}