Skip to content

fix(migrations): don't replay DB.raw side effects during plan collection#28

Merged
3m1n3nc3 merged 1 commit into
mainfrom
fix/migration-planning-side-effects
Jul 5, 2026
Merged

fix(migrations): don't replay DB.raw side effects during plan collection#28
3m1n3nc3 merged 1 commit into
mainfrom
fix/migration-planning-side-effects

Conversation

@3m1n3nc3

@3m1n3nc3 3m1n3nc3 commented Jul 5, 2026

Copy link
Copy Markdown
Member

A migration's up()/down() is invoked both to gather its schema operations and, when it uses DB.raw() directly, as a real side effect. Several flows re-invoke up() purely to (re)build column-mapping / feature metadata: MigrateCommand's per-migration plan, and syncPersistedColumnMappingsFromState / validatePersistedMetadataFeaturesForMigrations. On rollback, sync re-planned every still-applied migration's up(), replaying its raw SQL (e.g. an ADD CONSTRAINT) against a schema it was never meant to touch again — throwing 'already exists' and leaving state half-updated so later commands also failed. migrate survived only because the raw ran twice but idempotently.

  • Add a migration-planning context: while active, DB.raw is a no-op. It is anchored on globalThis via Symbol.for so the CLI bundle (dist/cli.mjs) and the library bundle (dist/index.mjs) share one AsyncLocalStorage — otherwise the flag would not cross from getMigrationPlan() to the migration's DB.raw().
  • getMigrationPlan(migration, direction, { inert }) runs the body inertly.
  • Planning-only callers pass inert: MigrateCommand's plan step, column-mapping rebuild, and feature validation. Actual apply/rollback stay non-inert, so the raw runs exactly once.

Verified end to end against a scratch database: migrate runs the raw once, and rollback of a later batch no longer replays a still-applied migration's raw SQL.

Tests: DB.raw runs on normal planning but is suppressed under inert planning while operations are still collected.

A migration's up()/down() is invoked both to gather its schema operations
and, when it uses DB.raw() directly, as a real side effect. Several flows
re-invoke up() purely to (re)build column-mapping / feature metadata:
MigrateCommand's per-migration plan, and syncPersistedColumnMappingsFromState
/ validatePersistedMetadataFeaturesForMigrations. On rollback, sync re-planned
every *still-applied* migration's up(), replaying its raw SQL (e.g. an ADD
CONSTRAINT) against a schema it was never meant to touch again — throwing
'already exists' and leaving state half-updated so later commands also failed.
migrate survived only because the raw ran twice but idempotently.

- Add a migration-planning context: while active, DB.raw is a no-op. It is
  anchored on globalThis via Symbol.for so the CLI bundle (dist/cli.mjs) and the
  library bundle (dist/index.mjs) share one AsyncLocalStorage — otherwise the
  flag would not cross from getMigrationPlan() to the migration's DB.raw().
- getMigrationPlan(migration, direction, { inert }) runs the body inertly.
- Planning-only callers pass inert: MigrateCommand's plan step, column-mapping
  rebuild, and feature validation. Actual apply/rollback stay non-inert, so the
  raw runs exactly once.

Verified end to end against a scratch database: migrate runs the raw once, and
rollback of a later batch no longer replays a still-applied migration's raw SQL.

Tests: DB.raw runs on normal planning but is suppressed under inert planning
while operations are still collected.
@3m1n3nc3 3m1n3nc3 merged commit cf447b9 into main Jul 5, 2026
6 checks passed
@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/helpers/column-mappings.ts 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@3m1n3nc3 3m1n3nc3 deleted the fix/migration-planning-side-effects branch July 5, 2026 08:30
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