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