From: cjc0013 Date: Sat, 11 Jul 2026 19:09:37 +0000 (-0400) Subject: Use identifier preparer for string type collations X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=06fd857c553a9e9cc7b4f30d179ee7582655e7e7;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Use identifier preparer for string type collations 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 --- diff --git a/doc/build/changelog/unreleased_21/9693.rst b/doc/build/changelog/unreleased_21/9693.rst new file mode 100644 index 0000000000..dfb2407cc2 --- /dev/null +++ b/doc/build/changelog/unreleased_21/9693.rst @@ -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` diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 5a815bfa7b..8d0ac04035 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -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 diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 42b64ec726..e8dd677205 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -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: diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index a3ce81da58..459d44a650 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -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` diff --git a/test/sql/test_types.py b/test/sql/test_types.py index fd82a34ea0..2568417629 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -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