package db import ( "context" "errors" "fmt" "io/fs" "path" "sort" "strings" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // ApplyPostgresMigrations runs ordered *.up.sql from an embedded subtree (idempotent via schema_migrations). func ApplyPostgresMigrations(ctx context.Context, pool *pgxpool.Pool, fsys fs.FS, subdir string) error { if _, err := pool.Exec(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (version TEXT NOT NULL PRIMARY KEY)`); err != nil { return fmt.Errorf("db: schema_migrations: %w", err) } entries, err := fs.ReadDir(fsys, subdir) if err != nil { return err } var ups []string for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".up.sql") { continue } ups = append(ups, e.Name()) } sort.Strings(ups) for _, name := range ups { ver := strings.TrimSuffix(name, ".up.sql") var dummy int err := pool.QueryRow(ctx, `SELECT 1 FROM schema_migrations WHERE version = $1`, ver).Scan(&dummy) if err == nil { continue } if !errors.Is(err, pgx.ErrNoRows) { return err } body, err := fs.ReadFile(fsys, path.Join(subdir, name)) if err != nil { return err } if _, err := pool.Exec(ctx, string(body)); err != nil { return fmt.Errorf("db: migrate %s: %w", name, err) } if _, err := pool.Exec(ctx, `INSERT INTO schema_migrations (version) VALUES ($1)`, ver); err != nil { return err } } return nil }