package main import ( "fmt" "os" "strconv" "github.com/gochat/gochat/internal/config" "github.com/gochat/gochat/internal/database" ) func main() { config.LoadDotEnvEnvironment() database.SanitizePostgresEnvironment() if len(os.Args) < 2 { printUsage() os.Exit(1) } command := os.Args[1] // Load the same root .env and environment overlay as the application. env := os.Getenv("GOCHAT_ENV") if env == "" { env = "development" } cfg, err := config.LoadWithEnv(env) if err != nil { fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) os.Exit(1) } dbURL := cfg.Database.MigrateDSN() if cfg.Server.Mode == "release" { if err := config.ValidateProductionDatabaseDSN(dbURL); err != nil { fmt.Fprintf(os.Stderr, "Error validating database DSN: %v\n", err) os.Exit(1) } } migrationsPath := cfg.Database.GetMigrationsPath() switch command { case "up": if err := database.RunMigrations(dbURL, migrationsPath); err != nil { fmt.Fprintf(os.Stderr, "Error running migrations up: %v\n", err) os.Exit(1) } fmt.Println("Migrations applied successfully (up)") case "down": if err := rejectDestructiveProductionMigration(env, -1); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } if err := database.RollbackMigrations(dbURL, migrationsPath); err != nil { fmt.Fprintf(os.Stderr, "Error running migrations down: %v\n", err) os.Exit(1) } fmt.Println("Migrations rolled back successfully (down)") case "force": if len(os.Args) < 3 { fmt.Fprintf(os.Stderr, "Error: 'force' requires a version argument\n") printUsage() os.Exit(1) } version, err := strconv.Atoi(os.Args[2]) if err != nil { fmt.Fprintf(os.Stderr, "Error: invalid version number '%s': %v\n", os.Args[2], err) os.Exit(1) } if err := database.ForceVersion(dbURL, migrationsPath, version); err != nil { fmt.Fprintf(os.Stderr, "Error forcing version %d: %v\n", version, err) os.Exit(1) } fmt.Printf("Migration version forced to %d\n", version) case "version": version, dirty, err := database.CurrentVersion(dbURL, migrationsPath) if err != nil { fmt.Fprintf(os.Stderr, "Error getting current version: %v\n", err) os.Exit(1) } if dirty { fmt.Printf("Current version: %d (DIRTY — a migration partially failed)\n", version) } else { fmt.Printf("Current version: %d\n", version) } case "steps": if len(os.Args) < 3 { fmt.Fprintf(os.Stderr, "Error: 'steps' requires a step count argument\n") printUsage() os.Exit(1) } steps, err := strconv.Atoi(os.Args[2]) if err != nil { fmt.Fprintf(os.Stderr, "Error: invalid step count '%s': %v\n", os.Args[2], err) os.Exit(1) } if err := rejectDestructiveProductionMigration(env, steps); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } if err := database.MigrateSteps(dbURL, migrationsPath, steps); err != nil { fmt.Fprintf(os.Stderr, "Error running %d migration steps: %v\n", steps, err) os.Exit(1) } fmt.Printf("Applied %d migration steps successfully\n", steps) default: fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command) printUsage() os.Exit(1) } } func rejectDestructiveProductionMigration(env string, steps int) error { if env == "production" && steps < 0 { return fmt.Errorf("destructive schema down is disabled in production; roll back the application image or restore a backup") } return nil } func printUsage() { fmt.Println("Usage: migrate [args]") fmt.Println("") fmt.Println("Commands:") fmt.Println(" up Apply all pending migrations") fmt.Println(" down Rollback all migrations") fmt.Println(" force Force version to v (fix dirty state)") fmt.Println(" version Show current migration version") fmt.Println(" steps Apply n steps (positive=up, negative=down)") fmt.Println("") fmt.Println("Environment: Uses database config from Viper (config.yaml)") }