From fb292d8b4406363849047dbd320d18428154f95a Mon Sep 17 00:00:00 2001 From: lilu Date: Tue, 4 Aug 2026 11:18:30 +0800 Subject: [PATCH] Fix RecursionError in reindent filters for chained BETWEEN...AND _next_token in ReindentFilter and AlignedIndentFilter used recursion to skip BETWEEN...AND pairs. With ~1000 chained pairs (8000 tokens, under MAX_GROUPING_TOKENS) this exceeded Python's recursion limit, causing SQLParseError via FilterStack's RecursionError handler. Convert the recursive calls to an iterative while loop that skips all consecutive BETWEEN...AND pairs without growing the call stack. --- sqlparse/filters/aligned_indent.py | 9 +++++---- sqlparse/filters/reindent.py | 9 +++++---- tests/test_format.py | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/sqlparse/filters/aligned_indent.py b/sqlparse/filters/aligned_indent.py index dc609263..fccf6f61 100644 --- a/sqlparse/filters/aligned_indent.py +++ b/sqlparse/filters/aligned_indent.py @@ -89,11 +89,12 @@ def _process_case(self, tlist): def _next_token(self, tlist, idx=-1): split_words = T.Keyword, self.split_words, True tidx, token = tlist.token_next_by(m=split_words, idx=idx) - # treat "BETWEEN x and y" as a single statement - if token and token.normalized == 'BETWEEN': - tidx, token = self._next_token(tlist, tidx) + # Skip chained BETWEEN ... AND pairs iteratively to avoid + # RecursionError on inputs with many BETWEEN clauses. + while token and token.normalized == 'BETWEEN': + tidx, token = tlist.token_next_by(m=split_words, idx=tidx) if token and token.normalized == 'AND': - tidx, token = self._next_token(tlist, tidx) + tidx, token = tlist.token_next_by(m=split_words, idx=tidx) return tidx, token def _split_kwds(self, tlist): diff --git a/sqlparse/filters/reindent.py b/sqlparse/filters/reindent.py index 9fb232f0..f0c55bf6 100644 --- a/sqlparse/filters/reindent.py +++ b/sqlparse/filters/reindent.py @@ -57,11 +57,12 @@ def _next_token(self, tlist, idx=-1): m_split = T.Keyword, split_words, True tidx, token = tlist.token_next_by(m=m_split, idx=idx) - if token and token.normalized == 'BETWEEN': - tidx, token = self._next_token(tlist, tidx) - + # Skip chained BETWEEN ... AND pairs iteratively to avoid + # RecursionError on inputs with many BETWEEN clauses. + while token and token.normalized == 'BETWEEN': + tidx, token = tlist.token_next_by(m=m_split, idx=tidx) if token and token.normalized == 'AND': - tidx, token = self._next_token(tlist, tidx) + tidx, token = tlist.token_next_by(m=m_split, idx=tidx) return tidx, token diff --git a/tests/test_format.py b/tests/test_format.py index a616f360..c336772f 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -729,3 +729,24 @@ def test_format_json_ops(): # issue542 "select foo->'bar', foo->'bar';", reindent=True) expected = "select foo->'bar',\n foo->'bar';" assert formatted == expected + + +def test_format_chained_between_and(): + """Chained BETWEEN...AND pairs must not cause RecursionError. + + _next_token in ReindentFilter and AlignedIndentFilter previously + used recursion to skip BETWEEN...AND pairs. With ~1000 pairs + (8000 tokens, under MAX_GROUPING_TOKENS) this exceeded Python's + recursion limit. + """ + parts = ["x"] + for i in range(1000): + parts.append(f"BETWEEN {i} AND {i + 1}") + sql = "SELECT * FROM t WHERE " + " ".join(parts) + + result = sqlparse.format(sql, reindent=True) + assert "SELECT" in result + assert "BETWEEN" in result + + result_aligned = sqlparse.format(sql, reindent_aligned=True) + assert "SELECT" in result_aligned