1# engine/result.py
2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8"""Define generic result set constructs."""
9
10from __future__ import annotations
11
12from enum import Enum
13import functools
14import itertools
15import operator
16import typing
17from typing import Any
18from typing import Callable
19from typing import cast
20from typing import Dict
21from typing import Generic
22from typing import Iterable
23from typing import Iterator
24from typing import List
25from typing import Mapping
26from typing import NoReturn
27from typing import Optional
28from typing import overload
29from typing import Sequence
30from typing import Set
31from typing import Tuple
32from typing import TYPE_CHECKING
33from typing import TypeVar
34from typing import Union
35
36from ._util_cy import tuplegetter as tuplegetter
37from .row import Row
38from .row import RowMapping
39from .. import exc
40from .. import util
41from ..sql.base import _generative
42from ..sql.base import HasMemoized
43from ..sql.base import InPlaceGenerative
44from ..util import deprecated
45from ..util import HasMemoized_ro_memoized_attribute
46from ..util import NONE_SET
47from ..util.typing import Literal
48from ..util.typing import Self
49from ..util.typing import TupleAny
50from ..util.typing import TypeVarTuple
51from ..util.typing import Unpack
52
53if typing.TYPE_CHECKING:
54 from ..sql.elements import SQLCoreOperations
55 from ..sql.type_api import _ResultProcessorType
56
57_KeyType = Union[str, "SQLCoreOperations[Any]"]
58_KeyIndexType = Union[_KeyType, int]
59
60# is overridden in cursor using _CursorKeyMapRecType
61_KeyMapRecType = Any
62
63_KeyMapType = Mapping[_KeyType, _KeyMapRecType]
64
65
66_RowData = Union[Row[Unpack[TupleAny]], RowMapping, Any]
67"""A generic form of "row" that accommodates for the different kinds of
68"rows" that different result objects return, including row, row mapping, and
69scalar values"""
70
71
72_R = TypeVar("_R", bound=_RowData)
73_T = TypeVar("_T", bound=Any)
74_Ts = TypeVarTuple("_Ts")
75
76_InterimRowType = Union[_R, TupleAny]
77"""a catchall "anything" kind of return type that can be applied
78across all the result types
79
80"""
81
82_InterimSupportsScalarsRowType = Union[Row[Unpack[TupleAny]], Any]
83
84_ProcessorsType = Sequence[Optional["_ResultProcessorType[Any]"]]
85_TupleGetterType = Callable[[Sequence[Any]], Sequence[Any]]
86_UniqueFilterType = Callable[[Any], Any]
87_UniqueFilterStateType = Tuple[Set[Any], Optional[_UniqueFilterType]]
88
89
90class ResultMetaData:
91 """Base for metadata about result rows."""
92
93 __slots__ = ()
94
95 _tuplefilter: Optional[_TupleGetterType] = None
96 _translated_indexes: Optional[Sequence[int]] = None
97 _unique_filters: Optional[Sequence[Callable[[Any], Any]]] = None
98 _keymap: _KeyMapType
99 _keys: Sequence[str]
100 _processors: Optional[_ProcessorsType]
101 _key_to_index: Dict[_KeyType, int]
102
103 @property
104 def keys(self) -> RMKeyView:
105 return RMKeyView(self)
106
107 def _has_key(self, key: object) -> bool:
108 raise NotImplementedError()
109
110 def _for_freeze(self) -> ResultMetaData:
111 raise NotImplementedError()
112
113 @overload
114 def _key_fallback(
115 self, key: Any, err: Optional[Exception], raiseerr: Literal[True] = ...
116 ) -> NoReturn: ...
117
118 @overload
119 def _key_fallback(
120 self,
121 key: Any,
122 err: Optional[Exception],
123 raiseerr: Literal[False] = ...,
124 ) -> None: ...
125
126 @overload
127 def _key_fallback(
128 self, key: Any, err: Optional[Exception], raiseerr: bool = ...
129 ) -> Optional[NoReturn]: ...
130
131 def _key_fallback(
132 self, key: Any, err: Optional[Exception], raiseerr: bool = True
133 ) -> Optional[NoReturn]:
134 assert raiseerr
135 raise KeyError(key) from err
136
137 def _raise_for_ambiguous_column_name(
138 self, rec: _KeyMapRecType
139 ) -> NoReturn:
140 raise NotImplementedError(
141 "ambiguous column name logic is implemented for "
142 "CursorResultMetaData"
143 )
144
145 def _index_for_key(
146 self, key: _KeyIndexType, raiseerr: bool
147 ) -> Optional[int]:
148 raise NotImplementedError()
149
150 def _indexes_for_keys(
151 self, keys: Sequence[_KeyIndexType]
152 ) -> Sequence[int]:
153 raise NotImplementedError()
154
155 def _metadata_for_keys(
156 self, keys: Sequence[_KeyIndexType]
157 ) -> Iterator[_KeyMapRecType]:
158 raise NotImplementedError()
159
160 def _reduce(self, keys: Sequence[_KeyIndexType]) -> ResultMetaData:
161 raise NotImplementedError()
162
163 def _getter(
164 self, key: Any, raiseerr: bool = True
165 ) -> Optional[Callable[[Row[Unpack[TupleAny]]], Any]]:
166 index = self._index_for_key(key, raiseerr)
167
168 if index is not None:
169 return operator.itemgetter(index)
170 else:
171 return None
172
173 def _row_as_tuple_getter(
174 self, keys: Sequence[_KeyIndexType]
175 ) -> _TupleGetterType:
176 indexes = self._indexes_for_keys(keys)
177 return tuplegetter(*indexes)
178
179 def _make_key_to_index(
180 self, keymap: Mapping[_KeyType, Sequence[Any]], index: int
181 ) -> Dict[_KeyType, int]:
182 return {
183 key: rec[index]
184 for key, rec in keymap.items()
185 if rec[index] is not None
186 }
187
188 def _key_not_found(self, key: Any, attr_error: bool) -> NoReturn:
189 if key in self._keymap:
190 # the index must be none in this case
191 self._raise_for_ambiguous_column_name(self._keymap[key])
192 else:
193 # unknown key
194 if attr_error:
195 try:
196 self._key_fallback(key, None)
197 except KeyError as ke:
198 raise AttributeError(ke.args[0]) from ke
199 else:
200 self._key_fallback(key, None)
201
202 @property
203 def _effective_processors(self) -> Optional[_ProcessorsType]:
204 if not self._processors or NONE_SET.issuperset(self._processors):
205 return None
206 else:
207 return self._processors
208
209
210class RMKeyView(typing.KeysView[Any]):
211 __slots__ = ("_parent", "_keys")
212
213 _parent: ResultMetaData
214 _keys: Sequence[str]
215
216 def __init__(self, parent: ResultMetaData):
217 self._parent = parent
218 self._keys = [k for k in parent._keys if k is not None]
219
220 def __len__(self) -> int:
221 return len(self._keys)
222
223 def __repr__(self) -> str:
224 return "{0.__class__.__name__}({0._keys!r})".format(self)
225
226 def __iter__(self) -> Iterator[str]:
227 return iter(self._keys)
228
229 def __contains__(self, item: Any) -> bool:
230 if isinstance(item, int):
231 return False
232
233 # note this also includes special key fallback behaviors
234 # which also don't seem to be tested in test_resultset right now
235 return self._parent._has_key(item)
236
237 def __eq__(self, other: Any) -> bool:
238 return list(other) == list(self)
239
240 def __ne__(self, other: Any) -> bool:
241 return list(other) != list(self)
242
243
244class SimpleResultMetaData(ResultMetaData):
245 """result metadata for in-memory collections."""
246
247 __slots__ = (
248 "_keys",
249 "_keymap",
250 "_processors",
251 "_tuplefilter",
252 "_translated_indexes",
253 "_unique_filters",
254 "_key_to_index",
255 )
256
257 _keys: Sequence[str]
258
259 def __init__(
260 self,
261 keys: Sequence[str],
262 extra: Optional[Sequence[Any]] = None,
263 _processors: Optional[_ProcessorsType] = None,
264 _tuplefilter: Optional[_TupleGetterType] = None,
265 _translated_indexes: Optional[Sequence[int]] = None,
266 _unique_filters: Optional[Sequence[Callable[[Any], Any]]] = None,
267 ):
268 self._keys = list(keys)
269 self._tuplefilter = _tuplefilter
270 self._translated_indexes = _translated_indexes
271 self._unique_filters = _unique_filters
272 if extra:
273 assert len(self._keys) == len(extra)
274 recs_names = [
275 (
276 (name,) + (extras if extras else ()),
277 (index, name, extras),
278 )
279 for index, (name, extras) in enumerate(zip(self._keys, extra))
280 ]
281 else:
282 recs_names = [
283 ((name,), (index, name, ()))
284 for index, name in enumerate(self._keys)
285 ]
286
287 self._keymap = {key: rec for keys, rec in recs_names for key in keys}
288
289 self._processors = _processors
290
291 self._key_to_index = self._make_key_to_index(self._keymap, 0)
292
293 def _has_key(self, key: object) -> bool:
294 return key in self._keymap
295
296 def _for_freeze(self) -> ResultMetaData:
297 unique_filters = self._unique_filters
298 if unique_filters and self._tuplefilter:
299 unique_filters = self._tuplefilter(unique_filters)
300
301 # TODO: are we freezing the result with or without uniqueness
302 # applied?
303 return SimpleResultMetaData(
304 self._keys,
305 extra=[self._keymap[key][2] for key in self._keys],
306 _unique_filters=unique_filters,
307 )
308
309 def __getstate__(self) -> Dict[str, Any]:
310 return {
311 "_keys": self._keys,
312 "_translated_indexes": self._translated_indexes,
313 }
314
315 def __setstate__(self, state: Dict[str, Any]) -> None:
316 if state["_translated_indexes"]:
317 _translated_indexes = state["_translated_indexes"]
318 _tuplefilter = tuplegetter(*_translated_indexes)
319 else:
320 _translated_indexes = _tuplefilter = None
321 self.__init__( # type: ignore
322 state["_keys"],
323 _translated_indexes=_translated_indexes,
324 _tuplefilter=_tuplefilter,
325 )
326
327 def _index_for_key(self, key: Any, raiseerr: bool = True) -> int:
328 if int in key.__class__.__mro__:
329 key = self._keys[key]
330 try:
331 rec = self._keymap[key]
332 except KeyError as ke:
333 rec = self._key_fallback(key, ke, raiseerr)
334
335 return rec[0] # type: ignore[no-any-return]
336
337 def _indexes_for_keys(self, keys: Sequence[Any]) -> Sequence[int]:
338 return [self._keymap[key][0] for key in keys]
339
340 def _metadata_for_keys(
341 self, keys: Sequence[Any]
342 ) -> Iterator[_KeyMapRecType]:
343 for key in keys:
344 if int in key.__class__.__mro__:
345 key = self._keys[key]
346
347 try:
348 rec = self._keymap[key]
349 except KeyError as ke:
350 rec = self._key_fallback(key, ke, True)
351
352 yield rec
353
354 def _reduce(self, keys: Sequence[Any]) -> ResultMetaData:
355 try:
356 metadata_for_keys = [
357 self._keymap[
358 self._keys[key] if int in key.__class__.__mro__ else key
359 ]
360 for key in keys
361 ]
362 except KeyError as ke:
363 self._key_fallback(ke.args[0], ke, True)
364
365 indexes: Sequence[int]
366 new_keys: Sequence[str]
367 extra: Sequence[Any]
368 indexes, new_keys, extra = zip(*metadata_for_keys)
369
370 if self._translated_indexes:
371 indexes = [self._translated_indexes[idx] for idx in indexes]
372
373 tup = tuplegetter(*indexes)
374
375 new_metadata = SimpleResultMetaData(
376 new_keys,
377 extra=extra,
378 _tuplefilter=tup,
379 _translated_indexes=indexes,
380 _processors=self._processors,
381 _unique_filters=self._unique_filters,
382 )
383
384 return new_metadata
385
386
387def result_tuple(
388 fields: Sequence[str], extra: Optional[Any] = None
389) -> Callable[[Iterable[Any]], Row[Unpack[TupleAny]]]:
390 parent = SimpleResultMetaData(fields, extra)
391 return functools.partial(
392 Row, parent, parent._effective_processors, parent._key_to_index
393 )
394
395
396# a symbol that indicates to internal Result methods that
397# "no row is returned". We can't use None for those cases where a scalar
398# filter is applied to rows.
399class _NoRow(Enum):
400 _NO_ROW = 0
401
402
403_NO_ROW = _NoRow._NO_ROW
404
405
406class ResultInternal(InPlaceGenerative, Generic[_R]):
407 __slots__ = ()
408
409 _real_result: Optional[Result[Unpack[TupleAny]]] = None
410 _generate_rows: bool = True
411 _row_logging_fn: Optional[Callable[[Any], Any]]
412
413 _unique_filter_state: Optional[_UniqueFilterStateType] = None
414 _post_creational_filter: Optional[Callable[[Any], Any]] = None
415 _is_cursor = False
416
417 _metadata: ResultMetaData
418
419 _source_supports_scalars: bool
420
421 def _fetchiter_impl(
422 self,
423 ) -> Iterator[_InterimRowType[Row[Unpack[TupleAny]]]]:
424 raise NotImplementedError()
425
426 def _fetchone_impl(
427 self, hard_close: bool = False
428 ) -> Optional[_InterimRowType[Row[Unpack[TupleAny]]]]:
429 raise NotImplementedError()
430
431 def _fetchmany_impl(
432 self, size: Optional[int] = None
433 ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]:
434 raise NotImplementedError()
435
436 def _fetchall_impl(
437 self,
438 ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]:
439 raise NotImplementedError()
440
441 def _soft_close(self, hard: bool = False) -> None:
442 raise NotImplementedError()
443
444 @HasMemoized_ro_memoized_attribute
445 def _row_getter(self) -> Optional[Callable[..., _R]]:
446 real_result: Result[Unpack[TupleAny]] = (
447 self._real_result
448 if self._real_result
449 else cast("Result[Unpack[TupleAny]]", self)
450 )
451
452 if real_result._source_supports_scalars:
453 if not self._generate_rows:
454 return None
455 else:
456 _proc = Row
457
458 def process_row(
459 metadata: ResultMetaData,
460 processors: Optional[_ProcessorsType],
461 key_to_index: Dict[_KeyType, int],
462 scalar_obj: Any,
463 ) -> Row[Unpack[TupleAny]]:
464 return _proc(
465 metadata, processors, key_to_index, (scalar_obj,)
466 )
467
468 else:
469 process_row = Row # type: ignore
470
471 metadata = self._metadata
472
473 key_to_index = metadata._key_to_index
474 processors = metadata._effective_processors
475 tf = metadata._tuplefilter
476
477 if tf and not real_result._source_supports_scalars:
478 if processors:
479 processors = tf(processors)
480
481 _make_row_orig: Callable[..., _R] = functools.partial( # type: ignore # noqa E501
482 process_row, metadata, processors, key_to_index
483 )
484
485 fixed_tf = tf
486
487 def make_row(row: _InterimRowType[Row[Unpack[TupleAny]]]) -> _R:
488 return _make_row_orig(fixed_tf(row))
489
490 else:
491 make_row = functools.partial( # type: ignore
492 process_row, metadata, processors, key_to_index
493 )
494
495 if real_result._row_logging_fn:
496 _log_row = real_result._row_logging_fn
497 _make_row = make_row
498
499 def make_row(row: _InterimRowType[Row[Unpack[TupleAny]]]) -> _R:
500 return _log_row(_make_row(row)) # type: ignore
501
502 return make_row
503
504 @HasMemoized_ro_memoized_attribute
505 def _iterator_getter(self) -> Callable[..., Iterator[_R]]:
506 make_row = self._row_getter
507
508 post_creational_filter = self._post_creational_filter
509
510 if self._unique_filter_state:
511 uniques, strategy = self._unique_strategy
512
513 def iterrows(self: Result[Unpack[TupleAny]]) -> Iterator[_R]:
514 for raw_row in self._fetchiter_impl():
515 obj: _InterimRowType[Any] = (
516 make_row(raw_row) if make_row else raw_row
517 )
518 hashed = strategy(obj) if strategy else obj
519 if hashed in uniques:
520 continue
521 uniques.add(hashed)
522 if post_creational_filter:
523 obj = post_creational_filter(obj)
524 yield obj # type: ignore
525
526 else:
527
528 def iterrows(self: Result[Unpack[TupleAny]]) -> Iterator[_R]:
529 for raw_row in self._fetchiter_impl():
530 row: _InterimRowType[Any] = (
531 make_row(raw_row) if make_row else raw_row
532 )
533 if post_creational_filter:
534 row = post_creational_filter(row)
535 yield row # type: ignore
536
537 return iterrows
538
539 def _raw_all_rows(self) -> List[_R]:
540 make_row = self._row_getter
541 assert make_row is not None
542 rows = self._fetchall_impl()
543 return [make_row(row) for row in rows]
544
545 def _allrows(self) -> List[_R]:
546 post_creational_filter = self._post_creational_filter
547
548 make_row = self._row_getter
549
550 rows = self._fetchall_impl()
551 made_rows: List[_InterimRowType[_R]]
552 if make_row:
553 made_rows = [make_row(row) for row in rows]
554 else:
555 made_rows = rows # type: ignore
556
557 interim_rows: List[_R]
558
559 if self._unique_filter_state:
560 uniques, strategy = self._unique_strategy
561
562 interim_rows = [
563 made_row # type: ignore
564 for made_row, sig_row in [
565 (
566 made_row,
567 strategy(made_row) if strategy else made_row,
568 )
569 for made_row in made_rows
570 ]
571 if sig_row not in uniques and not uniques.add(sig_row) # type: ignore # noqa: E501
572 ]
573 else:
574 interim_rows = made_rows # type: ignore
575
576 if post_creational_filter:
577 interim_rows = [
578 post_creational_filter(row) for row in interim_rows
579 ]
580 return interim_rows
581
582 @HasMemoized_ro_memoized_attribute
583 def _onerow_getter(
584 self,
585 ) -> Callable[..., Union[Literal[_NoRow._NO_ROW], _R]]:
586 make_row = self._row_getter
587
588 post_creational_filter = self._post_creational_filter
589
590 if self._unique_filter_state:
591 uniques, strategy = self._unique_strategy
592
593 def onerow(self: Result[Unpack[TupleAny]]) -> Union[_NoRow, _R]:
594 _onerow = self._fetchone_impl
595 while True:
596 row = _onerow()
597 if row is None:
598 return _NO_ROW
599 else:
600 obj: _InterimRowType[Any] = (
601 make_row(row) if make_row else row
602 )
603 hashed = strategy(obj) if strategy else obj
604 if hashed in uniques:
605 continue
606 else:
607 uniques.add(hashed)
608 if post_creational_filter:
609 obj = post_creational_filter(obj)
610 return obj # type: ignore
611
612 else:
613
614 def onerow(self: Result[Unpack[TupleAny]]) -> Union[_NoRow, _R]:
615 row = self._fetchone_impl()
616 if row is None:
617 return _NO_ROW
618 else:
619 interim_row: _InterimRowType[Any] = (
620 make_row(row) if make_row else row
621 )
622 if post_creational_filter:
623 interim_row = post_creational_filter(interim_row)
624 return interim_row # type: ignore
625
626 return onerow
627
628 @HasMemoized_ro_memoized_attribute
629 def _manyrow_getter(self) -> Callable[..., List[_R]]:
630 make_row = self._row_getter
631
632 post_creational_filter = self._post_creational_filter
633
634 if self._unique_filter_state:
635 uniques, strategy = self._unique_strategy
636
637 def filterrows(
638 make_row: Optional[Callable[..., _R]],
639 rows: List[Any],
640 strategy: Optional[Callable[[List[Any]], Any]],
641 uniques: Set[Any],
642 ) -> List[_R]:
643 if make_row:
644 rows = [make_row(row) for row in rows]
645
646 if strategy:
647 made_rows = (
648 (made_row, strategy(made_row)) for made_row in rows
649 )
650 else:
651 made_rows = ((made_row, made_row) for made_row in rows)
652 return [
653 made_row
654 for made_row, sig_row in made_rows
655 if sig_row not in uniques and not uniques.add(sig_row) # type: ignore # noqa: E501
656 ]
657
658 def manyrows(
659 self: ResultInternal[_R], num: Optional[int]
660 ) -> List[_R]:
661 collect: List[_R] = []
662
663 _manyrows = self._fetchmany_impl
664
665 if num is None:
666 # if None is passed, we don't know the default
667 # manyrows number, DBAPI has this as cursor.arraysize
668 # different DBAPIs / fetch strategies may be different.
669 # do a fetch to find what the number is. if there are
670 # only fewer rows left, then it doesn't matter.
671 real_result = (
672 self._real_result
673 if self._real_result
674 else cast("Result[Unpack[TupleAny]]", self)
675 )
676 if real_result._yield_per:
677 num_required = num = real_result._yield_per
678 else:
679 rows = _manyrows(num)
680 num = len(rows)
681 assert make_row is not None
682 collect.extend(
683 filterrows(make_row, rows, strategy, uniques)
684 )
685 num_required = num - len(collect)
686 else:
687 num_required = num
688
689 assert num is not None
690
691 while num_required:
692 rows = _manyrows(num_required)
693 if not rows:
694 break
695
696 collect.extend(
697 filterrows(make_row, rows, strategy, uniques)
698 )
699 num_required = num - len(collect)
700
701 if post_creational_filter:
702 collect = [post_creational_filter(row) for row in collect]
703 return collect
704
705 else:
706
707 def manyrows(
708 self: ResultInternal[_R], num: Optional[int]
709 ) -> List[_R]:
710 if num is None:
711 real_result = (
712 self._real_result
713 if self._real_result
714 else cast("Result[Unpack[TupleAny]]", self)
715 )
716 num = real_result._yield_per
717
718 rows: List[_InterimRowType[Any]] = self._fetchmany_impl(num)
719 if make_row:
720 rows = [make_row(row) for row in rows]
721 if post_creational_filter:
722 rows = [post_creational_filter(row) for row in rows]
723 return rows # type: ignore
724
725 return manyrows
726
727 @overload
728 def _only_one_row(
729 self: ResultInternal[Row[_T, Unpack[TupleAny]]],
730 raise_for_second_row: bool,
731 raise_for_none: bool,
732 scalar: Literal[True],
733 ) -> _T: ...
734
735 @overload
736 def _only_one_row(
737 self,
738 raise_for_second_row: bool,
739 raise_for_none: Literal[True],
740 scalar: bool,
741 ) -> _R: ...
742
743 @overload
744 def _only_one_row(
745 self,
746 raise_for_second_row: bool,
747 raise_for_none: bool,
748 scalar: bool,
749 ) -> Optional[_R]: ...
750
751 def _only_one_row(
752 self,
753 raise_for_second_row: bool,
754 raise_for_none: bool,
755 scalar: bool,
756 ) -> Optional[_R]:
757 onerow = self._fetchone_impl
758
759 row: Optional[_InterimRowType[Any]] = onerow(hard_close=True)
760 if row is None:
761 if raise_for_none:
762 raise exc.NoResultFound(
763 "No row was found when one was required"
764 )
765 else:
766 return None
767
768 if scalar and self._source_supports_scalars:
769 self._generate_rows = False
770 make_row = None
771 else:
772 make_row = self._row_getter
773
774 try:
775 row = make_row(row) if make_row else row
776 except:
777 self._soft_close(hard=True)
778 raise
779
780 if raise_for_second_row:
781 if self._unique_filter_state:
782 # for no second row but uniqueness, need to essentially
783 # consume the entire result :(
784 uniques, strategy = self._unique_strategy
785
786 existing_row_hash = strategy(row) if strategy else row
787
788 while True:
789 next_row: Any = onerow(hard_close=True)
790 if next_row is None:
791 next_row = _NO_ROW
792 break
793
794 try:
795 next_row = make_row(next_row) if make_row else next_row
796
797 if strategy:
798 assert next_row is not _NO_ROW
799 if existing_row_hash == strategy(next_row):
800 continue
801 elif row == next_row:
802 continue
803 # here, we have a row and it's different
804 break
805 except:
806 self._soft_close(hard=True)
807 raise
808 else:
809 next_row = onerow(hard_close=True)
810 if next_row is None:
811 next_row = _NO_ROW
812
813 if next_row is not _NO_ROW:
814 self._soft_close(hard=True)
815 raise exc.MultipleResultsFound(
816 "Multiple rows were found when exactly one was required"
817 if raise_for_none
818 else "Multiple rows were found when one or none "
819 "was required"
820 )
821 else:
822 # if we checked for second row then that would have
823 # closed us :)
824 self._soft_close(hard=True)
825
826 if not scalar:
827 post_creational_filter = self._post_creational_filter
828 if post_creational_filter:
829 row = post_creational_filter(row)
830
831 if scalar and make_row:
832 return row[0] # type: ignore
833 else:
834 return row # type: ignore
835
836 def _iter_impl(self) -> Iterator[_R]:
837 return self._iterator_getter(self)
838
839 def _next_impl(self) -> _R:
840 row = self._onerow_getter(self)
841 if row is _NO_ROW:
842 raise StopIteration()
843 else:
844 return row
845
846 @_generative
847 def _column_slices(self, indexes: Sequence[_KeyIndexType]) -> Self:
848 real_result = (
849 self._real_result
850 if self._real_result
851 else cast("Result[Any]", self)
852 )
853
854 if not real_result._source_supports_scalars or len(indexes) != 1:
855 self._metadata = self._metadata._reduce(indexes)
856
857 assert self._generate_rows
858
859 return self
860
861 @HasMemoized.memoized_attribute
862 def _unique_strategy(self) -> _UniqueFilterStateType:
863 assert self._unique_filter_state is not None
864 uniques, strategy = self._unique_filter_state
865
866 real_result = (
867 self._real_result
868 if self._real_result is not None
869 else cast("Result[Unpack[TupleAny]]", self)
870 )
871
872 if not strategy and self._metadata._unique_filters:
873 if (
874 real_result._source_supports_scalars
875 and not self._generate_rows
876 ):
877 strategy = self._metadata._unique_filters[0]
878 else:
879 filters = self._metadata._unique_filters
880 if self._metadata._tuplefilter:
881 filters = self._metadata._tuplefilter(filters)
882
883 strategy = operator.methodcaller("_filter_on_values", filters)
884 return uniques, strategy
885
886
887class _WithKeys:
888 __slots__ = ()
889
890 _metadata: ResultMetaData
891
892 # used mainly to share documentation on the keys method.
893 def keys(self) -> RMKeyView:
894 """Return an iterable view which yields the string keys that would
895 be represented by each :class:`_engine.Row`.
896
897 The keys can represent the labels of the columns returned by a core
898 statement or the names of the orm classes returned by an orm
899 execution.
900
901 The view also can be tested for key containment using the Python
902 ``in`` operator, which will test both for the string keys represented
903 in the view, as well as for alternate keys such as column objects.
904
905 .. versionchanged:: 1.4 a key view object is returned rather than a
906 plain list.
907
908
909 """
910 return self._metadata.keys
911
912
913class Result(_WithKeys, ResultInternal[Row[Unpack[_Ts]]]):
914 """Represent a set of database results.
915
916 .. versionadded:: 1.4 The :class:`_engine.Result` object provides a
917 completely updated usage model and calling facade for SQLAlchemy
918 Core and SQLAlchemy ORM. In Core, it forms the basis of the
919 :class:`_engine.CursorResult` object which replaces the previous
920 :class:`_engine.ResultProxy` interface. When using the ORM, a
921 higher level object called :class:`_engine.ChunkedIteratorResult`
922 is normally used.
923
924 .. note:: In SQLAlchemy 1.4 and above, this object is
925 used for ORM results returned by :meth:`_orm.Session.execute`, which can
926 yield instances of ORM mapped objects either individually or within
927 tuple-like rows. Note that the :class:`_engine.Result` object does not
928 deduplicate instances or rows automatically as is the case with the
929 legacy :class:`_orm.Query` object. For in-Python de-duplication of
930 instances or rows, use the :meth:`_engine.Result.unique` modifier
931 method.
932
933 .. seealso::
934
935 :ref:`tutorial_fetching_rows` - in the :doc:`/tutorial/index`
936
937 """
938
939 __slots__ = ("_metadata", "__dict__")
940
941 _row_logging_fn: Optional[
942 Callable[[Row[Unpack[TupleAny]]], Row[Unpack[TupleAny]]]
943 ] = None
944
945 _source_supports_scalars: bool = False
946
947 _yield_per: Optional[int] = None
948
949 _attributes: util.immutabledict[Any, Any] = util.immutabledict()
950
951 def __init__(self, cursor_metadata: ResultMetaData):
952 self._metadata = cursor_metadata
953
954 def __enter__(self) -> Self:
955 return self
956
957 def __exit__(self, type_: Any, value: Any, traceback: Any) -> None:
958 self.close()
959
960 def close(self) -> None:
961 """close this :class:`_engine.Result`.
962
963 The behavior of this method is implementation specific, and is
964 not implemented by default. The method should generally end
965 the resources in use by the result object and also cause any
966 subsequent iteration or row fetching to raise
967 :class:`.ResourceClosedError`.
968
969 .. versionadded:: 1.4.27 - ``.close()`` was previously not generally
970 available for all :class:`_engine.Result` classes, instead only
971 being available on the :class:`_engine.CursorResult` returned for
972 Core statement executions. As most other result objects, namely the
973 ones used by the ORM, are proxying a :class:`_engine.CursorResult`
974 in any case, this allows the underlying cursor result to be closed
975 from the outside facade for the case when the ORM query is using
976 the ``yield_per`` execution option where it does not immediately
977 exhaust and autoclose the database cursor.
978
979 """
980 self._soft_close(hard=True)
981
982 @property
983 def _soft_closed(self) -> bool:
984 raise NotImplementedError()
985
986 @property
987 def closed(self) -> bool:
988 """return ``True`` if this :class:`_engine.Result` reports .closed
989
990 .. versionadded:: 1.4.43
991
992 """
993 raise NotImplementedError()
994
995 @_generative
996 def yield_per(self, num: int) -> Self:
997 """Configure the row-fetching strategy to fetch ``num`` rows at a time.
998
999 This impacts the underlying behavior of the result when iterating over
1000 the result object, or otherwise making use of methods such as
1001 :meth:`_engine.Result.fetchone` that return one row at a time. Data
1002 from the underlying cursor or other data source will be buffered up to
1003 this many rows in memory, and the buffered collection will then be
1004 yielded out one row at a time or as many rows are requested. Each time
1005 the buffer clears, it will be refreshed to this many rows or as many
1006 rows remain if fewer remain.
1007
1008 The :meth:`_engine.Result.yield_per` method is generally used in
1009 conjunction with the
1010 :paramref:`_engine.Connection.execution_options.stream_results`
1011 execution option, which will allow the database dialect in use to make
1012 use of a server side cursor, if the DBAPI supports a specific "server
1013 side cursor" mode separate from its default mode of operation.
1014
1015 .. tip::
1016
1017 Consider using the
1018 :paramref:`_engine.Connection.execution_options.yield_per`
1019 execution option, which will simultaneously set
1020 :paramref:`_engine.Connection.execution_options.stream_results`
1021 to ensure the use of server side cursors, as well as automatically
1022 invoke the :meth:`_engine.Result.yield_per` method to establish
1023 a fixed row buffer size at once.
1024
1025 The :paramref:`_engine.Connection.execution_options.yield_per`
1026 execution option is available for ORM operations, with
1027 :class:`_orm.Session`-oriented use described at
1028 :ref:`orm_queryguide_yield_per`. The Core-only version which works
1029 with :class:`_engine.Connection` is new as of SQLAlchemy 1.4.40.
1030
1031 .. versionadded:: 1.4
1032
1033 :param num: number of rows to fetch each time the buffer is refilled.
1034 If set to a value below 1, fetches all rows for the next buffer.
1035
1036 .. seealso::
1037
1038 :ref:`engine_stream_results` - describes Core behavior for
1039 :meth:`_engine.Result.yield_per`
1040
1041 :ref:`orm_queryguide_yield_per` - in the :ref:`queryguide_toplevel`
1042
1043 """
1044 self._yield_per = num
1045 return self
1046
1047 @_generative
1048 def unique(self, strategy: Optional[_UniqueFilterType] = None) -> Self:
1049 """Apply unique filtering to the objects returned by this
1050 :class:`_engine.Result`.
1051
1052 When this filter is applied with no arguments, the rows or objects
1053 returned will filtered such that each row is returned uniquely. The
1054 algorithm used to determine this uniqueness is by default the Python
1055 hashing identity of the whole tuple. In some cases a specialized
1056 per-entity hashing scheme may be used, such as when using the ORM, a
1057 scheme is applied which works against the primary key identity of
1058 returned objects.
1059
1060 The unique filter is applied **after all other filters**, which means
1061 if the columns returned have been refined using a method such as the
1062 :meth:`_engine.Result.columns` or :meth:`_engine.Result.scalars`
1063 method, the uniquing is applied to **only the column or columns
1064 returned**. This occurs regardless of the order in which these
1065 methods have been called upon the :class:`_engine.Result` object.
1066
1067 The unique filter also changes the calculus used for methods like
1068 :meth:`_engine.Result.fetchmany` and :meth:`_engine.Result.partitions`.
1069 When using :meth:`_engine.Result.unique`, these methods will continue
1070 to yield the number of rows or objects requested, after uniquing
1071 has been applied. However, this necessarily impacts the buffering
1072 behavior of the underlying cursor or datasource, such that multiple
1073 underlying calls to ``cursor.fetchmany()`` may be necessary in order
1074 to accumulate enough objects in order to provide a unique collection
1075 of the requested size.
1076
1077 :param strategy: a callable that will be applied to rows or objects
1078 being iterated, which should return an object that represents the
1079 unique value of the row. A Python ``set()`` is used to store
1080 these identities. If not passed, a default uniqueness strategy
1081 is used which may have been assembled by the source of this
1082 :class:`_engine.Result` object.
1083
1084 """
1085 self._unique_filter_state = (set(), strategy)
1086 return self
1087
1088 def columns(self, *col_expressions: _KeyIndexType) -> Self:
1089 r"""Establish the columns that should be returned in each row.
1090
1091 This method may be used to limit the columns returned as well
1092 as to reorder them. The given list of expressions are normally
1093 a series of integers or string key names. They may also be
1094 appropriate :class:`.ColumnElement` objects which correspond to
1095 a given statement construct.
1096
1097 .. versionchanged:: 2.0 Due to a bug in 1.4, the
1098 :meth:`_engine.Result.columns` method had an incorrect behavior
1099 where calling upon the method with just one index would cause the
1100 :class:`_engine.Result` object to yield scalar values rather than
1101 :class:`_engine.Row` objects. In version 2.0, this behavior
1102 has been corrected such that calling upon
1103 :meth:`_engine.Result.columns` with a single index will
1104 produce a :class:`_engine.Result` object that continues
1105 to yield :class:`_engine.Row` objects, which include
1106 only a single column.
1107
1108 E.g.::
1109
1110 statement = select(table.c.x, table.c.y, table.c.z)
1111 result = connection.execute(statement)
1112
1113 for z, y in result.columns("z", "y"):
1114 ...
1115
1116 Example of using the column objects from the statement itself::
1117
1118 for z, y in result.columns(
1119 statement.selected_columns.c.z, statement.selected_columns.c.y
1120 ):
1121 ...
1122
1123 .. versionadded:: 1.4
1124
1125 :param \*col_expressions: indicates columns to be returned. Elements
1126 may be integer row indexes, string column names, or appropriate
1127 :class:`.ColumnElement` objects corresponding to a select construct.
1128
1129 :return: this :class:`_engine.Result` object with the modifications
1130 given.
1131
1132 """
1133 return self._column_slices(col_expressions)
1134
1135 @overload
1136 def scalars(self: Result[_T, Unpack[TupleAny]]) -> ScalarResult[_T]: ...
1137
1138 @overload
1139 def scalars(
1140 self: Result[_T, Unpack[TupleAny]], index: Literal[0]
1141 ) -> ScalarResult[_T]: ...
1142
1143 @overload
1144 def scalars(self, index: _KeyIndexType = 0) -> ScalarResult[Any]: ...
1145
1146 def scalars(self, index: _KeyIndexType = 0) -> ScalarResult[Any]:
1147 """Return a :class:`_engine.ScalarResult` filtering object which
1148 will return single elements rather than :class:`_row.Row` objects.
1149
1150 E.g.::
1151
1152 >>> result = conn.execute(text("select int_id from table"))
1153 >>> result.scalars().all()
1154 [1, 2, 3]
1155
1156 When results are fetched from the :class:`_engine.ScalarResult`
1157 filtering object, the single column-row that would be returned by the
1158 :class:`_engine.Result` is instead returned as the column's value.
1159
1160 .. versionadded:: 1.4
1161
1162 :param index: integer or row key indicating the column to be fetched
1163 from each row, defaults to ``0`` indicating the first column.
1164
1165 :return: a new :class:`_engine.ScalarResult` filtering object referring
1166 to this :class:`_engine.Result` object.
1167
1168 """
1169 return ScalarResult(self, index)
1170
1171 def _getter(
1172 self, key: _KeyIndexType, raiseerr: bool = True
1173 ) -> Optional[Callable[[Row[Unpack[TupleAny]]], Any]]:
1174 """return a callable that will retrieve the given key from a
1175 :class:`_engine.Row`.
1176
1177 """
1178 if self._source_supports_scalars:
1179 raise NotImplementedError(
1180 "can't use this function in 'only scalars' mode"
1181 )
1182 return self._metadata._getter(key, raiseerr)
1183
1184 def _tuple_getter(self, keys: Sequence[_KeyIndexType]) -> _TupleGetterType:
1185 """return a callable that will retrieve the given keys from a
1186 :class:`_engine.Row`.
1187
1188 """
1189 if self._source_supports_scalars:
1190 raise NotImplementedError(
1191 "can't use this function in 'only scalars' mode"
1192 )
1193 return self._metadata._row_as_tuple_getter(keys)
1194
1195 def mappings(self) -> MappingResult:
1196 """Apply a mappings filter to returned rows, returning an instance of
1197 :class:`_engine.MappingResult`.
1198
1199 When this filter is applied, fetching rows will return
1200 :class:`_engine.RowMapping` objects instead of :class:`_engine.Row`
1201 objects.
1202
1203 .. versionadded:: 1.4
1204
1205 :return: a new :class:`_engine.MappingResult` filtering object
1206 referring to this :class:`_engine.Result` object.
1207
1208 """
1209
1210 return MappingResult(self)
1211
1212 @property
1213 @deprecated(
1214 "2.1.0",
1215 "The :attr:`.Result.t` method is deprecated, :class:`.Row` "
1216 "now behaves like a tuple and can unpack types directly.",
1217 )
1218 def t(self) -> TupleResult[Tuple[Unpack[_Ts]]]:
1219 """Apply a "typed tuple" typing filter to returned rows.
1220
1221 The :attr:`_engine.Result.t` attribute is a synonym for
1222 calling the :meth:`_engine.Result.tuples` method.
1223
1224 .. versionadded:: 2.0
1225
1226 .. seealso::
1227
1228 :ref:`change_10635` - describes a migration path from this
1229 workaround for SQLAlchemy 2.1.
1230
1231 """
1232 return self # type: ignore
1233
1234 @deprecated(
1235 "2.1.0",
1236 "The :meth:`.Result.tuples` method is deprecated, :class:`.Row` "
1237 "now behaves like a tuple and can unpack types directly.",
1238 )
1239 def tuples(self) -> TupleResult[Tuple[Unpack[_Ts]]]:
1240 """Apply a "typed tuple" typing filter to returned rows.
1241
1242 This method returns the same :class:`_engine.Result` object
1243 at runtime,
1244 however annotates as returning a :class:`_engine.TupleResult` object
1245 that will indicate to :pep:`484` typing tools that plain typed
1246 ``Tuple`` instances are returned rather than rows. This allows
1247 tuple unpacking and ``__getitem__`` access of :class:`_engine.Row`
1248 objects to by typed, for those cases where the statement invoked
1249 itself included typing information.
1250
1251 .. versionadded:: 2.0
1252
1253 :return: the :class:`_engine.TupleResult` type at typing time.
1254
1255 .. seealso::
1256
1257 :ref:`change_10635` - describes a migration path from this
1258 workaround for SQLAlchemy 2.1.
1259
1260 :attr:`_engine.Result.t` - shorter synonym
1261
1262 :attr:`_engine.Row._t` - :class:`_engine.Row` version
1263
1264 """
1265
1266 return self # type: ignore
1267
1268 def _raw_row_iterator(self) -> Iterator[_RowData]:
1269 """Return a safe iterator that yields raw row data.
1270
1271 This is used by the :meth:`_engine.Result.merge` method
1272 to merge multiple compatible results together.
1273
1274 """
1275 raise NotImplementedError()
1276
1277 def __iter__(self) -> Iterator[Row[Unpack[_Ts]]]:
1278 return self._iter_impl()
1279
1280 def __next__(self) -> Row[Unpack[_Ts]]:
1281 return self._next_impl()
1282
1283 def partitions(
1284 self, size: Optional[int] = None
1285 ) -> Iterator[Sequence[Row[Unpack[_Ts]]]]:
1286 """Iterate through sub-lists of rows of the size given.
1287
1288 Each list will be of the size given, excluding the last list to
1289 be yielded, which may have a small number of rows. No empty
1290 lists will be yielded.
1291
1292 The result object is automatically closed when the iterator
1293 is fully consumed.
1294
1295 Note that the backend driver will usually buffer the entire result
1296 ahead of time unless the
1297 :paramref:`.Connection.execution_options.stream_results` execution
1298 option is used indicating that the driver should not pre-buffer
1299 results, if possible. Not all drivers support this option and
1300 the option is silently ignored for those who do not.
1301
1302 When using the ORM, the :meth:`_engine.Result.partitions` method
1303 is typically more effective from a memory perspective when it is
1304 combined with use of the
1305 :ref:`yield_per execution option <orm_queryguide_yield_per>`,
1306 which instructs both the DBAPI driver to use server side cursors,
1307 if available, as well as instructs the ORM loading internals to only
1308 build a certain amount of ORM objects from a result at a time before
1309 yielding them out.
1310
1311 .. versionadded:: 1.4
1312
1313 :param size: indicate the maximum number of rows to be present
1314 in each list yielded. If None, makes use of the value set by
1315 the :meth:`_engine.Result.yield_per`, method, if it were called,
1316 or the :paramref:`_engine.Connection.execution_options.yield_per`
1317 execution option, which is equivalent in this regard. If
1318 yield_per weren't set, it makes use of the
1319 :meth:`_engine.Result.fetchmany` default, which may be backend
1320 specific and not well defined.
1321
1322 :return: iterator of lists
1323
1324 .. seealso::
1325
1326 :ref:`engine_stream_results`
1327
1328 :ref:`orm_queryguide_yield_per` - in the :ref:`queryguide_toplevel`
1329
1330 """
1331
1332 getter = self._manyrow_getter
1333
1334 while True:
1335 partition = getter(self, size)
1336 if partition:
1337 yield partition
1338 else:
1339 break
1340
1341 def fetchall(self) -> Sequence[Row[Unpack[_Ts]]]:
1342 """A synonym for the :meth:`_engine.Result.all` method."""
1343
1344 return self._allrows()
1345
1346 def fetchone(self) -> Optional[Row[Unpack[_Ts]]]:
1347 """Fetch one row.
1348
1349 When all rows are exhausted, returns None.
1350
1351 This method is provided for backwards compatibility with
1352 SQLAlchemy 1.x.x.
1353
1354 To fetch the first row of a result only, use the
1355 :meth:`_engine.Result.first` method. To iterate through all
1356 rows, iterate the :class:`_engine.Result` object directly.
1357
1358 :return: a :class:`_engine.Row` object if no filters are applied,
1359 or ``None`` if no rows remain.
1360
1361 """
1362 row = self._onerow_getter(self)
1363 if row is _NO_ROW:
1364 return None
1365 else:
1366 return row
1367
1368 def fetchmany(
1369 self, size: Optional[int] = None
1370 ) -> Sequence[Row[Unpack[_Ts]]]:
1371 """Fetch many rows.
1372
1373 When all rows are exhausted, returns an empty sequence.
1374
1375 This method is provided for backwards compatibility with
1376 SQLAlchemy 1.x.x.
1377
1378 To fetch rows in groups, use the :meth:`_engine.Result.partitions`
1379 method.
1380
1381 :return: a sequence of :class:`_engine.Row` objects.
1382
1383 .. seealso::
1384
1385 :meth:`_engine.Result.partitions`
1386
1387 """
1388
1389 return self._manyrow_getter(self, size)
1390
1391 def all(self) -> Sequence[Row[Unpack[_Ts]]]:
1392 """Return all rows in a sequence.
1393
1394 Closes the result set after invocation. Subsequent invocations
1395 will return an empty sequence.
1396
1397 .. versionadded:: 1.4
1398
1399 :return: a sequence of :class:`_engine.Row` objects.
1400
1401 .. seealso::
1402
1403 :ref:`engine_stream_results` - How to stream a large result set
1404 without loading it completely in python.
1405
1406 """
1407
1408 return self._allrows()
1409
1410 def first(self) -> Optional[Row[Unpack[_Ts]]]:
1411 """Fetch the first row or ``None`` if no row is present.
1412
1413 Closes the result set and discards remaining rows.
1414
1415 .. note:: This method returns one **row**, e.g. tuple, by default.
1416 To return exactly one single scalar value, that is, the first
1417 column of the first row, use the
1418 :meth:`_engine.Result.scalar` method,
1419 or combine :meth:`_engine.Result.scalars` and
1420 :meth:`_engine.Result.first`.
1421
1422 Additionally, in contrast to the behavior of the legacy ORM
1423 :meth:`_orm.Query.first` method, **no limit is applied** to the
1424 SQL query which was invoked to produce this
1425 :class:`_engine.Result`;
1426 for a DBAPI driver that buffers results in memory before yielding
1427 rows, all rows will be sent to the Python process and all but
1428 the first row will be discarded.
1429
1430 .. seealso::
1431
1432 :ref:`migration_20_unify_select`
1433
1434 :return: a :class:`_engine.Row` object, or None
1435 if no rows remain.
1436
1437 .. seealso::
1438
1439 :meth:`_engine.Result.scalar`
1440
1441 :meth:`_engine.Result.one`
1442
1443 """
1444
1445 return self._only_one_row(
1446 raise_for_second_row=False, raise_for_none=False, scalar=False
1447 )
1448
1449 def one_or_none(self) -> Optional[Row[Unpack[_Ts]]]:
1450 """Return at most one result or raise an exception.
1451
1452 Returns ``None`` if the result has no rows.
1453 Raises :class:`.MultipleResultsFound`
1454 if multiple rows are returned.
1455
1456 .. versionadded:: 1.4
1457
1458 :return: The first :class:`_engine.Row` or ``None`` if no row
1459 is available.
1460
1461 :raises: :class:`.MultipleResultsFound`
1462
1463 .. seealso::
1464
1465 :meth:`_engine.Result.first`
1466
1467 :meth:`_engine.Result.one`
1468
1469 """
1470 return self._only_one_row(
1471 raise_for_second_row=True, raise_for_none=False, scalar=False
1472 )
1473
1474 def scalar_one(self: Result[_T, Unpack[TupleAny]]) -> _T:
1475 """Return exactly one scalar result or raise an exception.
1476
1477 This is equivalent to calling :meth:`_engine.Result.scalars` and
1478 then :meth:`_engine.ScalarResult.one`.
1479
1480 .. seealso::
1481
1482 :meth:`_engine.ScalarResult.one`
1483
1484 :meth:`_engine.Result.scalars`
1485
1486 """
1487 return self._only_one_row(
1488 raise_for_second_row=True, raise_for_none=True, scalar=True
1489 )
1490
1491 def scalar_one_or_none(self: Result[_T, Unpack[TupleAny]]) -> Optional[_T]:
1492 """Return exactly one scalar result or ``None``.
1493
1494 This is equivalent to calling :meth:`_engine.Result.scalars` and
1495 then :meth:`_engine.ScalarResult.one_or_none`.
1496
1497 .. seealso::
1498
1499 :meth:`_engine.ScalarResult.one_or_none`
1500
1501 :meth:`_engine.Result.scalars`
1502
1503 """
1504 return self._only_one_row(
1505 raise_for_second_row=True, raise_for_none=False, scalar=True
1506 )
1507
1508 def one(self) -> Row[Unpack[_Ts]]:
1509 """Return exactly one row or raise an exception.
1510
1511 Raises :class:`_exc.NoResultFound` if the result returns no
1512 rows, or :class:`_exc.MultipleResultsFound` if multiple rows
1513 would be returned.
1514
1515 .. note:: This method returns one **row**, e.g. tuple, by default.
1516 To return exactly one single scalar value, that is, the first
1517 column of the first row, use the
1518 :meth:`_engine.Result.scalar_one` method, or combine
1519 :meth:`_engine.Result.scalars` and
1520 :meth:`_engine.Result.one`.
1521
1522 .. versionadded:: 1.4
1523
1524 :return: The first :class:`_engine.Row`.
1525
1526 :raises: :class:`.MultipleResultsFound`, :class:`.NoResultFound`
1527
1528 .. seealso::
1529
1530 :meth:`_engine.Result.first`
1531
1532 :meth:`_engine.Result.one_or_none`
1533
1534 :meth:`_engine.Result.scalar_one`
1535
1536 """
1537 return self._only_one_row(
1538 raise_for_second_row=True, raise_for_none=True, scalar=False
1539 )
1540
1541 def scalar(self: Result[_T, Unpack[TupleAny]]) -> Optional[_T]:
1542 """Fetch the first column of the first row, and close the result set.
1543
1544 Returns ``None`` if there are no rows to fetch.
1545
1546 No validation is performed to test if additional rows remain.
1547
1548 After calling this method, the object is fully closed,
1549 e.g. the :meth:`_engine.CursorResult.close`
1550 method will have been called.
1551
1552 :return: a Python scalar value, or ``None`` if no rows remain.
1553
1554 """
1555 return self._only_one_row(
1556 raise_for_second_row=False, raise_for_none=False, scalar=True
1557 )
1558
1559 def freeze(self) -> FrozenResult[Unpack[_Ts]]:
1560 """Return a callable object that will produce copies of this
1561 :class:`_engine.Result` when invoked.
1562
1563 The callable object returned is an instance of
1564 :class:`_engine.FrozenResult`.
1565
1566 This is used for result set caching. The method must be called
1567 on the result when it has been unconsumed, and calling the method
1568 will consume the result fully. When the :class:`_engine.FrozenResult`
1569 is retrieved from a cache, it can be called any number of times where
1570 it will produce a new :class:`_engine.Result` object each time
1571 against its stored set of rows.
1572
1573 .. seealso::
1574
1575 :ref:`do_orm_execute_re_executing` - example usage within the
1576 ORM to implement a result-set cache.
1577
1578 """
1579
1580 return FrozenResult(self)
1581
1582 def merge(
1583 self, *others: Result[Unpack[TupleAny]]
1584 ) -> MergedResult[Unpack[TupleAny]]:
1585 """Merge this :class:`_engine.Result` with other compatible result
1586 objects.
1587
1588 The object returned is an instance of :class:`_engine.MergedResult`,
1589 which will be composed of iterators from the given result
1590 objects.
1591
1592 The new result will use the metadata from this result object.
1593 The subsequent result objects must be against an identical
1594 set of result / cursor metadata, otherwise the behavior is
1595 undefined.
1596
1597 """
1598 return MergedResult(self._metadata, (self,) + others)
1599
1600
1601class FilterResult(ResultInternal[_R]):
1602 """A wrapper for a :class:`_engine.Result` that returns objects other than
1603 :class:`_engine.Row` objects, such as dictionaries or scalar objects.
1604
1605 :class:`_engine.FilterResult` is the common base for additional result
1606 APIs including :class:`_engine.MappingResult`,
1607 :class:`_engine.ScalarResult` and :class:`_engine.AsyncResult`.
1608
1609 """
1610
1611 __slots__ = (
1612 "_real_result",
1613 "_post_creational_filter",
1614 "_metadata",
1615 "_unique_filter_state",
1616 "__dict__",
1617 )
1618
1619 _post_creational_filter: Optional[Callable[[Any], Any]]
1620
1621 _real_result: Result[Unpack[TupleAny]]
1622
1623 def __enter__(self) -> Self:
1624 return self
1625
1626 def __exit__(self, type_: Any, value: Any, traceback: Any) -> None:
1627 self._real_result.__exit__(type_, value, traceback)
1628
1629 @_generative
1630 def yield_per(self, num: int) -> Self:
1631 """Configure the row-fetching strategy to fetch ``num`` rows at a time.
1632
1633 The :meth:`_engine.FilterResult.yield_per` method is a pass through
1634 to the :meth:`_engine.Result.yield_per` method. See that method's
1635 documentation for usage notes.
1636
1637 .. versionadded:: 1.4.40 - added :meth:`_engine.FilterResult.yield_per`
1638 so that the method is available on all result set implementations
1639
1640 .. seealso::
1641
1642 :ref:`engine_stream_results` - describes Core behavior for
1643 :meth:`_engine.Result.yield_per`
1644
1645 :ref:`orm_queryguide_yield_per` - in the :ref:`queryguide_toplevel`
1646
1647 """
1648 self._real_result = self._real_result.yield_per(num)
1649 return self
1650
1651 def _soft_close(self, hard: bool = False) -> None:
1652 self._real_result._soft_close(hard=hard)
1653
1654 @property
1655 def _soft_closed(self) -> bool:
1656 return self._real_result._soft_closed
1657
1658 @property
1659 def closed(self) -> bool:
1660 """Return ``True`` if the underlying :class:`_engine.Result` reports
1661 closed
1662
1663 .. versionadded:: 1.4.43
1664
1665 """
1666 return self._real_result.closed
1667
1668 def close(self) -> None:
1669 """Close this :class:`_engine.FilterResult`.
1670
1671 .. versionadded:: 1.4.43
1672
1673 """
1674 self._real_result.close()
1675
1676 @property
1677 def _attributes(self) -> Dict[Any, Any]:
1678 return self._real_result._attributes
1679
1680 def _fetchiter_impl(
1681 self,
1682 ) -> Iterator[_InterimRowType[Row[Unpack[TupleAny]]]]:
1683 return self._real_result._fetchiter_impl()
1684
1685 def _fetchone_impl(
1686 self, hard_close: bool = False
1687 ) -> Optional[_InterimRowType[Row[Unpack[TupleAny]]]]:
1688 return self._real_result._fetchone_impl(hard_close=hard_close)
1689
1690 def _fetchall_impl(
1691 self,
1692 ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]:
1693 return self._real_result._fetchall_impl()
1694
1695 def _fetchmany_impl(
1696 self, size: Optional[int] = None
1697 ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]:
1698 return self._real_result._fetchmany_impl(size=size)
1699
1700
1701class ScalarResult(FilterResult[_R]):
1702 """A wrapper for a :class:`_engine.Result` that returns scalar values
1703 rather than :class:`_row.Row` values.
1704
1705 The :class:`_engine.ScalarResult` object is acquired by calling the
1706 :meth:`_engine.Result.scalars` method.
1707
1708 A special limitation of :class:`_engine.ScalarResult` is that it has
1709 no ``fetchone()`` method; since the semantics of ``fetchone()`` are that
1710 the ``None`` value indicates no more results, this is not compatible
1711 with :class:`_engine.ScalarResult` since there is no way to distinguish
1712 between ``None`` as a row value versus ``None`` as an indicator. Use
1713 ``next(result)`` to receive values individually.
1714
1715 """
1716
1717 __slots__ = ()
1718
1719 _generate_rows = False
1720
1721 _post_creational_filter: Optional[Callable[[Any], Any]]
1722
1723 def __init__(
1724 self, real_result: Result[Unpack[TupleAny]], index: _KeyIndexType
1725 ):
1726 self._real_result = real_result
1727
1728 if real_result._source_supports_scalars:
1729 self._metadata = real_result._metadata
1730 self._post_creational_filter = None
1731 else:
1732 self._metadata = real_result._metadata._reduce([index])
1733 self._post_creational_filter = operator.itemgetter(0)
1734
1735 self._unique_filter_state = real_result._unique_filter_state
1736
1737 def unique(self, strategy: Optional[_UniqueFilterType] = None) -> Self:
1738 """Apply unique filtering to the objects returned by this
1739 :class:`_engine.ScalarResult`.
1740
1741 See :meth:`_engine.Result.unique` for usage details.
1742
1743 """
1744 self._unique_filter_state = (set(), strategy)
1745 return self
1746
1747 def partitions(self, size: Optional[int] = None) -> Iterator[Sequence[_R]]:
1748 """Iterate through sub-lists of elements of the size given.
1749
1750 Equivalent to :meth:`_engine.Result.partitions` except that
1751 scalar values, rather than :class:`_engine.Row` objects,
1752 are returned.
1753
1754 """
1755
1756 getter = self._manyrow_getter
1757
1758 while True:
1759 partition = getter(self, size)
1760 if partition:
1761 yield partition
1762 else:
1763 break
1764
1765 def fetchall(self) -> Sequence[_R]:
1766 """A synonym for the :meth:`_engine.ScalarResult.all` method."""
1767
1768 return self._allrows()
1769
1770 def fetchmany(self, size: Optional[int] = None) -> Sequence[_R]:
1771 """Fetch many objects.
1772
1773 Equivalent to :meth:`_engine.Result.fetchmany` except that
1774 scalar values, rather than :class:`_engine.Row` objects,
1775 are returned.
1776
1777 """
1778 return self._manyrow_getter(self, size)
1779
1780 def all(self) -> Sequence[_R]:
1781 """Return all scalar values in a sequence.
1782
1783 Equivalent to :meth:`_engine.Result.all` except that
1784 scalar values, rather than :class:`_engine.Row` objects,
1785 are returned.
1786
1787 """
1788 return self._allrows()
1789
1790 def __iter__(self) -> Iterator[_R]:
1791 return self._iter_impl()
1792
1793 def __next__(self) -> _R:
1794 return self._next_impl()
1795
1796 def first(self) -> Optional[_R]:
1797 """Fetch the first object or ``None`` if no object is present.
1798
1799 Equivalent to :meth:`_engine.Result.first` except that
1800 scalar values, rather than :class:`_engine.Row` objects,
1801 are returned.
1802
1803
1804 """
1805 return self._only_one_row(
1806 raise_for_second_row=False, raise_for_none=False, scalar=False
1807 )
1808
1809 def one_or_none(self) -> Optional[_R]:
1810 """Return at most one object or raise an exception.
1811
1812 Equivalent to :meth:`_engine.Result.one_or_none` except that
1813 scalar values, rather than :class:`_engine.Row` objects,
1814 are returned.
1815
1816 """
1817 return self._only_one_row(
1818 raise_for_second_row=True, raise_for_none=False, scalar=False
1819 )
1820
1821 def one(self) -> _R:
1822 """Return exactly one object or raise an exception.
1823
1824 Equivalent to :meth:`_engine.Result.one` except that
1825 scalar values, rather than :class:`_engine.Row` objects,
1826 are returned.
1827
1828 """
1829 return self._only_one_row(
1830 raise_for_second_row=True, raise_for_none=True, scalar=False
1831 )
1832
1833
1834class TupleResult(FilterResult[_R], util.TypingOnly):
1835 """A :class:`_engine.Result` that's typed as returning plain
1836 Python tuples instead of rows.
1837
1838 Since :class:`_engine.Row` acts like a tuple in every way already,
1839 this class is a typing only class, regular :class:`_engine.Result` is
1840 still used at runtime.
1841
1842 """
1843
1844 __slots__ = ()
1845
1846 if TYPE_CHECKING:
1847
1848 def partitions(
1849 self, size: Optional[int] = None
1850 ) -> Iterator[Sequence[_R]]:
1851 """Iterate through sub-lists of elements of the size given.
1852
1853 Equivalent to :meth:`_engine.Result.partitions` except that
1854 tuple values, rather than :class:`_engine.Row` objects,
1855 are returned.
1856
1857 """
1858 ...
1859
1860 def fetchone(self) -> Optional[_R]:
1861 """Fetch one tuple.
1862
1863 Equivalent to :meth:`_engine.Result.fetchone` except that
1864 tuple values, rather than :class:`_engine.Row`
1865 objects, are returned.
1866
1867 """
1868 ...
1869
1870 def fetchall(self) -> Sequence[_R]:
1871 """A synonym for the :meth:`_engine.ScalarResult.all` method."""
1872 ...
1873
1874 def fetchmany(self, size: Optional[int] = None) -> Sequence[_R]:
1875 """Fetch many objects.
1876
1877 Equivalent to :meth:`_engine.Result.fetchmany` except that
1878 tuple values, rather than :class:`_engine.Row` objects,
1879 are returned.
1880
1881 """
1882 ...
1883
1884 def all(self) -> Sequence[_R]: # noqa: A001
1885 """Return all scalar values in a sequence.
1886
1887 Equivalent to :meth:`_engine.Result.all` except that
1888 tuple values, rather than :class:`_engine.Row` objects,
1889 are returned.
1890
1891 """
1892 ...
1893
1894 def __iter__(self) -> Iterator[_R]: ...
1895
1896 def __next__(self) -> _R: ...
1897
1898 def first(self) -> Optional[_R]:
1899 """Fetch the first object or ``None`` if no object is present.
1900
1901 Equivalent to :meth:`_engine.Result.first` except that
1902 tuple values, rather than :class:`_engine.Row` objects,
1903 are returned.
1904
1905
1906 """
1907 ...
1908
1909 def one_or_none(self) -> Optional[_R]:
1910 """Return at most one object or raise an exception.
1911
1912 Equivalent to :meth:`_engine.Result.one_or_none` except that
1913 tuple values, rather than :class:`_engine.Row` objects,
1914 are returned.
1915
1916 """
1917 ...
1918
1919 def one(self) -> _R:
1920 """Return exactly one object or raise an exception.
1921
1922 Equivalent to :meth:`_engine.Result.one` except that
1923 tuple values, rather than :class:`_engine.Row` objects,
1924 are returned.
1925
1926 """
1927 ...
1928
1929 @overload
1930 def scalar_one(self: TupleResult[Tuple[_T]]) -> _T: ...
1931
1932 @overload
1933 def scalar_one(self) -> Any: ...
1934
1935 def scalar_one(self) -> Any:
1936 """Return exactly one scalar result or raise an exception.
1937
1938 This is equivalent to calling :meth:`_engine.Result.scalars`
1939 and then :meth:`_engine.ScalarResult.one`.
1940
1941 .. seealso::
1942
1943 :meth:`_engine.ScalarResult.one`
1944
1945 :meth:`_engine.Result.scalars`
1946
1947 """
1948 ...
1949
1950 @overload
1951 def scalar_one_or_none(
1952 self: TupleResult[Tuple[_T]],
1953 ) -> Optional[_T]: ...
1954
1955 @overload
1956 def scalar_one_or_none(self) -> Optional[Any]: ...
1957
1958 def scalar_one_or_none(self) -> Optional[Any]:
1959 """Return exactly one or no scalar result.
1960
1961 This is equivalent to calling :meth:`_engine.Result.scalars`
1962 and then :meth:`_engine.ScalarResult.one_or_none`.
1963
1964 .. seealso::
1965
1966 :meth:`_engine.ScalarResult.one_or_none`
1967
1968 :meth:`_engine.Result.scalars`
1969
1970 """
1971 ...
1972
1973 @overload
1974 def scalar(self: TupleResult[Tuple[_T]]) -> Optional[_T]: ...
1975
1976 @overload
1977 def scalar(self) -> Any: ...
1978
1979 def scalar(self) -> Any:
1980 """Fetch the first column of the first row, and close the result
1981 set.
1982
1983 Returns ``None`` if there are no rows to fetch.
1984
1985 No validation is performed to test if additional rows remain.
1986
1987 After calling this method, the object is fully closed,
1988 e.g. the :meth:`_engine.CursorResult.close`
1989 method will have been called.
1990
1991 :return: a Python scalar value , or ``None`` if no rows remain.
1992
1993 """
1994 ...
1995
1996
1997class MappingResult(_WithKeys, FilterResult[RowMapping]):
1998 """A wrapper for a :class:`_engine.Result` that returns dictionary values
1999 rather than :class:`_engine.Row` values.
2000
2001 The :class:`_engine.MappingResult` object is acquired by calling the
2002 :meth:`_engine.Result.mappings` method.
2003
2004 """
2005
2006 __slots__ = ()
2007
2008 _generate_rows = True
2009
2010 _post_creational_filter = operator.attrgetter("_mapping")
2011
2012 def __init__(self, result: Result[Unpack[TupleAny]]):
2013 self._real_result = result
2014 self._unique_filter_state = result._unique_filter_state
2015 self._metadata = result._metadata
2016 if result._source_supports_scalars:
2017 self._metadata = self._metadata._reduce([0])
2018
2019 def unique(self, strategy: Optional[_UniqueFilterType] = None) -> Self:
2020 """Apply unique filtering to the objects returned by this
2021 :class:`_engine.MappingResult`.
2022
2023 See :meth:`_engine.Result.unique` for usage details.
2024
2025 """
2026 self._unique_filter_state = (set(), strategy)
2027 return self
2028
2029 def columns(self, *col_expressions: _KeyIndexType) -> Self:
2030 r"""Establish the columns that should be returned in each row."""
2031 return self._column_slices(col_expressions)
2032
2033 def partitions(
2034 self, size: Optional[int] = None
2035 ) -> Iterator[Sequence[RowMapping]]:
2036 """Iterate through sub-lists of elements of the size given.
2037
2038 Equivalent to :meth:`_engine.Result.partitions` except that
2039 :class:`_engine.RowMapping` values, rather than :class:`_engine.Row`
2040 objects, are returned.
2041
2042 """
2043
2044 getter = self._manyrow_getter
2045
2046 while True:
2047 partition = getter(self, size)
2048 if partition:
2049 yield partition
2050 else:
2051 break
2052
2053 def fetchall(self) -> Sequence[RowMapping]:
2054 """A synonym for the :meth:`_engine.MappingResult.all` method."""
2055
2056 return self._allrows()
2057
2058 def fetchone(self) -> Optional[RowMapping]:
2059 """Fetch one object.
2060
2061 Equivalent to :meth:`_engine.Result.fetchone` except that
2062 :class:`_engine.RowMapping` values, rather than :class:`_engine.Row`
2063 objects, are returned.
2064
2065 """
2066
2067 row = self._onerow_getter(self)
2068 if row is _NO_ROW:
2069 return None
2070 else:
2071 return row
2072
2073 def fetchmany(self, size: Optional[int] = None) -> Sequence[RowMapping]:
2074 """Fetch many objects.
2075
2076 Equivalent to :meth:`_engine.Result.fetchmany` except that
2077 :class:`_engine.RowMapping` values, rather than :class:`_engine.Row`
2078 objects, are returned.
2079
2080 """
2081
2082 return self._manyrow_getter(self, size)
2083
2084 def all(self) -> Sequence[RowMapping]:
2085 """Return all scalar values in a sequence.
2086
2087 Equivalent to :meth:`_engine.Result.all` except that
2088 :class:`_engine.RowMapping` values, rather than :class:`_engine.Row`
2089 objects, are returned.
2090
2091 """
2092
2093 return self._allrows()
2094
2095 def __iter__(self) -> Iterator[RowMapping]:
2096 return self._iter_impl()
2097
2098 def __next__(self) -> RowMapping:
2099 return self._next_impl()
2100
2101 def first(self) -> Optional[RowMapping]:
2102 """Fetch the first object or ``None`` if no object is present.
2103
2104 Equivalent to :meth:`_engine.Result.first` except that
2105 :class:`_engine.RowMapping` values, rather than :class:`_engine.Row`
2106 objects, are returned.
2107
2108
2109 """
2110 return self._only_one_row(
2111 raise_for_second_row=False, raise_for_none=False, scalar=False
2112 )
2113
2114 def one_or_none(self) -> Optional[RowMapping]:
2115 """Return at most one object or raise an exception.
2116
2117 Equivalent to :meth:`_engine.Result.one_or_none` except that
2118 :class:`_engine.RowMapping` values, rather than :class:`_engine.Row`
2119 objects, are returned.
2120
2121 """
2122 return self._only_one_row(
2123 raise_for_second_row=True, raise_for_none=False, scalar=False
2124 )
2125
2126 def one(self) -> RowMapping:
2127 """Return exactly one object or raise an exception.
2128
2129 Equivalent to :meth:`_engine.Result.one` except that
2130 :class:`_engine.RowMapping` values, rather than :class:`_engine.Row`
2131 objects, are returned.
2132
2133 """
2134 return self._only_one_row(
2135 raise_for_second_row=True, raise_for_none=True, scalar=False
2136 )
2137
2138
2139class FrozenResult(Generic[Unpack[_Ts]]):
2140 """Represents a :class:`_engine.Result` object in a "frozen" state suitable
2141 for caching.
2142
2143 The :class:`_engine.FrozenResult` object is returned from the
2144 :meth:`_engine.Result.freeze` method of any :class:`_engine.Result`
2145 object.
2146
2147 A new iterable :class:`_engine.Result` object is generated from a fixed
2148 set of data each time the :class:`_engine.FrozenResult` is invoked as
2149 a callable::
2150
2151
2152 result = connection.execute(query)
2153
2154 frozen = result.freeze()
2155
2156 unfrozen_result_one = frozen()
2157
2158 for row in unfrozen_result_one:
2159 print(row)
2160
2161 unfrozen_result_two = frozen()
2162 rows = unfrozen_result_two.all()
2163
2164 # ... etc
2165
2166 .. versionadded:: 1.4
2167
2168 .. seealso::
2169
2170 :ref:`do_orm_execute_re_executing` - example usage within the
2171 ORM to implement a result-set cache.
2172
2173 :func:`_orm.loading.merge_frozen_result` - ORM function to merge
2174 a frozen result back into a :class:`_orm.Session`.
2175
2176 """
2177
2178 data: Sequence[Any]
2179
2180 def __init__(self, result: Result[Unpack[_Ts]]):
2181 self.metadata = result._metadata._for_freeze()
2182 self._source_supports_scalars = result._source_supports_scalars
2183 self._attributes = result._attributes
2184
2185 if self._source_supports_scalars:
2186 self.data = list(result._raw_row_iterator())
2187 else:
2188 self.data = result.fetchall()
2189
2190 def rewrite_rows(self) -> Sequence[Sequence[Any]]:
2191 if self._source_supports_scalars:
2192 return [[elem] for elem in self.data]
2193 else:
2194 return [list(row) for row in self.data]
2195
2196 def with_new_rows(
2197 self, tuple_data: Sequence[Row[Unpack[_Ts]]]
2198 ) -> FrozenResult[Unpack[_Ts]]:
2199 fr = FrozenResult.__new__(FrozenResult)
2200 fr.metadata = self.metadata
2201 fr._attributes = self._attributes
2202 fr._source_supports_scalars = self._source_supports_scalars
2203
2204 if self._source_supports_scalars:
2205 fr.data = [d[0] for d in tuple_data] # type: ignore[misc]
2206 else:
2207 fr.data = tuple_data
2208 return fr
2209
2210 def __call__(self) -> Result[Unpack[_Ts]]:
2211 result: IteratorResult[Unpack[_Ts]] = IteratorResult(
2212 self.metadata, iter(self.data)
2213 )
2214 result._attributes = self._attributes
2215 result._source_supports_scalars = self._source_supports_scalars
2216 return result
2217
2218
2219class IteratorResult(Result[Unpack[_Ts]]):
2220 """A :class:`_engine.Result` that gets data from a Python iterator of
2221 :class:`_engine.Row` objects or similar row-like data.
2222
2223 .. versionadded:: 1.4
2224
2225 """
2226
2227 _hard_closed = False
2228 _soft_closed = False
2229
2230 def __init__(
2231 self,
2232 cursor_metadata: ResultMetaData,
2233 iterator: Iterator[_InterimSupportsScalarsRowType],
2234 raw: Optional[Result[Any]] = None,
2235 _source_supports_scalars: bool = False,
2236 ):
2237 self._metadata = cursor_metadata
2238 self.iterator = iterator
2239 self.raw = raw
2240 self._source_supports_scalars = _source_supports_scalars
2241
2242 @property
2243 def closed(self) -> bool:
2244 """Return ``True`` if this :class:`_engine.IteratorResult` has
2245 been closed
2246
2247 .. versionadded:: 1.4.43
2248
2249 """
2250 return self._hard_closed
2251
2252 def _soft_close(self, hard: bool = False, **kw: Any) -> None:
2253 if hard:
2254 self._hard_closed = True
2255 if self.raw is not None:
2256 self.raw._soft_close(hard=hard, **kw)
2257 self.iterator = iter([])
2258 self._reset_memoizations()
2259 self._soft_closed = True
2260
2261 def _raise_hard_closed(self) -> NoReturn:
2262 raise exc.ResourceClosedError("This result object is closed.")
2263
2264 def _raw_row_iterator(self) -> Iterator[_RowData]:
2265 return self.iterator
2266
2267 def _fetchiter_impl(self) -> Iterator[_InterimSupportsScalarsRowType]:
2268 if self._hard_closed:
2269 self._raise_hard_closed()
2270 return self.iterator
2271
2272 def _fetchone_impl(
2273 self, hard_close: bool = False
2274 ) -> Optional[_InterimRowType[Row[Unpack[TupleAny]]]]:
2275 if self._hard_closed:
2276 self._raise_hard_closed()
2277
2278 row = next(self.iterator, _NO_ROW)
2279 if row is _NO_ROW:
2280 self._soft_close(hard=hard_close)
2281 return None
2282 else:
2283 return row
2284
2285 def _fetchall_impl(
2286 self,
2287 ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]:
2288 if self._hard_closed:
2289 self._raise_hard_closed()
2290 try:
2291 return list(self.iterator)
2292 finally:
2293 self._soft_close()
2294
2295 def _fetchmany_impl(
2296 self, size: Optional[int] = None
2297 ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]:
2298 if self._hard_closed:
2299 self._raise_hard_closed()
2300
2301 return list(itertools.islice(self.iterator, 0, size))
2302
2303
2304def null_result() -> IteratorResult[Any]:
2305 return IteratorResult(SimpleResultMetaData([]), iter([]))
2306
2307
2308class ChunkedIteratorResult(IteratorResult[Unpack[_Ts]]):
2309 """An :class:`_engine.IteratorResult` that works from an
2310 iterator-producing callable.
2311
2312 The given ``chunks`` argument is a function that is given a number of rows
2313 to return in each chunk, or ``None`` for all rows. The function should
2314 then return an un-consumed iterator of lists, each list of the requested
2315 size.
2316
2317 The function can be called at any time again, in which case it should
2318 continue from the same result set but adjust the chunk size as given.
2319
2320 .. versionadded:: 1.4
2321
2322 """
2323
2324 def __init__(
2325 self,
2326 cursor_metadata: ResultMetaData,
2327 chunks: Callable[
2328 [Optional[int]], Iterator[Sequence[_InterimRowType[_R]]]
2329 ],
2330 source_supports_scalars: bool = False,
2331 raw: Optional[Result[Any]] = None,
2332 dynamic_yield_per: bool = False,
2333 ):
2334 self._metadata = cursor_metadata
2335 self.chunks = chunks
2336 self._source_supports_scalars = source_supports_scalars
2337 self.raw = raw
2338 self.iterator = itertools.chain.from_iterable(self.chunks(None))
2339 self.dynamic_yield_per = dynamic_yield_per
2340
2341 @_generative
2342 def yield_per(self, num: int) -> Self:
2343 # TODO: this throws away the iterator which may be holding
2344 # onto a chunk. the yield_per cannot be changed once any
2345 # rows have been fetched. either find a way to enforce this,
2346 # or we can't use itertools.chain and will instead have to
2347 # keep track.
2348
2349 self._yield_per = num
2350 self.iterator = itertools.chain.from_iterable(self.chunks(num))
2351 return self
2352
2353 def _soft_close(self, hard: bool = False, **kw: Any) -> None:
2354 super()._soft_close(hard=hard, **kw)
2355 self.chunks = lambda size: [] # type: ignore
2356
2357 def _fetchmany_impl(
2358 self, size: Optional[int] = None
2359 ) -> List[_InterimRowType[Row[Unpack[TupleAny]]]]:
2360 if self.dynamic_yield_per:
2361 self.iterator = itertools.chain.from_iterable(self.chunks(size))
2362 return super()._fetchmany_impl(size=size)
2363
2364
2365class MergedResult(IteratorResult[Unpack[_Ts]]):
2366 """A :class:`_engine.Result` that is merged from any number of
2367 :class:`_engine.Result` objects.
2368
2369 Returned by the :meth:`_engine.Result.merge` method.
2370
2371 .. versionadded:: 1.4
2372
2373 """
2374
2375 closed = False
2376 rowcount: Optional[int]
2377
2378 def __init__(
2379 self,
2380 cursor_metadata: ResultMetaData,
2381 results: Sequence[Result[Unpack[_Ts]]],
2382 ):
2383 self._results = results
2384 super().__init__(
2385 cursor_metadata,
2386 itertools.chain.from_iterable(
2387 r._raw_row_iterator() for r in results
2388 ),
2389 )
2390
2391 self._unique_filter_state = results[0]._unique_filter_state
2392 self._yield_per = results[0]._yield_per
2393
2394 # going to try something w/ this in next rev
2395 self._source_supports_scalars = results[0]._source_supports_scalars
2396
2397 self._attributes = self._attributes.merge_with(
2398 *[r._attributes for r in results]
2399 )
2400
2401 def _soft_close(self, hard: bool = False, **kw: Any) -> None:
2402 for r in self._results:
2403 r._soft_close(hard=hard, **kw)
2404 if hard:
2405 self.closed = True