-
Notifications
You must be signed in to change notification settings - Fork 6
test(db): regression test for migration 000027 idempotency (closes #246) #540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cristim
merged 4 commits into
feat/multicloud-web-frontend
from
fix/246-migration-idempotent
May 22, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ad3f216
fix(db): add regression test for migration 000027 idempotency
cristim 958d51a
fix(db): fix 000040 down SQL column scope and test rollback loop
cristim e44b702
refactor(migrations): extract getMigrationsPath to shared test helper
cristim d9a4db6
fix(db): derive rollback step count from schema_migrations row count
cristim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| //go:build integration | ||
| // +build integration | ||
|
|
||
| package migrations_test | ||
|
|
||
| import ( | ||
| "path/filepath" | ||
| "runtime" | ||
| ) | ||
|
|
||
| // getMigrationsPath resolves the migrations directory relative to this test | ||
| // file so that migration test files can locate the SQL files regardless of | ||
| // the working directory when tests are invoked. | ||
| func getMigrationsPath() string { | ||
| _, filename, _, _ := runtime.Caller(0) | ||
| return filepath.Dir(filename) | ||
| } |
111 changes: 111 additions & 0 deletions
111
internal/database/postgres/migrations/savings_snapshots_pk_test.go
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| //go:build integration | ||
| // +build integration | ||
|
|
||
| package migrations_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/LeanerCloud/CUDly/internal/database/postgres/migrations" | ||
| "github.com/LeanerCloud/CUDly/internal/database/postgres/testhelpers" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // TestMigration_SavingsSnapshotsPK verifies that migration 000027 is | ||
| // idempotent on a fresh database. | ||
| // | ||
| // On a fresh DB, migration 000018 already adds savings_snapshots_pkey. Before | ||
| // the fix, 000027's bare ADD CONSTRAINT would fail with "multiple primary | ||
| // keys for table savings_snapshots". After the fix, the DROP CONSTRAINT IF | ||
| // EXISTS guard makes 000027 safe to apply regardless of whether 000018 has | ||
| // already created the constraint. | ||
| func TestMigration_SavingsSnapshotsPK(t *testing.T) { | ||
| ctx := context.Background() | ||
| migrationsPath := getMigrationsPath() | ||
|
|
||
| t.Run("full migration from scratch succeeds (000018 + 000027 coexist)", func(t *testing.T) { | ||
| container, err := testhelpers.SetupPostgresContainer(ctx, t) | ||
| require.NoError(t, err) | ||
| defer container.Cleanup(ctx) | ||
| pool := container.DB.Pool() | ||
|
|
||
| // Running all migrations must succeed on a fresh DB — 000027's | ||
| // DROP CONSTRAINT IF EXISTS prevents "multiple primary keys" | ||
| // that the pre-fix ADD CONSTRAINT caused. | ||
| require.NoError(t, migrations.RunMigrations(ctx, pool, migrationsPath, "", ""), | ||
| "full migration from scratch should succeed with idempotent 000027") | ||
|
|
||
| // The primary key must exist and cover (id, timestamp). | ||
| var constraintExists bool | ||
| err = pool.QueryRow(ctx, ` | ||
| SELECT EXISTS ( | ||
| SELECT 1 FROM pg_constraint | ||
| WHERE conname = 'savings_snapshots_pkey' | ||
| AND conrelid = 'savings_snapshots'::regclass | ||
| AND contype = 'p' | ||
| )`).Scan(&constraintExists) | ||
| require.NoError(t, err) | ||
| assert.True(t, constraintExists, "savings_snapshots_pkey should exist after migrations") | ||
| }) | ||
|
|
||
| t.Run("rollback to 000026 then re-apply 000027 succeeds", func(t *testing.T) { | ||
| container, err := testhelpers.SetupPostgresContainer(ctx, t) | ||
| require.NoError(t, err) | ||
| defer container.Cleanup(ctx) | ||
| pool := container.DB.Pool() | ||
|
|
||
| require.NoError(t, migrations.RunMigrations(ctx, pool, migrationsPath, "", "")) | ||
|
|
||
| // Count how many applied migrations are above version 26. | ||
| // Using COUNT from schema_migrations is correct even when migration | ||
| // numbering has gaps: Steps(-n) steps back through n applied | ||
| // migrations, so we need the actual row count, not an arithmetic | ||
| // difference between version numbers. | ||
| var stepsToRollback int | ||
| err = pool.QueryRow(ctx, `SELECT COUNT(*) FROM schema_migrations WHERE version > 26`).Scan(&stepsToRollback) | ||
| require.NoError(t, err) | ||
| require.Greater(t, stepsToRollback, 0, "there should be migrations above 000026") | ||
|
|
||
| // RollbackMigrations caps a single call at 10 steps; call in a loop. | ||
| const maxRollbackPerCall = 10 | ||
| for remaining := stepsToRollback; remaining > 0; remaining -= maxRollbackPerCall { | ||
| batch := remaining | ||
| if batch > maxRollbackPerCall { | ||
| batch = maxRollbackPerCall | ||
| } | ||
| require.NoError(t, migrations.RollbackMigrations(ctx, pool, migrationsPath, batch), | ||
| "rollback to 000026 should succeed") | ||
| } | ||
|
|
||
| // Verify the constraint is gone (000027 was rolled back). | ||
| var constraintExists bool | ||
| err = pool.QueryRow(ctx, ` | ||
| SELECT EXISTS ( | ||
| SELECT 1 FROM pg_constraint | ||
| WHERE conname = 'savings_snapshots_pkey' | ||
| AND conrelid = 'savings_snapshots'::regclass | ||
| AND contype = 'p' | ||
| )`).Scan(&constraintExists) | ||
| require.NoError(t, err) | ||
| assert.False(t, constraintExists, "savings_snapshots_pkey should be gone after rollback past 000027") | ||
|
|
||
| // Re-apply all migrations — 000027 must succeed even though | ||
| // 000018's schema_migrations entry is still present (that migration | ||
| // was not rolled back, though its original constraint was superseded | ||
| // when 000027 ran and dropped/re-created savings_snapshots_pkey). | ||
| require.NoError(t, migrations.RunMigrations(ctx, pool, migrationsPath, "", ""), | ||
| "re-applying 000027 over a DB where 000018 already ran must succeed") | ||
|
|
||
| err = pool.QueryRow(ctx, ` | ||
| SELECT EXISTS ( | ||
| SELECT 1 FROM pg_constraint | ||
| WHERE conname = 'savings_snapshots_pkey' | ||
| AND conrelid = 'savings_snapshots'::regclass | ||
| AND contype = 'p' | ||
| )`).Scan(&constraintExists) | ||
| require.NoError(t, err) | ||
| assert.True(t, constraintExists, "savings_snapshots_pkey should be restored after re-apply") | ||
| }) | ||
| } | ||
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
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.