From 04b466879bbfd24b57a52faff217dad0ca7d5503 Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:52:09 -0400 Subject: [PATCH] Honor comma_first for reindent_aligned formatting --- CHANGELOG | 6 +++++- docs/source/api.rst | 4 +++- sqlparse/filters/aligned_indent.py | 22 ++++++++++++++++++++-- sqlparse/formatter.py | 3 ++- tests/test_format.py | 21 +++++++++++++++++++++ 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 44d5938e..54ba52a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,11 @@ Development Version ------------------- -Nothing yet. +Enhancements + +* Honor the ``comma_first`` option when formatting with + ``reindent_aligned``, so the commas line up under the keywords + (issue490). Release 0.6.0 (Aug 13, 2026) diff --git a/docs/source/api.rst b/docs/source/api.rst index 7c6fd70a..e285d2f1 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -82,7 +82,9 @@ The :meth:`~sqlparse.format` function accepts the following keyword arguments. in a programming language. Allowed values are "python" and "php". ``comma_first`` - If ``True`` comma-first notation for column names is used. + If ``True`` comma-first notation for column names is used. This also + applies to ``reindent_aligned``, where the commas line up under the + keywords. Security and Performance Considerations diff --git a/sqlparse/filters/aligned_indent.py b/sqlparse/filters/aligned_indent.py index 6ac99d62..5e7f5c5a 100644 --- a/sqlparse/filters/aligned_indent.py +++ b/sqlparse/filters/aligned_indent.py @@ -22,11 +22,12 @@ class AlignedIndentFilter: 'UNION', 'VALUES', 'SET', 'BETWEEN', 'EXCEPT') - def __init__(self, char=' ', n='\n'): + def __init__(self, char=' ', n='\n', comma_first=False): self.n = n self.offset = 0 self.indent = 0 self.char = char + self.comma_first = comma_first self._max_kwd_len = len('select') def nl(self, offset=1): @@ -62,9 +63,26 @@ def _process_identifierlist(self, tlist): # columns being selected identifiers = list(tlist.get_identifiers()) identifiers.pop(0) - [tlist.insert_before(token, self.nl()) for token in identifiers] + for token in identifiers: + if self.comma_first: + self._break_before_comma(tlist, token) + else: + tlist.insert_before(token, self.nl()) self._process_default(tlist) + def _break_before_comma(self, tlist, token): + # comma-first puts the separator at the start of the next line, two + # columns left of where the aligned item sits, so the commas line up + # just under the keyword. + _, comma = tlist.token_prev(tlist.token_index(token)) + if comma is None: + return + tlist.insert_before(comma, self.nl(offset=-1)) + # keep a single space between the comma and the following item + _, ws = tlist.token_next(tlist.token_index(comma), skip_ws=False) + if ws is not None and ws.ttype is not T.Whitespace: + tlist.insert_after(comma, sql.Token(T.Whitespace, ' ')) + def _process_case(self, tlist): offset_ = len('case ') + len('when ') cases = tlist.get_cases(skip_ws=True) diff --git a/sqlparse/formatter.py b/sqlparse/formatter.py index 1fba2466..cf7755a0 100644 --- a/sqlparse/formatter.py +++ b/sqlparse/formatter.py @@ -182,7 +182,8 @@ def build_filter_stack(stack, options): if options.get('reindent_aligned', False): stack.enable_grouping() stack.stmtprocess.append( - filters.AlignedIndentFilter(char=options['indent_char'])) + filters.AlignedIndentFilter(char=options['indent_char'], + comma_first=options['comma_first'])) if options.get('right_margin'): stack.enable_grouping() diff --git a/tests/test_format.py b/tests/test_format.py index 93495067..f7b2c755 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -345,6 +345,27 @@ def test_window_functions(self): '(PARTITION BY b, c ORDER BY d DESC) as row_num', ' from table']) + def test_comma_first(self): + sql = ('select j.jobtitle, count(*), max(salary) as top ' + 'from employee e, job j where e.orig_salary < 43000 ' + 'group by j.jobtitle') + f = lambda s: sqlparse.format(s, reindent_aligned=True, + comma_first=True) + assert f(sql) == '\n'.join([ + 'select j.jobtitle', + ' , count(*)', + ' , max(salary) as top', + ' from employee e', + ' , job j', + ' where e.orig_salary < 43000', + ' group by j.jobtitle']) + + def test_comma_first_single_column(self): + # nothing to move to the front when there is only one column + f = lambda s: sqlparse.format(s, reindent_aligned=True, + comma_first=True) + assert f('select a from t') == 'select a\n from t' + class TestSpacesAroundOperators: @staticmethod