diff --git a/provider_run.go b/provider_run.go index e69f94dcd..95b4421ce 100644 --- a/provider_run.go +++ b/provider_run.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io/fs" + "runtime/debug" "sort" "strconv" "strings" @@ -424,7 +425,13 @@ func runMigration(ctx context.Context, db database.DBTxConn, m *Migration, direc // runGo is a helper function that runs the given Go functions in the given direction. It must only // be called after the migration has been initialized. -func runGo(ctx context.Context, db database.DBTxConn, m *Migration, direction bool) error { +func runGo(ctx context.Context, db database.DBTxConn, m *Migration, direction bool) (retErr error) { + defer func() { + if r := recover(); r != nil { + retErr = fmt.Errorf("panic: %v\n%s", r, debug.Stack()) + } + }() + switch db := db.(type) { case *sql.Conn: return fmt.Errorf("go migrations are not supported with *sql.Conn") diff --git a/provider_run_test.go b/provider_run_test.go index f30e09e22..e6926ea16 100644 --- a/provider_run_test.go +++ b/provider_run_test.go @@ -724,6 +724,34 @@ func TestSQLiteSharedCache(t *testing.T) { }) } +func TestGoMigrationPanic(t *testing.T) { + t.Parallel() + + ctx := context.Background() + const ( + wantErrString = "panic: runtime error: index out of range [7] with length 0" + ) + migration := goose.NewGoMigration( + 1, + &goose.GoFunc{RunTx: func(ctx context.Context, tx *sql.Tx) error { + var ss []int + _ = ss[7] + return nil + }}, + nil, + ) + p, err := goose.NewProvider(goose.DialectSQLite3, newDB(t), nil, + goose.WithGoMigrations(migration), // Add a Go migration that panics. + ) + check.NoError(t, err) + _, err = p.Up(ctx) + check.HasError(t, err) + check.Contains(t, err.Error(), wantErrString) + var expected *goose.PartialError + check.Bool(t, errors.As(err, &expected), true) + check.Contains(t, expected.Err.Error(), wantErrString) +} + func TestCustomStoreTableExists(t *testing.T) { t.Parallel()