package main import ( "context" "database/sql" "log/slog" "os" "os/signal" "path/filepath" "sort" "strings" "syscall" _ "github.com/jackc/pgx/v5/stdlib" ) func main() { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) databaseURL := os.Getenv("DATABASE_URL") if databaseURL == "" { logger.Error("DATABASE_URL is required") os.Exit(1) } directory := os.Getenv("MIGRATIONS_DIR") if directory == "" { directory = "migrations" } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() db, err := sql.Open("pgx", databaseURL) if err != nil { logger.Error("database open failed", "error", err) os.Exit(1) } defer db.Close() if err := migrate(ctx, db, directory, logger); err != nil { logger.Error("migration failed", "error", err) os.Exit(1) } } func migrate(ctx context.Context, db *sql.DB, directory string, logger *slog.Logger) error { entries, err := os.ReadDir(directory) if err != nil { return err } var names []string for _, entry := range entries { if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".sql") { names = append(names, entry.Name()) } } sort.Strings(names) if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS stow_schema_migrations (name TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now())`); err != nil { return err } for _, name := range names { var applied bool if err := db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM stow_schema_migrations WHERE name=$1)`, name).Scan(&applied); err != nil { return err } if applied { continue } content, err := os.ReadFile(filepath.Join(directory, name)) if err != nil { return err } tx, err := db.BeginTx(ctx, nil) if err != nil { return err } if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(735160)`); err != nil { tx.Rollback() return err } if _, err := tx.ExecContext(ctx, string(content)); err != nil { tx.Rollback() return err } if _, err := tx.ExecContext(ctx, `INSERT INTO stow_schema_migrations (name) VALUES ($1)`, name); err != nil { tx.Rollback() return err } if err := tx.Commit(); err != nil { return err } logger.Info("applied migration", "name", name) } return nil }