Skip to content

Commit 693e281

Browse files
committed
Fix trailing SQL comments breaking multiline submit and query execution
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
1 parent d0a6cc2 commit 693e281

4 files changed

Lines changed: 73 additions & 4 deletions

File tree

changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ Features:
55
---------
66
* Add support for `\\T` prompt escape sequence to display transaction status (similar to psql's `%x`).
77

8+
Bug Fixes:
9+
----------
10+
* Fix trailing SQL comments preventing query submission and execution.
11+
* ``SELECT 1; -- note`` now submits correctly in multiline mode
12+
* ``rstrip(";")`` in ``pgexecute.py`` now handles comments after the semicolon
13+
814
4.4.0 (2025-12-24)
915
==================
1016

pgcli/pgbuffer.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22

3+
import sqlparse
34
from prompt_toolkit.enums import DEFAULT_BUFFER
45
from prompt_toolkit.filters import Condition
56
from prompt_toolkit.application import get_app
@@ -11,8 +12,11 @@
1112
def _is_complete(sql):
1213
# A complete command is an sql statement that ends with a semicolon, unless
1314
# there's an open quote surrounding it, as is common when writing a
14-
# CREATE FUNCTION command
15-
return sql.endswith(";") and not is_open_quote(sql)
15+
# CREATE FUNCTION command.
16+
# Strip trailing comments so that "SELECT 1; -- note" is recognized as
17+
# complete (the semicolon is not at the end when a comment follows).
18+
stripped = sqlparse.format(sql, strip_comments=True).strip()
19+
return stripped.endswith(";") and not is_open_quote(sql)
1620

1721

1822
"""

pgcli/pgexecute.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,11 @@ def run(
361361
# run each sql query
362362
for sql in sqlarr:
363363
# Remove spaces, eol and semi-colons.
364-
sql = sql.rstrip(";")
365-
sql = sqlparse.format(sql, strip_comments=False).strip()
364+
# Strip comments first so rstrip(";") works when there are
365+
# trailing comments after the semicolon, e.g.:
366+
# vacuum freeze verbose t; -- 82% towards emergency
367+
sql = sqlparse.format(sql, strip_comments=True).strip().rstrip(";")
368+
sql = sql.strip()
366369
if not sql:
367370
continue
368371
try:

tests/test_trailing_comments.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Tests for SQL trailing comment handling.
2+
3+
Verifies that statements with comments after the semicolon are handled
4+
correctly in both the input buffer (pgbuffer) and query execution (pgexecute).
5+
"""
6+
7+
import pytest
8+
from pgcli.pgbuffer import _is_complete
9+
10+
11+
class TestIsCompleteWithTrailingComments:
12+
"""Test _is_complete() handles trailing SQL comments after semicolons."""
13+
14+
def test_simple_semicolon(self):
15+
assert _is_complete("SELECT 1;") is True
16+
17+
def test_no_semicolon(self):
18+
assert _is_complete("SELECT 1") is False
19+
20+
def test_trailing_single_line_comment(self):
21+
assert _is_complete("SELECT 1; -- a comment") is True
22+
23+
def test_trailing_block_comment(self):
24+
assert _is_complete("SELECT 1; /* block comment */") is True
25+
26+
def test_vacuum_with_comment(self):
27+
assert (
28+
_is_complete(
29+
"vacuum freeze verbose tpd.file_delivery; -- 82% towards emergency"
30+
)
31+
is True
32+
)
33+
34+
def test_comment_only(self):
35+
assert _is_complete("-- just a comment") is False
36+
37+
def test_semicolon_inside_string(self):
38+
assert _is_complete("SELECT ';'") is False
39+
40+
def test_semicolon_inside_string_with_trailing_comment(self):
41+
assert _is_complete("SELECT ';' FROM t; -- note") is True
42+
43+
def test_open_quote(self):
44+
assert _is_complete("SELECT '") is False
45+
46+
def test_empty_string(self):
47+
assert _is_complete("") is False
48+
49+
def test_multiple_semicolons_with_comment(self):
50+
assert _is_complete("SELECT 1; SELECT 2; -- done") is True
51+
52+
def test_comment_with_special_chars(self):
53+
assert (
54+
_is_complete("VACUUM ANALYZE; -- 81.0% towards emergency, 971 MB")
55+
is True
56+
)

0 commit comments

Comments
 (0)