diff --git a/sqlparse/engine/grouping.py b/sqlparse/engine/grouping.py index d8cfa9e..fae6983 100644 --- a/sqlparse/engine/grouping.py +++ b/sqlparse/engine/grouping.py @@ -309,13 +309,34 @@ def group_operator(tlist): def group_identifier_list(tlist): m_role = T.Keyword, ('null', 'role') + m_to = T.Keyword, 'TO' sqlcls = (sql.Function, sql.Case, sql.Identifier, sql.Comparison, sql.IdentifierList, sql.Operation) ttypes = (T_NUMERICAL + T_STRING + T_NAME + (T.Keyword, T.Comment, T.Wildcard)) + def is_rename_separator(token): + # In ``RENAME TABLE a TO b, c TO d`` the comma separates independent + # rename clauses, so the target of one clause (``b``) and the source + # of the next (``c``) must not be merged into a single IdentifierList. + # Detect that layout by the ``TO`` keywords flanking both sides of the + # comma and leave such a comma ungrouped (issue779). + parent = token.parent + if parent is None: + return False + cidx = parent.token_index(token) + pidx, prev_ = parent.token_prev(cidx) + nidx, next_ = parent.token_next(cidx) + if prev_ is None or next_ is None: + return False + _, before_prev = parent.token_prev(pidx) + _, after_next = parent.token_next(nidx) + return (before_prev is not None and before_prev.match(*m_to) + and after_next is not None and after_next.match(*m_to)) + def match(token): - return token.match(T.Punctuation, ',') + return token.match(T.Punctuation, ',') \ + and not is_rename_separator(token) def valid(token): return imt(token, i=sqlcls, m=m_role, t=ttypes) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index aca7f7b..83aced2 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -414,6 +414,30 @@ def test_issue562_tzcasts(): 'SELECT f(HOUR\n from bar AT TIME ZONE \'UTC\')\nfrom foo' +def test_issue779_rename_table_pairs(): + # The comma in ``RENAME TABLE a TO b, c TO d`` separates independent + # rename clauses. The target of one clause and the source of the next + # must stay separate Identifiers and not be merged into an IdentifierList. + p = sqlparse.parse('RENAME TABLE a TO b, c TO d')[0] + idents = [t for t in p.tokens if isinstance(t, sql.Identifier)] + assert [t.value for t in idents] == ['a', 'b', 'c', 'd'] + assert not [t for t in p.tokens if isinstance(t, sql.IdentifierList)] + comma = p.token_next_by(m=(T.Punctuation, ','))[1] + assert comma is not None and comma.parent is p + + # Backtick-quoted identifiers from the original report behave the same. + p = sqlparse.parse( + 'rename table `db`.`test1` to test2, `db`.`test3` to test4')[0] + idents = [t for t in p.tokens if isinstance(t, sql.Identifier)] + assert [t.value for t in idents] == [ + '`db`.`test1`', 'test2', '`db`.`test3`', 'test4'] + assert not [t for t in p.tokens if isinstance(t, sql.IdentifierList)] + + # A genuine ``TO`` target list (e.g. GRANT ... TO a, b) must still group. + p = sqlparse.parse('GRANT SELECT ON t TO a, b')[0] + assert p.token_next_by(i=sql.IdentifierList)[1] is not None + + def test_as_in_parentheses_indents(): # did raise NoneType has no attribute is_group in _process_parentheses formatted = sqlparse.format('(as foo)', reindent=True)