Skip to content

feat: add journaled schema migration runner - #3

Merged
fabbrik merged 1 commit into
mainfrom
feat/schema-migration-runner
Sep 18, 2026
Merged

fabbrik merged 1 commit into
mainfrom
feat/schema-migration-runner

Conversation

@fabbrik

@fabbrik fabbrik commented Sep 18, 2026

Copy link
Copy Markdown
Owner

Summary

Adds ExperienceSchemaMigrator.MigrateAsync(NpgsqlDataSource, CancellationToken) to AgentExperience.Storage.Postgres, and returns the scripts it applied.

  • Journaled. Applied scripts are recorded in agent_experience.schema_versions, so a rerun applies nothing. A database whose 0001 was applied by hand is journaled on the next run without losing rows.
  • One transaction per script. A failing script rolls back its own transaction and throws ExperienceStoreException naming it, with no SQL text or row data. Scripts applied before it stay applied.
  • Serialized across processes. The run holds a PostgreSQL session advisory lock on its own connection, so two hosts starting at once cannot apply the same script twice. The lock is always released.
  • Explicit. Hosts call it at startup. The store never migrates, on construction or otherwise.
  • Pins: dbup-postgresql 7.0.1 and dbup-core 6.1.1, both exact, verified against Npgsql 10.0.3. The Abstractions and Core dependency-boundary tests now forbid DbUp too.

This closes the Story 2.1 epic criterion for documented migrations, which was deferred when 2.1 was split, and unblocks Story 2.4.

Behaviour worth knowing (documented)

  • Cancelling once scripts are running is ignored: DbUp's upgrade has no cancellation point, so the call runs to completion. Cancelling before the call, or while waiting for the lock, throws OperationCanceledException and leaves no lock held.
  • Each script gets Npgsql's default 30s command timeout, while the lock wait is deliberately unbounded. Hosts can raise it with Command Timeout on the data source.
  • The data source must allow at least two connections, and must not use multiplexing.
  • Migrating needs CREATE on the database and on the schema. The store itself still only needs INSERT and SELECT.

Deferred

Verifying that the runner emits nothing to the host's log sink. Logged in deferred-work.md.

Test plan

  • dotnet build --configuration Release: 0 warnings, 0 errors
  • dotnet test --configuration Release: 315 of 315 pass (52 store tests, up from 40). Locally with TESTCONTAINERS_RYUK_DISABLED=true.
  • One test per edge case on its own database: fresh, rerun, hand-applied 0001, concurrent runs blocked on the lock, failing script, cancellation. Two more cover a second script applying on top of a journaled one with $body$ quoting intact, and the embedded scripts matching ScriptNames.
  • CI green

🤖 Generated with Claude Code

ExperienceSchemaMigrator.MigrateAsync applies the package's embedded scripts
through DbUp: journaled to agent_experience.schema_versions, one transaction
per script, serialized across processes by a session advisory lock held on its
own connection. Hosts call it explicitly; the store never migrates.

Replaces the manual apply loop in the README and the test fixture, and closes
the Story 2.1 epic criterion for documented migrations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fabbrik
fabbrik merged commit a3ed9b5 into main Sep 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant