Files
kjol/go/cmd/migrate/source.go

152 lines
3.3 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
)
type direction string
const (
dirUp direction = "up"
dirDown direction = "down"
)
type migrationFile struct {
Version uint
Identifier string
Direction direction
Filename string // e.g. "0000001_INITIAL_CREATE.up.sql"
}
type migrationSource struct {
dir string
index []uint // sorted unique version numbers
migrations map[uint]map[direction]*migrationFile // version -> direction -> file
}
// matches: 0000001_INITIAL_CREATE.up.sql
var migrationRegex = regexp.MustCompile(`^([0-9]+)_(.*)\.(up|down)\.(.*)$`)
func newMigrationSource(dir string) (*migrationSource, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("failed to read migrations directory: %w", err)
}
s := &migrationSource{
dir: dir,
migrations: make(map[uint]map[direction]*migrationFile),
}
versionSet := make(map[uint]struct{})
for _, entry := range entries {
if entry.IsDir() {
continue
}
name := entry.Name()
m := migrationRegex.FindStringSubmatch(name)
if len(m) != 5 {
// skip non-matching filenames (same as library)
continue
}
versionUint64, err := strconv.ParseUint(m[1], 10, 64)
if err != nil {
continue
}
version := uint(versionUint64)
dir := direction(m[3])
mf := &migrationFile{
Version: version,
Identifier: m[2],
Direction: dir,
Filename: name,
}
if _, ok := s.migrations[version]; !ok {
s.migrations[version] = make(map[direction]*migrationFile)
}
if _, exists := s.migrations[version][dir]; exists {
return nil, fmt.Errorf("duplicate migration version %d direction %s", version, dir)
}
s.migrations[version][dir] = mf
versionSet[version] = struct{}{}
}
// build sorted index
s.index = make([]uint, 0, len(versionSet))
for v := range versionSet {
s.index = append(s.index, v)
}
sort.Slice(s.index, func(i, j int) bool {
return s.index[i] < s.index[j]
})
return s, nil
}
func (s *migrationSource) first() (uint, error) {
if len(s.index) == 0 {
return 0, os.ErrNotExist
}
return s.index[0], nil
}
func (s *migrationSource) next(version uint) (uint, error) {
pos := sort.Search(len(s.index), func(i int) bool {
return s.index[i] > version
})
if pos >= len(s.index) {
return 0, os.ErrNotExist
}
return s.index[pos], nil
}
func (s *migrationSource) prev(version uint) (uint, error) {
pos := sort.Search(len(s.index), func(i int) bool {
return s.index[i] >= version
})
// pos is the index of version (or where it would be inserted)
// we want the one before it
if pos <= 0 {
return 0, os.ErrNotExist
}
return s.index[pos-1], nil
}
func (s *migrationSource) readUp(version uint) (string, error) {
if dirs, ok := s.migrations[version]; ok {
if mf, ok := dirs[dirUp]; ok {
return filepath.Join(s.dir, mf.Filename), nil
}
}
return "", os.ErrNotExist
}
func (s *migrationSource) readDown(version uint) (string, error) {
if dirs, ok := s.migrations[version]; ok {
if mf, ok := dirs[dirDown]; ok {
return filepath.Join(s.dir, mf.Filename), nil
}
}
return "", os.ErrNotExist
}
func (s *migrationSource) versionExists(version uint) bool {
pos := sort.Search(len(s.index), func(i int) bool {
return s.index[i] >= version
})
return pos < len(s.index) && s.index[pos] == version
}