From f825d22e34e16237d7046c4d2778cce30a2932ca Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 26 Jun 2026 04:32:21 +0530 Subject: [PATCH 1/3] fix --- apps/cli-go/internal/db/diff/diff.go | 23 ++++++++++++++++++++++- apps/cli-go/internal/db/diff/diff_test.go | 18 ++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/apps/cli-go/internal/db/diff/diff.go b/apps/cli-go/internal/db/diff/diff.go index aa286eea5b..76a2cf5f8c 100644 --- a/apps/cli-go/internal/db/diff/diff.go +++ b/apps/cli-go/internal/db/diff/diff.go @@ -70,7 +70,7 @@ func loadDeclaredSchemas(fsys afero.Fs) ([]string, error) { } } if schemas := utils.Config.Db.Migrations.SchemaPaths; len(schemas) > 0 { - return schemas.Files(afero.NewIOFS(fsys)) + return loadSchemaPaths(afero.NewIOFS(fsys), schemas) } if exists, err := afero.DirExists(fsys, utils.SchemasDir); err != nil { return nil, errors.Errorf("failed to check schemas: %w", err) @@ -95,6 +95,27 @@ func loadDeclaredSchemas(fsys afero.Fs) ([]string, error) { return declared, nil } +func loadSchemaPaths(fsys fs.FS, schemas []string) ([]string, error) { + var declared []string + set := make(map[string]struct{}) + for _, pattern := range schemas { + matches, err := fs.Glob(fsys, filepath.ToSlash(pattern)) + if err != nil { + return nil, errors.Errorf("failed to glob files: %w", err) + } + sort.Strings(matches) + for _, item := range matches { + fp := filepath.ToSlash(item) + if _, exists := set[fp]; exists { + continue + } + set[fp] = struct{}{} + declared = append(declared, fp) + } + } + return declared, nil +} + // https://github.com/djrobstep/migra/blob/master/migra/statements.py#L6 var dropStatementPattern = regexp.MustCompile(`(?i)drop\s+`) diff --git a/apps/cli-go/internal/db/diff/diff_test.go b/apps/cli-go/internal/db/diff/diff_test.go index e0df5b328a..3ccc53244e 100644 --- a/apps/cli-go/internal/db/diff/diff_test.go +++ b/apps/cli-go/internal/db/diff/diff_test.go @@ -409,3 +409,21 @@ func TestLoadSchemas(t *testing.T) { assert.NoError(t, err) assert.ElementsMatch(t, expected, schemas) } + +func TestLoadSchemasSkipsEmptySchemaPathGlobs(t *testing.T) { + fsys := afero.NewMemMapFs() + matched := filepath.Join(utils.SupabaseDirPath, "schemas", "tables", "players.sql") + require.NoError(t, afero.WriteFile(fsys, matched, nil, 0644)) + utils.Config.Db.Migrations.SchemaPaths = []string{ + filepath.Join(utils.SupabaseDirPath, "schemas", "tables", "*.sql"), + filepath.Join(utils.SupabaseDirPath, "schemas", "materialized_views", "*.sql"), + } + t.Cleanup(func() { + utils.Config.Db.Migrations.SchemaPaths = nil + }) + + schemas, err := loadDeclaredSchemas(fsys) + + assert.NoError(t, err) + assert.Equal(t, []string{filepath.ToSlash(matched)}, schemas) +} From ce69a2a48f0e9528336564c7e41a77b513086be8 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 26 Jun 2026 04:52:53 +0530 Subject: [PATCH 2/3] nit --- apps/cli-go/internal/db/diff/diff.go | 17 +++++++++++++ apps/cli-go/internal/db/diff/diff_test.go | 31 +++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/apps/cli-go/internal/db/diff/diff.go b/apps/cli-go/internal/db/diff/diff.go index 76a2cf5f8c..07805a4b5e 100644 --- a/apps/cli-go/internal/db/diff/diff.go +++ b/apps/cli-go/internal/db/diff/diff.go @@ -97,11 +97,17 @@ func loadDeclaredSchemas(fsys afero.Fs) ([]string, error) { func loadSchemaPaths(fsys fs.FS, schemas []string) ([]string, error) { var declared []string + var skipped []string set := make(map[string]struct{}) for _, pattern := range schemas { matches, err := fs.Glob(fsys, filepath.ToSlash(pattern)) if err != nil { return nil, errors.Errorf("failed to glob files: %w", err) + } else if len(matches) == 0 && !hasWildcardStar(pattern) { + return nil, errors.Errorf("no files matched pattern: %s", pattern) + } else if len(matches) == 0 { + skipped = append(skipped, pattern) + continue } sort.Strings(matches) for _, item := range matches { @@ -113,9 +119,20 @@ func loadSchemaPaths(fsys fs.FS, schemas []string) ([]string, error) { declared = append(declared, fp) } } + if len(declared) == 0 && len(skipped) > 0 { + var errs []error + for _, pattern := range skipped { + errs = append(errs, errors.Errorf("no files matched pattern: %s", pattern)) + } + return nil, errors.Join(errs...) + } return declared, nil } +func hasWildcardStar(pattern string) bool { + return strings.Contains(pattern, "*") +} + // https://github.com/djrobstep/migra/blob/master/migra/statements.py#L6 var dropStatementPattern = regexp.MustCompile(`(?i)drop\s+`) diff --git a/apps/cli-go/internal/db/diff/diff_test.go b/apps/cli-go/internal/db/diff/diff_test.go index 3ccc53244e..2a6a2d4ca4 100644 --- a/apps/cli-go/internal/db/diff/diff_test.go +++ b/apps/cli-go/internal/db/diff/diff_test.go @@ -427,3 +427,34 @@ func TestLoadSchemasSkipsEmptySchemaPathGlobs(t *testing.T) { assert.NoError(t, err) assert.Equal(t, []string{filepath.ToSlash(matched)}, schemas) } + +func TestLoadSchemasErrorsOnMissingLiteralSchemaPath(t *testing.T) { + fsys := afero.NewMemMapFs() + utils.Config.Db.Migrations.SchemaPaths = []string{ + filepath.Join(utils.SupabaseDirPath, "schemas", "tables", "players.sql"), + } + t.Cleanup(func() { + utils.Config.Db.Migrations.SchemaPaths = nil + }) + + schemas, err := loadDeclaredSchemas(fsys) + + assert.ErrorContains(t, err, "no files matched pattern") + assert.Empty(t, schemas) +} + +func TestLoadSchemasErrorsWhenAllSchemaPathGlobsAreEmpty(t *testing.T) { + fsys := afero.NewMemMapFs() + utils.Config.Db.Migrations.SchemaPaths = []string{ + filepath.Join(utils.SupabaseDirPath, "schemas", "tables", "*.sql"), + filepath.Join(utils.SupabaseDirPath, "schemas", "views", "*.sql"), + } + t.Cleanup(func() { + utils.Config.Db.Migrations.SchemaPaths = nil + }) + + schemas, err := loadDeclaredSchemas(fsys) + + assert.ErrorContains(t, err, "no files matched pattern") + assert.Empty(t, schemas) +} From cab53da998fd7835096e15b21d02df8eb1f1283e Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:58:12 +0530 Subject: [PATCH 3/3] refactor --- apps/cli-go/internal/db/diff/diff.go | 45 ++++----------------------- apps/cli-go/pkg/config/config.go | 39 ++++++++++++++++++++++- apps/cli-go/pkg/config/config_test.go | 28 +++++++++++++++++ 3 files changed, 72 insertions(+), 40 deletions(-) diff --git a/apps/cli-go/internal/db/diff/diff.go b/apps/cli-go/internal/db/diff/diff.go index 07805a4b5e..5d2790e5b9 100644 --- a/apps/cli-go/internal/db/diff/diff.go +++ b/apps/cli-go/internal/db/diff/diff.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/afero" "github.com/supabase/cli/internal/db/start" "github.com/supabase/cli/internal/utils" + configpkg "github.com/supabase/cli/pkg/config" "github.com/supabase/cli/pkg/migration" "github.com/supabase/cli/pkg/parser" ) @@ -70,7 +71,11 @@ func loadDeclaredSchemas(fsys afero.Fs) ([]string, error) { } } if schemas := utils.Config.Db.Migrations.SchemaPaths; len(schemas) > 0 { - return loadSchemaPaths(afero.NewIOFS(fsys), schemas) + return schemas.Files( + afero.NewIOFS(fsys), + configpkg.WithSkipEmptyGlobs(), + configpkg.WithErrorOnAllSkippedGlobs(), + ) } if exists, err := afero.DirExists(fsys, utils.SchemasDir); err != nil { return nil, errors.Errorf("failed to check schemas: %w", err) @@ -95,44 +100,6 @@ func loadDeclaredSchemas(fsys afero.Fs) ([]string, error) { return declared, nil } -func loadSchemaPaths(fsys fs.FS, schemas []string) ([]string, error) { - var declared []string - var skipped []string - set := make(map[string]struct{}) - for _, pattern := range schemas { - matches, err := fs.Glob(fsys, filepath.ToSlash(pattern)) - if err != nil { - return nil, errors.Errorf("failed to glob files: %w", err) - } else if len(matches) == 0 && !hasWildcardStar(pattern) { - return nil, errors.Errorf("no files matched pattern: %s", pattern) - } else if len(matches) == 0 { - skipped = append(skipped, pattern) - continue - } - sort.Strings(matches) - for _, item := range matches { - fp := filepath.ToSlash(item) - if _, exists := set[fp]; exists { - continue - } - set[fp] = struct{}{} - declared = append(declared, fp) - } - } - if len(declared) == 0 && len(skipped) > 0 { - var errs []error - for _, pattern := range skipped { - errs = append(errs, errors.Errorf("no files matched pattern: %s", pattern)) - } - return nil, errors.Join(errs...) - } - return declared, nil -} - -func hasWildcardStar(pattern string) bool { - return strings.Contains(pattern, "*") -} - // https://github.com/djrobstep/migra/blob/master/migra/statements.py#L6 var dropStatementPattern = regexp.MustCompile(`(?i)drop\s+`) diff --git a/apps/cli-go/pkg/config/config.go b/apps/cli-go/pkg/config/config.go index b2bf3f4a99..b9f253230a 100644 --- a/apps/cli-go/pkg/config/config.go +++ b/apps/cli-go/pkg/config/config.go @@ -97,11 +97,35 @@ func (p *RequestPolicy) UnmarshalText(text []byte) error { type Glob []string +type globOptions struct { + skipEmptyGlobs bool + errorOnAllSkipped bool +} + +type GlobOption func(*globOptions) + +func WithSkipEmptyGlobs() GlobOption { + return func(o *globOptions) { + o.skipEmptyGlobs = true + } +} + +func WithErrorOnAllSkippedGlobs() GlobOption { + return func(o *globOptions) { + o.errorOnAllSkipped = true + } +} + // Match the glob patterns in the given FS to get a deduplicated // array of all migrations files to apply in the declared order. -func (g Glob) Files(fsys fs.FS) ([]string, error) { +func (g Glob) Files(fsys fs.FS, options ...GlobOption) ([]string, error) { + opts := globOptions{} + for _, apply := range options { + apply(&opts) + } var result []string var allErrors []error + var skipped []string set := make(map[string]struct{}) for _, pattern := range g { // Glob expects / as path separator on windows @@ -109,6 +133,10 @@ func (g Glob) Files(fsys fs.FS) ([]string, error) { if err != nil { allErrors = append(allErrors, errors.Errorf("failed to glob files: %w", err)) } else if len(matches) == 0 { + if opts.skipEmptyGlobs && hasGlobMeta(pattern) { + skipped = append(skipped, pattern) + continue + } allErrors = append(allErrors, errors.Errorf("no files matched pattern: %s", pattern)) } sort.Strings(matches) @@ -121,9 +149,18 @@ func (g Glob) Files(fsys fs.FS) ([]string, error) { } } } + if opts.errorOnAllSkipped && len(result) == 0 && len(skipped) > 0 { + for _, pattern := range skipped { + allErrors = append(allErrors, errors.Errorf("no files matched pattern: %s", pattern)) + } + } return result, errors.Join(allErrors...) } +func hasGlobMeta(pattern string) bool { + return strings.ContainsAny(pattern, `*?[`) +} + // We follow these rules when adding new config: // 1. Update init_config.toml (and init_config.test.toml) with the new key, default value, and comments to explain usage. // 2. Update config struct with new field and toml tag (spelled in snake_case). diff --git a/apps/cli-go/pkg/config/config_test.go b/apps/cli-go/pkg/config/config_test.go index fa38005152..6c2697ca0e 100644 --- a/apps/cli-go/pkg/config/config_test.go +++ b/apps/cli-go/pkg/config/config_test.go @@ -676,6 +676,34 @@ func TestGlobFiles(t *testing.T) { // Validate files assert.Empty(t, files) }) + + t.Run("skips empty globs when configured", func(t *testing.T) { + fsys := fs.MapFS{ + "supabase/schemas/tables/players.sql": &fs.MapFile{}, + } + g := Glob{ + "supabase/schemas/tables/*.sql", + "supabase/schemas/materialized_views/*.sql", + } + + files, err := g.Files(fsys, WithSkipEmptyGlobs()) + + assert.NoError(t, err) + assert.Equal(t, []string{"supabase/schemas/tables/players.sql"}, files) + }) + + t.Run("errors when all skipped globs are empty and configured to fail", func(t *testing.T) { + fsys := fs.MapFS{} + g := Glob{ + "supabase/schemas/tables/*.sql", + "supabase/schemas/materialized_views/*.sql", + } + + files, err := g.Files(fsys, WithSkipEmptyGlobs(), WithErrorOnAllSkippedGlobs()) + + assert.ErrorContains(t, err, "no files matched pattern") + assert.Empty(t, files) + }) } func TestLoadFunctionImportMap(t *testing.T) {