1# engine/row.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
8"""Define row constructs including :class:`.Row`."""
9
10
11import operator
12
13from .. import util
14from ..sql import util as sql_util
15from ..util.compat import collections_abc
16
17MD_INDEX = 0 # integer index in cursor.description
18
19# This reconstructor is necessary so that pickles with the C extension or
20# without use the same Binary format.
21try:
22 # We need a different reconstructor on the C extension so that we can
23 # add extra checks that fields have correctly been initialized by
24 # __setstate__.
25 from sqlalchemy.cresultproxy import safe_rowproxy_reconstructor
26
27 # The extra function embedding is needed so that the
28 # reconstructor function has the same signature whether or not
29 # the extension is present.
30 def rowproxy_reconstructor(cls, state):
31 return safe_rowproxy_reconstructor(cls, state)
32
33
34except ImportError:
35
36 def rowproxy_reconstructor(cls, state):
37 obj = cls.__new__(cls)
38 obj.__setstate__(state)
39 return obj
40
41
42KEY_INTEGER_ONLY = 0
43"""__getitem__ only allows integer values, raises TypeError otherwise"""
44
45KEY_OBJECTS_ONLY = 1
46"""__getitem__ only allows string/object values, raises TypeError otherwise"""
47
48KEY_OBJECTS_BUT_WARN = 2
49"""__getitem__ allows integer or string/object values, but emits a 2.0
50deprecation warning if string/object is passed"""
51
52KEY_OBJECTS_NO_WARN = 3
53"""__getitem__ allows integer or string/object values with no warnings
54or errors."""
55
56try:
57 from sqlalchemy.cresultproxy import BaseRow
58
59 _baserow_usecext = True
60except ImportError:
61 _baserow_usecext = False
62
63 class BaseRow(object):
64 __slots__ = ("_parent", "_data", "_keymap", "_key_style")
65
66 def __init__(self, parent, processors, keymap, key_style, data):
67 """Row objects are constructed by CursorResult objects."""
68
69 object.__setattr__(self, "_parent", parent)
70
71 if processors:
72 object.__setattr__(
73 self,
74 "_data",
75 tuple(
76 [
77 proc(value) if proc else value
78 for proc, value in zip(processors, data)
79 ]
80 ),
81 )
82 else:
83 object.__setattr__(self, "_data", tuple(data))
84
85 object.__setattr__(self, "_keymap", keymap)
86
87 object.__setattr__(self, "_key_style", key_style)
88
89 def __reduce__(self):
90 return (
91 rowproxy_reconstructor,
92 (self.__class__, self.__getstate__()),
93 )
94
95 def _filter_on_values(self, filters):
96 return Row(
97 self._parent,
98 filters,
99 self._keymap,
100 self._key_style,
101 self._data,
102 )
103
104 def _values_impl(self):
105 return list(self)
106
107 def __iter__(self):
108 return iter(self._data)
109
110 def __len__(self):
111 return len(self._data)
112
113 def __hash__(self):
114 return hash(self._data)
115
116 def _get_by_int_impl(self, key):
117 return self._data[key]
118
119 def _get_by_key_impl(self, key):
120 if int in key.__class__.__mro__:
121 return self._data[key]
122
123 if self._key_style == KEY_INTEGER_ONLY:
124 self._parent._raise_for_nonint(key)
125
126 # the following is all LegacyRow support. none of this
127 # should be called if not LegacyRow
128 # assert isinstance(self, LegacyRow)
129
130 try:
131 rec = self._keymap[key]
132 except KeyError as ke:
133 if isinstance(key, slice):
134 return tuple(self._data[key])
135 else:
136 rec = self._parent._key_fallback(key, ke)
137 except TypeError:
138 if isinstance(key, slice):
139 return tuple(self._data[key])
140 else:
141 raise
142
143 mdindex = rec[MD_INDEX]
144 if mdindex is None:
145 self._parent._raise_for_ambiguous_column_name(rec)
146
147 elif self._key_style == KEY_OBJECTS_BUT_WARN and mdindex != key:
148 self._parent._warn_for_nonint(key)
149
150 return self._data[mdindex]
151
152 # The original 1.4 plan was that Row would not allow row["str"]
153 # access, however as the C extensions were inadvertently allowing
154 # this coupled with the fact that orm Session sets future=True,
155 # this allows a softer upgrade path. see #6218
156 __getitem__ = _get_by_key_impl
157
158 def _get_by_key_impl_mapping(self, key):
159 try:
160 rec = self._keymap[key]
161 except KeyError as ke:
162 rec = self._parent._key_fallback(key, ke)
163
164 mdindex = rec[MD_INDEX]
165 if mdindex is None:
166 self._parent._raise_for_ambiguous_column_name(rec)
167 elif (
168 self._key_style == KEY_OBJECTS_ONLY
169 and int in key.__class__.__mro__
170 ):
171 raise KeyError(key)
172
173 return self._data[mdindex]
174
175 def __getattr__(self, name):
176 try:
177 return self._get_by_key_impl_mapping(name)
178 except KeyError as e:
179 util.raise_(AttributeError(e.args[0]), replace_context=e)
180
181
182class Row(BaseRow, collections_abc.Sequence):
183 """Represent a single result row.
184
185 The :class:`.Row` object represents a row of a database result. It is
186 typically associated in the 1.x series of SQLAlchemy with the
187 :class:`_engine.CursorResult` object, however is also used by the ORM for
188 tuple-like results as of SQLAlchemy 1.4.
189
190 The :class:`.Row` object seeks to act as much like a Python named
191 tuple as possible. For mapping (i.e. dictionary) behavior on a row,
192 such as testing for containment of keys, refer to the :attr:`.Row._mapping`
193 attribute.
194
195 .. seealso::
196
197 :ref:`tutorial_selecting_data` - includes examples of selecting
198 rows from SELECT statements.
199
200 :class:`.LegacyRow` - Compatibility interface introduced in SQLAlchemy
201 1.4.
202
203 .. versionchanged:: 1.4
204
205 Renamed ``RowProxy`` to :class:`.Row`. :class:`.Row` is no longer a
206 "proxy" object in that it contains the final form of data within it,
207 and now acts mostly like a named tuple. Mapping-like functionality is
208 moved to the :attr:`.Row._mapping` attribute, but will remain available
209 in SQLAlchemy 1.x series via the :class:`.LegacyRow` class that is used
210 by :class:`_engine.LegacyCursorResult`.
211 See :ref:`change_4710_core` for background
212 on this change.
213
214 """
215
216 __slots__ = ()
217
218 # in 2.0, this should be KEY_INTEGER_ONLY
219 _default_key_style = KEY_OBJECTS_BUT_WARN
220
221 def __setattr__(self, name, value):
222 raise AttributeError("can't set attribute")
223
224 def __delattr__(self, name):
225 raise AttributeError("can't delete attribute")
226
227 @property
228 def _mapping(self):
229 """Return a :class:`.RowMapping` for this :class:`.Row`.
230
231 This object provides a consistent Python mapping (i.e. dictionary)
232 interface for the data contained within the row. The :class:`.Row`
233 by itself behaves like a named tuple, however in the 1.4 series of
234 SQLAlchemy, the :class:`.LegacyRow` class is still used by Core which
235 continues to have mapping-like behaviors against the row object
236 itself.
237
238 .. seealso::
239
240 :attr:`.Row._fields`
241
242 .. versionadded:: 1.4
243
244 """
245 return RowMapping(
246 self._parent,
247 None,
248 self._keymap,
249 RowMapping._default_key_style,
250 self._data,
251 )
252
253 def _special_name_accessor(name):
254 """Handle ambiguous names such as "count" and "index" """
255
256 @property
257 def go(self):
258 if self._parent._has_key(name):
259 return self.__getattr__(name)
260 else:
261
262 def meth(*arg, **kw):
263 return getattr(collections_abc.Sequence, name)(
264 self, *arg, **kw
265 )
266
267 return meth
268
269 return go
270
271 count = _special_name_accessor("count")
272 index = _special_name_accessor("index")
273
274 def __contains__(self, key):
275 return key in self._data
276
277 def __getstate__(self):
278 return {
279 "_parent": self._parent,
280 "_data": self._data,
281 "_key_style": self._key_style,
282 }
283
284 def __setstate__(self, state):
285 parent = state["_parent"]
286 object.__setattr__(self, "_parent", parent)
287 object.__setattr__(self, "_data", state["_data"])
288 object.__setattr__(self, "_keymap", parent._keymap)
289 object.__setattr__(self, "_key_style", state["_key_style"])
290
291 def _op(self, other, op):
292 return (
293 op(tuple(self), tuple(other))
294 if isinstance(other, Row)
295 else op(tuple(self), other)
296 )
297
298 __hash__ = BaseRow.__hash__
299
300 def __lt__(self, other):
301 return self._op(other, operator.lt)
302
303 def __le__(self, other):
304 return self._op(other, operator.le)
305
306 def __ge__(self, other):
307 return self._op(other, operator.ge)
308
309 def __gt__(self, other):
310 return self._op(other, operator.gt)
311
312 def __eq__(self, other):
313 return self._op(other, operator.eq)
314
315 def __ne__(self, other):
316 return self._op(other, operator.ne)
317
318 def __repr__(self):
319 return repr(sql_util._repr_row(self))
320
321 @util.deprecated_20(
322 ":meth:`.Row.keys`",
323 alternative="Use the namedtuple standard accessor "
324 ":attr:`.Row._fields`, or for full mapping behavior use "
325 "row._mapping.keys() ",
326 )
327 def keys(self):
328 """Return the list of keys as strings represented by this
329 :class:`.Row`.
330
331 The keys can represent the labels of the columns returned by a core
332 statement or the names of the orm classes returned by an orm
333 execution.
334
335 This method is analogous to the Python dictionary ``.keys()`` method,
336 except that it returns a list, not an iterator.
337
338 .. seealso::
339
340 :attr:`.Row._fields`
341
342 :attr:`.Row._mapping`
343
344 """
345 return self._parent.keys
346
347 @property
348 def _fields(self):
349 """Return a tuple of string keys as represented by this
350 :class:`.Row`.
351
352 The keys can represent the labels of the columns returned by a core
353 statement or the names of the orm classes returned by an orm
354 execution.
355
356 This attribute is analogous to the Python named tuple ``._fields``
357 attribute.
358
359 .. versionadded:: 1.4
360
361 .. seealso::
362
363 :attr:`.Row._mapping`
364
365 """
366 return tuple([k for k in self._parent.keys if k is not None])
367
368 def _asdict(self):
369 """Return a new dict which maps field names to their corresponding
370 values.
371
372 This method is analogous to the Python named tuple ``._asdict()``
373 method, and works by applying the ``dict()`` constructor to the
374 :attr:`.Row._mapping` attribute.
375
376 .. versionadded:: 1.4
377
378 .. seealso::
379
380 :attr:`.Row._mapping`
381
382 """
383 return dict(self._mapping)
384
385 def _replace(self):
386 raise NotImplementedError()
387
388 @property
389 def _field_defaults(self):
390 raise NotImplementedError()
391
392
393class LegacyRow(Row):
394 """A subclass of :class:`.Row` that delivers 1.x SQLAlchemy behaviors
395 for Core.
396
397 The :class:`.LegacyRow` class is where most of the Python mapping
398 (i.e. dictionary-like)
399 behaviors are implemented for the row object. The mapping behavior
400 of :class:`.Row` going forward is accessible via the :class:`.Row._mapping`
401 attribute.
402
403 .. versionadded:: 1.4 - added :class:`.LegacyRow` which encapsulates most
404 of the deprecated behaviors of :class:`.Row`.
405
406 """
407
408 __slots__ = ()
409
410 if util.SQLALCHEMY_WARN_20:
411 _default_key_style = KEY_OBJECTS_BUT_WARN
412 else:
413 _default_key_style = KEY_OBJECTS_NO_WARN
414
415 def __contains__(self, key):
416 return self._parent._contains(key, self)
417
418 # prior to #6218, LegacyRow would redirect the behavior of __getitem__
419 # for the non C version of BaseRow. This is now set up by Python BaseRow
420 # in all cases
421 # if not _baserow_usecext:
422 # __getitem__ = BaseRow._get_by_key_impl
423
424 @util.deprecated(
425 "1.4",
426 "The :meth:`.LegacyRow.has_key` method is deprecated and will be "
427 "removed in a future release. To test for key membership, use "
428 "the :attr:`Row._mapping` attribute, i.e. 'key in row._mapping`.",
429 )
430 def has_key(self, key):
431 """Return True if this :class:`.LegacyRow` contains the given key.
432
433 Through the SQLAlchemy 1.x series, the ``__contains__()`` method of
434 :class:`.Row` (or :class:`.LegacyRow` as of SQLAlchemy 1.4) also links
435 to :meth:`.Row.has_key`, in that an expression such as ::
436
437 "some_col" in row
438
439 Will return True if the row contains a column named ``"some_col"``,
440 in the way that a Python mapping works.
441
442 However, it is planned that the 2.0 series of SQLAlchemy will reverse
443 this behavior so that ``__contains__()`` will refer to a value being
444 present in the row, in the way that a Python tuple works.
445
446 .. seealso::
447
448 :ref:`change_4710_core`
449
450 """
451
452 return self._parent._has_key(key)
453
454 @util.deprecated(
455 "1.4",
456 "The :meth:`.LegacyRow.items` method is deprecated and will be "
457 "removed in a future release. Use the :attr:`Row._mapping` "
458 "attribute, i.e., 'row._mapping.items()'.",
459 )
460 def items(self):
461 """Return a list of tuples, each tuple containing a key/value pair.
462
463 This method is analogous to the Python dictionary ``.items()`` method,
464 except that it returns a list, not an iterator.
465
466 """
467
468 return [(key, self[key]) for key in self.keys()]
469
470 @util.deprecated(
471 "1.4",
472 "The :meth:`.LegacyRow.iterkeys` method is deprecated and will be "
473 "removed in a future release. Use the :attr:`Row._mapping` "
474 "attribute, i.e., 'row._mapping.keys()'.",
475 )
476 def iterkeys(self):
477 """Return a an iterator against the :meth:`.Row.keys` method.
478
479 This method is analogous to the Python-2-only dictionary
480 ``.iterkeys()`` method.
481
482 """
483 return iter(self._parent.keys)
484
485 @util.deprecated(
486 "1.4",
487 "The :meth:`.LegacyRow.itervalues` method is deprecated and will be "
488 "removed in a future release. Use the :attr:`Row._mapping` "
489 "attribute, i.e., 'row._mapping.values()'.",
490 )
491 def itervalues(self):
492 """Return a an iterator against the :meth:`.Row.values` method.
493
494 This method is analogous to the Python-2-only dictionary
495 ``.itervalues()`` method.
496
497 """
498 return iter(self)
499
500 @util.deprecated(
501 "1.4",
502 "The :meth:`.LegacyRow.values` method is deprecated and will be "
503 "removed in a future release. Use the :attr:`Row._mapping` "
504 "attribute, i.e., 'row._mapping.values()'.",
505 )
506 def values(self):
507 """Return the values represented by this :class:`.Row` as a list.
508
509 This method is analogous to the Python dictionary ``.values()`` method,
510 except that it returns a list, not an iterator.
511
512 """
513
514 return self._values_impl()
515
516
517BaseRowProxy = BaseRow
518RowProxy = Row
519
520
521class ROMappingView(
522 collections_abc.KeysView,
523 collections_abc.ValuesView,
524 collections_abc.ItemsView,
525):
526 __slots__ = (
527 "_mapping",
528 "_items",
529 )
530
531 def __init__(self, mapping, items):
532 self._mapping = mapping
533 self._items = items
534
535 def __len__(self):
536 return len(self._items)
537
538 def __repr__(self):
539 return "{0.__class__.__name__}({0._mapping!r})".format(self)
540
541 def __iter__(self):
542 return iter(self._items)
543
544 def __contains__(self, item):
545 return item in self._items
546
547 def __eq__(self, other):
548 return list(other) == list(self)
549
550 def __ne__(self, other):
551 return list(other) != list(self)
552
553
554class RowMapping(BaseRow, collections_abc.Mapping):
555 """A ``Mapping`` that maps column names and objects to :class:`.Row`
556 values.
557
558 The :class:`.RowMapping` is available from a :class:`.Row` via the
559 :attr:`.Row._mapping` attribute, as well as from the iterable interface
560 provided by the :class:`.MappingResult` object returned by the
561 :meth:`_engine.Result.mappings` method.
562
563 :class:`.RowMapping` supplies Python mapping (i.e. dictionary) access to
564 the contents of the row. This includes support for testing of
565 containment of specific keys (string column names or objects), as well
566 as iteration of keys, values, and items::
567
568 for row in result:
569 if 'a' in row._mapping:
570 print("Column 'a': %s" % row._mapping['a'])
571
572 print("Column b: %s" % row._mapping[table.c.b])
573
574
575 .. versionadded:: 1.4 The :class:`.RowMapping` object replaces the
576 mapping-like access previously provided by a database result row,
577 which now seeks to behave mostly like a named tuple.
578
579 """
580
581 __slots__ = ()
582
583 _default_key_style = KEY_OBJECTS_ONLY
584
585 if not _baserow_usecext:
586
587 __getitem__ = BaseRow._get_by_key_impl_mapping
588
589 def _values_impl(self):
590 return list(self._data)
591
592 def __iter__(self):
593 return (k for k in self._parent.keys if k is not None)
594
595 def __len__(self):
596 return len(self._data)
597
598 def __contains__(self, key):
599 return self._parent._has_key(key)
600
601 def __repr__(self):
602 return repr(dict(self))
603
604 def items(self):
605 """Return a view of key/value tuples for the elements in the
606 underlying :class:`.Row`.
607
608 """
609 return ROMappingView(self, [(key, self[key]) for key in self.keys()])
610
611 def keys(self):
612 """Return a view of 'keys' for string column names represented
613 by the underlying :class:`.Row`.
614
615 """
616
617 return self._parent.keys
618
619 def values(self):
620 """Return a view of values for the values represented in the
621 underlying :class:`.Row`.
622
623 """
624 return ROMappingView(self, self._values_impl())