diff options
-rw-r--r-- | lib/sqlalchemy/ansisql.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/engine.py | 9 | ||||
-rw-r--r-- | lib/sqlalchemy/mapping/properties.py | 217 | ||||
-rw-r--r-- | lib/sqlalchemy/schema.py | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/sql.py | 50 | ||||
-rw-r--r-- | test/mapper.py | 7 |
6 files changed, 140 insertions, 152 deletions
diff --git a/lib/sqlalchemy/ansisql.py b/lib/sqlalchemy/ansisql.py index 88bd0f8bd..ac10d27f1 100644 --- a/lib/sqlalchemy/ansisql.py +++ b/lib/sqlalchemy/ansisql.py @@ -239,8 +239,8 @@ class ANSICompiler(sql.Compiled): return self.engine.bindtemplate % name def visit_alias(self, alias): - self.froms[alias] = self.get_from_text(alias.selectable) + " AS " + alias.name - self.strings[alias] = self.get_str(alias.selectable) + self.froms[alias] = self.get_from_text(alias.original) + " AS " + alias.name + self.strings[alias] = self.get_str(alias.original) def visit_select(self, select): diff --git a/lib/sqlalchemy/engine.py b/lib/sqlalchemy/engine.py index 1ca9d00fd..73b8769f2 100644 --- a/lib/sqlalchemy/engine.py +++ b/lib/sqlalchemy/engine.py @@ -546,7 +546,7 @@ class SQLEngine(schema.SchemaEngine): else: parameters = parameters.values() - self.execute(statement, parameters, connection=connection, cursor=cursor) + self.execute(statement, parameters, connection=connection, cursor=cursor, return_raw=True) return cursor self.pre_exec(proxy, compiled, parameters, **kwargs) @@ -555,7 +555,7 @@ class SQLEngine(schema.SchemaEngine): self.post_exec(proxy, compiled, parameters, **kwargs) return ResultProxy(cursor, self, typemap=compiled.typemap) - def execute(self, statement, parameters, connection=None, cursor=None, echo=None, typemap=None, commit=False, **kwargs): + def execute(self, statement, parameters, connection=None, cursor=None, echo=None, typemap=None, commit=False, return_raw=False, **kwargs): """executes the given string-based SQL statement with the given parameters. The parameters can be a dictionary or a list, or a list of dictionaries or lists, depending @@ -606,7 +606,10 @@ class SQLEngine(schema.SchemaEngine): except: self.do_rollback(connection) raise - return ResultProxy(cursor, self, typemap=typemap) + if return_raw: + return cursor + else: + return ResultProxy(cursor, self, typemap=typemap) def _execute(self, c, statement, parameters): try: diff --git a/lib/sqlalchemy/mapping/properties.py b/lib/sqlalchemy/mapping/properties.py index 3a2aaab2f..571bfc0b5 100644 --- a/lib/sqlalchemy/mapping/properties.py +++ b/lib/sqlalchemy/mapping/properties.py @@ -107,7 +107,7 @@ class PropertyLoader(MapperProperty): """describes an object property that holds a single item or list of items that correspond to a related database table.""" - def __init__(self, argument, secondary, primaryjoin, secondaryjoin, foreignkey=None, uselist=None, private=False, association=None, use_alias=False, selectalias=None, order_by=False, attributeext=None, backref=None, is_backref=False, post_update=False): + def __init__(self, argument, secondary, primaryjoin, secondaryjoin, foreignkey=None, uselist=None, private=False, association=None, use_alias=None, selectalias=None, order_by=False, attributeext=None, backref=None, is_backref=False, post_update=False): self.uselist = uselist self.argument = argument self.secondary = secondary @@ -127,11 +127,10 @@ class PropertyLoader(MapperProperty): self.private = private self.association = association - if isinstance(selectalias, str): - print "'selectalias' argument to property is deprecated. please use 'use_alias=True'" - self.use_alias = True - else: - self.use_alias = use_alias + if selectalias is not None: + print "'selectalias' argument to relation() is deprecated. eager loads automatically alias-ize tables now." + if use_alias is not None: + print "'use_alias' argument to relation() is deprecated. eager loads automatically alias-ize tables now." self.order_by = order_by self.attributeext=attributeext self.backref = backref @@ -289,7 +288,6 @@ class PropertyLoader(MapperProperty): elif self.association is not None: c = self.mapper._get_criterion(key, value) & self.primaryjoin return c.copy_container() - return None def register_deleted(self, obj, uow): @@ -743,97 +741,71 @@ class EagerLoader(PropertyLoader): if recursion_stack is None: recursion_stack = {} - - if self.use_alias: - pass - - # figure out tables in the various join clauses we have, because user-defined - # whereclauses that reference the same tables will be converted to use - # aliases of those tables - self.to_alias = util.HashSet() - [self.to_alias.append(f) for f in self.primaryjoin._get_from_objects()] - if self.secondaryjoin is not None: - [self.to_alias.append(f) for f in self.secondaryjoin._get_from_objects()] - try: - del self.to_alias[parent.table] - except KeyError: - pass - # if this eagermapper is to select using an "alias" to isolate it from other - # eager mappers against the same table, we have to redefine our secondary - # or primary join condition to reference the aliased table (and the order_by). - # else we set up the target clause objects as what they are defined in the - # superclass. - if self.use_alias: - self.eagertarget = self.target.alias() - aliasizer = Aliasizer(self.target, aliases={self.target:self.eagertarget}) - if self.secondaryjoin is not None: - self.eagersecondary = self.secondaryjoin.copy_container() - self.eagersecondary.accept_visitor(aliasizer) - self.eagerprimary = self.primaryjoin.copy_container() - self.eagerprimary.accept_visitor(aliasizer) - else: - self.eagerprimary = self.primaryjoin.copy_container() - self.eagerprimary.accept_visitor(aliasizer) - if self.order_by: - self.eager_order_by = [o.copy_container() for o in util.to_list(self.order_by)] - for i in range(0, len(self.eager_order_by)): - if isinstance(self.eager_order_by[i], schema.Column): - self.eager_order_by[i] = self.eagertarget._get_col_by_original(self.eager_order_by[i]) - else: - self.eager_order_by[i].accept_visitor(aliasizer) - else: - self.eager_order_by = self.order_by - - # we have to propigate the "use_alias" fact into - # any sub-mappers that are also eagerloading so that they create a unique tablename - # as well. this copies our child mapper and replaces any eager properties on the - # new mapper with an equivalent eager property, just containing use_alias=True - eagerprops = [] - for key, prop in self.mapper.props.iteritems(): - if isinstance(prop, EagerLoader) and not prop.use_alias: - eagerprops.append(prop) - if len(eagerprops): - recursion_stack[self] = True - self.mapper = self.mapper.copy() - try: - for prop in eagerprops: - p = prop.copy() - p.use_alias=True - - self.mapper.props[prop.key] = p - - if recursion_stack.has_key(prop): - raise ArgumentError("Circular eager load relationship detected on " + str(self.mapper) + " " + key + repr(self.mapper.props)) - - p.do_init_subclass(prop.key, prop.parent, recursion_stack) - - p.eagerprimary = p.eagerprimary.copy_container() - aliasizer = Aliasizer(p.parent.table, aliases={p.parent.table:self.eagertarget}) - p.eagerprimary.accept_visitor(aliasizer) - finally: - del recursion_stack[self] - + self.eagertarget = self.target.alias() + if self.secondary: + self.eagersecondary = self.secondary.alias() + self.aliasizer = Aliasizer(self.target, self.secondary, aliases={ + self.target:self.eagertarget, + self.secondary:self.eagersecondary + }) + self.eagersecondaryjoin = self.secondaryjoin.copy_container() + self.eagersecondaryjoin.accept_visitor(self.aliasizer) + self.eagerprimary = self.primaryjoin.copy_container() + self.eagerprimary.accept_visitor(self.aliasizer) else: - self.eagertarget = self.target - self.eagerprimary = self.primaryjoin - self.eagersecondary = self.secondaryjoin - self.eager_order_by = self.order_by - - def setup(self, key, statement, recursion_stack = None, eagertable=None, **options): - """add a left outer join to the statement thats being constructed""" - - if recursion_stack is None: - recursion_stack = {} + self.aliasizer = Aliasizer(self.target, aliases={self.target:self.eagertarget}) + self.eagerprimary = self.primaryjoin.copy_container() + self.eagerprimary.accept_visitor(self.aliasizer) - if statement.whereclause is not None: - # "aliasize" the tables referenced in the user-defined whereclause to not - # collide with the tables used by the eager load - # note that we arent affecting the mapper's table, nor our own primary or secondary joins - aliasizer = Aliasizer(*self.to_alias) - statement.whereclause.accept_visitor(aliasizer) - for alias in aliasizer.aliases.values(): - statement.append_from(alias) + if self.order_by: + self.eager_order_by = self._aliasize_orderby(self.order_by) + else: + self.eager_order_by = None + + eagerprops = [] + # create a new "eager chain", starting from this eager loader and descending downwards + # through all sub-eagerloaders. this will copy all those eagerloaders and have them set up + # aliases distinct to this eager chain. if a recursive relationship to any of the tables is detected, + # the chain will terminate by copying that eager loader into a lazy loader. + for key, prop in self.mapper.props.iteritems(): + if isinstance(prop, EagerLoader): + eagerprops.append(prop) + if len(eagerprops): + recursion_stack[self.parent.table] = True + self.mapper = self.mapper.copy() + try: + for prop in eagerprops: + if recursion_stack.has_key(prop.target): + # recursion - set the relationship as a LazyLoader + p = EagerLazyOption(None, False).create_prop(self.mapper, prop.key) + continue + p = prop.copy() + self.mapper.props[prop.key] = p + #print "we are:", id(self), self.target.name, (self.secondary and self.secondary.name or "None"), self.parent.table.name + #print "prop is",id(prop), prop.target.name, (prop.secondary and prop.secondary.name or "None"), prop.parent.table.name + p.do_init_subclass(prop.key, prop.parent, recursion_stack) + p.eagerprimary = p.eagerprimary.copy_container() + aliasizer = Aliasizer(p.parent.table, aliases={p.parent.table:self.eagertarget}) + p.eagerprimary.accept_visitor(aliasizer) + #print "new eagertqarget", p.eagertarget.name, (p.secondary and p.secondary.name or "none"), p.parent.table.name + finally: + del recursion_stack[self.parent.table] + + def _aliasize_orderby(self, orderby, copy=True): + if copy: + orderby = [o.copy_container() for o in util.to_list(orderby)] + else: + orderby = util.to_list(orderby) + for i in range(0, len(orderby)): + if isinstance(orderby[i], schema.Column): + orderby[i] = self.eagertarget._get_col_by_original(orderby[i]) + else: + orderby[i].accept_visitor(self.aliasizer) + return orderby + + def setup(self, key, statement, eagertable=None, **options): + """add a left outer join to the statement thats being constructed""" if hasattr(statement, '_outerjoin'): towrap = statement._outerjoin @@ -841,9 +813,9 @@ class EagerLoader(PropertyLoader): towrap = self.parent.table if self.secondaryjoin is not None: - statement._outerjoin = sql.outerjoin(towrap, self.secondary, self.eagerprimary).outerjoin(self.eagertarget, self.eagersecondary) + statement._outerjoin = sql.outerjoin(towrap, self.eagersecondary, self.eagerprimary).outerjoin(self.eagertarget, self.eagersecondaryjoin) if self.order_by is False and self.secondary.default_order_by() is not None: - statement.order_by(*self.secondary.default_order_by()) + statement.order_by(*self.eagersecondary.default_order_by()) else: statement._outerjoin = towrap.outerjoin(self.eagertarget, self.eagerprimary) if self.order_by is False and self.eagertarget.default_order_by() is not None: @@ -851,16 +823,12 @@ class EagerLoader(PropertyLoader): if self.eager_order_by: statement.order_by(*util.to_list(self.eager_order_by)) - + elif getattr(statement, 'order_by_clause', None): + self._aliasize_orderby(statement.order_by_clause, False) + statement.append_from(statement._outerjoin) - recursion_stack[self] = True - try: - for key, value in self.mapper.props.iteritems(): - if recursion_stack.has_key(value): - raise InvalidRequestError("Circular eager load relationship detected on " + str(self.mapper) + " " + key + repr(self.mapper.props)) - value.setup(key, statement, recursion_stack=recursion_stack, eagertable=self.eagertarget) - finally: - del recursion_stack[self] + for key, value in self.mapper.props.iteritems(): + value.setup(key, statement, eagertable=self.eagertarget) def execute(self, instance, row, identitykey, imap, isnew): """receive a row. tell our mapper to look for a new object instance in the row, and attach @@ -884,16 +852,10 @@ class EagerLoader(PropertyLoader): def _instance(self, row, imap, result_list=None): """gets an instance from a row, via this EagerLoader's mapper.""" - # if we have an alias for our mapper's table via the use_alias - # parameter, we need to translate the - # aliased columns from the incoming row into a new row that maps - # the values against the columns of the mapper's original non-aliased table. - if self.use_alias: - fakerow = {} - fakerow = util.DictDecorator(row) - for c in self.eagertarget.c: - fakerow[c.original] = row[c] - row = fakerow + fakerow = util.DictDecorator(row) + for c in self.eagertarget.c: + fakerow[c.parent] = row[c] + row = fakerow return self.mapper._instance(row, imap, result_list) class GenericOption(MapperOption): @@ -918,6 +880,7 @@ class GenericOption(MapperOption): def create_prop(self, mapper, key): kwargs = util.constructor_args(oldprop) mapper.set_property(key, class_(**kwargs )) + class EagerLazyOption(GenericOption): """an option that switches a PropertyLoader to be an EagerLoader or LazyLoader""" @@ -941,10 +904,6 @@ class EagerLazyOption(GenericOption): newprop = class_.__new__(class_) newprop.__dict__.update(oldprop.__dict__) newprop.do_init_subclass(key, mapper) - if self.kwargs.get('selectalias', None): - newprop.use_alias = True - elif self.kwargs.get('use_alias', None) is not None: - newprop.use_alias = self.kwargs['use_alias'] mapper.set_property(key, newprop) class DeferredOption(GenericOption): @@ -969,28 +928,26 @@ class Aliasizer(sql.ClauseVisitor): for t in tables: self.tables[t] = t self.binary = None - self.match = False self.aliases = kwargs.get('aliases', {}) - def get_alias(self, table): try: return self.aliases[table] except: return self.aliases.setdefault(table, sql.alias(table)) - def visit_compound(self, compound): - for i in range(0, len(compound.clauses)): - if isinstance(compound.clauses[i], schema.Column) and self.tables.has_key(compound.clauses[i].table): - compound.clauses[i] = self.get_alias(compound.clauses[i].table)._get_col_by_original(compound.clauses[i]) - self.match = True - + 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)._get_col_by_original(clist.clauses[i]) + if clist.clauses[i] is None: + raise "cant get orig for " + str(orig) + " against table " + orig.table.name + " " + self.get_alias(orig.table).name 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)._get_col_by_original(binary.left) - self.match = True if isinstance(binary.right, schema.Column) and self.tables.has_key(binary.right.table): binary.right = self.get_alias(binary.right.table)._get_col_by_original(binary.right) - self.match = True class BinaryVisitor(sql.ClauseVisitor): def __init__(self, func): diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 12b3c7707..857fc3cf7 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -248,10 +248,12 @@ class Column(SchemaItem): self.default = kwargs.pop('default', None) self.foreign_key = None self._orig = None + self._parent = None if len(kwargs): raise ArgumentError("Unknown arguments passed to Column: " + repr(kwargs.keys())) original = property(lambda s: s._orig or s) + parent = property(lambda s:s._parent or s) engine = property(lambda s: s.table.engine) def __repr__(self): @@ -307,6 +309,7 @@ class Column(SchemaItem): c = Column(name or self.name, self.type, fk, self.default, key = name or self.key, primary_key = self.primary_key, nullable = self.nullable, hidden = self.hidden) c.table = selectable c._orig = self.original + c._parent = self if not c.hidden: selectable.columns[c.key] = c if self.primary_key: @@ -369,7 +372,7 @@ class ForeignKey(SchemaItem): def references(self, table): """returns True if the given table is referenced by this ForeignKey.""" - return table._get_col_by_original(self.column) is not None + return table._get_col_by_original(self.column, False) is not None def _init_column(self): # ForeignKey inits its remote column as late as possible, so tables can diff --git a/lib/sqlalchemy/sql.py b/lib/sqlalchemy/sql.py index 9b3571384..cf42b2e83 100644 --- a/lib/sqlalchemy/sql.py +++ b/lib/sqlalchemy/sql.py @@ -522,6 +522,7 @@ class ColumnElement(Selectable, CompareMixin): primary_key = property(lambda self:getattr(self, '_primary_key', False)) foreign_key = property(lambda self:getattr(self, '_foreign_key', False)) original = property(lambda self:getattr(self, '_original', self)) + parent = property(lambda self:getattr(self, '_parent', self)) columns = property(lambda self:[self]) def _make_proxy(self, selectable, name=None): """creates a new ColumnElement representing this ColumnElement as it appears in the select list @@ -563,12 +564,19 @@ class FromClause(Selectable): return Join(self, right, isouter = True, *args, **kwargs) def alias(self, name=None): return Alias(self, name) - def _get_col_by_original(self, column): + def _get_col_by_original(self, column, raiseerr=True): """given a column which is a schema.Column object attached to a schema.Table object (i.e. an "original" column), return the Column object from this Selectable which corresponds to that original Column, or None if this Selectable does not contain the column.""" - return self.original_columns.get(column.original, None) + try: + return self.original_columns[column.original] + except KeyError: + if not raiseerr: + return None + else: + raise InvalidRequestError("cant get orig for " + str(column) + " with table " + column.table.id + " from table " + self.id) + def _get_exported_attribute(self, name): try: return getattr(self, name) @@ -595,6 +603,8 @@ class FromClause(Selectable): for co in column.columns: cp = self._proxy_column(co) self._orig_cols[co.original] = cp + if getattr(self, 'oid_column', None): + self._orig_cols[self.oid_column.original] = self.oid_column def _exportable_columns(self): return [] def _proxy_column(self, column): @@ -699,6 +709,8 @@ class ClauseList(ClauseElement): self.clauses.append(clause) def accept_visitor(self, visitor): for c in self.clauses: + if c is None: + raise "oh weird" + repr(self.clauses) c.accept_visitor(visitor) visitor.visit_clauselist(self) def _get_from_objects(self): @@ -904,13 +916,17 @@ class Join(FromClause): class Alias(FromClause): def __init__(self, selectable, alias = None): - while isinstance(selectable, Alias): - selectable = selectable.selectable + baseselectable = selectable + while isinstance(baseselectable, Alias): + baseselectable = baseselectable.selectable + self.original = baseselectable self.selectable = selectable if alias is None: - n = getattr(selectable, 'name') + n = getattr(self.original, 'name') if n is None: n = 'anon' + elif len(n) > 15: + n = n[0:15] alias = n + "_" + hex(random.randint(0, 65535))[2:] self.name = alias self.id = self.name @@ -949,6 +965,7 @@ class Label(ColumnElement): key = property(lambda s: s.name) _label = property(lambda s: s.name) original = property(lambda s:s.obj.original) + parent = property(lambda s:s.obj.parent) def accept_visitor(self, visitor): self.obj.accept_visitor(visitor) visitor.visit_label(self) @@ -1009,7 +1026,8 @@ class ColumnImpl(ColumnElement): engine = property(lambda s: s.column.engine) default_label = property(lambda s:s._label) - original = property(lambda self:self.column) + original = property(lambda self:self.column.original) + parent = property(lambda self:self.column.parent) columns = property(lambda self:[self.column]) def label(self, name): @@ -1073,6 +1091,9 @@ class TableImpl(FromClause): self._orig_cols= {} for c in self.columns: self._orig_cols[c.original] = c + oid = self.oid_column + if oid is not None: + self._orig_cols[oid.original] = oid return self._orig_cols oid_column = property(_oid_col) @@ -1132,13 +1153,18 @@ class SelectBaseMixin(object): if not hasattr(self, attribute): l = ClauseList(*clauses) setattr(self, attribute, l) - self.append_clause(prefix, l) else: getattr(self, attribute).clauses += clauses - def append_clause(self, keyword, clause): - if type(clause) == str: - clause = TextClause(clause) - self.clauses.append((keyword, clause)) + def _get_clauses(self): + # TODO: this is a little stupid. make ORDER BY/GROUP BY keywords handled by + # the compiler, make group_by_clause/order_by_clause regular attributes + x =[] + if getattr(self, 'group_by_clause', None): + x.append(("GROUP BY", self.group_by_clause)) + if getattr(self, 'order_by_clause', None): + x.append(("ORDER BY", self.order_by_clause)) + return x + clauses = property(_get_clauses) def select(self, whereclauses = None, **params): return select([self], whereclauses, **params) def _get_from_objects(self): @@ -1157,7 +1183,6 @@ class CompoundSelect(SelectBaseMixin, FromClause): for s in self.selects: s.group_by(None) s.order_by(None) - self.clauses = [] group_by = kwargs.get('group_by', None) if group_by: self.group_by(*group_by) @@ -1211,7 +1236,6 @@ class Select(SelectBaseMixin, FromClause): # indicates if this select statement is a subquery as a criterion # inside of a WHERE clause self.is_where = False - self.clauses = [] self.distinct = distinct self._text = None diff --git a/test/mapper.py b/test/mapper.py index 38c168dfe..102c20966 100644 --- a/test/mapper.py +++ b/test/mapper.py @@ -538,7 +538,7 @@ class EagerTest(MapperSuperTest): self.assert_result(l, User, *user_all_result) objectstore.clear() m = mapper(Item, orderitems, is_primary=True, properties = dict( - keywords = relation(mapper(Keyword, keywords), itemkeywords, lazy = False), + keywords = relation(mapper(Keyword, keywords), itemkeywords, lazy = False, order_by=[keywords.c.keyword_id]), )) l = m.select((Item.c.item_name=='item 2') | (Item.c.item_name=='item 5') | (Item.c.item_name=='item 3'), order_by=[Item.c.item_id], limit=2) self.assert_result(l, Item, *[item_keyword_result[1], item_keyword_result[2]]) @@ -617,6 +617,7 @@ class EagerTest(MapperSuperTest): """tests eager loading with two relations simulatneously, from the same table. """ openorders = alias(orders, 'openorders') closedorders = alias(orders, 'closedorders') + ordermapper = mapper(Order, orders) m = mapper(User, users, properties = dict( addresses = relation(mapper(Address, addresses), lazy = False), open_orders = relation(mapper(Order, openorders), primaryjoin = and_(openorders.c.isopen == 1, users.c.user_id==openorders.c.user_id), lazy = False), @@ -659,7 +660,7 @@ class EagerTest(MapperSuperTest): items = orderitems m = mapper(Item, items, properties = dict( - keywords = relation(mapper(Keyword, keywords), itemkeywords, lazy=False), + keywords = relation(mapper(Keyword, keywords), itemkeywords, lazy=False, order_by=[keywords.c.keyword_id]), )) l = m.select() self.assert_result(l, Item, *item_keyword_result) @@ -678,7 +679,7 @@ class EagerTest(MapperSuperTest): m = mapper(Item, items, properties = dict( - keywords = relation(mapper(Keyword, keywords), itemkeywords, lazy = False), + keywords = relation(mapper(Keyword, keywords), itemkeywords, lazy = False, order_by=[keywords.c.keyword_id]), )) m = mapper(Order, orders, properties = dict( |