codekingpro/portable-devtools
114k
1# engine/cursor.py
2# Copyright (C) 2005-2024 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# mypy: allow-untyped-defs, allow-untyped-calls
8
9"""Define cursor-specific result set constructs including
10:class:`.CursorResult`."""
11
12
13from __future__ import annotations
14
15import collections
16import functools
17import operator
18import typing
19from typing import Any
20from typing import cast
21from typing import ClassVar
22from typing import Dict
23from typing import Iterator
24from typing import List
25from typing import Mapping
26from typing import NoReturn
27from typing import Optional
28from typing import Sequence
29from typing import Tuple
30from typing import TYPE_CHECKING
31from typing import TypeVar
32from typing import Union
33
34from .result import IteratorResult
35from .result import MergedResult
36from .result import Result
37from .result import ResultMetaData
38from .result import SimpleResultMetaData
39from .result import tuplegetter
40from .row import Row
41from .. import exc
42from .. import util
43from ..sql import elements
44from ..sql import sqltypes
45from ..sql import util as sql_util
46from ..sql.base import _generative
47from ..sql.compiler import ResultColumnsEntry
48from ..sql.compiler import RM_NAME
49from ..sql.compiler import RM_OBJECTS
50from ..sql.compiler import RM_RENDERED_NAME
51from ..sql.compiler import RM_TYPE
52from ..sql.type_api import TypeEngine
53from ..util import compat
54from ..util.typing import Literal
55from ..util.typing import Self
56
57
58if typing.TYPE_CHECKING:
59 from .base import Connection
60 from .default import DefaultExecutionContext
61 from .interfaces import _DBAPICursorDescription
62 from .interfaces import DBAPICursor
63 from .interfaces import Dialect
64 from .interfaces import ExecutionContext
65 from .result import _KeyIndexType
66 from .result import _KeyMapRecType
67 from .result import _KeyMapType
68 from .result import _KeyType
69 from .result import _ProcessorsType
70 from .result import _TupleGetterType
71 from ..sql.type_api import _ResultProcessorType
72
73
74_T = TypeVar("_T", bound=Any)
75
76
77# metadata entry tuple indexes.
78# using raw tuple is faster than namedtuple.
79# these match up to the positions in
80# _CursorKeyMapRecType
81MD_INDEX: Literal[0] = 0
82"""integer index in cursor.description
83
84"""
85
86MD_RESULT_MAP_INDEX: Literal[1] = 1
87"""integer index in compiled._result_columns"""
88
89MD_OBJECTS: Literal[2] = 2
90"""other string keys and ColumnElement obj that can match.
91
92This comes from compiler.RM_OBJECTS / compiler.ResultColumnsEntry.objects
93
94"""
95
96MD_LOOKUP_KEY: Literal[3] = 3
97"""string key we usually expect for key-based lookup
98
99this comes from compiler.RM_NAME / compiler.ResultColumnsEntry.name
100"""
101
102
103MD_RENDERED_NAME: Literal[4] = 4
104"""name that is usually in cursor.description
105
106this comes from compiler.RENDERED_NAME / compiler.ResultColumnsEntry.keyname
107"""
108
109
110MD_PROCESSOR: Literal[5] = 5
111"""callable to process a result value into a row"""
112
113MD_UNTRANSLATED: Literal[6] = 6
114"""raw name from cursor.description"""
115
116
117_CursorKeyMapRecType = Tuple[
118 Optional[int], # MD_INDEX, None means the record is ambiguously named
119 int, # MD_RESULT_MAP_INDEX
120 List[Any], # MD_OBJECTS
121 str, # MD_LOOKUP_KEY
122 str, # MD_RENDERED_NAME
123 Optional["_ResultProcessorType[Any]"], # MD_PROCESSOR
124 Optional[str], # MD_UNTRANSLATED
125]
126
127_CursorKeyMapType = Mapping["_KeyType", _CursorKeyMapRecType]
128
129# same as _CursorKeyMapRecType except the MD_INDEX value is definitely
130# not None
131_NonAmbigCursorKeyMapRecType = Tuple[
132 int,
133 int,
134 List[Any],
135 str,
136 str,
137 Optional["_ResultProcessorType[Any]"],
138 str,
139]
140
141
142class CursorResultMetaData(ResultMetaData):
143 """Result metadata for DBAPI cursors."""
144
145 __slots__ = (
146 "_keymap",
147 "_processors",
148 "_keys",
149 "_keymap_by_result_column_idx",
150 "_tuplefilter",
151 "_translated_indexes",
152 "_safe_for_cache",
153 "_unpickled",
154 "_key_to_index",
155 # don't need _unique_filters support here for now. Can be added
156 # if a need arises.
157 )
158
159 _keymap: _CursorKeyMapType
160 _processors: _ProcessorsType
161 _keymap_by_result_column_idx: Optional[Dict[int, _KeyMapRecType]]
162 _unpickled: bool
163 _safe_for_cache: bool
164 _translated_indexes: Optional[List[int]]
165
166 returns_rows: ClassVar[bool] = True
167
168 def _has_key(self, key: Any) -> bool:
169 return key in self._keymap
170
171 def _for_freeze(self) -> ResultMetaData:
172 return SimpleResultMetaData(
173 self._keys,
174 extra=[self._keymap[key][MD_OBJECTS] for key in self._keys],
175 )
176
177 def _make_new_metadata(
178 self,
179 *,
180 unpickled: bool,
181 processors: _ProcessorsType,
182 keys: Sequence[str],
183 keymap: _KeyMapType,
184 tuplefilter: Optional[_TupleGetterType],
185 translated_indexes: Optional[List[int]],
186 safe_for_cache: bool,
187 keymap_by_result_column_idx: Any,
188 ) -> CursorResultMetaData:
189 new_obj = self.__class__.__new__(self.__class__)
190 new_obj._unpickled = unpickled
191 new_obj._processors = processors
192 new_obj._keys = keys
193 new_obj._keymap = keymap
194 new_obj._tuplefilter = tuplefilter
195 new_obj._translated_indexes = translated_indexes
196 new_obj._safe_for_cache = safe_for_cache
197 new_obj._keymap_by_result_column_idx = keymap_by_result_column_idx
198 new_obj._key_to_index = self._make_key_to_index(keymap, MD_INDEX)
199 return new_obj
200
201 def _remove_processors(self) -> CursorResultMetaData:
202 assert not self._tuplefilter
203 return self._make_new_metadata(
204 unpickled=self._unpickled,
205 processors=[None] * len(self._processors),
206 tuplefilter=None,
207 translated_indexes=None,
208 keymap={
209 key: value[0:5] + (None,) + value[6:]
210 for key, value in self._keymap.items()
211 },
212 keys=self._keys,
213 safe_for_cache=self._safe_for_cache,
214 keymap_by_result_column_idx=self._keymap_by_result_column_idx,
215 )
216
217 def _splice_horizontally(
218 self, other: CursorResultMetaData
219 ) -> CursorResultMetaData:
220 assert not self._tuplefilter
221
222 keymap = dict(self._keymap)
223 offset = len(self._keys)
224 keymap.update(
225 {
226 key: (
227 # int index should be None for ambiguous key
228 (
229 value[0] + offset
230 if value[0] is not None and key not in keymap
231 else None
232 ),
233 value[1] + offset,
234 *value[2:],
235 )
236 for key, value in other._keymap.items()
237 }
238 )
239 return self._make_new_metadata(
240 unpickled=self._unpickled,
241 processors=self._processors + other._processors, # type: ignore
242 tuplefilter=None,
243 translated_indexes=None,
244 keys=self._keys + other._keys, # type: ignore
245 keymap=keymap,
246 safe_for_cache=self._safe_for_cache,
247 keymap_by_result_column_idx={
248 metadata_entry[MD_RESULT_MAP_INDEX]: metadata_entry
249 for metadata_entry in keymap.values()
250 },
251 )
252
253 def _reduce(self, keys: Sequence[_KeyIndexType]) -> ResultMetaData:
254 recs = list(self._metadata_for_keys(keys))
255
256 indexes = [rec[MD_INDEX] for rec in recs]
257 new_keys: List[str] = [rec[MD_LOOKUP_KEY] for rec in recs]
258
259 if self._translated_indexes:
260 indexes = [self._translated_indexes[idx] for idx in indexes]
261 tup = tuplegetter(*indexes)
262 new_recs = [(index,) + rec[1:] for index, rec in enumerate(recs)]
263
264 keymap = {rec[MD_LOOKUP_KEY]: rec for rec in new_recs}
265 # TODO: need unit test for:
266 # result = connection.execute("raw sql, no columns").scalars()
267 # without the "or ()" it's failing because MD_OBJECTS is None
268 keymap.update(
269 (e, new_rec)
270 for new_rec in new_recs
271 for e in new_rec[MD_OBJECTS] or ()
272 )
273
274 return self._make_new_metadata(
275 unpickled=self._unpickled,
276 processors=self._processors,
277 keys=new_keys,
278 tuplefilter=tup,
279 translated_indexes=indexes,
280 keymap=keymap, # type: ignore[arg-type]
281 safe_for_cache=self._safe_for_cache,
282 keymap_by_result_column_idx=self._keymap_by_result_column_idx,
283 )
284
285 def _adapt_to_context(self, context: ExecutionContext) -> ResultMetaData:
286 """When using a cached Compiled construct that has a _result_map,
287 for a new statement that used the cached Compiled, we need to ensure
288 the keymap has the Column objects from our new statement as keys.
289 So here we rewrite keymap with new entries for the new columns
290 as matched to those of the cached statement.
291
292 """
293
294 if not context.compiled or not context.compiled._result_columns:
295 return self
296
297 compiled_statement = context.compiled.statement
298 invoked_statement = context.invoked_statement
299
300 if TYPE_CHECKING:
301 assert isinstance(invoked_statement, elements.ClauseElement)
302
303 if compiled_statement is invoked_statement:
304 return self
305
306 assert invoked_statement is not None
307
308 # this is the most common path for Core statements when
309 # caching is used. In ORM use, this codepath is not really used
310 # as the _result_disable_adapt_to_context execution option is
311 # set by the ORM.
312
313 # make a copy and add the columns from the invoked statement
314 # to the result map.
315
316 keymap_by_position = self._keymap_by_result_column_idx
317
318 if keymap_by_position is None:
319 # first retrival from cache, this map will not be set up yet,
320 # initialize lazily
321 keymap_by_position = self._keymap_by_result_column_idx = {
322 metadata_entry[MD_RESULT_MAP_INDEX]: metadata_entry
323 for metadata_entry in self._keymap.values()
324 }
325
326 assert not self._tuplefilter
327 return self._make_new_metadata(
328 keymap=compat.dict_union(
329 self._keymap,
330 {
331 new: keymap_by_position[idx]
332 for idx, new in enumerate(
333 invoked_statement._all_selected_columns
334 )
335 if idx in keymap_by_position
336 },
337 ),
338 unpickled=self._unpickled,
339 processors=self._processors,
340 tuplefilter=None,
341 translated_indexes=None,
342 keys=self._keys,
343 safe_for_cache=self._safe_for_cache,
344 keymap_by_result_column_idx=self._keymap_by_result_column_idx,
345 )
346
347 def __init__(
348 self,
349 parent: CursorResult[Any],
350 cursor_description: _DBAPICursorDescription,
351 ):
352 context = parent.context
353 self._tuplefilter = None
354 self._translated_indexes = None
355 self._safe_for_cache = self._unpickled = False
356
357 if context.result_column_struct:
358 (
359 result_columns,
360 cols_are_ordered,
361 textual_ordered,
362 ad_hoc_textual,
363 loose_column_name_matching,
364 ) = context.result_column_struct
365 num_ctx_cols = len(result_columns)
366 else:
367 result_columns = cols_are_ordered = ( # type: ignore
368 num_ctx_cols
369 ) = ad_hoc_textual = loose_column_name_matching = (
370 textual_ordered
371 ) = False
372
373 # merge cursor.description with the column info
374 # present in the compiled structure, if any
375 raw = self._merge_cursor_description(
376 context,
377 cursor_description,
378 result_columns,
379 num_ctx_cols,
380 cols_are_ordered,
381 textual_ordered,
382 ad_hoc_textual,
383 loose_column_name_matching,
384 )
385
386 # processors in key order which are used when building up
387 # a row
388 self._processors = [
389 metadata_entry[MD_PROCESSOR] for metadata_entry in raw
390 ]
391
392 # this is used when using this ResultMetaData in a Core-only cache
393 # retrieval context. it's initialized on first cache retrieval
394 # when the _result_disable_adapt_to_context execution option
395 # (which the ORM generally sets) is not set.
396 self._keymap_by_result_column_idx = None
397
398 # for compiled SQL constructs, copy additional lookup keys into
399 # the key lookup map, such as Column objects, labels,
400 # column keys and other names
401 if num_ctx_cols:
402 # keymap by primary string...
403 by_key = {
404 metadata_entry[MD_LOOKUP_KEY]: metadata_entry
405 for metadata_entry in raw
406 }
407
408 if len(by_key) != num_ctx_cols:
409 # if by-primary-string dictionary smaller than
410 # number of columns, assume we have dupes; (this check
411 # is also in place if string dictionary is bigger, as
412 # can occur when '*' was used as one of the compiled columns,
413 # which may or may not be suggestive of dupes), rewrite
414 # dupe records with "None" for index which results in
415 # ambiguous column exception when accessed.
416 #
417 # this is considered to be the less common case as it is not
418 # common to have dupe column keys in a SELECT statement.
419 #
420 # new in 1.4: get the complete set of all possible keys,
421 # strings, objects, whatever, that are dupes across two
422 # different records, first.
423 index_by_key: Dict[Any, Any] = {}
424 dupes = set()
425 for metadata_entry in raw:
426 for key in (metadata_entry[MD_RENDERED_NAME],) + (
427 metadata_entry[MD_OBJECTS] or ()
428 ):
429 idx = metadata_entry[MD_INDEX]
430 # if this key has been associated with more than one
431 # positional index, it's a dupe
432 if index_by_key.setdefault(key, idx) != idx:
433 dupes.add(key)
434
435 # then put everything we have into the keymap excluding only
436 # those keys that are dupes.
437 self._keymap = {
438 obj_elem: metadata_entry
439 for metadata_entry in raw
440 if metadata_entry[MD_OBJECTS]
441 for obj_elem in metadata_entry[MD_OBJECTS]
442 if obj_elem not in dupes
443 }
444
445 # then for the dupe keys, put the "ambiguous column"
446 # record into by_key.
447 by_key.update(
448 {
449 key: (None, None, [], key, key, None, None)
450 for key in dupes
451 }
452 )
453
454 else:
455 # no dupes - copy secondary elements from compiled
456 # columns into self._keymap. this is the most common
457 # codepath for Core / ORM statement executions before the
458 # result metadata is cached
459 self._keymap = {
460 obj_elem: metadata_entry
461 for metadata_entry in raw
462 if metadata_entry[MD_OBJECTS]
463 for obj_elem in metadata_entry[MD_OBJECTS]
464 }
465 # update keymap with primary string names taking
466 # precedence
467 self._keymap.update(by_key)
468 else:
469 # no compiled objects to map, just create keymap by primary string
470 self._keymap = {
471 metadata_entry[MD_LOOKUP_KEY]: metadata_entry
472 for metadata_entry in raw
473 }
474
475 # update keymap with "translated" names. In SQLAlchemy this is a
476 # sqlite only thing, and in fact impacting only extremely old SQLite
477 # versions unlikely to be present in modern Python versions.
478 # however, the pyhive third party dialect is
479 # also using this hook, which means others still might use it as well.
480 # I dislike having this awkward hook here but as long as we need
481 # to use names in cursor.description in some cases we need to have
482 # some hook to accomplish this.
483 if not num_ctx_cols and context._translate_colname:
484 self._keymap.update(
485 {
486 metadata_entry[MD_UNTRANSLATED]: self._keymap[
487 metadata_entry[MD_LOOKUP_KEY]
488 ]
489 for metadata_entry in raw
490 if metadata_entry[MD_UNTRANSLATED]
491 }
492 )
493
494 self._key_to_index = self._make_key_to_index(self._keymap, MD_INDEX)
495
496 def _merge_cursor_description(
497 self,
498 context,
499 cursor_description,
500 result_columns,
501 num_ctx_cols,
502 cols_are_ordered,
503 textual_ordered,
504 ad_hoc_textual,
505 loose_column_name_matching,
506 ):
507 """Merge a cursor.description with compiled result column information.
508
509 There are at least four separate strategies used here, selected
510 depending on the type of SQL construct used to start with.
511
512 The most common case is that of the compiled SQL expression construct,
513 which generated the column names present in the raw SQL string and
514 which has the identical number of columns as were reported by
515 cursor.description. In this case, we assume a 1-1 positional mapping
516 between the entries in cursor.description and the compiled object.
517 This is also the most performant case as we disregard extracting /
518 decoding the column names present in cursor.description since we
519 already have the desired name we generated in the compiled SQL
520 construct.
521
522 The next common case is that of the completely raw string SQL,
523 such as passed to connection.execute(). In this case we have no
524 compiled construct to work with, so we extract and decode the
525 names from cursor.description and index those as the primary
526 result row target keys.
527
528 The remaining fairly common case is that of the textual SQL
529 that includes at least partial column information; this is when
530 we use a :class:`_expression.TextualSelect` construct.
531 This construct may have
532 unordered or ordered column information. In the ordered case, we
533 merge the cursor.description and the compiled construct's information
534 positionally, and warn if there are additional description names
535 present, however we still decode the names in cursor.description
536 as we don't have a guarantee that the names in the columns match
537 on these. In the unordered case, we match names in cursor.description
538 to that of the compiled construct based on name matching.
539 In both of these cases, the cursor.description names and the column
540 expression objects and names are indexed as result row target keys.
541
542 The final case is much less common, where we have a compiled
543 non-textual SQL expression construct, but the number of columns
544 in cursor.description doesn't match what's in the compiled
545 construct. We make the guess here that there might be textual
546 column expressions in the compiled construct that themselves include
547 a comma in them causing them to split. We do the same name-matching
548 as with textual non-ordered columns.
549
550 The name-matched system of merging is the same as that used by
551 SQLAlchemy for all cases up through the 0.9 series. Positional
552 matching for compiled SQL expressions was introduced in 1.0 as a
553 major performance feature, and positional matching for textual
554 :class:`_expression.TextualSelect` objects in 1.1.
555 As name matching is no longer
556 a common case, it was acceptable to factor it into smaller generator-
557 oriented methods that are easier to understand, but incur slightly
558 more performance overhead.
559
560 """
561
562 if (
563 num_ctx_cols
564 and cols_are_ordered
565 and not textual_ordered
566 and num_ctx_cols == len(cursor_description)
567 ):
568 self._keys = [elem[0] for elem in result_columns]
569 # pure positional 1-1 case; doesn't need to read
570 # the names from cursor.description
571
572 # most common case for Core and ORM
573
574 # this metadata is safe to cache because we are guaranteed
575 # to have the columns in the same order for new executions
576 self._safe_for_cache = True
577 return [
578 (
579 idx,
580 idx,
581 rmap_entry[RM_OBJECTS],
582 rmap_entry[RM_NAME],
583 rmap_entry[RM_RENDERED_NAME],
584 context.get_result_processor(
585 rmap_entry[RM_TYPE],
586 rmap_entry[RM_RENDERED_NAME],
587 cursor_description[idx][1],
588 ),
589 None,
590 )
591 for idx, rmap_entry in enumerate(result_columns)
592 ]
593 else:
594 # name-based or text-positional cases, where we need
595 # to read cursor.description names
596
597 if textual_ordered or (
598 ad_hoc_textual and len(cursor_description) == num_ctx_cols
599 ):
600 self._safe_for_cache = True
601 # textual positional case
602 raw_iterator = self._merge_textual_cols_by_position(
603 context, cursor_description, result_columns
604 )
605 elif num_ctx_cols:
606 # compiled SQL with a mismatch of description cols
607 # vs. compiled cols, or textual w/ unordered columns
608 # the order of columns can change if the query is
609 # against a "select *", so not safe to cache
610 self._safe_for_cache = False
611 raw_iterator = self._merge_cols_by_name(
612 context,
613 cursor_description,
614 result_columns,
615 loose_column_name_matching,
616 )
617 else:
618 # no compiled SQL, just a raw string, order of columns
619 # can change for "select *"
620 self._safe_for_cache = False
621 raw_iterator = self._merge_cols_by_none(
622 context, cursor_description
623 )
624
625 return [
626 (
627 idx,
628 ridx,
629 obj,
630 cursor_colname,
631 cursor_colname,
632 context.get_result_processor(
633 mapped_type, cursor_colname, coltype
634 ),
635 untranslated,
636 )
637 for (
638 idx,
639 ridx,
640 cursor_colname,
641 mapped_type,
642 coltype,
643 obj,
644 untranslated,
645 ) in raw_iterator
646 ]
647
648 def _colnames_from_description(self, context, cursor_description):
649 """Extract column names and data types from a cursor.description.
650
651 Applies unicode decoding, column translation, "normalization",
652 and case sensitivity rules to the names based on the dialect.
653
654 """
655
656 dialect = context.dialect
657 translate_colname = context._translate_colname
658 normalize_name = (
659 dialect.normalize_name if dialect.requires_name_normalize else None
660 )
661 untranslated = None
662
663 self._keys = []
664
665 for idx, rec in enumerate(cursor_description):
666 colname = rec[0]
667 coltype = rec[1]
668
669 if translate_colname:
670 colname, untranslated = translate_colname(colname)
671
672 if normalize_name:
673 colname = normalize_name(colname)
674
675 self._keys.append(colname)
676
677 yield idx, colname, untranslated, coltype
678
679 def _merge_textual_cols_by_position(
680 self, context, cursor_description, result_columns
681 ):
682 num_ctx_cols = len(result_columns)
683
684 if num_ctx_cols > len(cursor_description):
685 util.warn(
686 "Number of columns in textual SQL (%d) is "
687 "smaller than number of columns requested (%d)"
688 % (num_ctx_cols, len(cursor_description))
689 )
690 seen = set()
691
692 for (
693 idx,
694 colname,
695 untranslated,
696 coltype,
697 ) in self._colnames_from_description(context, cursor_description):
698 if idx < num_ctx_cols:
699 ctx_rec = result_columns[idx]
700 obj = ctx_rec[RM_OBJECTS]
701 ridx = idx
702 mapped_type = ctx_rec[RM_TYPE]
703 if obj[0] in seen:
704 raise exc.InvalidRequestError(
705 "Duplicate column expression requested "
706 "in textual SQL: %r" % obj[0]
707 )
708 seen.add(obj[0])
709 else:
710 mapped_type = sqltypes.NULLTYPE
711 obj = None
712 ridx = None
713 yield idx, ridx, colname, mapped_type, coltype, obj, untranslated
714
715 def _merge_cols_by_name(
716 self,
717 context,
718 cursor_description,
719 result_columns,
720 loose_column_name_matching,
721 ):
722 match_map = self._create_description_match_map(
723 result_columns, loose_column_name_matching
724 )
725 mapped_type: TypeEngine[Any]
726
727 for (
728 idx,
729 colname,
730 untranslated,
731 coltype,
732 ) in self._colnames_from_description(context, cursor_description):
733 try:
734 ctx_rec = match_map[colname]
735 except KeyError:
736 mapped_type = sqltypes.NULLTYPE
737 obj = None
738 result_columns_idx = None
739 else:
740 obj = ctx_rec[1]
741 mapped_type = ctx_rec[2]
742 result_columns_idx = ctx_rec[3]
743 yield (
744 idx,
745 result_columns_idx,
746 colname,
747 mapped_type,
748 coltype,
749 obj,
750 untranslated,
751 )
752
753 @classmethod
754 def _create_description_match_map(
755 cls,
756 result_columns: List[ResultColumnsEntry],
757 loose_column_name_matching: bool = False,
758 ) -> Dict[
759 Union[str, object], Tuple[str, Tuple[Any, ...], TypeEngine[Any], int]
760 ]:
761 """when matching cursor.description to a set of names that are present
762 in a Compiled object, as is the case with TextualSelect, get all the
763 names we expect might match those in cursor.description.
764 """
765
766 d: Dict[
767 Union[str, object],
768 Tuple[str, Tuple[Any, ...], TypeEngine[Any], int],
769 ] = {}
770 for ridx, elem in enumerate(result_columns):
771 key = elem[RM_RENDERED_NAME]
772 if key in d:
773 # conflicting keyname - just add the column-linked objects
774 # to the existing record. if there is a duplicate column
775 # name in the cursor description, this will allow all of those
776 # objects to raise an ambiguous column error
777 e_name, e_obj, e_type, e_ridx = d[key]
778 d[key] = e_name, e_obj + elem[RM_OBJECTS], e_type, ridx
779 else:
780 d[key] = (elem[RM_NAME], elem[RM_OBJECTS], elem[RM_TYPE], ridx)
781
782 if loose_column_name_matching:
783 # when using a textual statement with an unordered set
784 # of columns that line up, we are expecting the user
785 # to be using label names in the SQL that match to the column
786 # expressions. Enable more liberal matching for this case;
787 # duplicate keys that are ambiguous will be fixed later.
788 for r_key in elem[RM_OBJECTS]:
789 d.setdefault(
790 r_key,
791 (elem[RM_NAME], elem[RM_OBJECTS], elem[RM_TYPE], ridx),
792 )
793 return d
794
795 def _merge_cols_by_none(self, context, cursor_description):
796 for (
797 idx,
798 colname,
799 untranslated,
800 coltype,
801 ) in self._colnames_from_description(context, cursor_description):
802 yield (
803 idx,
804 None,
805 colname,
806 sqltypes.NULLTYPE,
807 coltype,
808 None,
809 untranslated,
810 )
811
812 if not TYPE_CHECKING:
813
814 def _key_fallback(
815 self, key: Any, err: Optional[Exception], raiseerr: bool = True
816 ) -> Optional[NoReturn]:
817 if raiseerr:
818 if self._unpickled and isinstance(key, elements.ColumnElement):
819 raise exc.NoSuchColumnError(
820 "Row was unpickled; lookup by ColumnElement "
821 "is unsupported"
822 ) from err
823 else:
824 raise exc.NoSuchColumnError(
825 "Could not locate column in row for column '%s'"
826 % util.string_or_unprintable(key)
827 ) from err
828 else:
829 return None
830
831 def _raise_for_ambiguous_column_name(self, rec):
832 raise exc.InvalidRequestError(
833 "Ambiguous column name '%s' in "
834 "result set column descriptions" % rec[MD_LOOKUP_KEY]
835 )
836
837 def _index_for_key(self, key: Any, raiseerr: bool = True) -> Optional[int]:
838 # TODO: can consider pre-loading ints and negative ints
839 # into _keymap - also no coverage here
840 if isinstance(key, int):
841 key = self._keys[key]
842
843 try:
844 rec = self._keymap[key]
845 except KeyError as ke:
846 x = self._key_fallback(key, ke, raiseerr)
847 assert x is None
848 return None
849
850 index = rec[0]
851
852 if index is None:
853 self._raise_for_ambiguous_column_name(rec)
854 return index
855
856 def _indexes_for_keys(self, keys):
857 try:
858 return [self._keymap[key][0] for key in keys]
859 except KeyError as ke:
860 # ensure it raises
861 CursorResultMetaData._key_fallback(self, ke.args[0], ke)
862
863 def _metadata_for_keys(
864 self, keys: Sequence[Any]
865 ) -> Iterator[_NonAmbigCursorKeyMapRecType]:
866 for key in keys:
867 if int in key.__class__.__mro__:
868 key = self._keys[key]
869
870 try:
871 rec = self._keymap[key]
872 except KeyError as ke:
873 # ensure it raises
874 CursorResultMetaData._key_fallback(self, ke.args[0], ke)
875
876 index = rec[MD_INDEX]
877
878 if index is None:
879 self._raise_for_ambiguous_column_name(rec)
880
881 yield cast(_NonAmbigCursorKeyMapRecType, rec)
882
883 def __getstate__(self):
884 # TODO: consider serializing this as SimpleResultMetaData
885 return {
886 "_keymap": {
887 key: (
888 rec[MD_INDEX],
889 rec[MD_RESULT_MAP_INDEX],
890 [],
891 key,
892 rec[MD_RENDERED_NAME],
893 None,
894 None,
895 )
896 for key, rec in self._keymap.items()
897 if isinstance(key, (str, int))
898 },
899 "_keys": self._keys,
900 "_translated_indexes": self._translated_indexes,
901 }
902
903 def __setstate__(self, state):
904 self._processors = [None for _ in range(len(state["_keys"]))]
905 self._keymap = state["_keymap"]
906 self._keymap_by_result_column_idx = None
907 self._key_to_index = self._make_key_to_index(self._keymap, MD_INDEX)
908 self._keys = state["_keys"]
909 self._unpickled = True
910 if state["_translated_indexes"]:
911 self._translated_indexes = cast(
912 "List[int]", state["_translated_indexes"]
913 )
914 self._tuplefilter = tuplegetter(*self._translated_indexes)
915 else:
916 self._translated_indexes = self._tuplefilter = None
917
918
919class ResultFetchStrategy:
920 """Define a fetching strategy for a result object.
921
922
923 .. versionadded:: 1.4
924
925 """
926
927 __slots__ = ()
928
929 alternate_cursor_description: Optional[_DBAPICursorDescription] = None
930
931 def soft_close(
932 self, result: CursorResult[Any], dbapi_cursor: Optional[DBAPICursor]
933 ) -> None:
934 raise NotImplementedError()
935
936 def hard_close(
937 self, result: CursorResult[Any], dbapi_cursor: Optional[DBAPICursor]
938 ) -> None:
939 raise NotImplementedError()
940
941 def yield_per(
942 self,
943 result: CursorResult[Any],
944 dbapi_cursor: Optional[DBAPICursor],
945 num: int,
946 ) -> None:
947 return
948
949 def fetchone(
950 self,
951 result: CursorResult[Any],
952 dbapi_cursor: DBAPICursor,
953 hard_close: bool = False,
954 ) -> Any:
955 raise NotImplementedError()
956
957 def fetchmany(
958 self,
959 result: CursorResult[Any],
960 dbapi_cursor: DBAPICursor,
961 size: Optional[int] = None,
962 ) -> Any:
963 raise NotImplementedError()
964
965 def fetchall(
966 self,
967 result: CursorResult[Any],
968 dbapi_cursor: DBAPICursor,
969 ) -> Any:
970 raise NotImplementedError()
971
972 def handle_exception(
973 self,
974 result: CursorResult[Any],
975 dbapi_cursor: Optional[DBAPICursor],
976 err: BaseException,
977 ) -> NoReturn:
978 raise err
979
980
981class NoCursorFetchStrategy(ResultFetchStrategy):
982 """Cursor strategy for a result that has no open cursor.
983
984 There are two varieties of this strategy, one for DQL and one for
985 DML (and also DDL), each of which represent a result that had a cursor
986 but no longer has one.
987
988 """
989
990 __slots__ = ()
991
992 def soft_close(self, result, dbapi_cursor):
993 pass
994
995 def hard_close(self, result, dbapi_cursor):
996 pass
997
998 def fetchone(self, result, dbapi_cursor, hard_close=False):
999 return self._non_result(result, None)
1000
1001 def fetchmany(self, result, dbapi_cursor, size=None):
1002 return self._non_result(result, [])
1003
1004 def fetchall(self, result, dbapi_cursor):
1005 return self._non_result(result, [])
1006
1007 def _non_result(self, result, default, err=None):
1008 raise NotImplementedError()
1009
1010
1011class NoCursorDQLFetchStrategy(NoCursorFetchStrategy):
1012 """Cursor strategy for a DQL result that has no open cursor.
1013
1014 This is a result set that can return rows, i.e. for a SELECT, or for an
1015 INSERT, UPDATE, DELETE that includes RETURNING. However it is in the state
1016 where the cursor is closed and no rows remain available. The owning result
1017 object may or may not be "hard closed", which determines if the fetch
1018 methods send empty results or raise for closed result.
1019
1020 """
1021
1022 __slots__ = ()
1023
1024 def _non_result(self, result, default, err=None):
1025 if result.closed:
1026 raise exc.ResourceClosedError(
1027 "This result object is closed."
1028 ) from err
1029 else:
1030 return default
1031
1032
1033_NO_CURSOR_DQL = NoCursorDQLFetchStrategy()
1034
1035
1036class NoCursorDMLFetchStrategy(NoCursorFetchStrategy):
1037 """Cursor strategy for a DML result that has no open cursor.
1038
1039 This is a result set that does not return rows, i.e. for an INSERT,
1040 UPDATE, DELETE that does not include RETURNING.
1041
1042 """
1043
1044 __slots__ = ()
1045
1046 def _non_result(self, result, default, err=None):
1047 # we only expect to have a _NoResultMetaData() here right now.
1048 assert not result._metadata.returns_rows
1049 result._metadata._we_dont_return_rows(err)
1050
1051
1052_NO_CURSOR_DML = NoCursorDMLFetchStrategy()
1053
1054
1055class CursorFetchStrategy(ResultFetchStrategy):
1056 """Call fetch methods from a DBAPI cursor.
1057
1058 Alternate versions of this class may instead buffer the rows from
1059 cursors or not use cursors at all.
1060
1061 """
1062
1063 __slots__ = ()
1064
1065 def soft_close(
1066 self, result: CursorResult[Any], dbapi_cursor: Optional[DBAPICursor]
1067 ) -> None:
1068 result.cursor_strategy = _NO_CURSOR_DQL
1069
1070 def hard_close(
1071 self, result: CursorResult[Any], dbapi_cursor: Optional[DBAPICursor]
1072 ) -> None:
1073 result.cursor_strategy = _NO_CURSOR_DQL
1074
1075 def handle_exception(
1076 self,
1077 result: CursorResult[Any],
1078 dbapi_cursor: Optional[DBAPICursor],
1079 err: BaseException,
1080 ) -> NoReturn:
1081 result.connection._handle_dbapi_exception(
1082 err, None, None, dbapi_cursor, result.context
1083 )
1084
1085 def yield_per(
1086 self,
1087 result: CursorResult[Any],
1088 dbapi_cursor: Optional[DBAPICursor],
1089 num: int,
1090 ) -> None:
1091 result.cursor_strategy = BufferedRowCursorFetchStrategy(
1092 dbapi_cursor,
1093 {"max_row_buffer": num},
1094 initial_buffer=collections.deque(),
1095 growth_factor=0,
1096 )
1097
1098 def fetchone(
1099 self,
1100 result: CursorResult[Any],
1101 dbapi_cursor: DBAPICursor,
1102 hard_close: bool = False,
1103 ) -> Any:
1104 try:
1105 row = dbapi_cursor.fetchone()
1106 if row is None:
1107 result._soft_close(hard=hard_close)
1108 return row
1109 except BaseException as e:
1110 self.handle_exception(result, dbapi_cursor, e)
1111
1112 def fetchmany(
1113 self,
1114 result: CursorResult[Any],
1115 dbapi_cursor: DBAPICursor,
1116 size: Optional[int] = None,
1117 ) -> Any:
1118 try:
1119 if size is None:
1120 l = dbapi_cursor.fetchmany()
1121 else:
1122 l = dbapi_cursor.fetchmany(size)
1123
1124 if not l:
1125 result._soft_close()
1126 return l
1127 except BaseException as e:
1128 self.handle_exception(result, dbapi_cursor, e)
1129
1130 def fetchall(
1131 self,
1132 result: CursorResult[Any],
1133 dbapi_cursor: DBAPICursor,
1134 ) -> Any:
1135 try:
1136 rows = dbapi_cursor.fetchall()
1137 result._soft_close()
1138 return rows
1139 except BaseException as e:
1140 self.handle_exception(result, dbapi_cursor, e)
1141
1142
1143_DEFAULT_FETCH = CursorFetchStrategy()
1144
1145
1146class BufferedRowCursorFetchStrategy(CursorFetchStrategy):
1147 """A cursor fetch strategy with row buffering behavior.
1148
1149 This strategy buffers the contents of a selection of rows
1150 before ``fetchone()`` is called. This is to allow the results of
1151 ``cursor.description`` to be available immediately, when
1152 interfacing with a DB-API that requires rows to be consumed before
1153 this information is available (currently psycopg2, when used with
1154 server-side cursors).
1155
1156 The pre-fetching behavior fetches only one row initially, and then
1157 grows its buffer size by a fixed amount with each successive need
1158 for additional rows up the ``max_row_buffer`` size, which defaults
1159 to 1000::
1160
1161 with psycopg2_engine.connect() as conn:
1162
1163 result = conn.execution_options(
1164 stream_results=True, max_row_buffer=50
1165 ).execute(text("select * from table"))
1166
1167 .. versionadded:: 1.4 ``max_row_buffer`` may now exceed 1000 rows.
1168
1169 .. seealso::
1170
1171 :ref:`psycopg2_execution_options`
1172 """
1173
1174 __slots__ = ("_max_row_buffer", "_rowbuffer", "_bufsize", "_growth_factor")
1175
1176 def __init__(
1177 self,
1178 dbapi_cursor,
1179 execution_options,
1180 growth_factor=5,
1181 initial_buffer=None,
1182 ):
1183 self._max_row_buffer = execution_options.get("max_row_buffer", 1000)
1184
1185 if initial_buffer is not None:
1186 self._rowbuffer = initial_buffer
1187 else:
1188 self._rowbuffer = collections.deque(dbapi_cursor.fetchmany(1))
1189 self._growth_factor = growth_factor
1190
1191 if growth_factor:
1192 self._bufsize = min(self._max_row_buffer, self._growth_factor)
1193 else:
1194 self._bufsize = self._max_row_buffer
1195
1196 @classmethod
1197 def create(cls, result):
1198 return BufferedRowCursorFetchStrategy(
1199 result.cursor,
1200 result.context.execution_options,
