summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2011-07-24 17:51:01 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2011-07-24 17:51:01 -0400
commit8a483dbf38168ff43ca0652229b1d46afb23235d (patch)
tree0707d1e1f3912bda8262e32bd1e4e008a7c67b88
parent9867086943d60e347695930dd7f442f9e95e4577 (diff)
downloadsqlalchemy-8a483dbf38168ff43ca0652229b1d46afb23235d.tar.gz
- 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"
-rw-r--r--CHANGES6
-rw-r--r--lib/sqlalchemy/orm/properties.py44
-rw-r--r--lib/sqlalchemy/orm/query.py9
-rw-r--r--lib/sqlalchemy/sql/expression.py116
-rw-r--r--lib/sqlalchemy/sql/util.py13
-rw-r--r--lib/sqlalchemy/sql/visitors.py80
-rw-r--r--test/orm/test_joins.py42
-rw-r--r--test/sql/test_generative.py13
-rw-r--r--test/sql/test_selectable.py7
9 files changed, 203 insertions, 127 deletions
diff --git a/CHANGES b/CHANGES
index 6744bfb0b..7fa9ffd77 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 494d94bb0..4de438e55 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 8d64d69b4..a3b13abb2 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 071bb3c50..fa0586e2d 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 77c3e45ec..ed0afef24 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 0c6be97d7..b94f07f58 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 a3f08a025..6c90dbf73 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 47e45bbb9..f9333dbf5 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 63be50a97..555271f16 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'))