summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/ext/baked.py7
-rw-r--r--lib/sqlalchemy/ext/horizontal_shard.py25
-rw-r--r--lib/sqlalchemy/orm/loading.py100
-rw-r--r--lib/sqlalchemy/orm/query.py70
-rw-r--r--lib/sqlalchemy/orm/strategies.py8
5 files changed, 117 insertions, 93 deletions
diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py
index e5e31c1f9..ca07be784 100644
--- a/lib/sqlalchemy/ext/baked.py
+++ b/lib/sqlalchemy/ext/baked.py
@@ -425,9 +425,12 @@ class Result(object):
return str(self._as_query())
def __iter__(self):
+ return iter(self._iter())
+
+ def _iter(self):
bq = self.bq
if not self.session.enable_baked_queries or bq._spoiled:
- return iter(self._as_query())
+ return self._as_query()._iter()
baked_context = bq._bakery.get(bq._effective_key(self.session), None)
if baked_context is None:
@@ -548,7 +551,7 @@ class Result(object):
Equivalent to :meth:`_query.Query.all`.
"""
- return list(self)
+ return self._iter().all()
def get(self, ident):
"""Retrieve an object based on identity.
diff --git a/lib/sqlalchemy/ext/horizontal_shard.py b/lib/sqlalchemy/ext/horizontal_shard.py
index aa2921498..931f45699 100644
--- a/lib/sqlalchemy/ext/horizontal_shard.py
+++ b/lib/sqlalchemy/ext/horizontal_shard.py
@@ -15,12 +15,13 @@ the source distribution.
"""
+import copy
+
from .. import inspect
from .. import util
from ..orm.query import Query
from ..orm.session import Session
-
__all__ = ["ShardedSession", "ShardedQuery"]
@@ -44,11 +45,18 @@ class ShardedQuery(Query):
def _execute_and_instances(self, context):
def iter_for_shard(shard_id):
- context.attributes["shard_id"] = context.identity_token = shard_id
- result = self._connection_from_session(
+ # shallow copy, so that each context may be used by
+ # ORM load events and similar.
+ copied_context = copy.copy(context)
+ copied_context.attributes = context.attributes.copy()
+
+ copied_context.attributes[
+ "shard_id"
+ ] = copied_context.identity_token = shard_id
+ result_ = self._connection_from_session(
mapper=self._bind_mapper(), shard_id=shard_id
- ).execute(context.statement, self._params)
- return self.instances(result, context)
+ ).execute(copied_context.statement, self._params)
+ return self.instances(result_, copied_context)
if context.identity_token is not None:
return iter_for_shard(context.identity_token)
@@ -57,11 +65,10 @@ class ShardedQuery(Query):
else:
partial = []
for shard_id in self.query_chooser(self):
- partial.extend(iter_for_shard(shard_id))
+ result_ = iter_for_shard(shard_id)
+ partial.append(result_)
- # if some kind of in memory 'sorting'
- # were done, this is where it would happen
- return iter(partial)
+ return partial[0].merge(*partial[1:])
def _execute_crud(self, stmt, mapper):
def exec_for_shard(shard_id):
diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py
index d781df980..10d937945 100644
--- a/lib/sqlalchemy/orm/loading.py
+++ b/lib/sqlalchemy/orm/loading.py
@@ -29,9 +29,11 @@ from .util import state_str
from .. import exc as sa_exc
from .. import util
from ..engine import result_tuple
+from ..engine.result import ChunkedIteratorResult
+from ..engine.result import FrozenResult
+from ..engine.result import SimpleResultMetaData
from ..sql import util as sql_util
-
_new_runid = util.counter()
@@ -41,20 +43,7 @@ def instances(query, cursor, context):
context.runid = _new_runid()
context.post_load_paths = {}
- filtered = query._has_mapper_entities
-
- single_entity = query.is_single_entity
-
- if filtered:
- if single_entity:
- filter_fn = id
- else:
-
- def filter_fn(row):
- return tuple(
- id(item) if ent.use_id_for_hash else item
- for ent, item in zip(query._entities, row)
- )
+ single_entity = context.is_single_entity
try:
(process, labels, extra) = list(
@@ -66,42 +55,66 @@ def instances(query, cursor, context):
)
)
- if not single_entity:
- keyed_tuple = result_tuple(labels, extra)
+ if query._yield_per and (
+ context.loaders_require_buffering
+ or context.loaders_require_uniquing
+ ):
+ raise sa_exc.InvalidRequestError(
+ "Can't use yield_per with eager loaders that require uniquing "
+ "or row buffering, e.g. joinedload() against collections "
+ "or subqueryload(). Consider the selectinload() strategy "
+ "for better flexibility in loading objects."
+ )
+
+ except Exception:
+ with util.safe_reraise():
+ cursor.close()
+
+ row_metadata = SimpleResultMetaData(
+ labels,
+ extra,
+ _unique_filters=[
+ id if ent.use_id_for_hash else None for ent in query._entities
+ ],
+ )
+ def chunks(size):
while True:
+ yield_per = size
+
context.partials = {}
- if query._yield_per:
- fetch = cursor.fetchmany(query._yield_per)
+ if yield_per:
+ fetch = cursor.fetchmany(yield_per)
if not fetch:
break
else:
fetch = cursor.fetchall()
- if single_entity:
- proc = process[0]
- rows = [proc(row) for row in fetch]
- else:
- rows = [
- keyed_tuple([proc(row) for proc in process])
- for row in fetch
- ]
+ rows = [tuple([proc(row) for proc in process]) for row in fetch]
for path, post_load in context.post_load_paths.items():
post_load.invoke(context, path)
- if filtered:
- rows = util.unique_list(rows, filter_fn)
+ yield rows
- for row in rows:
- yield row
-
- if not query._yield_per:
+ if not yield_per:
break
- except Exception:
- with util.safe_reraise():
- cursor.close()
+
+ result = ChunkedIteratorResult(row_metadata, chunks)
+ if query._yield_per:
+ result.yield_per(query._yield_per)
+
+ if single_entity:
+ result = result.scalars()
+
+ # filtered = context.loaders_require_uniquing
+ filtered = query._has_mapper_entities
+
+ if filtered:
+ result = result.unique()
+
+ return result
@util.preload_module("sqlalchemy.orm.query")
@@ -114,10 +127,18 @@ def merge_result(query, iterator, load=True):
# flush current contents if we expect to load data
session._autoflush()
+ # TODO: need test coverage and documentation for the FrozenResult
+ # use case.
+ if isinstance(iterator, FrozenResult):
+ frozen_result = iterator
+ iterator = iter(frozen_result.data)
+ else:
+ frozen_result = None
+
autoflush = session.autoflush
try:
session.autoflush = False
- single_entity = len(query._entities) == 1
+ single_entity = not frozen_result and len(query._entities) == 1
if single_entity:
if isinstance(query._entities[0], querylib._MapperEntity):
result = [
@@ -156,7 +177,10 @@ def merge_result(query, iterator, load=True):
)
result.append(keyed_tuple(newrow))
- return iter(result)
+ if frozen_result:
+ return frozen_result.with_data(result)
+ else:
+ return iter(result)
finally:
session.autoflush = autoflush
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index 1fc299cec..5588828eb 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -728,15 +728,6 @@ class Query(Generative):
"""
self._enable_eagerloads = value
- def _no_yield_per(self, message):
- raise sa_exc.InvalidRequestError(
- "The yield_per Query option is currently not "
- "compatible with %s eager loading. Please "
- "specify lazyload('*') or query.enable_eagerloads(False) in "
- "order to "
- "proceed with query.yield_per()." % message
- )
-
@_generative
def with_labels(self):
"""Apply column labels to the return value of Query.statement.
@@ -3238,7 +3229,7 @@ class Query(Generative):
:ref:`faq_query_deduplicating`
"""
- return list(self)
+ return self._iter().all()
@_generative
@_assertions(_no_clauseelement_condition)
@@ -3283,14 +3274,11 @@ class Query(Generative):
:meth:`_query.Query.one_or_none`
"""
+ # replicates limit(1) behavior
if self._statement is not None:
- ret = list(self)[0:1]
+ return self._iter().first()
else:
- ret = list(self[0:1])
- if len(ret) > 0:
- return ret[0]
- else:
- return None
+ return self.limit(1)._iter().first()
def one_or_none(self):
"""Return at most one result or raise an exception.
@@ -3316,17 +3304,7 @@ class Query(Generative):
:meth:`_query.Query.one`
"""
- ret = list(self)
-
- l = len(ret)
- if l == 1:
- return ret[0]
- elif l == 0:
- return None
- else:
- raise orm_exc.MultipleResultsFound(
- "Multiple rows were found for one_or_none()"
- )
+ return self._iter().one_or_none()
def one(self):
"""Return exactly one result or raise an exception.
@@ -3346,19 +3324,7 @@ class Query(Generative):
:meth:`_query.Query.one_or_none`
"""
- try:
- ret = self.one_or_none()
- except orm_exc.MultipleResultsFound as err:
- util.raise_(
- orm_exc.MultipleResultsFound(
- "Multiple rows were found for one()"
- ),
- replace_context=err,
- )
- else:
- if ret is None:
- raise orm_exc.NoResultFound("No row was found for one()")
- return ret
+ return self._iter().one()
def scalar(self):
"""Return the first element of the first result or None
@@ -3379,6 +3345,7 @@ class Query(Generative):
This results in an execution of the underlying query.
"""
+ # TODO: not sure why we can't use result.scalar() here
try:
ret = self.one()
if not isinstance(ret, collections_abc.Sequence):
@@ -3388,6 +3355,24 @@ class Query(Generative):
return None
def __iter__(self):
+ return self._iter().__iter__()
+
+ # TODO: having _iter(), _execute_and_instances, _connection_from_session,
+ # etc., is all too much.
+
+ # new recipes / extensions should be based on an event hook of some kind,
+ # can allow an execution that would return a Result to take in all the
+ # information and return a different Result. this has to be at
+ # the session / connection .execute() level, and can perhaps be
+ # before_execute() but needs to be focused around rewriting of results.
+
+ # the dialect do_execute() *may* be this but that seems a bit too low
+ # level. it may need to be ORM session based and be a session event,
+ # becasue it might not invoke the cursor, might invoke for multiple
+ # connections, etc. OK really has to be a session level event in this
+ # case to support horizontal sharding.
+
+ def _iter(self):
context = self._compile_context()
context.statement.label_style = LABEL_STYLE_TABLENAME_PLUS_COL
if self._autoflush:
@@ -4795,6 +4780,9 @@ class QueryContext(object):
"post_load_paths",
"identity_token",
"single_inh_entities",
+ "is_single_entity",
+ "loaders_require_uniquing",
+ "loaders_require_buffering",
)
def __init__(self, query):
@@ -4815,6 +4803,8 @@ class QueryContext(object):
self.whereclause = query._criterion
self.order_by = query._order_by
+ self.is_single_entity = query.is_single_entity
+ self.loaders_require_buffering = self.loaders_require_uniquing = False
self.multi_row_eager_loaders = False
self.adapter = None
self.froms = ()
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index 2e9b2f316..9009c3425 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -1070,8 +1070,8 @@ class SubqueryLoader(PostLoader):
if not context.query._enable_eagerloads or context.refresh_state:
return
- elif context.query._yield_per:
- context.query._no_yield_per("subquery")
+
+ context.loaders_require_buffering = True
path = path[self.parent_property]
@@ -1536,8 +1536,8 @@ class JoinedLoader(AbstractRelationshipLoader):
if not context.query._enable_eagerloads:
return
- elif context.query._yield_per and self.uselist:
- context.query._no_yield_per("joined collection")
+ elif self.uselist:
+ context.loaders_require_uniquing = True
path = path[self.parent_property]