Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[flake8]
exclude =
tests,
docs,
dist
max-complexity = 31
statistics = True
show-source = True
65 changes: 25 additions & 40 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This workflow will install Python dependencies, run tests and lint using uv
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Python application
Expand All @@ -8,56 +8,41 @@ on:
branches:
- master
pull_request:
branches: [ master ]
schedule:
- cron: '0 12 * * *'

jobs:
test:
name: Run tests on Python ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
name: Run tests on ${{ matrix.py }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- python-version: "3.10"
os: ubuntu-latest
- python-version: "3.11"
os: ubuntu-latest
- python-version: "3.12"
os: ubuntu-latest
- python-version: "3.13"
os: ubuntu-latest
- python-version: "3.14"
os: ubuntu-latest
# Test on additional platforms for Python 3.11
- python-version: "3.11"
os: macos-latest
- python-version: "3.11"
os: windows-latest
py:
#- "3.13-dev"
- "3.12"
- "3.11"
- "3.10"
- "3.9"
- "3.8"
- "pypy-3.10"
- "pypy-3.9"
- "pypy-3.8"

steps:
- uses: actions/checkout@v4

- name: Setup uv
uses: astral-sh/setup-uv@v5
- name: Set up Python ${{ matrix.py }}
uses: actions/setup-python@v5
with:
enable-cache: true
python-version: ${{ matrix.python-version }}

- name: Install dependencies and run tests
run: uv run --group dev pytest tests/

- name: Run lint (Python 3.11 only)
if: matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest'
run: uv run --group dev ruff check sqlparse/

- name: Generate coverage report (Python 3.11 only)
if: matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest'
python-version: ${{ matrix.py }}
allow-prereleases: true
check-latest: true
- name: Install dependencies
run: |
uv run --group dev coverage run -m pytest tests/
uv run --group dev coverage combine
uv run --group dev coverage xml

python -m pip install --upgrade pip hatch
- name: Lint with flake8
run: hatch run flake8
- name: Test with pytest and coverage
run: hatch run cov
- name: Publish to codecov
if: matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v4
78 changes: 78 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
python-sqlparse - Parse SQL statements
======================================

|buildstatus|_
|coverage|_
|docs|_
|packageversion|_

.. docincludebegin

sqlparse is a non-validating SQL parser for Python.
It provides support for parsing, splitting and formatting SQL statements.

The module is compatible with Python 3.8+ and released under the terms of the
`New BSD license <https://opensource.org/licenses/BSD-3-Clause>`_.

Visit the project page at https://github.com/andialbrecht/sqlparse for
further information about this project.


Quick Start
-----------

.. code-block:: sh

$ pip install sqlparse

.. code-block:: python

>>> import sqlparse

>>> # Split a string containing two SQL statements:
>>> raw = 'select * from foo; select * from bar;'
>>> statements = sqlparse.split(raw)
>>> statements
['select * from foo;', 'select * from bar;']

>>> # Format the first statement and print it out:
>>> first = statements[0]
>>> print(sqlparse.format(first, reindent=True, keyword_case='upper'))
SELECT *
FROM foo;

>>> # Parsing a SQL statement:
>>> parsed = sqlparse.parse('select * from foo')[0]
>>> parsed.tokens
[<DML 'select' at 0x7f22c5e15368>, <Whitespace ' ' at 0x7f22c5e153b0>, <Wildcard '*' … ]
>>>

Links
-----

Project page
https://github.com/andialbrecht/sqlparse

Bug tracker
https://github.com/andialbrecht/sqlparse/issues

Documentation
https://sqlparse.readthedocs.io/

Online Demo
https://sqlformat.org/


sqlparse is licensed under the BSD license.

Parts of the code are based on pygments written by Georg Brandl and others.
pygments-Homepage: http://pygments.org/

.. |buildstatus| image:: https://github.com/andialbrecht/sqlparse/actions/workflows/python-app.yml/badge.svg
.. _buildstatus: https://github.com/andialbrecht/sqlparse/actions/workflows/python-app.yml
.. |coverage| image:: https://codecov.io/gh/andialbrecht/sqlparse/branch/master/graph/badge.svg
.. _coverage: https://codecov.io/gh/andialbrecht/sqlparse
.. |docs| image:: https://readthedocs.org/projects/sqlparse/badge/?version=latest
.. _docs: https://sqlparse.readthedocs.io/en/latest/?badge=latest
.. |packageversion| image:: https://img.shields.io/pypi/v/sqlparse?color=%2334D058&label=pypi%20package
.. _packageversion: https://pypi.org/project/sqlparse
9 changes: 5 additions & 4 deletions sqlparse/filters/aligned_indent.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,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):
Expand Down
9 changes: 5 additions & 4 deletions sqlparse/filters/reindent.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,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

Expand Down
21 changes: 21 additions & 0 deletions tests/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,3 +784,24 @@ def test_strip_ws_removes_trailing_ws_in_groups(): # issue782
strip_whitespace=True)
expected = '(where foo = bar) from'
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