From a594e747a29052ec86a399c7080f6a363225ad97 Mon Sep 17 00:00:00 2001 From: adenzhou1350 <209601943+adenzhou1350@users.noreply.github.com> Date: Tue, 6 Oct 2026 14:59:01 +0800 Subject: [PATCH] fix: avoid duplicate breaks before aliased subqueries --- sqlparse/filters/reindent.py | 13 ++++- tests/test_aliased_subquery_reindent.py | 74 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/test_aliased_subquery_reindent.py diff --git a/sqlparse/filters/reindent.py b/sqlparse/filters/reindent.py index f8f64532..609317d2 100644 --- a/sqlparse/filters/reindent.py +++ b/sqlparse/filters/reindent.py @@ -191,7 +191,18 @@ def _process_identifierlist(self, tlist): if comma is None: continue token = comma - tlist.insert_before(token, self.nl(offset=adjust)) + first = (token.token_first() + if isinstance(token, sql.Identifier) + else token) + _, query = (first.token_next_by( + t=(T.Keyword.DML, T.Keyword.DDL)) + if isinstance(first, sql.Parenthesis) + else (None, None)) + # Query parentheses add their own leading newline. + # An alias wraps them in an Identifier, but must not + # cause the list to add a second break before it. + if query is None: + tlist.insert_before(token, self.nl(offset=adjust)) if self.comma_first: _, ws = tlist.token_next( tlist.token_index(token), skip_ws=False) diff --git a/tests/test_aliased_subquery_reindent.py b/tests/test_aliased_subquery_reindent.py new file mode 100644 index 00000000..7ef85cf8 --- /dev/null +++ b/tests/test_aliased_subquery_reindent.py @@ -0,0 +1,74 @@ +import pytest + +import sqlparse + + +@pytest.mark.parametrize('width', [2, 4]) +def test_aliased_subquery_has_one_leading_break(width): + # Issue 334: both the IdentifierList and the query Parenthesis added a + # newline when an alias wrapped the parenthesis in an Identifier. + source = ('select c.id, c.value, ' + '(select count(*) as cnt from d where d.item_id = id) ' + 'as "Count", c.whatever from something as c') + expected = '\n'.join([ + 'SELECT c.id,', + ' c.value,', + ' ' * width + '(SELECT count(*) AS cnt', + ' ' * (width + 1) + 'FROM d', + ' ' * (width + 1) + 'WHERE d.item_id = id) AS "Count",', + ' c.whatever', + 'FROM something AS c', + ]) + options = {'reindent': True, 'keyword_case': 'upper', 'indent_width': width} + assert sqlparse.format(source, **options) == expected + assert sqlparse.format(expected, **options) == expected + + +@pytest.mark.parametrize('alias', ['as counted', 'counted', 'as "Count"']) +def test_multiple_aliased_query_columns(alias): + source = f'select a, (select 1) {alias}, (select 2) othercol, b from tbl' + expected = '\n'.join([ + 'select a,', + f' (select 1) {alias},', + ' (select 2) othercol,', + ' b', + 'from tbl', + ]) + assert sqlparse.format(source, reindent=True) == expected + + +@pytest.mark.parametrize('source,expected', [ + ('select (select 1) as counted, c.id from something as c', + 'select\n (select 1) as counted,\n c.id\nfrom something as c'), + ('select (select 1) as counted from something as c', + 'select\n (select 1) as counted\nfrom something as c'), + ('select c.id, (c.value + 1) as counted, c.whatever from something as c', + 'select c.id,\n (c.value + 1) as counted,\n' + ' c.whatever\nfrom something as c'), + ('select * from (select 1) as counted', + 'select *\nfrom\n (select 1) as counted'), +]) +def test_other_parenthesis_layouts_are_unchanged(source, expected): + assert sqlparse.format(source, reindent=True) == expected + + +@pytest.mark.parametrize('options,expected', [ + ({'indent_tabs': True}, + 'select a,\n\t\t(select 1) as counted,\n\tb\nfrom tbl'), + ({'indent_columns': True}, + 'select\n a,\n (select 1) as counted,\n b\nfrom tbl'), + ({'wrap_after': 100}, + 'select a,\n (select 1) as counted, b\nfrom tbl'), + ({'comma_first': True}, + 'select a\n ,\n (select 1) as counted\n , b\nfrom tbl'), +]) +def test_aliased_subquery_format_options(options, expected): + source = 'select a, (select 1) as counted, b from tbl' + assert sqlparse.format(source, reindent=True, **options) == expected + + +def test_statement_separator_blank_line_is_retained(): + source = 'select a, (select 1) as counted, b from tbl; select 2;' + expected = ('select a,\n (select 1) as counted,\n b\n' + 'from tbl;\n\nselect 2;') + assert sqlparse.format(source, reindent=True) == expected