From: Mike Bayer Date: Tue, 5 Aug 2025 18:05:49 +0000 (-0400) Subject: Fix use_existing_column with Annotated mapped_column in polymorphic inheritance X-Git-Tag: rel_2_0_43~7^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=ebd7c715fb9c1efd2675f83dcc21dca4581ea1c9;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Fix use_existing_column with Annotated mapped_column in polymorphic inheritance Fixed issue where :paramref:`_orm.mapped_column.use_existing_column` parameter in :func:`_orm.mapped_column` would not work when the :func:`_orm.mapped_column` is used inside of an ``Annotated`` type alias in polymorphic inheritance scenarios. The parameter is now properly recognized and processed during declarative mapping configuration. Fixes: #12787 Change-Id: I0505df3f3714434e98052c4488f6b1b1d2b1f755 (cherry picked from commit 14a18d4591818bfa5081c4286881cbd017dc186b) --- diff --git a/doc/build/changelog/unreleased_20/12787.rst b/doc/build/changelog/unreleased_20/12787.rst new file mode 100644 index 0000000000..44c36fe5f8 --- /dev/null +++ b/doc/build/changelog/unreleased_20/12787.rst @@ -0,0 +1,9 @@ +.. change:: + :tags: bug, orm + :tickets: 12787 + + Fixed issue where :paramref:`_orm.mapped_column.use_existing_column` + parameter in :func:`_orm.mapped_column` would not work when the + :func:`_orm.mapped_column` is used inside of an ``Annotated`` type alias in + polymorphic inheritance scenarios. The parameter is now properly recognized + and processed during declarative mapping configuration. diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index 2d1ec13f19..43c4aa362b 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -393,7 +393,9 @@ class CompositeProperty( self.composite_class = argument if is_dataclass(self.composite_class): - self._setup_for_dataclass(registry, cls, originating_module, key) + self._setup_for_dataclass( + decl_scan, registry, cls, originating_module, key + ) else: for attr in self.attrs: if ( @@ -437,6 +439,7 @@ class CompositeProperty( @util.preload_module("sqlalchemy.orm.decl_base") def _setup_for_dataclass( self, + decl_scan: _ClassScanMapperConfig, registry: _RegistryType, cls: Type[Any], originating_module: Optional[str], @@ -464,6 +467,7 @@ class CompositeProperty( if isinstance(attr, MappedColumn): attr.declarative_scan_for_composite( + decl_scan, registry, cls, originating_module, diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 164ae009b2..88540be6d9 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -663,20 +663,12 @@ class MappedColumn( # Column will be merged into it in _init_column_for_annotation(). return MappedColumn() - def declarative_scan( + def _adjust_for_existing_column( self, decl_scan: _ClassScanMapperConfig, - registry: _RegistryType, - cls: Type[Any], - originating_module: Optional[str], key: str, - mapped_container: Optional[Type[Mapped[Any]]], - annotation: Optional[_AnnotationScanType], - extracted_mapped_annotation: Optional[_AnnotationScanType], - is_dataclass_field: bool, - ) -> None: - column = self.column - + given_column: Column[_T], + ) -> Column[_T]: if ( self._use_existing_column and decl_scan.inherits @@ -688,10 +680,31 @@ class MappedColumn( ) supercls_mapper = class_mapper(decl_scan.inherits, False) - colname = column.name if column.name is not None else key - column = self.column = supercls_mapper.local_table.c.get( # type: ignore[assignment] # noqa: E501 - colname, column + colname = ( + given_column.name if given_column.name is not None else key ) + given_column = supercls_mapper.local_table.c.get( # type: ignore[assignment] # noqa: E501 + colname, given_column + ) + return given_column + + def declarative_scan( + self, + decl_scan: _ClassScanMapperConfig, + registry: _RegistryType, + cls: Type[Any], + originating_module: Optional[str], + key: str, + mapped_container: Optional[Type[Mapped[Any]]], + annotation: Optional[_AnnotationScanType], + extracted_mapped_annotation: Optional[_AnnotationScanType], + is_dataclass_field: bool, + ) -> None: + column = self.column + + column = self.column = self._adjust_for_existing_column( + decl_scan, key, self.column + ) if column.key is None: column.key = key @@ -708,6 +721,8 @@ class MappedColumn( self._init_column_for_annotation( cls, + decl_scan, + key, registry, extracted_mapped_annotation, originating_module, @@ -716,6 +731,7 @@ class MappedColumn( @util.preload_module("sqlalchemy.orm.decl_base") def declarative_scan_for_composite( self, + decl_scan: _ClassScanMapperConfig, registry: _RegistryType, cls: Type[Any], originating_module: Optional[str], @@ -726,12 +742,14 @@ class MappedColumn( decl_base = util.preloaded.orm_decl_base decl_base._undefer_column_name(param_name, self.column) self._init_column_for_annotation( - cls, registry, param_annotation, originating_module + cls, decl_scan, key, registry, param_annotation, originating_module ) def _init_column_for_annotation( self, cls: Type[Any], + decl_scan: _ClassScanMapperConfig, + key: str, registry: _RegistryType, argument: _AnnotationScanType, originating_module: Optional[str], @@ -778,6 +796,11 @@ class MappedColumn( use_args_from = None if use_args_from is not None: + + self.column = use_args_from._adjust_for_existing_column( + decl_scan, key, self.column + ) + if ( not self._has_insert_default and use_args_from.column.default is not None diff --git a/test/orm/declarative/test_tm_future_annotations_sync.py b/test/orm/declarative/test_tm_future_annotations_sync.py index 3a84853099..d789c90b04 100644 --- a/test/orm/declarative/test_tm_future_annotations_sync.py +++ b/test/orm/declarative/test_tm_future_annotations_sync.py @@ -2178,6 +2178,40 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL): else: is_(getattr(Element.__table__.c.data, paramname), override_value) + def test_use_existing_column_from_pep_593(self, decl_base): + """test #12787""" + + global Label + Label = Annotated[ + str, mapped_column(String(20), use_existing_column=True) + ] + + class A(decl_base): + __tablename__ = "table_a" + + id: Mapped[int] = mapped_column(primary_key=True) + discriminator: Mapped[int] + + __mapper_args__ = { + "polymorphic_on": "discriminator", + "polymorphic_abstract": True, + } + + class A_1(A): + label: Mapped[Label] + + __mapper_args__ = {"polymorphic_identity": 1} + + class A_2(A): + label: Mapped[Label] + + __mapper_args__ = {"polymorphic_identity": 2} + + is_(A_1.label.property.columns[0], A_2.label.property.columns[0]) + + eq_(A_1.label.property.columns[0].table, A.__table__) + eq_(A_2.label.property.columns[0].table, A.__table__) + @testing.variation( "union", [ diff --git a/test/orm/declarative/test_typed_mapping.py b/test/orm/declarative/test_typed_mapping.py index 750a1b34f5..78863bca81 100644 --- a/test/orm/declarative/test_typed_mapping.py +++ b/test/orm/declarative/test_typed_mapping.py @@ -2169,6 +2169,40 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL): else: is_(getattr(Element.__table__.c.data, paramname), override_value) + def test_use_existing_column_from_pep_593(self, decl_base): + """test #12787""" + + # anno only: global Label + Label = Annotated[ + str, mapped_column(String(20), use_existing_column=True) + ] + + class A(decl_base): + __tablename__ = "table_a" + + id: Mapped[int] = mapped_column(primary_key=True) + discriminator: Mapped[int] + + __mapper_args__ = { + "polymorphic_on": "discriminator", + "polymorphic_abstract": True, + } + + class A_1(A): + label: Mapped[Label] + + __mapper_args__ = {"polymorphic_identity": 1} + + class A_2(A): + label: Mapped[Label] + + __mapper_args__ = {"polymorphic_identity": 2} + + is_(A_1.label.property.columns[0], A_2.label.property.columns[0]) + + eq_(A_1.label.property.columns[0].table, A.__table__) + eq_(A_2.label.property.columns[0].table, A.__table__) + @testing.variation( "union", [