diff --git a/changelog.rst b/changelog.rst index 81fbefe78..ce3602e4f 100644 --- a/changelog.rst +++ b/changelog.rst @@ -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) ================== diff --git a/pgcli/main.py b/pgcli/main.py index 994f116e5..c38dfabbd 100644 --- a/pgcli/main.py +++ b/pgcli/main.py @@ -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) diff --git a/tests/features/command_option.feature b/tests/features/command_option.feature index 38f5fef51..0f9403c92 100644 --- a/tests/features/command_option.feature +++ b/tests/features/command_option.feature @@ -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 diff --git a/tests/features/steps/command_option.py b/tests/features/steps/command_option.py index b783dc7a7..3ffe67e98 100644 --- a/tests/features/steps/command_option.py +++ b/tests/features/steps/command_option.py @@ -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}" diff --git a/tests/test_main.py b/tests/test_main.py index decae97b1..334221b1a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -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 == ""