]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- rewrite cloned_traverse() and replacement_traverse() to use a straight
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 24 Jul 2011 21:51:01 +0000 (17:51 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 24 Jul 2011 21:51:01 +0000 (17:51 -0400)
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"

CHANGES
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/sql/expression.py
lib/sqlalchemy/sql/util.py
lib/sqlalchemy/sql/visitors.py
test/orm/test_joins.py
test/sql/test_generative.py
test/sql/test_selectable.py

diff --git a/CHANGES b/CHANGES
index 6744bfb0b24f268c7dd599b0cc82ca4895f4fd6b..7fa9ffd779b581e7b371ea2c195e970e3404a7f7 100644 (file)
--- 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.
index 494d94bb02bf4bcad20e5c146cc9d5e4a9ecaf1f..4de438e55e4d2ef5e5a5effb13c366926dff36c5 100644 (file)
@@ -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
index 8d64d69b4111c63de9db233f522af3926f623140..a3b13abb27729e5d81ab2378964f29f1326798da 100644 (file)
@@ -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`, 
index 071bb3c50db2b4907c4ccdc82a239d826dfb3d9e..fa0586e2d4ac6c76c11e9e7e1adbdee6a51bf409 100644 (file)
@@ -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):
 
index 77c3e45ec449da7f5bc23597d82e1bdae4e43257..ed0afef2431a3a47f9583df2006573073b35e0ca 100644 (file)
@@ -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
index 0c6be97d78a72d06824ac039cc5812795c8f963d..b94f07f58da6f7d2e306208a0bb5ccfe6ed563ee 100644 (file)
@@ -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
index a3f08a025c936a8b4fb4ee44c6ab4e2dfbd5ae05..6c90dbf739c2247a7c293f924ea1ba65d7d5208f 100644 (file)
@@ -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'
index 47e45bbb942295f07c4ac6566693547f3d3dd2ef..f9333dbf5329b0092474c22716944721636b4f65 100644 (file)
@@ -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 '
index 63be50a974e4fb5bc816b19ce6106902fa6a6c63..555271f16f799ec4474453404da6ab8d74795194 100644 (file)
@@ -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'))