_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.
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
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.
# 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
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, \
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).\
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
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.
return visitors.replacement_traverse(
clause,
- {'column_collections':False},
+ {},
replace
)
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`,
except:
return "unprintable element %r" % element
-def _clone(element):
+def _clone(element, **kw):
return element._clone()
def _expand_cloned(elements):
"""
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
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):
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
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:
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')
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
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,
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,
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
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):
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):
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,
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):
(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):
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):
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 []) \
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."""
else:
return ()
- def _copy_internals(self, clone=_clone):
+ def _copy_internals(self, clone=_clone, **kw):
# TODO: coverage
self.parameters = self.parameters.copy()
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
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):
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:
"""
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
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/
"""
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
"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'
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
'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, '
':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 '
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'))