fix(migrate): validate migration directory before provider creation
Documentation / docs (pull_request) Successful in 29s
ci / check (pull_request) Successful in 17m24s

This commit is contained in:
end committed 2026-10-27 12:12:59 -07:00
1 parent d912b475b5
commit 3d903403c8
2 files changed
+49

No files matched your search

+8
View File
@@ -15,6 +15,7 @@ import (
// New returns a provider for SQL files in dir and optional provider-local Go // New returns a provider for SQL files in dir and optional provider-local Go
// migrations. Pass "." for files at the embedded root and a nonnil logger. // migrations. Pass "." for files at the embedded root and a nonnil logger.
// The selected directory must exist, including when using only Go migrations.
// Construction does not connect to db; the caller owns and closes db. // Construction does not connect to db; the caller owns and closes db.
// //
// PostgreSQL uses Goose's session advisory locker with its default lock ID and // PostgreSQL uses Goose's session advisory locker with its default lock ID and
@@ -36,6 +37,13 @@ func New(db *sql.DB, dialect goose.Dialect, files embed.FS, dir string, logger *
if err != nil { if err != nil {
return nil, fmt.Errorf("migrate: directory %q: %w", dir, err) return nil, fmt.Errorf("migrate: directory %q: %w", dir, err)
} }
info, err := fs.Stat(root, ".")
if err != nil {
return nil, fmt.Errorf("migrate: directory %q: %w", dir, err)
}
if !info.IsDir() {
return nil, fmt.Errorf("migrate: directory %q: not a directory", dir)
}
opts := []goose.ProviderOption{ opts := []goose.ProviderOption{
goose.WithDisableGlobalRegistry(true), goose.WithDisableGlobalRegistry(true),
goose.WithGoMigrations(migrations...), goose.WithGoMigrations(migrations...),
+41
View File
@@ -8,6 +8,7 @@ import (
"embed" "embed"
"errors" "errors"
"io" "io"
"io/fs"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
@@ -414,3 +415,43 @@ func TestConstructor(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
} }
func TestConstructorDirectory(t *testing.T) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
// Directory validation and Go-only construction must remain offline.
if err := db.Close(); err != nil {
t.Fatal(err)
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
for _, test := range []struct {
dir string
wantErr string
}{
{dir: "missing", wantErr: "directory"},
{dir: "testdata/migrations/00007_events.sql", wantErr: "not a directory"},
{dir: "."},
{dir: "testdata"},
} {
t.Run(test.dir, func(t *testing.T) {
p, err := migrate.New(db, goose.DialectSQLite3, fixtures, test.dir, logger, goose.NewGoMigration(10, nil, nil))
if test.wantErr != "" {
if err == nil || p != nil || !strings.Contains(err.Error(), test.wantErr) || !strings.Contains(err.Error(), test.dir) {
t.Fatalf("New = %v, %v; want directory error containing %q and %q", p, err, test.wantErr, test.dir)
}
if test.dir == "missing" && !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("New error = %v; want fs.ErrNotExist", err)
}
return
}
if err != nil {
t.Fatal(err)
}
if sources := p.ListSources(); len(sources) != 1 || sources[0].Version != 10 {
t.Fatalf("ListSources = %v; want Go migration 10 only", sources)
}
})
}
}