Skip to content
43 changes: 35 additions & 8 deletions apps/cli-go/cmd/db_schema_declarative.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
declarativeLocal bool
declarativeReset bool
declarativeApply bool
declarativeNoApply bool
declarativeFile string
declarativeName string

Expand Down Expand Up @@ -102,6 +103,26 @@ func resolveDeclarativeMigrationName(name, file string) string {
return file
}

// resolveDeclarativeSyncShouldApply decides whether to apply the generated migration.
// Precedence: --no-apply > --apply > global --yes > TTY prompt > non-TTY default (skip).
func resolveDeclarativeSyncShouldApply(
applyFlag, noApplyFlag, yesFlag, tty bool,
prompt func() (bool, error),
) (bool, error) {
switch {
case noApplyFlag:
return false, nil
case applyFlag:
return true, nil
case yesFlag:
return true, nil
case tty:
return prompt()
default:
return false, nil
}
}

func ensureLocalDatabaseStarted(ctx context.Context, local bool, isRunning func() error, startDatabase func(context.Context) error) error {
if !local {
return nil
Expand Down Expand Up @@ -360,14 +381,17 @@ func runDeclarativeSync(cmd *cobra.Command, args []string) error {
}

// Step 6: Prompt to apply migration to local DB
shouldApply := declarativeApply
if !shouldApply && isTTY() && !viper.GetBool("YES") {
shouldApply, err = console.PromptYesNo(ctx, "Apply this migration to local database?", true)
if err != nil {
return err
}
} else if viper.GetBool("YES") {
shouldApply = true
shouldApply, err := resolveDeclarativeSyncShouldApply(
declarativeApply,
declarativeNoApply,
viper.GetBool("YES"),
isTTY(),
func() (bool, error) {
return console.PromptYesNo(ctx, "Apply this migration to local database?", true)
},
)
if err != nil {
return err
}

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

generateFlags := dbDeclarativeGenerateCmd.Flags()
generateFlags.BoolVar(&declarativeOverwrite, "overwrite", false, "Overwrite declarative schema files without confirmation.")
Expand Down
99 changes: 99 additions & 0 deletions apps/cli-go/cmd/db_schema_declarative_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,105 @@ func mockFsysWithMigrations() afero.Fs {
return fsys
}

func TestResolveDeclarativeSyncShouldApply(t *testing.T) {
t.Run("no-apply alone returns false without prompting", func(t *testing.T) {
got, err := resolveDeclarativeSyncShouldApply(
false, true, false, true,
func() (bool, error) {
t.Fatal("prompt should not be called")
return false, nil
},
)
require.NoError(t, err)
assert.False(t, got)
})

t.Run("no-apply wins over yes", func(t *testing.T) {
got, err := resolveDeclarativeSyncShouldApply(
false, true, true, false,
func() (bool, error) {
t.Fatal("prompt should not be called")
return false, nil
},
)
require.NoError(t, err)
assert.False(t, got)
})

t.Run("apply alone returns true without prompting", func(t *testing.T) {
got, err := resolveDeclarativeSyncShouldApply(
true, false, false, true,
func() (bool, error) {
t.Fatal("prompt should not be called")
return false, nil
},
)
require.NoError(t, err)
assert.True(t, got)
})

t.Run("TTY without flags prompts", func(t *testing.T) {
prompted := false
got, err := resolveDeclarativeSyncShouldApply(
false, false, false, true,
func() (bool, error) {
prompted = true
return true, nil
},
)
require.NoError(t, err)
assert.True(t, prompted)
assert.True(t, got)
})

t.Run("non-TTY without flags skips apply", func(t *testing.T) {
got, err := resolveDeclarativeSyncShouldApply(
false, false, false, false,
func() (bool, error) {
t.Fatal("prompt should not be called")
return false, nil
},
)
require.NoError(t, err)
assert.False(t, got)
})

t.Run("yes alone on non-TTY applies without prompting", func(t *testing.T) {
got, err := resolveDeclarativeSyncShouldApply(
false, false, true, false,
func() (bool, error) {
t.Fatal("prompt should not be called")
return false, nil
},
)
require.NoError(t, err)
assert.True(t, got)
})

t.Run("yes wins over TTY prompt", func(t *testing.T) {
got, err := resolveDeclarativeSyncShouldApply(
false, false, true, true,
func() (bool, error) {
t.Fatal("prompt should not be called")
return false, nil
},
)
require.NoError(t, err)
assert.True(t, got)
})

t.Run("prompt error propagates", func(t *testing.T) {
expected := errors.New("interrupt")
_, err := resolveDeclarativeSyncShouldApply(
false, false, false, true,
func() (bool, error) {
return false, expected
},
)
assert.ErrorIs(t, err, expected)
})
}

func TestResolveDeclarativeMigrationName(t *testing.T) {
t.Run("prefers explicit name", func(t *testing.T) {
name := resolveDeclarativeMigrationName("custom_name", "fallback_file")
Expand Down
Loading
Loading