From b8560ee9d413d8d0a8978ba661415f7cad7c31d1 Mon Sep 17 00:00:00 2001 From: Dylan Pulver Date: Wed, 2 Sep 2026 11:38:47 +0300 Subject: [PATCH] Keep delimited identifiers' case in identifier_case IdentifierCaseFilter exempted only double-quoted identifiers, but SQL_REGEX recognizes four quoting styles. The other three reach the filter as plain T.Name and had their case changed, so `MyTbl` became `MYTBL` (issue433). --- CHANGELOG | 7 ++++++- sqlparse/filters/tokens.py | 8 +++++++- tests/test_format.py | 12 ++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 44d5938e..d83b8340 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,12 @@ Development Version ------------------- -Nothing yet. +Bug Fixes + +* `identifier_case` no longer changes the case of identifiers quoted with + backticks, acute accents or square brackets. Only double-quoted identifiers + were exempt before, although the lexer recognizes all four quoting styles + (issue433). Release 0.6.0 (Aug 13, 2026) diff --git a/sqlparse/filters/tokens.py b/sqlparse/filters/tokens.py index cc00a844..b5ec4d49 100644 --- a/sqlparse/filters/tokens.py +++ b/sqlparse/filters/tokens.py @@ -29,9 +29,15 @@ class KeywordCaseFilter(_CaseFilter): class IdentifierCaseFilter(_CaseFilter): ttype = T.Name, T.String.Symbol + # Opening delimiters of a quoted identifier. SQL_REGEX recognizes four + # quoting styles, and only the double-quoted one reaches this filter as + # T.String.Symbol; the backtick, acute-accent and [bracket] forms arrive + # as plain T.Name, indistinguishable from a bare identifier here. + QUOTES = '"`´[' + def process(self, stream): for ttype, value in stream: - if ttype in self.ttype and value.strip()[0] != '"': + if ttype in self.ttype and value.strip()[0] not in self.QUOTES: value = self.convert(value) yield ttype, value diff --git a/tests/test_format.py b/tests/test_format.py index 93495067..237b7200 100644 --- a/tests/test_format.py +++ b/tests/test_format.py @@ -38,6 +38,18 @@ def test_identifiercase_quotes(self): res = sqlparse.format(sql, identifier_case="upper") assert res == 'select * from "foo"."bar"' + @pytest.mark.parametrize('sql', [ + 'select * from `myS`.`Tbl`', # MySQL + 'select * from ´myS´.´Tbl´', + 'select * from [myS].[Tbl]', # T-SQL / SQLite + ]) + @pytest.mark.parametrize('case', ['upper', 'lower', 'capitalize']) + def test_identifiercase_non_ansi_quotes(self, sql, case): + # issue433: a delimited identifier keeps its case whichever of the + # four quoting styles SQL_REGEX recognizes was used. The mixed-case + # names make every one of the three conversions observable. + assert sqlparse.format(sql, identifier_case=case) == sql + def test_strip_comments_single(self): sql = 'select *-- statement starts here\nfrom foo' res = sqlparse.format(sql, strip_comments=True)