aliased vs. non-aliased specificity in loader options on lazy load

Issue #3963 resolved
Mike Bayer repo owner created an issue

all of these cases emit eager load for B.cs when they are specified as lazyload:

from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = 'a'
    id = Column(Integer, primary_key=True)
    bs = relationship("B")

class B(Base):
    __tablename__ = 'b'
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey('a.id'))
    cs = relationship("C")

class C(Base):
    __tablename__ = 'c'
    id = Column(Integer, primary_key=True)
    b_id = Column(ForeignKey('b.id'))

e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)
s.add(A(bs=[B(cs=[C()])]))
s.add(A())
s.commit()
s.close()

aa = aliased(A)
q = s.query(aa, A).filter(aa.id == 1).filter(A.id == 2).options(joinedload(A.bs).joinedload(B.cs))
for a, real_a in q:
    for b in a.bs:
        # was eager loaded, when should lazy load
        for c in b.cs:
            print c


s.close()

print "--------------------------------"

aa = aliased(A)
q = s.query(aa, A).filter(aa.id == 1).filter(A.id == 2).options(defaultload(A.bs).joinedload(B.cs))
for a, real_a in q:
    for b in a.bs:
        # was eager loaded, when should lazy load
        for c in b.cs:
            print c


s.close()

print "--------------------------------"

aa = aliased(A)
q = s.query(A, aa).filter(A.id == 1).filter(aa.id == 2).options(defaultload(aa.bs).joinedload(B.cs))
for a, real_a in q:
    for b in a.bs:
        # was eager loaded, when should lazy load
        for c in b.cs:
            print c

_chop_path is important here, this fixes:

diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py
index bae2b73..60ed2be 100644
--- a/lib/sqlalchemy/orm/strategy_options.py
+++ b/lib/sqlalchemy/orm/strategy_options.py
@@ -376,7 +376,8 @@ class _UnboundLoad(Load):
                         _WILDCARD_TOKEN,) and c_token != p_prop.key:
                     return None
             elif isinstance(c_token, PropComparator):
-                if c_token.property is not p_prop:
+                if c_token.property is not p_prop or \
+                        c_token._parententity is not p_mapper:
                     return None
         else:
             i += 1

Comments (4)

  1. Mike Bayer reporter

    Compare entities also on chop_path

    When comparing query._current_path to options, the path chop was not taking into account that the query or the options are against aliased classes that don't match the mapper.

    The issue does not seem to take place for the Load() version of _chop_path.

    Fixed bug to improve upon the specificity of loader options that take effect subsequent to the lazy load of a related entity, so that the loader options will match to an aliased or non-aliased entity more specifically if those options include entity information.

    Fixes: #3963 Change-Id: Ifdff37d579042fcc62bdeabce9e2413e9a03fbba

    → <<cset 86c43d2e0f0c>>

  2. Mike Bayer reporter
    • changed status to open

    regression:

    from sqlalchemy import *
    from sqlalchemy.orm import *
    from sqlalchemy.ext.declarative import declarative_base
    
    Base = declarative_base()
    
    
    class Milestone(Base):
        __tablename__ = 'milestone'
        id = Column(Integer, primary_key=True)
    
        milestone_type = Column(String(64), nullable=False)
        owner_id = Column(
            Integer,
            ForeignKey('owner.id', name='milestone_owner_id_owner_id_fkey'))
    
        owner = relationship('Owner', back_populates='milestone')
    
        __mapper_args__ = {
            'polymorphic_on': milestone_type,
        }
    
    
    class Sprint(Milestone):
        __tablename__ = 'sprint'
        id = Column(
            Integer,
            ForeignKey('milestone.id', name='sprint_id_milestone_id_fkey'),
            primary_key=True)
    
        __mapper_args__ = {
            'polymorphic_identity': 'sprint'
        }
    
    
    class Owner(Base):
        __tablename__ = 'owner'
        id = Column(Integer, primary_key=True)
    
        dept_id = Column(
            Integer,
            ForeignKey('dept.id', name='owner_dept_id_dept_id_fkey'))
        dept = relationship('Dept', back_populates='owner')
        milestone = relationship('Milestone', back_populates='owner')
    
    
    class Dept(Base):
        __tablename__ = 'dept'
        id = Column(Integer, primary_key=True)
        owner = relationship('Owner', back_populates='dept')
    
    
    e = create_engine("sqlite://", echo=True)
    Base.metadata.create_all(e)
    
    sess = Session(e)
    
    d = Dept(id=1)
    o = Owner(id=1)
    o.dept = d
    s = Sprint(id=1)
    s.owner = o
    sess.add(o)
    sess.commit()
    
    sess.query(Sprint).filter(Sprint.id.in_([1])).options(
        subqueryload(Sprint.owner).subqueryload(Owner.dept)).all()
    

    the third query (which is also wrong in 1.1, separate bug) does not emit anymore

  3. Mike Bayer reporter

    Repair regression to pathing for subclasses

    Issue #3963's initial commit narrowed the "current path" match rules too much such that a path that matches current path on subclass would no longer match.

    Change-Id: I8c9a0db91a09d789cfb8666288a913f8bbcdb2e9 Fixes: #3963

    → <<cset 770e1e399cb0>>

  4. Log in to comment