Skip to content

Refuse to re-apply an edited already-applied migration (closes #29)#32

Open
CatFortman wants to merge 2 commits into
developmentfrom
fix/edited-migration-guard
Open

Refuse to re-apply an edited already-applied migration (closes #29)#32
CatFortman wants to merge 2 commits into
developmentfrom
fix/edited-migration-guard

Conversation

@CatFortman

Copy link
Copy Markdown
Owner

Summary

  • Fixes the critical bug in Editing an already-applied migration silently re-runs it on apply #29: CheckMigrationApplied matches on (MigrationName, Checksum, Success = 1), so editing an already-applied migration produces a (name, new-checksum) pair that isn't in history. HasBeenApplied returned false and ApplyScriptFile silently re-executed the edited migration against a database where the original had already run. Dry-run already flagged this as CHANGED, but nothing stopped a plain apply.
  • ApplyScriptFile (migrations only) now looks up the checksum of the migration's last successful apply by name alone (SqlStatements.SelectLatestSuccessfulMigrationChecksum) before the existing skip check. If a successful history row exists and its checksum differs from the file on disk, it throws immediately, naming the migration, instead of running the edited SQL.
  • The decision logic is factored into a pure, unit-tested function (MigrationService.DetectEditedMigration) separate from the DB lookup, per the pattern established by Add a unit test project — CI currently runs 'dotnet test' against zero tests #18's test project.
  • Object scripts are unaffected — re-applying an edited proc/view is still the designed workflow (this only guards ScriptKind.Migration).
  • A migration whose only history row is a failed attempt (Success = 0) is still allowed to apply/re-run after being fixed, since the guard only considers Success = 1 rows.

Depends on #31 (adds the test project this PR's new tests live in) — until that merges, this PR's diff includes both branches' commits; only the last commit here is new.

I intentionally did not add the optional --force-changed escape hatch the issue mentions, to keep this PR scoped to the hard-stop fix; happy to follow up separately if wanted.

Test plan

  • dotnet test MigrationOps.sln — 43/43 passing, including new DetectEditedMigrationTests (unchanged-applied allowed, edited-applied rejected, failed-then-fixed allowed)
  • Verified live against a throwaway LocalDB database:
    • Fresh apply of a new migration + object script — succeeds, history recorded
    • Re-running apply with no changes — clean no-op skip
    • Editing the applied migration and re-running apply — halts with Migration '...' was already applied with checksum ... but the file now has checksum .... Migrations are immutable once applied - create a new migration instead of editing this one.; confirmed the edited SQL never ran (no new column) and no duplicate/second history row was written
    • Fixing a migration whose only history row was a failed attempt — applies successfully (exit 0)
    • Editing an already-applied object script (view) — still re-applies normally, not blocked

…pure logic

CI's `dotnet test` currently passes vacuously with zero tests. Adds unit
coverage for the tag/checksum header parsing, the CREATE OR ALTER guard on
database object scripts, and the dry-run classification logic (applied /
pending / drifted-migration-vs-drifted-object-script / checksum-missing /
tagless), which is the trickiest and most safety-critical logic in the repo.

EnsureCreateOrAlterStatement and AddPlanEntry move from private to internal
(with InternalsVisibleTo) so they're directly testable without a live SQL
Server connection.

Closes #18
CheckMigrationApplied matches on (name, checksum, Success=1), so an edited
migration file produces a (name, new-checksum) pair with no history row -
HasBeenApplied returns false and apply silently re-executes the modified
migration against a database where the original already ran. Dry-run already
classified this as CHANGED, but nothing stopped a plain `apply`.

ApplyScriptFile now looks up the checksum of the migration's last
*successful* apply by name alone before the existing (name, checksum) check.
If one exists and differs from the file's current checksum, it throws
immediately naming the migration, instead of running the edited SQL. Object
scripts are unaffected - re-applying an edited proc/view is still the
designed workflow.

A migration whose only history is a failed attempt (Success = 0) is still
allowed to apply after being fixed, since the guard only looks at
Success = 1 rows.

Verified live against LocalDB (see PR description): fresh apply, unchanged
no-op re-run, edited-migration throw (DB left untouched, no duplicate
history row), failed-then-fixed re-run, and an edited object script still
re-applying normally.

Closes #29
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