forked from getfider/fider
-
Notifications
You must be signed in to change notification settings - Fork 0
/
migrate.go
139 lines (117 loc) · 3.49 KB
/
migrate.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
package dbx
import (
"context"
"database/sql"
stdErrors "errors"
"io/ioutil"
"os"
"sort"
"strings"
"github.com/getfider/fider/app/models/dto"
"github.com/getfider/fider/app/pkg/env"
"github.com/getfider/fider/app/pkg/errors"
"github.com/getfider/fider/app/pkg/log"
)
// ErrNoChanges means that the migration process didn't change execute any file
var ErrNoChanges = stdErrors.New("nothing to migrate.")
// Migrate the database to latest version
func Migrate(ctx context.Context, path string) error {
log.Info(ctx, "Running migrations...")
dir, err := os.Open(env.Path(path))
if err != nil {
return errors.Wrap(err, "failed to open dir '%s'", path)
}
files, err := dir.Readdir(0)
if err != nil {
return errors.Wrap(err, "failed to read files from dir '%s'", path)
}
versions := make([]string, len(files))
versionFiles := make(map[string]string, len(files))
for i, file := range files {
fileName := file.Name()
parts := strings.Split(fileName, "_")
if len(parts[0]) != 12 {
return errors.New("migration file must have exactly 12 chars for version: '%s' is invalid.", fileName)
}
versions[i] = parts[0]
versionFiles[versions[i]] = fileName
}
sort.Strings(versions)
log.Infof(ctx, "Found total of @{Total} migration files.", dto.Props{
"Total": len(versions),
})
lastVersion, err := getLastMigration()
if err != nil {
return errors.Wrap(err, "failed to get last migration record")
}
log.Infof(ctx, "Current version is @{Version}", dto.Props{
"Version": lastVersion,
})
totalMigrationsExecuted := 0
// Apply all migrations
for _, version := range versions {
if version > lastVersion {
fileName := versionFiles[version]
log.Infof(ctx, "Running Version: @{Version} (@{FileName})", dto.Props{
"Version": version,
"FileName": fileName,
})
err := runMigration(ctx, version, path, fileName)
if err != nil {
return errors.Wrap(err, "failed to run migration '%s'", fileName)
}
totalMigrationsExecuted++
}
}
if totalMigrationsExecuted > 0 {
log.Infof(ctx, "@{Count} migrations have been applied.", dto.Props{
"Count": totalMigrationsExecuted,
})
} else {
log.Info(ctx, "Migrations are already up to date.")
}
return nil
}
func runMigration(ctx context.Context, version, path, fileName string) error {
filePath := env.Path(path + "/" + fileName)
content, err := ioutil.ReadFile(filePath)
if err != nil {
return errors.Wrap(err, "failed to read file '%s'", filePath)
}
trx, err := BeginTx(ctx)
if err != nil {
return err
}
_, err = trx.tx.Exec(string(content))
if err != nil {
return err
}
_, err = trx.Execute("INSERT INTO migrations_history (version, filename) VALUES ($1, $2)", version, fileName)
if err != nil {
return err
}
return trx.Commit()
}
func getLastMigration() (string, error) {
_, err := conn.Exec(`CREATE TABLE IF NOT EXISTS migrations_history (
version BIGINT PRIMARY KEY,
filename VARCHAR(100) null,
date TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`)
if err != nil {
return "", err
}
var lastVersion sql.NullString
row := conn.QueryRow("SELECT CAST(MAX(version) as varchar) FROM migrations_history LIMIT 1")
err = row.Scan(&lastVersion)
if err != nil {
return "", err
}
if !lastVersion.Valid {
// If it's the first run, maybe we have records on old migrations table, so try to get from it.
// This SHOULD be removed in the far future.
row := conn.QueryRow("SELECT CAST(version as varchar) FROM schema_migrations LIMIT 1")
_ = row.Scan(&lastVersion)
}
return lastVersion.String, nil
}