From 255156a0c5d2694f6a6cad91ff3c91f7247bc90b Mon Sep 17 00:00:00 2001 From: Jonathan Braat Date: Wed, 15 Jan 2025 13:05:31 +1300 Subject: [PATCH 1/8] [ADD] create new repeatable migration --- cmd/migration.go | 6 +++++- internal/migration/new/new.go | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cmd/migration.go b/cmd/migration.go index 30b7716ac..cb2117deb 100644 --- a/cmd/migration.go +++ b/cmd/migration.go @@ -39,12 +39,14 @@ var ( }, } + repeatable bool + migrationNewCmd = &cobra.Command{ Use: "new ", Short: "Create an empty migration script", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return new.Run(args[0], os.Stdin, afero.NewOsFs()) + return new.Run(repeatable, args[0], os.Stdin, afero.NewOsFs()) }, } @@ -149,6 +151,8 @@ func init() { migrationFetchCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local") migrationCmd.AddCommand(migrationFetchCmd) // Build new command + newFlags := migrationNewCmd.Flags() + newFlags.BoolVarP(&repeatable, "repeatable", "r", false, "Creates a repeatable migration instead of a common migration.") migrationCmd.AddCommand(migrationNewCmd) rootCmd.AddCommand(migrationCmd) } diff --git a/internal/migration/new/new.go b/internal/migration/new/new.go index be232b591..d1026df7f 100644 --- a/internal/migration/new/new.go +++ b/internal/migration/new/new.go @@ -11,8 +11,16 @@ import ( "github.com/supabase/cli/internal/utils" ) -func Run(migrationName string, stdin afero.File, fsys afero.Fs) error { - path := GetMigrationPath(utils.GetCurrentTimestamp(), migrationName) +func Run(repeatable bool, migrationName string, stdin afero.File, fsys afero.Fs) error { + path := "" + + if repeatable { + // if migration name already exists, repeatable migration will be overwritten + path = GetRepeatableMigrationPath(migrationName) + } else { + path = GetMigrationPath(utils.GetCurrentTimestamp(), migrationName) + } + if err := utils.MkdirIfNotExistFS(fsys, filepath.Dir(path)); err != nil { return err } @@ -33,6 +41,11 @@ func GetMigrationPath(timestamp, name string) string { return filepath.Join(utils.MigrationsDir, fullName) } +func GetRepeatableMigrationPath(name string) string { + fullName := fmt.Sprintf("r_%s.sql", name) + return filepath.Join(utils.MigrationsDir, fullName) +} + func CopyStdinIfExists(stdin afero.File, dst io.Writer) error { if fi, err := stdin.Stat(); err != nil { return errors.Errorf("failed to initialise stdin: %w", err) From 2d25788bc0cfac8d3dcae5d927772fc74d54851b Mon Sep 17 00:00:00 2001 From: Jonathan Braat Date: Wed, 15 Jan 2025 13:22:51 +1300 Subject: [PATCH 2/8] [ADD] tests for new migration to include repeatable flag --- internal/migration/new/new_test.go | 51 ++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/internal/migration/new/new_test.go b/internal/migration/new/new_test.go index 39e2fe115..0291c422c 100644 --- a/internal/migration/new/new_test.go +++ b/internal/migration/new/new_test.go @@ -12,14 +12,14 @@ import ( ) func TestNewCommand(t *testing.T) { - t.Run("creates new migration file", func(t *testing.T) { + t.Run("creates new common migration file", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup empty stdin stdin, err := fsys.Create("/dev/stdin") require.NoError(t, err) // Run test - assert.NoError(t, Run("test_migrate", stdin, fsys)) + assert.NoError(t, Run(false, "test_migrate", stdin, fsys)) // Validate output files, err := afero.ReadDir(fsys, utils.MigrationsDir) assert.NoError(t, err) @@ -27,7 +27,44 @@ func TestNewCommand(t *testing.T) { assert.Regexp(t, `([0-9]{14})_test_migrate\.sql`, files[0].Name()) }) - t.Run("streams content from pipe", func(t *testing.T) { + t.Run("creates new repeatable migration file", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + // Setup empty stdin + stdin, err := fsys.Create("/dev/stdin") + require.NoError(t, err) + // Run test + assert.NoError(t, Run(true, "repeatable_test_migrate", stdin, fsys)) + // Validate output + files, err := afero.ReadDir(fsys, utils.MigrationsDir) + assert.NoError(t, err) + assert.Equal(t, 1, len(files)) + assert.Regexp(t, `r_repeatable_test_migrate\.sql`, files[0].Name()) + }) + + t.Run("streams content from pipe to common migration", func(t *testing.T) { + // Setup in-memory fs + fsys := afero.NewMemMapFs() + // Setup stdin + r, w, err := os.Pipe() + require.NoError(t, err) + script := "create table pet;\ndrop table pet;\n" + _, err = w.WriteString(script) + require.NoError(t, err) + require.NoError(t, w.Close()) + // Run test + assert.NoError(t, Run(false, "test_migrate", r, fsys)) + // Validate output + files, err := afero.ReadDir(fsys, utils.MigrationsDir) + assert.NoError(t, err) + assert.Equal(t, 1, len(files)) + path := filepath.Join(utils.MigrationsDir, files[0].Name()) + contents, err := afero.ReadFile(fsys, path) + assert.NoError(t, err) + assert.Equal(t, []byte(script), contents) + }) + + t.Run("streams content from pipe to repeatable migration", func(t *testing.T) { // Setup in-memory fs fsys := afero.NewMemMapFs() // Setup stdin @@ -38,7 +75,7 @@ func TestNewCommand(t *testing.T) { require.NoError(t, err) require.NoError(t, w.Close()) // Run test - assert.NoError(t, Run("test_migrate", r, fsys)) + assert.NoError(t, Run(true, "repeatable_test_migrate", r, fsys)) // Validate output files, err := afero.ReadDir(fsys, utils.MigrationsDir) assert.NoError(t, err) @@ -56,7 +93,8 @@ func TestNewCommand(t *testing.T) { stdin, err := fsys.Create("/dev/stdin") require.NoError(t, err) // Run test - assert.Error(t, Run("test_migrate", stdin, afero.NewReadOnlyFs(fsys))) + assert.Error(t, Run(false, "test_migrate", stdin, afero.NewReadOnlyFs(fsys))) + assert.Error(t, Run(true, "repeatable_test_migrate", stdin, afero.NewReadOnlyFs(fsys))) }) t.Run("throws error on closed pipe", func(t *testing.T) { @@ -67,6 +105,7 @@ func TestNewCommand(t *testing.T) { require.NoError(t, err) require.NoError(t, r.Close()) // Run test - assert.Error(t, Run("test_migrate", r, fsys)) + assert.Error(t, Run(false, "test_migrate", r, fsys)) + assert.Error(t, Run(true, "repeatable_test_migrate", r, fsys)) }) } From 9ad4525824d00478fa32939e0f7a8e110a63a19a Mon Sep 17 00:00:00 2001 From: Jonathan Braat Date: Wed, 15 Jan 2025 20:56:52 +1300 Subject: [PATCH 3/8] [ADD] migration up and tests --- internal/migration/up/up.go | 6 ++++++ pkg/migration/list.go | 23 +++++++++++++++++++++ pkg/migration/list_test.go | 41 +++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/internal/migration/up/up.go b/internal/migration/up/up.go index d33117f33..26fa0aa60 100644 --- a/internal/migration/up/up.go +++ b/internal/migration/up/up.go @@ -23,6 +23,7 @@ func Run(ctx context.Context, includeAll bool, config pgconn.Config, fsys afero. if err != nil { return err } + return migration.ApplyMigrations(ctx, pending, conn, afero.NewIOFS(fsys)) } @@ -45,6 +46,11 @@ func GetPendingMigrations(ctx context.Context, includeAll bool, conn *pgx.Conn, } utils.CmdSuggestion = suggestIgnoreFlag(diff) } + repeatableMigrations, err := migration.ListRepeatableMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)) + if err != nil { + return nil, err + } + diff = append(diff, repeatableMigrations...) return diff, err } diff --git a/pkg/migration/list.go b/pkg/migration/list.go index ec8bf2ac8..546682e22 100644 --- a/pkg/migration/list.go +++ b/pkg/migration/list.go @@ -8,6 +8,7 @@ import ( "path/filepath" "regexp" "strconv" + "strings" "github.com/go-errors/errors" "github.com/jackc/pgconn" @@ -45,6 +46,10 @@ func ListLocalMigrations(migrationsDir string, fsys fs.FS, filter ...func(string fmt.Fprintf(os.Stderr, "Skipping migration %s... (replace \"init\" with a different file name to apply this migration)\n", filename) continue } + if strings.HasPrefix(filename, "r_") { + // silently skip repeatable migrations + continue + } matches := migrateFilePattern.FindStringSubmatch(filename) if len(matches) == 0 { fmt.Fprintf(os.Stderr, "Skipping migration %s... (file name must match pattern \"_name.sql\")\n", filename) @@ -60,6 +65,24 @@ func ListLocalMigrations(migrationsDir string, fsys fs.FS, filter ...func(string return clean, nil } +func ListRepeatableMigrations(migrationsDir string, fsys fs.FS) ([]string, error) { + localMigrations, err := fs.ReadDir(fsys, migrationsDir) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return nil, errors.Errorf("failed to read directory: %w", err) + } + var repeatable []string + + for _, migration := range localMigrations { + filename := migration.Name() + if strings.HasPrefix(filename, "r_") && strings.HasSuffix(filename, ".sql") { + path := filepath.Join(migrationsDir, filename) + repeatable = append(repeatable, path) + } + } + + return repeatable, nil +} + var initSchemaPattern = regexp.MustCompile(`([0-9]{14})_init\.sql`) func shouldSkip(name string) bool { diff --git a/pkg/migration/list_test.go b/pkg/migration/list_test.go index 80d09fd40..aae07cc8d 100644 --- a/pkg/migration/list_test.go +++ b/pkg/migration/list_test.go @@ -90,3 +90,44 @@ func TestLocalMigrations(t *testing.T) { assert.ErrorContains(t, err, "failed to read directory:") }) } + +func TestRepeatableMigrations(t *testing.T) { + t.Run("loads repeatable migrations", func(t *testing.T) { + // Setup in-memory fs + files := []string{ + "r_test_view.sql", + "r_test_function.sql", + } + fsys := fs.MapFS{} + for _, name := range files { + fsys[name] = &fs.MapFile{} + } + // Run test + versions, err := ListRepeatableMigrations(".", fsys) + // Check error + assert.NoError(t, err) + assert.ElementsMatch(t, files, versions) + }) + + t.Run("ignores files without 'r_' prefix", func(t *testing.T) { + // Setup in-memory fs + fsys := fs.MapFS{ + "20211208000000_init.sql": &fs.MapFile{}, + "r_invalid.ts": &fs.MapFile{}, + } + // Run test + versions, err := ListRepeatableMigrations(".", fsys) + // Check error + assert.NoError(t, err) + assert.Empty(t, versions) + }) + + t.Run("throws error on open failure", func(t *testing.T) { + // Setup in-memory fs + fsys := fs.MapFS{"migrations": &fs.MapFile{}} + // Run test + _, err := ListRepeatableMigrations("migrations", fsys) + // Check error + assert.ErrorContains(t, err, "failed to read directory:") + }) +} From 654c2e3f5599edbeb2b0bcb2bd123bf9c1e92ab9 Mon Sep 17 00:00:00 2001 From: Jonathan Braat Date: Thu, 16 Jan 2025 13:08:30 +1300 Subject: [PATCH 4/8] [FIX] tests --- internal/migration/up/up.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/internal/migration/up/up.go b/internal/migration/up/up.go index 26fa0aa60..e59aa7192 100644 --- a/internal/migration/up/up.go +++ b/internal/migration/up/up.go @@ -24,7 +24,17 @@ func Run(ctx context.Context, includeAll bool, config pgconn.Config, fsys afero. return err } - return migration.ApplyMigrations(ctx, pending, conn, afero.NewIOFS(fsys)) + err = migration.ApplyMigrations(ctx, pending, conn, afero.NewIOFS(fsys)) + if err != nil { + return err + } + + repeatableMigrations, err := migration.ListRepeatableMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)) + if err != nil { + return err + } + + return migration.ApplyMigrations(ctx, repeatableMigrations, conn, afero.NewIOFS(fsys)) } func GetPendingMigrations(ctx context.Context, includeAll bool, conn *pgx.Conn, fsys afero.Fs) ([]string, error) { @@ -46,11 +56,6 @@ func GetPendingMigrations(ctx context.Context, includeAll bool, conn *pgx.Conn, } utils.CmdSuggestion = suggestIgnoreFlag(diff) } - repeatableMigrations, err := migration.ListRepeatableMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)) - if err != nil { - return nil, err - } - diff = append(diff, repeatableMigrations...) return diff, err } From 9fca837514162c31005ded27d208762e6b107c03 Mon Sep 17 00:00:00 2001 From: Jonathan Braat Date: Sun, 19 Jan 2025 19:55:37 +1300 Subject: [PATCH 5/8] [REF] requested changes --- cmd/migration.go | 2 +- internal/migration/new/new.go | 2 +- internal/migration/up/up.go | 17 ++++++----------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/cmd/migration.go b/cmd/migration.go index cb2117deb..226c461d0 100644 --- a/cmd/migration.go +++ b/cmd/migration.go @@ -152,7 +152,7 @@ func init() { migrationCmd.AddCommand(migrationFetchCmd) // Build new command newFlags := migrationNewCmd.Flags() - newFlags.BoolVarP(&repeatable, "repeatable", "r", false, "Creates a repeatable migration instead of a common migration.") + newFlags.BoolVarP(&repeatable, "repeatable", "r", false, "Creates a repeatable migration instead of a versioned migration.") migrationCmd.AddCommand(migrationNewCmd) rootCmd.AddCommand(migrationCmd) } diff --git a/internal/migration/new/new.go b/internal/migration/new/new.go index d1026df7f..86f36f03d 100644 --- a/internal/migration/new/new.go +++ b/internal/migration/new/new.go @@ -12,7 +12,7 @@ import ( ) func Run(repeatable bool, migrationName string, stdin afero.File, fsys afero.Fs) error { - path := "" + var path string if repeatable { // if migration name already exists, repeatable migration will be overwritten diff --git a/internal/migration/up/up.go b/internal/migration/up/up.go index e59aa7192..26fa0aa60 100644 --- a/internal/migration/up/up.go +++ b/internal/migration/up/up.go @@ -24,17 +24,7 @@ func Run(ctx context.Context, includeAll bool, config pgconn.Config, fsys afero. return err } - err = migration.ApplyMigrations(ctx, pending, conn, afero.NewIOFS(fsys)) - if err != nil { - return err - } - - repeatableMigrations, err := migration.ListRepeatableMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)) - if err != nil { - return err - } - - return migration.ApplyMigrations(ctx, repeatableMigrations, conn, afero.NewIOFS(fsys)) + return migration.ApplyMigrations(ctx, pending, conn, afero.NewIOFS(fsys)) } func GetPendingMigrations(ctx context.Context, includeAll bool, conn *pgx.Conn, fsys afero.Fs) ([]string, error) { @@ -56,6 +46,11 @@ func GetPendingMigrations(ctx context.Context, includeAll bool, conn *pgx.Conn, } utils.CmdSuggestion = suggestIgnoreFlag(diff) } + repeatableMigrations, err := migration.ListRepeatableMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)) + if err != nil { + return nil, err + } + diff = append(diff, repeatableMigrations...) return diff, err } From c4b27a535d24598e4c0be05f4948fb6949a9817d Mon Sep 17 00:00:00 2001 From: Jonathan Braat Date: Sun, 19 Jan 2025 20:18:13 +1300 Subject: [PATCH 6/8] [FIX] migration up tests --- internal/migration/up/up.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/migration/up/up.go b/internal/migration/up/up.go index 26fa0aa60..f696aed11 100644 --- a/internal/migration/up/up.go +++ b/internal/migration/up/up.go @@ -46,6 +46,9 @@ func GetPendingMigrations(ctx context.Context, includeAll bool, conn *pgx.Conn, } utils.CmdSuggestion = suggestIgnoreFlag(diff) } + if err != nil { + return diff, err + } repeatableMigrations, err := migration.ListRepeatableMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)) if err != nil { return nil, err From 34169f2a7a40cb8838beae2f1783eacc919b0af8 Mon Sep 17 00:00:00 2001 From: Jonathan Braat Date: Mon, 20 Jan 2025 16:13:59 +1300 Subject: [PATCH 7/8] [ADD] repeatable migrations to MigrateAndSeed --- internal/migration/apply/apply.go | 5 +++++ internal/migration/list/list.go | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/internal/migration/apply/apply.go b/internal/migration/apply/apply.go index 224b342f7..57b597731 100644 --- a/internal/migration/apply/apply.go +++ b/internal/migration/apply/apply.go @@ -15,6 +15,11 @@ func MigrateAndSeed(ctx context.Context, version string, conn *pgx.Conn, fsys af if err != nil { return err } + repeatableMigrations, err := list.LoadRepeatableMigrations(fsys) + if err != nil { + return err + } + migrations = append(migrations, repeatableMigrations...) if err := migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys)); err != nil { return err } diff --git a/internal/migration/list/list.go b/internal/migration/list/list.go index 3107d4ec6..3ef48258d 100644 --- a/internal/migration/list/list.go +++ b/internal/migration/list/list.go @@ -103,3 +103,7 @@ func LoadPartialMigrations(version string, fsys afero.Fs) ([]string, error) { } return migration.ListLocalMigrations(utils.MigrationsDir, afero.NewIOFS(fsys), filter) } + +func LoadRepeatableMigrations(fsys afero.Fs) ([]string, error) { + return migration.ListRepeatableMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)) +} From 322044b462784586f5ffb688788986fd01a6f41d Mon Sep 17 00:00:00 2001 From: Jonathan Braat Date: Thu, 23 Jan 2025 21:57:14 +1300 Subject: [PATCH 8/8] [REF] treat repeatable migrations as versioned migrations with name as version --- internal/migration/apply/apply.go | 5 ---- internal/migration/list/list.go | 41 ++++++++++++++++++++++++++---- internal/migration/up/up.go | 8 ------ pkg/migration/file.go | 6 ++++- pkg/migration/list.go | 31 +++++------------------ pkg/migration/list_test.go | 42 +------------------------------ 6 files changed, 48 insertions(+), 85 deletions(-) diff --git a/internal/migration/apply/apply.go b/internal/migration/apply/apply.go index 57b597731..224b342f7 100644 --- a/internal/migration/apply/apply.go +++ b/internal/migration/apply/apply.go @@ -15,11 +15,6 @@ func MigrateAndSeed(ctx context.Context, version string, conn *pgx.Conn, fsys af if err != nil { return err } - repeatableMigrations, err := list.LoadRepeatableMigrations(fsys) - if err != nil { - return err - } - migrations = append(migrations, repeatableMigrations...) if err := migration.ApplyMigrations(ctx, migrations, conn, afero.NewIOFS(fsys)); err != nil { return err } diff --git a/internal/migration/list/list.go b/internal/migration/list/list.go index 3ef48258d..5d5ddd56f 100644 --- a/internal/migration/list/list.go +++ b/internal/migration/list/list.go @@ -5,6 +5,7 @@ import ( "fmt" "math" "strconv" + "strings" "github.com/charmbracelet/glamour" "github.com/go-errors/errors" @@ -68,6 +69,40 @@ func makeTable(remoteMigrations, localMigrations []string) string { j++ } } + + for i, j := 0, 0; i < len(remoteMigrations) || j < len(localMigrations); { + if i < len(remoteMigrations) && !strings.HasPrefix(remoteMigrations[i], "r_") { + i++ + continue + } + + if j < len(localMigrations) && !strings.HasPrefix(localMigrations[j], "r_") { + j++ + continue + } + + // Append repeatable migrations to table + if i >= len(remoteMigrations) { + table += fmt.Sprintf("|`%s`|` `|` `|\n", localMigrations[j]) + j++ + } else if j >= len(localMigrations) { + table += fmt.Sprintf("|` `|`%s`|` `|\n", remoteMigrations[i]) + i++ + } else { + if localMigrations[j] < remoteMigrations[i] { + table += fmt.Sprintf("|`%s`|` `|` `|\n", localMigrations[j]) + j++ + } else if remoteMigrations[i] < localMigrations[j] { + table += fmt.Sprintf("|` `|`%s`|` `|\n", remoteMigrations[i]) + i++ + } else { + table += fmt.Sprintf("|`%s`|`%s`|` `|\n", localMigrations[j], remoteMigrations[i]) + i++ + j++ + } + } + } + return table } @@ -99,11 +134,7 @@ func LoadLocalVersions(fsys afero.Fs) ([]string, error) { func LoadPartialMigrations(version string, fsys afero.Fs) ([]string, error) { filter := func(v string) bool { - return version == "" || v <= version + return version == "" || strings.HasPrefix(version, "r_") || v <= version } return migration.ListLocalMigrations(utils.MigrationsDir, afero.NewIOFS(fsys), filter) } - -func LoadRepeatableMigrations(fsys afero.Fs) ([]string, error) { - return migration.ListRepeatableMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)) -} diff --git a/internal/migration/up/up.go b/internal/migration/up/up.go index f696aed11..3f2531d99 100644 --- a/internal/migration/up/up.go +++ b/internal/migration/up/up.go @@ -46,14 +46,6 @@ func GetPendingMigrations(ctx context.Context, includeAll bool, conn *pgx.Conn, } utils.CmdSuggestion = suggestIgnoreFlag(diff) } - if err != nil { - return diff, err - } - repeatableMigrations, err := migration.ListRepeatableMigrations(utils.MigrationsDir, afero.NewIOFS(fsys)) - if err != nil { - return nil, err - } - diff = append(diff, repeatableMigrations...) return diff, err } diff --git a/pkg/migration/file.go b/pkg/migration/file.go index 79da1c86a..cb45a5430 100644 --- a/pkg/migration/file.go +++ b/pkg/migration/file.go @@ -23,7 +23,7 @@ type MigrationFile struct { Statements []string } -var migrateFilePattern = regexp.MustCompile(`^([0-9]+)_(.*)\.sql$`) +var migrateFilePattern = regexp.MustCompile(`^([0-9]+|r)_(.*)\.sql$`) func NewMigrationFromFile(path string, fsys fs.FS) (*MigrationFile, error) { lines, err := parseFile(path, fsys) @@ -38,6 +38,10 @@ func NewMigrationFromFile(path string, fsys fs.FS) (*MigrationFile, error) { file.Version = matches[1] file.Name = matches[2] } + // Repeatable migration version => r_name + if file.Version == "r" { + file.Version += "_" + file.Name + } return &file, nil } diff --git a/pkg/migration/list.go b/pkg/migration/list.go index 546682e22..b4161538d 100644 --- a/pkg/migration/list.go +++ b/pkg/migration/list.go @@ -8,7 +8,6 @@ import ( "path/filepath" "regexp" "strconv" - "strings" "github.com/go-errors/errors" "github.com/jackc/pgconn" @@ -46,18 +45,18 @@ func ListLocalMigrations(migrationsDir string, fsys fs.FS, filter ...func(string fmt.Fprintf(os.Stderr, "Skipping migration %s... (replace \"init\" with a different file name to apply this migration)\n", filename) continue } - if strings.HasPrefix(filename, "r_") { - // silently skip repeatable migrations - continue - } matches := migrateFilePattern.FindStringSubmatch(filename) if len(matches) == 0 { - fmt.Fprintf(os.Stderr, "Skipping migration %s... (file name must match pattern \"_name.sql\")\n", filename) + fmt.Fprintf(os.Stderr, "Skipping migration %s... (file name must match pattern \"_name.sql\" or \"r_name.sql\")\n", filename) continue } path := filepath.Join(migrationsDir, filename) for _, keep := range filter { - if version := matches[1]; keep(version) { + version := matches[1] + if version == "r" && len(matches) > 2 { + version += "_" + matches[2] + } + if keep(version) { clean = append(clean, path) } } @@ -65,24 +64,6 @@ func ListLocalMigrations(migrationsDir string, fsys fs.FS, filter ...func(string return clean, nil } -func ListRepeatableMigrations(migrationsDir string, fsys fs.FS) ([]string, error) { - localMigrations, err := fs.ReadDir(fsys, migrationsDir) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return nil, errors.Errorf("failed to read directory: %w", err) - } - var repeatable []string - - for _, migration := range localMigrations { - filename := migration.Name() - if strings.HasPrefix(filename, "r_") && strings.HasSuffix(filename, ".sql") { - path := filepath.Join(migrationsDir, filename) - repeatable = append(repeatable, path) - } - } - - return repeatable, nil -} - var initSchemaPattern = regexp.MustCompile(`([0-9]{14})_init\.sql`) func shouldSkip(name string) bool { diff --git a/pkg/migration/list_test.go b/pkg/migration/list_test.go index aae07cc8d..a9babf61c 100644 --- a/pkg/migration/list_test.go +++ b/pkg/migration/list_test.go @@ -73,6 +73,7 @@ func TestLocalMigrations(t *testing.T) { fsys := fs.MapFS{ "20211208000000_init.sql": &fs.MapFile{}, "20211208000001_invalid.ts": &fs.MapFile{}, + "r_invalid.ts": &fs.MapFile{}, } // Run test versions, err := ListLocalMigrations(".", fsys) @@ -90,44 +91,3 @@ func TestLocalMigrations(t *testing.T) { assert.ErrorContains(t, err, "failed to read directory:") }) } - -func TestRepeatableMigrations(t *testing.T) { - t.Run("loads repeatable migrations", func(t *testing.T) { - // Setup in-memory fs - files := []string{ - "r_test_view.sql", - "r_test_function.sql", - } - fsys := fs.MapFS{} - for _, name := range files { - fsys[name] = &fs.MapFile{} - } - // Run test - versions, err := ListRepeatableMigrations(".", fsys) - // Check error - assert.NoError(t, err) - assert.ElementsMatch(t, files, versions) - }) - - t.Run("ignores files without 'r_' prefix", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{ - "20211208000000_init.sql": &fs.MapFile{}, - "r_invalid.ts": &fs.MapFile{}, - } - // Run test - versions, err := ListRepeatableMigrations(".", fsys) - // Check error - assert.NoError(t, err) - assert.Empty(t, versions) - }) - - t.Run("throws error on open failure", func(t *testing.T) { - // Setup in-memory fs - fsys := fs.MapFS{"migrations": &fs.MapFile{}} - // Run test - _, err := ListRepeatableMigrations("migrations", fsys) - // Check error - assert.ErrorContains(t, err, "failed to read directory:") - }) -}