package common import ( "context" "database/sql" "embed" "errors" "fmt" "log/slog" "path" "regexp" "strconv" "strings" "github.com/nyttikord/avl" ) var regexpMigration = regexp.MustCompile(`(\d{3})-(.*)\.sql`) type migrationData struct { id uint64 name string content string } func Migrate(ctx context.Context, log *slog.Logger, db *sql.DB, migrations embed.FS, dir string) error { entries, err := migrations.ReadDir(dir) if err != nil { return err } tree := avl.NewKeySimple[uint64, migrationData]() for _, entry := range entries { if strings.HasSuffix(entry.Name(), ".sql") { log.Debug("reading migration...", "path", dir+"/"+entry.Name()) subs := regexpMigration.FindStringSubmatch(entry.Name()) if len(subs) < 3 { return fmt.Errorf("invalid migration name %s", entry.Name()) } id, _ := strconv.ParseUint(subs[1], 10, 16) b, err := migrations.ReadFile(path.Join(dir, entry.Name())) if err != nil { return err } tree.Insert(id, migrationData{id, subs[2], string(b)}) } else { log.Warn("invalid migration entry, skipping", "path", dir+"/"+entry.Name()) } } for _, mig := range tree.Sort() { log.Debug("migrating...", "id", mig.id, "name", mig.name) _, err := db.ExecContext(ctx, mig.content) if err != nil { log.Error("migrating", "id", mig.id, "name", mig.name) return err } } return nil } type Loadable interface { Load(context.Context, *sql.DB, *sql.Rows) error } func GetValues[T Loadable](ctx context.Context, db *sql.DB, table, data, where string, args ...any) ([]T, error) { arr := make([]T, 0) rows, err := db.QueryContext( ctx, fmt.Sprintf(`SELECT %s FROM %s WHERE %s`, data, table, where), args..., ) if err != nil { if !errors.Is(err, sql.ErrNoRows) { return arr, err } } else { defer rows.Close() for rows.Next() { var v T err = v.Load(ctx, db, rows) if err != nil { return nil, err } arr = append(arr, v) } } return arr, nil }