blob: 61c26f2c85e63810326b748ca2e018a9f8002e97 [file] [log] [blame]
Sergiusz Bazanski30317b42019-08-01 16:50:41 +02001package mirko
2
3// Migration support via github.com/golang-migrations/migrate for go_embed data in Bazel.
4// For example usage, see go/mirko/tests/sql.
5
6import (
7 "bytes"
8 "fmt"
9 "io"
10 "io/ioutil"
11 "os"
12 "strconv"
13 "strings"
14
15 "github.com/golang-migrate/migrate/v4/source"
16)
17
18func NewMigrationsFromBazel(data map[string][]byte) (source.Driver, error) {
19 migrations := make(map[uint]*migration)
20
21 for k, v := range data {
22 parts := strings.Split(k, ".")
23 errInvalid := fmt.Errorf("invalid migration filename: %q", k)
24
25 if len(parts) != 3 {
26 return nil, errInvalid
27 }
28 if parts[2] != "sql" {
29 return nil, errInvalid
30 }
31 if parts[1] != "up" && parts[1] != "down" {
32 return nil, errInvalid
33 }
34 direction := parts[1]
35
36 nameParts := strings.SplitN(parts[0], "_", 2)
37 if len(nameParts) != 2 {
38 return nil, errInvalid
39 }
40
41 name := nameParts[1]
42
43 version32, err := strconv.ParseUint(nameParts[0], 10, 32)
44 if err != nil {
45 return nil, errInvalid
46 }
47 version := uint(version32)
48
49 m, ok := migrations[version]
50 if !ok {
51 migrations[version] = &migration{
52 version: version,
53 name: name,
54 }
55 m = migrations[version]
56 } else {
57 if m.name != name {
58 if err != nil {
59 return nil, fmt.Errorf("migration version %d exists under diffrent names (%q vs %q)", version, name, m.name)
60 }
61 }
62 }
63
64 if direction == "up" {
65 m.up = v
66 } else {
67 m.down = v
68 }
69 }
70
71 var first uint
72 for version, migration := range migrations {
73 if migration.up == nil {
74 return nil, fmt.Errorf("migration version %d has no up file", version)
75 }
76 if migration.down == nil {
77 return nil, fmt.Errorf("migration version %d has no down file", version)
78 }
79 if first == 0 {
80 first = version
81 }
82 if version < first {
83 first = version
84 }
85 }
86
87 if first == 0 {
88 return nil, fmt.Errorf("no migrations, or lowest migration version is 0")
89 }
90
91 return &migrationSource{
92 migrations: migrations,
93 first: first,
94 }, nil
95}
96
97type migrationSource struct {
98 migrations map[uint]*migration
99 first uint
100}
101
102type migration struct {
103 version uint
104 name string
105 up []byte
106 down []byte
107}
108
109func (s *migrationSource) Open(url string) (source.Driver, error) {
110 if url != "" {
111 return nil, fmt.Errorf("bazel migration source is not configure via an URL")
112 }
113 return s, nil
114}
115
116func (s *migrationSource) Close() error {
117 return nil
118}
119
120func (s *migrationSource) First() (uint, error) {
121 return s.first, nil
122}
123
124func (s *migrationSource) Prev(version uint) (uint, error) {
125 var prev uint
126 for ver, _ := range s.migrations {
127 if ver > prev && ver < version {
128 prev = ver
129 }
130 }
131 if prev == 0 {
132 return 0, os.ErrNotExist
133 }
134 return prev, nil
135}
136
137func (s *migrationSource) Next(version uint) (uint, error) {
138 var next uint
139 for ver, _ := range s.migrations {
140 if ver <= version {
141 continue
142 }
143 if next == 0 {
144 next = ver
145 }
146 if ver < next {
147 next = ver
148 }
149 }
150 if next <= version {
151 return 0, os.ErrNotExist
152 }
153 return next, nil
154}
155
156func (s *migrationSource) ReadUp(version uint) (io.ReadCloser, string, error) {
157 m, ok := s.migrations[version]
158 if !ok {
159 return nil, "", os.ErrNotExist
160 }
161
162 return ioutil.NopCloser(bytes.NewReader(m.up)), m.name, nil
163}
164
165func (s *migrationSource) ReadDown(version uint) (io.ReadCloser, string, error) {
166 m, ok := s.migrations[version]
167 if !ok {
168 return nil, "", os.ErrNotExist
169 }
170
171 return ioutil.NopCloser(bytes.NewReader(m.down)), m.name, nil
172}