Skip to content

Commit a1900a3

Browse files
author
adarshsm
committed
Group ROLE as an identifier when used as a column name (issue798)
ROLE is a non-reserved keyword, so it can also be a column name, but the lexer always tokenized it as a keyword and `group_identifier` only wraps Name/String.Symbol tokens. As a result `SELECT a, role, b` left `role` as a bare keyword while `a` and `b` became Identifiers, so `get_identifiers()` returned it inconsistently. Simply dropping ROLE from the keyword list is not viable: it would make `CREATE ROLE r` parse as an identifier aliased to the role name. Instead, add `group_identifier_role`, which wraps ROLE in an Identifier only when it sits in an identifier list (flanked by a comma). Outside a list -- e.g. `CREATE ROLE`, `SET ROLE`, `DROP ROLE` -- there is no adjacent comma, so it keeps its keyword role. This mirrors the existing `('null', 'role')` special-case already present in `group_identifier_list`.
1 parent 60cdc64 commit a1900a3

3 files changed

Lines changed: 46 additions & 0 deletions

File tree

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ Bug Fixes
6161
when it is followed by a float literal written without a leading zero, so
6262
that `x BETWEEN .03 AND .06` parses its bounds as numbers (issue601, pr868
6363
by deepakganesh78).
64+
* Group `ROLE` as an identifier when it is used as a column name in an
65+
identifier list (e.g. `SELECT a, role, b`), so it appears alongside the
66+
other columns in `get_identifiers()`, while keeping it a keyword elsewhere
67+
such as `CREATE ROLE` (issue798, pr872 by adarshsm).
6468
* Fix a late-binding closure bug in `TokenList.token_not_matching`.
6569

6670
Other

sqlparse/engine/grouping.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,27 @@ def group_identifier(tlist):
254254
tidx, token = tlist.token_next_by(t=ttypes, idx=tidx)
255255

256256

257+
@recurse(sql.Identifier)
258+
def group_identifier_role(tlist):
259+
# ROLE is a non-reserved keyword, so it may also be a column name. When it
260+
# appears as a member of an identifier list (i.e. flanked by a comma) wrap
261+
# it in an Identifier, matching the neighbouring plain names, so consumers
262+
# such as ``get_identifiers()`` see a uniform Identifier rather than a bare
263+
# keyword token. Outside of a list (e.g. ``CREATE ROLE r``) there is no
264+
# adjacent comma, so it keeps its keyword role. See issue #798.
265+
m_role = T.Keyword, 'ROLE'
266+
tidx, token = tlist.token_next_by(m=m_role)
267+
while token:
268+
_, prev_ = tlist.token_prev(tidx)
269+
_, next_ = tlist.token_next(tidx)
270+
in_list = (prev_ is not None and prev_.match(T.Punctuation, ',')) or (
271+
next_ is not None and next_.match(T.Punctuation, ',')
272+
)
273+
if in_list:
274+
tlist.group_tokens(sql.Identifier, tidx, tidx)
275+
tidx, token = tlist.token_next_by(m=m_role, idx=tidx)
276+
277+
257278
@recurse(sql.Over)
258279
def group_over(tlist):
259280
tidx, token = tlist.token_next_by(m=sql.Over.M_OPEN)
@@ -457,6 +478,7 @@ def group(stmt):
457478
group_period,
458479
group_arrays,
459480
group_identifier,
481+
group_identifier_role,
460482
group_order,
461483
group_typecasts,
462484
group_tzcasts,

tests/test_grouping.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,26 @@ def test_grouping_identifier_list_with_inline_comments():
241241
assert isinstance(p.tokens[0].tokens[3], sql.Identifier)
242242

243243

244+
def test_grouping_identifier_list_with_role():
245+
# issue798: ROLE is a non-reserved keyword usable as a column name; in an
246+
# identifier list it should be an Identifier like its plain-name peers.
247+
p = sqlparse.parse('SELECT a, role, b FROM t')[0]
248+
assert isinstance(p.tokens[2], sql.IdentifierList)
249+
identifiers = list(p.tokens[2].get_identifiers())
250+
assert all(isinstance(i, sql.Identifier) for i in identifiers)
251+
assert [i.value for i in identifiers] == ['a', 'role', 'b']
252+
253+
254+
def test_grouping_role_keyword_outside_identifier_list():
255+
# issue798: ROLE keeps its keyword role outside an identifier list, so e.g.
256+
# CREATE ROLE is not misparsed as an identifier aliased to the role name.
257+
for sql_str in ('CREATE ROLE myrole', 'SET ROLE admin', 'DROP ROLE r1, r2'):
258+
p = sqlparse.parse(sql_str)[0]
259+
role = next(t for t in p.flatten() if t.value.upper() == 'ROLE')
260+
assert role.ttype is T.Keyword
261+
assert not isinstance(role.parent, sql.Identifier)
262+
263+
244264
def test_grouping_identifiers_with_operators():
245265
p = sqlparse.parse('a+b as c from table where (d-e)%2= 1')[0]
246266
assert len([x for x in p.flatten() if x.ttype == T.Name]) == 5

0 commit comments

Comments
 (0)