S04 follow-up: split migration DSN from app DSN when PgBouncer is introduced

Context

Currently the application uses a single dsn for both application queries and migrations. This works at bootstrap because there is no PgBouncer in front of PostgreSQL.

When PgBouncer is introduced in production (transaction pooling mode), the migration client must use a separate DSN that connects directly to PostgreSQL:

  • Goose uses session-level advisory locks (`pg_try_advisory_lock`) to coordinate migrations across multiple instances.
  • PgBouncer transaction pooling breaks session-level locks because the connection is not held for the duration of a session.

Scope

  1. Split the `Config` struct: reintroduce `MigrationDSN` alongside `DSN`.
  2. Update `NewMigrationClient` to use `cfg.MigrationDSN` and switch to `QueryExecModeCacheStatement` (safe on direct connections, faster than simple protocol).
  3. Update docs, CI, and mise tasks.

References

  • Related: MR !39 (merged) (database tooling bootstrap) — introduced the single DSN
  • Tracked in code as `TODO(pgbouncer-split)` in `internal/database/database.go`
  • Spec: S04 Database (MR !46 (merged))

Trigger

This work starts when PgBouncer is provisioned in front of PostgreSQL for the artifact-registry deployment, or when the cluster topology decision finalizes PgBouncer as a required component.

Edited by Suleimi Ahmed