mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 08:23:45 +00:00
feat(multiplayer): add down migrations and a rollback runner
Add migrations.Rollback(ctx, db, dir, steps): reverses the N most recently applied migrations, newest first, each in its own committed transaction under the same advisory lock Apply uses. Down SQL lives in migrations/down/<version>.sql (a subdirectory, so Apply's *.sql glob over the main directory is untouched); a missing down file for a migration being rolled back is a hard error rather than a silent partial reversal. Wire it into cmd/migrate as --rollback=N. Add down files for all six existing migrations, each dropping objects in FK-safe reverse dependency order. Adversarial review: could not run the new integration test (TestPostgreSQLMigrationsRollBackAndReapplyCleanly, gated behind COSMIC_CLASH_POSTGRES_DSN / scripts/run_postgres_integration.sh) against a real database in this sandbox - Docker Desktop's own overlayfs ran out of space pulling postgres:17-alpine, unrelated to this change. Verified instead by hand-tracing every DROP against its forward migration's FK graph, confirming Apply's directory glob does not pick up the down/ subdirectory, and a clean go build/vet/test -tags integration. Worth an explicit real run before this is trusted in CI.
This commit is contained in:
@@ -488,3 +488,64 @@ func TestPostgreSQLMigrationsAreForwardExecutable(t *testing.T) {
|
||||
t.Fatal("assignments migration did not create its table")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostgreSQLMigrationsRollBackAndReapplyCleanly(t *testing.T) {
|
||||
db := openIntegrationPostgres(t)
|
||||
applyIntegrationMigrations(t, db)
|
||||
dir := filepath.Join("..", "migrations")
|
||||
tableExists := func(table string) bool {
|
||||
var count int
|
||||
if err := db.QueryRow(`SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1`, table).Scan(&count); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return count == 1
|
||||
}
|
||||
if !tableExists("assignments") || !tableExists("allocations") {
|
||||
t.Fatal("expected forward-applied schema before rollback")
|
||||
}
|
||||
|
||||
// Roll back every migration one at a time, in reverse, checking each
|
||||
// down file actually undoes what its forward file created — not just
|
||||
// that Rollback returns nil.
|
||||
if err := migrations.Rollback(context.Background(), db, dir, 1); err != nil {
|
||||
t.Fatalf("rollback 0006: %v", err)
|
||||
}
|
||||
var hasAllocationClaimColumn bool
|
||||
if err := db.QueryRow(`SELECT count(*) > 0 FROM information_schema.columns WHERE table_name = 'matches' AND column_name = 'allocation_id'`).Scan(&hasAllocationClaimColumn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if hasAllocationClaimColumn {
|
||||
t.Fatal("0006 rollback did not drop matches.allocation_id")
|
||||
}
|
||||
|
||||
if err := migrations.Rollback(context.Background(), db, dir, 4); err != nil {
|
||||
t.Fatalf("rollback remaining down to 0001: %v", err)
|
||||
}
|
||||
if tableExists("assignments") || tableExists("allocations") || tableExists("game_servers") {
|
||||
t.Fatal("rollback left later-migration tables behind")
|
||||
}
|
||||
|
||||
if err := migrations.Rollback(context.Background(), db, dir, 1); err != nil {
|
||||
t.Fatalf("rollback 0001: %v", err)
|
||||
}
|
||||
if tableExists("identities") || tableExists("matches") {
|
||||
t.Fatal("0001 rollback did not drop its own tables")
|
||||
}
|
||||
var remaining int
|
||||
if err := db.QueryRow(`SELECT count(*) FROM schema_migrations`).Scan(&remaining); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if remaining != 0 {
|
||||
t.Fatalf("expected schema_migrations empty after full rollback, got %d rows", remaining)
|
||||
}
|
||||
|
||||
// Reapplying from a fully rolled-back state must reach the same schema,
|
||||
// proving down files don't leave orphaned state that trips a forward
|
||||
// re-run (e.g. a constraint or index Apply then tries to recreate).
|
||||
if err := migrations.Apply(context.Background(), db, dir); err != nil {
|
||||
t.Fatalf("reapply after full rollback: %v", err)
|
||||
}
|
||||
if !tableExists("assignments") || !tableExists("allocations") {
|
||||
t.Fatal("reapply after rollback did not recreate the schema")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user