summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2023-04-27 16:48:25 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2023-04-29 01:07:53 -0400
commite7e09649761cfb4afc242c541ab403258e75edd5 (patch)
treedaa3360ec06e4c10dd2efcc80f4f51198c7b7424 /test
parent1329037bfed428e458547824a861ce1aa9df0c78 (diff)
downloadsqlalchemy-e7e09649761cfb4afc242c541ab403258e75edd5.tar.gz
improve natural_path usage in two places
Fixed loader strategy pathing issues where eager loaders such as :func:`_orm.joinedload` / :func:`_orm.selectinload` would fail to traverse fully for many-levels deep following a load that had a :func:`_orm.with_polymorphic` or similar construct as an interim member. Here we can take advantage of 2.0's refactoring of strategy_options to identify the "chop_path" concept can be simplified to work with "natural" paths alone. In addition, identified existing logic in PropRegistry that works fine, but needed the "is_unnatural" attribute to be more accurate for a given path, so we set that up front to True if the ancestor is_unnatural. Fixes: #9715 Change-Id: Ie6b3f55b6a23d0d32628afd22437094263745114
Diffstat (limited to 'test')
-rw-r--r--test/orm/inheritance/test_assorted_poly.py211
1 files changed, 211 insertions, 0 deletions
diff --git a/test/orm/inheritance/test_assorted_poly.py b/test/orm/inheritance/test_assorted_poly.py
index 4bebc9b10..a40a9ae74 100644
--- a/test/orm/inheritance/test_assorted_poly.py
+++ b/test/orm/inheritance/test_assorted_poly.py
@@ -3,6 +3,10 @@ These are generally tests derived from specific user issues.
"""
+from __future__ import annotations
+
+from typing import Optional
+
from sqlalchemy import exists
from sqlalchemy import ForeignKey
from sqlalchemy import func
@@ -17,10 +21,14 @@ from sqlalchemy.orm import aliased
from sqlalchemy.orm import class_mapper
from sqlalchemy.orm import column_property
from sqlalchemy.orm import contains_eager
+from sqlalchemy.orm import immediateload
from sqlalchemy.orm import join
from sqlalchemy.orm import joinedload
+from sqlalchemy.orm import Mapped
+from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import polymorphic_union
from sqlalchemy.orm import relationship
+from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import with_polymorphic
@@ -2554,3 +2562,206 @@ class Issue8168Test(AssertsCompiledSQL, fixtures.TestBase):
)
else:
scenario.fail()
+
+
+class PolyIntoSelfReferentialTest(
+ fixtures.DeclarativeMappedTest, AssertsExecutionResults
+):
+ """test for #9715"""
+
+ @classmethod
+ def setup_classes(cls):
+ Base = cls.DeclarativeBasic
+
+ class A(Base):
+ __tablename__ = "a"
+
+ id: Mapped[int] = mapped_column(
+ primary_key=True, autoincrement=True
+ )
+
+ rel_id: Mapped[int] = mapped_column(ForeignKey("related.id"))
+
+ related = relationship("Related")
+
+ class Related(Base):
+ __tablename__ = "related"
+
+ id: Mapped[int] = mapped_column(
+ primary_key=True, autoincrement=True
+ )
+ rel_data: Mapped[str]
+ type: Mapped[str] = mapped_column()
+
+ other_related_id: Mapped[int] = mapped_column(
+ ForeignKey("other_related.id")
+ )
+
+ other_related = relationship("OtherRelated")
+
+ __mapper_args__ = {
+ "polymorphic_identity": "related",
+ "polymorphic_on": type,
+ }
+
+ class SubRelated(Related):
+ __tablename__ = "sub_related"
+
+ id: Mapped[int] = mapped_column(
+ ForeignKey("related.id"), primary_key=True
+ )
+ sub_rel_data: Mapped[str]
+
+ __mapper_args__ = {"polymorphic_identity": "sub_related"}
+
+ class OtherRelated(Base):
+ __tablename__ = "other_related"
+
+ id: Mapped[int] = mapped_column(
+ primary_key=True, autoincrement=True
+ )
+ name: Mapped[str]
+
+ parent_id: Mapped[Optional[int]] = mapped_column(
+ ForeignKey("other_related.id")
+ )
+ parent = relationship("OtherRelated", lazy="raise", remote_side=id)
+
+ @classmethod
+ def insert_data(cls, connection):
+ A, SubRelated, OtherRelated = cls.classes(
+ "A", "SubRelated", "OtherRelated"
+ )
+
+ with Session(connection) as sess:
+
+ grandparent_otherrel1 = OtherRelated(name="GP1")
+ grandparent_otherrel2 = OtherRelated(name="GP2")
+
+ parent_otherrel1 = OtherRelated(
+ name="P1", parent=grandparent_otherrel1
+ )
+ parent_otherrel2 = OtherRelated(
+ name="P2", parent=grandparent_otherrel2
+ )
+
+ otherrel1 = OtherRelated(name="A1", parent=parent_otherrel1)
+ otherrel3 = OtherRelated(name="A2", parent=parent_otherrel2)
+
+ address1 = SubRelated(
+ rel_data="ST1", other_related=otherrel1, sub_rel_data="w1"
+ )
+ address3 = SubRelated(
+ rel_data="ST2", other_related=otherrel3, sub_rel_data="w2"
+ )
+
+ a1 = A(related=address1)
+ a2 = A(related=address3)
+
+ sess.add_all([a1, a2])
+ sess.commit()
+
+ def _run_load(self, *opt):
+ A = self.classes.A
+ stmt = select(A).options(*opt)
+
+ sess = fixture_session()
+ all_a = sess.scalars(stmt).all()
+
+ sess.close()
+
+ with self.assert_statement_count(testing.db, 0):
+ for a1 in all_a:
+ d1 = a1.related
+ d2 = d1.other_related
+ d3 = d2.parent
+ d4 = d3.parent
+ assert d4.name in ("GP1", "GP2")
+
+ @testing.variation("use_workaround", [True, False])
+ def test_workaround(self, use_workaround):
+ A, Related, SubRelated, OtherRelated = self.classes(
+ "A", "Related", "SubRelated", "OtherRelated"
+ )
+
+ related = with_polymorphic(Related, [SubRelated], flat=True)
+
+ opt = [
+ (
+ joinedload(A.related.of_type(related))
+ .joinedload(related.other_related)
+ .joinedload(
+ OtherRelated.parent,
+ )
+ )
+ ]
+ if use_workaround:
+ opt.append(
+ joinedload(
+ A.related,
+ Related.other_related,
+ OtherRelated.parent,
+ OtherRelated.parent,
+ )
+ )
+ else:
+ opt[0] = opt[0].joinedload(OtherRelated.parent)
+
+ self._run_load(*opt)
+
+ @testing.combinations(
+ (("joined", "joined", "joined", "joined"),),
+ (("selectin", "selectin", "selectin", "selectin"),),
+ (("selectin", "selectin", "joined", "joined"),),
+ (("selectin", "selectin", "joined", "selectin"),),
+ (("joined", "selectin", "joined", "selectin"),),
+ # TODO: immediateload (and lazyload) do not support the target item
+ # being a with_polymorphic. this seems to be a limitation in the
+ # current_path logic
+ # (("immediate", "joined", "joined", "joined"),),
+ argnames="loaders",
+ )
+ @testing.variation("use_wpoly", [True, False])
+ def test_all_load(self, loaders, use_wpoly):
+ A, Related, SubRelated, OtherRelated = self.classes(
+ "A", "Related", "SubRelated", "OtherRelated"
+ )
+
+ if use_wpoly:
+ related = with_polymorphic(Related, [SubRelated], flat=True)
+ else:
+ related = SubRelated
+
+ opt = None
+ for i, (load_type, element) in enumerate(
+ zip(
+ loaders,
+ [
+ A.related.of_type(related),
+ related.other_related,
+ OtherRelated.parent,
+ OtherRelated.parent,
+ ],
+ )
+ ):
+ if i == 0:
+ if load_type == "joined":
+ opt = joinedload(element)
+ elif load_type == "selectin":
+ opt = selectinload(element)
+ elif load_type == "immediate":
+ opt = immediateload(element)
+ else:
+ assert False
+ else:
+ assert opt is not None
+ if load_type == "joined":
+ opt = opt.joinedload(element)
+ elif load_type == "selectin":
+ opt = opt.selectinload(element)
+ elif load_type == "immediate":
+ opt = opt.immediateload(element)
+ else:
+ assert False
+
+ self._run_load(opt)