Skip to content

Commit 945c80c

Browse files
committed
fix: #543 more properly identify CREATE TABLE ... LIKE ... statements
1 parent 60486b9 commit 945c80c

File tree

5 files changed

+63
-29
lines changed

5 files changed

+63
-29
lines changed

sqlparse/engine/grouping.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def group_typecasts(tlist):
7777
def match(token):
7878
return token.match(T.Punctuation, '::')
7979

80-
def valid(token):
80+
def valid(token, idx):
8181
return token is not None
8282

8383
def post(tlist, pidx, tidx, nidx):
@@ -91,10 +91,10 @@ def group_tzcasts(tlist):
9191
def match(token):
9292
return token.ttype == T.Keyword.TZCast
9393

94-
def valid_prev(token):
94+
def valid_prev(token, idx):
9595
return token is not None
9696

97-
def valid_next(token):
97+
def valid_next(token, idx):
9898
return token is not None and (
9999
token.is_whitespace
100100
or token.match(T.Keyword, 'AS')
@@ -119,13 +119,13 @@ def match(token):
119119
def match_to_extend(token):
120120
return isinstance(token, sql.TypedLiteral)
121121

122-
def valid_prev(token):
122+
def valid_prev(token, idx):
123123
return token is not None
124124

125-
def valid_next(token):
125+
def valid_next(token, idx):
126126
return token is not None and token.match(*sql.TypedLiteral.M_CLOSE)
127127

128-
def valid_final(token):
128+
def valid_final(token, idx):
129129
return token is not None and token.match(*sql.TypedLiteral.M_EXTEND)
130130

131131
def post(tlist, pidx, tidx, nidx):
@@ -141,12 +141,12 @@ def group_period(tlist):
141141
def match(token):
142142
return token.match(T.Punctuation, '.')
143143

144-
def valid_prev(token):
144+
def valid_prev(token, idx):
145145
sqlcls = sql.SquareBrackets, sql.Identifier
146146
ttypes = T.Name, T.String.Symbol
147147
return imt(token, i=sqlcls, t=ttypes)
148148

149-
def valid_next(token):
149+
def valid_next(token, idx):
150150
# issue261, allow invalid next token
151151
return True
152152

@@ -166,10 +166,10 @@ def group_as(tlist):
166166
def match(token):
167167
return token.is_keyword and token.normalized == 'AS'
168168

169-
def valid_prev(token):
169+
def valid_prev(token, idx):
170170
return token.normalized == 'NULL' or not token.is_keyword
171171

172-
def valid_next(token):
172+
def valid_next(token, idx):
173173
ttypes = T.DML, T.DDL, T.CTE
174174
return not imt(token, t=ttypes) and token is not None
175175

@@ -183,7 +183,7 @@ def group_assignment(tlist):
183183
def match(token):
184184
return token.match(T.Assignment, ':=')
185185

186-
def valid(token):
186+
def valid(token, idx):
187187
return token is not None and token.ttype not in (T.Keyword,)
188188

189189
def post(tlist, pidx, tidx, nidx):
@@ -202,9 +202,9 @@ def group_comparison(tlist):
202202
ttypes = T_NUMERICAL + T_STRING + T_NAME
203203

204204
def match(token):
205-
return token.ttype == T.Operator.Comparison
205+
return imt(token, t=(T.Operator.Comparison), m=(T.Keyword, 'LIKE'))
206206

207-
def valid(token):
207+
def valid(token, idx):
208208
if imt(token, t=ttypes, i=sqlcls):
209209
return True
210210
elif token and token.is_keyword and token.normalized == 'NULL':
@@ -215,7 +215,22 @@ def valid(token):
215215
def post(tlist, pidx, tidx, nidx):
216216
return pidx, nidx
217217

218-
valid_prev = valid_next = valid
218+
def valid_next(token, idx):
219+
return valid(token, idx)
220+
221+
def valid_prev(token, idx):
222+
# https://dev.mysql.com/doc/refman/8.0/en/create-table-like.html
223+
# LIKE is usually a compatarator, except when used in
224+
# `CREATE TABLE x LIKE y` statements, Check if we are
225+
# constructing a table - otherwise assume it is indeed a comparator
226+
two_tokens_back_idx = idx - 3
227+
if two_tokens_back_idx >= 0:
228+
_, two_tokens_back = tlist.token_next(two_tokens_back_idx)
229+
if imt(two_tokens_back, m=(T.Keyword, 'TABLE')):
230+
return False
231+
232+
return valid(token, idx)
233+
219234
_group(tlist, sql.Comparison, match,
220235
valid_prev, valid_next, post, extend=False)
221236

@@ -237,10 +252,10 @@ def group_arrays(tlist):
237252
def match(token):
238253
return isinstance(token, sql.SquareBrackets)
239254

240-
def valid_prev(token):
255+
def valid_prev(token, idx):
241256
return imt(token, i=sqlcls, t=ttypes)
242257

243-
def valid_next(token):
258+
def valid_next(token, idx):
244259
return True
245260

246261
def post(tlist, pidx, tidx, nidx):
@@ -258,7 +273,7 @@ def group_operator(tlist):
258273
def match(token):
259274
return imt(token, t=(T.Operator, T.Wildcard))
260275

261-
def valid(token):
276+
def valid(token, idx):
262277
return imt(token, i=sqlcls, t=ttypes) \
263278
or (token and token.match(
264279
T.Keyword,
@@ -283,7 +298,7 @@ def group_identifier_list(tlist):
283298
def match(token):
284299
return token.match(T.Punctuation, ',')
285300

286-
def valid(token):
301+
def valid(token, idx):
287302
return imt(token, i=sqlcls, m=m_role, t=ttypes)
288303

289304
def post(tlist, pidx, tidx, nidx):
@@ -431,8 +446,8 @@ def group(stmt):
431446

432447

433448
def _group(tlist, cls, match,
434-
valid_prev=lambda t: True,
435-
valid_next=lambda t: True,
449+
valid_prev=lambda t, idx: True,
450+
valid_next=lambda t, idx: True,
436451
post=None,
437452
extend=True,
438453
recurse=True
@@ -454,7 +469,7 @@ def _group(tlist, cls, match,
454469

455470
if match(token):
456471
nidx, next_ = tlist.token_next(tidx)
457-
if prev_ and valid_prev(prev_) and valid_next(next_):
472+
if prev_ and valid_prev(prev_, pidx) and valid_next(next_, nidx):
458473
from_idx, to_idx = post(tlist, pidx, tidx, nidx)
459474
grp = tlist.group_tokens(cls, from_idx, to_idx, extend=extend)
460475

sqlparse/keywords.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@
8282
r'(EXPLODE|INLINE|PARSE_URL_TUPLE|POSEXPLODE|STACK)\b',
8383
tokens.Keyword),
8484
(r"(AT|WITH')\s+TIME\s+ZONE\s+'[^']+'", tokens.Keyword.TZCast),
85-
(r'(NOT\s+)?(LIKE|ILIKE|RLIKE)\b', tokens.Operator.Comparison),
85+
(r'(NOT\s+)(LIKE|ILIKE|RLIKE)\b', tokens.Operator.Comparison),
86+
(r'(ILIKE|RLIKE)\b', tokens.Operator.Comparison),
8687
(r'(NOT\s+)?(REGEXP)\b', tokens.Operator.Comparison),
8788
# Check for keywords, also returns tokens.Name if regex matches
8889
# but the match isn't a keyword.

tests/test_grouping.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,10 @@ def test_comparison_with_strings(operator):
498498
assert p.tokens[0].right.ttype == T.String.Single
499499

500500

501-
def test_like_and_ilike_comparison():
501+
@pytest.mark.parametrize('operator', (
502+
'LIKE', 'NOT LIKE', 'ILIKE', 'NOT ILIKE', 'RLIKE', 'NOT RLIKE'
503+
))
504+
def test_like_and_ilike_comparison(operator):
502505
def validate_where_clause(where_clause, expected_tokens):
503506
assert len(where_clause.tokens) == len(expected_tokens)
504507
for where_token, expected_token in zip(where_clause, expected_tokens):
@@ -513,22 +516,22 @@ def validate_where_clause(where_clause, expected_tokens):
513516
assert (isinstance(where_token, expected_ttype)
514517
and re.match(expected_value, where_token.value))
515518

516-
[p1] = sqlparse.parse("select * from mytable where mytable.mycolumn LIKE 'expr%' limit 5;")
519+
[p1] = sqlparse.parse(f"select * from mytable where mytable.mycolumn {operator} 'expr%' limit 5;")
517520
[p1_where] = [token for token in p1 if isinstance(token, sql.Where)]
518521
validate_where_clause(p1_where, [
519522
(T.Keyword, "where"),
520523
(T.Whitespace, None),
521-
(sql.Comparison, r"mytable.mycolumn LIKE.*"),
524+
(sql.Comparison, f"mytable.mycolumn {operator}.*"),
522525
(T.Whitespace, None),
523526
])
524527

525528
[p2] = sqlparse.parse(
526-
"select * from mytable where mycolumn NOT ILIKE '-expr' group by othercolumn;")
529+
f"select * from mytable where mycolumn {operator} '-expr' group by othercolumn;")
527530
[p2_where] = [token for token in p2 if isinstance(token, sql.Where)]
528531
validate_where_clause(p2_where, [
529532
(T.Keyword, "where"),
530533
(T.Whitespace, None),
531-
(sql.Comparison, r"mycolumn NOT ILIKE.*"),
534+
(sql.Comparison, f"mycolumn {operator}.*"),
532535
(T.Whitespace, None),
533536
])
534537

tests/test_regressions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,18 @@ def test_copy_issue672():
444444
p = sqlparse.parse('select * from foo')[0]
445445
copied = copy.deepcopy(p)
446446
assert str(p) == str(copied)
447+
448+
449+
def test_copy_issue543():
450+
tokens = sqlparse.parse('create table tab1.b like tab2')[0].tokens
451+
assert [(t.ttype, t.value) for t in tokens if t.ttype != T.Whitespace] == \
452+
[
453+
(T.DDL, 'create'),
454+
(T.Keyword, 'table'),
455+
(None, 'tab1.b'),
456+
(T.Keyword, 'like'),
457+
(None, 'tab2')
458+
]
459+
460+
comparison = sqlparse.parse('a LIKE "b"')[0].tokens[0]
461+
assert isinstance(comparison, sql.Comparison)

tests/test_tokenize.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,10 @@ def test_parse_window_as():
209209

210210

211211
@pytest.mark.parametrize('s', (
212-
"LIKE", "ILIKE", "NOT LIKE", "NOT ILIKE",
212+
"ILIKE", "NOT LIKE", "NOT ILIKE",
213213
"NOT LIKE", "NOT ILIKE",
214214
))
215-
def test_like_and_ilike_parsed_as_comparisons(s):
215+
def test_likeish_but_not_like_parsed_as_comparisons(s):
216216
p = sqlparse.parse(s)[0]
217217
assert len(p.tokens) == 1
218218
assert p.tokens[0].ttype == T.Operator.Comparison

0 commit comments

Comments
 (0)