Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,13 @@ jobs:
- run: helm lint charts/initium
- run: helm template test-release charts/initium --set sampleDeployment.enabled=true --set 'initContainers[0].name=wait' --set 'initContainers[0].command[0]=wait-for' --set 'initContainers[0].args[0]=--target' --set 'initContainers[0].args[1]=tcp://localhost:5432'
- run: helm unittest charts/initium
ci:
if: always()
needs: [lint, test, build, helm-lint]
runs-on: ubuntu-latest
steps:
- run: |
if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
echo "One or more jobs failed or were cancelled"
exit 1
fi
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- 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.
- `--reconcile-all` CLI flag to override all seed sets to reconcile mode for a single run.
- `--dry-run` CLI flag to preview what changes reconciliation would make without modifying the database.
- Per-row tracking table (`initium_seed_rows`) for change detection and orphan deletion in reconcile mode.
- Content hash (`content_hash` column) on the seed tracking table for fast "anything changed?" checks before row-by-row comparison.
- 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.

### Changed
- Reconcile hash-skip now only applies to seed sets without `@ref:` expressions. Seed sets containing `@ref:` references always run row-level reconciliation to prevent stale foreign keys when upstream auto-generated IDs shift.
- Hash computation sorts tables by `(order, table_name)` instead of just `order` for deterministic hashing when multiple tables share the same order value.
- Dry-run mode treats `@ref:` expressions as literals to avoid failures when references haven't been populated yet (e.g., auto_id + refs within the same seed set).

### Fixed
- `--reconcile-all` now rejects seed sets where any table is missing `unique_key`, preventing reconciliation from generating identical row keys and updating/deleting wrong rows.
- Reconcile mode validation now rejects empty/whitespace-only `unique_key` entries and reserved column names like `_ref`.
- Reconcile mode validation now checks that every row contains all `unique_key` columns, preventing incomplete row keys during reconciliation.
- MySQL row tracking table now uses SHA-256 generated column (`row_key_hash`) for the primary key instead of `row_key(255)` prefix, preventing key collisions for JSON keys exceeding 255 bytes.

## [1.1.0] - 2026-02-26

### Added
Expand Down
63 changes: 58 additions & 5 deletions docs/seeding.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ phases:
seed_sets: # Optional. Seed sets to apply in this phase.
- name: initial_data
order: 1 # Optional. Controls execution order across seed sets.
mode: once # Optional. "once" (default) or "reconcile".
tables:
- table: config
order: 1 # Optional. Controls execution order within a seed set.
Expand Down Expand Up @@ -82,6 +83,7 @@ phases:
| `phases[].wait_for[].timeout` | string | No | Per-object timeout override (e.g. `60s`, `2m`, `1m30s`) |
| `phases[].seed_sets[].name` | string | Yes | Unique name for the seed set (used in tracking) |
| `phases[].seed_sets[].order` | integer | No | Execution order (lower values first, default: 0) |
| `phases[].seed_sets[].mode` | string | No | Seed mode: `once` (default) or `reconcile` |
| `phases[].seed_sets[].tables[].table` | string | Yes | Target database table name |
| `phases[].seed_sets[].tables[].order` | integer | No | Execution order within the seed set (default: 0) |
| `phases[].seed_sets[].tables[].unique_key` | string[] | No | Columns for duplicate detection |
Expand Down Expand Up @@ -213,6 +215,55 @@ rows:
password_hash: "{{ env.ADMIN_PASSWORD_HASH }}"
```

### Reconcile Mode

By default, seed sets are applied once and never modified (`mode: once`). Reconcile mode makes seeding declarative: the rendered spec becomes the source of truth, and initium reconciles the database to match it whenever the rendered spec changes.

If the rendered spec has not changed since the last run (content hash match), initium treats the seed set as already reconciled and skips it. Out-of-band database changes are not corrected until a spec change triggers reconciliation again.

Enable reconcile mode per seed set:

```yaml
seed_sets:
- name: departments
mode: reconcile # "once" (default) or "reconcile"
tables:
- table: departments
unique_key: [name] # Required for reconcile mode
rows:
- name: Engineering
- name: Sales
```

Or override all seed sets for a single run:

```bash
initium seed --spec /seeds/seed.yaml --reconcile-all
```

**How it works:**

1. On each run, initium computes a content hash of the rendered seed set (after template/env expansion).
2. If the hash matches the stored hash, the seed set is skipped (no-op).
3. If the hash differs, initium reconciles row by row:
- **New rows** (in spec but not in DB) are inserted.
- **Changed rows** (different values for same unique key) are updated.
- **Removed rows** (in DB but not in spec) are deleted.

**Requirements:**
- 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.
- Environment variable changes trigger reconciliation (resolved values are compared, not raw templates).

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

**Dry-run mode:** Preview what reconciliation would do without modifying the database:

```bash
initium seed --spec /seeds/seed.yaml --dry-run
```

This logs insert/update/delete counts per table without executing any changes.

### Reset Mode

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:
Expand Down Expand Up @@ -276,11 +327,13 @@ spec:

## CLI Reference

| Flag | Default | Description |
| --------- | ---------- | --------------------------------------- |
| `--spec` | (required) | Path to seed spec file (YAML or JSON) |
| `--reset` | `false` | Delete existing data and re-apply seeds |
| `--json` | `false` | Enable JSON log output |
| Flag | Default | Description |
| ------------------ | ---------- | --------------------------------------------------------- |
| `--spec` | (required) | Path to seed spec file (YAML or JSON) |
| `--reset` | `false` | Delete existing data and re-apply seeds |
| `--dry-run` | `false` | Preview changes without modifying the database |
| `--reconcile-all` | `false` | Override all seed sets to reconcile mode for this run |
| `--json` | `false` | Enable JSON log output |

## Failure Modes

Expand Down
19 changes: 18 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,18 @@ enum Commands {
help = "Reset mode: delete existing data before re-seeding"
)]
reset: bool,
#[arg(
long,
env = "INITIUM_DRY_RUN",
help = "Dry-run: show what would change without modifying the database"
)]
dry_run: bool,
#[arg(
long,
env = "INITIUM_RECONCILE_ALL",
help = "Override all seed sets to reconcile mode for this run"
)]
reconcile_all: bool,
},

/// Render templates into config files
Expand Down Expand Up @@ -313,7 +325,12 @@ fn main() {
lock_file,
args,
} => cmd::migrate::run(&log, &args, &workdir, &lock_file),
Commands::Seed { spec, reset } => seed::run(&log, &spec, reset),
Commands::Seed {
spec,
reset,
dry_run,
reconcile_all,
} => seed::run(&log, &spec, reset, dry_run, reconcile_all),
Commands::Render {
template,
output,
Expand Down
Loading
Loading