1# orm/descriptor_props.py
2# Copyright (C) 2005-2025 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"""Descriptor properties are more "auxiliary" properties
9that exist as configurational elements, but don't participate
10as actively in the load/persist ORM loop.
11
12"""
13from __future__ import annotations
14
15from dataclasses import is_dataclass
16import inspect
17import itertools
18import operator
19import typing
20from typing import Any
21from typing import Callable
22from typing import Dict
23from typing import get_args
24from typing import List
25from typing import NoReturn
26from typing import Optional
27from typing import Sequence
28from typing import Tuple
29from typing import Type
30from typing import TYPE_CHECKING
31from typing import TypeVar
32from typing import Union
33import weakref
34
35from . import attributes
36from . import util as orm_util
37from .base import _DeclarativeMapped
38from .base import DONT_SET
39from .base import LoaderCallableStatus
40from .base import Mapped
41from .base import PassiveFlag
42from .base import SQLORMOperations
43from .interfaces import _AttributeOptions
44from .interfaces import _IntrospectsAnnotations
45from .interfaces import _MapsColumns
46from .interfaces import MapperProperty
47from .interfaces import PropComparator
48from .util import de_stringify_annotation
49from .. import event
50from .. import exc as sa_exc
51from .. import schema
52from .. import sql
53from .. import util
54from ..sql import expression
55from ..sql import operators
56from ..sql.base import _NoArg
57from ..sql.elements import BindParameter
58from ..util.typing import de_optionalize_union_types
59from ..util.typing import includes_none
60from ..util.typing import is_fwd_ref
61from ..util.typing import is_pep593
62from ..util.typing import is_union
63from ..util.typing import TupleAny
64from ..util.typing import Unpack
65
66
67if typing.TYPE_CHECKING:
68 from ._typing import _InstanceDict
69 from ._typing import _RegistryType
70 from .attributes import History
71 from .attributes import InstrumentedAttribute
72 from .attributes import QueryableAttribute
73 from .context import _ORMCompileState
74 from .decl_base import _ClassScanAbstractConfig
75 from .decl_base import _DeclarativeMapperConfig
76 from .interfaces import _DataclassArguments
77 from .mapper import Mapper
78 from .properties import ColumnProperty
79 from .properties import MappedColumn
80 from .state import InstanceState
81 from ..engine.base import Connection
82 from ..engine.row import Row
83 from ..sql._typing import _DMLColumnArgument
84 from ..sql._typing import _InfoType
85 from ..sql.elements import ClauseList
86 from ..sql.elements import ColumnElement
87 from ..sql.operators import OperatorType
88 from ..sql.schema import Column
89 from ..sql.selectable import Select
90 from ..util.typing import _AnnotationScanType
91 from ..util.typing import CallableReference
92 from ..util.typing import DescriptorReference
93 from ..util.typing import RODescriptorReference
94
95_T = TypeVar("_T", bound=Any)
96_PT = TypeVar("_PT", bound=Any)
97
98
99class DescriptorProperty(MapperProperty[_T]):
100 """:class:`.MapperProperty` which proxies access to a
101 user-defined descriptor."""
102
103 doc: Optional[str] = None
104
105 uses_objects = False
106 _links_to_entity = False
107
108 descriptor: DescriptorReference[Any]
109
110 def _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
111 raise NotImplementedError(
112 "This MapperProperty does not implement column loader strategies"
113 )
114
115 def get_history(
116 self,
117 state: InstanceState[Any],
118 dict_: _InstanceDict,
119 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
120 ) -> History:
121 raise NotImplementedError()
122
123 def instrument_class(self, mapper: Mapper[Any]) -> None:
124 prop = self
125
126 class _ProxyImpl(attributes._AttributeImpl):
127 accepts_scalar_loader = False
128 load_on_unexpire = True
129 collection = False
130
131 @property
132 def uses_objects(self) -> bool: # type: ignore
133 return prop.uses_objects
134
135 def __init__(self, key: str):
136 self.key = key
137
138 def get_history(
139 self,
140 state: InstanceState[Any],
141 dict_: _InstanceDict,
142 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
143 ) -> History:
144 return prop.get_history(state, dict_, passive)
145
146 if self.descriptor is None:
147 desc = getattr(mapper.class_, self.key, None)
148 if mapper._is_userland_descriptor(self.key, desc):
149 self.descriptor = desc
150
151 if self.descriptor is None:
152
153 def fset(obj: Any, value: Any) -> None:
154 setattr(obj, self.name, value)
155
156 def fdel(obj: Any) -> None:
157 delattr(obj, self.name)
158
159 def fget(obj: Any) -> Any:
160 return getattr(obj, self.name)
161
162 self.descriptor = property(fget=fget, fset=fset, fdel=fdel)
163
164 proxy_attr = attributes._create_proxied_attribute(self.descriptor)(
165 self.parent.class_,
166 self.key,
167 self.descriptor,
168 lambda: self._comparator_factory(mapper),
169 doc=self.doc,
170 original_property=self,
171 )
172
173 proxy_attr.impl = _ProxyImpl(self.key)
174 mapper.class_manager.instrument_attribute(self.key, proxy_attr)
175
176
177_CompositeAttrType = Union[
178 str,
179 "Column[_T]",
180 "MappedColumn[_T]",
181 "InstrumentedAttribute[_T]",
182 "Mapped[_T]",
183]
184
185
186_CC = TypeVar("_CC", bound=Any)
187
188
189_composite_getters: weakref.WeakKeyDictionary[
190 Type[Any], Callable[[Any], Tuple[Any, ...]]
191] = weakref.WeakKeyDictionary()
192
193
194class CompositeProperty(
195 _MapsColumns[_CC], _IntrospectsAnnotations, DescriptorProperty[_CC]
196):
197 """Defines a "composite" mapped attribute, representing a collection
198 of columns as one attribute.
199
200 :class:`.CompositeProperty` is constructed using the :func:`.composite`
201 function.
202
203 .. seealso::
204
205 :ref:`mapper_composite`
206
207 """
208
209 composite_class: Union[Type[_CC], Callable[..., _CC]]
210 attrs: Tuple[_CompositeAttrType[Any], ...]
211
212 _generated_composite_accessor: CallableReference[
213 Optional[Callable[[_CC], Tuple[Any, ...]]]
214 ]
215
216 comparator_factory: Type[Comparator[_CC]]
217
218 def __init__(
219 self,
220 _class_or_attr: Union[
221 None, Type[_CC], Callable[..., _CC], _CompositeAttrType[Any]
222 ] = None,
223 *attrs: _CompositeAttrType[Any],
224 return_none_on: Union[
225 _NoArg, None, Callable[..., bool]
226 ] = _NoArg.NO_ARG,
227 attribute_options: Optional[_AttributeOptions] = None,
228 active_history: bool = False,
229 deferred: bool = False,
230 group: Optional[str] = None,
231 comparator_factory: Optional[Type[Comparator[_CC]]] = None,
232 info: Optional[_InfoType] = None,
233 **kwargs: Any,
234 ):
235 super().__init__(attribute_options=attribute_options)
236
237 if isinstance(_class_or_attr, (Mapped, str, sql.ColumnElement)):
238 self.attrs = (_class_or_attr,) + attrs
239 # will initialize within declarative_scan
240 self.composite_class = None # type: ignore
241 else:
242 self.composite_class = _class_or_attr # type: ignore
243 self.attrs = attrs
244
245 self.return_none_on = return_none_on
246 self.active_history = active_history
247 self.deferred = deferred
248 self.group = group
249 self.comparator_factory = (
250 comparator_factory
251 if comparator_factory is not None
252 else self.__class__.Comparator
253 )
254 self._generated_composite_accessor = None
255 if info is not None:
256 self.info.update(info)
257
258 util.set_creation_order(self)
259 self._create_descriptor()
260 self._init_accessor()
261
262 @util.memoized_property
263 def _construct_composite(self) -> Callable[..., Any]:
264 return_none_on = self.return_none_on
265 if callable(return_none_on):
266
267 def construct(*args: Any) -> Any:
268 if return_none_on(*args):
269 return None
270 else:
271 return self.composite_class(*args)
272
273 return construct
274 else:
275 return self.composite_class
276
277 def instrument_class(self, mapper: Mapper[Any]) -> None:
278 super().instrument_class(mapper)
279 self._setup_event_handlers()
280
281 def _composite_values_from_instance(self, value: _CC) -> Tuple[Any, ...]:
282 if self._generated_composite_accessor:
283 return self._generated_composite_accessor(value)
284 else:
285 try:
286 accessor = value.__composite_values__
287 except AttributeError as ae:
288 raise sa_exc.InvalidRequestError(
289 f"Composite class {self.composite_class.__name__} is not "
290 f"a dataclass and does not define a __composite_values__()"
291 " method; can't get state"
292 ) from ae
293 else:
294 return accessor() # type: ignore
295
296 def do_init(self) -> None:
297 """Initialization which occurs after the :class:`.Composite`
298 has been associated with its parent mapper.
299
300 """
301 self._setup_arguments_on_columns()
302
303 _COMPOSITE_FGET = object()
304
305 def _create_descriptor(self) -> None:
306 """Create the Python descriptor that will serve as
307 the access point on instances of the mapped class.
308
309 """
310
311 def fget(instance: Any) -> Any:
312 dict_ = attributes.instance_dict(instance)
313 state = attributes.instance_state(instance)
314
315 if self.key not in dict_:
316 # key not present. Iterate through related
317 # attributes, retrieve their values. This
318 # ensures they all load.
319 values = [
320 getattr(instance, key) for key in self._attribute_keys
321 ]
322
323 if self.key not in dict_:
324 dict_[self.key] = self._construct_composite(*values)
325 state.manager.dispatch.refresh(
326 state, self._COMPOSITE_FGET, [self.key]
327 )
328
329 return dict_.get(self.key, None)
330
331 def fset(instance: Any, value: Any) -> None:
332 if value is LoaderCallableStatus.DONT_SET:
333 return
334
335 dict_ = attributes.instance_dict(instance)
336 state = attributes.instance_state(instance)
337 attr = state.manager[self.key]
338
339 if attr.dispatch._active_history:
340 previous = fget(instance)
341 else:
342 previous = dict_.get(self.key, LoaderCallableStatus.NO_VALUE)
343
344 for fn in attr.dispatch.set:
345 value = fn(state, value, previous, attr.impl)
346 dict_[self.key] = value
347 if value is None:
348 for key in self._attribute_keys:
349 setattr(instance, key, None)
350 else:
351 for key, value in zip(
352 self._attribute_keys,
353 self._composite_values_from_instance(value),
354 ):
355 setattr(instance, key, value)
356
357 def fdel(instance: Any) -> None:
358 state = attributes.instance_state(instance)
359 dict_ = attributes.instance_dict(instance)
360 attr = state.manager[self.key]
361
362 if attr.dispatch._active_history:
363 previous = fget(instance)
364 dict_.pop(self.key, None)
365 else:
366 previous = dict_.pop(self.key, LoaderCallableStatus.NO_VALUE)
367
368 attr = state.manager[self.key]
369 attr.dispatch.remove(state, previous, attr.impl)
370 for key in self._attribute_keys:
371 setattr(instance, key, None)
372
373 self.descriptor = property(fget, fset, fdel)
374
375 @util.preload_module("sqlalchemy.orm.properties")
376 def declarative_scan(
377 self,
378 decl_scan: _DeclarativeMapperConfig,
379 registry: _RegistryType,
380 cls: Type[Any],
381 originating_module: Optional[str],
382 key: str,
383 mapped_container: Optional[Type[Mapped[Any]]],
384 annotation: Optional[_AnnotationScanType],
385 extracted_mapped_annotation: Optional[_AnnotationScanType],
386 is_dataclass_field: bool,
387 ) -> None:
388 MappedColumn = util.preloaded.orm_properties.MappedColumn
389 if (
390 self.composite_class is None
391 and extracted_mapped_annotation is None
392 ):
393 self._raise_for_required(key, cls)
394 argument = extracted_mapped_annotation
395
396 if is_pep593(argument):
397 argument = get_args(argument)[0]
398
399 if argument and self.composite_class is None:
400 if isinstance(argument, str) or is_fwd_ref(
401 argument, check_generic=True
402 ):
403 if originating_module is None:
404 str_arg = (
405 argument.__forward_arg__
406 if hasattr(argument, "__forward_arg__")
407 else str(argument)
408 )
409 raise sa_exc.ArgumentError(
410 f"Can't use forward ref {argument} for composite "
411 f"class argument; set up the type as Mapped[{str_arg}]"
412 )
413 argument = de_stringify_annotation(
414 cls, argument, originating_module, include_generic=True
415 )
416
417 if is_union(argument) and includes_none(argument):
418 if self.return_none_on is _NoArg.NO_ARG:
419 self.return_none_on = lambda *args: all(
420 arg is None for arg in args
421 )
422 argument = de_optionalize_union_types(argument)
423
424 self.composite_class = argument
425
426 if is_dataclass(self.composite_class):
427 self._setup_for_dataclass(
428 decl_scan, registry, cls, originating_module, key
429 )
430 else:
431 for attr in self.attrs:
432 if (
433 isinstance(attr, (MappedColumn, schema.Column))
434 and attr.name is None
435 ):
436 raise sa_exc.ArgumentError(
437 "Composite class column arguments must be named "
438 "unless a dataclass is used"
439 )
440 self._init_accessor()
441
442 def _init_accessor(self) -> None:
443 if is_dataclass(self.composite_class) and not hasattr(
444 self.composite_class, "__composite_values__"
445 ):
446 insp = inspect.signature(self.composite_class)
447 getter = operator.attrgetter(
448 *[p.name for p in insp.parameters.values()]
449 )
450 if len(insp.parameters) == 1:
451 self._generated_composite_accessor = lambda obj: (getter(obj),)
452 else:
453 self._generated_composite_accessor = getter
454
455 if (
456 self.composite_class is not None
457 and isinstance(self.composite_class, type)
458 and self.composite_class not in _composite_getters
459 ):
460 if self._generated_composite_accessor is not None:
461 _composite_getters[self.composite_class] = (
462 self._generated_composite_accessor
463 )
464 elif hasattr(self.composite_class, "__composite_values__"):
465 _composite_getters[self.composite_class] = (
466 lambda obj: obj.__composite_values__()
467 )
468
469 @util.preload_module("sqlalchemy.orm.properties")
470 @util.preload_module("sqlalchemy.orm.decl_base")
471 def _setup_for_dataclass(
472 self,
473 decl_scan: _DeclarativeMapperConfig,
474 registry: _RegistryType,
475 cls: Type[Any],
476 originating_module: Optional[str],
477 key: str,
478 ) -> None:
479 MappedColumn = util.preloaded.orm_properties.MappedColumn
480
481 decl_base = util.preloaded.orm_decl_base
482
483 insp = inspect.signature(self.composite_class)
484 for param, attr in itertools.zip_longest(
485 insp.parameters.values(), self.attrs
486 ):
487 if param is None:
488 raise sa_exc.ArgumentError(
489 f"number of composite attributes "
490 f"{len(self.attrs)} exceeds "
491 f"that of the number of attributes in class "
492 f"{self.composite_class.__name__} {len(insp.parameters)}"
493 )
494 if attr is None:
495 # fill in missing attr spots with empty MappedColumn
496 attr = MappedColumn()
497 self.attrs += (attr,)
498
499 if isinstance(attr, MappedColumn):
500 attr.declarative_scan_for_composite(
501 decl_scan,
502 registry,
503 cls,
504 originating_module,
505 key,
506 param.name,
507 param.annotation,
508 )
509 elif isinstance(attr, schema.Column):
510 decl_base._undefer_column_name(param.name, attr)
511
512 @util.memoized_property
513 def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]:
514 return [getattr(self.parent.class_, prop.key) for prop in self.props]
515
516 @util.memoized_property
517 @util.preload_module("orm.properties")
518 def props(self) -> Sequence[MapperProperty[Any]]:
519 props = []
520 MappedColumn = util.preloaded.orm_properties.MappedColumn
521
522 for attr in self.attrs:
523 if isinstance(attr, str):
524 prop = self.parent.get_property(attr, _configure_mappers=False)
525 elif isinstance(attr, schema.Column):
526 prop = self.parent._columntoproperty[attr]
527 elif isinstance(attr, MappedColumn):
528 prop = self.parent._columntoproperty[attr.column]
529 elif isinstance(attr, attributes.InstrumentedAttribute):
530 prop = attr.property
531 else:
532 prop = None
533
534 if not isinstance(prop, MapperProperty):
535 raise sa_exc.ArgumentError(
536 "Composite expects Column objects or mapped "
537 f"attributes/attribute names as arguments, got: {attr!r}"
538 )
539
540 props.append(prop)
541 return props
542
543 def _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
544 return self._comparable_elements
545
546 @util.non_memoized_property
547 @util.preload_module("orm.properties")
548 def columns(self) -> Sequence[Column[Any]]:
549 MappedColumn = util.preloaded.orm_properties.MappedColumn
550 return [
551 a.column if isinstance(a, MappedColumn) else a
552 for a in self.attrs
553 if isinstance(a, (schema.Column, MappedColumn))
554 ]
555
556 @property
557 def mapper_property_to_assign(self) -> Optional[MapperProperty[_CC]]:
558 return self
559
560 @property
561 def columns_to_assign(self) -> List[Tuple[schema.Column[Any], int]]:
562 return [(c, 0) for c in self.columns if c.table is None]
563
564 @util.preload_module("orm.properties")
565 def _setup_arguments_on_columns(self) -> None:
566 """Propagate configuration arguments made on this composite
567 to the target columns, for those that apply.
568
569 """
570 ColumnProperty = util.preloaded.orm_properties.ColumnProperty
571
572 for prop in self.props:
573 if not isinstance(prop, ColumnProperty):
574 continue
575 else:
576 cprop = prop
577
578 cprop.active_history = self.active_history
579 if self.deferred:
580 cprop.deferred = self.deferred
581 cprop.strategy_key = (("deferred", True), ("instrument", True))
582 cprop.group = self.group
583
584 def _setup_event_handlers(self) -> None:
585 """Establish events that populate/expire the composite attribute."""
586
587 def load_handler(
588 state: InstanceState[Any], context: _ORMCompileState
589 ) -> None:
590 _load_refresh_handler(state, context, None, is_refresh=False)
591
592 def refresh_handler(
593 state: InstanceState[Any],
594 context: _ORMCompileState,
595 to_load: Optional[Sequence[str]],
596 ) -> None:
597 # note this corresponds to sqlalchemy.ext.mutable load_attrs()
598
599 if not to_load or (
600 {self.key}.union(self._attribute_keys)
601 ).intersection(to_load):
602 _load_refresh_handler(state, context, to_load, is_refresh=True)
603
604 def _load_refresh_handler(
605 state: InstanceState[Any],
606 context: _ORMCompileState,
607 to_load: Optional[Sequence[str]],
608 is_refresh: bool,
609 ) -> None:
610 dict_ = state.dict
611
612 # if context indicates we are coming from the
613 # fget() handler, this already set the value; skip the
614 # handler here. (other handlers like mutablecomposite will still
615 # want to catch it)
616 # there's an insufficiency here in that the fget() handler
617 # really should not be using the refresh event and there should
618 # be some other event that mutablecomposite can subscribe
619 # towards for this.
620
621 if (
622 not is_refresh or context is self._COMPOSITE_FGET
623 ) and self.key in dict_:
624 return
625
626 # if column elements aren't loaded, skip.
627 # __get__() will initiate a load for those
628 # columns
629 for k in self._attribute_keys:
630 if k not in dict_:
631 return
632
633 dict_[self.key] = self._construct_composite(
634 *[state.dict[key] for key in self._attribute_keys]
635 )
636
637 def expire_handler(
638 state: InstanceState[Any], keys: Optional[Sequence[str]]
639 ) -> None:
640 if keys is None or set(self._attribute_keys).intersection(keys):
641 state.dict.pop(self.key, None)
642
643 def insert_update_handler(
644 mapper: Mapper[Any],
645 connection: Connection,
646 state: InstanceState[Any],
647 ) -> None:
648 """After an insert or update, some columns may be expired due
649 to server side defaults, or re-populated due to client side
650 defaults. Pop out the composite value here so that it
651 recreates.
652
653 """
654
655 state.dict.pop(self.key, None)
656
657 event.listen(
658 self.parent, "after_insert", insert_update_handler, raw=True
659 )
660 event.listen(
661 self.parent, "after_update", insert_update_handler, raw=True
662 )
663 event.listen(
664 self.parent, "load", load_handler, raw=True, propagate=True
665 )
666 event.listen(
667 self.parent, "refresh", refresh_handler, raw=True, propagate=True
668 )
669 event.listen(
670 self.parent, "expire", expire_handler, raw=True, propagate=True
671 )
672
673 proxy_attr = self.parent.class_manager[self.key]
674 proxy_attr.impl.dispatch = proxy_attr.dispatch # type: ignore
675 proxy_attr.impl.dispatch._active_history = self.active_history
676
677 # TODO: need a deserialize hook here
678
679 @util.memoized_property
680 def _attribute_keys(self) -> Sequence[str]:
681 return [prop.key for prop in self.props]
682
683 def _populate_composite_bulk_save_mappings_fn(
684 self,
685 ) -> Callable[[Dict[str, Any]], None]:
686 if self._generated_composite_accessor:
687 get_values = self._generated_composite_accessor
688 else:
689
690 def get_values(val: Any) -> Tuple[Any]:
691 return val.__composite_values__() # type: ignore
692
693 attrs = [prop.key for prop in self.props]
694
695 def populate(dest_dict: Dict[str, Any]) -> None:
696 dest_dict.update(
697 {
698 key: val
699 for key, val in zip(
700 attrs, get_values(dest_dict.pop(self.key))
701 )
702 }
703 )
704
705 return populate
706
707 def get_history(
708 self,
709 state: InstanceState[Any],
710 dict_: _InstanceDict,
711 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
712 ) -> History:
713 """Provided for userland code that uses attributes.get_history()."""
714
715 added: List[Any] = []
716 deleted: List[Any] = []
717
718 has_history = False
719 for prop in self.props:
720 key = prop.key
721 hist = state.manager[key].impl.get_history(state, dict_)
722 if hist.has_changes():
723 has_history = True
724
725 non_deleted = hist.non_deleted()
726 if non_deleted:
727 added.extend(non_deleted)
728 else:
729 added.append(None)
730 if hist.deleted:
731 deleted.extend(hist.deleted)
732 else:
733 deleted.append(None)
734
735 if has_history:
736 return attributes.History(
737 [self._construct_composite(*added)],
738 (),
739 [self._construct_composite(*deleted)],
740 )
741 else:
742 return attributes.History(
743 (), [self._construct_composite(*added)], ()
744 )
745
746 def _comparator_factory(
747 self, mapper: Mapper[Any]
748 ) -> Composite.Comparator[_CC]:
749 return self.comparator_factory(self, mapper)
750
751 class CompositeBundle(orm_util.Bundle[_T]):
752 def __init__(
753 self,
754 property_: Composite[_T],
755 expr: ClauseList,
756 ):
757 self.property = property_
758 super().__init__(property_.key, *expr)
759
760 def create_row_processor(
761 self,
762 query: Select[Unpack[TupleAny]],
763 procs: Sequence[Callable[[Row[Unpack[TupleAny]]], Any]],
764 labels: Sequence[str],
765 ) -> Callable[[Row[Unpack[TupleAny]]], Any]:
766 def proc(row: Row[Unpack[TupleAny]]) -> Any:
767 return self.property._construct_composite(
768 *[proc(row) for proc in procs]
769 )
770
771 return proc
772
773 class Comparator(PropComparator[_PT]):
774 """Produce boolean, comparison, and other operators for
775 :class:`.Composite` attributes.
776
777 See the example in :ref:`composite_operations` for an overview
778 of usage , as well as the documentation for :class:`.PropComparator`.
779
780 .. seealso::
781
782 :class:`.PropComparator`
783
784 :class:`.ColumnOperators`
785
786 :ref:`types_operators`
787
788 :attr:`.TypeEngine.comparator_factory`
789
790 """
791
792 # https://github.com/python/mypy/issues/4266
793 __hash__ = None # type: ignore
794
795 prop: RODescriptorReference[Composite[_PT]]
796
797 @util.memoized_property
798 def clauses(self) -> ClauseList:
799 return expression.ClauseList(
800 group=False, *self._comparable_elements
801 )
802
803 def __clause_element__(self) -> CompositeProperty.CompositeBundle[_PT]:
804 return self.expression
805
806 @util.memoized_property
807 def expression(self) -> CompositeProperty.CompositeBundle[_PT]:
808 clauses = self.clauses._annotate(
809 {
810 "parententity": self._parententity,
811 "parentmapper": self._parententity,
812 "proxy_key": self.prop.key,
813 }
814 )
815 return CompositeProperty.CompositeBundle(self.prop, clauses)
816
817 def _bulk_update_tuples(
818 self, value: Any
819 ) -> Sequence[Tuple[_DMLColumnArgument, Any]]:
820 if isinstance(value, BindParameter):
821 value = value.value
822
823 values: Sequence[Any]
824
825 if value is None:
826 values = [None for key in self.prop._attribute_keys]
827 elif isinstance(self.prop.composite_class, type) and isinstance(
828 value, self.prop.composite_class
829 ):
830 values = self.prop._composite_values_from_instance(
831 value # type: ignore[arg-type]
832 )
833 else:
834 raise sa_exc.ArgumentError(
835 "Can't UPDATE composite attribute %s to %r"
836 % (self.prop, value)
837 )
838
839 return list(zip(self._comparable_elements, values))
840
841 def _bulk_dml_setter(self, key: str) -> Optional[Callable[..., Any]]:
842 return self.prop._populate_composite_bulk_save_mappings_fn()
843
844 @util.memoized_property
845 def _comparable_elements(self) -> Sequence[QueryableAttribute[Any]]:
846 if self._adapt_to_entity:
847 return [
848 getattr(self._adapt_to_entity.entity, prop.key)
849 for prop in self.prop._comparable_elements
850 ]
851 else:
852 return self.prop._comparable_elements
853
854 def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
855 return self._compare(operators.eq, other)
856
857 def __ne__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
858 return self._compare(operators.ne, other)
859
860 def __lt__(self, other: Any) -> ColumnElement[bool]:
861 return self._compare(operators.lt, other)
862
863 def __gt__(self, other: Any) -> ColumnElement[bool]:
864 return self._compare(operators.gt, other)
865
866 def __le__(self, other: Any) -> ColumnElement[bool]:
867 return self._compare(operators.le, other)
868
869 def __ge__(self, other: Any) -> ColumnElement[bool]:
870 return self._compare(operators.ge, other)
871
872 def desc(self) -> operators.OrderingOperators: # type: ignore[override] # noqa: E501
873 return expression.OrderByList(
874 [e.desc() for e in self._comparable_elements]
875 )
876
877 def asc(self) -> operators.OrderingOperators: # type: ignore[override] # noqa: E501
878 return expression.OrderByList(
879 [e.asc() for e in self._comparable_elements]
880 )
881
882 def nulls_first(self) -> operators.OrderingOperators: # type: ignore[override] # noqa: E501
883 return expression.OrderByList(
884 [e.nulls_first() for e in self._comparable_elements]
885 )
886
887 def nulls_last(self) -> operators.OrderingOperators: # type: ignore[override] # noqa: E501
888 return expression.OrderByList(
889 [e.nulls_last() for e in self._comparable_elements]
890 )
891
892 # what might be interesting would be if we create
893 # an instance of the composite class itself with
894 # the columns as data members, then use "hybrid style" comparison
895 # to create these comparisons. then your Point.__eq__() method could
896 # be where comparison behavior is defined for SQL also. Likely
897 # not a good choice for default behavior though, not clear how it would
898 # work w/ dataclasses, etc. also no demand for any of this anyway.
899 def _compare(
900 self, operator: OperatorType, other: Any
901 ) -> ColumnElement[bool]:
902 values: Sequence[Any]
903 if other is None:
904 values = [None] * len(self.prop._comparable_elements)
905 else:
906 values = self.prop._composite_values_from_instance(other)
907 comparisons = [
908 operator(a, b)
909 for a, b in zip(self.prop._comparable_elements, values)
910 ]
911 if self._adapt_to_entity:
912 assert self.adapter is not None
913 comparisons = [self.adapter(x) for x in comparisons]
914 return sql.and_(*comparisons)
915
916 def __str__(self) -> str:
917 return str(self.parent.class_.__name__) + "." + self.key
918
919
920class Composite(CompositeProperty[_T], _DeclarativeMapped[_T]):
921 """Declarative-compatible front-end for the :class:`.CompositeProperty`
922 class.
923
924 Public constructor is the :func:`_orm.composite` function.
925
926 .. versionchanged:: 2.0 Added :class:`_orm.Composite` as a Declarative
927 compatible subclass of :class:`_orm.CompositeProperty`.
928
929 .. seealso::
930
931 :ref:`mapper_composite`
932
933 """
934
935 inherit_cache = True
936 """:meta private:"""
937
938
939class ConcreteInheritedProperty(DescriptorProperty[_T]):
940 """A 'do nothing' :class:`.MapperProperty` that disables
941 an attribute on a concrete subclass that is only present
942 on the inherited mapper, not the concrete classes' mapper.
943
944 Cases where this occurs include:
945
946 * When the superclass mapper is mapped against a
947 "polymorphic union", which includes all attributes from
948 all subclasses.
949 * When a relationship() is configured on an inherited mapper,
950 but not on the subclass mapper. Concrete mappers require
951 that relationship() is configured explicitly on each
952 subclass.
953
954 """
955
956 def _comparator_factory(
957 self, mapper: Mapper[Any]
958 ) -> Type[PropComparator[_T]]:
959 comparator_callable = None
960
961 for m in self.parent.iterate_to_root():
962 p = m._props[self.key]
963 if getattr(p, "comparator_factory", None) is not None:
964 comparator_callable = p.comparator_factory
965 break
966 assert comparator_callable is not None
967 return comparator_callable(p, mapper) # type: ignore
968
969 def __init__(self) -> None:
970 super().__init__()
971
972 def warn() -> NoReturn:
973 raise AttributeError(
974 "Concrete %s does not implement "
975 "attribute %r at the instance level. Add "
976 "this property explicitly to %s."
977 % (self.parent, self.key, self.parent)
978 )
979
980 class NoninheritedConcreteProp:
981 def __set__(s: Any, obj: Any, value: Any) -> NoReturn:
982 warn()
983
984 def __delete__(s: Any, obj: Any) -> NoReturn:
985 warn()
986
987 def __get__(s: Any, obj: Any, owner: Any) -> Any:
988 if obj is None:
989 return self.descriptor
990 warn()
991
992 self.descriptor = NoninheritedConcreteProp()
993
994
995class SynonymProperty(DescriptorProperty[_T]):
996 """Denote an attribute name as a synonym to a mapped property,
997 in that the attribute will mirror the value and expression behavior
998 of another attribute.
999
1000 :class:`.Synonym` is constructed using the :func:`_orm.synonym`
1001 function.
1002
1003 .. seealso::
1004
1005 :ref:`synonyms` - Overview of synonyms
1006
1007 """
1008
1009 comparator_factory: Optional[Type[PropComparator[_T]]]
1010
1011 def __init__(
1012 self,
1013 name: str,
1014 map_column: Optional[bool] = None,
1015 descriptor: Optional[Any] = None,
1016 comparator_factory: Optional[Type[PropComparator[_T]]] = None,
1017 attribute_options: Optional[_AttributeOptions] = None,
1018 info: Optional[_InfoType] = None,
1019 doc: Optional[str] = None,
1020 ):
1021 super().__init__(attribute_options=attribute_options)
1022
1023 self.name = name
1024 self.map_column = map_column
1025 self.descriptor = descriptor
1026 self.comparator_factory = comparator_factory
1027 if doc:
1028 self.doc = doc
1029 elif descriptor and descriptor.__doc__:
1030 self.doc = descriptor.__doc__
1031 else:
1032 self.doc = None
1033 if info:
1034 self.info.update(info)
1035
1036 util.set_creation_order(self)
1037
1038 if not TYPE_CHECKING:
1039
1040 @property
1041 def uses_objects(self) -> bool:
1042 return getattr(self.parent.class_, self.name).impl.uses_objects
1043
1044 # TODO: when initialized, check _proxied_object,
1045 # emit a warning if its not a column-based property
1046
1047 @util.memoized_property
1048 def _proxied_object(
1049 self,
1050 ) -> Union[MapperProperty[_T], SQLORMOperations[_T]]:
1051 attr = getattr(self.parent.class_, self.name)
1052 if not hasattr(attr, "property") or not isinstance(
1053 attr.property, MapperProperty
1054 ):
1055 # attribute is a non-MapperProprerty proxy such as
1056 # hybrid or association proxy
1057 if isinstance(attr, attributes.QueryableAttribute):
1058 return attr.comparator
1059 elif isinstance(attr, SQLORMOperations):
1060 # assocaition proxy comes here
1061 return attr
1062
1063 raise sa_exc.InvalidRequestError(
1064 """synonym() attribute "%s.%s" only supports """
1065 """ORM mapped attributes, got %r"""
1066 % (self.parent.class_.__name__, self.name, attr)
1067 )
1068 return attr.property
1069
1070 def _column_strategy_attrs(self) -> Sequence[QueryableAttribute[Any]]:
1071 return (getattr(self.parent.class_, self.name),)
1072
1073 def _comparator_factory(self, mapper: Mapper[Any]) -> SQLORMOperations[_T]:
1074 prop = self._proxied_object
1075
1076 if isinstance(prop, MapperProperty):
1077 if self.comparator_factory:
1078 comp = self.comparator_factory(prop, mapper)
1079 else:
1080 comp = prop.comparator_factory(prop, mapper)
1081 return comp
1082 else:
1083 return prop
1084
1085 def get_history(
1086 self,
1087 state: InstanceState[Any],
1088 dict_: _InstanceDict,
1089 passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
1090 ) -> History:
1091 attr: QueryableAttribute[Any] = getattr(self.parent.class_, self.name)
1092 return attr.impl.get_history(state, dict_, passive=passive)
1093
1094 def _get_dataclass_setup_options(
1095 self,
1096 decl_scan: _ClassScanAbstractConfig,
1097 key: str,
1098 dataclass_setup_arguments: _DataclassArguments,
1099 enable_descriptor_defaults: bool,
1100 ) -> _AttributeOptions:
1101 dataclasses_default = self._attribute_options.dataclasses_default
1102 if (
1103 dataclasses_default is not _NoArg.NO_ARG
1104 and not callable(dataclasses_default)
1105 and enable_descriptor_defaults
1106 and not getattr(
1107 decl_scan.cls, "_sa_disable_descriptor_defaults", False
1108 )
1109 ):
1110 proxied = decl_scan.collected_attributes[self.name]
1111 proxied_default = proxied._attribute_options.dataclasses_default
1112 if proxied_default != dataclasses_default:
1113 raise sa_exc.ArgumentError(
1114 f"Synonym {key!r} default argument "
1115 f"{dataclasses_default!r} must match the dataclasses "
1116 f"default value of proxied object {self.name!r}, "
1117 f"""currently {
1118 repr(proxied_default)
1119 if proxied_default is not _NoArg.NO_ARG
1120 else 'not set'}"""
1121 )
1122 self._default_scalar_value = dataclasses_default
1123 return self._attribute_options._replace(
1124 dataclasses_default=DONT_SET
1125 )
1126
1127 return self._attribute_options
1128
1129 @util.preload_module("sqlalchemy.orm.properties")
1130 def set_parent(self, parent: Mapper[Any], init: bool) -> None:
1131 properties = util.preloaded.orm_properties
1132
1133 if self.map_column:
1134 # implement the 'map_column' option.
1135 if self.key not in parent.persist_selectable.c:
1136 raise sa_exc.ArgumentError(
1137 "Can't compile synonym '%s': no column on table "
1138 "'%s' named '%s'"
1139 % (
1140 self.name,
1141 parent.persist_selectable.description,
1142 self.key,
1143 )
1144 )
1145 elif (
1146 parent.persist_selectable.c[self.key]
1147 in parent._columntoproperty
1148 and parent._columntoproperty[
1149 parent.persist_selectable.c[self.key]
1150 ].key
1151 == self.name
1152 ):
1153 raise sa_exc.ArgumentError(
1154 "Can't call map_column=True for synonym %r=%r, "
1155 "a ColumnProperty already exists keyed to the name "
1156 "%r for column %r"
1157 % (self.key, self.name, self.name, self.key)
1158 )
1159 p: ColumnProperty[Any] = properties.ColumnProperty(
1160 parent.persist_selectable.c[self.key]
1161 )
1162 parent._configure_property(self.name, p, init=init, setparent=True)
1163 p._mapped_by_synonym = self.key
1164
1165 self.parent = parent
1166
1167
1168class Synonym(SynonymProperty[_T], _DeclarativeMapped[_T]):
1169 """Declarative front-end for the :class:`.SynonymProperty` class.
1170
1171 Public constructor is the :func:`_orm.synonym` function.
1172
1173 .. versionchanged:: 2.0 Added :class:`_orm.Synonym` as a Declarative
1174 compatible subclass for :class:`_orm.SynonymProperty`
1175
1176 .. seealso::
1177
1178 :ref:`synonyms` - Overview of synonyms
1179
1180 """
1181
1182 inherit_cache = True
1183 """:meta private:"""