Deterministic Synthetic Data Generation Strategies for Complex Relational Schemas
Tue Jul 14 2015
Relying on hardcoded static SQL dumps (seed.sql) or completely random data generators creates two distinct engineering failures: static dumps become obsolete as schemas evolve, while purely random data causes non-deterministic test failures when edge-case values unpredictably trigger validation errors.
The Principle of Deterministic Seeded Randomness
To guarantee that a test failure is 100% reproducible across local developer machines and CI build runners, synthetic data generation must be deterministic.
Given the exact same initial seed value $S$, a pseudorandom number generator (PRNG) must produce the exact same sequence of synthetic attributes $A_1, A_2, \dots, A_n$:
$$\text{PRNG}(S) \to { \text{Email}_1, \text{UUID}_1, \text{Amount}_1 }$$
import random
from faker import Faker
def generate_test_user(seed_value: int) -> dict:
# Initialize PRNG state deterministically
fake = Faker()
Faker.seed(seed_value)
random.seed(seed_value)
return {
"user_id": fake.uuid4(),
"email": fake.company_email(),
"account_balance": round(random.uniform(10.0, 5000.0), 2),
}
Enforcing Schema Rules via Type-Driven BuildersInstead of hardcoding attributes, derive generator rules directly from database column metadata:
1. VARCHAR Boundaries: Enforce string length limits based on SQL column definitions (VARCHAR(50) -> fake.text(max_nb_chars=50)).
2. Numeric Precision: Respect floating-point and decimal constraints (DECIMAL(10,2) -> round(val, 2)).
3. Unique Constraints: Maintain an in-memory bloom filter during the run to prevent duplicate collisions on unique index columns (UNIQUE(email)).
--