package postgres import ( "context" "database/sql" "errors" "fmt" "io/fs" "strings" "sync" "github.com/pressly/goose/v3" ) // gooseMu serialises access to goose's package-level filesystem state so // concurrent calls to RunMigrations from independent services in the same // process do not race on goose.SetBaseFS. var gooseMu sync.Mutex // RunMigrations applies every pending Up migration found under dir inside fsys // against db. The PostgreSQL dialect is forced; goose's package-level base FS // is restored to the OS filesystem on the way out so a second caller in the // same process is safe. // // dir is the path within fsys (use "." when the migration files sit at the // embed root). The function does not handle Down migrations or partial // targets — services apply the full forward sequence at startup. func RunMigrations(ctx context.Context, db *sql.DB, fsys fs.FS, dir string) error { if db == nil { return errors.New("run migrations: nil db") } if fsys == nil { return errors.New("run migrations: nil fs") } if strings.TrimSpace(dir) == "" { return errors.New("run migrations: dir must not be empty") } gooseMu.Lock() defer gooseMu.Unlock() goose.SetBaseFS(fsys) defer goose.SetBaseFS(nil) if err := goose.SetDialect("postgres"); err != nil { return fmt.Errorf("run migrations: set dialect: %w", err) } if err := goose.UpContext(ctx, db, dir); err != nil { return fmt.Errorf("run migrations: %w", err) } return nil }