go/mirko: add SQL migrations machinery
This uses github.com/golang-migrate/migrate and adds a Source that
allows using go_embed data files.
We also provide a test/example.
Change-Id: Icd2b6c7f7d0f728073b3fdf39b432b33ce61a3cd
diff --git a/go/mirko/sql_migrations.go b/go/mirko/sql_migrations.go
new file mode 100644
index 0000000..61c26f2
--- /dev/null
+++ b/go/mirko/sql_migrations.go
@@ -0,0 +1,172 @@
+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
+}