]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
run declarative scan for non-mapped annotated if allow_unmapped
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 20 Oct 2023 14:19:35 +0000 (10:19 -0400)
committermike bayer <mike_mp@zzzcomputing.com>
Fri, 20 Oct 2023 18:13:35 +0000 (18:13 +0000)
Fixed issue where the ``__allow_unmapped__`` directive failed to allow for
legacy :class:`.Column` / :func:`.deferred` mappings that nonetheless had
annotations such as ``Any`` or a specific type without ``Mapped[]`` as
their type, without errors related to locating the attribute name.

issue is not ticketed yet but is coming

Fixes: #10516
Change-Id: I471d6838f4dcc113addd284a39a5bb0b885b64ea

doc/build/changelog/unreleased_20/10516.rst [new file with mode: 0644]
lib/sqlalchemy/orm/decl_base.py
test/orm/declarative/test_tm_future_annotations_sync.py
test/orm/declarative/test_typed_mapping.py

diff --git a/doc/build/changelog/unreleased_20/10516.rst b/doc/build/changelog/unreleased_20/10516.rst
new file mode 100644 (file)
index 0000000..fadd12c
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 10516
+
+    Fixed issue where the ``__allow_unmapped__`` directive failed to allow for
+    legacy :class:`.Column` / :func:`.deferred` mappings that nonetheless had
+    annotations such as ``Any`` or a specific type without ``Mapped[]`` as
+    their type, without errors related to locating the attribute name.
index 9d10599499a4c6024ef1387641966e5cccb0e612..d5ef3db470ac1d03b7830c93d583253868520474 100644 (file)
@@ -1434,9 +1434,9 @@ class _ClassScanMapperConfig(_MapperConfig):
             cls, "_sa_decl_prepare_nocascade", strict=True
         )
 
+        allow_unmapped_annotations = self.allow_unmapped_annotations
         expect_annotations_wo_mapped = (
-            self.allow_unmapped_annotations
-            or self.is_dataclass_prior_to_mapping
+            allow_unmapped_annotations or self.is_dataclass_prior_to_mapping
         )
 
         look_for_dataclass_things = bool(self.dataclass_setup_arguments)
@@ -1531,7 +1531,15 @@ class _ClassScanMapperConfig(_MapperConfig):
                     # Mapped[] etc. were not used.  If annotation is None,
                     # do declarative_scan so that the property can raise
                     # for required
-                    if mapped_container is not None or annotation is None:
+                    if (
+                        mapped_container is not None
+                        or annotation is None
+                        # issue #10516: need to do declarative_scan even with
+                        # a non-Mapped annotation if we are doing
+                        # __allow_unmapped__, for things like col.name
+                        # assignment
+                        or allow_unmapped_annotations
+                    ):
                         try:
                             value.declarative_scan(
                                 self,
index 03c758352f2209beb8c391288d0764d16164e85f..ec5f5e8209775a280d9d169e35a4c778dbcb8729 100644 (file)
@@ -373,6 +373,45 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
             status: int
 
+    @testing.variation("annotation", ["none", "any", "datatype"])
+    @testing.variation("explicit_name", [True, False])
+    @testing.variation("attribute", ["column", "deferred"])
+    def test_allow_unmapped_cols(self, annotation, explicit_name, attribute):
+        class Base(DeclarativeBase):
+            __allow_unmapped__ = True
+
+        if attribute.column:
+            if explicit_name:
+                attr = Column("data_one", Integer)
+            else:
+                attr = Column(Integer)
+        elif attribute.deferred:
+            if explicit_name:
+                attr = deferred(Column("data_one", Integer))
+            else:
+                attr = deferred(Column(Integer))
+        else:
+            attribute.fail()
+
+        class MyClass(Base):
+            __tablename__ = "mytable"
+
+            id: Mapped[int] = mapped_column(primary_key=True)
+
+            if annotation.none:
+                data = attr
+            elif annotation.any:
+                data: Any = attr
+            elif annotation.datatype:
+                data: int = attr
+            else:
+                annotation.fail()
+
+        if explicit_name:
+            eq_(MyClass.__table__.c.keys(), ["id", "data_one"])
+        else:
+            eq_(MyClass.__table__.c.keys(), ["id", "data"])
+
     def test_column_default(self, decl_base):
         class MyClass(decl_base):
             __tablename__ = "mytable"
index 4202166fbc0e16799efd40437b80ec2a7bea0d1f..6b8becf9c02be7e68aa09972b6c52142732d8232 100644 (file)
@@ -364,6 +364,45 @@ class MappedColumnTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
             status: int
 
+    @testing.variation("annotation", ["none", "any", "datatype"])
+    @testing.variation("explicit_name", [True, False])
+    @testing.variation("attribute", ["column", "deferred"])
+    def test_allow_unmapped_cols(self, annotation, explicit_name, attribute):
+        class Base(DeclarativeBase):
+            __allow_unmapped__ = True
+
+        if attribute.column:
+            if explicit_name:
+                attr = Column("data_one", Integer)
+            else:
+                attr = Column(Integer)
+        elif attribute.deferred:
+            if explicit_name:
+                attr = deferred(Column("data_one", Integer))
+            else:
+                attr = deferred(Column(Integer))
+        else:
+            attribute.fail()
+
+        class MyClass(Base):
+            __tablename__ = "mytable"
+
+            id: Mapped[int] = mapped_column(primary_key=True)
+
+            if annotation.none:
+                data = attr
+            elif annotation.any:
+                data: Any = attr
+            elif annotation.datatype:
+                data: int = attr
+            else:
+                annotation.fail()
+
+        if explicit_name:
+            eq_(MyClass.__table__.c.keys(), ["id", "data_one"])
+        else:
+            eq_(MyClass.__table__.c.keys(), ["id", "data"])
+
     def test_column_default(self, decl_base):
         class MyClass(decl_base):
             __tablename__ = "mytable"