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