diff --git a/CHANGELOG b/CHANGELOG index 44d5938e..507e5c37 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,10 @@ Development Version ------------------- -Nothing yet. +Bug Fixes + +* Keep parenthesized expressions in comma-separated identifier lists + while preserving separate VALUES rows (issue343). Release 0.6.0 (Aug 13, 2026) diff --git a/sqlparse/engine/grouping.py b/sqlparse/engine/grouping.py index d8cfa9e1..39f1bb91 100644 --- a/sqlparse/engine/grouping.py +++ b/sqlparse/engine/grouping.py @@ -310,7 +310,7 @@ def post(tlist, pidx, tidx, nidx): def group_identifier_list(tlist): m_role = T.Keyword, ('null', 'role') sqlcls = (sql.Function, sql.Case, sql.Identifier, sql.Comparison, - sql.IdentifierList, sql.Operation) + sql.IdentifierList, sql.Operation, sql.Parenthesis) ttypes = (T_NUMERICAL + T_STRING + T_NAME + (T.Keyword, T.Comment, T.Wildcard)) @@ -318,6 +318,10 @@ def match(token): return token.match(T.Punctuation, ',') def valid(token): + if (isinstance(token, sql.Parenthesis) + and isinstance(token.parent, sql.Values)): + # VALUES rows are separate tuples, not select-list expressions. + return False return imt(token, i=sqlcls, m=m_role, t=ttypes) def post(tlist, pidx, tidx, nidx): @@ -468,8 +472,8 @@ def group(stmt): group_assignment, align_comments, - group_identifier_list, group_values, + group_identifier_list, ]: func(stmt) return stmt diff --git a/tests/test_format.py b/tests/test_format.py index 93495067..b36993c0 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -606,6 +606,16 @@ def test_identifier_and_functions(self): ' nvl(1)', 'from dual']) + @pytest.mark.parametrize('expressions', [ + ['a', 'b', '(a-b)'], + ['(a-b)', 'a', 'b'], + ['a', '(a-b)', 'b'], + ]) + def test_parenthesized_select_list(self, expressions): + query = f"SELECT {', '.join(expressions)} FROM bla" + expected = 'SELECT ' + ',\n '.join(expressions) + '\nFROM bla' + assert sqlparse.format(query, reindent=True) == expected + def test_insert_values(self): # issue 329 f = lambda sql: sqlparse.format(sql, reindent=True) diff --git a/tests/test_grouping.py b/tests/test_grouping.py index 20fab9b7..b045a33a 100644 --- a/tests/test_grouping.py +++ b/tests/test_grouping.py @@ -207,6 +207,46 @@ def test_grouping_identifier_list(): assert isinstance(p.tokens[0].tokens[1], sql.IdentifierList) +@pytest.mark.parametrize('expressions', [ + ['a', 'b', '(a-b)'], + ['(a-b)', 'a', 'b'], + ['a', '(a-b)', 'b'], + ['(a)', '(b)'], + ['a', '((b-c))'], + ['a', '(SELECT b FROM c)'], +]) +def test_grouping_parenthesized_select_list(expressions): + # issue343: parentheses must not split a comma-separated select list. + query = f"SELECT {', '.join(expressions)} FROM bla" + parsed = sqlparse.parse(query)[0] + _, identifiers = parsed.token_next_by(i=sql.IdentifierList) + assert identifiers is not None + assert [str(token) for token in identifiers.get_identifiers()] == expressions + assert str(parsed) == query + assert all(token.parent is identifiers for token in identifiers.tokens) + + +@pytest.mark.parametrize('rows', [ + ['(1, 2)', '(3, 4)', '(5, 6)'], + ['((1), (2))', '((3), (4))'], + ['((SELECT a, (b-c) FROM bar), 1)', '(2, 3)'], +]) +def test_grouping_values_rows_with_parenthesized_expressions(rows): + query = f"INSERT INTO foo VALUES {', '.join(rows)}" + parsed = sqlparse.parse(query)[0] + _, values = parsed.token_next_by(i=sql.Values) + assert values is not None + assert [str(token) for token in values.tokens + if isinstance(token, sql.Parenthesis)] == rows + assert not any(isinstance(token, sql.IdentifierList) + for token in values.tokens) + assert str(parsed) == query + for row in values.get_sublists(): + _, identifiers = row.token_next_by(i=sql.IdentifierList) + assert identifiers is not None + assert len(list(identifiers.get_identifiers())) == 2 + + def test_grouping_identifier_list_subquery(): """identifier lists should still work in subqueries with aliases""" p = sqlparse.parse("select * from ("