Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add custom seed path to config #2702

Merged
merged 15 commits into from
Sep 25, 2024
Merged
2 changes: 1 addition & 1 deletion cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ func init() {
pushFlags := dbPushCmd.Flags()
pushFlags.BoolVar(&includeAll, "include-all", false, "Include all migrations not found on remote history table.")
pushFlags.BoolVar(&includeRoles, "include-roles", false, "Include custom roles from "+utils.CustomRolesPath+".")
pushFlags.BoolVar(&includeSeed, "include-seed", false, "Include seed data from "+utils.SeedDataPath+".")
pushFlags.BoolVar(&includeSeed, "include-seed", false, "Include seed data from your config.")
pushFlags.BoolVar(&dryRun, "dry-run", false, "Print the migrations that would be applied, but don't actually apply them.")
pushFlags.String("db-url", "", "Pushes to the database specified by the connection string (must be percent-encoded).")
pushFlags.Bool("linked", true, "Pushes to the linked project.")
Expand Down
2 changes: 1 addition & 1 deletion internal/db/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func Run(ctx context.Context, dryRun, ignoreVersionMismatch bool, includeRoles,
fmt.Fprintln(os.Stderr, "Would push these migrations:")
fmt.Fprint(os.Stderr, utils.Bold(confirmPushAll(pending)))
if includeSeed {
fmt.Fprintln(os.Stderr, "Would seed data "+utils.Bold(utils.SeedDataPath)+"...")
fmt.Fprintln(os.Stderr, "Would seed data "+utils.Bold(utils.GetSeedsFilepaths())+"...")
}
} else {
msg := fmt.Sprintf("Do you want to push these migrations to the remote database?\n%s\n", confirmPushAll(pending))
Expand Down
3 changes: 2 additions & 1 deletion internal/db/push/push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ func TestPushAll(t *testing.T) {

t.Run("throws error on seed failure", func(t *testing.T) {
// Setup in-memory fs
fsys := &fstest.OpenErrorFs{DenyPath: utils.SeedDataPath}
fsys := &fstest.OpenErrorFs{DenyPath: utils.DefaultSeedDataPath}
_, _ = fsys.Create(utils.DefaultSeedDataPath)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new files searching behavior we need to ensure the file actually exist on the filesystem. Otherwise, the "Glob" filter will just not return anything, the seeds will be skipped and the file will never be attempted to open.

path := filepath.Join(utils.MigrationsDir, "0_test.sql")
require.NoError(t, afero.WriteFile(fsys, path, []byte{}, 0644))
// Setup mock postgres
Expand Down
2 changes: 1 addition & 1 deletion internal/db/reset/reset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ func TestResetRemote(t *testing.T) {
fsys := afero.NewMemMapFs()
path := filepath.Join(utils.MigrationsDir, "0_schema.sql")
require.NoError(t, afero.WriteFile(fsys, path, nil, 0644))
seedPath := filepath.Join(utils.SeedDataPath)
seedPath := filepath.Join(utils.DefaultSeedDataPath)
// Will raise an error when seeding
require.NoError(t, afero.WriteFile(fsys, seedPath, []byte("INSERT INTO test_table;"), 0644))
// Setup mock postgres
Expand Down
2 changes: 1 addition & 1 deletion internal/db/start/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func TestStartDatabase(t *testing.T) {
roles := "create role test"
require.NoError(t, afero.WriteFile(fsys, utils.CustomRolesPath, []byte(roles), 0644))
seed := "INSERT INTO employees(name) VALUES ('Alice')"
require.NoError(t, afero.WriteFile(fsys, utils.SeedDataPath, []byte(seed), 0644))
require.NoError(t, afero.WriteFile(fsys, utils.DefaultSeedDataPath, []byte(seed), 0644))
// Setup mock docker
require.NoError(t, apitest.MockDocker(utils.Docker))
defer gock.OffAll()
Expand Down
2 changes: 1 addition & 1 deletion internal/init/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func Run(ctx context.Context, fsys afero.Fs, createVscodeSettings, createIntelli
}

func initSeed(fsys afero.Fs) error {
f, err := fsys.OpenFile(utils.SeedDataPath, os.O_WRONLY|os.O_CREATE, 0644)
f, err := fsys.OpenFile(utils.DefaultSeedDataPath, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return errors.Errorf("failed to create seed file: %w", err)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/init/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestInitCommand(t *testing.T) {
assert.NoError(t, err)
assert.True(t, exists)
// Validate generated seed.sql
exists, err = afero.Exists(fsys, utils.SeedDataPath)
exists, err = afero.Exists(fsys, utils.DefaultSeedDataPath)
assert.NoError(t, err)
assert.True(t, exists)
// Validate vscode settings file isn't generated
Expand Down Expand Up @@ -72,7 +72,8 @@ func TestInitCommand(t *testing.T) {

t.Run("throws error on seed failure", func(t *testing.T) {
// Setup in-memory fs
fsys := &fstest.OpenErrorFs{DenyPath: utils.SeedDataPath}
fsys := &fstest.OpenErrorFs{DenyPath: utils.DefaultSeedDataPath}
_, _ = fsys.Create(utils.DefaultSeedDataPath)
// Run test
err := Run(context.Background(), fsys, nil, nil, utils.InitParams{})
// Check error
Expand Down
12 changes: 8 additions & 4 deletions internal/migration/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ func MigrateAndSeed(ctx context.Context, version string, conn *pgx.Conn, fsys af
}

func SeedDatabase(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
err := migration.SeedData(ctx, []string{utils.SeedDataPath}, conn, afero.NewIOFS(fsys))
if errors.Is(err, os.ErrNotExist) {
return nil
if seedPaths, err := utils.GetSeedFiles(fsys); err != nil {
return err
} else {
err := migration.SeedData(ctx, seedPaths, conn, afero.NewIOFS(fsys))
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
avallete marked this conversation as resolved.
Show resolved Hide resolved
return err
}

func CreateCustomRoles(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
Expand Down
17 changes: 11 additions & 6 deletions internal/migration/apply/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestMigrateDatabase(t *testing.T) {
path := filepath.Join(utils.MigrationsDir, "0_test.sql")
sql := "create schema public"
require.NoError(t, afero.WriteFile(fsys, path, []byte(sql), 0644))
seedPath := filepath.Join(utils.SeedDataPath)
seedPath := filepath.Join(utils.DefaultSeedDataPath)
// This will raise an error when seeding
require.NoError(t, afero.WriteFile(fsys, seedPath, []byte("INSERT INTO test_table;"), 0644))
// Setup mock postgres
Expand Down Expand Up @@ -82,7 +82,7 @@ func TestSeedDatabase(t *testing.T) {
fsys := afero.NewMemMapFs()
// Setup seed file
sql := "INSERT INTO employees(name) VALUES ('Alice')"
require.NoError(t, afero.WriteFile(fsys, utils.SeedDataPath, []byte(sql), 0644))
require.NoError(t, afero.WriteFile(fsys, utils.DefaultSeedDataPath, []byte(sql), 0644))
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
Expand All @@ -99,10 +99,15 @@ func TestSeedDatabase(t *testing.T) {
})

t.Run("throws error on read failure", func(t *testing.T) {
// Setup in-memory fs
fsys := &fstest.OpenErrorFs{DenyPath: utils.SeedDataPath}
// Wrap the fs with OpenErrorFs
errorFs := &fstest.OpenErrorFs{
DenyPath: utils.DefaultSeedDataPath,
}
_, _ = errorFs.Create(utils.DefaultSeedDataPath)

// Run test
err := SeedDatabase(context.Background(), nil, fsys)
err := SeedDatabase(context.Background(), nil, errorFs)

// Check error
assert.ErrorIs(t, err, os.ErrPermission)
})
Expand All @@ -112,7 +117,7 @@ func TestSeedDatabase(t *testing.T) {
fsys := afero.NewMemMapFs()
// Setup seed file
sql := "INSERT INTO employees(name) VALUES ('Alice')"
require.NoError(t, afero.WriteFile(fsys, utils.SeedDataPath, []byte(sql), 0644))
require.NoError(t, afero.WriteFile(fsys, utils.DefaultSeedDataPath, []byte(sql), 0644))
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
Expand Down
40 changes: 39 additions & 1 deletion internal/utils/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"regexp"
"sort"
"time"

"github.com/docker/docker/client"
Expand Down Expand Up @@ -148,7 +149,7 @@ var (
FallbackImportMapPath = filepath.Join(FunctionsDir, "import_map.json")
FallbackEnvFilePath = filepath.Join(FunctionsDir, ".env")
DbTestsDir = filepath.Join(SupabaseDirPath, "tests")
SeedDataPath = filepath.Join(SupabaseDirPath, "seed.sql")
DefaultSeedDataPath = filepath.Join(SupabaseDirPath, "seed.sql")
sweatybridge marked this conversation as resolved.
Show resolved Hide resolved
CustomRolesPath = filepath.Join(SupabaseDirPath, "roles.sql")

ErrNotLinked = errors.Errorf("Cannot find project ref. Have you run %s?", Aqua("supabase link"))
Expand All @@ -157,6 +158,43 @@ var (
ErrNotRunning = errors.Errorf("%s is not running.", Aqua("supabase start"))
)

func GetSeedsFilepaths() string {
if seedPaths, _ := GetSeedFiles(afero.NewOsFs()); seedPaths != nil {
return fmt.Sprintf("%v", seedPaths)
}
return DefaultSeedDataPath
}

/*
** Match the glob patterns from the config to get
** a deduplicated array of all migrations files to apply
** in the declared order
*/
func GetSeedFiles(fsys afero.Fs) ([]string, error) {
seedPaths := Config.Db.Seed.Path
fileSet := make(map[string]struct{})
var files []string
for _, pattern := range seedPaths {
fullPattern := filepath.Join(SupabaseDirPath, pattern)
matches, err := afero.Glob(fsys, fullPattern)
if err != nil {
return nil, errors.Errorf("failed to apply glob pattern for %w", err)
}
avallete marked this conversation as resolved.
Show resolved Hide resolved
sort.Strings(matches)
// Dedup the new matches with previously matched seeds to avoid duplication
// between matching patterns
for _, match := range matches {
if _, exists := fileSet[match]; !exists {
fileSet[match] = struct{}{}
files = append(files, match)
} else {
fmt.Fprintf(GetDebugLogger(), "Duplicate seed file found skipping: %s\n", match)
}
}
}
return files, nil
avallete marked this conversation as resolved.
Show resolved Hide resolved
}

func GetCurrentTimestamp() string {
// Magic number: https://stackoverflow.com/q/45160822.
return time.Now().UTC().Format("20060102150405")
Expand Down
73 changes: 73 additions & 0 deletions internal/utils/misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,76 @@ func TestProjectRoot(t *testing.T) {
assert.Equal(t, cwd, path)
})
}

func TestGetSeedFiles(t *testing.T) {
t.Run("returns seed files matching patterns", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Create seed files
require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed1.sql", []byte("INSERT INTO table1 VALUES (1);"), 0644))
require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed2.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed3.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/another.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/ignore.sql", []byte("INSERT INTO table3 VALUES (3);"), 0644))
// Mock config patterns
Config.Db.Seed.Path = []string{"seeds/seed[12].sql", "seeds/ano*.sql"}

// Run test
files, err := GetSeedFiles(fsys)

// Check error
assert.NoError(t, err)
// Validate files
assert.ElementsMatch(t, []string{"supabase/seeds/seed1.sql", "supabase/seeds/seed2.sql", "supabase/seeds/another.sql"}, files)
})
t.Run("returns seed files matching patterns skip duplicates", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Create seed files
require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed1.sql", []byte("INSERT INTO table1 VALUES (1);"), 0644))
require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed2.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/seed3.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/another.sql", []byte("INSERT INTO table2 VALUES (2);"), 0644))
require.NoError(t, afero.WriteFile(fsys, "supabase/seeds/ignore.sql", []byte("INSERT INTO table3 VALUES (3);"), 0644))
// Mock config patterns
Config.Db.Seed.Path = []string{"seeds/seed[12].sql", "seeds/ano*.sql", "seeds/seed*.sql"}

// Run test
files, err := GetSeedFiles(fsys)

// Check error
assert.NoError(t, err)
// Validate files
assert.ElementsMatch(t, []string{"supabase/seeds/seed1.sql", "supabase/seeds/seed2.sql", "supabase/seeds/another.sql", "supabase/seeds/seed3.sql"}, files)
})

t.Run("returns error on invalid pattern", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Mock config patterns
Config.Db.Seed.Path = []string{"[*!#@D#"}

// Run test
files, err := GetSeedFiles(fsys)

// Check error
assert.Nil(t, err)
// The resuling seed list should be empty
assert.ElementsMatch(t, []string{}, files)
})

t.Run("returns empty list if no files match", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Mock config patterns
Config.Db.Seed.Path = []string{"seeds/*.sql"}

// Run test
files, err := GetSeedFiles(fsys)

// Check error
assert.NoError(t, err)
// Validate files
assert.Empty(t, files)
})
}
4 changes: 3 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ type (
}

seed struct {
Enabled bool `toml:"enabled"`
Enabled bool `toml:"enabled"`
Path []string `toml:"path"`
avallete marked this conversation as resolved.
Show resolved Hide resolved
}

pooler struct {
Expand Down Expand Up @@ -465,6 +466,7 @@ func NewConfig(editors ...ConfigEditor) config {
},
Seed: seed{
Enabled: true,
Path: []string{"./seed.sql"},
avallete marked this conversation as resolved.
Show resolved Hide resolved
},
},
Realtime: realtime{
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/templates/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100

[db.seed]
# Determines whether to seed the database after migrations during a db reset
enabled = true
# Specifies seed files to load during db reset using glob patterns
# Base path is the Supabase project folder
# Example: path = ['./seeds/*.sql', '../project-src/seeds/*-load-testing.sql']
path = ['./seed.sql']

[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/testdata/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100

[db.seed]
# Determines whether to seed the database after migrations during a db reset
enabled = true
# Specifies seed files to load during db reset using glob patterns
# Base path is the Supabase project folder
# Example: path = ['./seeds/*.sql', '../project-src/seeds/*-load-testing.sql']
path = ['./seed.sql']

[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv6)
Expand Down
4 changes: 2 additions & 2 deletions pkg/config/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type pathBuilder struct {
FallbackImportMapPath string
FallbackEnvFilePath string
DbTestsDir string
SeedDataPath string
DefaultSeedDataPath string
CustomRolesPath string
}

Expand Down Expand Up @@ -63,7 +63,7 @@ func NewPathBuilder(configPath string) pathBuilder {
FallbackImportMapPath: filepath.Join(base, "functions", "import_map.json"),
FallbackEnvFilePath: filepath.Join(base, "functions", ".env"),
DbTestsDir: filepath.Join(base, "tests"),
SeedDataPath: filepath.Join(base, "seed.sql"),
DefaultSeedDataPath: filepath.Join(base, "seed.sql"),
CustomRolesPath: filepath.Join(base, "roles.sql"),
}
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/migration/seed.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import (

func SeedData(ctx context.Context, pending []string, conn *pgx.Conn, fsys fs.FS) error {
for _, path := range pending {
filename := filepath.Base(path)
fmt.Fprintf(os.Stderr, "Seeding data from %s...\n", filename)
fmt.Fprintf(os.Stderr, "Seeding data from %s...\n", path)
avallete marked this conversation as resolved.
Show resolved Hide resolved
// Batch seed commands, safe to use statement cache
if seed, err := NewMigrationFromFile(path, fsys); err != nil {
return err
Expand Down