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
57 changes: 46 additions & 11 deletions apps/cli-go/cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ var (
usePgAdmin bool
usePgSchema bool
usePgDelta bool
useDeclarative bool
pullDiffEngine = utils.EnumFlag{
Allowed: []string{"migra", "pg-delta"},
Value: "migra",
Expand All @@ -106,7 +107,7 @@ var (
return diff.RunExplicit(cmd.Context(), diffFrom, diffTo, schema, outputPath, afero.NewOsFs())
}
}
useDelta := shouldUsePgDelta()
useDelta := resolveDiffEngine(cmd.Flags().Changed("use-migra"), usePgAdmin, usePgSchema, shouldUsePgDelta())
Comment thread
avallete marked this conversation as resolved.
if usePgAdmin {
return diff.RunPgAdmin(cmd.Context(), schema, file, flags.DbConfig, afero.NewOsFs())
}
Expand Down Expand Up @@ -176,12 +177,19 @@ var (
if len(args) > 0 {
name = args[0]
}
// Declarative export is opt-in via --declarative. Enabling pg-delta in config
// does not switch db pull to declarative output; it keeps the migration-file
// workflow and only defaults the shadow diff engine below.
useDeclarativePgDelta := useDeclarative
usePgDeltaDiff := resolvePullDiffEngine(
cmd.Flags().Changed("diff-engine"),
pullDiffEngine.Value,
shouldUsePgDelta(),
)
pullDiffer := diff.DiffSchemaMigra
usePgDeltaDiff := pullDiffEngine.Value == "pg-delta"
if usePgDeltaDiff {
pullDiffer = diff.DiffPgDelta
}
useDeclarativePgDelta := shouldUseDeclarativePgDeltaPull(usePgDeltaDiff)
return pull.Run(cmd.Context(), schema, flags.DbConfig, name, useDeclarativePgDelta, usePgDeltaDiff, pullDiffer, afero.NewOsFs())
},
PostRun: func(cmd *cobra.Command, args []string) {
Expand Down Expand Up @@ -210,8 +218,15 @@ var (
Use: "commit",
Short: "Commit remote changes as a new migration",
RunE: func(cmd *cobra.Command, args []string) error {
useDelta := shouldUsePgDelta()
return pull.Run(cmd.Context(), schema, flags.DbConfig, "remote_commit", useDelta, false, diff.DiffSchemaMigra, afero.NewOsFs())
// remote commit always writes a timestamped migration file. When pg-delta is
// enabled it only swaps the shadow diff engine; it never switches to the
// declarative export path.
usePgDeltaDiff := shouldUsePgDelta()
pullDiffer := diff.DiffSchemaMigra
if usePgDeltaDiff {
pullDiffer = diff.DiffPgDelta
}
return pull.Run(cmd.Context(), schema, flags.DbConfig, "remote_commit", false, usePgDeltaDiff, pullDiffer, afero.NewOsFs())
},
}

Expand Down Expand Up @@ -361,11 +376,28 @@ func shouldUsePgDelta() bool {
return utils.IsPgDeltaEnabled() || usePgDelta || viper.GetBool("EXPERIMENTAL_PG_DELTA")
}

func shouldUseDeclarativePgDeltaPull(usePgDeltaDiff bool) bool {
if usePgDeltaDiff {
// resolveDiffEngine reports whether `db diff` should run in pg-delta mode. The config /
// env default (pgDeltaDefault) applies unless an explicit non-pg-delta engine is selected:
// --use-migra, --use-pgadmin, or --use-pg-schema is an authoritative rollback that clears
// pg-delta mode so diff.Run skips pg-delta-specific declarative shadow setup and the
// PGDELTA_DEBUG capture path. --use-migra defaults to true, so only an explicit pass
// (useMigraChanged) counts as opting out.
func resolveDiffEngine(useMigraChanged, usePgAdmin, usePgSchema, pgDeltaDefault bool) bool {
if useMigraChanged || usePgAdmin || usePgSchema {
return false
}
return shouldUsePgDelta()
return pgDeltaDefault
}

// resolvePullDiffEngine selects whether migration-style db pull uses pg-delta for the
// shadow diff step. An explicit --diff-engine flag always wins, so --diff-engine migra is
// an authoritative rollback even when pg-delta is enabled in config; otherwise the default
// follows whether pg-delta is the active engine (config / env).
func resolvePullDiffEngine(engineFlagChanged bool, engine string, pgDeltaDefault bool) bool {
if engineFlagChanged {
return engine == "pg-delta"
}
return pgDeltaDefault
}

func init() {
Expand Down Expand Up @@ -427,15 +459,18 @@ func init() {
dbCmd.AddCommand(dbPushCmd)
// Build pull command
pullFlags := dbPullCmd.Flags()
// This flag activates declarative pull output through pg-delta instead of the
// legacy migration SQL pull path.
pullFlags.BoolVar(&usePgDelta, "use-pg-delta", false, "Use pg-delta to pull declarative schema.")
// --declarative switches pull output from a timestamped migration to declarative
// schema files exported through pg-delta. --use-pg-delta is the deprecated alias.
pullFlags.BoolVar(&useDeclarative, "declarative", false, "Pull schema as declarative files using pg-delta instead of creating a migration.")
pullFlags.BoolVar(&useDeclarative, "use-pg-delta", false, "Use pg-delta to pull declarative schema.")
cobra.CheckErr(pullFlags.MarkDeprecated("use-pg-delta", "use --declarative with [experimental.pgdelta] enabled = true in your config.toml instead."))
pullFlags.Var(&pullDiffEngine, "diff-engine", "Diff engine to use for migration-style db pull.")
pullFlags.StringSliceVarP(&schema, "schema", "s", []string{}, "Comma separated list of schema to include.")
pullFlags.String("db-url", "", "Pulls from the database specified by the connection string (must be percent-encoded).")
pullFlags.Bool("linked", true, "Pulls from the linked project.")
pullFlags.Bool("local", false, "Pulls from the local database.")
dbPullCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local")
dbPullCmd.MarkFlagsMutuallyExclusive("declarative", "diff-engine")
dbPullCmd.MarkFlagsMutuallyExclusive("use-pg-delta", "diff-engine")
pullFlags.StringVarP(&dbPassword, "password", "p", "", "Password to your remote Postgres database.")
cobra.CheckErr(viper.BindPFlag("DB_PASSWORD", pullFlags.Lookup("password")))
Expand Down
38 changes: 0 additions & 38 deletions apps/cli-go/cmd/db_pull_routing_test.go

This file was deleted.

47 changes: 47 additions & 0 deletions apps/cli-go/cmd/db_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cmd

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestResolvePullDiffEngine(t *testing.T) {
t.Run("defaults to pg-delta when enabled in config", func(t *testing.T) {
assert.True(t, resolvePullDiffEngine(false, "migra", true))
})

t.Run("defaults to migra when pg-delta is not active", func(t *testing.T) {
assert.False(t, resolvePullDiffEngine(false, "migra", false))
})

t.Run("explicit --diff-engine migra overrides config default", func(t *testing.T) {
assert.False(t, resolvePullDiffEngine(true, "migra", true))
})

t.Run("explicit --diff-engine pg-delta wins when config disabled", func(t *testing.T) {
assert.True(t, resolvePullDiffEngine(true, "pg-delta", false))
})
}

func TestResolveDiffEngine(t *testing.T) {
t.Run("uses pg-delta when enabled in config and no engine flag set", func(t *testing.T) {
assert.True(t, resolveDiffEngine(false, false, false, true))
})

t.Run("uses migra when pg-delta is not active", func(t *testing.T) {
assert.False(t, resolveDiffEngine(false, false, false, false))
})

t.Run("explicit --use-migra clears config-driven pg-delta", func(t *testing.T) {
assert.False(t, resolveDiffEngine(true, false, false, true))
})

t.Run("explicit --use-pg-schema clears config-driven pg-delta", func(t *testing.T) {
assert.False(t, resolveDiffEngine(false, false, true, true))
})

t.Run("explicit --use-pgadmin clears config-driven pg-delta", func(t *testing.T) {
assert.False(t, resolveDiffEngine(false, true, false, true))
})
}
2 changes: 1 addition & 1 deletion apps/cli-go/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var (
initInteractive bool
createVscodeSettings bool
createIntellijSettings bool
initParams = utils.InitParams{}
initParams = utils.InitParams{UsePgDelta: true}

initCmd = &cobra.Command{
GroupID: groupLocalDev,
Expand Down
2 changes: 2 additions & 0 deletions apps/cli-go/docs/supabase/db/diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Runs [djrobstep/migra](https://github.com/djrobstep/migra) in a container to com

By default, all schemas in the target database are diffed. Use the `--schema public,extensions` flag to restrict diffing to a subset of schemas.

Projects created by a recent `supabase init` default to the pg-delta diff engine (`[experimental.pgdelta] enabled = true` in `config.toml`). Existing projects are unaffected and keep using migra unless they opt in. To fall back to the legacy migra engine, set `enabled = false` under `[experimental.pgdelta]`, or pass `--use-migra` for a single run.

While the diff command is able to capture most schema changes, there are cases where it is known to fail. Currently, this could happen if you schema contains:

- Changes to publication
Expand Down
4 changes: 2 additions & 2 deletions apps/cli-go/docs/supabase/db/pull.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Optionally, a new row can be inserted into the migration history table to reflec

If no entries exist in the migration history table, the default diff engine uses `pg_dump` to capture all contents of the remote schemas you have created. Otherwise, this command will only diff schema changes against the remote database, similar to running `db diff --linked`.

Pass `--diff-engine pg-delta` to keep the migration-file `db pull` workflow while using pg-delta for the shadow diff step. On initial pull, pg-delta replaces `pg_dump` and produces the full migration from the shadow diff alone. Pass `--use-pg-delta` to switch to the declarative pg-delta export workflow instead.
Pass `--diff-engine pg-delta` to keep the migration-file `db pull` workflow while using pg-delta for the shadow diff step. On initial pull, pg-delta replaces `pg_dump` and produces the full migration from the shadow diff alone. Pass `--declarative` to switch to the declarative pg-delta export workflow instead.

When `[experimental.pgdelta] enabled = true` is set in `config.toml`, `db pull` defaults to the declarative export path. Explicit `--diff-engine pg-delta` still selects the migration-file workflow.
When `[experimental.pgdelta] enabled = true` (the default for projects created by a recent `supabase init`), the migration-file `db pull` workflow uses pg-delta for the shadow diff step by default; it does not switch to declarative output. Existing projects without the section are unaffected and keep using migra. To fall back to the legacy migra engine, set `enabled = false` under `[experimental.pgdelta]`, or pass `--diff-engine migra` for a single run.

When pulling from a remote database with `--db-url`, prefer a direct connection (`db.<project-ref>.supabase.co:5432`) over the connection pooler so pg-delta can introspect the full catalog reliably.

Expand Down
2 changes: 1 addition & 1 deletion apps/cli-go/internal/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func Run(ctx context.Context, starter StarterTemplate, fsys afero.Fs, options ..
if err := downloadSample(ctx, client, starter.Url, fsys); err != nil {
return err
}
} else if err := initBlank.Run(ctx, fsys, false, utils.InitParams{Overwrite: true}); err != nil {
} else if err := initBlank.Run(ctx, fsys, false, utils.InitParams{Overwrite: true, UsePgDelta: true}); err != nil {
return err
}
// 1. Login
Expand Down
5 changes: 5 additions & 0 deletions apps/cli-go/internal/utils/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ func ToRealtimeEnv(addr config.AddressFamily) string {
type InitParams struct {
ProjectId string
UseOrioleDB bool
UsePgDelta bool
Overwrite bool
}

Expand All @@ -225,6 +226,10 @@ func InitConfig(params InitParams, fsys afero.Fs) error {
if params.UseOrioleDB {
c.Experimental.OrioleDBVersion = "15.1.0.150"
}
// The supabase init command opts new projects into pg-delta. Existing configs are
// unaffected because mergeDefaultValues ejects with this flag false (default stays
// migra), and other InitConfig callers leave it disabled.
c.Experimental.PgDeltaInitEnabled = params.UsePgDelta
// Create config file
if err := MkdirIfNotExistFS(fsys, SupabaseDirPath); err != nil {
return err
Expand Down
29 changes: 29 additions & 0 deletions apps/cli-go/internal/utils/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,35 @@ func TestInitConfig(t *testing.T) {
assert.True(t, exists)
})

t.Run("generated config enables pgdelta when requested", func(t *testing.T) {
fsys := afero.NewMemMapFs()
params := InitParams{
ProjectId: "test-project",
UsePgDelta: true,
}

err := InitConfig(params, fsys)

require.NoError(t, err)
content, err := afero.ReadFile(fsys, ConfigPath)
require.NoError(t, err)
assert.Contains(t, string(content), "[experimental.pgdelta]\nenabled = true")
})

t.Run("generated config leaves pgdelta disabled by default", func(t *testing.T) {
fsys := afero.NewMemMapFs()
params := InitParams{
ProjectId: "test-project",
}

err := InitConfig(params, fsys)

require.NoError(t, err)
content, err := afero.ReadFile(fsys, ConfigPath)
require.NoError(t, err)
assert.Contains(t, string(content), "[experimental.pgdelta]\nenabled = false")
})

t.Run("creates config with orioledb", func(t *testing.T) {
fsys := afero.NewMemMapFs()
params := InitParams{
Expand Down
5 changes: 5 additions & 0 deletions apps/cli-go/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ type (
Webhooks *webhooks `toml:"webhooks" json:"webhooks"`
PgDelta *PgDeltaConfig `toml:"pgdelta" json:"pgdelta"`
Inspect inspect `toml:"inspect" json:"inspect"`
// PgDeltaInitEnabled drives the [experimental.pgdelta] enabled value rendered
// by Eject. It is true only for the supabase init scaffold so freshly generated
// projects opt into pg-delta, and false when Eject feeds mergeDefaultValues so
// existing configs without the section keep resolving to migra (non-breaking).
PgDeltaInitEnabled bool `toml:"-" json:"-"`
}
)

Expand Down
43 changes: 43 additions & 0 deletions apps/cli-go/pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,49 @@ format_options = "not-json"
err := config.Load("", fsys)
assert.ErrorContains(t, err, "experimental.pgdelta.format_options")
})

t.Run("init scaffold opts into pgdelta", func(t *testing.T) {
config := NewConfig()
// supabase init renders the scaffold with the pg-delta opt-in flag set
config.Experimental.PgDeltaInitEnabled = true
var buf bytes.Buffer
require.NoError(t, config.Eject(&buf))
fsys := fs.MapFS{"supabase/config.toml": &fs.MapFile{Data: buf.Bytes()}}
// Reloading the generated config resolves to pg-delta
require.NoError(t, config.Load("", fsys))
require.NotNil(t, config.Experimental.PgDelta)
assert.True(t, config.Experimental.PgDelta.Enabled)
})

t.Run("absent pgdelta section falls back to migra", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
[experimental]
orioledb_version = ""
`)},
}

// The default ejected by mergeDefaultValues keeps pg-delta disabled, so a config
// without the section resolves to migra (PgDelta is non-nil only for version pinning).
require.NoError(t, config.Load("", fsys))
require.NotNil(t, config.Experimental.PgDelta)
assert.False(t, config.Experimental.PgDelta.Enabled)
})

t.Run("explicit enabled false restores migra", func(t *testing.T) {
config := NewConfig()
fsys := fs.MapFS{
"supabase/config.toml": &fs.MapFile{Data: []byte(`
[experimental.pgdelta]
enabled = false
`)},
}

require.NoError(t, config.Load("", fsys))
require.NotNil(t, config.Experimental.PgDelta)
assert.False(t, config.Experimental.PgDelta.Enabled)
})
}

func TestPgDeltaNpmVersionPinning(t *testing.T) {
Expand Down
7 changes: 4 additions & 3 deletions apps/cli-go/pkg/config/templates/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,10 @@ s3_access_key = "env(S3_ACCESS_KEY)"
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = "env(S3_SECRET_KEY)"

# [experimental.pgdelta]
# When enabled, pg-delta becomes the active engine for supported schema flows.
# enabled = false
# pg-delta is the schema diff engine for db diff / db pull / db remote commit.
# Set enabled = false to fall back to the legacy migra engine.
[experimental.pgdelta]
enabled = {{ .Experimental.PgDeltaInitEnabled }}
# Directory under `supabase/` where declarative files are written.
# declarative_schema_path = "./database"
# JSON string passed through to pg-delta SQL formatting.
Expand Down
Loading
Loading