Skip to content

Commit 8e4aef4

Browse files
DiegoDAFclaudediego-feito-storij-bennet
authored
Add support for -y/--yes flag to bypass confirmation prompts (#1544)
* Add support for -y/--yes flag to bypass confirmation prompts This commit adds support for the -y/--yes option to pgcli, allowing users to bypass destructive command confirmation prompts. This is particularly useful for automated scripts and CI/CD pipelines. Features: - Single flag to skip all destructive confirmations: pgcli -y - Long form: pgcli --yes - Useful for automated environments - Maintains safety by default (flag must be explicitly set) - Does not override transaction requirements When the flag is set: - Destructive commands execute without prompting - Transaction requirements still apply - Error handling remains the same Comprehensive unit tests included to verify: - Flag initialization - Confirmation bypass behavior - Normal confirmation flow without flag Made with ❤️ and 🤖 Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Suppress 'Your call!' message when using --yes flag When using the --yes flag to auto-confirm destructive commands, pgcli should not display the "Your call!" message since no user interaction occurred. This message is now only shown when the user manually confirms the destructive warning prompt. Made with ❤️ and 🤖 Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Pass force_destructive into confirm_destructive_query Address review feedback: instead of checking self.force_destructive at both call sites, confirm_destructive_query now takes a `force` parameter and owns the whole 'should we proceed?' decision. Behaviour is unchanged: non-destructive returns None, force returns True, a non-tty stdin returns None (no prompt), otherwise the user is prompted. The parameter defaults to False, so existing callers are unaffected. * Assert the user is not prompted, not which helper is called test_force_destructive_skips_confirmation asserted that confirm_destructive_query was never called with -y. Now that the helper takes the force parameter it IS called and decides internally not to prompt, so the old assertion encoded the previous structure rather than the behaviour. It now patches prompt_utils.confirm and asserts the user is never prompted, which is the actual guarantee -y makes. --------- Co-authored-by: DiegoDAF <DiegoDAF@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Diego - pgDBA <151667121+diego-feito-stori@users.noreply.github.com> Co-authored-by: Irina Truong <637013+j-bennet@users.noreply.github.com>
1 parent c817053 commit 8e4aef4

4 files changed

Lines changed: 81 additions & 7 deletions

File tree

‎changelog.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Upcoming
33

44
Features:
55
---------
6+
* Add support for forcing destructive commands without confirmation.
7+
* Command line option `-y` or `--yes`.
8+
* Skips the destructive command confirmation prompt when enabled.
9+
* Useful for automated scripts and CI/CD pipelines.
610
* Add a ``-t``/``--tuples-only`` command line option that prints the rows and
711
nothing else, matching psql: no column headers, no title, no status footer
812
and no timing line. The configured table format is left untouched, so ``\T``

‎pgcli/main.py‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,14 @@ def __init__(
231231
ssh_tunnel_url: str | None = None,
232232
connect_timeout: int | None = None,
233233
log_file: str | None = None,
234+
force_destructive: bool = False,
234235
):
235236
self.force_passwd_prompt = force_passwd_prompt
236237
self.never_passwd_prompt = never_passwd_prompt
237238
self.pgexecute = pgexecute
238239
self.dsn_alias = None
239240
self.watch_command = None
241+
self.force_destructive = force_destructive
240242

241243
# Load config.
242244
c = self.config = get_config(pgclirc_file)
@@ -596,7 +598,7 @@ def execute_from_file(self, pattern, **_):
596598
):
597599
message = "Destructive statements must be run within a transaction. Command execution stopped."
598600
return [(None, None, None, message)]
599-
destroy = confirm_destructive_query(query, self.destructive_warning, self.dsn_alias)
601+
destroy = confirm_destructive_query(query, self.destructive_warning, self.dsn_alias, self.force_destructive)
600602
if destroy is False:
601603
message = "Wise choice. Command execution stopped."
602604
return [(None, None, None, message)]
@@ -920,11 +922,11 @@ def execute_command(self, text, handle_closed_connection=True):
920922
):
921923
click.secho("Destructive statements must be run within a transaction.")
922924
raise KeyboardInterrupt
923-
destroy = confirm_destructive_query(text, self.destructive_warning, self.dsn_alias)
925+
destroy = confirm_destructive_query(text, self.destructive_warning, self.dsn_alias, self.force_destructive)
924926
if destroy is False:
925927
click.secho("Wise choice!")
926928
raise KeyboardInterrupt
927-
elif destroy:
929+
elif destroy and not self.force_destructive:
928930
click.secho("Your call!")
929931

