Skip to content

Commit 2893bd1

Browse files
committed
Parse double dollars (PostgreSQL) as literal strings (fixes andialbrecht#277).
1 parent b7a30d0 commit 2893bd1

File tree

6 files changed

+56
-5
lines changed

6 files changed

+56
-5
lines changed

CHANGELOG

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
Development Version
22
-------------------
33

4+
Notable Changes
5+
6+
* PostgreSQL: Function bodys are parsed as literal string. Previously
7+
sqlparse assumed that all function bodys are parsable psql
8+
strings (see issue277).
9+
410
Bug Fixes
511

612
* Fix a regression to parse streams again (issue273, reported and
713
test case by gmccreight).
814
* Improve Python 2/3 compatibility when using parsestream (isseu190,
915
by phdru).
16+
* Improve splitting of PostgreSQL functions (issue277).
1017

1118

1219
Release 0.2.0 (Jul 20, 2016)

sqlparse/keywords.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,22 @@
1010
from sqlparse import tokens
1111

1212

13-
def is_keyword(value):
13+
def is_keyword(value, remaining):
1414
val = value.upper()
1515
return (KEYWORDS_COMMON.get(val) or
1616
KEYWORDS_ORACLE.get(val) or
1717
KEYWORDS.get(val, tokens.Name)), value
1818

1919

20+
def parse_literal_string(value, remaining):
21+
try:
22+
end = remaining[len(value):].index(value)
23+
except ValueError:
24+
return tokens.Name.Builtin, value
25+
literal = remaining[:end + (len(value) * 2)]
26+
return tokens.Literal, literal
27+
28+
2029
SQL_REGEX = {
2130
'root': [
2231
(r'(--|# )\+.*?(\r\n|\r|\n|$)', tokens.Comment.Single.Hint),
@@ -35,7 +44,7 @@ def is_keyword(value):
3544

3645
(r"`(``|[^`])*`", tokens.Name),
3746
(r"´(´´|[^´])*´", tokens.Name),
38-
(r'\$([_A-Z]\w*)?\$', tokens.Name.Builtin),
47+
(r'\$([_A-Z]\w*)?\$', parse_literal_string),
3948

4049
(r'\?', tokens.Name.Placeholder),
4150
(r'%(\(\w+\))?s', tokens.Name.Placeholder),

sqlparse/lexer.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,14 @@ def get_tokens(text, encoding=None):
5050
if not m:
5151
continue
5252
elif isinstance(action, tokens._TokenType):
53+
consume_pos = m.end() - pos - 1
5354
yield action, m.group()
5455
elif callable(action):
55-
yield action(m.group())
56+
ttype, value = action(m.group(), text[pos:])
57+
consume_pos = len(value) - 1
58+
yield ttype, value
5659

57-
consume(iterable, m.end() - pos - 1)
60+
consume(iterable, consume_pos)
5861
break
5962
else:
6063
yield tokens.Error, char

tests/files/function_psql4.sql

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
CREATE FUNCTION doubledollarinbody(var1 text) RETURNS text
2+
/* see issue277 */
3+
LANGUAGE plpgsql
4+
AS $_$
5+
DECLARE
6+
str text;
7+
BEGIN
8+
str = $$'foo'$$||var1;
9+
execute 'select '||str into str;
10+
return str;
11+
END
12+
$_$;

tests/test_parse.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,22 @@ def test_stmt_tokens_parents():
384384
stmt = sqlparse.parse(s)[0]
385385
for token in stmt.tokens:
386386
assert token.has_ancestor(stmt)
387+
388+
389+
@pytest.mark.parametrize('sql, is_literal', [
390+
('$$foo$$', True),
391+
('$_$foo$_$', True),
392+
('$token$ foo $token$', True),
393+
# don't parse inner tokens
394+
('$_$ foo $token$bar$token$ baz$_$', True),
395+
('$A$ foo $B$', False) # tokens don't match
396+
])
397+
def test_dbldollar_as_literal(sql, is_literal):
398+
# see issue 277
399+
p = sqlparse.parse(sql)[0]
400+
if is_literal:
401+
assert len(p.tokens) == 1
402+
assert p.tokens[0].ttype == T.Literal
403+
else:
404+
for token in p.tokens:
405+
assert token.ttype != T.Literal

tests/test_split.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ def test_split_backslash():
2727
@pytest.mark.parametrize('fn', ['function.sql',
2828
'function_psql.sql',
2929
'function_psql2.sql',
30-
'function_psql3.sql'])
30+
'function_psql3.sql',
31+
'function_psql4.sql'])
3132
def test_split_create_function(load_file, fn):
3233
sql = load_file(fn)
3334
stmts = sqlparse.parse(sql)

0 commit comments

Comments
 (0)