Skip to content

Fix database version stamp running before its own migrations - #756

Merged
KrzysztofPajak merged 1 commit into
developfrom
fix/migration-version-stamp
Aug 8, 2026
Merged

Fix database version stamp running before its own migrations#756
KrzysztofPajak merged 1 commit into
developfrom
fix/migration-version-stamp

Conversation

@KrzysztofPajak

Copy link
Copy Markdown
Member

Type: bugfix

Issue

No issue was open for this; it came out of an audit of the migration runner.

MigrationManager.GetCurrentMigrations selects migrations with
Version.CompareTo(installedVersion) > 0. A version recorded in GrandNodeVersion before that
version's own migrations have run therefore puts them at the installed version, and they are
never selected again. The MigrationDb.Identity bookkeeping does not rescue this, because the
version filter is applied first.

Three defects combined to make that happen:

  1. Every MigrationUpgradeDbVersion_XX recorded GrandVersion.SupportedDBVersion, not its own
    version.
    Upgrading a 1.0 installation stamped 2.4 as soon as MigrationUpgradeDbVersion_11
    ran.
  2. The stamp had no guaranteed position within its version. All six stamps declare
    Priority => 0; in 1.1, 2.0, 2.1 and 2.2 the data migrations declare 0 as well, so the order
    came down to assembly scan order. In 2.4 the stamp (Priority => 0) was guaranteed to run
    before MigrationUpdateAdminSiteMap (1) and MigrationUpdateStandardPermissionNames (2).
  3. 1.1 through 2.2 set only DataBaseVersion, while RunMigrationProcess reads
    InstalledVersion first when it is non-empty.

Separately, MigrationProcess logged a failed migration and carried on to the next one — including
the version stamp — and version.DataBaseVersion.Split('.')[1] threw on a version string without a
second segment.

Reproduction: take an installation on 1.0, start the app, and kill the process after
MigrationUpgradeDbVersion_11 commits. GrandNodeVersion now reads 2.4. On the next start no
migration from 1.1 through 2.4 is selected, so permissions, sitemap entries and resource strings
from those versions are missing permanently.

Solution

  • IMigrationVersionStamp — a marker interface. MigrationManager orders migrations carrying it
    last within their version, whatever Priority they declare, so the ordering guarantee no longer
    depends on hand-assigned integers staying consistent across six folders.
  • Ordering now sorts on DbVersion (which already implements IComparable<DbVersion>) instead of
    Version.ToString(). The string form sorts "2.10" before "2.2".
  • MigrationUpgradeDbVersionBase records the migration's own Version, in both
    InstalledVersion and DataBaseVersion. The six stamp classes are reduced to Version and
    Identity. All Identity GUIDs are unchanged — they are persisted identities, and altering
    one would re-run the migration on every existing installation.
  • RunMigrationProcess stops at the first failed migration rather than continuing to the stamp, so
    a version is only recorded once everything below it succeeded and the run is retried on the next
    start. Version parsing no longer throws on a malformed version string.
  • Five regression tests in Grand.Modules.Tests.

Note for reviewers: MigrationManager scans loaded assemblies, and a project reference alone does
not load one. The existing GetCurrentMigrations_Exists test was therefore asserting against a
local test fixture rather than the shipped migrations. The new tests hold the migration assembly in
a static field so the scan sees it.

Breaking changes

None to any public contract. IMigrationVersionStamp is additive, and no Identity, migration
Name, or Version changed.

Two intentional behaviour changes:

  • Each version stamp now records its own version instead of the latest supported one. On a run that
    completes, the end state is identical to before (the last stamp still writes the current version);
    it differs only when a run is interrupted, which is the point of the fix.
  • A failed migration now aborts the run instead of letting later migrations proceed on top of it.

This does not retroactively repair an installation already left with a version it never truly
reached; such an installation still needs its GrandNodeVersion corrected by hand.

Testing

  1. dotnet build ./GrandNode.sln — succeeds.
  2. dotnet test ./src/Tests/Grand.Modules.Tests/Grand.Modules.Tests.csproj — 22/22 pass.
  3. dotnet test ./src/Tests/Grand.Infrastructure.Tests/Grand.Infrastructure.Tests.csproj — 84/84 pass.
  4. To confirm the tests actually guard the defect, temporarily change
    MigrationUpgradeDbVersionBase.UpgradeProcess back to writing
    GrandVersion.SupportedDBVersion and re-run step 2. VersionStamp_RecordsItsOwnVersion fails
    with expected: "1.1", actual: "2.4". Revert.
  5. Upgrade path on a real database: restore a dump from an older version (or set
    GrandNodeVersion.InstalledVersion and DataBaseVersion to 2.3), start the app, and confirm
    the MigrationDb collection gains one row per 2.4 migration and GrandNodeVersion ends at
    2.4.
  6. Interruption path: with the database at 2.3, put a breakpoint or a thrown exception in
    MigrationUpdateStandardPermissionNames, start the app, and confirm GrandNodeVersion still
    reads 2.3 — before this change it would already read 2.4 and the remaining 2.4 migrations
    would never run again.

🤖 Generated with Claude Code

GetCurrentMigrations filters on Version.CompareTo(installed) > 0, so a
version recorded before that version's migrations completed puts them at
or below the installed version and they are never selected again. The
MigrationDb.Identity check does not help - the version filter runs first.

Three defects combined to trigger this:

- Every MigrationUpgradeDbVersion_XX recorded GrandVersion.SupportedDBVersion
  instead of its own version, so upgrading a 1.0 installation stamped 2.4 from
  the 1.1 migration onwards.
- The stamp had no guaranteed position. All six declared Priority 0, and in
  1.1, 2.0, 2.1 and 2.2 the data migrations declared 0 as well, leaving the
  order to assembly scan order. In 2.4 the stamp was guaranteed to run first.
- 1.1 through 2.2 set only DataBaseVersion, while RunMigrationProcess reads
  InstalledVersion first.

An upgrade interrupted after the stamp left an installation claiming a version
whose permissions and sitemap entries had never been written, with no way to
recover automatically.

IMigrationVersionStamp marks the stamp so MigrationManager orders it last
within its version regardless of the priority it declares. Ordering now uses
DbVersion rather than its string form, which would sort "2.10" before "2.2".
MigrationUpgradeDbVersionBase records the migration's own version in both
fields. Identity GUIDs are unchanged - they are persisted identities.

RunMigrationProcess now stops at the first failed migration instead of
continuing on to the stamp, so the version is only recorded once everything
below it succeeded, and the run is retried on the next start.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 8, 2026 14:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/Modules/Grand.Module.Migration/Migrations/MigrationProcess.cs Dismissed
@KrzysztofPajak
KrzysztofPajak merged commit cf8c909 into develop Aug 8, 2026
6 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the fix/migration-version-stamp branch August 8, 2026 16:05
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.

2 participants