package postgres import ( "database/sql" "fmt" "os" "path/filepath" "sort" "strings" ) func RunMigrations(db *sql.DB, dir string) error { if db == nil { return fmt.Errorf("db is nil") } if dir == "" { return fmt.Errorf("migration dir is required") } entries, err := os.ReadDir(dir) if err != nil { return err } files := make([]string, 0, len(entries)) for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".up.sql") { continue } files = append(files, entry.Name()) } sort.Strings(files) if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS cs_schema_migrations (version VARCHAR(255) PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`); err != nil { return err } for _, name := range files { version := strings.TrimSuffix(name, ".up.sql") var exists bool if err := db.QueryRow(`SELECT EXISTS (SELECT 1 FROM cs_schema_migrations WHERE version = $1)`, version).Scan(&exists); err != nil { return err } if exists { continue } content, err := os.ReadFile(filepath.Join(dir, name)) if err != nil { return err } tx, err := db.Begin() if err != nil { return err } if _, err := tx.Exec(string(content)); err != nil { _ = tx.Rollback() return fmt.Errorf("apply migration %s: %w", name, err) } if _, err := tx.Exec(`INSERT INTO cs_schema_migrations(version) VALUES ($1)`, version); err != nil { _ = tx.Rollback() return err } if err := tx.Commit(); err != nil { return err } } return nil }