--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 9564
+
+ Fixed issue where an annotation-only :class:`_orm.Mapped` directive could
+ not be used in a Declarative mixin class, without that attribute attempting
+ to take effect for single- or joined-inheritance subclasses of mapped
+ classes that had already mapped that attribute on a superclass, producing
+ conflicting column errors and/or warnings.
+
if not sa_dataclass_metadata_key:
def attribute_is_overridden(key: str, obj: Any) -> bool:
- return getattr(cls, key) is not obj
+ return getattr(cls, key, obj) is not obj
else:
# then test if this name is already handled and
# otherwise proceed to generate.
if not fixed_table:
- assert name in collected_attributes
+ assert (
+ name in collected_attributes
+ or attribute_is_overridden(name, None)
+ )
continue
else:
# here, the attribute is some other kind of
# copy mixin columns to the mapped class
for name, obj, annotation, is_dataclass in attributes_for_class():
+
if (
not fixed_table
and obj is None
and _is_mapped_annotation(annotation, cls, originating_class)
):
+ # obj is None means this is the annotation only path
+
+ if attribute_is_overridden(name, obj):
+ # perform same "overridden" check as we do for
+ # Column/MappedColumn, this is how a mixin col is not
+ # applied to an inherited subclass that does not have
+ # the mixin. the anno-only path added here for
+ # #9564
+ continue
+
collected_annotation = self._collect_annotation(
name, annotation, originating_class, True, obj
)
"ON company.company_id = person.company_id",
)
+ @testing.variation("anno_type", ["plain", "typemap", "annotated"])
+ @testing.variation("inh_type", ["single", "joined"])
+ def test_mixin_interp_on_inh(self, decl_base, inh_type, anno_type):
+
+ global anno_col
+
+ if anno_type.typemap:
+ anno_col = Annotated[str, 30]
+
+ decl_base.registry.update_type_annotation_map({anno_col: String})
+
+ class Mixin:
+ foo: Mapped[anno_col]
+
+ elif anno_type.annotated:
+ anno_col = Annotated[str, mapped_column(String)]
+
+ class Mixin:
+ foo: Mapped[anno_col]
+
+ else:
+
+ class Mixin:
+ foo: Mapped[str]
+
+ class Employee(Mixin, decl_base):
+ __tablename__ = "employee"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: Mapped[str]
+ type: Mapped[str]
+
+ __mapper_args__ = {
+ "polymorphic_on": "type",
+ "polymorphic_identity": "employee",
+ }
+
+ class Manager(Employee):
+ if inh_type.joined:
+ __tablename__ = "manager"
+
+ id: Mapped[int] = mapped_column( # noqa: A001
+ ForeignKey("employee.id"), primary_key=True
+ )
+
+ manager_data: Mapped[str] = mapped_column(nullable=True)
+
+ __mapper_args__ = {
+ "polymorphic_identity": "manager",
+ }
+
+ if inh_type.single:
+ self.assert_compile(
+ select(Manager),
+ "SELECT employee.id, employee.name, employee.type, "
+ "employee.foo, employee.manager_data FROM employee "
+ "WHERE employee.type IN (__[POSTCOMPILE_type_1])",
+ )
+ elif inh_type.joined:
+ self.assert_compile(
+ select(Manager),
+ "SELECT manager.id, employee.id AS id_1, employee.name, "
+ "employee.type, employee.foo, manager.manager_data "
+ "FROM employee JOIN manager ON employee.id = manager.id",
+ )
+ else:
+ inh_type.fail()
+
class WriteOnlyRelationshipTest(fixtures.TestBase):
def _assertions(self, A, B, lazy):
"ON company.company_id = person.company_id",
)
+ @testing.variation("anno_type", ["plain", "typemap", "annotated"])
+ @testing.variation("inh_type", ["single", "joined"])
+ def test_mixin_interp_on_inh(self, decl_base, inh_type, anno_type):
+
+ # anno only: global anno_col
+
+ if anno_type.typemap:
+ anno_col = Annotated[str, 30]
+
+ decl_base.registry.update_type_annotation_map({anno_col: String})
+
+ class Mixin:
+ foo: Mapped[anno_col]
+
+ elif anno_type.annotated:
+ anno_col = Annotated[str, mapped_column(String)]
+
+ class Mixin:
+ foo: Mapped[anno_col]
+
+ else:
+
+ class Mixin:
+ foo: Mapped[str]
+
+ class Employee(Mixin, decl_base):
+ __tablename__ = "employee"
+
+ id: Mapped[int] = mapped_column(primary_key=True)
+ name: Mapped[str]
+ type: Mapped[str]
+
+ __mapper_args__ = {
+ "polymorphic_on": "type",
+ "polymorphic_identity": "employee",
+ }
+
+ class Manager(Employee):
+ if inh_type.joined:
+ __tablename__ = "manager"
+
+ id: Mapped[int] = mapped_column( # noqa: A001
+ ForeignKey("employee.id"), primary_key=True
+ )
+
+ manager_data: Mapped[str] = mapped_column(nullable=True)
+
+ __mapper_args__ = {
+ "polymorphic_identity": "manager",
+ }
+
+ if inh_type.single:
+ self.assert_compile(
+ select(Manager),
+ "SELECT employee.id, employee.name, employee.type, "
+ "employee.foo, employee.manager_data FROM employee "
+ "WHERE employee.type IN (__[POSTCOMPILE_type_1])",
+ )
+ elif inh_type.joined:
+ self.assert_compile(
+ select(Manager),
+ "SELECT manager.id, employee.id AS id_1, employee.name, "
+ "employee.type, employee.foo, manager.manager_data "
+ "FROM employee JOIN manager ON employee.id = manager.id",
+ )
+ else:
+ inh_type.fail()
+
class WriteOnlyRelationshipTest(fixtures.TestBase):
def _assertions(self, A, B, lazy):