From: Mike Bayer Date: Thu, 15 Jan 2009 17:08:56 +0000 (+0000) Subject: - Query.from_self() as well as query.subquery() both disable X-Git-Tag: rel_0_5_1~11 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=454f1d7f583a6530c28b2ca86235695d6952d292;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Query.from_self() as well as query.subquery() both disable the rendering of eager joins inside the subquery produced. The "disable all eager joins" feature is available publically via a new query.enable_eagerloads() generative. [ticket:1276] - Added a rudimental series of set operations to Query that receive Query objects as arguments, including union(), union_all(), intersect(), except_(), insertsect_all(), except_all(). See the API documentation for Query.union() for examples. - Fixed bug that prevented Query.join() and eagerloads from attaching to a query that selected from a union or aliased union. --- diff --git a/CHANGES b/CHANGES index c45dd101b8..ecb9fa3865 100644 --- a/CHANGES +++ b/CHANGES @@ -24,7 +24,21 @@ CHANGES - Test coverage added for `relation()` objects specified on concrete mappers. [ticket:1237] - + + - Query.from_self() as well as query.subquery() both disable + the rendering of eager joins inside the subquery produced. + The "disable all eager joins" feature is available publically + via a new query.enable_eagerloads() generative. [ticket:1276] + + - Added a rudimental series of set operations to Query that + receive Query objects as arguments, including union(), + union_all(), intersect(), except_(), insertsect_all(), + except_all(). See the API documentation for + Query.union() for examples. + + - Fixed bug that prevented Query.join() and eagerloads from + attaching to a query that selected from a union or aliased union. + - A short documentation example added for bidirectional relations specified on concrete mappers. [ticket:1237] diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 3e2063f765..4fa68379d8 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -73,6 +73,7 @@ class Query(object): self._correlate = set() self._joinpoint = None self._with_labels = False + self._enable_eagerloads = True self.__joinable_tables = None self._having = None self._populate_existing = False @@ -131,16 +132,14 @@ class Query(object): def __set_select_from(self, from_obj): if isinstance(from_obj, expression._SelectBaseMixin): - # alias SELECTs and unions from_obj = from_obj.alias() self._from_obj = from_obj equivs = self.__all_equivs() if isinstance(from_obj, expression.Alias): - # dont alias a regular join (since its not an alias itself) self._from_obj_alias = sql_util.ColumnAdapter(self._from_obj, equivs) - + def _get_polymorphic_adapter(self, entity, selectable): self.__mapper_loads_polymorphically_with(entity.mapper, sql_util.ColumnAdapter(selectable, entity.mapper._equivalent_columns)) @@ -318,13 +317,37 @@ class Query(object): @property def statement(self): """The full SELECT statement represented by this Query.""" + return self._compile_context(labels=self._with_labels).statement._annotate({'_halt_adapt': True}) + @property + def _nested_statement(self): + return self.with_labels().enable_eagerloads(False).statement.correlate(None) + def subquery(self): - """return the full SELECT statement represented by this Query, embedded within an Alias.""" + """return the full SELECT statement represented by this Query, embedded within an Alias. + + Eager JOIN generation within the query is disabled. + + """ - return self.statement.alias() + return self.enable_eagerloads(False).statement.alias() + @_generative() + def enable_eagerloads(self, value): + """Control whether or not eager joins are rendered. + + When set to False, the returned Query will not render + eager joins regardless of eagerload() options + or mapper-level lazy=False configurations. + + This is used primarily when nesting the Query's + statement into a subquery or other + selectable. + + """ + self._enable_eagerloads = value + @_generative() def with_labels(self): """Apply column labels to the return value of Query.statement. @@ -524,23 +547,27 @@ class Query(object): m = _MapperEntity(self, entity) self.__setup_aliasizers([m]) - @_generative() def from_self(self, *entities): """return a Query that selects from this Query's SELECT statement. \*entities - optional list of entities which will replace those being selected. + """ + fromclause = self._nested_statement + q = self._from_selectable(fromclause) + if entities: + q._set_entities(entities) + return q + + _from_self = from_self - fromclause = self.with_labels().statement.correlate(None) + @_generative() + def _from_selectable(self, fromclause): self._statement = self._criterion = None self._order_by = self._group_by = self._distinct = False self._limit = self._offset = None self.__set_select_from(fromclause) - if entities: - self._set_entities(entities) - - _from_self = from_self def values(self, *columns): """Return an iterator yielding result tuples corresponding to the given list of columns""" @@ -692,6 +719,92 @@ class Query(object): else: self._having = criterion + def union(self, *q): + """Produce a UNION of this Query against one or more queries. + + e.g.:: + + q1 = sess.query(SomeClass).filter(SomeClass.foo=='bar') + q2 = sess.query(SomeClass).filter(SomeClass.bar=='foo') + + q3 = q1.union(q2) + + The method accepts multiple Query objects so as to control + the level of nesting. A series of ``union()`` calls such as:: + + x.union(y).union(z).all() + + will nest on each ``union()``, and produces:: + + SELECT * FROM (SELECT * FROM (SELECT * FROM X UNION SELECT * FROM y) UNION SELECT * FROM Z) + + Whereas:: + + x.union(y, z).all() + + produces:: + + SELECT * FROM (SELECT * FROM X UNION SELECT * FROM y UNION SELECT * FROM Z) + + """ + return self._from_selectable( + expression.union(*([self._nested_statement]+ [x._nested_statement for x in q]))) + + def union_all(self, *q): + """Produce a UNION ALL of this Query against one or more queries. + + Works the same way as :method:`union`. See that + method for usage examples. + + """ + return self._from_selectable( + expression.union_all(*([self._nested_statement]+ [x._nested_statement for x in q])) + ) + + def intersect(self, *q): + """Produce an INTERSECT of this Query against one or more queries. + + Works the same way as :method:`union`. See that + method for usage examples. + + """ + return self._from_selectable( + expression.intersect(*([self._nested_statement]+ [x._nested_statement for x in q])) + ) + + def intersect_all(self, *q): + """Produce an INTERSECT ALL of this Query against one or more queries. + + Works the same way as :method:`union`. See that + method for usage examples. + + """ + return self._from_selectable( + expression.intersect_all(*([self._nested_statement]+ [x._nested_statement for x in q])) + ) + + def except_(self, *q): + """Produce an EXCEPT of this Query against one or more queries. + + Works the same way as :method:`union`. See that + method for usage examples. + + """ + return self._from_selectable( + expression.except_(*([self._nested_statement]+ [x._nested_statement for x in q])) + ) + + def except_all(self, *q): + """Produce an EXCEPT ALL of this Query against one or more queries. + + Works the same way as :method:`union`. See that + method for usage examples. + + """ + return self._from_selectable( + expression.except_all(*([self._nested_statement]+ [x._nested_statement for x in q])) + ) + @util.accepts_a_list_as_starargs(list_deprecation='pending') def join(self, *props, **kwargs): """Create a join against this ``Query`` object's criterion @@ -1929,7 +2042,7 @@ class QueryContext(object): self.primary_columns = [] self.secondary_columns = [] self.eager_order_by = [] - + self.enable_eagerloads = query._enable_eagerloads self.eager_joins = {} self.froms = [] self.adapter = None diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 68c46f09ef..5f820565f1 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -603,6 +603,9 @@ class EagerLoader(AbstractRelationLoader): def setup_query(self, context, entity, path, adapter, column_collection=None, parentmapper=None, **kwargs): """Add a left outer join to the statement thats being constructed.""" + if not context.enable_eagerloads: + return + path = path + (self.key,) # check for user-defined eager alias @@ -649,7 +652,7 @@ class EagerLoader(AbstractRelationLoader): # whether or not the Query will wrap the selectable in a subquery, # and then attach eager load joins to that (i.e., in the case of LIMIT/OFFSET etc.) should_nest_selectable = context.query._should_nest_selectable - + if entity in context.eager_joins: entity_key, default_towrap = entity, entity.selectable elif should_nest_selectable or not context.from_clause or not sql_util.search(context.from_clause, entity.selectable): diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index c44583d338..0288f9964a 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -378,6 +378,10 @@ class _ORMJoin(expression.Join): if isinstance(onclause, basestring): prop = left_mapper.get_property(onclause) elif isinstance(onclause, attributes.QueryableAttribute): + # TODO: we might want to honor the current adapt_from, + # if already set. we would need to adjust how we calculate + # adapt_from though since it is present in too many cases + # at the moment (query tests illustrate that). adapt_from = onclause.__clause_element__() prop = onclause.property elif isinstance(onclause, MapperProperty): diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 48b1d0687f..65cc903dd0 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -3123,6 +3123,12 @@ class CompoundSelect(_SelectBaseMixin, FromClause): def self_group(self, against=None): return _FromGrouping(self) + def is_derived_from(self, fromclause): + for s in self.selects: + if s.is_derived_from(fromclause): + return True + return False + def _populate_column_collection(self): for cols in zip(*[s.c for s in self.selects]): proxy = cols[0]._make_proxy(self, name=self.use_labels and cols[0]._label or None) diff --git a/test/orm/query.py b/test/orm/query.py index cba57914d1..18363ac98a 100644 --- a/test/orm/query.py +++ b/test/orm/query.py @@ -714,7 +714,7 @@ class FilterTest(QueryTest): self.assertEquals([User(id=7),User(id=8),User(id=9)], sess.query(User).filter(User.addresses!=None).order_by(User.id).all()) -class FromSelfTest(QueryTest): +class FromSelfTest(QueryTest, AssertsCompiledSQL): def test_filter(self): assert [User(id=8), User(id=9)] == create_session().query(User).filter(User.id.in_([8,9]))._from_self().all() @@ -728,8 +728,22 @@ class FromSelfTest(QueryTest): (User(id=8), Address(id=3)), (User(id=8), Address(id=4)), (User(id=9), Address(id=5)) - ] == create_session().query(User).filter(User.id.in_([8,9]))._from_self().join('addresses').add_entity(Address).order_by(User.id, Address.id).all() + ] == create_session().query(User).filter(User.id.in_([8,9]))._from_self().\ + join('addresses').add_entity(Address).order_by(User.id, Address.id).all() + def test_no_eagerload(self): + """test that eagerloads are pushed outwards and not rendered in subqueries.""" + + s = create_session() + + self.assert_compile( + s.query(User).options(eagerload(User.addresses)).from_self().statement, + "SELECT anon_1.users_id, anon_1.users_name, addresses_1.id, addresses_1.user_id, "\ + "addresses_1.email_address FROM (SELECT users.id AS users_id, users.name AS users_name FROM users) AS anon_1 "\ + "LEFT OUTER JOIN addresses AS addresses_1 ON anon_1.users_id = addresses_1.user_id ORDER BY addresses_1.id" + ) + + def test_multiple_entities(self): sess = create_session() @@ -748,6 +762,56 @@ class FromSelfTest(QueryTest): # order_by(User.id, Address.id).first(), (User(id=8, addresses=[Address(), Address(), Address()]), Address(id=2)), ) + +class SetOpsTest(QueryTest, AssertsCompiledSQL): + + def test_union(self): + s = create_session() + + fred = s.query(User).filter(User.name=='fred') + ed = s.query(User).filter(User.name=='ed') + jack = s.query(User).filter(User.name=='jack') + + self.assertEquals(fred.union(ed).order_by(User.name).all(), + [User(name='ed'), User(name='fred')] + ) + + self.assertEquals(fred.union(ed, jack).order_by(User.name).all(), + [User(name='ed'), User(name='fred'), User(name='jack')] + ) + + @testing.fails_on('mysql', "mysql doesn't support intersect") + def test_intersect(self): + s = create_session() + + fred = s.query(User).filter(User.name=='fred') + ed = s.query(User).filter(User.name=='ed') + jack = s.query(User).filter(User.name=='jack') + self.assertEquals(fred.intersect(ed, jack).all(), + [] + ) + + self.assertEquals(fred.union(ed).intersect(ed.union(jack)).all(), + [User(name='ed')] + ) + + def test_eager_load(self): + s = create_session() + + fred = s.query(User).filter(User.name=='fred') + ed = s.query(User).filter(User.name=='ed') + jack = s.query(User).filter(User.name=='jack') + + def go(): + self.assertEquals( + fred.union(ed).order_by(User.name).options(eagerload(User.addresses)).all(), + [ + User(name='ed', addresses=[Address(), Address(), Address()]), + User(name='fred', addresses=[Address()]) + ] + ) + self.assert_sql_count(testing.db, go, 1) + class AggregateTest(QueryTest): diff --git a/test/sql/generative.py b/test/sql/generative.py index 39a72d89b5..2072fb75e8 100644 --- a/test/sql/generative.py +++ b/test/sql/generative.py @@ -284,6 +284,11 @@ class ClauseTest(TestBase, AssertsCompiledSQL): assert str(u) == str(u2) == str(u3) assert u2.compile().params == {'id_param':7} assert u3.compile().params == {'id_param':10} + + def test_adapt_union(self): + u = union(t1.select().where(t1.c.col1==4), t1.select().where(t1.c.col1==5)).alias() + + assert sql_util.ClauseAdapter(u).traverse(t1) is u def test_binds(self): """test that unique bindparams change their name upon clone() to prevent conflicts"""