Files
rdbms-playground/website/src/content/docs/reference/generating-sample-data.mdx
T
claude@clouddev1 6b4c4dcea4
ci / gate (push) Failing after 35s
ci / manifests (push) Successful in 3s
website / deploy (push) Successful in 35s
feat(types)!: drop the blob column type (ADR-0005 Amendment 2)
blob was a dead-end: declarable but never fillable (no literal in either
mode, seed-unsupported), so a blob column could only ever hold NULL. Remove
Type::Blob + Value/CellValue::Blob + the base64 CSV path + the grammar slot
+ completion/render/type-change/seed handling + the binder refusal (whose
message also carried a user-facing "DSL" copy-rule bug). The vocabulary is
now nine types; base64 stays (clipboard OSC-52).

Backward compat is the ADR-0015 migration framework's first real use: a
v1->v2 format bump (CURRENT_SCHEMA_VERSION across serializer/parser/skeleton)
with a migrator that rewrites `type: blob` -> `type: text`, and a forced .db
rebuild from the migrated text when a blob column was actually converted
(the stale .db keeps a STRICT BLOB engine column). Conversion to text is
non-destructive and CSV-free. Covered by a full-stack integration test and a
Tier-4 PTY test that opens a real legacy v1-blob project.

Also sweeps the nine-type vocabulary through CLAUDE.md, requirements.md, the
website (type reference, seed doc, highlight grammar), and the ADR-0030/0033/
0035 cross-references; CHANGELOG Removed entry; handoff-79.

BREAKING CHANGE: the `blob` column type is removed. Existing projects that
declared a blob column are migrated on first open (the column becomes text;
the original project.yaml is kept as a .v1.bak).
2026-06-22 21:25:38 +00:00

