--- /dev/null
+.. change::
+ :tags: bug, orm, regression
+ :tickets: 6419
+
+ Fixed regression where using :func:`_orm.selectinload` and
+ :func:`_orm.subqueryload` to load a two-level-deep path would lead to an
+ attribute error.
+
+.. change::
+ :tags: bug, orm, regression
+ :tickets: #6420
+
+ Fixed regression where using the :func:`_orm.noload` loader strategy in
+ conjunction with a "dynamic" relationship would lead to an attribute error
+ as the noload strategy would attempt to apply itself to the dynamic loader.
passive=attributes.PASSIVE_NO_INITIALIZE,
):
if not passive & attributes.SQL_OK:
- return self._get_collection_history(state, passive).added_items
+ data = self._get_collection_history(state, passive).added_items
else:
history = self._get_collection_history(state, passive)
- return history.added_plus_unchanged
+ data = history.added_plus_unchanged
+ return DynamicCollectionAdapter(data)
@util.memoized_property
def _append_token(self):
self.remove(state, dict_, value, initiator, passive=passive)
+class DynamicCollectionAdapter(object):
+ """simplified CollectionAdapter for internal API consistency"""
+
+ def __init__(self, data):
+ self.data = data
+
+ def __iter__(self):
+ return iter(self.data)
+
+ def _reset_empty(self):
+ pass
+
+ def __len__(self):
+ return len(self.data)
+
+ def __bool__(self):
+ return True
+
+ __nonzero__ = __bool__
+
+
class AppenderMixin(object):
query_class = None
(("lazy", "select"),)
).init_class_attribute(mapper)
- def _get_leftmost(self, subq_path, current_compile_state, is_root):
+ def _get_leftmost(
+ self,
+ orig_query_entity_index,
+ subq_path,
+ current_compile_state,
+ is_root,
+ ):
given_subq_path = subq_path
subq_path = subq_path.path
subq_mapper = orm_util._class_to_mapper(subq_path[0])
# of the current state. this is for the specific case of the entity
# is an AliasedClass against a subquery that's not otherwise going
# to adapt
-
new_subq_path = current_compile_state._entities[
- 0
+ orig_query_entity_index
].entity_zero._path_registry[leftmost_prop]
additional = len(subq_path) - len(new_subq_path)
if additional:
def _setup_query_from_rowproc(
self,
context,
+ query_entity,
path,
entity,
loadopt,
):
return
+ orig_query_entity_index = compile_state._entities.index(query_entity)
context.loaders_require_buffering = True
path = path[self.parent_property]
# if not via query option, check for
# a cycle
+ # TODO: why is this here??? this is now handled
+ # by the _check_recursive_postload call
if not path.contains(compile_state.attributes, "loader"):
if self.join_depth:
if (
leftmost_attr,
leftmost_relationship,
rewritten_path,
- ) = self._get_leftmost(rewritten_path, orig_compile_state, is_root)
+ ) = self._get_leftmost(
+ orig_query_entity_index,
+ rewritten_path,
+ orig_compile_state,
+ is_root,
+ )
# generate a new Query from the original, then
# produce a subquery from it.
subq = self._setup_query_from_rowproc(
context,
+ query_entity,
path,
path[-1],
loadopt,
from sqlalchemy.orm import configure_mappers
from sqlalchemy.orm import exc as orm_exc
from sqlalchemy.orm import mapper
+from sqlalchemy.orm import noload
from sqlalchemy.orm import Query
from sqlalchemy.orm import relationship
from sqlalchemy.testing import assert_raises
[],
)
+ @testing.combinations(
+ ("star",),
+ ("attronly",),
+ )
+ def test_noload_issue(self, type_):
+ """test #6420. a noload that hits the dynamic loader
+ should have no effect.
+
+ """
+
+ User, Address = self._user_address_fixture()
+
+ s = fixture_session()
+
+ if type_ == "star":
+ u1 = s.query(User).filter_by(id=7).options(noload("*")).first()
+ assert "name" not in u1.__dict__["name"]
+ elif type_ == "attronly":
+ u1 = (
+ s.query(User)
+ .filter_by(id=7)
+ .options(noload(User.addresses))
+ .first()
+ )
+
+ eq_(u1.__dict__["name"], "jack")
+
+ # noload doesn't affect a dynamic loader, because it has no state
+ eq_(list(u1.addresses), [Address(id=1)])
+
def test_m2m(self):
Order, Item = self._order_item_fixture(
items_args={"backref": backref("orders", lazy="dynamic")}
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import mapper
from sqlalchemy.orm import relationship
+from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
from sqlalchemy.orm import subqueryload
from sqlalchemy.orm import undefer
from sqlalchemy.testing import is_
from sqlalchemy.testing import is_not
from sqlalchemy.testing import is_true
+from sqlalchemy.testing.assertions import expect_warnings
from sqlalchemy.testing.assertsql import CompiledSQL
from sqlalchemy.testing.entities import ComparableEntity
from sqlalchemy.testing.fixtures import fixture_session
),
)
s.close()
+
+
+class Issue6149Test(fixtures.DeclarativeMappedTest):
+ @classmethod
+ def setup_classes(cls):
+ Base = cls.DeclarativeBasic
+
+ class Exam(ComparableEntity, Base):
+ __tablename__ = "exam"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String(50), nullable=False)
+ submissions = relationship(
+ "Submission", backref="exam", cascade="all", lazy=True
+ )
+
+ class Submission(ComparableEntity, Base):
+ __tablename__ = "submission"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ exam_id = Column(
+ Integer, ForeignKey("exam.id"), nullable=False
+ ) # backref exam
+ solutions = relationship(
+ "Solution",
+ backref="submission",
+ cascade="all",
+ order_by="Solution.id",
+ lazy=True,
+ )
+
+ class Solution(ComparableEntity, Base):
+ __tablename__ = "solution"
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ name = Column(String(50), nullable=False)
+ submission_id = Column(
+ Integer, ForeignKey("submission.id"), nullable=False
+ ) # backref submission
+
+ @classmethod
+ def insert_data(cls, connection):
+ Exam, Submission, Solution = cls.classes(
+ "Exam", "Submission", "Solution"
+ )
+
+ s = Session(connection)
+
+ e1 = Exam(
+ id=1,
+ name="e1",
+ submissions=[
+ Submission(
+ solutions=[Solution(name="s1"), Solution(name="s2")]
+ ),
+ Submission(
+ solutions=[Solution(name="s3"), Solution(name="s4")]
+ ),
+ ],
+ )
+
+ s.add(e1)
+ s.commit()
+
+ def test_issue_6419(self):
+ Exam, Submission, Solution = self.classes(
+ "Exam", "Submission", "Solution"
+ )
+
+ s = fixture_session()
+
+ for i in range(3):
+ # this warns because subqueryload is from the
+ # selectinload, which means we have to unwrap the
+ # selectinload query to see what its entities are.
+ with expect_warnings(r".*must invoke lambda callable"):
+
+ # the bug is that subqueryload looks at the query that
+ # selectinload created and assumed the "entity" was
+ # compile_state._entities[0], which in this case is a
+ # Bundle, it needs to look at compile_state._entities[1].
+ # so subqueryloader passes through orig_query_entity_index
+ # so it knows where to look.
+ ex1 = (
+ s.query(Exam)
+ .options(
+ selectinload(Exam.submissions).subqueryload(
+ Submission.solutions
+ )
+ )
+ .filter_by(id=1)
+ .first()
+ )
+
+ eq_(
+ ex1,
+ Exam(
+ name="e1",
+ submissions=[
+ Submission(
+ solutions=[
+ Solution(name="s1"),
+ Solution(name="s2"),
+ ]
+ ),
+ Submission(
+ solutions=[
+ Solution(name="s3"),
+ Solution(name="s4"),
+ ]
+ ),
+ ],
+ ),
+ )
+ s.close()