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