323 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Generating sample data
description: Fill tables with realistic, name-aware fake rows using seed — with foreign keys, reproducible runs, and per-column overrides.
sidebar:
order: 8
---
import Demo from '../../../components/Demo.astro';
Once a schema is in place you usually want rows to query against — enough of
them to make `where`, `group by`, `order by`, and `limit` interesting. Typing
those by hand is tedious. `seed` fills a table with **plausible, generated
data** in one command, so you can get to querying straight away. It works in
both [simple and advanced mode](/getting-started/modes/), with the same
syntax.
<Demo src="/casts/seed.cast" title="One line fills a table with realistic, ready-to-query rows — seed reads each column's name to decide what to make." />
The examples use the [example library](/getting-started/example-library/).
## Filling a table
`seed <table> <count>` generates rows and inserts them. If you leave out the
count, seed makes **20** rows:
```rdbms
seed members 6
```
```
6 row(s) seeded into members
┌───────────┬─────────────────┬────────────┐
│ member_id │ name │ joined │
├───────────┼─────────────────┼────────────┤
│ 1 │ Bret Leffler │ 2023-10-16 │
│ 2 │ Santa Nicolas │ 2024-03-14 │
│ 3 │ Vivienne Barton │ 2024-03-04 │
│ 4 │ Fatima Rippin │ 2022-11-14 │
│ 5 │ Lola Cole │ 2025-05-29 │
│ 6 │ Reina Waters │ 2023-11-25 │
└───────────┴─────────────────┴────────────┘
```
The data is **random**, so your rows will differ — run it again for a fresh
set, or see [Reproducible runs](#reproducible-runs) to pin it. `member_id`
was filled automatically (it is a `serial` column), exactly as it is for an
[`insert`](/reference/inserting-and-editing-data/); seed leaves `serial` and
`shortid` columns to the database.
Notice the values are not random noise: `name` produced believable people and
`joined` produced recent dates. That is because seed reads each **column's
name** to decide what to generate.
## How columns are filled
A column's **name** chooses a generator, but only when the column's
[type](/reference/types/) fits — a column called `email` typed `int` will not
get an address. Matching is case-insensitive and looks at the name's parts
(`first_name`, `signup_date`, `is_active`). A representative set:
| Column name looks like… | You get | For types |
|---|---|---|
| `name`, `first_name`, `last_name`, `full_name` | a person's name | `text` |
| `email` | an email address | `text` |
| `username`, `login`, `handle` | a username | `text` |
| `phone`, `mobile`, `tel` | a phone number | `text` |
| `city`, `country`, `state`, `street`, `zip` | address parts | `text` |
| `company`, `employer` · `job`, `position` | a company / job title | `text` |
| `description`, `bio`, `notes`, `comment` | a sentence or paragraph | `text` |
| `url`, `website` · `color` | a URL / hex colour | `text` |
| `price`, `amount`, `cost`, `salary` | a money amount | `int`, `real`, `decimal` |
| `age` · `quantity`, `qty`, `stock` | a plausible age / small number | `int` |
| `date`, `dob`, `created_at`, `updated_at` | a recent (or birth-window) date | `date`, `datetime` |
| `year`, `*_year`, `published`, `founded`, `birth_year` | a plausible year (a birth window for `birth_year`) | `int` |
| `priority`, `severity`, `rating`, `stars` | a value from a built-in set (low/medium/high, 1–5, …) | `text`, `int` |
| `is_active`, `has_*`, `enabled` | `true` / `false` | `bool` |
When a column's name **isn't** recognised, seed falls back to its **type**:
placeholder words for `text`, a number for `int` and `real`, a recent value
for `date`. A column like `isbn` or `title` — text with no specific meaning
seed can infer — gets placeholder words; pin it with the
[`set` clause](#choosing-values-yourself) if you want something specific.
Two name families are handled specially:
- **Identifier-like names** that are *not* a foreign key or the primary key —
`code`, `sku`, `ref`, `barcode`, a `*_id` that isn't a relationship — get
**unique** values, so they read like real identifiers and never collide.
- **Open-ended choice names** — `status`, `role`, `type`, `category`, and the
like — have no single sensible default, so seed fills them with placeholder
text and then [tells you](#columns-seed-cant-guess) to choose the real
values yourself. (Common choices that *do* have a conventional set —
`priority`, `severity`, `rating` — are filled from it, per the table above.)
Any column with a `unique` [constraint](/reference/constraints/) always gets
collision-free values, whatever its name — that is a correctness guarantee,
not a guess.
## Foreign keys
Seed respects [relationships](/reference/relationships/). A foreign-key column
is filled by **sampling from the rows that already exist** in the parent
table, so every generated reference is valid. Seed the parent first:
```rdbms
seed authors 5
seed books 6
```
```
6 row(s) seeded into books
┌─────────┬──────────────────┬───────────┬───────────┬─────────────────────────┐
│ book_id │ title │ author_id │ published │ isbn │
├─────────┼──────────────────┼───────────┼───────────┼─────────────────────────┤
│ 1 │ Austen Wuckert │ 4 │ 1976 │ sit nihil │
│ 2 │ Leanne Fisher │ 3 │ 1961 │ in ex │
│ 3 │ Ludwig Bahringer │ 5 │ 1986 │ sapiente provident │
│ 4 │ Jeff Little │ 3 │ 2021 │ fugit sint eum │
│ 5 │ Kameron Moore │ 1 │ 1995 │ incidunt cumque quia │
│ 6 │ Walker Hammes │ 5 │ 1997 │ enim saepe consequuntur │
└─────────┴──────────────────┴───────────┴───────────┴─────────────────────────┘
```
Every `author_id` points at a real author (1–5). Duplicates are expected and
correct — one author has many books. `published` got a plausible year on its
own (seed recognises year-like columns); `title` and `isbn` are placeholder
text, since neither name maps to a real-world generator — pin them with
[`set`](#choosing-values-yourself) if you want something specific.
If a parent table is **empty**, seed refuses rather than inventing a reference
that would break the relationship:
```
cannot seed `books`: parent table `authors` (referenced by `author_id`) has
no rows. Seed or insert into `authors` first.
```
This mirrors the order you would insert data by hand, and quietly teaches
foreign-key dependency order. A junction table linking two parents (a
many-to-many bridge) is filled with **distinct combinations** of the parents'
keys; if you ask for more rows than there are combinations, seed makes as many
as it can and tells you.
## Reproducible runs
Add `--seed <n>` to make a run **repeatable**: the same number produces the
same data, so a teacher can hand out one dataset and a demo stays stable.
```rdbms
seed members 6 --seed 42
```
```
6 row(s) seeded into members
┌───────────┬─────────────────┬────────────┐
│ member_id │ name │ joined │
├───────────┼─────────────────┼────────────┤
│ 1 │ Bret Leffler │ 2023-10-16 │
│ 2 │ Santa Nicolas │ 2024-03-14 │
│ 3 │ Vivienne Barton │ 2024-03-04 │
│ 4 │ Fatima Rippin │ 2022-11-14 │
│ 5 │ Lola Cole │ 2025-05-29 │
│ 6 │ Reina Waters │ 2023-11-25 │
└───────────┴─────────────────┴────────────┘
```
Run that again and you get the very same six members. "The same data" is
relative to the table's current contents: because foreign keys and unique
values read the rows already present, reproducibility assumes the same
starting point.
## Choosing values yourself
Seed's guesses are a starting point. The optional `set` clause pins how one or
more columns are filled. It reuses syntax you already know from
[`where`](/reference/querying-and-inspecting/) and `update`, so there is
nothing new to learn — four forms:
| Form | Example | Meaning |
|---|---|---|
| Fixed value | `set status = 'active'` | every row gets the same value |
| Pick from a list | `set role in ('admin', 'editor', 'viewer')` | a random choice from the list |
| Named generator | `set contact as email` | force a specific generator |
| Range | `set price between 10 and 100` | a value in the range (also dates) |
`status` has no built-in set — its real values are domain-specific — so it is
the natural column to pin:
```rdbms
seed tickets 6 set status in ('open', 'pending', 'closed')
```
```
6 row(s) seeded into tickets
┌───────────┬──────────────────────────┬─────────┬──────────┐
│ ticket_id │ subject │ status │ priority │
├───────────┼──────────────────────────┼─────────┼──────────┤
│ 7 │ atque libero │ pending │ high │
│ 8 │ culpa maiores et │ open │ low │
│ 9 │ natus rerum animi │ open │ medium │
│ 10 │ sapiente rem │ closed │ low │
│ 11 │ placeat blanditiis quasi │ closed │ high │
│ 12 │ sed exercitationem │ closed │ low │
└───────────┴──────────────────────────┴─────────┴──────────┘
```
`status` takes your values; `priority` filled itself from its built-in set
(low/medium/high). Comma-separate several `set` clauses to pin more than one
column at once.
Text values and list items are **quoted** (`'admin'`), exactly as elsewhere;
only numbers are bare. Dates in a range are quoted too
(`set joined between '2023-01-01' and '2024-12-31'`). A range on a number
column takes numeric bounds, a range on a date column takes date bounds — a
mismatched bound is a friendly error.
The named generators you can use after `as` are:
`age`, `bool`, `city`, `color`, `company`, `country`, `date`, `datetime`,
`email`, `first_name`, `job`, `last_name`, `name`, `paragraph`, `password`,
`phone`, `price`, `product`, `sentence`, `state`, `street`, `url`, `username`,
`zip`.
:::note
If you pin a `unique` column (or a single-column primary key) to a fixed value
or a list that is too short to fill every row, seed stops and explains — it
cannot make 20 distinct rows from three choices. Use a generator or a longer
list.
:::
## Filling one column
`seed <table>.<column>` fills **one column across the rows that already
exist**, rather than adding new rows — the natural follow-up to
[`add column`](/reference/columns/), and the way to repair a single
column seed guessed wrongly. Combined with `set`, it sets that column
deliberately:
```rdbms
seed tickets.status set status in ('open', 'closed')
```
```
12 row(s) seeded into tickets
┌───────────┬──────────────────────────┬────────┬──────────┐
│ ticket_id │ subject │ status │ priority │
├───────────┼──────────────────────────┼────────┼──────────┤
│ 1 │ ad natus │ closed │ low │
│ 2 │ iusto officia │ closed │ high │
│ 3 │ possimus error │ closed │ high │
│ 4 │ reprehenderit et earum │ open │ low │
│ 5 │ cumque autem voluptas │ open │ low │
│ 6 │ maxime sed sit │ closed │ medium │
│ 7 │ atque libero │ open │ high │
│ 8 │ culpa maiores et │ closed │ low │
│ 9 │ natus rerum animi │ closed │ medium │
│ 10 │ sapiente rem │ closed │ low │
│ 11 │ placeat blanditiis quasi │ closed │ high │
│ 12 │ sed exercitationem │ open │ low │
└───────────┴──────────────────────────┴────────┴──────────┘
```
Only `status` changed; the other columns are untouched. Column-fill **refuses**
primary-key and autogenerated (`serial` / `shortid`) columns — you do not
"fill in" an identity column — and on an empty table it is a no-op.
## Columns seed can't guess
Open-ended choice columns — `status`, `role`, `type`, and the like — get
placeholder text, because there is no single sensible value for them. After a
seed, the playground points this out:
```rdbms
seed tickets 6
```
```
6 row(s) seeded into tickets
┌───────────┬────────────────────────┬────────────────────────────────────┬──────────┐
│ ticket_id │ subject │ status │ priority │
├───────────┼────────────────────────┼────────────────────────────────────┼──────────┤
│ 1 │ ad natus │ temporibus eos rerum │ low │
│ 2 │ iusto officia │ iure aut provident │ high │
│ 3 │ possimus error │ consequatur consequuntur molestiae │ high │
│ 4 │ reprehenderit et earum │ recusandae est quibusdam │ low │
│ 5 │ cumque autem voluptas │ ea praesentium pariatur │ low │
│ 6 │ maxime sed sit │ sapiente et et │ medium │
└───────────┴────────────────────────┴────────────────────────────────────┴──────────┘
```
> `status` filled with generic text — they look like fixed value sets. Pin
> them next time with `set status in ('…', '…')`, or fix these rows with
> `seed tickets.status set status in ('…', '…')`.
Here `priority` was filled from its built-in set automatically, so only
`status` is flagged. The two fixes it suggests are the
[`set` clause](#choosing-values-yourself) on
the next seed, and [column-fill](#filling-one-column) to repair the rows you
just made. If a `check` constraint restricts a column to a list of values
(`check status in ('open', 'closed')`), seed reads that list and uses it
automatically — no override needed.
## Limits
- The most you can seed at once is **10,000** rows; more is a friendly error
(a guard against a typo like `seed members 1000000`). Seed in smaller
batches if you genuinely need more.
- `seed members 0` does nothing.
A whole `seed` is a **single step** in the history: one [`undo`](/using-the-playground/undo-and-history/)
removes every row it added, not one row at a time.
## Syntax
```rdbms-syntax
seed <Table> [<count>] [set <col> = <value> | in (<value>, ...) | as <generator> | between <low> and <high>][, ...] [--seed <n>]
seed <Table>.<column> [set ...] [--seed <n>]
```
See also [Inserting & editing data](/reference/inserting-and-editing-data/),
[Relationships](/reference/relationships/), [Columns](/reference/columns/), and
[Constraints](/reference/constraints/).