package mirko // Migration support via github.com/golang-migrations/migrate for go_embed data in Bazel. // For example usage, see go/mirko/tests/sql. import ( "bytes" "fmt" "io" "io/ioutil" "os" "strconv" "strings" "github.com/golang-migrate/migrate/v4/source" ) func NewMigrationsFromBazel(data map[string][]byte) (source.Driver, error) { migrations := make(map[uint]*migration) for k, v := range data { parts := strings.Split(k, ".") errInvalid := fmt.Errorf("invalid migration filename: %q", k) if len(parts) != 3 { return nil, errInvalid } if parts[2] != "sql" { return nil, errInvalid } if parts[1] != "up" && parts[1] != "down" { return nil, errInvalid } direction := parts[1] nameParts := strings.SplitN(parts[0], "_", 2) if len(nameParts) != 2 { return nil, errInvalid } name := nameParts[1] version32, err := strconv.ParseUint(nameParts[0], 10, 32) if err != nil { return nil, errInvalid } version := uint(version32) m, ok := migrations[version] if !ok { migrations[version] = &migration{ version: version, name: name, } m = migrations[version] } else { if m.name != name { if err != nil { return nil, fmt.Errorf("migration version %d exists under diffrent names (%q vs %q)", version, name, m.name) } } } if direction == "up" { m.up = v } else { m.down = v } } var first uint for version, migration := range migrations { if migration.up == nil { return nil, fmt.Errorf("migration version %d has no up file", version) } if migration.down == nil { return nil, fmt.Errorf("migration version %d has no down file", version) } if first == 0 { first = version } if version < first { first = version } } if first == 0 { return nil, fmt.Errorf("no migrations, or lowest migration version is 0") } return &migrationSource{ migrations: migrations, first: first, }, nil } type migrationSource struct { migrations map[uint]*migration first uint } type migration struct { version uint name string up []byte down []byte } func (s *migrationSource) Open(url string) (source.Driver, error) { if url != "" { return nil, fmt.Errorf("bazel migration source is not configure via an URL") } return s, nil } func (s *migrationSource) Close() error { return nil } func (s *migrationSource) First() (uint, error) { return s.first, nil } func (s *migrationSource) Prev(version uint) (uint, error) { var prev uint for ver, _ := range s.migrations { if ver > prev && ver < version { prev = ver } } if prev == 0 { return 0, os.ErrNotExist } return prev, nil } func (s *migrationSource) Next(version uint) (uint, error) { var next uint for ver, _ := range s.migrations { if ver <= version { continue } if next == 0 { next = ver } if ver < next { next = ver } } if next <= version { return 0, os.ErrNotExist } return next, nil } func (s *migrationSource) ReadUp(version uint) (io.ReadCloser, string, error) { m, ok := s.migrations[version] if !ok { return nil, "", os.ErrNotExist } return ioutil.NopCloser(bytes.NewReader(m.up)), m.name, nil } func (s *migrationSource) ReadDown(version uint) (io.ReadCloser, string, error) { m, ok := s.migrations[version] if !ok { return nil, "", os.ErrNotExist } return ioutil.NopCloser(bytes.NewReader(m.down)), m.name, nil }