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
6 changes: 6 additions & 0 deletions changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
==================

Expand Down
8 changes: 6 additions & 2 deletions pgcli/pgbuffer.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)


"""
Expand Down
7 changes: 5 additions & 2 deletions pgcli/pgexecute.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
56 changes: 56 additions & 0 deletions tests/test_trailing_comments.py
Original file line number Diff line number Diff line change
@@ -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
)