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