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
8 changes: 8 additions & 0 deletions changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ Features:
* Add a ``\service`` prompt token for connections made using ``service=...``
or ``PGSERVICE``. For example, set ``prompt = '\service> '``.

Bug fixes:
----------
* Fix `-t`/`--tuples-only` hiding SQL errors. The error of a failed
statement was printed in place of the status footer that `-t` suppresses,
so a broken query printed nothing at all. Errors now go to stderr under
`-t`, as with `psql -t`, so stdout still carries only rows.


4.7.1 (2026-09-20)
==================

Expand Down
8 changes: 8 additions & 0 deletions pgcli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1386,6 +1386,14 @@ def _evaluate_command(self, text):
):
title = None

if not success and status and self.tuples_only:
# For a failed statement `status` is the error message, not
# the "SELECT 3" footer that -t drops, so -t used to hide the
# error completely. With -t stdout carries only rows, so the
# error goes to stderr, as psql does.
click.echo(status, err=True)
status = None

execution = time() - start
formatted = format_output(title, cur, headers, status, settings, self.explain_mode)

Expand Down
4 changes: 4 additions & 0 deletions tests/features/command_option.feature
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Feature: run the cli with -c/--command option,
then we see an error message
and pgcli exits successfully

Scenario: run pgcli with -t and an invalid query
When we run pgcli with -t -c "SELECT invalid_column FROM nonexistent_table"
then we see the error on stderr and nothing on stdout

Scenario: run pgcli with -c and multiple statements
When we run pgcli with -c "SELECT 1; SELECT 2"
then we see both query results
Expand Down
31 changes: 31 additions & 0 deletions tests/features/steps/command_option.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,34 @@ def step_see_all_command_outputs(context):
# For the 3-command test, also check for third
if "third" in output or "result" in output:
assert "third" in output, f"Expected 'third' in output for 3-command test, but got: {output}"


@when('we run pgcli with -t -c "{command}"')
def step_run_pgcli_with_t_and_c(context, command):
"""Run pgcli with -t and -c, keeping stdout and stderr apart."""
cmd = [
"pgcli",
"-h",
context.conf["host"],
"-p",
str(context.conf["port"]),
"-U",
context.conf["user"],
"-d",
context.conf["dbname"],
"-t",
"-c",
command,
]
result = subprocess.run(cmd, cwd=context.package_root, capture_output=True, timeout=5)
context.cmd_output = result.stdout
context.cmd_stderr = result.stderr.decode()
context.exit_code = result.returncode


@then("we see the error on stderr and nothing on stdout")
def step_error_on_stderr_only(context):
"""With -t, stdout carries only rows, so the error must be on stderr."""
stdout = context.cmd_output.decode()
assert "does not exist" in context.cmd_stderr, f"Expected the error on stderr, got: {context.cmd_stderr!r}"
assert "does not exist" not in stdout, f"Expected no error on stdout, got: {stdout!r}"
20 changes: 20 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1087,3 +1087,23 @@ def test_connect_timeout_config_value_must_be_a_number(tmpdir):
f.write("[main]\nconnect_timeout = soon\n")
with pytest.raises(ValueError):
PGCli(pgclirc_file=rc)


@dbtest
def test_tuples_only_sends_errors_to_stderr(executor, capsys):
# The error of a failed statement travels in `status`, the footer -t
# drops, so -t used to print nothing at all for a broken query.
cli = PGCli(pgexecute=executor, tuples_only=True)
output, query = cli._evaluate_command("select nope")
assert not query.successful
assert 'column "nope" does not exist' in capsys.readouterr().err
assert not any("nope" in line for line in output)


@dbtest
def test_tuples_only_rows_stay_on_stdout(executor, capsys):
cli = PGCli(pgexecute=executor, tuples_only=True)
output, query = cli._evaluate_command("select 42")
assert query.successful
assert any("42" in line for line in output)
assert capsys.readouterr().err == ""
Loading