Skip to content

Commit 61be617

Browse files
committed
fix: support PostgreSQL dollar-quoted strings in statement splitter
1 parent 73204ea commit 61be617

2 files changed

Lines changed: 78 additions & 1 deletion

File tree

sqlit/domains/query/app/multi_statement.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,33 @@
1919
def _iter_sql_chars(sql: str) -> Iterator[tuple[int, str, bool]]:
2020
"""Iterate through SQL characters, tracking string literal context.
2121
22-
Handles escape sequences (backslash) and SQL-style doubled quotes.
22+
Handles escape sequences (backslash), SQL-style doubled quotes,
23+
and PostgreSQL dollar-quoted strings ($$ or $tag$).
2324
2425
Yields:
2526
(index, char, outside_string) tuples where outside_string is True
2627
when the character is not inside a string literal.
2728
"""
2829
in_single_quote = False
2930
in_double_quote = False
31+
in_dollar_tag: str | None = None
3032
i = 0
3133

3234
while i < len(sql):
35+
# If inside a dollar-quoted string, check for the closing tag
36+
if in_dollar_tag is not None:
37+
if sql[i:].startswith(in_dollar_tag):
38+
# Yield the characters of the closing tag as inside string
39+
for offset in range(len(in_dollar_tag)):
40+
yield (i + offset, sql[i + offset], False)
41+
i += len(in_dollar_tag)
42+
in_dollar_tag = None
43+
continue
44+
else:
45+
yield (i, sql[i], False)
46+
i += 1
47+
continue
48+
3349
char = sql[i]
3450

3551
# Handle escape sequences in strings
@@ -51,6 +67,18 @@ def _iter_sql_chars(sql: str) -> Iterator[tuple[int, str, bool]]:
5167
i += 2
5268
continue
5369

70+
# Check for PostgreSQL dollar-quoted string start
71+
if char == "$" and not in_single_quote and not in_double_quote:
72+
# Match $[a-zA-Z_][a-zA-Z0-9_]*$ or $$
73+
match = re.match(r"^\$([a-zA-Z_][a-zA-Z0-9_]*)?\$", sql[i:])
74+
if match:
75+
delimiter = match.group(0)
76+
in_dollar_tag = delimiter
77+
for offset in range(len(delimiter)):
78+
yield (i + offset, sql[i + offset], False)
79+
i += len(delimiter)
80+
continue
81+
5482
# Toggle quote state and yield
5583
if char == "'" and not in_double_quote:
5684
in_single_quote = not in_single_quote

tests/unit/test_multi_statement.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,55 @@ def test_handles_multiline_statements(self):
7676

7777
assert len(statements) == 2
7878

79+
def test_preserves_semicolons_in_dollar_quoted_strings(self):
80+
"""Should not split on semicolons inside dollar-quoted strings."""
81+
from sqlit.domains.query.app.multi_statement import split_statements
82+
83+
query = """
84+
CREATE OR REPLACE FUNCTION example()
85+
RETURNS void AS $$
86+
BEGIN
87+
INSERT INTO t (x) VALUES ('a;b');
88+
END;
89+
$$ LANGUAGE plpgsql;
90+
SELECT 1;
91+
"""
92+
statements = split_statements(query)
93+
94+
assert len(statements) == 2
95+
assert "CREATE OR REPLACE FUNCTION" in statements[0]
96+
assert "SELECT 1" in statements[1]
97+
98+
def test_preserves_semicolons_in_named_dollar_quoted_strings(self):
99+
"""Should not split on semicolons inside named dollar-quoted strings."""
100+
from sqlit.domains.query.app.multi_statement import split_statements
101+
102+
query = """
103+
CREATE OR REPLACE FUNCTION example()
104+
RETURNS void AS $func_tag$
105+
BEGIN
106+
INSERT INTO t (x) VALUES ('a;b');
107+
END;
108+
$func_tag$ LANGUAGE plpgsql;
109+
SELECT 1;
110+
"""
111+
statements = split_statements(query)
112+
113+
assert len(statements) == 2
114+
assert "CREATE OR REPLACE FUNCTION" in statements[0]
115+
assert "SELECT 1" in statements[1]
116+
117+
def test_dollar_quotes_inside_standard_strings_are_ignored(self):
118+
"""Should ignore dollar quote delimiters when inside standard string literals."""
119+
from sqlit.domains.query.app.multi_statement import split_statements
120+
121+
query = "INSERT INTO t (x) VALUES ('$$'); SELECT 1"
122+
statements = split_statements(query)
123+
124+
assert len(statements) == 2
125+
assert "INSERT" in statements[0]
126+
assert "SELECT 1" in statements[1]
127+
79128

80129
class TestMultiStatementResult:
81130
"""Tests for MultiStatementResult data structure."""

0 commit comments

Comments
 (0)