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