Skip to content

Commit 66a9bfb

Browse files
committed
Merge upstream features and bump to v4.4.1
Cherry-pick from upstream: - Named query quiet mode \nq (dbcli#1551) - \T prompt transaction status indicator (dbcli#1553) Also changes -t/--tuples-only to pure boolean flag (fixes bug where -t consumed next CLI argument as format value). Made with ❤️ and 🤖 Claude
1 parent ccced29 commit 66a9bfb

8 files changed

Lines changed: 121 additions & 4 deletions

File tree

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ Contributors:
144144
* Jay Knight (jay-knight)
145145
* fbdb
146146
* Charbel Jacquin (charbeljc)
147+
* Devadathan M B (devadathanmb)
147148
* Diego
148149
* Jeronimo Garcia (bechampion)
149150

changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ Features:
3636
* New ``host_key_policy`` setting in ``[ssh tunnels]`` config section
3737
* Options: ``auto-add`` (default, TOFU), ``warn`` (log warning), ``reject`` (known hosts only)
3838
* ``reject`` mode only connects to hosts already in ``~/.ssh/known_hosts``
39+
* Add named query quiet mode to hide query text during execution (upstream #1551).
40+
* New ``\nq`` command to save queries that execute without printing the query text
41+
* ``quiet`` flag stored in named query metadata
42+
* Useful for utility queries where only the result matters
43+
* Add ``\T`` prompt escape sequence to display transaction status (upstream #1553).
44+
* Shows transaction state in prompt: empty (idle), ``*`` (in transaction), ``!`` (failed transaction)
45+
* Similar to psql's ``%x`` prompt escape
3946

4047
Bug Fixes:
4148
----------

pgcli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.3.18"
1+
__version__ = "4.4.1"

pgcli/main.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ def __init__(
208208
self.output_file = None
209209
self.pgspecial = PGSpecial()
210210

211+
self.hide_named_query_text = "hide_named_query_text" in c["main"] and c["main"].as_bool("hide_named_query_text")
211212
self.explain_mode = False
212213
self.multi_line = c["main"].as_bool("multi_line")
213214
self.multiline_mode = c["main"].get("multi_line_mode", "psql")
@@ -342,7 +343,28 @@ def __init__(
342343
def quit(self):
343344
raise PgCliQuitError
344345

346+
def toggle_named_query_quiet(self):
347+
"""Toggle hiding of named query text"""
348+
self.hide_named_query_text = not self.hide_named_query_text
349+
status = "ON" if self.hide_named_query_text else "OFF"
350+
message = f"Named query quiet mode: {status}"
351+
return [(None, None, None, message)]
352+
353+
def _is_named_query_execution(self, text):
354+
"""Check if the command is a named query execution (\n <name>)."""
355+
text = text.strip()
356+
return text.startswith("\\n ") and not text.startswith("\\ns ") and not text.startswith("\\nd ")
357+
345358
def register_special_commands(self):
359+
self.pgspecial.register(
360+
self.toggle_named_query_quiet,
361+
"\\nq",
362+
"\\nq",
363+
"Toggle named query quiet mode (hide query text)",
364+
arg_type=NO_QUERY,
365+
case_sensitive=True,
366+
)
367+
346368
self.pgspecial.register(
347369
self.change_db,
348370
"\\c",
@@ -972,7 +994,14 @@ def execute_command(self, text, handle_closed_connection=True):
972994
if self.output_file and not text.startswith(("\\o ", "\\log-file", "\\? ", "\\echo ")):
973995
try:
974996
with open(self.output_file, "a", encoding="utf-8") as f:
975-
click.echo(text, file=f)
997+
should_hide = (
998+
self.hide_named_query_text
999+
and query.is_special
1000+
and query.successful
1001+
and self._is_named_query_execution(text)
1002+
)
1003+
if not should_hide:
1004+
click.echo(text, file=f)
9761005
click.echo("\n".join(output), file=f)
9771006
click.echo("", file=f) # extra newline
9781007
except OSError as e:
@@ -986,7 +1015,14 @@ def execute_command(self, text, handle_closed_connection=True):
9861015
try:
9871016
with open(self.log_file, "a", encoding="utf-8") as f:
9881017
click.echo(dt.datetime.now().isoformat(), file=f) # timestamp log
989-
click.echo(text, file=f)
1018+
should_hide = (
1019+
self.hide_named_query_text
1020+
and query.is_special
1021+
and query.successful
1022+
and self._is_named_query_execution(text)
1023+
)
1024+
if not should_hide:
1025+
click.echo(text, file=f)
9901026
click.echo("\n".join(output), file=f)
9911027
click.echo("", file=f) # extra newline
9921028
except OSError as e:
@@ -1324,6 +1360,18 @@ def _evaluate_command(self, text):
13241360
tuples_only=self.tuples_only,
13251361
show_status=self.show_status,
13261362
)
1363+
1364+
# Hide query text for named queries in quiet mode
1365+
if (
1366+
self.hide_named_query_text
1367+
and is_special
1368+
and success
1369+
and self._is_named_query_execution(text)
1370+
and title
1371+
and title.startswith("> ")
1372+
):
1373+
title = None
1374+
13271375
execution = time() - start
13281376
formatted = format_output(title, cur, headers, status, settings, self.explain_mode)
13291377

@@ -1447,6 +1495,7 @@ def get_prompt(self, string):
14471495
string = string.replace("\\i", str(self.pgexecute.pid) or "(none)")
14481496
string = string.replace("\\#", "#" if self.pgexecute.superuser else ">")
14491497
string = string.replace("\\n", "\n")
1498+
string = string.replace("\\T", self.pgexecute.transaction_indicator)
14501499
return string
14511500

14521501
def get_last_query(self):

pgcli/packages/sqlcompletion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ def suggest_special(text):
318318
return (Schema(), Function(schema=None, usage="special"))
319319
return (Schema(), rel_type(schema=None))
320320

321-
if cmd in ["\\n", "\\nd", "\\np", "\\ns"]:
321+
if cmd in ["\\n", "\\nd", "\\np", "\\nq", "\\ns"]:
322322
return (NamedQuery(),)
323323

324324
return (Keyword(), Special())

pgcli/pgclirc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ search_path_filter = False
134134
# Timing of sql statements and table rendering.
135135
timing = True
136136

137+
# Hide the query text when executing named queries (\n <name>).
138+
# Only the query results will be displayed.
139+
# Can be toggled at runtime with \nq command.
140+
hide_named_query_text = False
141+
137142
# Show/hide the informational toolbar with function keymap at the footer.
138143
show_bottom_toolbar = True
139144

@@ -192,6 +197,7 @@ verbose_errors = False
192197
# \i - Postgres PID
193198
# \# - "@" sign if logged in as superuser, '>' in other case
194199
# \n - Newline
200+
# \T - Transaction status: '*' if in a valid transaction, '!' if in a failed transaction, '?' if disconnected, empty otherwise
195201
# \dsn_alias - name of dsn connection string alias if -D option is used (empty otherwise)
196202
# \x1b[...m - insert ANSI escape sequence
197203
# eg: prompt = '\x1b[35m\u@\x1b[32m\h:\x1b[36m\d>'

pgcli/pgexecute.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,19 @@ def valid_transaction(self):
311311
status = self.conn.info.transaction_status # type: ignore[union-attr]
312312
return status == psycopg.pq.TransactionStatus.ACTIVE or status == psycopg.pq.TransactionStatus.INTRANS
313313

314+
def is_connection_closed(self):
315+
return self.conn.info.transaction_status == psycopg.pq.TransactionStatus.UNKNOWN
316+
317+
@property
318+
def transaction_indicator(self):
319+
if self.is_connection_closed():
320+
return "?"
321+
if self.failed_transaction():
322+
return "!"
323+
if self.valid_transaction():
324+
return "*"
325+
return ""
326+
314327
def run(
315328
self,
316329
statement,

tests/test_main.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,47 @@ def test_duration_in_words(duration_in_seconds, words):
902902
assert duration_in_words(duration_in_seconds) == words
903903

904904

905+
@pytest.mark.parametrize(
906+
"transaction_indicator,expected",
907+
[
908+
("*", "*testuser"), # valid transaction
909+
("!", "!testuser"), # failed transaction
910+
("?", "?testuser"), # connection closed
911+
("", "testuser"), # idle
912+
],
913+
)
914+
def test_get_prompt_with_transaction_status(transaction_indicator, expected):
915+
cli = PGCli()
916+
cli.pgexecute = mock.MagicMock()
917+
cli.pgexecute.user = "testuser"
918+
cli.pgexecute.dbname = "testdb"
919+
cli.pgexecute.host = "localhost"
920+
cli.pgexecute.short_host = "localhost"
921+
cli.pgexecute.port = 5432
922+
cli.pgexecute.pid = 12345
923+
cli.pgexecute.superuser = False
924+
cli.pgexecute.transaction_indicator = transaction_indicator
925+
926+
result = cli.get_prompt("\\T\\u")
927+
assert result == expected
928+
929+
930+
def test_get_prompt_transaction_status_in_full_prompt():
931+
cli = PGCli()
932+
cli.pgexecute = mock.MagicMock()
933+
cli.pgexecute.user = "user"
934+
cli.pgexecute.dbname = "mydb"
935+
cli.pgexecute.host = "db.example.com"
936+
cli.pgexecute.short_host = "db.example.com"
937+
cli.pgexecute.port = 5432
938+
cli.pgexecute.pid = 12345
939+
cli.pgexecute.superuser = False
940+
cli.pgexecute.transaction_indicator = "*"
941+
942+
result = cli.get_prompt("\\T\\u@\\h:\\d> ")
943+
assert result == "*user@db.example.com:mydb> "
944+
945+
905946
@dbtest
906947
def test_notifications(executor):
907948
run(executor, "listen chan1")

0 commit comments

Comments
 (0)