summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/query.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2014-01-21 20:10:23 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2014-01-21 20:10:23 -0500
commit07fb90c6cc14de6d02cf4be592c57d56831f59f7 (patch)
tree050ef65db988559c60f7aa40f2d0bfe24947e548 /lib/sqlalchemy/orm/query.py
parent560fd1d5ed643a1b0f95296f3b840c1963bbe67f (diff)
parentee1f4d21037690ad996c5eacf7e1200e92f2fbaa (diff)
downloadsqlalchemy-ticket_2501.tar.gz
Merge branch 'master' into ticket_2501ticket_2501
Conflicts: lib/sqlalchemy/orm/mapper.py
Diffstat (limited to 'lib/sqlalchemy/orm/query.py')
-rw-r--r--lib/sqlalchemy/orm/query.py562
1 files changed, 436 insertions, 126 deletions
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index f6fd07e61..6bd465e9c 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -1,5 +1,5 @@
# orm/query.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -24,37 +24,28 @@ from . import (
attributes, interfaces, object_mapper, persistence,
exc as orm_exc, loading
)
+from .base import _entity_descriptor, _is_aliased_class, \
+ _is_mapped_class, _orm_columns, _generative
+from .path_registry import PathRegistry
from .util import (
- AliasedClass, ORMAdapter, _entity_descriptor, PathRegistry,
- _is_aliased_class, _is_mapped_class, _orm_columns,
- join as orm_join, with_parent, aliased
+ AliasedClass, ORMAdapter, join as orm_join, with_parent, aliased
)
-from .. import sql, util, log, exc as sa_exc, inspect, inspection, \
- types as sqltypes
+from .. import sql, util, log, exc as sa_exc, inspect, inspection
from ..sql.expression import _interpret_as_from
from ..sql import (
util as sql_util,
expression, visitors
)
+from ..sql.base import ColumnCollection
+from . import properties
__all__ = ['Query', 'QueryContext', 'aliased']
-def _generative(*assertions):
- """Mark a method as generative."""
-
- @util.decorator
- def generate(fn, *args, **kw):
- self = args[0]._clone()
- for assertion in assertions:
- assertion(self, fn.__name__)
- fn(self, *args[1:], **kw)
- return self
- return generate
-
_path_registry = PathRegistry.root
-
+@inspection._self_inspects
+@log.class_logger
class Query(object):
"""ORM-level SQL construction object.
@@ -77,7 +68,6 @@ class Query(object):
_with_labels = False
_criterion = None
_yield_per = None
- _lockmode = None
_order_by = False
_group_by = False
_having = None
@@ -85,6 +75,7 @@ class Query(object):
_prefixes = None
_offset = None
_limit = None
+ _for_update_arg = None
_statement = None
_correlate = frozenset()
_populate_existing = False
@@ -118,6 +109,7 @@ class Query(object):
if entity_wrapper is None:
entity_wrapper = _QueryEntity
self._entities = []
+ self._primary_entity = None
for ent in util.to_list(entities):
entity_wrapper(self, ent)
@@ -299,11 +291,8 @@ class Query(object):
@property
def _mapper_entities(self):
- # TODO: this is wrong, its hardcoded to "primary entity" when
- # for the case of __all_equivs() it should not be
- # the name of this accessor is wrong too
for ent in self._entities:
- if hasattr(ent, 'primary_entity'):
+ if isinstance(ent, _MapperEntity):
yield ent
def _joinpoint_zero(self):
@@ -313,9 +302,10 @@ class Query(object):
)
def _mapper_zero_or_none(self):
- if not getattr(self._entities[0], 'primary_entity', False):
+ if self._primary_entity:
+ return self._primary_entity.mapper
+ else:
return None
- return self._entities[0].mapper
def _only_mapper_zero(self, rationale=None):
if len(self._entities) > 1:
@@ -327,16 +317,11 @@ class Query(object):
return self._mapper_zero()
def _only_full_mapper_zero(self, methname):
- if len(self._entities) != 1:
+ if self._entities != [self._primary_entity]:
raise sa_exc.InvalidRequestError(
"%s() can only be used against "
"a single mapped class." % methname)
- entity = self._entity_zero()
- if not hasattr(entity, 'primary_entity'):
- raise sa_exc.InvalidRequestError(
- "%s() can only be used against "
- "a single mapped class." % methname)
- return entity.entity_zero
+ return self._primary_entity.entity_zero
def _only_entity_zero(self, rationale=None):
if len(self._entities) > 1:
@@ -555,7 +540,7 @@ class Query(object):
:class:`.Query`, converted
to a scalar subquery with a label of the given name.
- Analogous to :meth:`sqlalchemy.sql.SelectBaseMixin.label`.
+ Analogous to :meth:`sqlalchemy.sql.expression.SelectBase.label`.
.. versionadded:: 0.6.5
@@ -567,7 +552,7 @@ class Query(object):
"""Return the full SELECT statement represented by this
:class:`.Query`, converted to a scalar subquery.
- Analogous to :meth:`sqlalchemy.sql.SelectBaseMixin.as_scalar`.
+ Analogous to :meth:`sqlalchemy.sql.expression.SelectBase.as_scalar`.
.. versionadded:: 0.6.5
@@ -698,7 +683,7 @@ class Query(object):
"""
- if not getattr(self._entities[0], 'primary_entity', False):
+ if not self._primary_entity:
raise sa_exc.InvalidRequestError(
"No primary mapper set up for this Query.")
entity = self._entities[0]._clone()
@@ -811,7 +796,7 @@ class Query(object):
if not self._populate_existing and \
not mapper.always_refresh and \
- self._lockmode is None:
+ self._for_update_arg is None:
instance = loading.get_from_identity(
self.session, key, attributes.PASSIVE_OFF)
@@ -903,11 +888,10 @@ class Query(object):
"""
if property is None:
- from sqlalchemy.orm import properties
mapper = object_mapper(instance)
for prop in mapper.iterate_properties:
- if isinstance(prop, properties.PropertyLoader) and \
+ if isinstance(prop, properties.RelationshipProperty) and \
prop.mapper is self._mapper_zero():
property = prop
break
@@ -936,7 +920,7 @@ class Query(object):
@_generative()
def with_session(self, session):
- """Return a :class:`Query` that will use the given :class:`.Session`.
+ """Return a :class:`.Query` that will use the given :class:`.Session`.
"""
@@ -1140,32 +1124,63 @@ class Query(object):
@_generative()
def with_lockmode(self, mode):
- """Return a new Query object with the specified locking mode.
+ """Return a new :class:`.Query` object with the specified "locking mode",
+ which essentially refers to the ``FOR UPDATE`` clause.
- :param mode: a string representing the desired locking mode. A
- corresponding value is passed to the ``for_update`` parameter of
- :meth:`~sqlalchemy.sql.expression.select` when the query is
- executed. Valid values are:
+ .. deprecated:: 0.9.0 superseded by :meth:`.Query.with_for_update`.
- ``'update'`` - passes ``for_update=True``, which translates to
- ``FOR UPDATE`` (standard SQL, supported by most dialects)
+ :param mode: a string representing the desired locking mode.
+ Valid values are:
- ``'update_nowait'`` - passes ``for_update='nowait'``, which
- translates to ``FOR UPDATE NOWAIT`` (supported by Oracle,
- PostgreSQL 8.1 upwards)
+ * ``None`` - translates to no lockmode
- ``'read'`` - passes ``for_update='read'``, which translates to
- ``LOCK IN SHARE MODE`` (for MySQL), and ``FOR SHARE`` (for
- PostgreSQL)
+ * ``'update'`` - translates to ``FOR UPDATE``
+ (standard SQL, supported by most dialects)
- ``'read_nowait'`` - passes ``for_update='read_nowait'``, which
- translates to ``FOR SHARE NOWAIT`` (supported by PostgreSQL).
+ * ``'update_nowait'`` - translates to ``FOR UPDATE NOWAIT``
+ (supported by Oracle, PostgreSQL 8.1 upwards)
+
+ * ``'read'`` - translates to ``LOCK IN SHARE MODE`` (for MySQL),
+ and ``FOR SHARE`` (for PostgreSQL)
+
+ .. seealso::
+
+ :meth:`.Query.with_for_update` - improved API for
+ specifying the ``FOR UPDATE`` clause.
- .. versionadded:: 0.7.7
- ``FOR SHARE`` and ``FOR SHARE NOWAIT`` (PostgreSQL).
"""
+ self._for_update_arg = LockmodeArg.parse_legacy_query(mode)
+
+ @_generative()
+ def with_for_update(self, read=False, nowait=False, of=None):
+ """return a new :class:`.Query` with the specified options for the
+ ``FOR UPDATE`` clause.
+
+ The behavior of this method is identical to that of
+ :meth:`.SelectBase.with_for_update`. When called with no arguments,
+ the resulting ``SELECT`` statement will have a ``FOR UPDATE`` clause
+ appended. When additional arguments are specified, backend-specific
+ options such as ``FOR UPDATE NOWAIT`` or ``LOCK IN SHARE MODE``
+ can take effect.
+
+ E.g.::
+
+ q = sess.query(User).with_for_update(nowait=True, of=User)
+
+ The above query on a Postgresql backend will render like::
- self._lockmode = mode
+ SELECT users.id AS users_id FROM users FOR UPDATE OF users NOWAIT
+
+ .. versionadded:: 0.9.0 :meth:`.Query.with_for_update` supersedes
+ the :meth:`.Query.with_lockmode` method.
+
+ .. seealso::
+
+ :meth:`.GenerativeSelect.with_for_update` - Core level method with
+ full argument and behavioral description.
+
+ """
+ self._for_update_arg = LockmodeArg(read=read, nowait=nowait, of=of)
@_generative()
def params(self, *args, **kwargs):
@@ -1300,7 +1315,7 @@ class Query(object):
"""apply a HAVING criterion to the query and return the
newly resulting :class:`.Query`.
- :meth:`having` is used in conjunction with :meth:`group_by`.
+ :meth:`~.Query.having` is used in conjunction with :meth:`~.Query.group_by`.
HAVING criterion makes it possible to use filters on aggregate
functions like COUNT, SUM, AVG, MAX, and MIN, eg.::
@@ -1478,7 +1493,7 @@ class Query(object):
q = session.query(User).join(Address)
- The above calling form of :meth:`.join` will raise an error if
+ The above calling form of :meth:`~.Query.join` will raise an error if
either there are no foreign keys between the two entities, or if
there are multiple foreign key linkages between them. In the
above calling form, :meth:`~.Query.join` is called upon to
@@ -1640,14 +1655,14 @@ class Query(object):
example :ref:`examples_xmlpersistence` which illustrates
an XPath-like query system using algorithmic joins.
- :param *props: A collection of one or more join conditions,
+ :param \*props: A collection of one or more join conditions,
each consisting of a relationship-bound attribute or string
relationship name representing an "on clause", or a single
target entity, or a tuple in the form of ``(target, onclause)``.
A special two-argument calling form of the form ``target, onclause``
is also accepted.
:param aliased=False: If True, indicate that the JOIN target should be
- anonymously aliased. Subsequent calls to :class:`~.Query.filter`
+ anonymously aliased. Subsequent calls to :meth:`~.Query.filter`
and similar will adapt the incoming criterion to the target
alias, until :meth:`~.Query.reset_joinpoint` is called.
:param from_joinpoint=False: When using ``aliased=True``, a setting
@@ -1827,14 +1842,30 @@ class Query(object):
raise sa_exc.InvalidRequestError(
"Can't construct a join from %s to %s, they "
"are the same entity" %
- (left, right))
+ (left, right))
l_info = inspect(left)
r_info = inspect(right)
- overlap = not create_aliases and \
- sql_util.selectables_overlap(l_info.selectable,
- r_info.selectable)
+
+ overlap = False
+ if not create_aliases:
+ right_mapper = getattr(r_info, "mapper", None)
+ # if the target is a joined inheritance mapping,
+ # be more liberal about auto-aliasing.
+ if right_mapper and (
+ right_mapper.with_polymorphic or
+ isinstance(right_mapper.mapped_table, expression.Join)
+ ):
+ for from_obj in self._from_obj or [l_info.selectable]:
+ if sql_util.selectables_overlap(l_info.selectable, from_obj) and \
+ sql_util.selectables_overlap(from_obj, r_info.selectable):
+ overlap = True
+ break
+ elif sql_util.selectables_overlap(l_info.selectable, r_info.selectable):
+ overlap = True
+
+
if overlap and l_info.selectable is r_info.selectable:
raise sa_exc.InvalidRequestError(
"Can't join table/selectable '%s' to itself" %
@@ -2219,7 +2250,7 @@ class Query(object):
``Query``.
:param \*prefixes: optional prefixes, typically strings,
- not using any commas. In particular is useful for MySQL keywords.
+ not using any commas. In particular is useful for MySQL keywords.
e.g.::
@@ -2414,10 +2445,10 @@ class Query(object):
"""
return [
{
- 'name':ent._label_name,
- 'type':ent.type,
- 'aliased':getattr(ent, 'is_aliased_class', False),
- 'expr':ent.expr
+ 'name': ent._label_name,
+ 'type': ent.type,
+ 'aliased': getattr(ent, 'is_aliased_class', False),
+ 'expr': ent.expr
}
for ent in self._entities
]
@@ -2500,7 +2531,7 @@ class Query(object):
.. versionadded:: 0.8.1
"""
- return sql.exists(self.with_entities('1').statement)
+ return sql.exists(self.with_labels().statement.with_only_columns(['1']))
def count(self):
"""Return a count of rows this Query would return.
@@ -2571,19 +2602,37 @@ class Query(object):
The expression evaluator currently doesn't account for differing
string collations between the database and Python.
- Returns the number of rows deleted, excluding any cascades.
+ :return: the count of rows matched as returned by the database's
+ "row count" feature.
- The method does *not* offer in-Python cascading of relationships - it
- is assumed that ON DELETE CASCADE is configured for any foreign key
- references which require it. The Session needs to be expired (occurs
- automatically after commit(), or call expire_all()) in order for the
- state of dependent objects subject to delete or delete-orphan cascade
- to be correctly represented.
+ This method has several key caveats:
- Note that the :meth:`.MapperEvents.before_delete` and
- :meth:`.MapperEvents.after_delete`
- events are **not** invoked from this method. It instead
- invokes :meth:`.SessionEvents.after_bulk_delete`.
+ * The method does **not** offer in-Python cascading of relationships - it
+ is assumed that ON DELETE CASCADE/SET NULL/etc. is configured for any foreign key
+ references which require it, otherwise the database may emit an
+ integrity violation if foreign key references are being enforced.
+
+ After the DELETE, dependent objects in the :class:`.Session` which
+ were impacted by an ON DELETE may not contain the current
+ state, or may have been deleted. This issue is resolved once the
+ :class:`.Session` is expired,
+ which normally occurs upon :meth:`.Session.commit` or can be forced
+ by using :meth:`.Session.expire_all`. Accessing an expired object
+ whose row has been deleted will invoke a SELECT to locate the
+ row; when the row is not found, an :class:`~sqlalchemy.orm.exc.ObjectDeletedError`
+ is raised.
+
+ * The :meth:`.MapperEvents.before_delete` and
+ :meth:`.MapperEvents.after_delete`
+ events are **not** invoked from this method. Instead, the
+ :meth:`.SessionEvents.after_bulk_delete` method is provided to act
+ upon a mass DELETE of entity rows.
+
+ .. seealso::
+
+ :meth:`.Query.update`
+
+ :ref:`inserts_and_updates` - Core SQL tutorial
"""
#TODO: cascades need handling.
@@ -2622,20 +2671,50 @@ class Query(object):
The expression evaluator currently doesn't account for differing
string collations between the database and Python.
- Returns the number of rows matched by the update.
+ :return: the count of rows matched as returned by the database's
+ "row count" feature.
+
+ This method has several key caveats:
+
+ * The method does **not** offer in-Python cascading of relationships - it
+ is assumed that ON UPDATE CASCADE is configured for any foreign key
+ references which require it, otherwise the database may emit an
+ integrity violation if foreign key references are being enforced.
+
+ After the UPDATE, dependent objects in the :class:`.Session` which
+ were impacted by an ON UPDATE CASCADE may not contain the current
+ state; this issue is resolved once the :class:`.Session` is expired,
+ which normally occurs upon :meth:`.Session.commit` or can be forced
+ by using :meth:`.Session.expire_all`.
+
+ * As of 0.8, this method will support multiple table updates, as detailed
+ in :ref:`multi_table_updates`, and this behavior does extend to support
+ updates of joined-inheritance and other multiple table mappings. However,
+ the **join condition of an inheritance mapper is currently not
+ automatically rendered**.
+ Care must be taken in any multiple-table update to explicitly include
+ the joining condition between those tables, even in mappings where
+ this is normally automatic.
+ E.g. if a class ``Engineer`` subclasses ``Employee``, an UPDATE of the
+ ``Engineer`` local table using criteria against the ``Employee``
+ local table might look like::
+
+ session.query(Engineer).\\
+ filter(Engineer.id == Employee.id).\\
+ filter(Employee.name == 'dilbert').\\
+ update({"engineer_type": "programmer"})
+
+ * The :meth:`.MapperEvents.before_update` and
+ :meth:`.MapperEvents.after_update`
+ events are **not** invoked from this method. Instead, the
+ :meth:`.SessionEvents.after_bulk_update` method is provided to act
+ upon a mass UPDATE of entity rows.
- The method does *not* offer in-Python cascading of relationships - it
- is assumed that ON UPDATE CASCADE is configured for any foreign key
- references which require it.
+ .. seealso::
- The Session needs to be expired (occurs automatically after commit(),
- or call expire_all()) in order for the state of dependent objects
- subject foreign key cascade to be correctly represented.
+ :meth:`.Query.delete`
- Note that the :meth:`.MapperEvents.before_update` and
- :meth:`.MapperEvents.after_update`
- events are **not** invoked from this method. It instead
- invokes :meth:`.SessionEvents.after_bulk_update`.
+ :ref:`inserts_and_updates` - Core SQL tutorial
"""
@@ -2650,13 +2729,6 @@ class Query(object):
update_op.exec_()
return update_op.rowcount
- _lockmode_lookup = {
- 'read': 'read',
- 'read_nowait': 'read_nowait',
- 'update': True,
- 'update_nowait': 'nowait',
- None: False
- }
def _compile_context(self, labels=True):
context = QueryContext(self)
@@ -2666,12 +2738,8 @@ class Query(object):
context.labels = labels
- if self._lockmode:
- try:
- context.for_update = self._lockmode_lookup[self._lockmode]
- except KeyError:
- raise sa_exc.ArgumentError(
- "Unknown lockmode %r" % self._lockmode)
+ context._for_update_arg = self._for_update_arg
+
for entity in self._entities:
entity.setup_context(self, context)
@@ -2755,9 +2823,10 @@ class Query(object):
statement = sql.select(
[inner] + context.secondary_columns,
- for_update=context.for_update,
use_labels=context.labels)
+ statement._for_update_arg = context._for_update_arg
+
from_clause = inner
for eager_join in context.eager_joins.values():
# EagerLoader places a 'stop_on' attribute on the join,
@@ -2800,11 +2869,12 @@ class Query(object):
context.whereclause,
from_obj=context.froms,
use_labels=context.labels,
- for_update=context.for_update,
order_by=context.order_by,
**self._select_args
)
+ statement._for_update_arg = context._for_update_arg
+
for hint in self._with_hints:
statement = statement.with_hint(*hint)
@@ -2832,14 +2902,34 @@ class Query(object):
if adapter:
single_crit = adapter.traverse(single_crit)
single_crit = self._adapt_clause(single_crit, False, False)
- context.whereclause = sql.and_(context.whereclause,
- single_crit)
+ context.whereclause = sql.and_(
+ sql.True_._ifnone(context.whereclause),
+ single_crit)
def __str__(self):
return str(self._compile_context().statement)
-inspection._self_inspects(Query)
+from ..sql.selectable import ForUpdateArg
+class LockmodeArg(ForUpdateArg):
+ @classmethod
+ def parse_legacy_query(self, mode):
+ if mode in (None, False):
+ return None
+
+ if mode == "read":
+ read = True
+ nowait = False
+ elif mode == "update":
+ read = nowait = False
+ elif mode == "update_nowait":
+ nowait = True
+ read = False
+ else:
+ raise sa_exc.ArgumentError(
+ "Unknown with_lockmode argument: %r" % mode)
+
+ return LockmodeArg(read=read, nowait=nowait)
class _QueryEntity(object):
"""represent an entity column returned within a Query result."""
@@ -2850,6 +2940,8 @@ class _QueryEntity(object):
if not isinstance(entity, util.string_types) and \
_is_mapped_class(entity):
cls = _MapperEntity
+ elif isinstance(entity, Bundle):
+ cls = _BundleEntity
else:
cls = _ColumnEntity
return object.__new__(cls)
@@ -2864,12 +2956,15 @@ class _MapperEntity(_QueryEntity):
"""mapper/class/AliasedClass entity"""
def __init__(self, query, entity):
- self.primary_entity = not query._entities
+ if not query._primary_entity:
+ query._primary_entity = self
query._entities.append(self)
self.entities = [entity]
self.expr = entity
+ supports_single_entity = True
+
def setup_entity(self, ext_info, aliased_adapter):
self.mapper = ext_info.mapper
self.aliased_adapter = aliased_adapter
@@ -2884,6 +2979,7 @@ class _MapperEntity(_QueryEntity):
else:
self._label_name = self.mapper.class_.__name__
self.path = self.entity_zero._path_registry
+ self.custom_rows = bool(self.mapper.dispatch.append_result)
def set_with_polymorphic(self, query, cls_or_mappers,
selectable, polymorphic_on):
@@ -2939,10 +3035,8 @@ class _MapperEntity(_QueryEntity):
return entity.common_parent(self.entity_zero)
- #_adapted_selectable = None
def adapt_to_selectable(self, query, sel):
query._entities.append(self)
- # self._adapted_selectable = sel
def _get_entity_clauses(self, query, context):
@@ -2980,7 +3074,7 @@ class _MapperEntity(_QueryEntity):
self.selectable,
self.mapper._equivalent_columns)
- if self.primary_entity:
+ if query._primary_entity is self:
_instance = loading.instance_processor(
self.mapper,
context,
@@ -3050,6 +3144,187 @@ class _MapperEntity(_QueryEntity):
def __str__(self):
return str(self.mapper)
+@inspection._self_inspects
+class Bundle(object):
+ """A grouping of SQL expressions that are returned by a :class:`.Query`
+ under one namespace.
+
+ The :class:`.Bundle` essentially allows nesting of the tuple-based
+ results returned by a column-oriented :class:`.Query` object. It also
+ is extensible via simple subclassing, where the primary capability
+ to override is that of how the set of expressions should be returned,
+ allowing post-processing as well as custom return types, without
+ involving ORM identity-mapped classes.
+
+ .. versionadded:: 0.9.0
+
+ .. seealso::
+
+ :ref:`bundles`
+
+ """
+
+ single_entity = False
+ """If True, queries for a single Bundle will be returned as a single
+ entity, rather than an element within a keyed tuple."""
+
+ def __init__(self, name, *exprs, **kw):
+ """Construct a new :class:`.Bundle`.
+
+ e.g.::
+
+ bn = Bundle("mybundle", MyClass.x, MyClass.y)
+
+ for row in session.query(bn).filter(bn.c.x == 5).filter(bn.c.y == 4):
+ print(row.mybundle.x, row.mybundle.y)
+
+ :param name: name of the bundle.
+ :param \*exprs: columns or SQL expressions comprising the bundle.
+ :param single_entity=False: if True, rows for this :class:`.Bundle`
+ can be returned as a "single entity" outside of any enclosing tuple
+ in the same manner as a mapped entity.
+
+ """
+ self.name = self._label = name
+ self.exprs = exprs
+ self.c = self.columns = ColumnCollection()
+ self.columns.update((getattr(col, "key", col._label), col)
+ for col in exprs)
+ self.single_entity = kw.pop('single_entity', self.single_entity)
+
+ columns = None
+ """A namespace of SQL expressions referred to by this :class:`.Bundle`.
+
+ e.g.::
+
+ bn = Bundle("mybundle", MyClass.x, MyClass.y)
+
+ q = sess.query(bn).filter(bn.c.x == 5)
+
+ Nesting of bundles is also supported::
+
+ b1 = Bundle("b1",
+ Bundle('b2', MyClass.a, MyClass.b),
+ Bundle('b3', MyClass.x, MyClass.y)
+ )
+
+ q = sess.query(b1).filter(b1.c.b2.c.a == 5).filter(b1.c.b3.c.y == 9)
+
+ .. seealso::
+
+ :attr:`.Bundle.c`
+
+ """
+
+ c = None
+ """An alias for :attr:`.Bundle.columns`."""
+
+ def _clone(self):
+ cloned = self.__class__.__new__(self.__class__)
+ cloned.__dict__.update(self.__dict__)
+ return cloned
+
+ def __clause_element__(self):
+ return expression.ClauseList(group=False, *self.c)
+
+ @property
+ def clauses(self):
+ return self.__clause_element__().clauses
+
+ def label(self, name):
+ """Provide a copy of this :class:`.Bundle` passing a new label."""
+
+ cloned = self._clone()
+ cloned.name = name
+ return cloned
+
+ def create_row_processor(self, query, procs, labels):
+ """Produce the "row processing" function for this :class:`.Bundle`.
+
+ May be overridden by subclasses.
+
+ .. seealso::
+
+ :ref:`bundles` - includes an example of subclassing.
+
+ """
+ def proc(row, result):
+ return util.KeyedTuple([proc(row, None) for proc in procs], labels)
+ return proc
+
+
+class _BundleEntity(_QueryEntity):
+ def __init__(self, query, bundle, setup_entities=True):
+ query._entities.append(self)
+ self.bundle = self.expr = bundle
+ self.type = type(bundle)
+ self._label_name = bundle.name
+ self._entities = []
+
+ if setup_entities:
+ for expr in bundle.exprs:
+ if isinstance(expr, Bundle):
+ _BundleEntity(self, expr)
+ else:
+ _ColumnEntity(self, expr, namespace=self)
+
+ self.entities = ()
+
+ self.filter_fn = lambda item: item
+
+ self.supports_single_entity = self.bundle.single_entity
+
+ custom_rows = False
+
+ @property
+ def entity_zero(self):
+ for ent in self._entities:
+ ezero = ent.entity_zero
+ if ezero is not None:
+ return ezero
+ else:
+ return None
+
+ def corresponds_to(self, entity):
+ # TODO: this seems to have no effect for
+ # _ColumnEntity either
+ return False
+
+ @property
+ def entity_zero_or_selectable(self):
+ for ent in self._entities:
+ ezero = ent.entity_zero_or_selectable
+ if ezero is not None:
+ return ezero
+ else:
+ return None
+
+ def adapt_to_selectable(self, query, sel):
+ c = _BundleEntity(query, self.bundle, setup_entities=False)
+ #c._label_name = self._label_name
+ #c.entity_zero = self.entity_zero
+ #c.entities = self.entities
+
+ for ent in self._entities:
+ ent.adapt_to_selectable(c, sel)
+
+ def setup_entity(self, ext_info, aliased_adapter):
+ for ent in self._entities:
+ ent.setup_entity(ext_info, aliased_adapter)
+
+ def setup_context(self, query, context):
+ for ent in self._entities:
+ ent.setup_context(query, context)
+
+ def row_processor(self, query, context, custom_rows):
+ procs, labels = zip(
+ *[ent.row_processor(query, context, custom_rows)
+ for ent in self._entities]
+ )
+
+ proc = self.bundle.create_row_processor(query, procs, labels)
+
+ return proc, self._label_name
class _ColumnEntity(_QueryEntity):
"""Column/expression based entity."""
@@ -3066,7 +3341,7 @@ class _ColumnEntity(_QueryEntity):
interfaces.PropComparator
)):
self._label_name = column.key
- column = column.__clause_element__()
+ column = column._query_clause_element()
else:
self._label_name = getattr(column, 'key', None)
@@ -3079,6 +3354,9 @@ class _ColumnEntity(_QueryEntity):
if c is not column:
return
+ elif isinstance(column, Bundle):
+ _BundleEntity(query, column)
+ return
if not isinstance(column, sql.ColumnElement):
raise sa_exc.InvalidRequestError(
@@ -3086,7 +3364,7 @@ class _ColumnEntity(_QueryEntity):
"expected - got '%r'" % (column, )
)
- type_ = column.type
+ self.type = type_ = column.type
if type_.hashable:
self.filter_fn = lambda item: item
else:
@@ -3129,6 +3407,9 @@ class _ColumnEntity(_QueryEntity):
else:
self.entity_zero = None
+ supports_single_entity = False
+ custom_rows = False
+
@property
def entity_zero_or_selectable(self):
if self.entity_zero is not None:
@@ -3138,10 +3419,6 @@ class _ColumnEntity(_QueryEntity):
else:
return None
- @property
- def type(self):
- return self.column.type
-
def adapt_to_selectable(self, query, sel):
c = _ColumnEntity(query, sel.corresponding_column(self.column))
c._label_name = self._label_name
@@ -3154,6 +3431,8 @@ class _ColumnEntity(_QueryEntity):
self.froms.add(ext_info.selectable)
def corresponds_to(self, entity):
+ # TODO: just returning False here,
+ # no tests fail
if self.entity_zero is None:
return False
elif _is_aliased_class(entity):
@@ -3188,14 +3467,11 @@ class _ColumnEntity(_QueryEntity):
return str(self.column)
-log.class_logger(Query)
-
-
class QueryContext(object):
multi_row_eager_loaders = False
adapter = None
froms = ()
- for_update = False
+ for_update = None
def __init__(self, query):
@@ -3230,6 +3506,38 @@ class QueryContext(object):
class AliasOption(interfaces.MapperOption):
def __init__(self, alias):
+ """Return a :class:`.MapperOption` that will indicate to the :class:`.Query`
+ that the main table has been aliased.
+
+ This is a seldom-used option to suit the
+ very rare case that :func:`.contains_eager`
+ is being used in conjunction with a user-defined SELECT
+ statement that aliases the parent table. E.g.::
+
+ # define an aliased UNION called 'ulist'
+ ulist = users.select(users.c.user_id==7).\\
+ union(users.select(users.c.user_id>7)).\\
+ alias('ulist')
+
+ # add on an eager load of "addresses"
+ statement = ulist.outerjoin(addresses).\\
+ select().apply_labels()
+
+ # create query, indicating "ulist" will be an
+ # alias for the main table, "addresses"
+ # property should be eager loaded
+ query = session.query(User).options(
+ contains_alias(ulist),
+ contains_eager(User.addresses))
+
+ # then get results via the statement
+ results = query.from_statement(statement).all()
+
+ :param alias: is the string name of an alias, or a
+ :class:`~.sql.expression.Alias` object representing
+ the alias.
+
+ """
self.alias = alias
def process_query(self, query):
@@ -3238,3 +3546,5 @@ class AliasOption(interfaces.MapperOption):
else:
alias = self.alias
query._from_obj_alias = sql_util.ColumnAdapter(alias)
+
+