forked from pressly/goose
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
406 lines (369 loc) · 11.5 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
401
402
403
404
405
406
package main
import (
"context"
"database/sql"
"errors"
"flag"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"runtime/debug"
"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"
)
var (
flags = flag.NewFlagSet("goose", flag.ExitOnError)
dir = flags.String("dir", cfg.DefaultMigrationDir, "directory with migration files, (GOOSE_MIGRATION_DIR env variable supported)")
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)")
timeout = flags.Duration("timeout", 0, "maximum allowed duration for queries to run; e.g., 1h13m")
)
var version string
func main() {
ctx := context.Background()
flags.Usage = usage
if err := flags.Parse(os.Args[1:]); err != nil {
log.Fatalf("failed to parse args: %v", err)
return
}
if *versionFlag {
buildInfo, ok := debug.ReadBuildInfo()
if version == "" && ok && buildInfo != nil && buildInfo.Main.Version != "" {
version = buildInfo.Main.Version
}
fmt.Printf("goose version: %s\n", strings.TrimSpace(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.RunContext(ctx, "create", nil, *dir, args[1:]...); err != nil {
log.Fatalf("goose run: %v", err)
}
return
case "fix":
if err := goose.RunContext(ctx, "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
case "beta":
remain := args[1:]
if len(remain) == 0 {
log.Println("goose beta: missing subcommand")
os.Exit(1)
}
switch remain[0] {
case "drivers":
printDrivers()
}
return
}
args = mergeArgs(args)
if len(args) < 3 {
flags.Usage()
os.Exit(1)
}
driver, dbstring, command := args[0], args[1], args[2]
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 timeout != nil && *timeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, *timeout)
defer cancel()
}
if err := goose.RunWithOptionsContext(
ctx,
command,
db,
*dir,
arguments,
options...,
); err != nil {
log.Fatalf("goose run: %v", err)
}
}
func printDrivers() {
drivers := mergeDrivers(sql.Drivers())
if len(drivers) == 0 {
fmt.Println("No drivers found")
return
}
fmt.Println("Available drivers:")
for _, driver := range drivers {
fmt.Printf(" %s\n", driver)
}
}
// mergeDrivers merges drivers with a common prefix into a single line.
func mergeDrivers(drivers []string) []string {
driverMap := make(map[string][]string)
for _, driver := range drivers {
parts := strings.Split(driver, "/")
if len(parts) > 1 {
// Merge drivers with a common prefix "/"
prefix := parts[0]
driverMap[prefix] = append(driverMap[prefix], driver)
} else {
// Add drivers without a prefix directly
driverMap[driver] = append(driverMap[driver], driver)
}
}
var merged []string
for _, versions := range driverMap {
sort.Strings(versions)
merged = append(merged, strings.Join(versions, ", "))
}
sort.Strings(merged)
return merged
}
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
ydb
turso
oracle
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 ydb "grpcs://localhost:2135/local?go_query_mode=scripting&go_fake_tx=scripting&go_query_bind=declare,numeric" status
goose turso "libsql://dbname.turso.io?authToken=token" status
goose oracle "oracle://user:password@localhost:1521/SERVICE?SSL=false" 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
GOOSE_DRIVER=turso GOOSE_DBSTRING="libsql://dbname.turso.io?authToken=token" goose status
GOOSE_DRIVER=oracle GOOSE_DBSTRING="oracle://user:password@localhost:1521/SERVICE?SSL=false" 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
}
stats, err := migrationstats.GatherStats(
migrationstats.NewFileWalker(filenames...),
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()
}