Commits

Mike Bayer committed ef8d6d1

some refinements to has()/any(); __join_and_criterion handles the whole exists() clause now,
applies ORM alias annotation such that only the left side which joins to the parent is aliased;
the internal contents are not aliased since that's not the has()/any()/etc. use case.
we may later identify the need for the right hand side to be subject to polymorphic adaption ? not sure.
also sets up explicit correlate condition.

  • Participants
  • Parent commits 49598ce
  • Branches user_defined_state

Comments (0)

Files changed (5)

lib/sqlalchemy/orm/properties.py

             else:
                 return self.prop._optimized_compare(other)
 
-        def __join_and_criterion(self, criterion=None, **kwargs):
+        def __criterion_exists(self, criterion=None, **kwargs):
             if getattr(self, '_of_type', None):
                 target_mapper = self._of_type
                 to_selectable = target_mapper._with_polymorphic_selectable
                     criterion = criterion & crit
             
             if sj:
-                j = pj & sj
+                j = _orm_annotate(pj) & sj
             else:
-                j = pj
+                j = _orm_annotate(pj, exclude=self.prop.remote_side)
                 
             if criterion and target_adapter:
                 # limit this adapter to annotated only?
                 criterion = target_adapter.traverse(criterion)
             
-            return _orm_annotate(j), criterion, _orm_annotate(dest)
+            # only have the "joined left side" of what we return be subject to Query adaption.  The right
+            # side of it is used for an exists() subquery and should not correlate or otherwise reach out
+            # to anything in the enclosing query.
+            if criterion:
+                criterion = criterion._annotate({'_halt_adapt': True})
+            return sql.exists([1], j & criterion, from_obj=dest).correlate(source)
             
         def any(self, criterion=None, **kwargs):
             if not self.prop.uselist:
                 raise sa_exc.InvalidRequestError("'any()' not implemented for scalar attributes. Use has().")
 
-            j, criterion, from_obj = self.__join_and_criterion(criterion, **kwargs)
-
-            return sql.exists([1], j & criterion, from_obj=from_obj)
+            return self.__criterion_exists(criterion, **kwargs)
 
         def has(self, criterion=None, **kwargs):
             if self.prop.uselist:
                 raise sa_exc.InvalidRequestError("'has()' not implemented for collections.  Use any().")
-            j, criterion, from_obj = self.__join_and_criterion(criterion, **kwargs)
-
-            return sql.exists([1], j & criterion, from_obj=from_obj)
+            return self.__criterion_exists(criterion, **kwargs)
 
         def contains(self, other):
             if not self.prop.uselist:
 
         def __negated_contains_or_equals(self, other):
             criterion = sql.and_(*[x==y for (x, y) in zip(self.prop.mapper.primary_key, self.prop.mapper.primary_key_from_instance(other))])
-            j, criterion, from_obj = self.__join_and_criterion(criterion)
-            return ~sql.exists([1], j & criterion, from_obj=from_obj)
+            return ~self.__criterion_exists(criterion)
             
         def __ne__(self, other):
             if other is None:

lib/sqlalchemy/orm/query.py

     
     def __replace_element(self, adapters):
         def replace(elem):
-            if '_Query__no_adapt' in elem._annotations:
+            if '_halt_adapt' in elem._annotations:
                 return elem
 
             for adapter in adapters:
     
     def __replace_orm_element(self, adapters):
         def replace(elem):
-            if '_Query__no_adapt' in elem._annotations:
+            if '_halt_adapt' in elem._annotations:
                 return elem
 
             if "_orm_adapt" in elem._annotations or "parententity" in elem._annotations:
             if context.eager_order_by:
                 statement.append_order_by(*context.eager_order_by)
 
-        context.statement = statement._annotate({'_Query__no_adapt': True})
+        context.statement = statement._annotate({'_halt_adapt': True})
 
         return context
 

lib/sqlalchemy/orm/util.py

     def reverse_operate(self, op, other, **kwargs):
         return self.adapter.traverse(self.comparator.reverse_operate(op, *other, **kwargs))
 
-def _orm_annotate(element):
+def _orm_annotate(element, exclude=None):
     def clone(elem):
-        if '_orm_adapt' not in elem._annotations:
+        if exclude and elem in exclude:
+            elem = elem._clone()
+        elif '_orm_adapt' not in elem._annotations:
             elem = elem._annotate({'_orm_adapt':True})
-            elem._copy_internals(clone=clone)
+        elem._copy_internals(clone=clone)
         return elem
     
     if element is not None:
         element = clone(element)
     return element
-    
+
+
 class _ORMJoin(expression.Join):
 
     __visit_name__ = expression.Join.__visit_name__

lib/sqlalchemy/sql/expression.py

     __visit_name__ = _UnaryExpression.__visit_name__
 
     def __init__(self, *args, **kwargs):
-        kwargs['correlate'] = True
         s = select(*args, **kwargs).as_scalar().self_group()
         _UnaryExpression.__init__(self, s, operator=operators.exists)
 

test/orm/query.py

             filter(User.addresses.any(id=4)).all()
 
         assert [User(id=9)] == sess.query(User).filter(User.addresses.any(email_address='fred@fred.com')).all()
+        
+        # test that any() doesn't overcorrelate
+        assert [User(id=7), User(id=8)] == sess.query(User).join("addresses").filter(~User.addresses.any(Address.email_address=='fred@fred.com')).all()
+        
+        # test that the contents are not adapted by the aliased join
+        assert [User(id=7), User(id=8)] == sess.query(User).join("addresses", aliased=True).filter(~User.addresses.any(Address.email_address=='fred@fred.com')).all()
 
+        assert [User(id=10)] == sess.query(User).outerjoin("addresses", aliased=True).filter(~User.addresses.any()).all()
+        
     @testing.unsupported('maxdb') # can core
     def test_has(self):
         sess = create_session()
 
         assert [Address(id=2), Address(id=3), Address(id=4)] == sess.query(Address).filter(Address.user.has(User.name.like('%ed%'), id=8)).all()
 
+        # test has() doesn't overcorrelate
+        assert [Address(id=2), Address(id=3), Address(id=4)] == sess.query(Address).join("user").filter(Address.user.has(User.name.like('%ed%'), id=8)).all()
+
+        # test has() doesnt' get subquery contents adapted by aliased join
+        assert [Address(id=2), Address(id=3), Address(id=4)] == sess.query(Address).join("user", aliased=True).filter(Address.user.has(User.name.like('%ed%'), id=8)).all()
+        
         dingaling = sess.query(Dingaling).get(2)
         assert [User(id=9)] == sess.query(User).filter(User.addresses.any(Address.dingaling==dingaling)).all()