]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Use identifier preparer for string type collations
authorcjc0013 <cjc0013@users.noreply.github.com>
Sat, 11 Jul 2026 19:09:37 +0000 (15:09 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 13 Jul 2026 22:08:13 +0000 (18:08 -0400)
Adjusted string type collation rendering to use the dialect identifier
preparer rather than unconditional quoting.  This allows PostgreSQL
schema-qualified collation names to be passed using quoted_name with
quote=False.  Added a PostgreSQL dialect documentation section with
examples for schema-qualified collation usage.

Fixes: #9693
pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/13418
pull-request-sha: 8d806f8dd185df42a04e363a1c2617fdfca030ad
Change-Id: Icc5b2c17cc46c7134c5e0a32f700a977b6d2dd8a

doc/build/changelog/unreleased_21/9693.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/sqltypes.py
test/sql/test_types.py

diff --git a/doc/build/changelog/unreleased_21/9693.rst b/doc/build/changelog/unreleased_21/9693.rst
new file mode 100644 (file)
index 0000000..dfb2407
--- /dev/null
@@ -0,0 +1,13 @@
+.. change::
+    :tags: usecase, schema, postgresql
+    :tickets: 9693
+
+    Adjusted string type collation rendering to use the dialect identifier
+    preparer rather than unconditional quoting.  This allows PostgreSQL
+    schema-qualified collation names to be passed using
+    :class:`.quoted_name` with ``quote=False``.  Pull request courtesy
+    cjc0013.
+
+    .. seealso::
+
+        :ref:`postgresql_collation`
index 5a815bfa7ba646fc2fd760e6067b361fdb9e3a5d..8d0ac0403501d0d6e32861ca4ab3206b2d39a7fa 100644 (file)
@@ -1773,6 +1773,46 @@ itself:
 
 .. versionadded:: 1.4.0b2
 
+.. _postgresql_collation:
+
+Schema-Qualified Collation Names
+---------------------------------
+
+.. versionchanged:: 2.1  String type collation rendering now uses the
+   dialect's identifier preparer rather than surrounding the collation
+   name with double quotes unconditionally.  Simple lowercase names are
+   rendered unquoted; mixed-case or special-character names are quoted
+   automatically.  :class:`.quoted_name` with ``quote=True`` or
+   ``quote=False`` is now also interpreted.
+
+PostgreSQL supports collations that are qualified by a schema name, such as
+``pg_catalog.default`` or ``myschema.my_collation``.  When passing a
+schema-qualified collation to the :paramref:`.String.collation` parameter,
+use :class:`.quoted_name` with ``quote=False`` so that the dot separator
+is not quoted as part of the identifier::
+
+    from sqlalchemy import Column, Text
+    from sqlalchemy.sql import quoted_name
+
+    Table(
+        "my_table",
+        metadata,
+        Column(
+            "data",
+            Text(collation=quoted_name("pg_catalog.default", False)),
+        ),
+    )
+
+The above will render the column type as
+``TEXT COLLATE pg_catalog.default``.  Without :class:`.quoted_name`, the
+collation string would be quoted as a single identifier, producing
+``TEXT COLLATE "pg_catalog.default"``, which PostgreSQL would reject.
+
+.. seealso::
+
+    :class:`.quoted_name`
+
+    :paramref:`.String.collation`
 
 
 """  # noqa: E501
index 42b64ec726391b238bddfac8e4c804897197d06d..e8dd67720507d53e4122dc91c4150174c827b47b 100644 (file)
@@ -7687,7 +7687,9 @@ class GenericTypeCompiler(TypeCompiler):
         if length:
             text += f"({length})"
         if collation:
-            text += f' COLLATE "{collation}"'
+            text += (
+                f" COLLATE {self.dialect.identifier_preparer.quote(collation)}"
+            )
         return text
 
     def visit_CHAR(self, type_: sqltypes.CHAR, **kw: Any) -> str:
index a3ce81da583c5b4fba42dcdf1b270371f722eb23..459d44a650b65c47875b17dfe2e35b53a1d7c6d0 100644 (file)
@@ -237,6 +237,11 @@ class String(Concatenable, TypeEngine[str]):
             >>> print(select(cast("some string", String(collation="utf8"))))
             {printsql}SELECT CAST(:param_1 AS VARCHAR COLLATE utf8) AS anon_1
 
+          For backends that support qualified collation names, a
+          :class:`.quoted_name` with ``quote=False`` may be used to render the
+          qualified name without applying quotes to the separator.  See
+          :ref:`postgresql_collation` for a PostgreSQL example.
+
           .. note::
 
             In most cases, the :class:`.Unicode` or :class:`.UnicodeText`
index fd82a34ea0423907bf71adc6fee210903ebd028c..2568417629073953d8872b1bb394a5dcca89d81a 100644 (file)
@@ -78,6 +78,7 @@ from sqlalchemy.sql import ddl
 from sqlalchemy.sql import elements
 from sqlalchemy.sql import null
 from sqlalchemy.sql import operators
+from sqlalchemy.sql import quoted_name
 from sqlalchemy.sql import sqltypes
 from sqlalchemy.sql import table
 from sqlalchemy.sql import type_api
@@ -3492,7 +3493,7 @@ class ExpressionTest(
         (lambda c1: c1.like("qpr"), "q LIKE :q_1->BINDCAST->[TEXT]"),
         (
             lambda c2: c2.like("qpr"),
-            'q LIKE :q_1->BINDCAST->[TEXT COLLATE "xyz"]',
+            "q LIKE :q_1->BINDCAST->[TEXT COLLATE xyz]",
         ),
         (
             # new behavior, a type with no collation passed into collate()
@@ -3500,11 +3501,11 @@ class ExpressionTest(
             # on the right side bind-cast. previous to #11576 we'd only
             # get TEXT for the bindcast.
             lambda c1: collate(c1, "abc").like("qpr"),
-            '(q COLLATE abc) LIKE :param_1->BINDCAST->[TEXT COLLATE "abc"]',
+            "(q COLLATE abc) LIKE :param_1->BINDCAST->[TEXT COLLATE abc]",
         ),
         (
             lambda c2: collate(c2, "abc").like("qpr"),
-            '(q COLLATE abc) LIKE :param_1->BINDCAST->[TEXT COLLATE "abc"]',
+            "(q COLLATE abc) LIKE :param_1->BINDCAST->[TEXT COLLATE abc]",
         ),
         argnames="testcase,expected",
     )
@@ -3549,7 +3550,7 @@ class ExpressionTest(
         )
         self.assert_compile(
             c2.like("qpr"),
-            'q LIKE :q_1->BINDCAST->[TEXT COLLATE "xyz"]',
+            "q LIKE :q_1->BINDCAST->[TEXT COLLATE xyz]",
             dialect=renders_bind_cast,
         )
 
@@ -4033,6 +4034,20 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_text_collation(self):
         self.assert_compile(Text(collation="FOO"), 'TEXT COLLATE "FOO"')
 
+    @testing.combinations(
+        ("foo", "TEXT COLLATE foo"),
+        ("FooIdentifier", 'TEXT COLLATE "FooIdentifier"'),
+        ("foo identifier", 'TEXT COLLATE "foo identifier"'),
+        (quoted_name("foo", True), 'TEXT COLLATE "foo"'),
+        (
+            quoted_name("schema_name.collation_name", False),
+            "TEXT COLLATE schema_name.collation_name",
+        ),
+        argnames="collation,expected",
+    )
+    def test_text_collation_identifier_preparer(self, collation, expected):
+        self.assert_compile(Text(collation=collation), expected)
+
     def test_default_compile_pg_inet(self):
         self.assert_compile(
             dialects.postgresql.INET(), "INET", allow_dialect_select=True