-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
400 lines (322 loc) · 8.77 KB
/
main.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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
package main
import (
"bufio"
"database/sql"
"fmt"
"github.com/Ravior/go-fast-migrate/util"
"github.com/golang-module/carbon"
"io/ioutil"
"os"
"regexp"
"strconv"
"strings"
)
var (
createMigrationSql = "CREATE TABLE IF NOT EXISTS migrations (" +
"id int(10) UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY," +
"migration varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL," +
"batch int(11) NOT NULL " +
") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"
updateMigrationSql = "INSERT INTO migrations (migration, batch) VALUES DummyString;"
queryAllMigrationSql = "SELECT * FROM migrations;"
queryLastMigrationSql = "SELECT batch FROM migrations ORDER BY batch DESC;"
migrationPath = "./migrations/" // MigrationInte files save path
migrationTemplate = "./template/migrate.template"
conn = util.ConfigHelper.GetString("db.migration.db", "default") // 迁移对应的数据库链接Key
)
func init() {
initMigration()
}
func initMigration() {
util.FileHelper.CreateDir(migrationPath)
// CreateMigration migrations table if not exist
_, err := util.DbHelper.Exec(createMigrationSql, conn)
util.SysHelper.CheckErr(err)
}
func checkArgs() {
if len(os.Args) < 2 {
util.SysHelper.Exit("Please Set Comamnd")
}
}
func main() {
util.DbHelper.GetConn("default")
checkArgs()
command := os.Args[1]
if strings.Compare(command, "new") == 0 {
fileName := ""
if len(os.Args) > 2 {
fileName = os.Args[2]
}
if len(fileName) <= 0 {
util.SysHelper.Exit("Please enter a migration file name")
}
util.LogHelper.Info("Try to create migration file: %s", fileName)
path, err := CreateMigration(fileName)
if err != nil {
util.SysHelper.Exit("创建迁移文件失败,错误信息:%v", err)
}
util.LogHelper.Info("CreateTable migration success! Path: %s", path)
} else if strings.Compare(command, "start") == 0 {
err := Migrate()
if err != nil {
util.SysHelper.Exit("执行迁移任务失败,错误信息:%v", err)
}
util.LogHelper.Info("执行迁移任务成功")
} else if strings.Compare(command, "refresh") == 0 {
_, err := Refresh()
if err != nil {
util.SysHelper.Exit("执行刷新任务失败,错误信息:%v", err)
}
} else if strings.Compare(command, "rollback") == 0 {
var step string
if len(os.Args) < 3 {
step = "1"
} else {
step = os.Args[2]
}
err := Rollback(step)
if err != nil {
util.SysHelper.Exit("执行迁移任务失败,错误信息:%v", err)
}
} else {
util.SysHelper.Exit("Command not support: %v", command)
}
}
// 创建新的迁移文件
func CreateMigration(name string) (string, error) {
timestamp := carbon.Now().Format("Y_m_d_His")
filename := fmt.Sprintf("%s_%s", timestamp, name)
filePath := fmt.Sprintf("%s%s.go", migrationPath, filename)
migrateFile, err := os.Create(filePath)
if err != nil {
return "", err
}
defer migrateFile.Close()
tmpfile, err := util.FileHelper.ReadFile(migrationTemplate)
if err != nil {
return "", err
}
// 迁移模板文件
templateStr := string(tmpfile)
nameArr := strings.Split(name, "_")
newNameArr := make([]string, len(nameArr))
for _, key := range nameArr {
newNameArr = append(newNameArr, util.StrHelper.Ucfirst(key))
}
newName := strings.Join(newNameArr, "") + timestamp
migrateFileWriter := bufio.NewWriter(migrateFile)
// 替换模板中占位字符串
rs := strings.Replace(templateStr, "DummyString", newName, -1)
_, err = migrateFileWriter.WriteString(rs)
if err != nil {
return "", err
}
_ = migrateFileWriter.Flush()
return filePath, nil
}
// 执行迁移
func Migrate() error {
var (
fSlices []string // 迁移目录下的文件列表
batch int
lastBatch int
hasMigratedFiles []string // 已经执行过迁移的文件
toMigrateFiles []string // 等待执行迁移的文件
insertStr string
symbol string
)
// List migrations files
files, err := ioutil.ReadDir(migrationPath)
if err != nil {
return err
}
for _, f := range files {
filename := strings.Replace(f.Name(), ".go", "", -1)
match, err := regexp.MatchString("\\d{4}_\\d{2}_\\d{2}_(\\w)+", filename)
if err != nil {
util.LogHelper.Warn("UnKown Migrate File: %s, Error: %v", filename, err)
continue
}
if match {
fSlices = append(fSlices, filename)
}
}
// Check migration version in database
rows, err := util.DbHelper.Query(queryAllMigrationSql, conn)
if err != nil {
return err
}
lastRow := util.DbHelper.QueryRow(queryLastMigrationSql, conn)
_ = lastRow.Scan(&lastBatch)
// 计算本次执行的批次号
batch = lastBatch + 1
defer rows.Close()
if lastBatch == 0 {
// No migration record in database, all migrations should to be Migrate
toMigrateFiles = fSlices
} else {
// Get migrated files' name
for rows.Next() {
// Row to Migration Struct
m, err := scanRow(rows)
if err != nil {
return err
}
hasMigratedFiles = append(hasMigratedFiles, m.Migration)
}
// Compare and get which migration not migrated yet
for _, v := range fSlices {
if !util.ArrHelper.StrArrContain(hasMigratedFiles, v) {
toMigrateFiles = append(toMigrateFiles, v)
}
}
}
// Nothing to Migrate, stop and log fatal
toMigrateLen := len(toMigrateFiles)
if toMigrateLen == 0 {
util.SysHelper.Exit("暂无可执行迁移计划")
}
// Migrate
for i, name := range toMigrateFiles {
_, err := util.ToolHelper.RunGoCmd(migrationPath + name + ".go", "up")
if err != nil {
util.SysHelper.Exit("执行迁移失败", err)
}
// Calculate the batch number, which is need to Migrate
if i+1 == toMigrateLen {
symbol = ""
} else {
symbol = ","
}
insertStr += "('" + name + "', " + strconv.Itoa(batch) + ")" + symbol
}
// Connect sql update statement
updateMigrationSql = strings.Replace(updateMigrationSql, "DummyString", insertStr, -1)
_, err = util.DbHelper.Exec(updateMigrationSql, conn)
if err != nil {
return err
}
return nil
}
func Rollback(step string) error {
var (
lastBatch int
toBatch int
err error
rows *sql.Rows
m *Migration
rollBackMig []string
)
lastRow := util.DbHelper.QueryRow(queryLastMigrationSql, conn)
_ = lastRow.Scan(&lastBatch)
if i, err := strconv.Atoi(step); err == nil {
if lastBatch >= i {
toBatch = lastBatch - (i - 1)
} else {
util.LogHelper.Error("Nothing to rollback")
return err
}
}
// Which migrations need to be Rollback
rows, err = util.DbHelper.Query("SELECT * FROM migrations WHERE `batch`>="+strconv.Itoa(toBatch), conn)
if err != nil {
return err
}
for rows.Next() {
m, err = scanRow(rows)
if err != nil {
return err
}
rollBackMig = append(rollBackMig, m.Migration)
}
for _, name := range rollBackMig {
_, err := util.ToolHelper.RunGoCmd(migrationPath + name + ".go", "down")
if err != nil {
util.SysHelper.Exit("执行回滚失败", err)
}
}
// Delete migrations record
_, err = util.DbHelper.Exec("DELETE FROM migrations WHERE `batch`>=" + strconv.Itoa(toBatch))
if err != nil {
return err
}
return nil
}
// Refresh migration: Rollback all and re-Migrate
func Refresh() (bool, error) {
var (
insertStr string
symbol string
err error
rows *sql.Rows
rollBackMig []string
m *Migration
)
rows, err = util.DbHelper.Query("SELECT * FROM migrations;", conn)
if err != nil {
return false, err
}
for rows.Next() {
m, err = scanRow(rows)
if err != nil {
return false, err
}
rollBackMig = append(rollBackMig, m.Migration)
}
// Rollback and re-Migrate
fileLen := len(rollBackMig)
if fileLen > 0 {
for i, name := range rollBackMig {
// down
_, err := util.ToolHelper.RunGoCmd(migrationPath + name + ".go", "down")
if err != nil {
util.SysHelper.Exit("执行回滚失败", err)
}
// up
_, err = util.ToolHelper.RunGoCmd(migrationPath + name + ".go", "up")
if err != nil {
util.SysHelper.Exit("执行迁移失败", err)
}
if i == fileLen-1 {
symbol = ""
} else {
symbol = ","
}
insertStr += "('" + name + "', 1)" + symbol
}
// Update migrations table
_, _ = util.DbHelper.Exec("TRUNCATE migrations;", conn)
_, err = util.DbHelper.Exec(strings.Replace(updateMigrationSql, "DummyString", insertStr, -1), conn)
if err != nil {
return false, err
}
return true, nil
} else {
return false, nil
}
}
type Migration struct {
ID int64
Migration string
Batch int64
}
type rowScanner interface {
Scan(dst ...interface{}) error
}
// Map sql row to struct
func scanRow(s rowScanner) (*Migration, error) {
var (
id int64
migration sql.NullString
batch int64
)
if err := s.Scan(&id, &migration, &batch); err != nil {
return nil, err
}
mig := &Migration{
ID: id,
Migration: migration.String,
Batch: batch,
}
return mig, nil
}