From 94bca373470c6fff0ba2d9bbc93e4dcdf1b8fadc Mon Sep 17 00:00:00 2001 From: bokwoon Date: Mon, 7 Aug 2023 11:50:47 +0800 Subject: [PATCH] expose a failed migration script's contents via MigrationError --- ddl/START_HERE.md | 4 ++++ ddl/migrate_cmd.go | 34 +++++++++++++++++++++++++++++++--- main.go | 4 ++++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/ddl/START_HERE.md b/ddl/START_HERE.md index 593d82c..009b3f5 100644 --- a/ddl/START_HERE.md +++ b/ddl/START_HERE.md @@ -72,6 +72,10 @@ There are tests that require a live database connection. They will only run if y $ go test ./ddl -tags=fts5 -postgres $POSTGRES_URL -mysql $MYSQL_URL -sqlserver $SQLSERVER_URL # -failfast -shuffle=on -coverprofile=coverage ``` +- For $POSTGRES\_URL, make sure sslmode=disable is in the query string e.g. `postgres://username:password@address:port/database?sslmode=disable`. + +- For $MYSQL\_URL, make sure multiStatements=true is in the query string e.g. `username:password@tcp(address:port)/database?multiStatements=true`. + If you have docker, you can use the docker-compose.yml (run `docker-compose up`) at the root of this project's directory to spin up Postgres, MySQL and SQL Server databases that are reachable at the following URLs: ```shell diff --git a/ddl/migrate_cmd.go b/ddl/migrate_cmd.go index 700e201..53276d4 100644 --- a/ddl/migrate_cmd.go +++ b/ddl/migrate_cmd.go @@ -334,6 +334,27 @@ func (m migration) rowmapper(row *sq.Row) migration { // https://www.reddit.com/r/golang/comments/ntyi7i/what_is_the_reason_go_chose_to_use_a_constant_as/h0w0tu7/ var rng = rand.New(rand.NewSource(int64(new(maphash.Hash).Sum64()))) +// MigrationError is returned by (*MigrateCmd).Run() if any errors were +// encountered when running a migration script (SQL syntax errors, violated +// constraints, etc). +type MigrationError struct { + Err error // Migration error. + Filename string // Migration filename. + Contents string // Contents of the migration script. + StartedAt time.Time // When the migration started at. + TimeTaken time.Duration // How long the migration took. +} + +// Error implements the error interface. +func (migrationErr *MigrationError) Error() string { + return migrationErr.Err.Error() +} + +// Unwrap returns the underlying error when running the migration. +func (migrationErr *MigrationError) Unwrap() error { + return migrationErr.Err +} + func (cmd *MigrateCmd) runWithRetry(conn *sql.Conn, queue []migration) error { isTx := len(queue) > 1 || (!strings.HasSuffix(queue[0].filename, ".txoff.sql") && cmd.Dialect != DialectMySQL) || strings.HasSuffix(queue[0].filename, ".tx.sql") isStmt := false @@ -472,12 +493,19 @@ func (cmd *MigrateCmd) run(conn *sql.Conn, migrations []migration) (stoppedAt in fmt.Fprintln(cmd.Stderr, timestamp()+"[START] "+m.filename) } m.startedAt = sql.NullTime{Time: time.Now(), Valid: true} - _, migrationErr := db.ExecContext(cmd.Ctx, contents) + _, err = db.ExecContext(cmd.Ctx, contents) timeTaken := time.Since(m.startedAt.Time) m.timeTakenNs = sql.NullInt64{Int64: int64(timeTaken), Valid: true} - if migrationErr != nil { + if err != nil { + migrationErr := &MigrationError{ + Err: err, + Filename: m.filename, + Contents: contents, + StartedAt: m.startedAt.Time, + TimeTaken: timeTaken, + } if cmd.driver.AnnotateError != nil { - migrationErr = cmd.driver.AnnotateError(migrationErr, contents) + migrationErr.Err = cmd.driver.AnnotateError(migrationErr.Err, contents) } m.success = false if cmd.Verbose { diff --git a/main.go b/main.go index 150594d..ca15e53 100644 --- a/main.go +++ b/main.go @@ -73,6 +73,10 @@ func main() { if errors.Is(err, flag.ErrHelp) { os.Exit(0) } + var migrationErr *ddl.MigrationError + if errors.As(err, &migrationErr) { + fmt.Fprintln(os.Stderr, migrationErr.Contents) + } fmt.Fprintln(os.Stderr, subcmd+": "+err.Error()) os.Exit(1) }