summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2017-05-22 15:08:10 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2017-05-23 11:13:18 -0400
commit1c692f019b16ff4f3eb17ea8d09731837cc3be76 (patch)
tree9829896302c427fe2751ebc862640578a3f882dc
parenteed7888f85a4255390e2522dbd428cbfe7a08bab (diff)
downloadsqlalchemy-1c692f019b16ff4f3eb17ea8d09731837cc3be76.tar.gz
Add clause adaptation for AliasedClass to with_parent()
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
-rw-r--r--doc/build/changelog/changelog_12.rst11
-rw-r--r--lib/sqlalchemy/orm/query.py25
-rw-r--r--lib/sqlalchemy/orm/relationships.py10
-rw-r--r--lib/sqlalchemy/orm/util.py17
-rw-r--r--test/orm/test_query.py54
5 files changed, 100 insertions, 17 deletions
diff --git a/doc/build/changelog/changelog_12.rst b/doc/build/changelog/changelog_12.rst
index 9cf441f81..10219e2bf 100644
--- a/doc/build/changelog/changelog_12.rst
+++ b/doc/build/changelog/changelog_12.rst
@@ -549,3 +549,14 @@
.. 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`.
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index f1734194a..19a7b07c1 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -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):
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py
index 43f53aec5..97adf4d8b 100644
--- a/lib/sqlalchemy/orm/relationships.py
+++ b/lib/sqlalchemy/orm/relationships.py
@@ -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,
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index eebe18837..9a397ccf3 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -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):
diff --git a/test/orm/test_query.py b/test/orm/test_query.py
index 9924c9547..082b62300 100644
--- a/test/orm/test_query.py
+++ b/test/orm/test_query.py
@@ -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