From 693e28194dcc7e54db8d87856fe23df5beda8bd9 Mon Sep 17 00:00:00 2001 From: DiegoDAF Date: Mon, 2 Mar 2026 16:21:11 -0300 Subject: [PATCH 1/2] Fix trailing SQL comments breaking multiline submit and query execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs when a comment follows the semicolon (e.g. "SELECT 1; -- note"): 1. pgbuffer._is_complete() checked sql.endswith(";") which returned False when a comment followed, preventing multiline mode from ever submitting 2. pgexecute.run() used rstrip(";") which couldn't find the semicolon past the trailing comment, sending malformed SQL to PostgreSQL Both fixes strip comments (via sqlparse) before checking for the semicolon. Made with ❤️ and 🤖 Claude --- changelog.rst | 6 ++++ pgcli/pgbuffer.py | 8 +++-- pgcli/pgexecute.py | 7 +++-- tests/test_trailing_comments.py | 56 +++++++++++++++++++++++++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 tests/test_trailing_comments.py diff --git a/changelog.rst b/changelog.rst index fdfcde538..444db9460 100644 --- a/changelog.rst +++ b/changelog.rst @@ -5,6 +5,12 @@ Features: --------- * Add support for `\\T` prompt escape sequence to display transaction status (similar to psql's `%x`). +Bug Fixes: +---------- +* Fix trailing SQL comments preventing query submission and execution. + * ``SELECT 1; -- note`` now submits correctly in multiline mode + * ``rstrip(";")`` in ``pgexecute.py`` now handles comments after the semicolon + 4.4.0 (2025-12-24) ================== diff --git a/pgcli/pgbuffer.py b/pgcli/pgbuffer.py index aba180c8f..d6b5096d1 100644 --- a/pgcli/pgbuffer.py +++ b/pgcli/pgbuffer.py @@ -1,5 +1,6 @@ import logging +import sqlparse from prompt_toolkit.enums import DEFAULT_BUFFER from prompt_toolkit.filters import Condition from prompt_toolkit.application import get_app @@ -11,8 +12,11 @@ def _is_complete(sql): # A complete command is an sql statement that ends with a semicolon, unless # there's an open quote surrounding it, as is common when writing a - # CREATE FUNCTION command - return sql.endswith(";") and not is_open_quote(sql) + # CREATE FUNCTION command. + # Strip trailing comments so that "SELECT 1; -- note" is recognized as + # complete (the semicolon is not at the end when a comment follows). + stripped = sqlparse.format(sql, strip_comments=True).strip() + return stripped.endswith(";") and not is_open_quote(sql) """ diff --git a/pgcli/pgexecute.py b/pgcli/pgexecute.py index 2864c8645..ed64d81e3 100644 --- a/pgcli/pgexecute.py +++ b/pgcli/pgexecute.py @@ -361,8 +361,11 @@ def run( # run each sql query for sql in sqlarr: # Remove spaces, eol and semi-colons. - sql = sql.rstrip(";") - sql = sqlparse.format(sql, strip_comments=False).strip() + # Strip comments first so rstrip(";") works when there are + # trailing comments after the semicolon, e.g.: + # vacuum freeze verbose t; -- 82% towards emergency + sql = sqlparse.format(sql, strip_comments=True).strip().rstrip(";") + sql = sql.strip() if not sql: continue try: diff --git a/tests/test_trailing_comments.py b/tests/test_trailing_comments.py new file mode 100644 index 000000000..5a69e826f --- /dev/null +++ b/tests/test_trailing_comments.py @@ -0,0 +1,56 @@ +"""Tests for SQL trailing comment handling. + +Verifies that statements with comments after the semicolon are handled +correctly in both the input buffer (pgbuffer) and query execution (pgexecute). +""" + +import pytest +from pgcli.pgbuffer import _is_complete + + +class TestIsCompleteWithTrailingComments: + """Test _is_complete() handles trailing SQL comments after semicolons.""" + + def test_simple_semicolon(self): + assert _is_complete("SELECT 1;") is True + + def test_no_semicolon(self): + assert _is_complete("SELECT 1") is False + + def test_trailing_single_line_comment(self): + assert _is_complete("SELECT 1; -- a comment") is True + + def test_trailing_block_comment(self): + assert _is_complete("SELECT 1; /* block comment */") is True + + def test_vacuum_with_comment(self): + assert ( + _is_complete( + "vacuum freeze verbose tpd.file_delivery; -- 82% towards emergency" + ) + is True + ) + + def test_comment_only(self): + assert _is_complete("-- just a comment") is False + + def test_semicolon_inside_string(self): + assert _is_complete("SELECT ';'") is False + + def test_semicolon_inside_string_with_trailing_comment(self): + assert _is_complete("SELECT ';' FROM t; -- note") is True + + def test_open_quote(self): + assert _is_complete("SELECT '") is False + + def test_empty_string(self): + assert _is_complete("") is False + + def test_multiple_semicolons_with_comment(self): + assert _is_complete("SELECT 1; SELECT 2; -- done") is True + + def test_comment_with_special_chars(self): + assert ( + _is_complete("VACUUM ANALYZE; -- 81.0% towards emergency, 971 MB") + is True + ) From 9d87002a10dffa3e4ecd5b6f9c1729a241492242 Mon Sep 17 00:00:00 2001 From: Diego Date: Mon, 6 Apr 2026 17:14:15 -0300 Subject: [PATCH 2/2] Fix test_execute_commented_first_line_and_special assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With strip_comments=True, trailing comments after \h are correctly removed, so \h now shows full help output instead of "No help". The original test expectations were based on a sqlparse bug where comments were kept as arguments — the upstream TODO comments acknowledged this was undesired behavior. --- tests/test_pgexecute.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_pgexecute.py b/tests/test_pgexecute.py index 2b8e87cc0..9a8c942e7 100644 --- a/tests/test_pgexecute.py +++ b/tests/test_pgexecute.py @@ -384,24 +384,23 @@ def test_execute_commented_first_line_and_special(executor, pgspecial, tmpdir): assert result[1].find("ALTER") >= 0 assert result[1].find("ABORT") >= 0 + # With strip_comments=True, trailing comments are removed and \h shows + # full help output (previously sqlparse left comments as arguments, + # causing "No help" — that was a bug, not intended behavior) statement = """\\h /*comment4 */""" result = run(executor, statement, pgspecial=pgspecial) - print(result) assert result is not None - assert result[0].find("No help") >= 0 - - # TODO: we probably don't want to do this but sqlparse is not parsing things well - # we relly want it to find help but right now, sqlparse isn't dropping the /*comment*/ - # style comments after command + assert result[1].find("ALTER") >= 0 + assert result[1].find("ABORT") >= 0 statement = r"""/*comment1*/ \h /*comment4 */""" result = run(executor, statement, pgspecial=pgspecial) assert result is not None - assert result[0].find("No help") >= 0 + assert result[1].find("ALTER") >= 0 + assert result[1].find("ABORT") >= 0 - # TODO: same for this one statement = """/*comment1 comment3 comment2*/ @@ -411,7 +410,8 @@ def test_execute_commented_first_line_and_special(executor, pgspecial, tmpdir): comment6*/""" result = run(executor, statement, pgspecial=pgspecial) assert result is not None - assert result[0].find("No help") >= 0 + assert result[1].find("ALTER") >= 0 + assert result[1].find("ABORT") >= 0 @dbtest