1919def _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
0 commit comments