From f1b99589650f676314b8ab3f2410a31b9005df5f Mon Sep 17 00:00:00 2001 From: Labib-Bin-Salam Date: Thu, 18 Jun 2026 10:40:36 +0100 Subject: [PATCH] Keep OFFSET/FETCH paging clause on one line in aligned indent The aligned-indent formatter matches its clause keywords as unanchored regular expressions (Token.match uses re.search), so `ON` matched inside `ONLY` and `SET` inside `OFFSET`. With reindent_aligned the `OFFSET ... FETCH NEXT n ROWS ONLY` clause was therefore broken across lines, leaving `ONLY` orphaned on its own indented line. Anchor the bare clause keywords with word boundaries and add OFFSET as an explicit clause opener, so the paging clause stays together on one aligned line (matching how LIMIT is handled). Fixes #842. --- sqlparse/filters/aligned_indent.py | 18 ++++++++++++------ tests/test_format.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/sqlparse/filters/aligned_indent.py b/sqlparse/filters/aligned_indent.py index 6ac99d62..b0436628 100644 --- a/sqlparse/filters/aligned_indent.py +++ b/sqlparse/filters/aligned_indent.py @@ -15,12 +15,18 @@ class AlignedIndentFilter: r'(INNER\s+|OUTER\s+|STRAIGHT\s+)?|' r'(CROSS\s+|NATURAL\s+)?)?JOIN\b') by_words = r'(GROUP|ORDER)\s+BY\b' - split_words = ('FROM', - join_words, 'ON', by_words, - 'WHERE', 'AND', 'OR', - 'HAVING', 'LIMIT', - 'UNION', 'VALUES', - 'SET', 'BETWEEN', 'EXCEPT') + # Keywords that start a new aligned line. They are matched as regular + # expressions (see ``_next_token``), and ``Token.match`` uses an + # unanchored ``search``, so each bare keyword is wrapped in word + # boundaries. Without them ``ON`` matches inside ``ONLY`` and ``SET`` + # inside ``OFFSET``, which broke the ``OFFSET ... FETCH NEXT n ROWS ONLY`` + # paging clause across lines (issue #842). + split_words = (r'\bFROM\b', + join_words, r'\bON\b', by_words, + r'\bWHERE\b', r'\bAND\b', r'\bOR\b', + r'\bHAVING\b', r'\bLIMIT\b', r'\bOFFSET\b', + r'\bUNION\b', r'\bVALUES\b', + r'\bSET\b', r'\bBETWEEN\b', r'\bEXCEPT\b') def __init__(self, char=' ', n='\n'): self.n = n diff --git a/tests/test_format.py b/tests/test_format.py index 0cdbcf88..9a7f20cb 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -345,6 +345,19 @@ def test_window_functions(self): '(PARTITION BY b, c ORDER BY d DESC) as row_num', ' from table']) + def test_offset_fetch(self): + # the OFFSET ... FETCH NEXT n ROWS ONLY paging clause should stay on + # a single aligned line and not be split by substring matches of the + # clause keywords (ON in ONLY, SET in OFFSET) -- issue #842 + sql = ('select id from tbl where id > 0 order by id asc ' + 'offset 0 rows fetch next 100 rows only') + assert self.formatter(sql) == '\n'.join([ + 'select id', + ' from tbl', + ' where id > 0', + ' order by id asc', + 'offset 0 rows fetch next 100 rows only']) + class TestSpacesAroundOperators: @staticmethod