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
7 changes: 6 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
Development Version
-------------------

Nothing yet.
Bug Fixes

* The time zone literal is no longer matched as part of the `AT TIME ZONE`
keyword. It was recased along with the keyword, so
`format("SELECT ts AT TIME ZONE 'Asia/Tokyo'", keyword_case='upper')`
returned `'ASIA/TOKYO'`, and a doubled quote in the literal split the token.


Release 0.6.0 (Aug 13, 2026)
Expand Down
2 changes: 1 addition & 1 deletion sqlparse/keywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ def find_delimited_spans(text):
(r'(LATERAL\s+VIEW\s+)'
r'(EXPLODE|INLINE|PARSE_URL_TUPLE|POSEXPLODE|STACK)\b',
tokens.Keyword),
(r"(AT|WITH')\s+TIME\s+ZONE\s+'[^']+'", tokens.Keyword.TZCast),
(r"AT\s+TIME\s+ZONE\b", tokens.Keyword.TZCast),
(r'(NOT\s+)?(LIKE|ILIKE|RLIKE)\b', tokens.Operator.Comparison),
(r'(NOT\s+)?(REGEXP)(\s+(BINARY))?\b', tokens.Operator.Comparison),
# Check for keywords, also returns tokens.Name if regex matches
Expand Down
16 changes: 16 additions & 0 deletions tests/test_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ def test_keywordcase(self):
res = sqlparse.format(sql.upper(), keyword_case='lower')
assert res == 'select * from BAR; -- SELECT FOO\n'

def test_keywordcase_leaves_tz_literal_alone(self):
# The TZCast rule matches the time zone literal as part of the keyword
# token, so recasing the token used to rewrite the literal too. 'UTC' is
# the only zone the existing tests use, and it survives either case.
sql = "SELECT ts AT TIME ZONE 'Asia/Tokyo'"
assert sqlparse.format(sql, keyword_case='upper') == sql
assert sqlparse.format(sql, keyword_case='lower') == \
"select ts at time zone 'Asia/Tokyo'"
assert sqlparse.format(sql, keyword_case='capitalize') == \
"Select ts At time zone 'Asia/Tokyo'"

sql = "SELECT ts AT TIME ZONE 'America/New_York'"
assert sqlparse.format(sql, keyword_case='upper') == sql
assert sqlparse.format(sql, keyword_case='lower') == \
"select ts at time zone 'America/New_York'"

def test_keywordcase_invalid_option(self):
sql = 'select * from bar; -- select foo\n'
with pytest.raises(SQLParseError):
Expand Down
15 changes: 14 additions & 1 deletion tests/test_tokenize.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,11 +234,24 @@ def test_near_like_and_ilike_parsed_appropriately(s):

@pytest.mark.parametrize('s', (
'AT TIME ZONE \'UTC\'',
'AT TIME ZONE \'Asia/Tokyo\'',
'AT TIME ZONE \'a\'\'b\'',
))
def test_parse_tzcast(s):
# The keyword and the time zone literal are separate tokens: the literal is
# data, so it has to stay a String for the filters to leave it alone.
p = sqlparse.parse(s)[0]
assert len(p.tokens) == 1
assert p.tokens[0].ttype == T.Keyword.TZCast
assert p.tokens[0].value == 'AT TIME ZONE'
assert p.tokens[-1].ttype == T.String.Single
assert str(p) == s


def test_parse_tzcast_groups_with_its_operand():
p = sqlparse.parse("SELECT ts AT TIME ZONE 'Asia/Tokyo'")[0]
identifier = p.tokens[-1]
assert isinstance(identifier, sql.Identifier)
assert str(identifier) == "ts AT TIME ZONE 'Asia/Tokyo'"


def test_cli_commands():
Expand Down