Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 72 additions & 4 deletions apps/cli-go/cmd/db.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package cmd

import (
"errors"
"fmt"
"os"
"path"
"path/filepath"

"github.com/spf13/afero"
Expand All @@ -24,6 +26,7 @@ import (
"github.com/supabase/cli/legacy/branch/delete"
"github.com/supabase/cli/legacy/branch/list"
"github.com/supabase/cli/legacy/branch/switch_"
"github.com/supabase/cli/pkg/config"
"github.com/supabase/cli/pkg/migration"
)

Expand Down Expand Up @@ -300,15 +303,23 @@ var (
},
}

noSeed bool
lastVersion uint
noSeed bool
lastVersion uint
seedSqlPaths []string

dbResetCmd = &cobra.Command{
Use: "reset",
Short: "Resets the local database to current migrations",
PreRunE: func(cmd *cobra.Command, args []string) error {
if err := validateDbResetSeedFlags(noSeed, seedSqlPaths); err != nil {
return err
}
warnRemoteResetSeedOverride(cmd, seedSqlPaths)
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if noSeed {
utils.Config.Db.Seed.Enabled = false
if err := applyDbResetSeedFlags(noSeed, seedSqlPaths); err != nil {
return err
}
return reset.Run(cmd.Context(), migrationVersion, lastVersion, flags.DbConfig, afero.NewOsFs())
},
Expand Down Expand Up @@ -470,6 +481,62 @@ func resolvePullDiffEngine(engineFlagChanged bool, engine string, pgDeltaDefault
return pgDeltaDefault
}

func validateDbResetSeedFlags(noSeed bool, patterns []string) error {
if noSeed && len(patterns) > 0 {
utils.CmdSuggestion = fmt.Sprintf("Use either %s to skip seeding or %s to override seed files, not both.", utils.Aqua("--no-seed"), utils.Aqua("--sql-paths"))
return errors.New("--no-seed cannot be used with --sql-paths")
}
for _, pattern := range patterns {
if len(pattern) == 0 {
utils.CmdSuggestion = fmt.Sprintf("Pass a non-empty file path or glob pattern to %s.", utils.Aqua("--sql-paths"))
return errors.New("--sql-paths requires a non-empty path or glob pattern")
}
}
return nil
}

func warnRemoteResetSeedOverride(cmd *cobra.Command, patterns []string) {
if len(patterns) == 0 {
return
}
if cmd.Flags().Changed("linked") || cmd.Flags().Changed("db-url") {
fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "--sql-paths overrides [db.seed].sql_paths and seeds the remote database selected by --linked or --db-url.")
}
}

func applyDbResetSeedFlags(noSeed bool, patterns []string) error {
if noSeed {
utils.Config.Db.Seed.Enabled = false
return nil
}
if len(patterns) == 0 {
return nil
}
resolved, err := resolveSeedSqlPaths(patterns)
if err != nil {
return err
}
utils.Config.Db.Seed.Enabled = true
utils.Config.Db.Seed.SqlPaths = resolved
return nil
}

func resolveSeedSqlPaths(patterns []string) ([]string, error) {
resolved := make([]string, len(patterns))
base := config.NewPathBuilder("").SupabaseDirPath
for i, pattern := range patterns {
if len(pattern) == 0 {
return nil, errors.New("--sql-paths requires a non-empty path or glob pattern")
}
if !filepath.IsAbs(pattern) {
resolved[i] = path.Join(base, pattern)
} else {
resolved[i] = pattern
}
}
return resolved, nil
}

func init() {
// Build branch command
dbBranchCmd.AddCommand(dbBranchCreateCmd)
Expand Down Expand Up @@ -570,6 +637,7 @@ func init() {
resetFlags.Bool("linked", false, "Resets the linked project with local migrations.")
resetFlags.Bool("local", true, "Resets the local database with local migrations.")
resetFlags.BoolVar(&noSeed, "no-seed", false, "Skip running the seed script after reset.")
resetFlags.StringArrayVar(&seedSqlPaths, "sql-paths", nil, "Override [db.seed].sql_paths for this reset. May be repeated; each value accepts a SQL file path or glob pattern relative to the supabase directory and force-enables seeding.")
dbResetCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local")
resetFlags.StringVar(&migrationVersion, "version", "", "Reset up to the specified version.")
resetFlags.UintVar(&lastVersion, "last", 0, "Reset up to the last n migration versions.")
Expand Down
100 changes: 100 additions & 0 deletions apps/cli-go/cmd/db_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package cmd

import (
"path/filepath"
"testing"

"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/internal/utils"
)

func TestResolvePullDiffEngine(t *testing.T) {
Expand Down Expand Up @@ -45,3 +48,100 @@ func TestResolveDiffEngine(t *testing.T) {
assert.False(t, resolveDiffEngine(false, true, false, true))
})
}

func TestResolveSeedSqlPaths(t *testing.T) {
t.Run("resolves relative paths against the supabase directory", func(t *testing.T) {
absoluteSeedPath := filepath.Join(t.TempDir(), "seed.sql")
got, err := resolveSeedSqlPaths([]string{
"./seeds/minimal.sql",
"./seeds/demo/*.sql",
"./seeds/tenant,one.sql",
absoluteSeedPath,
})

assert.NoError(t, err)
assert.Equal(t, []string{
filepath.Join(utils.SupabaseDirPath, "seeds", "minimal.sql"),
filepath.Join(utils.SupabaseDirPath, "seeds", "demo", "*.sql"),
filepath.Join(utils.SupabaseDirPath, "seeds", "tenant,one.sql"),
absoluteSeedPath,
}, got)
})

t.Run("rejects empty paths", func(t *testing.T) {
got, err := resolveSeedSqlPaths([]string{""})
assert.Nil(t, got)
assert.EqualError(t, err, "--sql-paths requires a non-empty path or glob pattern")
})
}

func TestValidateDbResetSeedFlags(t *testing.T) {
t.Run("rejects no seed with sql paths", func(t *testing.T) {
utils.CmdSuggestion = ""
t.Cleanup(func() { utils.CmdSuggestion = "" })

err := validateDbResetSeedFlags(true, []string{"./seed.sql"})

assert.EqualError(t, err, "--no-seed cannot be used with --sql-paths")
assert.Contains(t, utils.CmdSuggestion, "Use either")
assert.Contains(t, utils.CmdSuggestion, "--no-seed")
assert.Contains(t, utils.CmdSuggestion, "--sql-paths")
})

t.Run("rejects empty sql paths", func(t *testing.T) {
utils.CmdSuggestion = ""
t.Cleanup(func() { utils.CmdSuggestion = "" })

err := validateDbResetSeedFlags(false, []string{""})

assert.EqualError(t, err, "--sql-paths requires a non-empty path or glob pattern")
assert.Contains(t, utils.CmdSuggestion, "non-empty")
assert.Contains(t, utils.CmdSuggestion, "--sql-paths")
})
}

func TestApplyDbResetSeedFlags(t *testing.T) {
oldSeed := utils.Config.Db.Seed
t.Cleanup(func() { utils.Config.Db.Seed = oldSeed })

t.Run("leaves config unchanged without seed flags", func(t *testing.T) {
utils.Config.Db.Seed.Enabled = false
utils.Config.Db.Seed.SqlPaths = []string{"supabase/original.sql"}

assert.NoError(t, applyDbResetSeedFlags(false, nil))
assert.False(t, utils.Config.Db.Seed.Enabled)
assert.Equal(t, []string{"supabase/original.sql"}, []string(utils.Config.Db.Seed.SqlPaths))
})

t.Run("disables seed when no seed is set", func(t *testing.T) {
utils.Config.Db.Seed.Enabled = true
utils.Config.Db.Seed.SqlPaths = []string{"supabase/original.sql"}

assert.NoError(t, applyDbResetSeedFlags(true, nil))
assert.False(t, utils.Config.Db.Seed.Enabled)
assert.Equal(t, []string{"supabase/original.sql"}, []string(utils.Config.Db.Seed.SqlPaths))
})

t.Run("force enables seed and overrides sql paths", func(t *testing.T) {
utils.Config.Db.Seed.Enabled = false
utils.Config.Db.Seed.SqlPaths = []string{"supabase/original.sql"}

assert.NoError(t, applyDbResetSeedFlags(false, []string{"./seeds/base.sql"}))
assert.True(t, utils.Config.Db.Seed.Enabled)
assert.Equal(t, []string{filepath.Join(utils.SupabaseDirPath, "seeds", "base.sql")}, []string(utils.Config.Db.Seed.SqlPaths))
})
}

func TestSeedSqlPathsFlagPreservesCommas(t *testing.T) {
var values []string
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
flags.StringArrayVar(&values, "sql-paths", nil, "")

assert.NoError(t, flags.Parse([]string{
"--sql-paths",
"./seeds/tenant,one.sql",
"--sql-paths",
"./seeds/two.sql",
}))
assert.Equal(t, []string{"./seeds/tenant,one.sql", "./seeds/two.sql"}, values)
}
4 changes: 4 additions & 0 deletions apps/cli-go/docs/supabase/db/reset.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ Requires the local development stack to be started by running `supabase start`.

Recreates the local Postgres container and applies all local migrations found in `supabase/migrations` directory. If test data is defined in `supabase/seed.sql`, it will be seeded after the migrations are run. Any other data or schema changes made during local development will be discarded.

Use the `--no-seed` flag to skip seeding entirely. To override `[db.seed].sql_paths` for a single reset, pass one or more `--sql-paths` flags. Each value accepts the same file path or glob pattern syntax as `sql_paths`, relative to the `supabase` directory. Passing `--sql-paths` force-enables seeding for that reset even when `[db.seed].enabled = false`.

When running db reset with `--linked` or `--db-url` flag, a SQL script is executed to identify and drop all user created entities in the remote database. Since Postgres roles are cluster level entities, any custom roles created through the dashboard or `supabase/roles.sql` will not be deleted by remote reset.

If you combine `--sql-paths` with `--linked` or `--db-url`, the override seed files are applied to the selected remote database after migrations. Use this only when you intend to seed that remote target.
2 changes: 1 addition & 1 deletion apps/cli/docs/go-cli-porting-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ Legend:
| `db dump` | `ported` | [`../src/legacy/commands/db/dump/dump.command.ts`](../src/legacy/commands/db/dump/dump.command.ts) |
| `db push` | `wrapped` | [`../src/legacy/commands/db/push/push.command.ts`](../src/legacy/commands/db/push/push.command.ts) |
| `db pull` | `ported` | [`../src/legacy/commands/db/pull/pull.command.ts`](../src/legacy/commands/db/pull/pull.command.ts) — native pg-delta / migra; `--declarative` (deprecated alias `--use-pg-delta`) + `--diff-engine` (migra\|pg-delta); `--experimental` / initial `pg_dump` delegate to Go |
| `db reset` | `wrapped` | [`../src/legacy/commands/db/reset/reset.command.ts`](../src/legacy/commands/db/reset/reset.command.ts) |
| `db reset` | `wrapped` | [`../src/legacy/commands/db/reset/reset.command.ts`](../src/legacy/commands/db/reset/reset.command.ts) — includes Go-parity `--sql-paths` override for `[db.seed].sql_paths` |
| `db lint` | `ported` | [`../src/legacy/commands/db/lint/lint.command.ts`](../src/legacy/commands/db/lint/lint.command.ts) |
| `db start` | `wrapped` | [`../src/legacy/commands/db/start/start.command.ts`](../src/legacy/commands/db/start/start.command.ts) |
| `db query` | `ported` | [`../src/legacy/commands/db/query/query.command.ts`](../src/legacy/commands/db/query/query.command.ts) |
Expand Down
13 changes: 8 additions & 5 deletions apps/cli/src/legacy/commands/db/reset/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

## Files Read

| Path | Format | When |
| -------------------------------- | ---------- | ------------------------------------------------- |
| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` |
| `<workdir>/supabase/migrations/` | directory | always, to load migration files |
| seed files from config | SQL | unless `--no-seed` is set |
| Path | Format | When |
| --------------------------------------- | ---------- | ------------------------------------------------- |
| `~/.supabase/access-token` | plain text | when `SUPABASE_ACCESS_TOKEN` unset and `--linked` |
| `<workdir>/supabase/migrations/` | directory | always, to load migration files |
| seed files from config or `--sql-paths` | SQL | unless `--no-seed` is set |

## Files Written

Expand Down Expand Up @@ -52,6 +52,9 @@ Not applicable.
## Notes

- `--no-seed` skips running the seed script after reset.
- `--sql-paths` overrides `[db.seed].sql_paths` for one reset; repeat it to seed multiple files or glob patterns.
- `--sql-paths` force-enables seeding for that reset even when `[db.seed].enabled = false`.
- With `--linked` or `--db-url`, `--sql-paths` seeds the selected remote database after migrations.
- `--version` resets up to the specified migration version.
- `--last` resets up to the last n migration versions; mutually exclusive with `--version`.
- `--db-url`, `--linked`, and `--local` (default true) are mutually exclusive.
9 changes: 9 additions & 0 deletions apps/cli/src/legacy/commands/db/reset/reset.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";
import { legacyDbReset } from "./reset.handler.ts";

const noSqlPaths: ReadonlyArray<string> = [];

const config = {
dbUrl: Flag.string("db-url").pipe(
Flag.withDescription(
Expand All @@ -18,6 +20,13 @@ const config = {
noSeed: Flag.boolean("no-seed").pipe(
Flag.withDescription("Skip running the seed script after reset."),
),
sqlPaths: Flag.string("sql-paths").pipe(
Flag.atLeast(0),
Comment thread
jgoux marked this conversation as resolved.
Flag.withDescription(
"Override [db.seed].sql_paths for this reset. May be repeated; each value accepts a SQL file path or glob pattern relative to the supabase directory and force-enables seeding.",
),
Flag.withDefault(noSqlPaths),
),
version: Flag.string("version").pipe(
Flag.withDescription("Reset up to the specified version."),
Flag.optional,
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/legacy/commands/db/reset/reset.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const legacyDbReset = Effect.fn("legacy.db.reset")(function* (flags: Lega
if (flags.linked) args.push("--linked");
if (flags.local) args.push("--local");
if (flags.noSeed) args.push("--no-seed");
for (const path of flags.sqlPaths) args.push("--sql-paths", path);
if (Option.isSome(flags.version)) args.push("--version", flags.version.value);
if (Option.isSome(flags.last)) args.push("--last", String(flags.last.value));
yield* proxy.exec(args);
Expand Down
Loading
Loading