SynthForge SynthForge SynthForge IO

Blog ·

Five minutes from schema to seeded local DB

Paste your DDL, generate a dataset, and run one command to seed a local Postgres, MySQL, or SQLite. No factory_boy script. No fixtures.yaml. No seed.py.

practitioner synthetic-data databases

The seed script nobody maintains

You sit down to bring up a local dev database. You need real-ish data in it. Your options: a seed script that hasn’t survived the last three schema migrations, a fixtures.yaml somebody checked in two years ago that references tables that no longer exist, a seed.sql that only runs on one person’s machine, or nothing. Most teams pick nothing and hardcode IDs.

We built SynthForge to skip all of that. Paste your schema, generate a dataset, and run one command against your local database.

Hand SynthForge your schema

You already have the DDL. Paste it at /schemas/create/sql. Auto-detect is the default and handles most files; you can force Postgres, MySQL, SQLite or MariaDB when the guess goes wrong. SQL Server and CockroachDB DDL both go through the Postgres rules, which covers ordinary CREATE TABLE syntax and will not understand the vendor-specific parts. The parser is deterministic, with no LLM in the loop.

You can describe it in plain English. /schemas/create/ai is a single text box. Describe your tables and relationships; the AI agent emits a schema you can edit before you ever generate data.

You want a starting point. /schemas/create/template has pre-built schemas for common shapes (e-commerce, healthcare, SaaS). Pick one and customize.

You want to draw it. /schemas/new opens the visual editor directly.

You have a CSV of the real thing. /schemas/create/csv reads a sample, works out the columns and the keys and how your tables relate, and hands back a schema you can regenerate at any size.

Generate the dataset

Open “Generate dataset” from your schema. The form renders in this order:

Dataset name (optional). Then a random seed, also optional: set one and the generator makes the same choices next time, so a fixture stays stable across runs. UUID columns are the exception and come out fresh every time. Then a locale pack, if you want German addresses instead of American ones. Then row counts. Top-level tables take a plain number. Child tables (any table with a foreign key pointing up to a parent) get a mode toggle: fixed count or per parent row. That toggle is what we wrote about in the cardinality post; you set “8 to 12 orders per customer” and SynthForge derives the child count. For M:N junction tables, the structure autodetects and the form picks a sensible larger-side driver.

Below the row counts, the form shows a live memory estimate. If it climbs too high, the Generate button disables and the banner tells you why.

Output formats come next. Pick CSV. That’s the format that ships the SQL loader scripts. Parquet and JSON are there too, but they won’t seed a SQL database.

Hit Generate. The first job after an idle spell takes a little longer to start than the ones after it.

What you download

A zip. For a three-table schema it looks like this:

my-dataset.zip
├── csv/
│   ├── users.csv
│   ├── orders.csv
│   └── order_items.csv
├── import_postgresql.sql
├── import_mysql.sql
├── import_sqlite.sql
├── import_sqlserver.sql
├── import_mariadb.sql
├── import_duckdb.sql
└── import_cockroachdb.sql

One loader script per supported engine, seven total. To seed a local Postgres:

unzip my-dataset.zip && psql -v ON_ERROR_STOP=1 mydb -f import_postgresql.sql

The tables land in synthforge_import, not in mydb’s default schema. That is deliberate. The script creates that namespace, works only inside it, and drops only the tables it put there, so you can point it at a database you care about without auditing it first. Want the tables somewhere else? Find and replace synthforge_import in the script before you run it.

What that namespace is depends on the engine. Postgres, CockroachDB, DuckDB and SQL Server get a schema of that name. MySQL and MariaDB get a database. SQLite has no schemas at all, so its loader writes into whichever file you point it at and the isolation above does not apply: point it at a new file.

Each script runs the same way: drop its own tables in reverse-dependency order so children go before parents, create them in dependency order, load csv/{table}.csv, and make sure every foreign key is in place. Some engines let a foreign key be declared as part of CREATE TABLE. Others need a trailing ALTER TABLE ADD CONSTRAINT. The script does whichever the engine you picked actually supports, so it is worth reading the one you got rather than assuming. Either way the rows are referentially valid before they reach you, because child keys were sampled from parent IDs that already existed at generation time.

Why CSV plus COPY

COPY FROM (or your engine’s equivalent: LOAD DATA, .import, BULK INSERT) is considerably faster than a stream of per-row INSERT statements on any non-trivial load. How much faster depends on the engine and how wide your rows are. People who care already know this.

The CSVs are also portable. All seven loader scripts point at the same files. Generate once, hand the zip to a teammate on a different stack, and they run their own loader without touching the data.

The DDL travels with the artifact too. Each loader script creates its tables before it loads anything. No separate schema file to keep in step, and nothing to drift.

If you want a single INSERT-statement dump you could replay against a running instance, pg_dump --data-only is the right tool. That’s not what this is.

The one gotcha: the download cap

Rows are not usually what stops you. A dataset holds up to 10,000,000 of them, and every table counts toward that one number. What you are more likely to meet first is the 1 GB cap on the generated download.

Whether you hit it depends entirely on how wide your rows are. Five integer columns is 40 bytes a row, and a million of those is a small file. A 25-column table carrying free text lands near 365 bytes a row in the zipped artifact, which reaches 1 GB at around 2.9 million rows. Same row count, wildly different file.

Speed is not the constraint it used to be. A typical 15-column business table generates 200,000 rows in about 12 seconds. A 25-column table with free-text columns does 500,000 in about 40. A million rows of plain scalar columns is a matter of seconds, not minutes.

The generation form tells you before you spend the time: an oversized request turns the estimate into a warning, and past a hard ceiling the Generate button disables. Narrowing a wide column’s max_length usually brings the estimate down faster than cutting row counts. For volumes past ten million rows, split across several jobs and concatenate the CSVs.

The free tier

The first 20,000 rows of a dataset are free; past that, generation is metered in credits. 5 GB stored per account, two generation jobs running concurrently. No credit card required.

Try it

Start at app.synthforge.io and open /schemas/create/sql to paste your DDL. The full feature surface is on the data generation page.


Ready to get started?

Multi-table foreign-key integrity, AI schema design, seven SQL dialects, no credit card.