930932
output, query = self._evaluate_command(text)
@@ -1665,6 +1667,14 @@ def echo_via_pager(self, text, color=None):
16651667
type=str,
16661668
help="SQL statement to execute after connecting.",
16671669
)
1670+
@click.option(
1671+
"-y",
1672+
"--yes",
1673+
"force_destructive",
1674+
is_flag=True,
1675+
default=False,
1676+
help="Force destructive commands without confirmation prompt.",
1677+
)
16681678
@click.option(
16691679
"-f",
16701680
"--file",
@@ -1702,6 +1712,7 @@ def cli(
17021712
ssh_tunnel: str,
17031713
init_command: str,
17041714
log_file: str,
1715+
force_destructive: bool,
17051716
input_files: tuple,
17061717
connect_timeout: int | None,
17071718
):
@@ -1768,6 +1779,7 @@ def cli(
17681779
warn=warn,
17691780
ssh_tunnel_url=ssh_tunnel,
17701781
log_file=log_file,
1782+
force_destructive=force_destructive,
17711783
connect_timeout=connect_timeout,
17721784
)
17731785

‎pgcli/packages/prompt_utils.py‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,31 @@
33
from .parseutils import is_destructive
44

55

6-
def confirm_destructive_query(queries, keywords, alias):
6+
def confirm_destructive_query(queries, keywords, alias, force=False):
77
"""Check if the query is destructive and prompts the user to confirm.
88
99
Returns:
1010
* None if the query is non-destructive or we can't prompt the user.
11-
* True if the query is destructive and the user wants to proceed.
11+
* True if the query is destructive and the user wants to proceed, or if the
12+
confirmation was bypassed with `force`.
1213
* False if the query is destructive and the user doesn't want to proceed.
1314
1415
"""
16+
if not is_destructive(queries, keywords):
17+
return None
18+
19+
if force:
20+
return True
21+
22+
if not sys.stdin.isatty():
23+
return None
24+
1525
info = "You're about to run a destructive command"
1626
if alias:
1727
info += f" in {click.style(alias, fg='red')}"
1828

1929
prompt_text = f"{info}.\nDo you want to proceed?"
20-
if is_destructive(queries, keywords) and sys.stdin.isatty():
21-
return confirm(prompt_text)
30+
return confirm(prompt_text)
2231

2332

2433
def confirm(*args, **kwargs):

‎tests/test_main.py‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,55 @@ def test_notifications(executor):
778778
mock_secho.assert_not_called()
779779

780780

781+
def test_force_destructive_flag():
782+
"""Test that PGCli can be initialized with force_destructive flag."""
783+
cli = PGCli(force_destructive=True)
784+
assert cli.force_destructive is True
785+
786+
cli = PGCli(force_destructive=False)
787+
assert cli.force_destructive is False
788+
789+
cli = PGCli()
790+
assert cli.force_destructive is False
791+
792+
793+
@dbtest
794+
def test_force_destructive_skips_confirmation(executor):
795+
"""Test that force_destructive=True skips confirmation for destructive commands."""
796+
cli = PGCli(pgexecute=executor, force_destructive=True)
797+
cli.destructive_warning = ["drop", "alter"]
798+
799+
# The proceed/abort decision lives inside confirm_destructive_query, which is
800+
# told to force; what must not happen is the user being prompted.
801+
with mock.patch("pgcli.packages.prompt_utils.confirm") as mock_prompt:
802+
# Execute a destructive command
803+
result = cli.execute_command("ALTER TABLE test_table ADD COLUMN test_col TEXT;")
804+
805+
# Verify that the user was never prompted
806+
mock_prompt.assert_not_called()
807+
808+
# Verify that the command was attempted (even if it fails due to missing table)
809+
assert result is not None
810+
811+
812+
@dbtest
813+
def test_without_force_destructive_calls_confirmation(executor):
814+
"""Test that without force_destructive, confirmation is called for destructive commands."""
815+
cli = PGCli(pgexecute=executor, force_destructive=False)
816+
cli.destructive_warning = ["drop", "alter"]
817+
818+
# Mock confirm_destructive_query to return True (user confirms)
819+
with mock.patch("pgcli.main.confirm_destructive_query", return_value=True) as mock_confirm:
820+
# Execute a destructive command
821+
result = cli.execute_command("ALTER TABLE test_table ADD COLUMN test_col TEXT;")
822+
823+
# Verify that confirm_destructive_query WAS called
824+
mock_confirm.assert_called_once()
825+
826+
# Verify that the command was attempted
827+
assert result is not None
828+
829+
781830
def test_edit_named_query():
782831
"""Test \\ne edits/creates a named query via the external editor."""
783832
from pgspecial.namedqueries import NamedQueries

0 commit comments

Comments
 (0)