Files
Josh Creek 67609d71c0 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.
2026-09-01 12:41:30 +01:00

46 lines
1.2 KiB
Go

package main
import (
"context"
"database/sql"
"flag"
"fmt"
"os"
"time"
"github.com/cosmic-clash/cosmic-clash/server/migrations"
_ "github.com/jackc/pgx/v5/stdlib"
)
func main() {
dsn := flag.String("dsn", os.Getenv("COSMIC_CLASH_POSTGRES_DSN"), "PostgreSQL connection string")
directory := flag.String("dir", "migrations", "directory containing numbered SQL migrations")
rollback := flag.Int("rollback", 0, "roll back this many of the most recently applied migrations instead of applying forward")
flag.Parse()
if *dsn == "" {
fmt.Fprintln(os.Stderr, "migrate: --dsn or COSMIC_CLASH_POSTGRES_DSN is required")
os.Exit(2)
}
db, err := sql.Open("pgx", *dsn)
if err != nil {
fmt.Fprintln(os.Stderr, "migrate:", err)
os.Exit(1)
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if *rollback > 0 {
if err := migrations.Rollback(ctx, db, *directory, *rollback); err != nil {
fmt.Fprintln(os.Stderr, "migrate:", err)
os.Exit(1)
}
fmt.Printf("rolled back %d migration(s)\n", *rollback)
return
}
if err := migrations.Apply(ctx, db, *directory); err != nil {
fmt.Fprintln(os.Stderr, "migrate:", err)
os.Exit(1)
}
fmt.Println("migrations applied")
}