Skip to content

Commit 46cc858

Browse files
feat(cli): Add --no-apply flag for db schema declarative sync command (#5220)
## Summary This change introduces a new flag called `no-apply` to the `db schema declarative sync command` , allowing users to generate migration files without applying them to the local database. ## Current behavior ``` $ supabase db schema declarative sync --name test ... Created new migration at supabase/migrations/20260428093833_test.sql Apply this migration to local database? [Y/n] ``` The only escape hatches today: - `--apply` — force-applies the migration (opposite of what's wanted) - Closing stdin via piping, e.g. `true | supabase db schema declarative sync ... 2>&1 | cat` Closes #5218 ## New behavior - If `--no-apply` is set, the command writes the migration file and skips the apply step without any prompt - `--no-apply` overrides global `--yes` and cannot be combined with `--apply`. - Parity with legacy TS is maintained --------- Co-authored-by: Andrew Valleteau <avallete@users.noreply.github.com>
1 parent 72f03fa commit 46cc858

6 files changed

Lines changed: 251 additions & 116 deletions

File tree

apps/cli-go/cmd/db_schema_declarative.go

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ var (
3232
declarativeLocal bool
3333
declarativeReset bool
3434
declarativeApply bool
35+
declarativeNoApply bool
3536
declarativeFile string
3637
declarativeName string
3738

@@ -102,6 +103,26 @@ func resolveDeclarativeMigrationName(name, file string) string {
102103
return file
103104
}
104105

106+
// resolveDeclarativeSyncShouldApply decides whether to apply the generated migration.
107+
// Precedence: --no-apply > --apply > global --yes > TTY prompt > non-TTY default (skip).
108+
func resolveDeclarativeSyncShouldApply(
109+
applyFlag, noApplyFlag, yesFlag, tty bool,
110+
prompt func() (bool, error),
111+
) (bool, error) {
112+
switch {
113+
case noApplyFlag:
114+
return false, nil
115+
case applyFlag:
116+
return true, nil
117+
case yesFlag:
118+
return true, nil
119+
case tty:
120+
return prompt()
121+
default:
122+
return false, nil
123+
}
124+
}
125+
105126
func ensureLocalDatabaseStarted(ctx context.Context, local bool, isRunning func() error, startDatabase func(context.Context) error) error {
106127
if !local {
107128
return nil
@@ -360,14 +381,17 @@ func runDeclarativeSync(cmd *cobra.Command, args []string) error {
360381
}
361382

362383
// Step 6: Prompt to apply migration to local DB
363-
shouldApply := declarativeApply
364-
if !shouldApply && isTTY() && !viper.GetBool("YES") {
365-
shouldApply, err = console.PromptYesNo(ctx, "Apply this migration to local database?", true)
366-
if err != nil {
367-
return err
368-
}
369-
} else if viper.GetBool("YES") {
370-
shouldApply = true
384+
shouldApply, err := resolveDeclarativeSyncShouldApply(
385+
declarativeApply,
386+
declarativeNoApply,
387+
viper.GetBool("YES"),
388+
isTTY(),
389+
func() (bool, error) {
390+
return console.PromptYesNo(ctx, "Apply this migration to local database?", true)
391+
},
392+
)
393+
if err != nil {
394+
return err
371395
}
372396

373397
if shouldApply {
@@ -461,6 +485,9 @@ func init() {
461485
syncFlags.StringVarP(&declarativeFile, "file", "f", defaultDeclarativeSyncName, "Saves schema diff to a new migration file.")
462486
syncFlags.StringVar(&declarativeName, "name", "", "Name for the generated migration file.")
463487
syncFlags.BoolVar(&declarativeApply, "apply", false, "Apply the generated migration to the local database without prompting.")
488+
syncFlags.BoolVar(&declarativeNoApply, "no-apply", false,
489+
"Generate the migration file without prompting or applying it to the local database.")
490+
dbDeclarativeSyncCmd.MarkFlagsMutuallyExclusive("apply", "no-apply")
464491

465492
generateFlags := dbDeclarativeGenerateCmd.Flags()
466493
generateFlags.BoolVar(&declarativeOverwrite, "overwrite", false, "Overwrite declarative schema files without confirmation.")

apps/cli-go/cmd/db_schema_declarative_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,105 @@ func mockFsysWithMigrations() afero.Fs {
3131
return fsys
3232
}
3333

34+
func TestResolveDeclarativeSyncShouldApply(t *testing.T) {
35+
t.Run("no-apply alone returns false without prompting", func(t *testing.T) {
36+
got, err := resolveDeclarativeSyncShouldApply(
37+
false, true, false, true,
38+
func() (bool, error) {
39+
t.Fatal("prompt should not be called")
40+
return false, nil
41+
},
42+
)
43+
require.NoError(t, err)
44+
assert.False(t, got)
45+
})
46+
47+
t.Run("no-apply wins over yes", func(t *testing.T) {
48+
got, err := resolveDeclarativeSyncShouldApply(
49+
false, true, true, false,
50+
func() (bool, error) {
51+
t.Fatal("prompt should not be called")
52+
return false, nil
53+
},
54+
)
55+
require.NoError(t, err)
56+
assert.False(t, got)
57+
})
58+
59+
t.Run("apply alone returns true without prompting", func(t *testing.T) {
60+
got, err := resolveDeclarativeSyncShouldApply(
61+
true, false, false, true,
62+
func() (bool, error) {
63+
t.Fatal("prompt should not be called")
64+
return false, nil
65+
},
66+
)
67+
require.NoError(t, err)
68+
assert.True(t, got)
69+
})
70+
71+
t.Run("TTY without flags prompts", func(t *testing.T) {
72+
prompted := false
73+
got, err := resolveDeclarativeSyncShouldApply(
74+
false, false, false, true,
75+
func() (bool, error) {
76+
prompted = true
77+
return true, nil
78+
},
79+
)
80+
require.NoError(t, err)
81+
assert.True(t, prompted)
82+
assert.True(t, got)
83+
})
84+
85+
t.Run("non-TTY without flags skips apply", func(t *testing.T) {
86+
got, err := resolveDeclarativeSyncShouldApply(
87+
false, false, false, false,
88+
func() (bool, error) {
89+
t.Fatal("prompt should not be called")
90+
return false, nil
91+
},
92+
)
93+
require.NoError(t, err)
94+
assert.False(t, got)
95+
})
96+
97+
t.Run("yes alone on non-TTY applies without prompting", func(t *testing.T) {
98+
got, err := resolveDeclarativeSyncShouldApply(
99+
false, false, true, false,
100+
func() (bool, error) {
101+
t.Fatal("prompt should not be called")
102+
return false, nil
103+
},
104+
)
105+
require.NoError(t, err)
106+
assert.True(t, got)
107+
})
108+
109+
t.Run("yes wins over TTY prompt", func(t *testing.T) {
110+
got, err := resolveDeclarativeSyncShouldApply(
111+
false, false, true, true,
112+
func() (bool, error) {
113+
t.Fatal("prompt should not be called")
114+
return false, nil
115+
},
116+
)
117+
require.NoError(t, err)
118+
assert.True(t, got)
119+
})
120+
121+
t.Run("prompt error propagates", func(t *testing.T) {
122+
expected := errors.New("interrupt")
123+
_, err := resolveDeclarativeSyncShouldApply(
124+
false, false, false, true,
125+
func() (bool, error) {
126+
return false, expected
127+
},
128+
)
129+
assert.ErrorIs(t, err, expected)
130+
})
131+
}
132+
34133
func TestResolveDeclarativeMigrationName(t *testing.T) {
35134
t.Run("prefers explicit name", func(t *testing.T) {
36135
name := resolveDeclarativeMigrationName("custom_name", "fallback_file")

0 commit comments

Comments
 (0)