]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Add clause adaptation for AliasedClass to with_parent()
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 22 May 2017 19:08:10 +0000 (15:08 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 23 May 2017 15:13:18 +0000 (11:13 -0400)
Fixed bug where :meth:`.Query.with_parent` would not work if the
:class:`.Query` were against an :func:`.aliased` construct rather than
a regular mapped class.  Also adds a new parameter
:paramref:`.util.with_parent.from_entity` to the standalone
:func:`.util.with_parent` function as well as
:meth:`.Query.with_parent`.

Change-Id: Ic684dd63cc90b582c7580c9bba3c92fa3f286da7
Fixes: #3607
doc/build/changelog/changelog_12.rst
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/relationships.py
lib/sqlalchemy/orm/util.py
test/orm/test_query.py

index 9cf441f81bcd651386477079a693a8c2ed966ef7..10219e2bf59e7a264febefa68f7a5aab251a1575 100644 (file)
         .. seealso::
 
             :ref:`change_3934`
+
+    .. change:: 3607
+        :tags: bug, orm
+        :tickets: 3607
+
+        Fixed bug where :meth:`.Query.with_parent` would not work if the
+        :class:`.Query` were against an :func:`.aliased` construct rather than
+        a regular mapped class.  Also adds a new parameter
+        :paramref:`.util.with_parent.from_entity` to the standalone
+        :func:`.util.with_parent` function as well as
+        :meth:`.Query.with_parent`.
index f1734194a448765d097c7f8a127efbfe46d47523..19a7b07c151f10ff322d986afa825ac1c7093ca3 100644 (file)
@@ -968,7 +968,7 @@ class Query(object):
         """
         self._invoke_all_eagers = value
 
-    def with_parent(self, instance, property=None):
+    def with_parent(self, instance, property=None, from_entity=None):
         """Add filtering criterion that relates the given instance
         to a child object or collection, using its attribute state
         as well as an established :func:`.relationship()`
@@ -981,16 +981,31 @@ class Query(object):
         that the given property can be None, in which case a search is
         performed against this :class:`.Query` object's target mapper.
 
+        :param instance:
+          An instance which has some :func:`.relationship`.
+
+        :param property:
+          String property name, or class-bound attribute, which indicates
+          what relationship from the instance should be used to reconcile the
+          parent/child relationship.
+
+        :param from_entity:
+          Entity in which to consider as the left side.  This defaults to the
+          "zero" entity of the :class:`.Query` itself.
+
         """
 
+        if from_entity:
+            entity_zero = inspect(from_entity)
+        else:
+            entity_zero = self._entity_zero()
         if property is None:
-            mapper_zero = self._mapper_zero()
 
             mapper = object_mapper(instance)
 
             for prop in mapper.iterate_properties:
                 if isinstance(prop, properties.RelationshipProperty) and \
-                        prop.mapper is mapper_zero:
+                        prop.mapper is entity_zero.mapper:
                     property = prop
                     break
             else:
@@ -998,11 +1013,11 @@ class Query(object):
                     "Could not locate a property which relates instances "
                     "of class '%s' to instances of class '%s'" %
                     (
-                        self._mapper_zero().class_.__name__,
+                        entity_zero.mapper.class_.__name__,
                         instance.__class__.__name__)
                 )
 
-        return self.filter(with_parent(instance, property))
+        return self.filter(with_parent(instance, property, entity_zero.entity))
 
     @_generative()
     def add_entity(self, entity, alias=None):
index 43f53aec522afd51a231fb14d6035b5fb5517f01..97adf4d8b7ba55430068c6489d7bf63acab35396 100644 (file)
@@ -1355,10 +1355,16 @@ class RelationshipProperty(StrategizedProperty):
                 mapperlib.Mapper._configure_all()
             return self.prop
 
-    def _with_parent(self, instance, alias_secondary=True):
+    def _with_parent(self, instance, alias_secondary=True, from_entity=None):
         assert instance is not None
+        adapt_source = None
+        if from_entity is not None:
+            insp = inspect(from_entity)
+            if insp.is_aliased_class:
+                adapt_source = insp._adapter.adapt_clause
         return self._optimized_compare(
-            instance, value_is_parent=True, alias_secondary=alias_secondary)
+            instance, value_is_parent=True, adapt_source=adapt_source,
+            alias_secondary=alias_secondary)
 
     def _optimized_compare(self, state, value_is_parent=False,
                            adapt_source=None,
index eebe1883789b58aba8c8f6390ae7d2349ff1737a..9a397ccf33e0ff69683be4803c502fb18db4b468 100644 (file)
@@ -974,7 +974,7 @@ def outerjoin(left, right, onclause=None, full=False, join_to_left=None):
     return _ORMJoin(left, right, onclause, True, full)
 
 
-def with_parent(instance, prop):
+def with_parent(instance, prop, from_entity=None):
     """Create filtering criterion that relates this query's primary entity
     to the given related instance, using established :func:`.relationship()`
     configuration.
@@ -985,13 +985,6 @@ def with_parent(instance, prop):
     Python without the need to render joins to the parent table
     in the rendered statement.
 
-    .. versionchanged:: 0.6.4
-        This method accepts parent instances in all
-        persistence states, including transient, persistent, and detached.
-        Only the requisite primary key/foreign key attributes need to
-        be populated.  Previous versions didn't work with transient
-        instances.
-
     :param instance:
       An instance which has some :func:`.relationship`.
 
@@ -1000,6 +993,12 @@ def with_parent(instance, prop):
       what relationship from the instance should be used to reconcile the
       parent/child relationship.
 
+    :param from_entity:
+      Entity in which to consider as the left side.  This defaults to the
+      "zero" entity of the :class:`.Query` itself.
+
+      .. versionadded:: 1.2
+
     """
     if isinstance(prop, util.string_types):
         mapper = object_mapper(instance)
@@ -1007,7 +1006,7 @@ def with_parent(instance, prop):
     elif isinstance(prop, attributes.QueryableAttribute):
         prop = prop.property
 
-    return prop._with_parent(instance)
+    return prop._with_parent(instance, from_entity=from_entity)
 
 
 def has_identity(object):
index 9924c9547c7e5578532baa8c298d883b6b2dbd5c..082b62300234a648810efae00cf5009042202672 100644 (file)
@@ -3667,7 +3667,42 @@ class ParentTest(QueryTest, AssertsCompiledSQL):
             {'param_1': 7}
         )
 
-    @testing.fails("issue #3607")
+    def test_from_entity_standalone_fn(self):
+        User, Address = self.classes.User, self.classes.Address
+
+        sess = create_session()
+        u1 = sess.query(User).get(7)
+        q = sess.query(User, Address).filter(
+            with_parent(u1, "addresses", from_entity=Address))
+        self.assert_compile(
+            q,
+            "SELECT users.id AS users_id, users.name AS users_name, "
+            "addresses.id AS addresses_id, addresses.user_id "
+            "AS addresses_user_id, "
+            "addresses.email_address AS addresses_email_address "
+            "FROM users, addresses "
+            "WHERE :param_1 = addresses.user_id",
+            {'param_1': 7}
+        )
+
+    def test_from_entity_query_entity(self):
+        User, Address = self.classes.User, self.classes.Address
+
+        sess = create_session()
+        u1 = sess.query(User).get(7)
+        q = sess.query(User, Address).with_parent(
+            u1, "addresses", from_entity=Address)
+        self.assert_compile(
+            q,
+            "SELECT users.id AS users_id, users.name AS users_name, "
+            "addresses.id AS addresses_id, addresses.user_id "
+            "AS addresses_user_id, "
+            "addresses.email_address AS addresses_email_address "
+            "FROM users, addresses "
+            "WHERE :param_1 = addresses.user_id",
+            {'param_1': 7}
+        )
+
     def test_select_from_alias(self):
         User, Address = self.classes.User, self.classes.Address
 
@@ -3685,6 +3720,23 @@ class ParentTest(QueryTest, AssertsCompiledSQL):
             {'param_1': 7}
         )
 
+    def test_select_from_alias_explicit_prop(self):
+        User, Address = self.classes.User, self.classes.Address
+
+        sess = create_session()
+        u1 = sess.query(User).get(7)
+        a1 = aliased(Address)
+        q = sess.query(a1).with_parent(u1, "addresses")
+        self.assert_compile(
+            q,
+            "SELECT addresses_1.id AS addresses_1_id, "
+            "addresses_1.user_id AS addresses_1_user_id, "
+            "addresses_1.email_address AS addresses_1_email_address "
+            "FROM addresses AS addresses_1 "
+            "WHERE :param_1 = addresses_1.user_id",
+            {'param_1': 7}
+        )
+
     def test_noparent(self):
         Item, User = self.classes.Item, self.classes.User