
# Seeding Data

A freshly installed app has a schema and no rows. `sovrium seed` fills it from files
you keep beside your config, so a new checkout opens on a working app instead of an
empty grid.

```bash
sovrium seed
```

It reads `seed/<table>.yaml`, resolves the links between records, expands any date
tokens, and writes through the same code path the REST API uses. No server needs to
be running.

## The seed directory

One file per table, named after the table:

```
my-app/
  app.yaml
  config/tables/companies.yaml
  config/tables/contacts.yaml
  seed/companies.yaml
  seed/contacts.yaml
```

`seed/companies.yaml`:

```yaml
records:
  - key: northwind
    fields:
      name: Northwind Trading
      industry: Retail
```

`seed/contacts.yaml`:

```yaml
records:
  - key: priya
    fields:
      name: Priya Raman
      email: priya@northwind.example
      company: '@companies.northwind'
      last_contacted: '{{today-3d}}'
```

Tables are written parents-first. You do not have to order the files yourself — the
command reads your relationship fields and works out the order. A genuine cycle is
refused by name rather than guessed at.

## Linking records with `key`

`key` names a record inside your seed files. It is never written to a column and never
appears in your database — it exists so one record can point at another before either
one has an id.

Reference it from another file as `@<table>.<key>`:

```yaml
company: '@companies.northwind'
```

A many-to-many field takes a list:

```yaml
tags: ['@tags.urgent', '@tags.renewal']
```

For a one-to-many relationship the foreign key lives on the child, so you seed the
children pointing at the parent — not the parent listing its children.

## Dates that stay current

A fixed date ages. A pipeline whose deals all closed last spring reads as abandoned by
autumn, and a calendar seeded with fixed days is empty the moment you look at a
different month.

Write dates relative to the day the seed runs:

```yaml
close_date: '{{today+21d}}'
last_contacted: '{{today-3d}}'
due: '{{today}}'
```

Every replay recomputes them, so a demo reset nightly always shows work in progress.

## Modes

| Mode       | Behaviour                                                                  |
| ---------- | -------------------------------------------------------------------------- |
| `if-empty` | Default. Seeds a table only when it has no rows. Safe to run repeatedly.   |
| `upsert`   | Matches existing rows on a natural key and updates them; creates the rest. |
| `replace`  | Deletes the table's rows, then inserts. For demo environments that reset.  |

```bash
sovrium seed --mode replace
```

`if-empty` counts soft-deleted rows as present, so a table you emptied through the app
is not silently refilled underneath you.

### Choosing what `upsert` matches on

`upsert` needs to know which column identifies an existing row. Declare it at the top
of the file:

```yaml
mergeOn: [email]
records:
  - key: priya
    fields:
      email: priya@northwind.example
      name: Priya Raman
```

Without `mergeOn`, the command uses the table's single unique field. If the table has
none, or more than one, it stops and asks — matching on the wrong column would
overwrite unrelated records, so it will not guess.

Note `mergeOn` and `key` are different things. `key` links records inside your files;
`mergeOn` names real columns in your database.

## Options

| Option           | Meaning                                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `[config]`       | Config file. Defaults to `./app.yaml`.                                                                                          |
| `--dir <path>`   | Seed directory. Defaults to a `seed` folder beside the config file. An explicit path is resolved against the working directory. |
| `--mode <mode>`  | `if-empty`, `upsert`, or `replace`. Defaults to `if-empty`.                                                                     |
| `--table <name>` | Seed only this table. Repeat for several.                                                                                       |
| `--dry-run`      | Report what would be written and write nothing.                                                                                 |

The directory default follows the config file rather than your shell, so the same
command behaves identically whether you run it from the project root or from a
service unit with a different working directory.

## Attachments

Put files in `seed/assets/` and reference them by name:

```yaml
logo: '@asset:northwind-logo.avif'
```

The file is uploaded and the stored key is written to the field.

## What seeding does not do

**It does not run your automations.** Records are written directly, so an automation
that reacts to record creation will not have fired. If your app's demo value depends
on something an automation produces — an activity log, a derived status — seed that
too, written the way the automation would have written it.

**Some fields are refused rather than half-written**, each with a message naming the
file and record:

- A relationship pointing at its own table needs two passes and is not supported yet.
- An attachment field on a bucket other than `default`.
- `upsert` on a table whose seed data carries many-to-many links.

Refusing is deliberate. Writing the row and dropping the links would leave you with
data that looks complete and is not.

## When a record is rejected

The message names the file, the record, the field, the value, and the reason:

```
companies.yaml (key "northwind"): field "size" — 201 is not one of the declared
options ('1-10', '11-50', '51-200', '201-1000', '1000+') — the submitted value is a
number; quote it in YAML to keep it a string [CHECK constraint failed: check_size_enum]
```

### Quote select values that start with a digit

That example is the most common surprise. YAML reads a scalar beginning with a digit
as a number, and a trailing range or suffix does not stop it:

| You write        | YAML gives you |
| ---------------- | -------------- |
| `size: 201-1000` | `201`          |
| `size: 1000+`    | `1000`         |
| `code: 07`       | `7`            |
| `qty: 1e3`       | `1000`         |
| `rate: 2.0`      | `2`            |

Quote them, and the value survives:

```yaml
size: '201-1000'
```

`yes`, `no`, `on` and `off` are read as strings, so they need no quoting.

## Working on the data

`--dry-run` reports the plan without touching the database:

```bash
sovrium seed --dry-run
```

```
[dry-run] companies: would create 5 records
[dry-run] contacts: would create 7 records
[dry-run] no changes written
```

To iterate on one file, restrict the run:

```bash
sovrium seed --table contacts --mode replace
```
