Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ Bug Fixes
* Recognize ``MATERIALIZED`` as a keyword so it is parsed and formatted
consistently in ``CREATE MATERIALIZED VIEW`` statements (issue752).
* Fix ``get_real_name`` for names with more than two dotted parts (issue332).
* Keep a keyword such as ``BETWEEN`` from being reclassified as an identifier
when it is followed by a float literal written without a leading zero
(e.g. ``x BETWEEN .03 AND .06``), so the bounds are parsed as number tokens
(issue601).


Release 0.5.5 (Dec 19, 2025)
Expand Down
6 changes: 5 additions & 1 deletion sqlparse/keywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@
# see issue #39
# Spaces around period `schema . name` are valid identifier
# TODO: Spaces before period not implemented
(r'[A-ZÀ-Ü]\w*(?=\s*\.)', tokens.Name), # 'Name'.
# The negative lookahead ``(?!\d)`` keeps a following floating point
# literal such as ``.03`` from being mistaken for a member access, so a
# preceding keyword (e.g. ``BETWEEN``) is not reclassified as a name.
# See issue #601.
(r'[A-ZÀ-Ü]\w*(?=\s*\.(?!\d))', tokens.Name), # 'Name'.
# FIXME(atronah): never match,
# because `re.match` doesn't work with look-behind regexp feature
(r'(?<=\.)[A-ZÀ-Ü]\w*', tokens.Name), # .'Name'
Expand Down
28 changes: 28 additions & 0 deletions tests/test_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,34 @@ def test_materialized_view_issue752():
assert formatted == 'CREATE MATERIALIZED VIEW v AS SELECT 1'


@pytest.mark.parametrize('sql', [
'a BETWEEN .03 AND .06',
'a between .03 and .06',
])
def test_between_leading_dot_float_issue601(sql):
# A keyword followed by a float literal written without a leading zero
# (e.g. ``.03``) must not be reclassified as an identifier because of the
# following period. BETWEEN has to stay a keyword and the literals have to
# remain standalone number tokens.
p = sqlparse.parse(sql)[0]
kw = p.token_next_by(m=(T.Keyword, 'BETWEEN'))[1]
assert kw is not None
floats = list(p.flatten())
floats = [t for t in floats if t.ttype is T.Number.Float]
assert [t.value for t in floats] == ['.03', '.06']
# 'and' must remain a keyword too, not be absorbed into an identifier.
assert p.token_next_by(m=(T.Keyword, 'AND'))[1] is not None


def test_keyword_before_qualified_name_still_grouped():
# Regression guard for issue #601 fix: member access such as
# ``schema.name`` (with or without surrounding spaces) must keep working.
for stmt in ('select foo.bar', 'select foo . bar', 'select a.b.c'):
p = sqlparse.parse(stmt)[0]
ident = p.token_next_by(i=sql.Identifier)[1]
assert ident is not None


@pytest.fixture
def limit_recursion():
curr_limit = sys.getrecursionlimit()
Expand Down
Loading