]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Break association proxy into a descriptor + per-class accessor
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 27 Feb 2018 18:15:10 +0000 (13:15 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 27 Sep 2018 21:09:28 +0000 (17:09 -0400)
Reworked :class:`.AssociationProxy` to store state that's specific to a
parent class in a separate object, so that a single
:class:`.AssocationProxy` can serve for multiple parent classes, as is
intrinsic to inheritance, without any ambiguity in the state returned by it.
A new method :meth:`.AssociationProxy.for_class` is added to allow
inspection of class-specific state.

Change-Id: I634f88aae6306ac5c5237a0e1acbe07d0481d6b6
Fixes: #3423
doc/build/changelog/migration_13.rst
doc/build/changelog/unreleased_13/3423.rst [new file with mode: 0644]
doc/build/orm/extensions/associationproxy.rst
lib/sqlalchemy/ext/associationproxy.py
lib/sqlalchemy/orm/instrumentation.py
test/ext/test_associationproxy.py
test/orm/test_inspect.py

index f742eb6672d9e33ca6c7fff60b5303eba064863b..500062686921bfe818089bb21bc896bb4f376942 100644 (file)
@@ -76,6 +76,68 @@ to ``None``::
 
 :ticket:`4308`
 
+.. _change_3423:
+
+AssociationProxy stores class-specific state in a separate container
+--------------------------------------------------------------------
+
+The :class:`.AssociationProxy` object makes lots of decisions based on the
+parent mapped class it is associated with.   While the
+:class:`.AssociationProxy` historically began as a relatively simple "getter",
+it became apparent early on that it also needed to make decisions about what
+kind of attribute it is referring towards, e.g. scalar or collection, mapped
+object or simple value, and similar.  To achieve this, it needs to inspect the
+mapped attribute or other descriptor or attribute that it refers towards, as
+referenced from its parent class.   However in Python descriptor mechanics, a
+descriptor only learns about its "parent" class when it is accessed in the
+context of that class, such as calling ``MyClass.some_descriptor``, which calls
+the ``__get__()`` method which passes in the class.    The
+:class:`.AssociationProxy` object would therefore store state that is specific
+to that class, but only once this method were called; trying to inspect this
+state ahead of time without first accessing the :class:`.AssociationProxy`
+as a descriptor would raise an error.  Additionally, it would  assume that
+the first class to be seen by ``__get__()`` would be  the only parent class it
+needed to know about.  This is despite the fact that if a particular class
+has inheriting subclasses, the association proxy is really working
+on behalf of more than one parent class even though it was not explicitly
+re-used.  While even with this shortcoming, the association proxy would
+still get pretty far with its current behavior, it still leaves shortcomings
+in some cases as well as the complex problem of determining the best "owner"
+class.
+
+These problems are now solved in that :class:`.AssociationProxy` no longer
+modifies its own internal state when ``__get__()`` is called; instead, a new
+object is generated per-class known as :class:`.AssociationProxyInstance` which
+handles all the state specific to a particular mapped parent class (when the
+parent class is not mapped, no :class:`.AssociationProxyInstance` is generated).
+The concept of a single "owning class" for the association proxy, which was
+nonetheless improved in 1.1, has essentially been replaced with an approach
+where the AP now can treat any number of "owning" classes equally.
+
+To accommodate for applications that want to inspect this state for an
+:class:`.AssociationProxy` without necessarily calling ``__get__()``, a new
+method :meth:`.AssociationProxy.for_class` is added that provides direct access
+to a class-specific :class:`.AssociationProxyInstance`, demonstrated as::
+
+    class User(Base):
+        # ...
+
+        keywords = association_proxy('kws', 'keyword')
+
+
+    proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
+
+Once we have the :class:`.AssociationProxyInstance` object, in the above
+example stored in the ``proxy_state`` variable, we can look at attributes
+specific to the ``User.keywords`` proxy, such as ``target_class``::
+
+
+    >>> proxy_state.target_class
+    Keyword
+
+
+:ticket:`3423`
+
 .. _change_4246:
 
 FOR UPDATE clause is rendered within the joined eager load subquery as well as outside
diff --git a/doc/build/changelog/unreleased_13/3423.rst b/doc/build/changelog/unreleased_13/3423.rst
new file mode 100644 (file)
index 0000000..63317ae
--- /dev/null
@@ -0,0 +1,15 @@
+.. change::
+   :tags: bug, ext
+   :tickets: 3423
+
+   Reworked :class:`.AssociationProxy` to store state that's specific to a
+   parent class in a separate object, so that a single
+   :class:`.AssocationProxy` can serve for multiple parent classes, as is
+   intrinsic to inheritance, without any ambiguity in the state returned by it.
+   A new method :meth:`.AssociationProxy.for_class` is added to allow
+   inspection of class-specific state.
+
+   .. seealso::
+
+        :ref:`change_3423`
+
index 18803c75fff78dbb23d11b001f6e8c17c40eb46c..62b24c92843010abafb991a3f1007db3147cadd9 100644 (file)
@@ -511,4 +511,9 @@ API Documentation
    :undoc-members:
    :inherited-members:
 
+.. autoclass:: AssociationProxyInstance
+   :members:
+   :undoc-members:
+   :inherited-members:
+
 .. autodata:: ASSOCIATION_PROXY
index 3c27cb59f6bfd909e11c449b6da5bb34c1154ce1..acf13df46a4bdc77097af5e276f012c028bbf8c3 100644 (file)
@@ -162,25 +162,161 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
         self.proxy_bulk_set = proxy_bulk_set
         self.cascade_scalar_deletes = cascade_scalar_deletes
 
-        self.owning_class = None
         self.key = '_%s_%s_%s' % (
             type(self).__name__, target_collection, id(self))
-        self.collection_class = None
         if info:
             self.info = info
 
+    def __get__(self, obj, class_):
+        if class_ is None:
+            return self
+        inst = self._as_instance(class_)
+        if inst:
+            return inst.get(obj)
+        else:
+            return self
+
+    def __set__(self, obj, values):
+        class_ = type(obj)
+        return self._as_instance(class_).set(obj, values)
+
+    def __delete__(self, obj):
+        class_ = type(obj)
+        return self._as_instance(class_).delete(obj)
+
+    def for_class(self, class_):
+        """Return the internal state local to a specific mapped class.
+
+        E.g., given a class ``User``::
+
+            class User(Base):
+                # ...
+
+                keywords = association_proxy('kws', 'keyword')
+
+        If we access this :class:`.AssociationProxy` from
+        :attr:`.Mapper.all_orm_descriptors`, and we want to view the
+        target class for this proxy as mapped by ``User``::
+
+            inspect(User).all_orm_descriptors["keywords"].for_class(User).target_class
+
+        This returns an instance of :class:`.AssociationProxyInstance` that
+        is specific to the ``User`` class.   The :class:`.AssociationProxy`
+        object remains agnostic of its parent class.
+
+        .. versionadded:: 1.3 - :class:`.AssociationProxy` no longer stores
+           any state specific to a particular parent class; the state is now
+           stored in per-class :class:`.AssociationProxyInstance` objects.
+
+
+        """
+        return self._as_instance(class_)
+
+    def _as_instance(self, class_):
+        try:
+            return class_.__dict__[self.key + "_inst"]
+        except KeyError:
+            owner = self._calc_owner(class_)
+            if owner is not None:
+                result = AssociationProxyInstance(self, owner)
+                setattr(class_, self.key + "_inst", result)
+                return result
+            else:
+                return None
+
+    def _calc_owner(self, target_cls):
+        # we might be getting invoked for a subclass
+        # that is not mapped yet, in some declarative situations.
+        # save until we are mapped
+        try:
+            insp = inspect(target_cls)
+        except exc.NoInspectionAvailable:
+            # can't find a mapper, don't set owner. if we are a not-yet-mapped
+            # subclass, we can also scan through __mro__ to find a mapped
+            # class, but instead just wait for us to be called again against a
+            # mapped class normally.
+            return None
+        else:
+            return insp.mapper.class_manager.class_
+
+    def _default_getset(self, collection_class):
+        attr = self.value_attr
+        _getter = operator.attrgetter(attr)
+
+        def getter(target):
+            return _getter(target) if target is not None else None
+        if collection_class is dict:
+            def setter(o, k, v):
+                setattr(o, attr, v)
+        else:
+            def setter(o, v):
+                setattr(o, attr, v)
+        return getter, setter
+
+
+class AssociationProxyInstance(object):
+    """A per-class object that serves class- and object-specific results.
+
+    This is used by :class:`.AssociationProxy` when it is invoked
+    in terms of a specific class or instance of a class, i.e. when it is
+    used as a regular Python descriptor.
+
+    When referring to the :class:`.AssociationProxy` as a normal Python
+    descriptor, the :class:`.AssociationProxyInstance` is the object that
+    actually serves the information.   Under normal circumstances, its presence
+    is transparent::
+
+        >>> User.keywords.scalar
+        False
+
+    In the special case that the :class:`.AssociationProxy` object is being
+    accessed directly, in order to get an explicit handle to the
+    :class:`.AssociationProxyInstance`, use the
+    :meth:`.AssociationProxy.for_class` method::
+
+        proxy_state = inspect(User).all_orm_descriptors["keywords"].for_class(User)
+
+        # view if proxy object is scalar or not
+        >>> proxy_state.scalar
+        False
+
+    .. versionadded:: 1.3
+
+    """
+
+    def __init__(self, parent, owning_class):
+        self.parent = parent
+        self.key = parent.key
+        self.owning_class = owning_class
+        self.target_collection = parent.target_collection
+        self.value_attr = parent.value_attr
+        self.collection_class = None
+
+    def _get_property(self):
+        return orm.class_mapper(self.owning_class).\
+            get_property(self.target_collection)
+
+    @property
+    def _comparator(self):
+        return self._get_property().comparator
+
+    @util.memoized_property
+    def _unwrap_target_assoc_proxy(self):
+        attr = getattr(self.target_class, self.value_attr)
+        if isinstance(attr, (AssociationProxy, AssociationProxyInstance)):
+            return attr
+        return None
+
     @property
     def remote_attr(self):
         """The 'remote' :class:`.MapperProperty` referenced by this
-        :class:`.AssociationProxy`.
-
-        .. versionadded:: 0.7.3
+        :class:`.AssociationProxyInstance`.
 
-        See also:
+        ..seealso::
 
-        :attr:`.AssociationProxy.attr`
+            :attr:`.AssociationProxyInstance.attr`
 
-        :attr:`.AssociationProxy.local_attr`
+            :attr:`.AssociationProxyInstance.local_attr`
 
         """
         return getattr(self.target_class, self.value_attr)
@@ -188,15 +324,13 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
     @property
     def local_attr(self):
         """The 'local' :class:`.MapperProperty` referenced by this
-        :class:`.AssociationProxy`.
-
-        .. versionadded:: 0.7.3
+        :class:`.AssociationProxyInstance`.
 
-        See also:
+        .. seealso::
 
-        :attr:`.AssociationProxy.attr`
+            :attr:`.AssociationProxyInstance.attr`
 
-        :attr:`.AssociationProxy.remote_attr`
+            :attr:`.AssociationProxyInstance.remote_attr`
 
         """
         return getattr(self.owning_class, self.target_collection)
@@ -210,29 +344,19 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
 
             sess.query(Parent).join(*Parent.proxied.attr)
 
-        .. versionadded:: 0.7.3
+        .. seealso::
 
-        See also:
+            :attr:`.AssociationProxyInstance.local_attr`
 
-        :attr:`.AssociationProxy.local_attr`
-
-        :attr:`.AssociationProxy.remote_attr`
+            :attr:`.AssociationProxyInstance.remote_attr`
 
         """
         return (self.local_attr, self.remote_attr)
 
-    def _get_property(self):
-        owning_class = self.owning_class
-        if owning_class is None:
-            raise exc.InvalidRequestError(
-                "This association proxy has no mapped owning class; "
-                "can't locate a mapped property")
-        return (orm.class_mapper(owning_class).
-                get_property(self.target_collection))
-
     @util.memoized_property
     def target_class(self):
-        """The intermediary class handled by this :class:`.AssociationProxy`.
+        """The intermediary class handled by this
+        :class:`.AssociationProxyInstance`.
 
         Intercepted append/set/assignment events will result
         in the generation of new instances of this class.
@@ -242,8 +366,8 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
 
     @util.memoized_property
     def scalar(self):
-        """Return ``True`` if this :class:`.AssociationProxy` proxies a scalar
-        relationship on the local side."""
+        """Return ``True`` if this :class:`.AssociationProxyInstance`
+        proxies a scalar relationship on the local side."""
 
         scalar = not self._get_property().uselist
         if scalar:
@@ -259,39 +383,32 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
     def _target_is_object(self):
         return getattr(self.target_class, self.value_attr).impl.uses_objects
 
-    def _calc_owner(self, obj, class_):
-        if obj is not None and class_ is None:
-            target_cls = type(obj)
-        elif class_ is not None:
-            target_cls = class_
+    def _initialize_scalar_accessors(self):
+        if self.parent.getset_factory:
+            get, set = self.parent.getset_factory(None, self)
         else:
-            return
+            get, set = self.parent._default_getset(None)
+        self._scalar_get, self._scalar_set = get, set
 
-        # we might be getting invoked for a subclass
-        # that is not mapped yet, in some declarative situations.
-        # save until we are mapped
-        try:
-            insp = inspect(target_cls)
-        except exc.NoInspectionAvailable:
-            # can't find a mapper, don't set owner. if we are a not-yet-mapped
-            # subclass, we can also scan through __mro__ to find a mapped
-            # class, but instead just wait for us to be called again against a
-            # mapped class normally.
-            return
+    def _default_getset(self, collection_class):
+        attr = self.value_attr
+        _getter = operator.attrgetter(attr)
 
-        # note we can get our real .key here too
-        owner = insp.mapper.class_manager._locate_owning_manager(self)
-        if owner is not None:
-            self.owning_class = owner.class_
+        def getter(target):
+            return _getter(target) if target is not None else None
+        if collection_class is dict:
+            def setter(o, k, v):
+                return setattr(o, attr, v)
         else:
-            # the proxy is attached to a class that is not mapped
-            # (like a mixin), we are mapped, so, it's us.
-            self.owning_class = target_cls
+            def setter(o, v):
+                return setattr(o, attr, v)
+        return getter, setter
 
-    def __get__(self, obj, class_):
-        if self.owning_class is None:
-            self._calc_owner(obj, class_)
+    @property
+    def info(self):
+        return self.parent.info
 
+    def get(self, obj):
         if obj is None:
             return self
 
@@ -302,21 +419,23 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
             try:
                 # If the owning instance is reborn (orm session resurrect,
                 # etc.), refresh the proxy cache.
-                creator_id, proxy = getattr(obj, self.key)
-                if id(obj) == creator_id:
-                    return proxy
+                creator_id, self_id, proxy = getattr(obj, self.key)
             except AttributeError:
                 pass
-            proxy = self._new(_lazy_collection(obj, self.target_collection))
-            setattr(obj, self.key, (id(obj), proxy))
-            return proxy
+            else:
+                if id(obj) == creator_id and id(self) == self_id:
+                    assert self.collection_class is not None
+                    return proxy
 
-    def __set__(self, obj, values):
-        if self.owning_class is None:
-            self._calc_owner(obj, None)
+            self.collection_class, proxy = self._new(
+                _lazy_collection(obj, self.target_collection))
+            setattr(obj, self.key, (id(obj), id(self), proxy))
+            return proxy
 
+    def set(self, obj, values):
         if self.scalar:
-            creator = self.creator and self.creator or self.target_class
+            creator = self.parent.creator \
+                if self.parent.creator else self.target_class
             target = getattr(obj, self.target_collection)
             if target is None:
                 if values is None:
@@ -324,15 +443,16 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
                 setattr(obj, self.target_collection, creator(values))
             else:
                 self._scalar_set(target, values)
-                if values is None and self.cascade_scalar_deletes:
+                if values is None and self.parent.cascade_scalar_deletes:
                     setattr(obj, self.target_collection, None)
         else:
-            proxy = self.__get__(obj, None)
+            proxy = self.get(obj)
+            assert self.collection_class is not None
             if proxy is not values:
                 proxy.clear()
                 self._set(proxy, values)
 
-    def __delete__(self, obj):
+    def delete(self, obj):
         if self.owning_class is None:
             self._calc_owner(obj, None)
 
@@ -342,49 +462,29 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
                 delattr(target, self.value_attr)
         delattr(obj, self.target_collection)
 
-    def _initialize_scalar_accessors(self):
-        if self.getset_factory:
-            get, set = self.getset_factory(None, self)
-        else:
-            get, set = self._default_getset(None)
-        self._scalar_get, self._scalar_set = get, set
-
-    def _default_getset(self, collection_class):
-        attr = self.value_attr
-        _getter = operator.attrgetter(attr)
-
-        def getter(target):
-            return _getter(target) if target is not None else None
-
-        if collection_class is dict:
-            def setter(o, k, v):
-                setattr(o, attr, v)
-        else:
-            def setter(o, v):
-                setattr(o, attr, v)
-        return getter, setter
-
     def _new(self, lazy_collection):
-        creator = self.creator and self.creator or self.target_class
-        self.collection_class = util.duck_type_collection(lazy_collection())
+        creator = self.parent.creator if self.parent.creator else \
+            self.target_class
+        collection_class = util.duck_type_collection(lazy_collection())
 
-        if self.proxy_factory:
-            return self.proxy_factory(
+        if self.parent.proxy_factory:
+            return collection_class, self.parent.proxy_factory(
                 lazy_collection, creator, self.value_attr, self)
 
-        if self.getset_factory:
-            getter, setter = self.getset_factory(self.collection_class, self)
+        if self.parent.getset_factory:
+            getter, setter = self.parent.getset_factory(
+                collection_class, self)
         else:
-            getter, setter = self._default_getset(self.collection_class)
+            getter, setter = self.parent._default_getset(collection_class)
 
-        if self.collection_class is list:
-            return _AssociationList(
+        if collection_class is list:
+            return collection_class, _AssociationList(
                 lazy_collection, creator, getter, setter, self)
-        elif self.collection_class is dict:
-            return _AssociationDict(
+        elif collection_class is dict:
+            return collection_class, _AssociationDict(
                 lazy_collection, creator, getter, setter, self)
-        elif self.collection_class is set:
-            return _AssociationSet(
+        elif collection_class is set:
+            return collection_class, _AssociationSet(
                 lazy_collection, creator, getter, setter, self)
         else:
             raise exc.ArgumentError(
@@ -393,21 +493,9 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
                 'proxy_factory and proxy_bulk_set manually' %
                 (self.collection_class.__name__, self.target_collection))
 
-    def _inflate(self, proxy):
-        creator = self.creator and self.creator or self.target_class
-
-        if self.getset_factory:
-            getter, setter = self.getset_factory(self.collection_class, self)
-        else:
-            getter, setter = self._default_getset(self.collection_class)
-
-        proxy.creator = creator
-        proxy.getter = getter
-        proxy.setter = setter
-
     def _set(self, proxy, values):
-        if self.proxy_bulk_set:
-            self.proxy_bulk_set(proxy, values)
+        if self.parent.proxy_bulk_set:
+            self.parent.proxy_bulk_set(proxy, values)
         elif self.collection_class is list:
             proxy.extend(values)
         elif self.collection_class is dict:
@@ -419,16 +507,19 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
                 'no proxy_bulk_set supplied for custom '
                 'collection_class implementation')
 
-    @property
-    def _comparator(self):
-        return self._get_property().comparator
+    def _inflate(self, proxy):
+        creator = self.parent.creator and \
+            self.parent.creator or self.target_class
 
-    @util.memoized_property
-    def _unwrap_target_assoc_proxy(self):
-        attr = getattr(self.target_class, self.value_attr)
-        if isinstance(attr, AssociationProxy):
-            return attr
-        return None
+        if self.parent.getset_factory:
+            getter, setter = self.parent.getset_factory(
+                self.collection_class, self)
+        else:
+            getter, setter = self.parent._default_getset(self.collection_class)
+
+        proxy.creator = creator
+        proxy.getter = getter
+        proxy.setter = setter
 
     def _criterion_exists(self, criterion=None, **kwargs):
         is_has = kwargs.pop('is_has', None)
@@ -543,10 +634,6 @@ class AssociationProxy(interfaces.InspectionAttrInfo):
         return self._comparator.has(
             getattr(self.target_class, self.value_attr) != obj)
 
-    def __repr__(self):
-        return "AssociationProxy(%r, %r)" % (
-            self.target_collection, self.value_attr)
-
 
 class _lazy_collection(object):
     def __init__(self, obj, target):
index 1b839cf5c351d4b467a23895bfb138f02df31ddf..d34326e0fd206e3dbc62c9d3758b4aecc0733983 100644 (file)
@@ -115,41 +115,6 @@ class ClassManager(dict):
         # raises unless self.mapper has been assigned
         raise exc.UnmappedClassError(self.class_)
 
-    def _locate_owning_manager(self, attribute):
-        """Scan through all instrumented classes in our hierarchy
-        searching for the given object as an attribute, and return
-        the bottommost owner.
-
-        E.g.::
-
-            foo = foobar()
-
-            class Parent:
-                attr = foo
-
-            class Child(Parent):
-                pass
-
-        Child.manager._locate_owning_manager(foo) would
-        give us Parent.
-
-        Needed by association proxy to correctly figure out the
-        owning class when the attribute is accessed.
-
-        """
-
-        stack = [None]
-        for supercls in self.class_.__mro__:
-            mgr = manager_of_class(supercls)
-            if not mgr:
-                continue
-            for key in set(supercls.__dict__):
-                val = supercls.__dict__[key]
-                if val is attribute:
-                    stack.append(mgr)
-                    continue
-        return stack[-1]
-
     def _all_sqla_attributes(self, exclude=None):
         """return an iterator of all classbound attributes that are
         implement :class:`.InspectionAttr`.
index 70d5e1613af6c9dc221c923a4218f96b676ad0c1..212599d7ab3d841106042fb6f77599f41373adf2 100644 (file)
@@ -1981,7 +1981,8 @@ class AttributeAccessTest(fixtures.TestBase):
             __tablename__ = 'subparent'
             id = Column(Integer, ForeignKey(Parent.id), primary_key=True)
 
-        is_(SubParent.children.owning_class, Parent)
+        is_(SubParent.children.owning_class, SubParent)
+        is_(Parent.children.owning_class, Parent)
 
     def test_resolved_to_correct_class_two(self):
         Base = declarative_base()
@@ -2025,7 +2026,8 @@ class AttributeAccessTest(fixtures.TestBase):
             __tablename__ = 'subsubparent'
             id = Column(Integer, ForeignKey(SubParent.id), primary_key=True)
 
-        is_(SubSubParent.children.owning_class, SubParent)
+        is_(SubParent.children.owning_class, SubParent)
+        is_(SubSubParent.children.owning_class, SubSubParent)
 
     def test_resolved_to_correct_class_four(self):
         Base = declarative_base()
@@ -2049,7 +2051,8 @@ class AttributeAccessTest(fixtures.TestBase):
 
         sp = SubParent()
         sp.children = 'c'
-        is_(SubParent.children.owning_class, Parent)
+        is_(SubParent.children.owning_class, SubParent)
+        is_(Parent.children.owning_class, Parent)
 
     def test_resolved_to_correct_class_five(self):
         Base = declarative_base()
@@ -2078,7 +2081,7 @@ class AttributeAccessTest(fixtures.TestBase):
         is_(Parent.children.owning_class, Parent)
         eq_(p1.children, ["c1"])
 
-    def test_never_assign_nonetype(self):
+    def _test_never_assign_nonetype(self):
         foo = association_proxy('x', 'y')
         foo._calc_owner(None, None)
         is_(foo.owning_class, None)
@@ -2358,3 +2361,73 @@ class InfoTest(fixtures.TestBase):
         Foob.assoc.info["foo"] = 'bar'
 
         eq_(Foob.assoc.info, {'foo': 'bar'})
+
+class MultiOwnerTest(fixtures.DeclarativeMappedTest,
+                                     testing.AssertsCompiledSQL):
+    __dialect__ = 'default'
+
+    @classmethod
+    def setup_classes(cls):
+        Base = cls.DeclarativeBasic
+
+        class A(Base):
+            __tablename__ = 'a'
+            id = Column(Integer, primary_key=True)
+            type = Column(String(5), nullable=False)
+            d_values = association_proxy("ds", "value")
+
+            __mapper_args__ = {"polymorphic_on": type}
+
+        class B(A):
+            __tablename__ = 'b'
+            id = Column(ForeignKey('a.id'), primary_key=True)
+
+            ds = relationship("D", primaryjoin="D.b_id == B.id")
+
+            __mapper_args__ = {"polymorphic_identity": "b"}
+
+        class C(A):
+            __tablename__ = 'c'
+            id = Column(ForeignKey('a.id'), primary_key=True)
+
+            ds = relationship("D", primaryjoin="D.c_id == C.id")
+
+            __mapper_args__ = {"polymorphic_identity": "c"}
+
+        class C2(C):
+            __tablename__ = 'c2'
+            id = Column(ForeignKey('c.id'), primary_key=True)
+
+            ds = relationship("D", primaryjoin="D.c2_id == C2.id")
+
+            __mapper_args__ = {"polymorphic_identity": "c2"}
+
+        class D(Base):
+            __tablename__ = 'd'
+            id = Column(Integer, primary_key=True)
+            value = Column(String(50))
+            b_id = Column(ForeignKey('b.id'))
+            c_id = Column(ForeignKey('c.id'))
+            c2_id = Column(ForeignKey('c2.id'))
+
+    def test_any_has(self):
+        B, C, C2 = self.classes("B", "C", "C2")
+
+        self.assert_compile(
+            B.d_values.contains('b1'),
+            "EXISTS (SELECT 1 FROM d, b WHERE d.b_id = b.id "
+            "AND d.value = :value_1)"
+        )
+
+        self.assert_compile(
+            C2.d_values.contains("c2"),
+            "EXISTS (SELECT 1 FROM d, c2 WHERE d.c2_id = c2.id "
+            "AND d.value = :value_1)"
+        )
+
+        self.assert_compile(
+            C.d_values.contains('c1'),
+            "EXISTS (SELECT 1 FROM d, c WHERE d.c_id = c.id "
+            "AND d.value = :value_1)"
+        )
+
index a67ac4419c84113b3338cd7b868a251e6a7546af..37cafe599fb0ac4608b9504b98b3612d5dd57d97 100644 (file)
@@ -296,7 +296,7 @@ class TestORMInspection(_fixtures.FixtureTest):
         )
         is_(
             insp.all_orm_descriptors.some_assoc,
-            SomeClass.some_assoc
+            SomeClass.some_assoc.parent
         )
         is_(
             inspect(SomeClass).all_orm_descriptors.upper_name,