func (m *Migrator) apply(ctx context.Context, name, version string) error {
	content, err := fs.ReadFile(m.fs, name)
	if err != nil {
		return err
	}
	return m.inTransaction(ctx, func(tx *sql.Tx) error {
		if _, err := tx.ExecContext(ctx, `update `+m.table+` set version = '`+version+`'`); err != nil {
			return err
		}
		if _, err := tx.ExecContext(ctx, string(content)); err != nil {
			return err
		}
		return nil
	})
}
apply a file identified by name and update to version.
package migrate
import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"io/fs"
	"regexp"
)
var (
	upMatcher    = regexp.MustCompile(`^([\w-]+).up.sql$`)
	downMatcher  = regexp.MustCompile(`^([\w-]+).down.sql`)
	tableMatcher = regexp.MustCompile(`^[\w]+$`)
)
func Up(ctx context.Context, db *sql.DB, fsys fs.FS) error {
	m := New(Options{DB: db, FS: fsys})
	return m.MigrateUp(ctx)
}
func Down(ctx context.Context, db *sql.DB, fsys fs.FS) error {
	m := New(Options{DB: db, FS: fsys})
	return m.MigrateDown(ctx)
}
func To(ctx context.Context, db *sql.DB, fsys fs.FS, version string) error {
	m := New(Options{DB: db, FS: fsys})
	return m.MigrateTo(ctx, version)
}
type Migrator struct {
	db    *sql.DB
	fs    fs.FS
	table string
}
type Options struct {
	DB    *sql.DB
	FS    fs.FS
	Table string
}
func New(opts Options) *Migrator {
	if opts.DB == nil || opts.FS == nil {
		panic("DB and FS must be set")
	}
	if opts.Table == "" {
		opts.Table = "migrations"
	}
	if !tableMatcher.MatchString(opts.Table) {
		panic("illegal table name " + opts.Table + ", must match " + tableMatcher.String())
	}
	return &Migrator{
		db:    opts.DB,
		fs:    opts.FS,
		table: opts.Table,
	}
}
func (m *Migrator) MigrateUp(ctx context.Context) error {
	if err := m.createMigrationsTable(ctx); err != nil {
		return err
	}
	currentVersion, err := m.getCurrentVersion(ctx)
	if err != nil {
		return err
	}
	names, err := m.getFilenames(upMatcher)
	if err != nil {
		return err
	}
	for _, name := range names {
		thisVersion := upMatcher.ReplaceAllString(name, "$1")
		if thisVersion <= currentVersion {
			continue
		}
		if err := m.apply(ctx, name, thisVersion); err != nil {
			return err
		}
	}
	return nil
}
func (m *Migrator) MigrateDown(ctx context.Context) error {
	if err := m.createMigrationsTable(ctx); err != nil {
		return err
	}
	currentVersion, err := m.getCurrentVersion(ctx)
	if err != nil {
		return err
	}
	names, err := m.getFilenames(downMatcher)
	if err != nil {
		return err
	}
	for i := len(names) - 1; i >= 0; i-- {
		thisVersion := downMatcher.ReplaceAllString(names[i], "$1")
		if thisVersion > currentVersion {
			continue
		}
		nextVersion := ""
		if i > 0 {
			nextVersion = downMatcher.ReplaceAllString(names[i-1], "$1")
		}
		if err := m.apply(ctx, names[i], nextVersion); err != nil {
			return err
		}
	}
	return nil
}
func (m *Migrator) MigrateTo(ctx context.Context, version string) error {
	if version == "" {
		return m.MigrateDown(ctx)
	}
	if err := m.createMigrationsTable(ctx); err != nil {
		return err
	}
	currentVersion, err := m.getCurrentVersion(ctx)
	if err != nil {
		return err
	}
	if currentVersion == version {
		return nil
	}
	var matcher *regexp.Regexp
	if version > currentVersion {
		matcher = upMatcher
	} else {
		matcher = downMatcher
	}
	names, err := m.getFilenames(matcher)
	if err != nil {
		return err
	}
	foundVersion := false
	for _, name := range names {
		thisVersion := matcher.ReplaceAllString(name, "$1")
		if thisVersion == version {
			foundVersion = true
		}
	}
	if !foundVersion {
		return errors.New("error finding version " + version)
	}
	switch {
	case version > currentVersion:
		for _, name := range names {
			thisVersion := matcher.ReplaceAllString(name, "$1")
			if thisVersion <= currentVersion {
				continue
			}
			if thisVersion > version {
				break
			}
			if err := m.apply(ctx, name, thisVersion); err != nil {
				return err
			}
		}
	case version < currentVersion:
		for i := len(names) - 1; i >= 0; i-- {
			thisVersion := matcher.ReplaceAllString(names[i], "$1")
			if thisVersion > currentVersion {
				continue
			}
			if thisVersion <= version {
				break
			}
			nextVersion := matcher.ReplaceAllString(names[i-1], "$1")
			if err := m.apply(ctx, names[i], nextVersion); err != nil {
				return err
			}
		}
	}
	return nil
}