Skip to content

Commit 3eacddd

Browse files
feat: add reconcile mode for declarative database seeding
Seed sets can now operate in reconcile mode where the spec is the source of truth. On each run, initium detects changes and converges the database: new rows are inserted, changed rows are updated, removed rows are deleted. - Add `mode: reconcile` option to seed sets (default: `once`) - Add per-row tracking table for change detection and orphan deletion - Add content hash on tracking table for fast skip when unchanged - Add `--dry-run` flag to preview changes without modifying the database - Add `--reconcile-all` flag to override all seed sets to reconcile mode - Auto-migrate existing tracking tables (add content_hash column) - Support auto_id + @ref resolution during reconciliation - 12 new tests covering reconcile, dry-run, validation, and migration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1185450 commit 3eacddd

8 files changed

Lines changed: 2317 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Reconcile mode for seed sets (`mode: reconcile`): declarative seeding where the spec is the source of truth. Changed rows are updated, new rows are inserted, and removed rows are deleted automatically.
12+
- `--reconcile-all` CLI flag to override all seed sets to reconcile mode for a single run.
13+
- `--dry-run` CLI flag to preview what changes reconciliation would make without modifying the database.
14+
- Per-row tracking table (`initium_seed_rows`) for change detection and orphan deletion in reconcile mode.
15+
- Content hash (`content_hash` column) on the seed tracking table for fast "anything changed?" checks before row-by-row comparison.
16+
- Automatic migration of existing tracking tables: the `content_hash` column is added transparently on first run. Existing seed sets remain in `once` mode with no behavior change.
17+
1018
## [1.1.0] - 2026-02-26
1119

1220
### Added

docs/seeding.md

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ phases:
5050
seed_sets: # Optional. Seed sets to apply in this phase.
5151
- name: initial_data
5252
order: 1 # Optional. Controls execution order across seed sets.
53+
mode: once # Optional. "once" (default) or "reconcile".
5354
tables:
5455
- table: config
5556
order: 1 # Optional. Controls execution order within a seed set.
@@ -82,6 +83,7 @@ phases:
8283
| `phases[].wait_for[].timeout` | string | No | Per-object timeout override (e.g. `60s`, `2m`, `1m30s`) |
8384
| `phases[].seed_sets[].name` | string | Yes | Unique name for the seed set (used in tracking) |
8485
| `phases[].seed_sets[].order` | integer | No | Execution order (lower values first, default: 0) |
86+
| `phases[].seed_sets[].mode` | string | No | Seed mode: `once` (default) or `reconcile` |
8587
| `phases[].seed_sets[].tables[].table` | string | Yes | Target database table name |
8688
| `phases[].seed_sets[].tables[].order` | integer | No | Execution order within the seed set (default: 0) |
8789
| `phases[].seed_sets[].tables[].unique_key` | string[] | No | Columns for duplicate detection |
@@ -213,6 +215,53 @@ rows:
213215
password_hash: "{{ env.ADMIN_PASSWORD_HASH }}"
214216
```
215217

218+
### Reconcile Mode
219+
220+
By default, seed sets are applied once and never modified (`mode: once`). Reconcile mode makes seeding declarative: the spec becomes the source of truth, and initium converges the database to match it on every run.
221+
222+
Enable reconcile mode per seed set:
223+
224+
```yaml
225+
seed_sets:
226+
- name: departments
227+
mode: reconcile # "once" (default) or "reconcile"
228+
tables:
229+
- table: departments
230+
unique_key: [name] # Required for reconcile mode
231+
rows:
232+
- name: Engineering
233+
- name: Sales
234+
```
235+
236+
Or override all seed sets for a single run:
237+
238+
```bash
239+
initium seed --spec /seeds/seed.yaml --reconcile-all
240+
```
241+
242+
**How it works:**
243+
244+
1. On each run, initium computes a content hash of the rendered seed set (after template/env expansion).
245+
2. If the hash matches the stored hash, the seed set is skipped (no-op).
246+
3. If the hash differs, initium reconciles row by row:
247+
- **New rows** (in spec but not in DB) are inserted.
248+
- **Changed rows** (different values for same unique key) are updated.
249+
- **Removed rows** (in DB but not in spec) are deleted.
250+
251+
**Requirements:**
252+
- Every table in a reconciled seed set must have a `unique_key`. Without it, there is no way to identify which rows correspond to which spec entries.
253+
- Environment variable changes trigger reconciliation (resolved values are compared, not raw templates).
254+
255+
**Row tracking:** Initium creates a companion table (`{tracking_table}_rows`, e.g., `initium_seed_rows`) that stores the resolved values of each seeded row. This enables change detection and orphan deletion.
256+
257+
**Dry-run mode:** Preview what reconciliation would do without modifying the database:
258+
259+
```bash
260+
initium seed --spec /seeds/seed.yaml --dry-run
261+
```
262+
263+
This logs insert/update/delete counts per table without executing any changes.
264+
216265
### Reset Mode
217266

218267
Use `--reset` to delete all data from seeded tables and remove tracking entries before re-applying. Tables are deleted in reverse order to respect foreign key constraints:
@@ -276,11 +325,13 @@ spec:
276325

277326
## CLI Reference
278327

279-
| Flag | Default | Description |
280-
| --------- | ---------- | --------------------------------------- |
281-
| `--spec` | (required) | Path to seed spec file (YAML or JSON) |
282-
| `--reset` | `false` | Delete existing data and re-apply seeds |
283-
| `--json` | `false` | Enable JSON log output |
328+
| Flag | Default | Description |
329+
| ------------------ | ---------- | --------------------------------------------------------- |
330+
| `--spec` | (required) | Path to seed spec file (YAML or JSON) |
331+
| `--reset` | `false` | Delete existing data and re-apply seeds |
332+
| `--dry-run` | `false` | Preview changes without modifying the database |
333+
| `--reconcile-all` | `false` | Override all seed sets to reconcile mode for this run |
334+
| `--json` | `false` | Enable JSON log output |
284335

285336
## Failure Modes
286337

src/main.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,18 @@ enum Commands {
145145
help = "Reset mode: delete existing data before re-seeding"
146146
)]
147147
reset: bool,
148+
#[arg(
149+
long,
150+
env = "INITIUM_DRY_RUN",
151+
help = "Dry-run: show what would change without modifying the database"
152+
)]
153+
dry_run: bool,
154+
#[arg(
155+
long,
156+
env = "INITIUM_RECONCILE_ALL",
157+
help = "Override all seed sets to reconcile mode for this run"
158+
)]
159+
reconcile_all: bool,
148160
},
149161

150162
/// Render templates into config files
@@ -313,7 +325,12 @@ fn main() {
313325
lock_file,
314326
args,
315327
} => cmd::migrate::run(&log, &args, &workdir, &lock_file),
316-
Commands::Seed { spec, reset } => seed::run(&log, &spec, reset),
328+
Commands::Seed {
329+
spec,
330+
reset,
331+
dry_run,
332+
reconcile_all,
333+
} => seed::run(&log, &spec, reset, dry_run, reconcile_all),
317334
Commands::Render {
318335
template,
319336
output,

0 commit comments

Comments
 (0)