From 1c692f019b16ff4f3eb17ea8d09731837cc3be76 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 22 May 2017 15:08:10 -0400 Subject: [PATCH] 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 --- doc/build/changelog/changelog_12.rst | 11 ++++++ lib/sqlalchemy/orm/query.py | 25 ++++++++++--- lib/sqlalchemy/orm/relationships.py | 10 ++++-- lib/sqlalchemy/orm/util.py | 17 +++++---- test/orm/test_query.py | 54 +++++++++++++++++++++++++++- 5 files changed, 100 insertions(+), 17 deletions(-) diff --git a/doc/build/changelog/changelog_12.rst b/doc/build/changelog/changelog_12.rst index 9cf441f81b..10219e2bf5 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 f1734194a4..19a7b07c15 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 43f53aec52..97adf4d8b7 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 eebe188378..9a397ccf33 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 9924c9547c..082b623002 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 -- 2.39.5