Skip to content

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.

A spec becomes connected tablesA short spec describing customers, orders and payments passes through the generation module and comes out as three linked tables. Each order points at a customer, and each payment matches its order total.spec.yamlcustomers: 24orders: per customer: 1-4 total: sum(items)payments: amount: = totalcustomersC-1001C-1002C-1003ordersO-501C-10011,240O-502C-1001380O-503C-10032,015paymentsP-901O-5011,240P-902O-502380P-903O-5032,015

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.

  1. 01

    Live in the beta

    A 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.

  2. 02

    Live in the beta

    A 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.

  3. 03

    Live in the beta

    Data

    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
ecommerce.yamlspec_version 1
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 }

Every field, in the spec language reference

  1. 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.

  2. pii

    Labels the column's personal-data type. It shows in reports and is the default masking domain for the column.

  3. 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.

  4. 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.

  5. expr

    A value computed from columns defined earlier in the row. Expressions are pure: randomness enters only through generators.

  6. map

    A lookup from another column. A cancelled order gets a refunded payment, a processing order a pending one, everything else paid.

  7. rules

    Checks, not constraints. Every rule runs on every row, and a failing row is reported with its key instead of stopping the run.

  8. 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 numbers to customer IDs
row_indexcustomer_id
0CUS-000731
1CUS-000058
2CUS-000412
3CUS-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.

Output formats and their status
FormatNotesStatus
CSVRFC 4180, UTF-8 without BOM, LF line endings, a header row, minimal quoting.In the demoLive in the betaCLI, from source
JSONLOne object per row, keys in column order.Live in the betaCLI, from source
SQLPostgres INSERT statements, 1,000 rows per batch.Live in the betaCLI, from source
ParquetArrives 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.