Skip to content

Commit 1726ff5

Browse files
authored
Changed destructive_warning to take a list of destructive commands (#1328)
* Changed destructive_warning to take a list of destructive commands and added the dsn_alias as part of the destructive command warning * Updated parse_destructive_warning to handle None * Reverted auto formatted change to AUTHORS * Reverted auto formatted change to AUTHORS
1 parent c280f8e commit 1726ff5

File tree

10 files changed

+129
-51
lines changed

10 files changed

+129
-51
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ Contributors:
123123
* Daniel Kukula (dkuku)
124124
* Kian-Meng Ang (kianmeng)
125125
* Liu Zhao (astroshot)
126+
* Rigo Neri (rigoneri)
126127

127128
Creator:
128129
--------

DEVELOP.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,9 @@ in the ``tests`` directory. An example::
165165
First, install the requirements for testing:
166166

167167
::
168-
169-
$ pip install -r requirements-dev.txt
168+
$ pip install -U pip setuptools
169+
$ pip install --no-cache-dir ".[sshtunnel]"
170+
$ pip install -r requirements-dev.txt
170171

171172
Ensure that the database user has permissions to create and drop test databases
172173
by checking your ``pg_hba.conf`` file. The default user should be ``postgres``

changelog.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
Upcoming
2+
========
3+
4+
Features:
5+
---------
6+
7+
* Changed the `destructive_warning` config to be a list of commands that are considered
8+
destructive. This would allow you to be warned on `create`, `grant`, or `insert` queries.
9+
* Destructive warnings will now include the alias dsn connection string name if provided (-D option).
10+
111
3.5.0 (2022/09/15):
212
===================
313

pgcli/main.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
from .key_bindings import pgcli_bindings
6565
from .packages.formatter.sqlformatter import register_new_formatter
6666
from .packages.prompt_utils import confirm_destructive_query
67+
from .packages.parseutils import parse_destructive_warning
6768
from .__init__ import __version__
6869

6970
click.disable_unicode_literals_warning = True
@@ -224,11 +225,10 @@ def __init__(
224225
self.syntax_style = c["main"]["syntax_style"]
225226
self.cli_style = c["colors"]
226227
self.wider_completion_menu = c["main"].as_bool("wider_completion_menu")
227-
self.destructive_warning = warn or c["main"]["destructive_warning"]
228-
# also handle boolean format of destructive warning
229-
self.destructive_warning = {"true": "all", "false": "off"}.get(
230-
self.destructive_warning.lower(), self.destructive_warning
228+
self.destructive_warning = parse_destructive_warning(
229+
warn or c["main"].as_list("destructive_warning")
231230
)
231+
232232
self.less_chatty = bool(less_chatty) or c["main"].as_bool("less_chatty")
233233
self.null_string = c["main"].get("null_string", "<null>")
234234
self.prompt_format = (
@@ -424,8 +424,11 @@ def execute_from_file(self, pattern, **_):
424424
return [(None, None, None, str(e), "", False, True)]
425425

426426
if (
427-
self.destructive_warning != "off"
428-
and confirm_destructive_query(query, self.destructive_warning) is False
427+
self.destructive_warning
428+
and confirm_destructive_query(
429+
query, self.destructive_warning, self.dsn_alias
430+
)
431+
is False
429432
):
430433
message = "Wise choice. Command execution stopped."
431434
return [(None, None, None, message)]
@@ -693,15 +696,16 @@ def execute_command(self, text):
693696
query = MetaQuery(query=text, successful=False)
694697

695698
try:
696-
if self.destructive_warning != "off":
699+
if self.destructive_warning:
697700
destroy = confirm = confirm_destructive_query(
698-
text, self.destructive_warning
701+
text, self.destructive_warning, self.dsn_alias
699702
)
700703
if destroy is False:
701704
click.secho("Wise choice!")
702705
raise KeyboardInterrupt
703706
elif destroy:
704707
click.secho("Your call!")
708+
705709
output, query = self._evaluate_command(text)
706710
except KeyboardInterrupt:
707711
# Restart connection to the database
@@ -1266,7 +1270,6 @@ def echo_via_pager(self, text, color=None):
12661270
@click.option(
12671271
"--warn",
12681272
default=None,
1269-
type=click.Choice(["all", "moderate", "off"]),
12701273
help="Warn before running a destructive query.",
12711274
)
12721275
@click.option(
Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import sqlparse
22

33

4+
BASE_KEYWORDS = [
5+
"drop",
6+
"shutdown",
7+
"delete",
8+
"truncate",
9+
"alter",
10+
"unconditional_update",
11+
]
12+
ALL_KEYWORDS = BASE_KEYWORDS + ["update"]
13+
14+
415
def query_starts_with(formatted_sql, prefixes):
516
"""Check if the query starts with any item from *prefixes*."""
617
prefixes = [prefix.lower() for prefix in prefixes]
@@ -13,22 +24,35 @@ def query_is_unconditional_update(formatted_sql):
1324
return bool(tokens) and tokens[0] == "update" and "where" not in tokens
1425

1526

16-
def query_is_simple_update(formatted_sql):
17-
"""Check if the query starts with UPDATE."""
18-
tokens = formatted_sql.split()
19-
return bool(tokens) and tokens[0] == "update"
20-
21-
22-
def is_destructive(queries, warning_level="all"):
27+
def is_destructive(queries, keywords):
2328
"""Returns if any of the queries in *queries* is destructive."""
24-
keywords = ("drop", "shutdown", "delete", "truncate", "alter")
2529
for query in sqlparse.split(queries):
2630
if query:
2731
formatted_sql = sqlparse.format(query.lower(), strip_comments=True).strip()
28-
if query_starts_with(formatted_sql, keywords):
29-
return True
30-
if query_is_unconditional_update(formatted_sql):
32+
if "unconditional_update" in keywords and query_is_unconditional_update(
33+
formatted_sql
34+
):
3135
return True
32-
if warning_level == "all" and query_is_simple_update(formatted_sql):
36+
if query_starts_with(formatted_sql, keywords):
3337
return True
3438
return False
39+
40+
41+
def parse_destructive_warning(warning_level):
42+
"""Converts a deprecated destructive warning option to a list of command keywords."""
43+
if not warning_level:
44+
return []
45+
46+
if not isinstance(warning_level, list):
47+
if "," in warning_level:
48+
return warning_level.split(",")
49+
warning_level = [warning_level]
50+
51+
return {
52+
"true": ALL_KEYWORDS,
53+
"false": [],
54+
"all": ALL_KEYWORDS,
55+
"moderate": BASE_KEYWORDS,
56+
"off": [],
57+
"": [],
58+
}.get(warning_level[0], warning_level)

pgcli/packages/prompt_utils.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .parseutils import is_destructive
44

55

6-
def confirm_destructive_query(queries, warning_level):
6+
def confirm_destructive_query(queries, keywords, alias):
77
"""Check if the query is destructive and prompts the user to confirm.
88
99
Returns:
@@ -12,10 +12,12 @@ def confirm_destructive_query(queries, warning_level):
1212
* False if the query is destructive and the user doesn't want to proceed.
1313
1414
"""
15-
prompt_text = (
16-
"You're about to run a destructive command.\n" "Do you want to proceed? (y/n)"
17-
)
18-
if is_destructive(queries, warning_level) and sys.stdin.isatty():
15+
info = "You're about to run a destructive command"
16+
if alias:
17+
info += f" in {click.style(alias, fg='red')}"
18+
19+
prompt_text = f"{info}.\nDo you want to proceed? (y/n)"
20+
if is_destructive(queries, keywords) and sys.stdin.isatty():
1921
return prompt(prompt_text, type=bool)
2022

2123

pgcli/pgclirc

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@ multi_line = False
2222
# a command.
2323
multi_line_mode = psql
2424

25-
# Destructive warning mode will alert you before executing a sql statement
25+
# Destructive warning will alert you before executing a sql statement
2626
# that may cause harm to the database such as "drop table", "drop database",
27-
# "shutdown", "delete", or "update".
28-
# Possible values:
29-
# "all" - warn on data definition statements, server actions such as SHUTDOWN, DELETE or UPDATE
30-
# "moderate" - skip warning on UPDATE statements, except for unconditional updates
31-
# "off" - skip all warnings
32-
destructive_warning = all
27+
# "shutdown", "delete", or "update".
28+
# You can pass a list of destructive commands or leave it empty if you want to skip all warnings.
29+
# "unconditional_update" will warn you of update statements that don't have a where clause
30+
destructive_warning = drop, shutdown, delete, truncate, alter, update, unconditional_update
3331

3432
# Enables expand mode, which is similar to `\x` in psql.
3533
expand = False
@@ -140,7 +138,7 @@ less_chatty = False
140138
# \i - Postgres PID
141139
# \# - "@" sign if logged in as superuser, '>' in other case
142140
# \n - Newline
143-
# \dsn_alias - name of dsn alias if -D option is used (empty otherwise)
141+
# \dsn_alias - name of dsn connection string alias if -D option is used (empty otherwise)
144142
# \x1b[...m - insert ANSI escape sequence
145143
# eg: prompt = '\x1b[35m\u@\x1b[32m\h:\x1b[36m\d>'
146144
prompt = '\u@\h:\d> '
@@ -198,7 +196,8 @@ output.null = "#808080"
198196
# Named queries are queries you can execute by name.
199197
[named queries]
200198

201-
# DSN to call by -D option
199+
# Here's where you can provide a list of connection string aliases.
200+
# You can use it by passing the -D option. `pgcli -D example_dsn`
202201
[alias_dsn]
203202
# example_dsn = postgresql://[user[:password]@][netloc][:port][/dbname]
204203

tests/parseutils/test_parseutils.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import pytest
2-
from pgcli.packages.parseutils import is_destructive
2+
from pgcli.packages.parseutils import (
3+
is_destructive,
4+
parse_destructive_warning,
5+
BASE_KEYWORDS,
6+
ALL_KEYWORDS,
7+
)
38
from pgcli.packages.parseutils.tables import extract_tables
49
from pgcli.packages.parseutils.utils import find_prev_keyword, is_open_quote
510

@@ -263,18 +268,43 @@ def test_is_open_quote__open(sql):
263268

264269

265270
@pytest.mark.parametrize(
266-
("sql", "warning_level", "expected"),
271+
("sql", "keywords", "expected"),
272+
[
273+
("update abc set x = 1", ALL_KEYWORDS, True),
274+
("update abc set x = 1 where y = 2", ALL_KEYWORDS, True),
275+
("update abc set x = 1", BASE_KEYWORDS, True),
276+
("update abc set x = 1 where y = 2", BASE_KEYWORDS, False),
277+
("select x, y, z from abc", ALL_KEYWORDS, False),
278+
("drop abc", ALL_KEYWORDS, True),
279+
("alter abc", ALL_KEYWORDS, True),
280+
("delete abc", ALL_KEYWORDS, True),
281+
("truncate abc", ALL_KEYWORDS, True),
282+
("insert into abc values (1, 2, 3)", ALL_KEYWORDS, False),
283+
("insert into abc values (1, 2, 3)", BASE_KEYWORDS, False),
284+
("insert into abc values (1, 2, 3)", ["insert"], True),
285+
("insert into abc values (1, 2, 3)", ["insert"], True),
286+
],
287+
)
288+
def test_is_destructive(sql, keywords, expected):
289+
assert is_destructive(sql, keywords) == expected
290+
291+
292+
@pytest.mark.parametrize(
293+
("warning_level", "expected"),
267294
[
268-
("update abc set x = 1", "all", True),
269-
("update abc set x = 1 where y = 2", "all", True),
270-
("update abc set x = 1", "moderate", True),
271-
("update abc set x = 1 where y = 2", "moderate", False),
272-
("select x, y, z from abc", "all", False),
273-
("drop abc", "all", True),
274-
("alter abc", "all", True),
275-
("delete abc", "all", True),
276-
("truncate abc", "all", True),
295+
("true", ALL_KEYWORDS),
296+
("false", []),
297+
("all", ALL_KEYWORDS),
298+
("moderate", BASE_KEYWORDS),
299+
("off", []),
300+
("", []),
301+
(None, []),
302+
(ALL_KEYWORDS, ALL_KEYWORDS),
303+
(BASE_KEYWORDS, BASE_KEYWORDS),
304+
("insert", ["insert"]),
305+
("drop,alter,delete", ["drop", "alter", "delete"]),
306+
(["drop", "alter", "delete"], ["drop", "alter", "delete"]),
277307
],
278308
)
279-
def test_is_destructive(sql, warning_level, expected):
280-
assert is_destructive(sql, warning_level=warning_level) == expected
309+
def test_parse_destructive_warning(warning_level, expected):
310+
assert parse_destructive_warning(warning_level) == expected

tests/test_prompt_utils.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,11 @@ def test_confirm_destructive_query_notty():
77
stdin = click.get_text_stream("stdin")
88
if not stdin.isatty():
99
sql = "drop database foo;"
10-
assert confirm_destructive_query(sql, "all") is None
10+
assert confirm_destructive_query(sql, [], None) is None
11+
12+
13+
def test_confirm_destructive_query_with_alias():
14+
stdin = click.get_text_stream("stdin")
15+
if not stdin.isatty():
16+
sql = "drop database foo;"
17+
assert confirm_destructive_query(sql, ["drop"], "test") is None

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ deps = pytest>=2.7.0,<=3.0.7
55
mock>=1.0.1
66
behave>=1.2.4
77
pexpect==3.3
8+
sshtunnel>=0.4.0
89
commands = py.test
910
behave tests/features
1011
passenv = PGHOST

0 commit comments

Comments
 (0)