package store import ( "context" "embed" "sort" "strconv" "strings" ) //go:embed migrations/*.sql var migrationFiles embed.FS // Migrate applies any pending SQL migrations in order within a single transaction. func (s *Store) Migrate(ctx context.Context) error { conn, err := s.pool.Acquire(ctx) if err != nil { return err } defer conn.Release() tx, err := conn.Begin(ctx) if err != nil { return err } defer func() { if tx != nil { tx.Rollback(ctx) } }() _, err = tx.Exec(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations ( version INT PRIMARY KEY )`) if err != nil { return err } entries, err := migrationFiles.ReadDir("migrations") if err != nil { return err } type mig struct { ver int name string } var migs []mig for _, e := range entries { name := e.Name() if !strings.HasSuffix(name, ".sql") { continue } parts := strings.SplitN(name, "_", 2) v, err := strconv.Atoi(parts[0]) if err != nil { continue } migs = append(migs, mig{ver: v, name: name}) } sort.Slice(migs, func(i, j int) bool { return migs[i].ver < migs[j].ver }) for _, m := range migs { var exists bool err = tx.QueryRow(ctx, "SELECT EXISTS(SELECT 1 FROM schema_migrations WHERE version=$1)", m.ver).Scan(&exists) if err != nil { return err } if exists { continue } data, err := migrationFiles.ReadFile("migrations/" + m.name) if err != nil { return err } if _, err = tx.Exec(ctx, string(data)); err != nil { return err } if _, err = tx.Exec(ctx, "INSERT INTO schema_migrations(version) VALUES($1)", m.ver); err != nil { return err } } if err = tx.Commit(ctx); err != nil { return err } tx = nil return nil }