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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Core Devs:

Contributors:
-------------
* AC (ac1982)
* Brett
* Étienne BERSAC (bersace)
* Daniel Schwarz
Expand Down
5 changes: 5 additions & 0 deletions changelog.rst
Original file line number Diff line number Diff line change
@@ -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)
==================
Expand Down
4 changes: 2 additions & 2 deletions pgcli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down
30 changes: 30 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))

Expand Down
Loading