Generation
Describe the shop.Get the tables.
DOGM generation turns a YAML spec into connected, fictional tables: every order belongs to a customer, every total adds up, and the same seed gives the same bytes. You write the tables, keys, child counts and formulas once, and every run reads that same file.
Scenario, spec, data
Words, then YAML,then tables.
The spec sits in the middle. Someone on your team reviews and approves it, and every run after that reads the approved version.
01
Live in the betaA scenario
Plain words about the world you need: a shop where customers place one to three orders, and every order has one payment.
“Customers in Philippine cities, 1 to 3 orders each, one payment per order.”
Give the app plain words, SQL DDL or a Prisma schema and AI drafts the YAML for you to review. You can also write it by hand.
02
Live in the betaA spec
A versioned YAML document with tables, columns, keys and rules. It never holds data or key material, so you can review it in a pull request and commit it.
orders: rows_per_parent: { parent: customers, dist: uniform, min: 1, max: 3 }The compiler checks the spec and returns every error with a JSON path and a stable error code.
03
Live in the betaData
Linked tables with valid keys by construction, calculated totals, and a manifest that records exactly how they were made.
customers.part-00000.csv orders.part-00000.csv manifest.json
Hosted runs write these files today. Without an account, the demo generates three seeded scenarios in your browser.
The spec language
A real spec,line by line.
This is the e-commerce scenario the engine is tested against: three tables, two rules and a masking block. The numbered notes cover the lines that do the most work.
- customers
- orders
- payments
spec_version: 1locale: en-PHtables: customers: rows: 1000Note 1 primary_key: { column: customer_id, format: "CUS-{n:06}" } columns:Note 2 full_name: { gen: person.full_name, pii: person.name } email: { gen: person.email, from: full_name, unique: true, pii: person.email } city: { gen: ph.city, weights: population } joined_on: { gen: date, between: [2024-01-01, 2026-10-01] } orders:Note 3 rows_per_parent: { parent: customers, dist: uniform, min: 1, max: 3 } primary_key: { column: order_id, format: "ORD-{n:07}" } columns:Note 4 customer_id: { ref: customers.customer_id } order_date: { gen: date, after: parent.joined_on, within_days: [1, 90] } item_count: { gen: int, between: [1, 5] } unit_price_php: { gen: choice, values: [249, 399, 589, 1250, 2499], type: money }Note 5 subtotal_php: { expr: "item_count * unit_price_php" } shipping_php: { expr: "if(subtotal_php >= 1500, 0, 99)", type: money } total_php: { expr: "subtotal_php + shipping_php" } status: { gen: choice, values: [processing, shipped, delivered, cancelled], weights: [1, 1, 2, 1] } payments: rows_per_parent: { parent: orders, exactly: 1 } primary_key: { column: payment_id, format: "PAY-{n:07}" } columns: order_id: { ref: orders.order_id } method: { gen: choice, values: [GCash, Card, Bank transfer, Cash on delivery] } amount_php: { expr: "parent.total_php" }Note 6 status: { map: parent.status, cases: { cancelled: refunded, processing: pending }, default: paid } paid_on: { gen: date, after: parent.order_date, within_days: [0, 2] }Note 7rules: - "orders.total_php == orders.subtotal_php + orders.shipping_php" - "payments.paid_on >= parent.order_date"Note 8mask: key_ref: project_key columns: customers.full_name: { mode: consistent } customers.email: { mode: consistent, from: customers.full_name, on_collision: suffix }primary_key
Each table names its key and a display format. {n:06} is the row's permuted number, zero-padded, so IDs are unique and don't give away row order or table size.
pii
Labels the column's personal-data type. It shows in reports and is the default masking domain for the column.
rows_per_parent
A child table's size per parent row. Here each customer gets 1 to 3 orders, drawn uniformly. Other forms: an exact count, an expression, or cases by a parent column.
ref
A foreign key, valid by construction. On the owning parent it is simply the parent's key, so no order can point at a missing customer.
expr
A value computed from columns defined earlier in the row. Expressions are pure: randomness enters only through generators.
map
A lookup from another column. A cancelled order gets a refunded payment, a processing order a pending one, everything else paid.
rules
Checks, not constraints. Every rule runs on every row, and a failing row is reported with its key instead of stopping the run.
mask
Which columns to mask and how. key_ref names a key you supply at run time; the spec never holds key material. The engine applies it when you pass a key file, or skips it on purpose with --no-mask. on_collision: suffix keeps the unique email column unique, since two names can mask to the same stand-in.
Determinism
Same seed,same bytes.
Four inputs define a run: the spec, a seed, optional row counts and the engine version. Give the engine the same four and it writes the same files, byte for byte, on Linux, Windows, macOS or in the browser.
One draw
draw(seed, stream, row_index, k) -> u64
One draw: Philox keyed by the seed and the column's stream, counted by row and draw number.
| row_index | customer_id |
|---|---|
| 0 | CUS-000731 |
| 1 | CUS-000058 |
| 2 | CUS-000412 |
| 3 | CUS-000996 |
Illustrative values. The real mapping depends on the seed.
Every random draw is addressed, not streamed
There is no shared random stream that has to be read in order. Each draw is a pure function of the seed, the column, the row number and a draw counter, computed with Philox, a counter-based generator. Any cell can be computed on its own, so work can be split across machines and still agree.
Adding a column leaves the others alone
Each column draws from its own stream, derived from its table and name at compile time. Add a column and it gets a new stream, so the existing columns keep their values.
IDs are shuffled, and still unique
Primary keys come from a Feistel permutation of the row number: a reversible shuffle keyed by the seed. Every row gets a different ID, row 0 is not CUS-000000, and a foreign key is just the parent's shuffled number.
Stable within a minor version
Any change to a generator, sampling or a file writer bumps the engine's minor version and is listed in the changelog. Every run records the engine version it used.
Output formats
Files your testsalready read.
Output is one file per table per chunk, plus a manifest.json that lists every part with its row range and SHA-256.
| Format | Notes | Status |
|---|---|---|
| CSV | RFC 4180, UTF-8 without BOM, LF line endings, a header row, minimal quoting. | In the demoLive in the betaCLI, from source |
| JSONL | One object per row, keys in column order. | Live in the betaCLI, from source |
| SQL | Postgres INSERT statements, 1,000 rows per batch. | Live in the betaCLI, from source |
| Parquet | Arrives in the next engine release, with the writer and compression settings pinned so the bytes stay deterministic. | Planned |
Philippine locale
Built for datathat looks local.
The en-PH locale has generators for Philippine places, mobile numbers and government ID formats, next to fictional names and emails that can't reach an inbox.
ph.city
City names, optionally weighted by population, so larger cities appear more often.
ph.province
Province names.
ph.region
Region names.
ph.postal_code
Postal codes.
ph.address
Street-level addresses.
ph.mobile
Mobile numbers in Philippine format.
ph.tin
Tax identification numbers, format-valid and labelled synthetic.
ph.sss_no
SSS numbers, format-valid and labelled synthetic.
ph.philhealth_no
PhilHealth numbers, format-valid and labelled synthetic.
Fictional by construction
Name pools are curated and combined so they do not target real people. Emails use only RFC 2606 example domains, so nothing can reach a real inbox.
bea.castillo.4821@example.com
Format-valid is not the same as nonexistent
A generated TIN or SSS number follows the real format and can match a real one by chance. The checks report labels these columns as synthetic. Treat them as test values only.
Early-access beta
Try the demo now.Or start free.
The in-browser demo generates, masks and checks a connected sample, and nothing leaves your browser. Sign up and your workspace gets Pro limits at no cost while the beta lasts.
