]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add support for reflection of collation in types on PostgreSQL
authorDenis Laxalde <denis@laxalde.org>
Tue, 8 Apr 2025 13:00:27 +0000 (15:00 +0200)
committerDenis Laxalde <denis@laxalde.org>
Tue, 15 Jul 2025 10:31:30 +0000 (12:31 +0200)
The 'collation' returned by PGDialect._columns_query() is None if it
matches the default collation for the type. On the other hand, if the
column collation matches the one of the database but is explicitly set
at column creation, the value is reflected.

Related to #6511.

doc/build/changelog/unreleased_20/6511.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/testing/requirements.py
test/dialect/postgresql/test_reflection.py
test/engine/test_reflection.py
test/requirements.py

diff --git a/doc/build/changelog/unreleased_20/6511.rst b/doc/build/changelog/unreleased_20/6511.rst
new file mode 100644 (file)
index 0000000..cb09349
--- /dev/null
@@ -0,0 +1,6 @@
+.. change::
+    :tags: usecase, postgresql
+    :tickets: 6511
+
+    Added support for reflection of collation in types for PostgreSQL.
+    Pull request courtesy Denis Laxalde.
index 43251ce2773aeaf49d34582fa4150d55dbf9d3ab..24311842ee0973c110b20e1e2e4359b0587d6b39 100644 (file)
@@ -3793,6 +3793,7 @@ class PGDialect(default.DefaultDialect):
                 pg_catalog.pg_attribute.c.attnotnull.label("not_null"),
                 pg_catalog.pg_class.c.relname.label("table_name"),
                 pg_catalog.pg_description.c.description.label("comment"),
+                pg_catalog.pg_collation.c.collname.label("collation"),
                 generated,
                 identity,
             )
@@ -3818,6 +3819,19 @@ class PGDialect(default.DefaultDialect):
                     == pg_catalog.pg_attribute.c.attnum,
                 ),
             )
+            .outerjoin(
+                pg_catalog.pg_type,
+                pg_catalog.pg_type.c.oid == pg_catalog.pg_attribute.c.atttypid,
+            )
+            .outerjoin(
+                pg_catalog.pg_collation,
+                sql.and_(
+                    pg_catalog.pg_attribute.c.attcollation
+                    != pg_catalog.pg_type.c.typcollation,
+                    pg_catalog.pg_collation.c.oid
+                    == pg_catalog.pg_attribute.c.attcollation,
+                ),
+            )
             .where(self._pg_class_relkind_condition(relkinds))
             .order_by(
                 pg_catalog.pg_class.c.relname, pg_catalog.pg_attribute.c.attnum
@@ -3873,6 +3887,7 @@ class PGDialect(default.DefaultDialect):
         domains: Dict[str, ReflectedDomain],
         enums: Dict[str, ReflectedEnum],
         type_description: str,
+        collation: Optional[str],
     ) -> sqltypes.TypeEngine[Any]:
         """
         Attempts to reconstruct a column type defined in ischema_names based
@@ -3972,6 +3987,7 @@ class PGDialect(default.DefaultDialect):
                     domains,
                     enums,
                     type_description="DOMAIN '%s'" % domain["name"],
+                    collation=domain["collation"],
                 )
                 args = (domain["name"], data_type)
 
@@ -4004,6 +4020,9 @@ class PGDialect(default.DefaultDialect):
             )
             return sqltypes.NULLTYPE
 
+        if collation is not None:
+            kwargs["collation"] = collation
+
         data_type = schema_type(*args, **kwargs)
         if array_dim >= 1:
             # postgres does not preserve dimensionality or size of array types.
@@ -4027,6 +4046,7 @@ class PGDialect(default.DefaultDialect):
                 domains,
                 enums,
                 type_description="column '%s'" % row_dict["name"],
+                collation=row_dict["collation"],
             )
 
             default = row_dict["default"]
index 2f208ec008ad92e6c24971a00ef5c9e9bb0ea451..830c4ace9519d0c84b6fbfee66755ffc0f64d21c 100644 (file)
@@ -682,6 +682,11 @@ class SuiteRequirements(Requirements):
         and their reflection"""
         return exclusions.closed()
 
+    @property
+    def column_collation_reflection(self):
+        """Indicates if the database support column collation reflection"""
+        return exclusions.open()
+
     @property
     def view_column_reflection(self):
         """target database must support retrieval of the columns in a view,
index 5dd8e00070d6a2397ae6766176816ccda33323ee..74c925ef901ab049714bceb330f09993e9319e80 100644 (file)
@@ -2732,6 +2732,7 @@ class CustomTypeReflectionTest(fixtures.TestBase):
                 "default": None,
                 "not_null": False,
                 "comment": None,
+                "collation": None,
                 "generated": "",
                 "identity_options": None,
             }
@@ -2774,6 +2775,7 @@ class CustomTypeReflectionTest(fixtures.TestBase):
                 "default": None,
                 "not_null": False,
                 "comment": None,
+                "collation": None,
                 "generated": "",
                 "identity_options": None,
             }
index 6ba130add341816742f1f69a3845d8bdf123dac9..6239cf231c5b6a7d07f85e8f6b04b9970e791e45 100644 (file)
@@ -36,6 +36,7 @@ from sqlalchemy.testing import in_
 from sqlalchemy.testing import is_
 from sqlalchemy.testing import is_false
 from sqlalchemy.testing import is_instance_of
+from sqlalchemy.testing import is_none
 from sqlalchemy.testing import is_not
 from sqlalchemy.testing import is_true
 from sqlalchemy.testing import mock
@@ -1336,6 +1337,28 @@ class ReflectionTest(fixtures.TestBase, ComparesTables):
         eq_(t3.comment, "t1 comment")
         eq_(t3.c.id.comment, "c1 comment")
 
+    @testing.requires.column_collation_reflection
+    def test_column_collation_reflection(self, connection, metadata):
+        m1 = metadata
+        Table(
+            "t",
+            m1,
+            Column("collated", sa.String(collation="C")),
+            Column("not_collated", sa.String()),
+        )
+        m1.create_all(connection)
+
+        m2 = MetaData()
+        t2 = Table("t", m2, autoload_with=connection)
+
+        eq_(t2.c.collated.type.collation, "C")
+        is_none(t2.c.not_collated.type.collation)
+
+        insp = inspect(connection)
+        collated, not_collated = insp.get_columns("t")
+        eq_(collated["type"].collation, "C")
+        is_none(not_collated["type"].collation)
+
     @testing.requires.check_constraint_reflection
     def test_check_constraint_reflection(self, connection, metadata):
         m1 = metadata
index 72b609f21f1e30037975a083808c13cc9d0c3e33..64da25abe303df59d09dd81f2f4584ae2b35db5d 100644 (file)
@@ -175,6 +175,10 @@ class DefaultRequirements(SuiteRequirements):
     def constraint_comment_reflection(self):
         return only_on(["postgresql"])
 
+    @property
+    def column_collation_reflection(self):
+        return only_on(["postgresql"])
+
     @property
     def unbounded_varchar(self):
         """Target database must support VARCHAR with no length"""