From: Mike Bayer Date: Mon, 19 Jun 2006 21:44:56 +0000 (+0000) Subject: more development on using eager loads/limit/offset/join_via/order_by at the same... X-Git-Tag: rel_0_2_4~27 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=4445453a0cc1ca5e9f8095a4f8f3cf1371f91220;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git more development on using eager loads/limit/offset/join_via/order_by at the same time --- diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 4e096d99bd..196deab796 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -8,7 +8,7 @@ well as relationships. also defines some MapperOptions that can be used with the properties.""" -from sqlalchemy import sql, schema, util, attributes, exceptions +from sqlalchemy import sql, schema, util, attributes, exceptions, sql_util import sync import mapper import session as sessionlib @@ -450,7 +450,7 @@ class EagerLoader(LazyLoader): self.eagertarget = self.target.alias() if self.secondary: self.eagersecondary = self.secondary.alias() - self.aliasizer = Aliasizer(self.target, self.secondary, aliases={ + self.aliasizer = sql_util.Aliasizer(self.target, self.secondary, aliases={ self.target:self.eagertarget, self.secondary:self.eagersecondary }) @@ -461,7 +461,7 @@ class EagerLoader(LazyLoader): self.eagerprimary.accept_visitor(self.aliasizer) #print "JOINS:", str(self.eagerprimary), "|", str(self.eagersecondaryjoin) else: - self.aliasizer = Aliasizer(self.target, aliases={self.target:self.eagertarget}) + self.aliasizer = sql_util.Aliasizer(self.target, aliases={self.target:self.eagertarget}) self.eagerprimary = self.primaryjoin.copy_container() self.eagerprimary.accept_visitor(self.aliasizer) @@ -505,7 +505,7 @@ class EagerLoader(LazyLoader): p.do_init_subclass(prop.key, prop.parent, recursion_stack) p._create_eager_chain(recursion_stack=recursion_stack) p.eagerprimary = p.eagerprimary.copy_container() -# aliasizer = Aliasizer(p.parent.mapped_table, aliases={p.parent.mapped_table:self.eagertarget}) +# aliasizer = sql_util.Aliasizer(p.parent.mapped_table, aliases={p.parent.mapped_table:self.eagertarget}) p.eagerprimary.accept_visitor(self.aliasizer) #print "new eagertqarget", p.eagertarget.name, (p.secondary and p.secondary.name or "none"), p.parent.mapped_table.name finally: @@ -743,34 +743,6 @@ class DeferredOption(GenericOption): prop = ColumnProperty(*oldprop.columns, **self.kwargs) mapper._compile_property(key, prop) -class Aliasizer(sql.ClauseVisitor): - """converts a table instance within an expression to be an alias of that table.""" - def __init__(self, *tables, **kwargs): - self.tables = {} - self.aliases = kwargs.get('aliases', {}) - for t in tables: - self.tables[t] = t - if not self.aliases.has_key(t): - self.aliases[t] = sql.alias(t) - if isinstance(t, sql.Join): - for t2 in t.columns: - self.tables[t2.table] = t2 - self.aliases[t2.table] = self.aliases[t] - self.binary = None - def get_alias(self, table): - return self.aliases[table] - def visit_compound(self, compound): - self.visit_clauselist(compound) - def visit_clauselist(self, clist): - for i in range(0, len(clist.clauses)): - if isinstance(clist.clauses[i], schema.Column) and self.tables.has_key(clist.clauses[i].table): - orig = clist.clauses[i] - clist.clauses[i] = self.get_alias(clist.clauses[i].table).corresponding_column(clist.clauses[i]) - def visit_binary(self, binary): - if isinstance(binary.left, schema.Column) and self.tables.has_key(binary.left.table): - binary.left = self.get_alias(binary.left.table).corresponding_column(binary.left) - if isinstance(binary.right, schema.Column) and self.tables.has_key(binary.right.table): - binary.right = self.get_alias(binary.right.table).corresponding_column(binary.right) class BinaryVisitor(sql.ClauseVisitor): def __init__(self, func): diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 9111b78ecd..e3365b4078 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -5,7 +5,8 @@ # the MIT License: http://www.opensource.org/licenses/mit-license.php import session as sessionlib -from sqlalchemy import sql, util, exceptions +from sqlalchemy import sql, util, exceptions, sql_util + import mapper @@ -313,7 +314,17 @@ class Query(object): if self._should_nest(**kwargs): from_obj.append(self.table) - s2 = sql.select(self.table.primary_key, whereclause, use_labels=True, from_obj=from_obj, **kwargs) + + # if theres an order by, add those columns to the column list + # of the "rowcount" query we're going to make + if order_by: + order_by = util.to_list(order_by) or [] + cf = sql_util.ColumnFinder() + [o.accept_visitor(cf) for o in order_by] + else: + cf = [] + + s2 = sql.select(self.table.primary_key + list(cf), whereclause, use_labels=True, from_obj=from_obj, **kwargs) # raise "ok first thing", str(s2) if not kwargs.get('distinct', False) and order_by: s2.order_by(*util.to_list(order_by)) @@ -323,11 +334,16 @@ class Query(object): crit.append(s3.primary_key[i] == self.table.primary_key[i]) statement = sql.select([], sql.and_(*crit), from_obj=[self.table], use_labels=True) # raise "OK statement", str(statement) + + # now for the order by, convert the columns to their corresponding columns + # in the "rowcount" query, and tack that new order by onto the "rowcount" query if order_by: - # copy the order_by, since eager loaders will modify it, and we want the - # "inner" order_by to remain untouched - # see test/orm/mapper.py EagerTest.testmorelimit - order_by = [o.copy_container() for o in util.to_list(order_by)] + class Aliasizer(sql_util.Aliasizer): + def get_alias(self, table): + return s3 + order_by = [o.copy_container() for o in order_by] + aliasizer = Aliasizer(*[t for t in sql_util.TableFinder(s3)]) + [o.accept_visitor(aliasizer) for o in order_by] statement.order_by(*util.to_list(order_by)) else: from_obj.append(self.table) diff --git a/lib/sqlalchemy/sql_util.py b/lib/sqlalchemy/sql_util.py index 082b7c80fa..4015fd2442 100644 --- a/lib/sqlalchemy/sql_util.py +++ b/lib/sqlalchemy/sql_util.py @@ -1,5 +1,6 @@ import sqlalchemy.sql as sql import sqlalchemy.schema as schema +import sqlalchemy.util as util """utility functions that build upon SQL and Schema constructs""" @@ -70,3 +71,39 @@ class TableFinder(TableCollection, sql.ClauseVisitor): if self.check_columns: column.table.accept_visitor(self) +class ColumnFinder(sql.ClauseVisitor): + def __init__(self): + self.columns = util.Set() + def visit_column(self, c): + self.columns.add(c) + def __iter__(self): + return iter(self.columns) + +class Aliasizer(sql.ClauseVisitor): + """converts a table instance within an expression to be an alias of that table.""" + def __init__(self, *tables, **kwargs): + self.tables = {} + self.aliases = kwargs.get('aliases', {}) + for t in tables: + self.tables[t] = t + if not self.aliases.has_key(t): + self.aliases[t] = sql.alias(t) + if isinstance(t, sql.Join): + for t2 in t.columns: + self.tables[t2.table] = t2 + self.aliases[t2.table] = self.aliases[t] + self.binary = None + def get_alias(self, table): + return self.aliases[table] + def visit_compound(self, compound): + self.visit_clauselist(compound) + def visit_clauselist(self, clist): + for i in range(0, len(clist.clauses)): + if isinstance(clist.clauses[i], schema.Column) and self.tables.has_key(clist.clauses[i].table): + orig = clist.clauses[i] + clist.clauses[i] = self.get_alias(clist.clauses[i].table).corresponding_column(clist.clauses[i]) + def visit_binary(self, binary): + if isinstance(binary.left, schema.Column) and self.tables.has_key(binary.left.table): + binary.left = self.get_alias(binary.left.table).corresponding_column(binary.left) + if isinstance(binary.right, schema.Column) and self.tables.has_key(binary.right.table): + binary.right = self.get_alias(binary.right.table).corresponding_column(binary.right) diff --git a/test/orm/mapper.py b/test/orm/mapper.py index 13e79bc720..e1cdb51779 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -796,8 +796,8 @@ class EagerTest(MapperSuperTest): self.assert_result(l, Item, *[item_keyword_result[1], item_keyword_result[2]]) def testmorelimit(self): - """tests that the ORDER BY doesnt get clobbered with a nested eager load, when the ORDER BY - is an expression. requires the copying of the order by clause in query.compile()""" + """test that the ORDER BY is propigated from the inner select to the outer select, when using the + 'wrapped' select statement resulting from the combination of eager loading and limit/offset clauses.""" ordermapper = mapper(Order, orders, properties = dict( items = relation(mapper(Item, orderitems), lazy = False) )) @@ -812,6 +812,9 @@ class EagerTest(MapperSuperTest): l = q.select(q.join_to('orders'), order_by=desc(orders.c.user_id), limit=2, offset=1) self.assert_result(l, User, *(user_all_result[2], user_all_result[0])) + l = q.select(q.join_to('addresses'), order_by=desc(addresses.c.email_address), limit=1, offset=0) + self.assert_result(l, User, *(user_all_result[0],)) + def testonetoone(self): m = mapper(User, users, properties = dict( address = relation(mapper(Address, addresses), lazy = False, uselist = False)