The idea
Point a tool at a Postgres connection string. It introspects the schema — tables, foreign keys, unique indexes, check constraints, enums — and generates a full dataset that inserts cleanly in one pass. Every user_id points at a user that exists, every status is a legal enum value, every email is unique where the index says it must be. It runs as an HTTP API and a CLI over the same engine, so you can call it from a test fixture or from a CI job.
Why build this
Most teams seed their dev database one of two bad ways. Either they restore a sanitized dump of production — which is slow, stale, and one misconfigured redaction away from a breach notification — or they hand-write seeds.sql, which rots the moment someone adds a NOT NULL column and nobody notices until the migration fails on a teammate's laptop.
The existing fake-data libraries all solve the wrong half of the problem. faker gives you a plausible name; it knows nothing about the fact that orders.customer_id has to resolve. So people write hundreds of lines of glue to topologically order their inserts by hand, and that glue is the part that breaks.
Schema introspection is the cheap part, and it's been possible forever. What makes this worth building now is that column-name-to-generator mapping — the tedious step of deciding that shipping_addr_line_1 should produce a street address and ext_ref should produce an opaque token — is a small, one-shot classification job an LLM does well and cheaply. Infer it once, write it to a checked-in profile file, and never think about it again.
Stack sketch
- Engine: Python 3.12.
psycopgfor introspection, readinginformation_schemapluspg_catalogfor enums and partial indexes. - Constraint parsing:
pglastto parseCHECKexpressions into an AST, soCHECK (price > 0)becomes a bound on the generator instead of an insert failure. - Ordering:
networkxfor a topological sort of the FK graph; nullable FKs get cut first to break cycles. - Value generation:
fakerfor the common types, seeded from a per-run integer so output is reproducible. - Semantic inference: one Claude Haiku call per table at profile-init time, taking column names plus types and returning a generator name per column. Cached in
seedprofile.yaml. - Output:
COPY ... FROM STDINbinary streams rather than INSERTs — an order of magnitude faster for a million rows. - Surfaces: FastAPI for the HTTP mode, Typer for the CLI, shipped as a single Docker image.
Scope for v1
In:
- Postgres 14+ only.
seed initwritesseedprofile.yamlwith an inferred generator per column, meant to be reviewed and committed.seed run --rows users=1000 --rows orders=5000with per-table counts and sensible defaults.- Deterministic output from
--seed. - Respects FKs, uniques, enums, NOT NULL, and simple comparison check constraints.
Out:
- MySQL and SQLite.
- Any web UI.
- Reading from a production database at all — v1 never connects to prod, which is the entire security story.
- Exotic constraints: exclusion constraints, deferred triggers, and check expressions calling user-defined functions get flagged in
seed initand left to a manual override.
Where it could go
The obvious next step is distribution fidelity. Right now a generated table is uniform noise; real tables are skewed, and query plans that look fine on uniform data fall apart on real cardinality. You can copy pg_stats histograms and most-common-value lists out of production without copying a single row — that's aggregate metadata, not customer data — and shape the generator to match. Suddenly your local EXPLAIN output means something.
After that, subsetting. Instead of generating from nothing, take a real database and extract a referentially complete slice: 500 customers and everything transitively reachable from them. Same FK graph traversal, opposite direction. That's the feature teams with a 4TB production database actually want, and it's a natural paid tier.
The third path is CI ergonomics — a GitHub Action that spins up a Postgres service container, applies migrations, seeds it, and caches the resulting data directory keyed on the migration hash so most runs skip generation entirely.
Watch out for
The failure mode that will burn you is constraints the parser doesn't understand: a partial unique index, a trigger that rejects rows, a check calling a function. Generation succeeds, the insert fails 400,000 rows in, and the error message points at a column the user never configured. Fail loudly at init time on anything unparseable rather than optimistically at insert time.