package db import ( "context" "fmt" "path/filepath" "sort" "strings" "github.com/jackc/pgx/v5/pgxpool" ) type MigrationResult struct { Applied []string Skipped []string } func ApplyMigrations(ctx context.Context, pool *pgxpool.Pool, files map[string]string) (MigrationResult, error) { if _, err := pool.Exec(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (version text PRIMARY KEY, applied_at timestamptz NOT NULL DEFAULT now())`); err != nil { return MigrationResult{}, err } versions := make([]string, 0, len(files)) for v := range files { versions = append(versions, v) } sort.Strings(versions) res := MigrationResult{} for _, version := range versions { var exists bool if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM schema_migrations WHERE version=$1)`, version).Scan(&exists); err != nil { return res, err } if exists { res.Skipped = append(res.Skipped, version) continue } tx, err := pool.Begin(ctx) if err != nil { return res, err } if _, err = tx.Exec(ctx, files[version]); err != nil { _ = tx.Rollback(ctx) return res, fmt.Errorf("apply migration %s: %w", version, err) } if _, err = tx.Exec(ctx, `INSERT INTO schema_migrations(version) VALUES($1)`, version); err != nil { _ = tx.Rollback(ctx) return res, err } if err = tx.Commit(ctx); err != nil { return res, err } res.Applied = append(res.Applied, version) } return res, nil } func LoadMigrationFiles(paths []string, read func(string) ([]byte, error)) (map[string]string, error) { out := map[string]string{} for _, p := range paths { b, err := read(p) if err != nil { return nil, err } name := filepath.Base(p) if !strings.HasSuffix(name, ".sql") { continue } out[name] = string(b) } return out, nil }