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 + )