diff --git a/AUTHORS b/AUTHORS index 45019555e..c7f0c4c16 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ Core Devs: Contributors: ------------- + * AC (ac1982) * Wu Shuwen (dajiaohuang) * Brett * Étienne BERSAC (bersace) diff --git a/changelog.rst b/changelog.rst index 5a9bc17f1..1180d52ff 100644 --- a/changelog.rst +++ b/changelog.rst @@ -10,6 +10,8 @@ Features: Bug fixes: ---------- +* Display database errors from ``\ef`` and ``\ev`` without exiting the + interactive session, allowing recovery from an aborted transaction. * Fix the development setup instructions to activate the ``.venv`` directory created by ``uv venv``. diff --git a/pgcli/main.py b/pgcli/main.py index 6ea90ab5a..972b3c670 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 @@ -1170,7 +1170,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 a80f6ada2..c4f80477c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -28,6 +28,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 @@ -655,6 +656,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"))