This repository has been archived by the owner on Mar 12, 2024. It is now read-only.
forked from pressly/goose
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
345 lines (312 loc) · 9.78 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
package main
import (
"errors"
"flag"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"text/tabwriter"
"text/template"
"github.com/pressly/goose/v3"
"github.com/pressly/goose/v3/internal/cfg"
"github.com/pressly/goose/v3/internal/migrationstats"
"github.com/pressly/goose/v3/internal/migrationstats/migrationstatsos"
)
var (
flags = flag.NewFlagSet("goose", flag.ExitOnError)
dir = flags.String("dir", cfg.DefaultMigrationDir, "directory with migration files")
table = flags.String("table", "goose_db_version", "migrations table name")
verbose = flags.Bool("v", false, "enable verbose mode")
help = flags.Bool("h", false, "print help")
versionFlag = flags.Bool("version", false, "print version")
certfile = flags.String("certfile", "", "file path to root CA's certificates in pem format (only support on mysql)")
sequential = flags.Bool("s", false, "use sequential numbering for new migrations")
allowMissing = flags.Bool("allow-missing", false, "applies missing (out-of-order) migrations")
sslcert = flags.String("ssl-cert", "", "file path to SSL certificates in pem format (only support on mysql)")
sslkey = flags.String("ssl-key", "", "file path to SSL key in pem format (only support on mysql)")
noVersioning = flags.Bool("no-versioning", false, "apply migration commands with no versioning, in file order, from directory pointed to")
noColor = flags.Bool("no-color", false, "disable color output (NO_COLOR env variable supported)")
)
var (
version = "(devel)"
)
func main() {
flags.Usage = usage
if err := flags.Parse(os.Args[1:]); err != nil {
log.Fatalf("failed to parse args: %v", err)
return
}
if *versionFlag {
fmt.Printf("goose version: %s\n", version)
return
}
if *verbose {
goose.SetVerbose(true)
}
if *sequential {
goose.SetSequential(true)
}
goose.SetTableName(*table)
args := flags.Args()
if *help {
flags.Usage()
return
}
if len(args) == 0 {
flags.Usage()
os.Exit(1)
}
// The -dir option has not been set, check whether the env variable is set
// before defaulting to ".".
if *dir == cfg.DefaultMigrationDir && cfg.GOOSEMIGRATIONDIR != "" {
*dir = cfg.GOOSEMIGRATIONDIR
}
switch args[0] {
case "init":
if err := gooseInit(*dir); err != nil {
log.Fatalf("goose run: %v", err)
}
return
case "create":
if err := goose.Run("create", nil, *dir, args[1:]...); err != nil {
log.Fatalf("goose run: %v", err)
}
return
case "fix":
if err := goose.Run("fix", nil, *dir); err != nil {
log.Fatalf("goose run: %v", err)
}
return
case "env":
for _, env := range cfg.List() {
fmt.Printf("%s=%q\n", env.Name, env.Value)
}
return
case "validate":
if err := printValidate(*dir, *verbose); err != nil {
log.Fatalf("goose validate: %v", err)
}
return
}
args = mergeArgs(args)
if len(args) < 3 {
flags.Usage()
os.Exit(1)
}
driver, dbstring, command := args[0], args[1], args[2]
// To avoid breaking existing consumers. An implementation detail
// that consumers should not care which underlying driver is used.
switch driver {
case "sqlite3":
// Internally uses the CGo-free port of SQLite: modernc.org/sqlite
driver = "sqlite"
case "postgres":
driver = "pgx"
}
db, err := goose.OpenDBWithDriver(driver, normalizeDBString(driver, dbstring, *certfile, *sslcert, *sslkey))
if err != nil {
log.Fatalf("-dbstring=%q: %v\n", dbstring, err)
}
defer func() {
if err := db.Close(); err != nil {
log.Fatalf("goose: failed to close DB: %v\n", err)
}
}()
arguments := []string{}
if len(args) > 3 {
arguments = append(arguments, args[3:]...)
}
options := []goose.OptionsFunc{}
if *noColor || checkNoColorFromEnv() {
options = append(options, goose.WithNoColor(true))
}
if *allowMissing {
options = append(options, goose.WithAllowMissing())
}
if *noVersioning {
options = append(options, goose.WithNoVersioning())
}
if err := goose.RunWithOptions(
command,
db,
*dir,
arguments,
options...,
); err != nil {
log.Fatalf("goose run: %v", err)
}
}
func checkNoColorFromEnv() bool {
ok, _ := strconv.ParseBool(cfg.GOOSENOCOLOR)
return ok
}
func mergeArgs(args []string) []string {
if len(args) < 1 {
return args
}
if s := cfg.GOOSEDRIVER; s != "" {
args = append([]string{s}, args...)
}
if s := cfg.GOOSEDBSTRING; s != "" {
args = append([]string{args[0], s}, args[1:]...)
}
return args
}
func usage() {
fmt.Println(usagePrefix)
flags.PrintDefaults()
fmt.Println(usageCommands)
}
var (
usagePrefix = `Usage: goose [OPTIONS] DRIVER DBSTRING COMMAND
or
Set environment key
GOOSE_DRIVER=DRIVER
GOOSE_DBSTRING=DBSTRING
Usage: goose [OPTIONS] COMMAND
Drivers:
postgres
mysql
sqlite3
mssql
redshift
tidb
clickhouse
vertica
Examples:
goose sqlite3 ./foo.db status
goose sqlite3 ./foo.db create init sql
goose sqlite3 ./foo.db create add_some_column sql
goose sqlite3 ./foo.db create fetch_user_data go
goose sqlite3 ./foo.db up
goose postgres "user=postgres dbname=postgres sslmode=disable" status
goose mysql "user:password@/dbname?parseTime=true" status
goose redshift "postgres://user:[email protected]:5439/db" status
goose tidb "user:password@/dbname?parseTime=true" status
goose mssql "sqlserver://user:password@dbname:1433?database=master" status
goose clickhouse "tcp://127.0.0.1:9000" status
goose vertica "vertica://user:password@localhost:5433/dbname?connection_load_balance=1" status
GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose status
GOOSE_DRIVER=sqlite3 GOOSE_DBSTRING=./foo.db goose create init sql
GOOSE_DRIVER=postgres GOOSE_DBSTRING="user=postgres dbname=postgres sslmode=disable" goose status
GOOSE_DRIVER=mysql GOOSE_DBSTRING="user:password@/dbname" goose status
GOOSE_DRIVER=redshift GOOSE_DBSTRING="postgres://user:[email protected]:5439/db" goose status
Options:
`
usageCommands = `
Commands:
up Migrate the DB to the most recent version available
up-by-one Migrate the DB up by 1
up-to VERSION Migrate the DB to a specific VERSION
down Roll back the version by 1
down-to VERSION Roll back to a specific VERSION
redo Re-run the latest migration
reset Roll back all migrations
status Dump the migration status for the current DB
version Print the current version of the database
create NAME [sql|go] Creates new migration file with the current timestamp
fix Apply sequential ordering to migrations
validate Check migration files without running them
`
)
var sqlMigrationTemplate = template.Must(template.New("goose.sql-migration").Parse(`-- Thank you for giving goose a try!
--
-- This file was automatically created running goose init. If you're familiar with goose
-- feel free to remove/rename this file, write some SQL and goose up. Briefly,
--
-- Documentation can be found here: https://pressly.github.io/goose
--
-- A single goose .sql file holds both Up and Down migrations.
--
-- All goose .sql files are expected to have a -- +goose Up annotation.
-- The -- +goose Down annotation is optional, but recommended, and must come after the Up annotation.
--
-- The -- +goose NO TRANSACTION annotation may be added to the top of the file to run statements
-- outside a transaction. Both Up and Down migrations within this file will be run without a transaction.
--
-- More complex statements that have semicolons within them must be annotated with
-- the -- +goose StatementBegin and -- +goose StatementEnd annotations to be properly recognized.
--
-- Use GitHub issues for reporting bugs and requesting features, enjoy!
-- +goose Up
SELECT 'up SQL query';
-- +goose Down
SELECT 'down SQL query';
`))
// initDir will create a directory with an empty SQL migration file.
func gooseInit(dir string) error {
if dir == "" || dir == cfg.DefaultMigrationDir {
dir = "migrations"
}
_, err := os.Stat(dir)
switch {
case errors.Is(err, fs.ErrNotExist):
case err == nil, errors.Is(err, fs.ErrExist):
return fmt.Errorf("directory already exists: %s", dir)
default:
return err
}
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
return goose.CreateWithTemplate(nil, dir, sqlMigrationTemplate, "initial", "sql")
}
func gatherFilenames(filename string) ([]string, error) {
stat, err := os.Stat(filename)
if err != nil {
return nil, err
}
var filenames []string
if stat.IsDir() {
for _, pattern := range []string{"*.sql", "*.go"} {
file, err := filepath.Glob(filepath.Join(filename, pattern))
if err != nil {
return nil, err
}
filenames = append(filenames, file...)
}
} else {
filenames = append(filenames, filename)
}
sort.Strings(filenames)
return filenames, nil
}
func printValidate(filename string, verbose bool) error {
filenames, err := gatherFilenames(filename)
if err != nil {
return err
}
fileWalker := migrationstatsos.NewFileWalker(filenames...)
stats, err := migrationstats.GatherStats(fileWalker, false)
if err != nil {
return err
}
// TODO(mf): we should introduce a --debug flag, which allows printing
// more internal debug information and leave verbose for additional information.
if !verbose {
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent)
fmtPattern := "%v\t%v\t%v\t%v\t%v\t\n"
fmt.Fprintf(w, fmtPattern, "Type", "Txn", "Up", "Down", "Name")
fmt.Fprintf(w, fmtPattern, "────", "───", "──", "────", "────")
for _, m := range stats {
txnStr := "✔"
if !m.Tx {
txnStr = "✘"
}
fmt.Fprintf(w, fmtPattern,
strings.TrimPrefix(filepath.Ext(m.FileName), "."),
txnStr,
m.UpCount,
m.DownCount,
filepath.Base(m.FileName),
)
}
return w.Flush()
}