CLI · commit aa2b076

seedling

Realistic, referentially-consistent test data generator from your schema

Download seedling.zip
Quickstart
unzip seedling.zip && cd seedling
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
.venv/bin/python -m pytest -q

seedling

Realistic, referentially-consistent test data from your own schema. Point seedling at a SQLite/PostgreSQL DDL file, a live SQLite database or a Django project, and it drafts a generation plan, then produces deterministic data that respects foreign keys, unique and composite-unique constraints, CHECK constraints, correlated columns (updated_at >= created_at, city inside its state and country), computed totals and realistic distributions. It also validates a database afterwards and anonymises an existing one with HMAC-consistent fakes.

Features

Install

uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'     # add .[postgres] for PostgreSQL output
source .venv/bin/activate

Quickstart (100k rows into SQLite)

cd examples
seedling init     -s shop_schema.sql -o seedling.yaml --rows 4000
seedling generate -s shop_schema.sql -p seedling.yaml -o shop.db
seedling validate -s shop_schema.sql --db shop.db -p seedling.yaml
seedling mask     --db shop.db -o masked.db --key demo-key

Real output of the second command (Python 3.12, one core):

┏━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━┓
┃ table       ┃ rows   ┃ seconds ┃
┡━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━┩
│ categories  │ 24     │ 0.00    │
│ customers   │ 4,000  │ 0.05    │
│ addresses   │ 6,778  │ 0.04    │
│ orders      │ 15,897 │ 0.12    │
│ products    │ 4,000  │ 0.06    │
│ order_items │ 47,737 │ 0.27    │
│ payments    │ 21,533 │ 0.17    │
│ reviews     │ 3,102  │ 0.03    │
└─────────────┴────────┴─────────┘
generated 103,071 rows in 0.79s (130,666 rows/s) -> sqlite:shop.db  seed=42

Validation (all 89 checks passed; excerpt):

│ unique      │ order_items(order_id, product_id)  │ ok                          │
│ foreign key │ order_items(order_id) -> orders    │ ok                          │
│ check       │ order_items: quantity BETWEEN 1 AND 10 │ ok                      │
│ row count   │ customers                          │ 4000 rows (plan: 4000)      │
│ row count   │ order_items                        │ 47737 rows (plan: 15897..95382) │

What the data looks like:

sqlite> select id, first_name, email, phone, country from customers limit 3;
1|Mark|mark.smith@mail.example.com|+16833886752|United States
2|Anna|anna.diaz@mail.example.com|+12904298816|United States
3|Stephen|stephen.kelly@test.example|+17818637918|United States
sqlite> select id, line1, city, state, postal_code, country from addresses where id in (1,2);
1|753, Station Marg|Vadodara|Gujarat|399930|India
2|Bahnhofallee 599|Charlottenburg|Berlin|15949|Germany

Orders peak in November/December (seasonality: retail: 1967 and 2523 orders versus about 1100 in a normal month), 53% of order items point at the 40 most popular of 4000 products (zipf), and orders.total equals the sum of its items' line_total for every order.

Usage

seedling init     [schema source] [-o seedling.yaml] [--rows N] [--seed S] [--locale en_US|en_IN|de_DE] [--force]
seedling generate [schema source] [-p plan.yaml] --to sqlite|postgres|sql|csv|jsonl -o OUT
                  [--scale F] [--seed S] [--locale L] [--truncate] [--dialect sqlite|postgres] [--with-ddl]
seedling validate [schema source] --db SQLITE_FILE_OR_POSTGRES_DSN [-p plan.yaml] [--scale F]
seedling mask     --db SRC.db (-o COPY.db | --in-place) [-c mask.yaml] [--key SECRET] [--locale L] [--detect]

schema source:  -s FILE.sql | --source-db FILE.db | --django-settings pkg.settings [--app label ...] [--django-project DIR]

If no schema source is given and --to sqlite points at an existing database, that database is introspected. When the schema comes from a DDL file and the SQLite file lacks the tables, the DDL is executed first. Existing rows are never silently mixed in: pass --truncate to replace them.

Output targets:

--to -o notes
sqlite database file batched executemany, FK checks off during load
postgres DSN COPY; needs seedling[postgres]
sql file or - multi-row INSERTs in dependency order; computed/deferred columns as trailing UPDATEs; --dialect postgres writes TRUE/FALSE
csv / jsonl directory one file per table; computed columns are patched in place at the end
seedling generate -s shop_schema.sql --to csv -o out/ --scale 0.1 --seed 7
seedling generate -s shop_schema.sql --to sql -o dump.sql --with-ddl --dialect postgres
seedling generate --django-settings mysite.settings --django-project . -p seedling.yaml -o db.sqlite3 --truncate

Masking (seedling mask --db shop.db --detect lists what would be masked):

┃ column                ┃ kind        ┃ why         ┃
│ customers.first_name  │ first_name  │ column name │
│ customers.email       │ email       │ column name │
│ customers.phone       │ phone       │ column name │
│ customers.birth_date  │ date        │ column name │
│ addresses.city        │ city        │ column name │
│ addresses.postal_code │ postal_code │ column name │

customers.email mark.smith@mail.example.com becomes linda.castillo63@example.net; +16833886752 becomes +16500612553; 399930 becomes a six-digit code. See docs/design.md.

Configuration

seedling.yaml (full reference in docs/plan-reference.md):

seed: 42
locale: en_US
defaults: {date_from: '2023-01-01', date_to: '2025-12-31'}
tables:
  customers:
    rows: 4000
    locale: en_IN                         # names, phones, streets for this table
    columns:
      status: {gen: choice, values: {active: 80, inactive: 15, suspended: 5}}
      email: {null_ratio: 0}              # no gen: merged into the inferred spec
  orders:
    rows: {per: customers, min: 3, max: 12, dist: poisson, lambda: 5}
    computed:
      total: {agg: sum, of: order_items.line_total, by: order_id}

Anything you do not mention is inferred, so a plan can be empty. SEEDLING_MASK_KEY supplies the masking key.

Architecture

module role
ddl.py, introspect.py, schema.py DDL / SQLite / Django to a common Schema
sqlexpr.py SQL expression parser + three-valued evaluator for CHECKs, bounds extraction
inference.py, plan.py draft a plan from the schema; merge user overrides
generators.py, regexgen.py, rand.py, data/ generator catalog, regex-driven strings, seeded streams and distributions, word lists and gazetteer
engine.py ordering, row loop, uniqueness, CHECK retries, aggregates, deferred FKs
sinks.py SQLite, PostgreSQL, SQL dump, CSV, JSONL writers
validate.py, mask.py, cli.py validation queries, anonymiser, click CLI

Design notes and worked examples: docs/design.md.

Tests

.venv/bin/pytest -q          # 71 tests, about 2 s

Covers the DDL parser, ordering and cycles, unique/composite unique, CHECK rejection sampling, correlated and computed columns, determinism and stream independence, distribution sanity (Poisson, zipf, lognormal, normal), masking consistency and FK preservation, Django introspection on a bundled app, a 50k-row performance smoke test and the CLI end to end.

Limitations