summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/dynamic.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2021-02-25 17:07:06 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2021-02-25 17:19:19 -0500
commit852119c9f8235a75dcfa9339e6ebf2f57eb39154 (patch)
tree7b6f38e51358d3ec6245a45e19557eaf2f6465d2 /lib/sqlalchemy/orm/dynamic.py
parentdc615763d39916e9c037c7c376db1817cdf02764 (diff)
downloadsqlalchemy-852119c9f8235a75dcfa9339e6ebf2f57eb39154.tar.gz
Revert AppenderQuery modifications from ORM
We are unfortunately stuck with this class completely until we get rid of "dynamic" altogether. The usage contract includes the "query_class" mixin feature where users add their own methods, and this use case very well in line with 2.0's contract. As Query is not going away in any case this has to stay in "legacy" style, there's no point trying to change it as the new version was still fully dependent on Query. Fixes: #5981 Change-Id: I1bc623b17d976b4bb417ab623248d4ac227db74d
Diffstat (limited to 'lib/sqlalchemy/orm/dynamic.py')
-rw-r--r--lib/sqlalchemy/orm/dynamic.py313
1 files changed, 60 insertions, 253 deletions
diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py
index 32eb23199..bf8fc0e33 100644
--- a/lib/sqlalchemy/orm/dynamic.py
+++ b/lib/sqlalchemy/orm/dynamic.py
@@ -23,13 +23,7 @@ from . import util as orm_util
from .query import Query
from .. import exc
from .. import log
-from .. import sql
from .. import util
-from ..engine import result as _result
-from ..sql import selectable
-from ..sql import util as sql_util
-from ..sql.base import _generative
-from ..sql.base import Generative
@log.class_logger
@@ -80,6 +74,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
dispatch,
target_mapper,
order_by,
+ query_class=None,
**kw
):
super(DynamicAttributeImpl, self).__init__(
@@ -87,7 +82,12 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
)
self.target_mapper = target_mapper
self.order_by = order_by
- self.query_class = AppenderQuery
+ if not query_class:
+ self.query_class = AppenderQuery
+ elif AppenderMixin in query_class.mro():
+ self.query_class = query_class
+ else:
+ self.query_class = mixin_user_query(query_class)
def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
if not passive & attributes.SQL_OK:
@@ -259,26 +259,15 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
self.remove(state, dict_, value, initiator, passive=passive)
-class AppenderQuery(Generative):
- """A dynamic query that supports basic collection storage operations."""
+class AppenderMixin(object):
+ query_class = None
def __init__(self, attr, state):
-
- # this can be select() except for aliased=True flag on join()
- # and corresponding behaviors on select().
- self._is_core = False
- self._statement = Query([attr.target_mapper], None)
-
- # self._is_core = True
- # self._statement = sql.select(attr.target_mapper)._set_label_style(
- # selectable.LABEL_STYLE_TABLENAME_PLUS_COL
- # )
-
- self._autoflush = True
+ super(AppenderMixin, self).__init__(attr.target_mapper, None)
self.instance = instance = state.obj()
self.attr = attr
- self.mapper = mapper = object_mapper(instance)
+ mapper = object_mapper(instance)
prop = mapper._props[self.attr.key]
if prop.secondary is not None:
@@ -288,154 +277,20 @@ class AppenderQuery(Generative):
# is in the FROM. So we purposely put the mapper selectable
# in _from_obj[0] to ensure a user-defined join() later on
# doesn't fail, and secondary is then in _from_obj[1].
- self._statement = self._statement.select_from(
- prop.mapper.selectable, prop.secondary
- )
+ self._from_obj = (prop.mapper.selectable, prop.secondary)
- self._statement = self._statement.where(
+ self._where_criteria = (
prop._with_parent(instance, alias_secondary=False),
)
if self.attr.order_by:
-
- self._statement = self._statement.order_by(*self.attr.order_by)
-
- @_generative
- def autoflush(self, setting):
- """Set autoflush to a specific setting.
-
- Note that a Session with autoflush=False will
- not autoflush, even if this flag is set to True at the
- Query level. Therefore this flag is usually used only
- to disable autoflush for a specific Query.
-
- """
- self._autoflush = setting
-
- @property
- def statement(self):
- """Return the Core statement represented by this
- :class:`.AppenderQuery`.
-
- """
- if self._is_core:
- return self._statement._set_label_style(
- selectable.LABEL_STYLE_DISAMBIGUATE_ONLY
- )
-
- else:
- return self._statement.statement
-
- def filter(self, *criteria):
- """A synonym for the :meth:`_orm.AppenderQuery.where` method."""
-
- return self.where(*criteria)
-
- @_generative
- def where(self, *criteria):
- r"""Apply the given WHERE criterion, using SQL expressions.
-
- Equivalent to :meth:`.Select.where`.
-
- """
- self._statement = self._statement.where(*criteria)
-
- @_generative
- def order_by(self, *criteria):
- r"""Apply the given ORDER BY criterion, using SQL expressions.
-
- Equivalent to :meth:`.Select.order_by`.
-
- """
- self._statement = self._statement.order_by(*criteria)
-
- @_generative
- def filter_by(self, **kwargs):
- r"""Apply the given filtering criterion using keyword expressions.
-
- Equivalent to :meth:`.Select.filter_by`.
-
- """
- self._statement = self._statement.filter_by(**kwargs)
-
- @_generative
- def join(self, target, *props, **kwargs):
- r"""Create a SQL JOIN against this
- object's criterion.
-
- Equivalent to :meth:`.Select.join`.
- """
-
- self._statement = self._statement.join(target, *props, **kwargs)
-
- @_generative
- def outerjoin(self, target, *props, **kwargs):
- r"""Create a SQL LEFT OUTER JOIN against this
- object's criterion.
-
- Equivalent to :meth:`.Select.outerjoin`.
-
- """
-
- self._statement = self._statement.outerjoin(target, *props, **kwargs)
-
- def scalar(self):
- """Return the first element of the first result or None
- if no rows present. If multiple rows are returned,
- raises MultipleResultsFound.
-
- Equivalent to :meth:`_query.Query.scalar`.
-
- .. versionadded:: 1.1.6
-
- """
- return self._iter().scalar()
-
- def first(self):
- """Return the first row.
-
- Equivalent to :meth:`_query.Query.first`.
-
- """
-
- # replicates limit(1) behavior
- if self._statement is not None:
- return self._iter().first()
- else:
- return self.limit(1)._iter().first()
-
- def one(self):
- """Return exactly one result or raise an exception.
-
- Equivalent to :meth:`_query.Query.one`.
-
- """
- return self._iter().one()
-
- def one_or_none(self):
- """Return one or zero results, or raise an exception for multiple
- rows.
-
- Equivalent to :meth:`_query.Query.one_or_none`.
-
- .. versionadded:: 1.0.9
-
- """
- return self._iter().one_or_none()
-
- def all(self):
- """Return all rows.
-
- Equivalent to :meth:`_query.Query.all`.
-
- """
- return self._iter().all()
+ self._order_by_clauses = self.attr.order_by
def session(self):
sess = object_session(self.instance)
if (
sess is not None
- and self._autoflush
+ and self.autoflush
and sess.autoflush
and self.instance in sess
):
@@ -447,63 +302,17 @@ class AppenderQuery(Generative):
session = property(session, lambda s, x: None)
- def _execute(self, sess=None):
- # note we're returning an entirely new Query class instance
- # here without any assignment capabilities; the class of this
- # query is determined by the session.
- instance = self.instance
- if sess is None:
- sess = object_session(instance)
- if sess is None:
- raise orm_exc.DetachedInstanceError(
- "Parent instance %s is not bound to a Session, and no "
- "contextual session is established; lazy load operation "
- "of attribute '%s' cannot proceed"
- % (orm_util.instance_str(instance), self.attr.key)
- )
-
- result = sess.execute(self._statement)
- result = result.scalars()
-
- if result._attributes.get("filtered", False):
- result = result.unique()
-
- return result
-
- def _iter(self):
+ def __iter__(self):
sess = self.session
if sess is None:
- instance = self.instance
- state = attributes.instance_state(instance)
-
- if state.detached:
- raise orm_exc.DetachedInstanceError(
- "Parent instance %s is not bound to a Session, and no "
- "contextual session is established; lazy load operation "
- "of attribute '%s' cannot proceed"
- % (orm_util.instance_str(instance), self.attr.key)
- )
- else:
- iterator = (
- (item,)
- for item in self.attr._get_collection_history(
- state,
- attributes.PASSIVE_NO_INITIALIZE,
- ).added_items
- )
-
- row_metadata = _result.SimpleResultMetaData(
- (self.mapper.class_.__name__,),
- [],
- _unique_filters=[id],
- )
-
- return _result.IteratorResult(row_metadata, iterator).scalars()
+ return iter(
+ self.attr._get_collection_history(
+ attributes.instance_state(self.instance),
+ attributes.PASSIVE_NO_INITIALIZE,
+ ).added_items
+ )
else:
- return self._execute(sess)
-
- def __iter__(self):
- return iter(self._iter())
+ return iter(self._generate(sess))
def __getitem__(self, index):
sess = self.session
@@ -513,44 +322,9 @@ class AppenderQuery(Generative):
attributes.PASSIVE_NO_INITIALIZE,
).indexed(index)
else:
- return orm_util._getitem(
- self, index, allow_negative=not self.session.future
- )
-
- @_generative
- def limit(self, limit):
- self._statement = self._statement.limit(limit)
-
- @_generative
- def offset(self, offset):
- self._statement = self._statement.offset(offset)
-
- @_generative
- def slice(self, start, stop):
- """Computes the "slice" represented by
- the given indices and apply as LIMIT/OFFSET.
-
-
- """
- limit_clause, offset_clause = sql_util._make_slice(
- self._statement._limit_clause,
- self._statement._offset_clause,
- start,
- stop,
- )
-
- self._statement = self._statement.limit(limit_clause).offset(
- offset_clause
- )
+ return self._generate(sess).__getitem__(index)
def count(self):
- """return the 'count'.
-
- Equivalent to :meth:`_query.Query.count`.
-
-
- """
-
sess = self.session
if sess is None:
return len(
@@ -560,10 +334,33 @@ class AppenderQuery(Generative):
).added_items
)
else:
- col = sql.func.count(sql.literal_column("*"))
+ return self._generate(sess).count()
+
+ def _generate(self, sess=None):
+ # note we're returning an entirely new Query class instance
+ # here without any assignment capabilities; the class of this
+ # query is determined by the session.
+ instance = self.instance
+ if sess is None:
+ sess = object_session(instance)
+ if sess is None:
+ raise orm_exc.DetachedInstanceError(
+ "Parent instance %s is not bound to a Session, and no "
+ "contextual session is established; lazy load operation "
+ "of attribute '%s' cannot proceed"
+ % (orm_util.instance_str(instance), self.attr.key)
+ )
+
+ if self.query_class:
+ query = self.query_class(self.attr.target_mapper, session=sess)
+ else:
+ query = sess.query(self.attr.target_mapper)
+
+ query._where_criteria = self._where_criteria
+ query._from_obj = self._from_obj
+ query._order_by_clauses = self._order_by_clauses
- stmt = sql.select(col).select_from(self._statement.subquery())
- return self.session.execute(stmt).scalar()
+ return query
def extend(self, iterator):
for item in iterator:
@@ -591,6 +388,16 @@ class AppenderQuery(Generative):
)
+class AppenderQuery(AppenderMixin, Query):
+ """A dynamic query that supports basic collection storage operations."""
+
+
+def mixin_user_query(cls):
+ """Return a new class with AppenderQuery functionality layered over."""
+ name = "Appender" + cls.__name__
+ return type(name, (AppenderMixin, cls), {"query_class": cls})
+
+
class CollectionHistory(object):
"""Overrides AttributeHistory to receive append/remove events directly."""