From: Denis Laxalde Date: Tue, 8 Apr 2025 13:00:27 +0000 (+0200) Subject: Add support for reflection of collation in types on PostgreSQL X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=1bb3ad10e33b49760e8b68bab021716d53e7f0ca;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add support for reflection of collation in types on PostgreSQL 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. --- diff --git a/doc/build/changelog/unreleased_20/6511.rst b/doc/build/changelog/unreleased_20/6511.rst new file mode 100644 index 0000000000..cb093497d4 --- /dev/null +++ b/doc/build/changelog/unreleased_20/6511.rst @@ -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. diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 43251ce277..24311842ee 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -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"] diff --git a/lib/sqlalchemy/testing/requirements.py b/lib/sqlalchemy/testing/requirements.py index 2f208ec008..830c4ace95 100644 --- a/lib/sqlalchemy/testing/requirements.py +++ b/lib/sqlalchemy/testing/requirements.py @@ -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, diff --git a/test/dialect/postgresql/test_reflection.py b/test/dialect/postgresql/test_reflection.py index 5dd8e00070..74c925ef90 100644 --- a/test/dialect/postgresql/test_reflection.py +++ b/test/dialect/postgresql/test_reflection.py @@ -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, } diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index 6ba130add3..6239cf231c 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -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 diff --git a/test/requirements.py b/test/requirements.py index 72b609f21f..64da25abe3 100644 --- a/test/requirements.py +++ b/test/requirements.py @@ -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"""