forked from gsamokovarov/gloat
-
Notifications
You must be signed in to change notification settings - Fork 0
/
migration.go
257 lines (213 loc) · 6.75 KB
/
migration.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
package gloat
import (
"errors"
"fmt"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
var (
ErrNotFound = errors.New("version not found")
nameNormalizerRe = regexp.MustCompile(`([a-z])([A-Z])`)
versionFormat = "20060102150405"
)
// Migration holds all the relevant information for a migration. The content of
// the UP side, the DOWN side, a path and version. The version is used to
// determine the order of which the migrations would be executed. The path is
// the name in a store.
type Migration struct {
UpSQL []byte
DownSQL []byte
Path string
Version int64
Options MigrationOptions
AppliedAt time.Time
}
// Reversible returns true if the migration DownSQL content is present. E.g. if
// both of the directions are present in the migration folder.
func (m *Migration) Reversible() bool {
return len(m.DownSQL) != 0
}
// Persistable is any migration with non blank Path.
func (m *Migration) Persistable() bool {
return m.Path != ""
}
// GenerateMigration generates a new blank migration with blank UP and DOWN
// content defined from user entered content.
func GenerateMigration(str string) *Migration {
version := generateVersion()
path := generateMigrationPath(version, str)
return &Migration{
Path: path,
Version: version,
Options: DefaultMigrationOptions(),
}
}
// MigrationFromBytes builds a Migration struct from a path and a
// function. Functions like ioutil.ReadFile, go-bindata's Asset have
// the very same signature, so you can use them here.
func MigrationFromBytes(path string, read func(string) ([]byte, error)) (*Migration, error) {
version, err := versionFromPath(path)
if err != nil {
return nil, err
}
upSQL, err := read(filepath.Join(path, "up.sql"))
if err != nil {
return nil, err
}
// This may be an error from the OS or the go-bindata generated Asset
// function ("Asset %s can't read by error: %v"). Just ignore it, as we can
// have embedded irreversible migrations.
downSQL, _ := read(filepath.Join(path, "down.sql"))
optionsJSON, err := read(filepath.Join(path, "options.json"))
if err != nil {
optionsJSON = nil
}
options, err := parseMigrationOptions(optionsJSON)
if err != nil {
return nil, err
}
return &Migration{
UpSQL: upSQL,
DownSQL: downSQL,
Path: path,
Version: version,
Options: options,
AppliedAt: time.Time{},
}, nil
}
func generateMigrationPath(version int64, str string) string {
name := strings.ToLower(nameNormalizerRe.ReplaceAllString(str, "${1}_${2}"))
return fmt.Sprintf("%d_%s", version, name)
}
func generateVersion() int64 {
version, _ := strconv.ParseInt(time.Now().UTC().Format(versionFormat), 10, 64)
return version
}
func versionFromPath(path string) (int64, error) {
parts := strings.SplitN(filepath.Base(path), "_", 2)
if len(parts) == 0 {
return 0, fmt.Errorf("cannot extract version from %s", path)
}
return strconv.ParseInt(parts[0], 10, 64)
}
// Migrations is a slice of Migration pointers.
type Migrations []*Migration
// Except selects migrations that does not exist in the current ones.
// NB: other should be full migrations (e.g. from source)
// the ones from store are only metadata migrations (they don't include SQL statements)
func (m Migrations) Except(other Migrations) (result Migrations) {
// Mark the current transactions.
current := make(map[int64]time.Time)
for _, migrationMetadata := range m {
current[migrationMetadata.Version] = migrationMetadata.AppliedAt
}
// Mark the ones in the migrations set, which we do have to get.
new := make(map[int64]time.Time)
for _, fullMigration := range other {
new[fullMigration.Version] = fullMigration.AppliedAt
}
for _, fullMigration := range other {
_, will := new[fullMigration.Version]
_, has := current[fullMigration.Version]
if will && !has {
result = append(result, fullMigration)
}
}
return
}
// Intersect selects migrations that does exist in the current ones.
// NB: other should be full migrations (e.g. from source)
// the ones from store are only metadata migrations (they don't include SQL statements)
func (m Migrations) Intersect(other Migrations) (result Migrations) {
// Mark the current transactions.
store := make(map[int64]time.Time)
for _, migrationMetadata := range m {
store[migrationMetadata.Version] = migrationMetadata.AppliedAt
}
// Mark the ones in the migrations set, which we do have to get.
source := make(map[int64]time.Time)
for _, fullMigration := range other {
source[fullMigration.Version] = fullMigration.AppliedAt
}
for _, fullMigration := range other {
_, will := source[fullMigration.Version]
appliedAt, has := store[fullMigration.Version]
if will && has {
fullMigration.AppliedAt = appliedAt
result = append(result, fullMigration)
}
}
return
}
// Implementation for the sort.Sort interface.
func (m Migrations) Len() int { return len(m) }
// Less will sort by AppliedAt. If equal will sort by Version
func (m Migrations) Less(i, j int) bool {
if m[i].AppliedAt.Before(m[j].AppliedAt) {
return true
}
if m[i].AppliedAt.After(m[j].AppliedAt) {
return false
}
return m[i].Version < m[j].Version
}
func (m Migrations) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
// Sort is a convenience sorting method.
func (m Migrations) Sort() { sort.Sort(m) }
// ReverseSort is a convenience sorting method.
func (m Migrations) ReverseSort() { sort.Sort(sort.Reverse(m)) }
// Current returns the latest applied migration. Can be nil, if the migrations
// are empty.
func (m Migrations) Current() *Migration {
m.Sort()
if len(m) == 0 {
return nil
}
return m[len(m)-1]
}
// AppliedAfter selects the applied migrations from a Store after a given version.
func AppliedAfter(store Source, source Source, version int64) (Migrations, error) {
var appliedAfter Migrations
appliedMigrations, err := store.Collect()
if err != nil {
return nil, err
}
found := false
for i := 0; i < len(appliedMigrations); i++ {
if appliedMigrations[i].Version == version {
found = true
break
}
appliedAfter = append(appliedAfter, appliedMigrations[i])
}
if !found {
return nil, ErrNotFound
}
appliedAfter.ReverseSort()
availableMigrations, err := source.Collect()
if err != nil {
return nil, err
}
intersect := appliedAfter.Intersect(availableMigrations)
intersect.ReverseSort()
return intersect, nil
}
// UnappliedMigrations selects the unapplied migrations from a Source. For a
// migration to be unapplied it should not be present in the Store.
func UnappliedMigrations(store, source Source) (Migrations, error) {
appliedMigrations, err := store.Collect()
if err != nil {
return nil, err
}
incomingMigrations, err := source.Collect()
if err != nil {
return nil, err
}
unappliedMigrations := appliedMigrations.Except(incomingMigrations)
unappliedMigrations.Sort()
return unappliedMigrations, nil
}