1# orm/properties.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"""MapperProperty implementations.
9
10This is a private module which defines the behavior of individual ORM-
11mapped attributes.
12
13"""
14from __future__ import absolute_import
15
16from . import attributes
17from .descriptor_props import CompositeProperty
18from .descriptor_props import ConcreteInheritedProperty
19from .descriptor_props import SynonymProperty
20from .interfaces import PropComparator
21from .interfaces import StrategizedProperty
22from .relationships import RelationshipProperty
23from .util import _orm_full_deannotate
24from .. import log
25from .. import util
26from ..sql import coercions
27from ..sql import roles
28
29
30__all__ = [
31    "ColumnProperty",
32    "CompositeProperty",
33    "ConcreteInheritedProperty",
34    "RelationshipProperty",
35    "SynonymProperty",
36]
37
38
39@log.class_logger
40class ColumnProperty(StrategizedProperty):
41    """Describes an object attribute that corresponds to a table column.
42
43    Public constructor is the :func:`_orm.column_property` function.
44
45    """
46
47    strategy_wildcard_key = "column"
48    inherit_cache = True
49    _links_to_entity = False
50
51    __slots__ = (
52        "_orig_columns",
53        "columns",
54        "group",
55        "deferred",
56        "instrument",
57        "comparator_factory",
58        "descriptor",
59        "active_history",
60        "expire_on_flush",
61        "info",
62        "doc",
63        "strategy_key",
64        "_creation_order",
65        "_is_polymorphic_discriminator",
66        "_mapped_by_synonym",
67        "_deferred_column_loader",
68        "_raise_column_loader",
69        "_renders_in_subqueries",
70        "raiseload",
71    )
72
73    def __init__(self, *columns, **kwargs):
74        r"""Provide a column-level property for use with a mapping.
75
76        Column-based properties can normally be applied to the mapper's
77        ``properties`` dictionary using the :class:`_schema.Column`
78        element directly.
79        Use this function when the given column is not directly present within
80        the mapper's selectable; examples include SQL expressions, functions,
81        and scalar SELECT queries.
82
83        The :func:`_orm.column_property` function returns an instance of
84        :class:`.ColumnProperty`.
85
86        Columns that aren't present in the mapper's selectable won't be
87        persisted by the mapper and are effectively "read-only" attributes.
88
89        :param \*cols:
90              list of Column objects to be mapped.
91
92        :param active_history=False:
93          When ``True``, indicates that the "previous" value for a
94          scalar attribute should be loaded when replaced, if not
95          already loaded. Normally, history tracking logic for
96          simple non-primary-key scalar values only needs to be
97          aware of the "new" value in order to perform a flush. This
98          flag is available for applications that make use of
99          :func:`.attributes.get_history` or :meth:`.Session.is_modified`
100          which also need to know
101          the "previous" value of the attribute.
102
103        :param comparator_factory: a class which extends
104           :class:`.ColumnProperty.Comparator` which provides custom SQL
105           clause generation for comparison operations.
106
107        :param group:
108            a group name for this property when marked as deferred.
109
110        :param deferred:
111              when True, the column property is "deferred", meaning that
112              it does not load immediately, and is instead loaded when the
113              attribute is first accessed on an instance.  See also
114              :func:`~sqlalchemy.orm.deferred`.
115
116        :param doc:
117              optional string that will be applied as the doc on the
118              class-bound descriptor.
119
120        :param expire_on_flush=True:
121            Disable expiry on flush.   A column_property() which refers
122            to a SQL expression (and not a single table-bound column)
123            is considered to be a "read only" property; populating it
124            has no effect on the state of data, and it can only return
125            database state.   For this reason a column_property()'s value
126            is expired whenever the parent object is involved in a
127            flush, that is, has any kind of "dirty" state within a flush.
128            Setting this parameter to ``False`` will have the effect of
129            leaving any existing value present after the flush proceeds.
130            Note however that the :class:`.Session` with default expiration
131            settings still expires
132            all attributes after a :meth:`.Session.commit` call, however.
133
134        :param info: Optional data dictionary which will be populated into the
135            :attr:`.MapperProperty.info` attribute of this object.
136
137        :param raiseload: if True, indicates the column should raise an error
138            when undeferred, rather than loading the value.  This can be
139            altered at query time by using the :func:`.deferred` option with
140            raiseload=False.
141
142            .. versionadded:: 1.4
143
144            .. seealso::
145
146                :ref:`deferred_raiseload`
147
148        .. seealso::
149
150            :ref:`column_property_options` - to map columns while including
151            mapping options
152
153            :ref:`mapper_column_property_sql_expressions` - to map SQL
154            expressions
155
156        """
157        super(ColumnProperty, self).__init__()
158        self._orig_columns = [
159            coercions.expect(roles.LabeledColumnExprRole, c) for c in columns
160        ]
161        self.columns = [
162            coercions.expect(
163                roles.LabeledColumnExprRole, _orm_full_deannotate(c)
164            )
165            for c in columns
166        ]
167        self.group = kwargs.pop("group", None)
168        self.deferred = kwargs.pop("deferred", False)
169        self.raiseload = kwargs.pop("raiseload", False)
170        self.instrument = kwargs.pop("_instrument", True)
171        self.comparator_factory = kwargs.pop(
172            "comparator_factory", self.__class__.Comparator
173        )
174        self.descriptor = kwargs.pop("descriptor", None)
175        self.active_history = kwargs.pop("active_history", False)
176        self.expire_on_flush = kwargs.pop("expire_on_flush", True)
177
178        if "info" in kwargs:
179            self.info = kwargs.pop("info")
180
181        if "doc" in kwargs:
182            self.doc = kwargs.pop("doc")
183        else:
184            for col in reversed(self.columns):
185                doc = getattr(col, "doc", None)
186                if doc is not None:
187                    self.doc = doc
188                    break
189            else:
190                self.doc = None
191
192        if kwargs:
193            raise TypeError(
194                "%s received unexpected keyword argument(s): %s"
195                % (self.__class__.__name__, ", ".join(sorted(kwargs.keys())))
196            )
197
198        util.set_creation_order(self)
199
200        self.strategy_key = (
201            ("deferred", self.deferred),
202            ("instrument", self.instrument),
203        )
204        if self.raiseload:
205            self.strategy_key += (("raiseload", True),)
206
207    def _memoized_attr__renders_in_subqueries(self):
208        return ("deferred", True) not in self.strategy_key or (
209            self not in self.parent._readonly_props
210        )
211
212    @util.preload_module("sqlalchemy.orm.state", "sqlalchemy.orm.strategies")
213    def _memoized_attr__deferred_column_loader(self):
214        state = util.preloaded.orm_state
215        strategies = util.preloaded.orm_strategies
216        return state.InstanceState._instance_level_callable_processor(
217            self.parent.class_manager,
218            strategies.LoadDeferredColumns(self.key),
219            self.key,
220        )
221
222    @util.preload_module("sqlalchemy.orm.state", "sqlalchemy.orm.strategies")
223    def _memoized_attr__raise_column_loader(self):
224        state = util.preloaded.orm_state
225        strategies = util.preloaded.orm_strategies
226        return state.InstanceState._instance_level_callable_processor(
227            self.parent.class_manager,
228            strategies.LoadDeferredColumns(self.key, True),
229            self.key,
230        )
231
232    def __clause_element__(self):
233        """Allow the ColumnProperty to work in expression before it is turned
234        into an instrumented attribute.
235        """
236
237        return self.expression
238
239    @property
240    def expression(self):
241        """Return the primary column or expression for this ColumnProperty.
242
243        E.g.::
244
245
246            class File(Base):
247                # ...
248
249                name = Column(String(64))
250                extension = Column(String(8))
251                filename = column_property(name + '.' + extension)
252                path = column_property('C:/' + filename.expression)
253
254        .. seealso::
255
256            :ref:`mapper_column_property_sql_expressions_composed`
257
258        """
259        return self.columns[0]
260
261    def instrument_class(self, mapper):
262        if not self.instrument:
263            return
264
265        attributes.register_descriptor(
266            mapper.class_,
267            self.key,
268            comparator=self.comparator_factory(self, mapper),
269            parententity=mapper,
270            doc=self.doc,
271        )
272
273    def do_init(self):
274        super(ColumnProperty, self).do_init()
275
276        if len(self.columns) > 1 and set(self.parent.primary_key).issuperset(
277            self.columns
278        ):
279            util.warn(
280                (
281                    "On mapper %s, primary key column '%s' is being combined "
282                    "with distinct primary key column '%s' in attribute '%s'. "
283                    "Use explicit properties to give each column its own "
284                    "mapped attribute name."
285                )
286                % (self.parent, self.columns[1], self.columns[0], self.key)
287            )
288
289    def copy(self):
290        return ColumnProperty(
291            deferred=self.deferred,
292            group=self.group,
293            active_history=self.active_history,
294            *self.columns
295        )
296
297    def _getcommitted(
298        self, state, dict_, column, passive=attributes.PASSIVE_OFF
299    ):
300        return state.get_impl(self.key).get_committed_value(
301            state, dict_, passive=passive
302        )
303
304    def merge(
305        self,
306        session,
307        source_state,
308        source_dict,
309        dest_state,
310        dest_dict,
311        load,
312        _recursive,
313        _resolve_conflict_map,
314    ):
315        if not self.instrument:
316            return
317        elif self.key in source_dict:
318            value = source_dict[self.key]
319
320            if not load:
321                dest_dict[self.key] = value
322            else:
323                impl = dest_state.get_impl(self.key)
324                impl.set(dest_state, dest_dict, value, None)
325        elif dest_state.has_identity and self.key not in dest_dict:
326            dest_state._expire_attributes(
327                dest_dict, [self.key], no_loader=True
328            )
329
330    class Comparator(util.MemoizedSlots, PropComparator):
331        """Produce boolean, comparison, and other operators for
332        :class:`.ColumnProperty` attributes.
333
334        See the documentation for :class:`.PropComparator` for a brief
335        overview.
336
337        .. seealso::
338
339            :class:`.PropComparator`
340
341            :class:`.ColumnOperators`
342
343            :ref:`types_operators`
344
345            :attr:`.TypeEngine.comparator_factory`
346
347        """
348
349        __slots__ = "__clause_element__", "info", "expressions"
350
351        def _orm_annotate_column(self, column):
352            """annotate and possibly adapt a column to be returned
353            as the mapped-attribute exposed version of the column.
354
355            The column in this context needs to act as much like the
356            column in an ORM mapped context as possible, so includes
357            annotations to give hints to various ORM functions as to
358            the source entity of this column.   It also adapts it
359            to the mapper's with_polymorphic selectable if one is
360            present.
361
362            """
363
364            pe = self._parententity
365            annotations = {
366                "entity_namespace": pe,
367                "parententity": pe,
368                "parentmapper": pe,
369                "proxy_key": self.prop.key,
370            }
371
372            col = column
373
374            # for a mapper with polymorphic_on and an adapter, return
375            # the column against the polymorphic selectable.
376            # see also orm.util._orm_downgrade_polymorphic_columns
377            # for the reverse operation.
378            if self._parentmapper._polymorphic_adapter:
379                mapper_local_col = col
380                col = self._parentmapper._polymorphic_adapter.traverse(col)
381
382                # this is a clue to the ORM Query etc. that this column
383                # was adapted to the mapper's polymorphic_adapter.  the
384                # ORM uses this hint to know which column its adapting.
385                annotations["adapt_column"] = mapper_local_col
386
387            return col._annotate(annotations)._set_propagate_attrs(
388                {"compile_state_plugin": "orm", "plugin_subject": pe}
389            )
390
391        def _memoized_method___clause_element__(self):
392            if self.adapter:
393                return self.adapter(self.prop.columns[0], self.prop.key)
394            else:
395                return self._orm_annotate_column(self.prop.columns[0])
396
397        def _memoized_attr_info(self):
398            """The .info dictionary for this attribute."""
399
400            ce = self.__clause_element__()
401            try:
402                return ce.info
403            except AttributeError:
404                return self.prop.info
405
406        def _memoized_attr_expressions(self):
407            """The full sequence of columns referenced by this
408            attribute, adjusted for any aliasing in progress.
409
410            .. versionadded:: 1.3.17
411
412            """
413            if self.adapter:
414                return [
415                    self.adapter(col, self.prop.key)
416                    for col in self.prop.columns
417                ]
418            else:
419                return [
420                    self._orm_annotate_column(col) for col in self.prop.columns
421                ]
422
423        def _fallback_getattr(self, key):
424            """proxy attribute access down to the mapped column.
425
426            this allows user-defined comparison methods to be accessed.
427            """
428            return getattr(self.__clause_element__(), key)
429
430        def operate(self, op, *other, **kwargs):
431            return op(self.__clause_element__(), *other, **kwargs)
432
433        def reverse_operate(self, op, other, **kwargs):
434            col = self.__clause_element__()
435            return op(col._bind_param(op, other), col, **kwargs)
436
437    def __str__(self):
438        return str(self.parent.class_.__name__) + "." + self.key
439