Fix database version stamp running before its own migrations - #756
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type: bugfix
Issue
No issue was open for this; it came out of an audit of the migration runner.
MigrationManager.GetCurrentMigrationsselects migrations withVersion.CompareTo(installedVersion) > 0. A version recorded inGrandNodeVersionbefore thatversion's own migrations have run therefore puts them at the installed version, and they are
never selected again. The
MigrationDb.Identitybookkeeping does not rescue this, because theversion filter is applied first.
Three defects combined to make that happen:
MigrationUpgradeDbVersion_XXrecordedGrandVersion.SupportedDBVersion, not its ownversion. Upgrading a 1.0 installation stamped
2.4as soon asMigrationUpgradeDbVersion_11ran.
Priority => 0; in 1.1, 2.0, 2.1 and 2.2 the data migrations declare0as well, so the ordercame down to assembly scan order. In 2.4 the stamp (
Priority => 0) was guaranteed to runbefore
MigrationUpdateAdminSiteMap(1) andMigrationUpdateStandardPermissionNames(2).DataBaseVersion, whileRunMigrationProcessreadsInstalledVersionfirst when it is non-empty.Separately,
MigrationProcesslogged a failed migration and carried on to the next one — includingthe version stamp — and
version.DataBaseVersion.Split('.')[1]threw on a version string without asecond segment.
Reproduction: take an installation on 1.0, start the app, and kill the process after
MigrationUpgradeDbVersion_11commits.GrandNodeVersionnow reads2.4. On the next start nomigration 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.MigrationManagerorders migrations carrying itlast within their version, whatever
Prioritythey declare, so the ordering guarantee no longerdepends on hand-assigned integers staying consistent across six folders.
DbVersion(which already implementsIComparable<DbVersion>) instead ofVersion.ToString(). The string form sorts"2.10"before"2.2".MigrationUpgradeDbVersionBaserecords the migration's ownVersion, in bothInstalledVersionandDataBaseVersion. The six stamp classes are reduced toVersionandIdentity. AllIdentityGUIDs are unchanged — they are persisted identities, and alteringone would re-run the migration on every existing installation.
RunMigrationProcessstops at the first failed migration rather than continuing to the stamp, soa 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.
Grand.Modules.Tests.Note for reviewers:
MigrationManagerscans loaded assemblies, and a project reference alone doesnot load one. The existing
GetCurrentMigrations_Existstest was therefore asserting against alocal 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.
IMigrationVersionStampis additive, and noIdentity, migrationName, orVersionchanged.Two intentional behaviour changes:
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.
This does not retroactively repair an installation already left with a version it never truly
reached; such an installation still needs its
GrandNodeVersioncorrected by hand.Testing
dotnet build ./GrandNode.sln— succeeds.dotnet test ./src/Tests/Grand.Modules.Tests/Grand.Modules.Tests.csproj— 22/22 pass.dotnet test ./src/Tests/Grand.Infrastructure.Tests/Grand.Infrastructure.Tests.csproj— 84/84 pass.MigrationUpgradeDbVersionBase.UpgradeProcessback to writingGrandVersion.SupportedDBVersionand re-run step 2.VersionStamp_RecordsItsOwnVersionfailswith
expected: "1.1", actual: "2.4". Revert.GrandNodeVersion.InstalledVersionandDataBaseVersionto2.3), start the app, and confirmthe
MigrationDbcollection gains one row per 2.4 migration andGrandNodeVersionends at2.4.2.3, put a breakpoint or a thrown exception inMigrationUpdateStandardPermissionNames, start the app, and confirmGrandNodeVersionstillreads
2.3— before this change it would already read2.4and the remaining 2.4 migrationswould never run again.
🤖 Generated with Claude Code