From: Mike Bayer Date: Sun, 24 Jul 2011 21:51:01 +0000 (-0400) Subject: - rewrite cloned_traverse() and replacement_traverse() to use a straight X-Git-Tag: rel_0_7_2~10 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8a483dbf38168ff43ca0652229b1d46afb23235d;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - rewrite cloned_traverse() and replacement_traverse() to use a straight recursive descent with clone() + _copy_internals(). This is essentially what it was doing anyway with lots of unnecessary steps. Fix Alias() to honor the given clone() function which may have been the reason the traversal hadn't been fixed sooner. Alias._copy_internals() will specifically skip an alias of a Table as a more specific form of what it was doing before. This may need to be further improved such that ClauseAdapter or replacement_traverse() send it some specific hints what not to dig into; **kw has been added to all _copy_internals() to support this. replacement/clone traversal is at least clear now. - apply new no_replacement_traverse annotation to join created by _create_joins(), fixes [ticket:2195] - can replace orm.query "_halt_adapt" with "no_replacement_traverse" --- diff --git a/CHANGES b/CHANGES index 6744bfb0b2..7fa9ffd779 100644 --- a/CHANGES +++ b/CHANGES @@ -32,6 +32,12 @@ CHANGES _with_invoke_all_eagers() which selects old/new behavior [ticket:2213] + - A rework of "replacement traversal" within + the ORM as it alters selectables to be against + aliases of things (i.e. clause adaption) includes + a fix for multiply-nested any()/has() constructs + against a joined table structure. [ticket:2195] + - Fixed regression from 0.6 where Session.add() against an object which contained None in a collection would raise an internal exception. diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 494d94bb02..4de438e55e 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -13,7 +13,7 @@ mapped attributes. from sqlalchemy import sql, util, log, exc as sa_exc from sqlalchemy.sql.util import ClauseAdapter, criterion_as_pairs, \ - join_condition + join_condition, _shallow_annotate from sqlalchemy.sql import operators, expression from sqlalchemy.orm import attributes, dependency, mapper, \ object_mapper, strategies, configure_mappers @@ -167,9 +167,6 @@ class ColumnProperty(StrategizedProperty): log.class_logger(ColumnProperty) - - - class RelationshipProperty(StrategizedProperty): """Describes an object property that holds a single item or list of items that correspond to a related database table. @@ -448,7 +445,7 @@ class RelationshipProperty(StrategizedProperty): # should not correlate or otherwise reach out # to anything in the enclosing query. if criterion is not None: - criterion = criterion._annotate({'_halt_adapt': True}) + criterion = criterion._annotate({'no_replacement_traverse': True}) crit = j & criterion @@ -1485,6 +1482,14 @@ class RelationshipProperty(StrategizedProperty): else: aliased = True + # place a barrier on the destination such that + # replacement traversals won't ever dig into it. + # its internal structure remains fixed + # regardless of context. + dest_selectable = _shallow_annotate( + dest_selectable, + {'no_replacement_traverse':True}) + aliased = aliased or (source_selectable is not None) primaryjoin, secondaryjoin, secondary = self.primaryjoin, \ @@ -1508,13 +1513,10 @@ class RelationshipProperty(StrategizedProperty): if secondary is not None: secondary = secondary.alias() primary_aliasizer = ClauseAdapter(secondary) - if dest_selectable is not None: - secondary_aliasizer = \ - ClauseAdapter(dest_selectable, - equivalents=self.mapper._equivalent_columns).\ - chain(primary_aliasizer) - else: - secondary_aliasizer = primary_aliasizer + secondary_aliasizer = \ + ClauseAdapter(dest_selectable, + equivalents=self.mapper._equivalent_columns).\ + chain(primary_aliasizer) if source_selectable is not None: primary_aliasizer = \ ClauseAdapter(secondary).\ @@ -1523,20 +1525,14 @@ class RelationshipProperty(StrategizedProperty): secondaryjoin = \ secondary_aliasizer.traverse(secondaryjoin) else: - if dest_selectable is not None: - primary_aliasizer = ClauseAdapter(dest_selectable, - exclude=self.local_side, - equivalents=self.mapper._equivalent_columns) - if source_selectable is not None: - primary_aliasizer.chain( - ClauseAdapter(source_selectable, - exclude=self.remote_side, - equivalents=self.parent._equivalent_columns)) - elif source_selectable is not None: - primary_aliasizer = \ + primary_aliasizer = ClauseAdapter(dest_selectable, + exclude=self.local_side, + equivalents=self.mapper._equivalent_columns) + if source_selectable is not None: + primary_aliasizer.chain( ClauseAdapter(source_selectable, exclude=self.remote_side, - equivalents=self.parent._equivalent_columns) + equivalents=self.parent._equivalent_columns)) secondary_aliasizer = None primaryjoin = primary_aliasizer.traverse(primaryjoin) target_adapter = secondary_aliasizer or primary_aliasizer diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 8d64d69b41..a3b13abb27 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -251,9 +251,6 @@ class Query(object): return clause def replace(elem): - if '_halt_adapt' in elem._annotations: - return elem - for _orm_only, adapter in adapters: # if 'orm only', look for ORM annotations # in the element before adapting. @@ -267,7 +264,7 @@ class Query(object): return visitors.replacement_traverse( clause, - {'column_collections':False}, + {}, replace ) @@ -438,7 +435,9 @@ class Query(object): statement if self._params: stmt = stmt.params(self._params) - return stmt._annotate({'_halt_adapt': True}) + # TODO: there's no tests covering effects of + # the annotation not being there + return stmt._annotate({'no_replacement_traverse': True}) def subquery(self, name=None): """return the full SELECT statement represented by this :class:`.Query`, diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 071bb3c50d..fa0586e2d4 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -1216,7 +1216,7 @@ def _string_or_unprintable(element): except: return "unprintable element %r" % element -def _clone(element): +def _clone(element, **kw): return element._clone() def _expand_cloned(elements): @@ -1522,12 +1522,16 @@ class ClauseElement(Visitable): """ return self is other - def _copy_internals(self, clone=_clone): + def _copy_internals(self, clone=_clone, **kw): """Reassign internal elements to be clones of themselves. Called during a copy-and-traverse operation on newly shallow-copied elements to create a deep copy. + The given clone function should be used, which may be applying + additional transformations to the element (i.e. replacement + traversal, cloned traversal, annotations). + """ pass @@ -2755,8 +2759,8 @@ class _TextClause(Executable, ClauseElement): else: return self - def _copy_internals(self, clone=_clone): - self.bindparams = dict((b.key, clone(b)) + def _copy_internals(self, clone=_clone, **kw): + self.bindparams = dict((b.key, clone(b, **kw)) for b in self.bindparams.values()) def get_children(self, **kwargs): @@ -2846,8 +2850,8 @@ class ClauseList(ClauseElement): else: self.clauses.append(_literal_as_text(clause)) - def _copy_internals(self, clone=_clone): - self.clauses = [clone(clause) for clause in self.clauses] + def _copy_internals(self, clone=_clone, **kw): + self.clauses = [clone(clause, **kw) for clause in self.clauses] def get_children(self, **kwargs): return self.clauses @@ -2947,12 +2951,13 @@ class _Case(ColumnElement): else: self.else_ = None - def _copy_internals(self, clone=_clone): + def _copy_internals(self, clone=_clone, **kw): if self.value is not None: - self.value = clone(self.value) - self.whens = [(clone(x), clone(y)) for x, y in self.whens] + self.value = clone(self.value, **kw) + self.whens = [(clone(x, **kw), clone(y, **kw)) + for x, y in self.whens] if self.else_ is not None: - self.else_ = clone(self.else_) + self.else_ = clone(self.else_, **kw) def get_children(self, **kwargs): if self.value is not None: @@ -3028,8 +3033,8 @@ class FunctionElement(Executable, ColumnElement, FromClause): def get_children(self, **kwargs): return self.clause_expr, - def _copy_internals(self, clone=_clone): - self.clause_expr = clone(self.clause_expr) + def _copy_internals(self, clone=_clone, **kw): + self.clause_expr = clone(self.clause_expr, **kw) self._reset_exported() util.reset_memoized(self, 'clauses') @@ -3120,9 +3125,9 @@ class _Cast(ColumnElement): self.clause = _literal_as_binds(clause, None) self.typeclause = _TypeClause(self.type) - def _copy_internals(self, clone=_clone): - self.clause = clone(self.clause) - self.typeclause = clone(self.typeclause) + def _copy_internals(self, clone=_clone, **kw): + self.clause = clone(self.clause, **kw) + self.typeclause = clone(self.typeclause, **kw) def get_children(self, **kwargs): return self.clause, self.typeclause @@ -3141,8 +3146,8 @@ class _Extract(ColumnElement): self.field = field self.expr = _literal_as_binds(expr, None) - def _copy_internals(self, clone=_clone): - self.expr = clone(self.expr) + def _copy_internals(self, clone=_clone, **kw): + self.expr = clone(self.expr, **kw) def get_children(self, **kwargs): return self.expr, @@ -3170,8 +3175,8 @@ class _UnaryExpression(ColumnElement): def _from_objects(self): return self.element._from_objects - def _copy_internals(self, clone=_clone): - self.element = clone(self.element) + def _copy_internals(self, clone=_clone, **kw): + self.element = clone(self.element, **kw) def get_children(self, **kwargs): return self.element, @@ -3233,9 +3238,9 @@ class _BinaryExpression(ColumnElement): def _from_objects(self): return self.left._from_objects + self.right._from_objects - def _copy_internals(self, clone=_clone): - self.left = clone(self.left) - self.right = clone(self.right) + def _copy_internals(self, clone=_clone, **kw): + self.left = clone(self.left, **kw) + self.right = clone(self.right, **kw) def get_children(self, **kwargs): return self.left, self.right @@ -3373,11 +3378,11 @@ class Join(FromClause): self.foreign_keys.update(itertools.chain( *[col.foreign_keys for col in columns])) - def _copy_internals(self, clone=_clone): + def _copy_internals(self, clone=_clone, **kw): self._reset_exported() - self.left = clone(self.left) - self.right = clone(self.right) - self.onclause = clone(self.onclause) + self.left = clone(self.left, **kw) + self.right = clone(self.right, **kw) + self.onclause = clone(self.onclause, **kw) self.__folded_equivalents = None def get_children(self, **kwargs): @@ -3525,21 +3530,24 @@ class Alias(FromClause): for col in self.element.columns: col._make_proxy(self) - def _copy_internals(self, clone=_clone): + def _copy_internals(self, clone=_clone, **kw): + # don't apply anything to an aliased Table + # for now. May want to drive this from + # the given **kw. + if isinstance(self.element, TableClause): + return self._reset_exported() - self.element = _clone(self.element) + self.element = clone(self.element, **kw) baseselectable = self.element while isinstance(baseselectable, Alias): baseselectable = baseselectable.element self.original = baseselectable - def get_children(self, column_collections=True, - aliased_selectables=True, **kwargs): + def get_children(self, column_collections=True, **kw): if column_collections: for c in self.c: yield c - if aliased_selectables: - yield self.element + yield self.element @property def _from_objects(self): @@ -3563,8 +3571,8 @@ class _Grouping(ColumnElement): def _label(self): return getattr(self.element, '_label', None) or self.anon_label - def _copy_internals(self, clone=_clone): - self.element = clone(self.element) + def _copy_internals(self, clone=_clone, **kw): + self.element = clone(self.element, **kw) def get_children(self, **kwargs): return self.element, @@ -3615,8 +3623,8 @@ class _FromGrouping(FromClause): def get_children(self, **kwargs): return self.element, - def _copy_internals(self, clone=_clone): - self.element = clone(self.element) + def _copy_internals(self, clone=_clone, **kw): + self.element = clone(self.element, **kw) @property def _from_objects(self): @@ -3662,12 +3670,12 @@ class _Over(ColumnElement): (self.func, self.partition_by, self.order_by) if c is not None] - def _copy_internals(self, clone=_clone): - self.func = clone(self.func) + def _copy_internals(self, clone=_clone, **kw): + self.func = clone(self.func, **kw) if self.partition_by is not None: - self.partition_by = clone(self.partition_by) + self.partition_by = clone(self.partition_by, **kw) if self.order_by is not None: - self.order_by = clone(self.order_by) + self.order_by = clone(self.order_by, **kw) @property def _from_objects(self): @@ -3732,8 +3740,8 @@ class _Label(ColumnElement): def get_children(self, **kwargs): return self.element, - def _copy_internals(self, clone=_clone): - self.element = clone(self.element) + def _copy_internals(self, clone=_clone, **kw): + self.element = clone(self.element, **kw) @property def _from_objects(self): @@ -4244,14 +4252,14 @@ class CompoundSelect(_SelectBase): proxy.proxies = [c._annotate({'weight': i + 1}) for (i, c) in enumerate(cols)] - def _copy_internals(self, clone=_clone): + def _copy_internals(self, clone=_clone, **kw): self._reset_exported() - self.selects = [clone(s) for s in self.selects] + self.selects = [clone(s, **kw) for s in self.selects] if hasattr(self, '_col_map'): del self._col_map for attr in ('_order_by_clause', '_group_by_clause'): if getattr(self, attr) is not None: - setattr(self, attr, clone(getattr(self, attr))) + setattr(self, attr, clone(getattr(self, attr), **kw)) def get_children(self, column_collections=True, **kwargs): return (column_collections and list(self.c) or []) \ @@ -4477,17 +4485,17 @@ class Select(_SelectBase): return True return False - def _copy_internals(self, clone=_clone): + def _copy_internals(self, clone=_clone, **kw): self._reset_exported() - from_cloned = dict((f, clone(f)) + from_cloned = dict((f, clone(f, **kw)) for f in self._froms.union(self._correlate)) self._froms = util.OrderedSet(from_cloned[f] for f in self._froms) self._correlate = set(from_cloned[f] for f in self._correlate) - self._raw_columns = [clone(c) for c in self._raw_columns] + self._raw_columns = [clone(c, **kw) for c in self._raw_columns] for attr in '_whereclause', '_having', '_order_by_clause', \ '_group_by_clause': if getattr(self, attr) is not None: - setattr(self, attr, clone(getattr(self, attr))) + setattr(self, attr, clone(getattr(self, attr), **kw)) def get_children(self, column_collections=True, **kwargs): """return child elements as per the ClauseElement specification.""" @@ -4910,7 +4918,7 @@ class Insert(ValuesBase): else: return () - def _copy_internals(self, clone=_clone): + def _copy_internals(self, clone=_clone, **kw): # TODO: coverage self.parameters = self.parameters.copy() @@ -4959,9 +4967,9 @@ class Update(ValuesBase): else: return () - def _copy_internals(self, clone=_clone): + def _copy_internals(self, clone=_clone, **kw): # TODO: coverage - self._whereclause = clone(self._whereclause) + self._whereclause = clone(self._whereclause, **kw) self.parameters = self.parameters.copy() @_generative @@ -5020,9 +5028,9 @@ class Delete(UpdateBase): else: self._whereclause = _literal_as_text(whereclause) - def _copy_internals(self, clone=_clone): + def _copy_internals(self, clone=_clone, **kw): # TODO: coverage - self._whereclause = clone(self._whereclause) + self._whereclause = clone(self._whereclause, **kw) class _IdentifiedClause(Executable, ClauseElement): diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index 77c3e45ec4..ed0afef243 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -417,6 +417,17 @@ def _deep_deannotate(element): element = clone(element) return element +def _shallow_annotate(element, annotations): + """Annotate the given ClauseElement and copy its internals so that + internal objects refer to the new annotated object. + + Basically used to apply a "dont traverse" annotation to a + selectable, without digging throughout the whole + structure wasting time. + """ + element = element._annotate(annotations) + element._copy_internals() + return element def splice_joins(left, right, stop_on=None): if left is None: @@ -639,7 +650,7 @@ class ClauseAdapter(visitors.ReplacingCloningVisitor): """ def __init__(self, selectable, equivalents=None, include=None, exclude=None): - self.__traverse_options__ = {'column_collections':False, 'stop_on':[selectable]} + self.__traverse_options__ = {'stop_on':[selectable]} self.selectable = selectable self.include = include self.exclude = exclude diff --git a/lib/sqlalchemy/sql/visitors.py b/lib/sqlalchemy/sql/visitors.py index 0c6be97d78..b94f07f58d 100644 --- a/lib/sqlalchemy/sql/visitors.py +++ b/lib/sqlalchemy/sql/visitors.py @@ -19,7 +19,7 @@ use a non-visitor traversal system. For many examples of how the visit system is used, see the sqlalchemy.sql.util and the sqlalchemy.sql.compiler modules. For an introduction to clause adaption, see -http://techspot.zzzeek.org/?p=19 . +http://techspot.zzzeek.org/2008/01/23/expression-transformations/ """ @@ -212,55 +212,51 @@ def traverse_depthfirst(obj, opts, visitors): return traverse_using(iterate_depthfirst(obj, opts), obj, visitors) def cloned_traverse(obj, opts, visitors): - """clone the given expression structure, allowing modifications by visitors.""" + """clone the given expression structure, allowing + modifications by visitors.""" cloned = util.column_dict() + stop_on = util.column_set(opts.get('stop_on', [])) - def clone(element): - if element not in cloned: - cloned[element] = element._clone() - return cloned[element] - - obj = clone(obj) - stack = [obj] - - while stack: - t = stack.pop() - if t in cloned: - continue - t._copy_internals(clone=clone) - - meth = visitors.get(t.__visit_name__, None) - if meth: - meth(t) - - for c in t.get_children(**opts): - stack.append(c) + def clone(elem): + if elem in stop_on: + return elem + else: + if elem not in cloned: + cloned[elem] = newelem = elem._clone() + newelem._copy_internals(clone=clone) + meth = visitors.get(newelem.__visit_name__, None) + if meth: + meth(newelem) + return cloned[elem] + + if obj is not None: + obj = clone(obj) return obj + def replacement_traverse(obj, opts, replace): - """clone the given expression structure, allowing element replacement by a given replacement function.""" + """clone the given expression structure, allowing element + replacement by a given replacement function.""" cloned = util.column_dict() stop_on = util.column_set(opts.get('stop_on', [])) - def clone(element): - newelem = replace(element) - if newelem is not None: - stop_on.add(newelem) - return newelem - - if element not in cloned: - cloned[element] = element._clone() - return cloned[element] - - obj = clone(obj) - stack = [obj] - while stack: - t = stack.pop() - if t in stop_on: - continue - t._copy_internals(clone=clone) - for c in t.get_children(**opts): - stack.append(c) + def clone(elem, **kw): + if elem in stop_on or \ + 'no_replacement_traverse' in elem._annotations: + return elem + else: + newelem = replace(elem) + if newelem is not None: + stop_on.add(newelem) + return newelem + else: + if elem not in cloned: + cloned[elem] = newelem = elem._clone() + newelem._copy_internals(clone=clone, **kw) + return cloned[elem] + + if obj is not None: + obj = clone(obj, **opts) return obj diff --git a/test/orm/test_joins.py b/test/orm/test_joins.py index a3f08a025c..6c90dbf739 100644 --- a/test/orm/test_joins.py +++ b/test/orm/test_joins.py @@ -1438,6 +1438,48 @@ class SelfRefMixedTest(fixtures.MappedTest, AssertsCompiledSQL): "assoc_table_1.right_id JOIN sub_table ON nodes.id = sub_table.node_id", ) +class CreateJoinsTest(fixtures.ORMTest, AssertsCompiledSQL): + + def _inherits_fixture(self): + m = MetaData() + base = Table('base', m, Column('id', Integer, primary_key=True)) + a = Table('a', m, + Column('id', Integer, ForeignKey('base.id'), primary_key=True), + Column('b_id', Integer, ForeignKey('b.id'))) + b = Table('b', m, + Column('id', Integer, ForeignKey('base.id'), primary_key=True), + Column('c_id', Integer, ForeignKey('c.id'))) + c = Table('c', m, + Column('id', Integer, ForeignKey('base.id'), primary_key=True)) + class Base(object): + pass + class A(Base): + pass + class B(Base): + pass + class C(Base): + pass + mapper(Base, base) + mapper(A, a, inherits=Base, properties={'b':relationship(B, primaryjoin=a.c.b_id==b.c.id)}) + mapper(B, b, inherits=Base, properties={'c':relationship(C, primaryjoin=b.c.c_id==c.c.id)}) + mapper(C, c, inherits=Base) + return A, B, C, Base + + def test_double_level_aliased_exists(self): + A, B, C, Base = self._inherits_fixture() + s = Session() + self.assert_compile( + s.query(A).filter(A.b.has(B.c.has(C.id==5))), + "SELECT a.id AS a_id, base.id AS base_id, a.b_id AS a_b_id " + "FROM base JOIN a ON base.id = a.id WHERE " + "EXISTS (SELECT 1 FROM (SELECT base.id AS base_id, b.id AS " + "b_id, b.c_id AS b_c_id FROM base JOIN b ON base.id = b.id) " + "AS anon_1 WHERE a.b_id = anon_1.b_id AND (EXISTS " + "(SELECT 1 FROM (SELECT base.id AS base_id, c.id AS c_id " + "FROM base JOIN c ON base.id = c.id) AS anon_2 " + "WHERE anon_1.b_c_id = anon_2.c_id AND anon_2.c_id = ?" + ")))" + ) class SelfReferentialTest(fixtures.MappedTest, AssertsCompiledSQL): run_setup_mappers = 'once' diff --git a/test/sql/test_generative.py b/test/sql/test_generative.py index 47e45bbb94..f9333dbf53 100644 --- a/test/sql/test_generative.py +++ b/test/sql/test_generative.py @@ -779,7 +779,7 @@ class ClauseAdapterTest(fixtures.TestBase, AssertsCompiledSQL): s2 = s2._clone() assert s2.is_derived_from(s1) - def test_aliasedselect_to_aliasedselect(self): + def test_aliasedselect_to_aliasedselect_straight(self): # original issue from ticket #904 @@ -791,6 +791,10 @@ class ClauseAdapterTest(fixtures.TestBase, AssertsCompiledSQL): 'AS col2, table1.col3 AS col3 FROM table1) ' 'AS foo LIMIT :param_1 OFFSET :param_2', {'param_1': 5, 'param_2': 10}) + + def test_aliasedselect_to_aliasedselect_join(self): + s1 = select([t1]).alias('foo') + s2 = select([s1]).limit(5).offset(10).alias() j = s1.outerjoin(t2, s1.c.col1 == t2.c.col1) self.assert_compile(sql_util.ClauseAdapter(s2).traverse(j).select(), 'SELECT anon_1.col1, anon_1.col2, ' @@ -803,8 +807,15 @@ class ClauseAdapterTest(fixtures.TestBase, AssertsCompiledSQL): ':param_2) AS anon_1 LEFT OUTER JOIN ' 'table2 ON anon_1.col1 = table2.col1', {'param_1': 5, 'param_2': 10}) + + def test_aliasedselect_to_aliasedselect_join_nested_table(self): + s1 = select([t1]).alias('foo') + s2 = select([s1]).limit(5).offset(10).alias() talias = t1.alias('bar') + + assert not s2.is_derived_from(talias) j = s1.outerjoin(talias, s1.c.col1 == talias.c.col1) + self.assert_compile(sql_util.ClauseAdapter(s2).traverse(j).select(), 'SELECT anon_1.col1, anon_1.col2, ' 'anon_1.col3, bar.col1, bar.col2, bar.col3 ' diff --git a/test/sql/test_selectable.py b/test/sql/test_selectable.py index 63be50a974..555271f16f 100644 --- a/test/sql/test_selectable.py +++ b/test/sql/test_selectable.py @@ -914,6 +914,13 @@ class AnnotationsTest(fixtures.TestBase): b5 = visitors.cloned_traverse(b3, {}, {'binary':visit_binary}) assert str(b5) == ":bar = table1.col2" + def test_annotate_aliased(self): + t1 = table('t1', column('c1')) + s = select([(t1.c.c1 + 3).label('bat')]) + a = s.alias() + a = sql_util._deep_annotate(a, {'foo': 'bar'}) + eq_(a._annotations['foo'], 'bar') + eq_(a.element._annotations['foo'], 'bar') def test_annotate_expressions(self): table1 = table('table1', column('col1'), column('col2'))