Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Upcoming (TBD)

Bug fixes:
----------
* Fix special commands being broken while explain mode (F5) is on. Every input
was prefixed with ``EXPLAIN (...)`` and sent to the server as SQL, including
backslash commands and the bare words ``exit``/``quit``, so ``\q``, ``\d``,
``\i``, named queries and ``\G`` all failed with ``syntax error at or near
"\"`` and there was no way to leave explain mode or quit. Special commands
are now detected first and the EXPLAIN prefix is applied only to real SQL.
* Restore cursor shape behaviour for Emacs mode
* Fix ``TypeError: cannot use a string pattern on a bytes-like object`` when
completion metadata comes back as bytes (e.g. ``SQL_ASCII`` client encoding).
Expand Down
11 changes: 8 additions & 3 deletions pgcli/pgexecute.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,11 @@ def run(
if not sql:
continue
try:
if explain_mode:
sql = self.explain_prefix() + sql
elif pgspecial:
# Try special commands first, regardless of explain mode: they
# are not SQL, so prefixing them with EXPLAIN just sends garbage
# to the server. The EXPLAIN prefix is applied further down, to
# statements that are not special commands.
if pgspecial:
# \G is treated specially since we have to set the expanded output.
if sql.endswith("\\G"):
if not pgspecial.expanded_output:
Expand Down Expand Up @@ -410,6 +412,9 @@ def run(
pass

# Not a special command, so execute as normal sql
if explain_mode:
sql = self.explain_prefix() + sql

yield self.execute_normal_sql(sql) + (sql, True, False)
except psycopg.DatabaseError as e:
_logger.error("sql: %r, error: %r", sql, e)
Expand Down
56 changes: 56 additions & 0 deletions tests/test_pgexecute.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,62 @@ def execute(self, *args, **kwargs):
self.protocol_message = "Command not supported"


@dbtest
def test_explain_mode_does_not_wrap_special_command(executor):
"""A special command is dispatched as a special command in explain mode.

Prefixing it with EXPLAIN would send it to the server as invalid SQL, which
used to make it impossible to even quit while explain mode was on.
"""
quit_handler = MagicMock()
pgspecial = PGSpecial()
pgspecial.register(
quit_handler,
"\\q",
"\\q",
"Quit pgcli.",
arg_type=NO_QUERY,
case_sensitive=True,
aliases=(":q",),
)
with patch.object(executor, "execute_normal_sql") as normal_sql:
list(executor.run("\\q", pgspecial=pgspecial, explain_mode=True))

quit_handler.assert_called_once()
normal_sql.assert_not_called()


@dbtest
def test_explain_mode_runs_describe_as_special(executor, pgspecial):
"""A describe command still runs as a special command in explain mode."""
with patch.object(executor, "execute_normal_sql") as normal_sql:
result = list(executor.run("\\dt", pgspecial=pgspecial, explain_mode=True))

normal_sql.assert_not_called()
assert result[0][6] is True # is_special


@dbtest
def test_explain_mode_wraps_normal_sql(executor, pgspecial):
"""Normal SQL is still prefixed with EXPLAIN in explain mode."""
with patch.object(executor, "execute_normal_sql", return_value=("", None, None, "")) as normal_sql:
list(executor.run("select 1", pgspecial=pgspecial, explain_mode=True))

normal_sql.assert_called_once()
assert normal_sql.call_args.args[0] == executor.explain_prefix() + "select 1"


@dbtest
def test_explain_mode_strips_G_suffix(executor, pgspecial):
"""`select ... \\G` strips the \\G in explain mode instead of sending it."""
with patch.object(executor, "execute_normal_sql", return_value=("", None, None, "")) as normal_sql:
list(executor.run("select 1 \\G", pgspecial=pgspecial, explain_mode=True))

sent = normal_sql.call_args.args[0]
assert sent == executor.explain_prefix() + "select 1"
assert "\\G" not in sent


@dbtest
def test_exit_without_active_connection(executor):
quit_handler = MagicMock()
Expand Down
Loading