From 08d06a2f112c7dc9d3b6f3571feaa6fd0b08c574 Mon Sep 17 00:00:00 2001 From: AC Date: Wed, 14 Oct 2026 14:17:56 +0800 Subject: [PATCH] Keep the prompt after database errors opening the editor --- AUTHORS | 1 + changelog.rst | 5 +++++ pgcli/main.py | 4 ++-- tests/test_main.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index f235e11bc..658f719c2 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ Core Devs: Contributors: ------------- + * AC (ac1982) * Brett * Étienne BERSAC (bersace) * Daniel Schwarz diff --git a/changelog.rst b/changelog.rst index 83e0ac390..85add15e8 100644 --- a/changelog.rst +++ b/changelog.rst @@ -1,6 +1,11 @@ Upcoming ======== +Bug fixes: +---------- +* Display database errors from ``\ef`` and ``\ev`` without exiting the + interactive session, allowing recovery from an aborted transaction. + 4.7.1 (2026-09-20) ================== diff --git a/pgcli/main.py b/pgcli/main.py index 3ff820ce2..987eae74a 100644 --- a/pgcli/main.py +++ b/pgcli/main.py @@ -82,7 +82,7 @@ from getpass import getuser -from psycopg import OperationalError, InterfaceError, Notify +from psycopg import DatabaseError, OperationalError, InterfaceError, Notify from psycopg.conninfo import make_conninfo, conninfo_to_dict from psycopg.errors import Diagnostic @@ -1133,7 +1133,7 @@ def run_cli(self): try: text = self.handle_editor_command(text) - except RuntimeError as e: + except (RuntimeError, DatabaseError) as e: logger.error("sql: %r, error: %r", text, e) logger.error("traceback: %r", traceback.format_exc()) click.secho(str(e), err=True, fg="red") diff --git a/tests/test_main.py b/tests/test_main.py index be29393cc..c873284f7 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -27,6 +27,7 @@ ) from pgcli.pgexecute import PGExecute from psycopg.conninfo import conninfo_to_dict +from psycopg.pq import TransactionStatus from pgspecial.main import PAGER_OFF, PAGER_LONG_OUTPUT, PAGER_ALWAYS from utils import dbtest, run from collections import namedtuple @@ -602,6 +603,35 @@ def test_file_mode_runs_statements(tmpdir): mock_exec.assert_called_once_with("select 1;\nselect 2;") +@dbtest +@pytest.mark.parametrize("editor_command", [r"\ef example", r"\ev example"]) +def test_editor_in_failed_transaction(executor, tmpdir, capsys, editor_command): + cli = PGCli(pgexecute=executor, pgclirc_file=str(tmpdir.join("rcfile"))) + executor.conn.execute("BEGIN") + list(executor.run("SELECT 1 / 0", exception_formatter=str)) + prompt = mock.Mock() + + def commands(): + yield editor_command + assert executor.conn.info.transaction_status == TransactionStatus.INERROR + yield "ROLLBACK" + raise EOFError + + prompt.prompt.side_effect = commands() + with ( + mock.patch.object(cli, "refresh_completions"), + mock.patch.object(cli, "_build_cli", return_value=prompt), + mock.patch.object(cli, "echo_via_pager"), + mock.patch("pgcli.main.special.open_external_editor") as editor, + ): + cli.run_cli() + assert "current transaction is aborted" in capsys.readouterr().err + editor.assert_not_called() + with executor.conn.cursor() as cursor: + cursor.execute("SELECT 1") + assert cursor.fetchone() == (1,) + + def test_missing_rc_dir(tmpdir): rcfile = str(tmpdir.join("subdir").join("rcfile"))