diff options
| author | mike bayer <mike_mp@zzzcomputing.com> | 2022-12-01 21:41:21 +0000 |
|---|---|---|
| committer | Gerrit Code Review <gerrit@ci3.zzzcomputing.com> | 2022-12-01 21:41:21 +0000 |
| commit | 123fffd7bf159d5b1c5a6a3e54f50945dc48ab2a (patch) | |
| tree | e1560d41eb90b1d954ca46fe0c7bd44a3523d464 /lib/sqlalchemy | |
| parent | 990663c732e5bde43ed05eba0ade6d96fc7a2b26 (diff) | |
| parent | de68627dd1ba9c2dd44bb3d11be1a3945b285205 (diff) | |
| download | sqlalchemy-123fffd7bf159d5b1c5a6a3e54f50945dc48ab2a.tar.gz | |
Merge "add new pattern for single inh column override" into main
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/orm/_orm_constructors.py | 14 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/decl_base.py | 84 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/descriptor_props.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/properties.py | 20 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 2 |
6 files changed, 93 insertions, 31 deletions
diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py index fe5df2105..cb28ac060 100644 --- a/lib/sqlalchemy/orm/_orm_constructors.py +++ b/lib/sqlalchemy/orm/_orm_constructors.py @@ -113,6 +113,7 @@ def mapped_column( deferred: Union[_NoArg, bool] = _NoArg.NO_ARG, deferred_group: Optional[str] = None, deferred_raiseload: bool = False, + use_existing_column: bool = False, name: Optional[str] = None, type_: Optional[_TypeEngineArgument[Any]] = None, autoincrement: Union[bool, Literal["auto", "ignore_fk"]] = "auto", @@ -209,6 +210,18 @@ def mapped_column( :ref:`orm_queryguide_deferred_raiseload` + :param use_existing_column: if True, will attempt to locate the given + column name on an inherited superclass (typically single inheriting + superclass), and if present, will not produce a new column, mapping + to the superclass column as though it were omitted from this class. + This is used for mixins that add new columns to an inherited superclass. + + .. seealso:: + + :ref:`orm_inheritance_column_conflicts` + + .. versionadded:: 2.0.0b4 + :param default: Passed directly to the :paramref:`_schema.Column.default` parameter if the :paramref:`_orm.mapped_column.insert_default` parameter is not present. @@ -283,6 +296,7 @@ def mapped_column( primary_key=primary_key, server_default=server_default, server_onupdate=server_onupdate, + use_existing_column=use_existing_column, quote=quote, comment=comment, system=system, diff --git a/lib/sqlalchemy/orm/decl_base.py b/lib/sqlalchemy/orm/decl_base.py index 1e716e687..797828377 100644 --- a/lib/sqlalchemy/orm/decl_base.py +++ b/lib/sqlalchemy/orm/decl_base.py @@ -128,6 +128,28 @@ def _declared_mapping_info( return None +def _is_supercls_for_inherits(cls: Type[Any]) -> bool: + """return True if this class will be used as a superclass to set in + 'inherits'. + + This includes deferred mapper configs that aren't mapped yet, however does + not include classes with _sa_decl_prepare_nocascade (e.g. + ``AbstractConcreteBase``); these concrete-only classes are not set up as + "inherits" until after mappers are configured using + mapper._set_concrete_base() + + """ + if _DeferredMapperConfig.has_cls(cls): + return not _get_immediate_cls_attr( + cls, "_sa_decl_prepare_nocascade", strict=True + ) + # regular mapping + elif _is_mapped_class(cls): + return True + else: + return False + + def _resolve_for_abstract_or_classical(cls: Type[Any]) -> Optional[Type[Any]]: if cls is object: return None @@ -380,11 +402,8 @@ class _ImperativeMapperConfig(_MapperConfig): c = _resolve_for_abstract_or_classical(base_) if c is None: continue - if _declared_mapping_info( - c - ) is not None and not _get_immediate_cls_attr( - c, "_sa_decl_prepare_nocascade", strict=True - ): + + if _is_supercls_for_inherits(c) and c not in inherits_search: inherits_search.append(c) if inherits_search: @@ -430,6 +449,7 @@ class _ClassScanMapperConfig(_MapperConfig): "allow_unmapped_annotations", ) + is_deferred = False registry: _RegistryType clsdict_view: _ClassDict collected_annotations: Dict[str, _CollectedAnnotation] @@ -532,13 +552,15 @@ class _ClassScanMapperConfig(_MapperConfig): self.classname, self.cls, registry._class_registry ) + self._setup_inheriting_mapper(mapper_kw) + self._extract_mappable_attributes() self._extract_declared_columns() self._setup_table(table) - self._setup_inheritance(mapper_kw) + self._setup_inheriting_columns(mapper_kw) self._early_mapping(mapper_kw) @@ -739,13 +761,7 @@ class _ClassScanMapperConfig(_MapperConfig): # need to do this all the way up the hierarchy first # (see #8190) - class_mapped = ( - base is not cls - and _declared_mapping_info(base) is not None - and not _get_immediate_cls_attr( - base, "_sa_decl_prepare_nocascade", strict=True - ) - ) + class_mapped = base is not cls and _is_supercls_for_inherits(base) local_attributes_for_class = self._cls_attr_resolver(base) @@ -1358,6 +1374,7 @@ class _ClassScanMapperConfig(_MapperConfig): if mapped_container is not None or annotation is None: try: value.declarative_scan( + self, self.registry, cls, originating_module, @@ -1558,11 +1575,8 @@ class _ClassScanMapperConfig(_MapperConfig): else: return manager.registry.metadata - def _setup_inheritance(self, mapper_kw: _MapperKwArgs) -> None: - table = self.local_table + def _setup_inheriting_mapper(self, mapper_kw: _MapperKwArgs) -> None: cls = self.cls - table_args = self.table_args - declared_columns = self.declared_columns inherits = mapper_kw.get("inherits", None) @@ -1574,13 +1588,9 @@ class _ClassScanMapperConfig(_MapperConfig): c = _resolve_for_abstract_or_classical(base_) if c is None: continue - if _declared_mapping_info( - c - ) is not None and not _get_immediate_cls_attr( - c, "_sa_decl_prepare_nocascade", strict=True - ): - if c not in inherits_search: - inherits_search.append(c) + + if _is_supercls_for_inherits(c) and c not in inherits_search: + inherits_search.append(c) if inherits_search: if len(inherits_search) > 1: @@ -1594,6 +1604,12 @@ class _ClassScanMapperConfig(_MapperConfig): self.inherits = inherits + def _setup_inheriting_columns(self, mapper_kw: _MapperKwArgs) -> None: + table = self.local_table + cls = self.cls + table_args = self.table_args + declared_columns = self.declared_columns + if ( table is None and self.inherits is None @@ -1636,9 +1652,12 @@ class _ClassScanMapperConfig(_MapperConfig): if inherited_table.c[col.name] is col: continue raise exc.ArgumentError( - "Column '%s' on class %s conflicts with " - "existing column '%s'" - % (col, cls, inherited_table.c[col.name]) + f"Column '{col}' on class {cls.__name__} " + f"conflicts with existing column " + f"'{inherited_table.c[col.name]}'. If using " + f"Declarative, consider using the " + "use_existing_column parameter of mapped_column() " + "to resolve conflicts." ) if col.primary_key: raise exc.ArgumentError( @@ -1695,14 +1714,15 @@ class _ClassScanMapperConfig(_MapperConfig): mapper_args["inherits"] = self.inherits if self.inherits and not mapper_args.get("concrete", False): + # note the superclass is expected to have a Mapper assigned and + # not be a deferred config, as this is called within map() + inherited_mapper = class_mapper(self.inherits, False) + inherited_table = inherited_mapper.local_table + # single or joined inheritance # exclude any cols on the inherited table which are # not mapped on the parent class, to avoid # mapping columns specific to sibling/nephew classes - inherited_mapper = _declared_mapping_info(self.inherits) - assert isinstance(inherited_mapper, Mapper) - inherited_table = inherited_mapper.local_table - if "exclude_properties" not in mapper_args: mapper_args["exclude_properties"] = exclude_properties = { c.key @@ -1768,6 +1788,8 @@ def _as_dc_declaredattr( class _DeferredMapperConfig(_ClassScanMapperConfig): _cls: weakref.ref[Type[Any]] + is_deferred = True + _configs: util.OrderedDict[ weakref.ref[Type[Any]], _DeferredMapperConfig ] = util.OrderedDict() diff --git a/lib/sqlalchemy/orm/descriptor_props.py b/lib/sqlalchemy/orm/descriptor_props.py index 55c7e9290..56d6b2f6f 100644 --- a/lib/sqlalchemy/orm/descriptor_props.py +++ b/lib/sqlalchemy/orm/descriptor_props.py @@ -64,6 +64,7 @@ if typing.TYPE_CHECKING: from .attributes import InstrumentedAttribute from .attributes import QueryableAttribute from .context import ORMCompileState + from .decl_base import _ClassScanMapperConfig from .mapper import Mapper from .properties import ColumnProperty from .properties import MappedColumn @@ -332,6 +333,7 @@ class CompositeProperty( @util.preload_module("sqlalchemy.orm.properties") def declarative_scan( self, + decl_scan: _ClassScanMapperConfig, registry: _RegistryType, cls: Type[Any], originating_module: Optional[str], diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 48d0689f8..939426495 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -84,6 +84,7 @@ if typing.TYPE_CHECKING: from .context import ORMCompileState from .context import QueryContext from .decl_api import RegistryType + from .decl_base import _ClassScanMapperConfig from .loading import _PopulatorDict from .mapper import Mapper from .path_registry import AbstractEntityRegistry @@ -157,6 +158,7 @@ class _IntrospectsAnnotations: def declarative_scan( self, + decl_scan: _ClassScanMapperConfig, registry: RegistryType, cls: Type[Any], originating_module: Optional[str], diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 0b26cb872..21df672ec 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -28,6 +28,7 @@ from typing import TypeVar from . import attributes from . import strategy_options from .base import _DeclarativeMapped +from .base import class_mapper from .descriptor_props import CompositeProperty from .descriptor_props import ConcreteInheritedProperty from .descriptor_props import SynonymProperty @@ -65,6 +66,7 @@ if TYPE_CHECKING: from ._typing import _ORMColumnExprArgument from ._typing import _RegistryType from .base import Mapped + from .decl_base import _ClassScanMapperConfig from .mapper import Mapper from .session import Session from .state import _InstallLoaderCallableProto @@ -191,6 +193,7 @@ class ColumnProperty( def declarative_scan( self, + decl_scan: _ClassScanMapperConfig, registry: _RegistryType, cls: Type[Any], originating_module: Optional[str], @@ -530,6 +533,7 @@ class MappedColumn( "deferred_raiseload", "_attribute_options", "_has_dataclass_arguments", + "_use_existing_column", ) deferred: bool @@ -545,6 +549,8 @@ class MappedColumn( "attribute_options", _DEFAULT_ATTRIBUTE_OPTIONS ) + self._use_existing_column = kw.pop("use_existing_column", False) + self._has_dataclass_arguments = False if attr_opts is not None and attr_opts != _DEFAULT_ATTRIBUTE_OPTIONS: @@ -591,6 +597,7 @@ class MappedColumn( new._attribute_options = self._attribute_options new._has_insert_default = self._has_insert_default new._has_dataclass_arguments = self._has_dataclass_arguments + new._use_existing_column = self._use_existing_column util.set_creation_order(new) return new @@ -634,6 +641,7 @@ class MappedColumn( def declarative_scan( self, + decl_scan: _ClassScanMapperConfig, registry: _RegistryType, cls: Type[Any], originating_module: Optional[str], @@ -644,6 +652,18 @@ class MappedColumn( is_dataclass_field: bool, ) -> None: column = self.column + + if self._use_existing_column and decl_scan.inherits: + if decl_scan.is_deferred: + raise sa_exc.ArgumentError( + "Can't use use_existing_column with deferred mappers" + ) + supercls_mapper = class_mapper(decl_scan.inherits, False) + + column = self.column = supercls_mapper.local_table.c.get( # type: ignore # noqa: E501 + key, column + ) + if column.key is None: column.key = key if column.name is None: diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index 73d11e880..4a9bcd711 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -101,6 +101,7 @@ if typing.TYPE_CHECKING: from .base import Mapped from .clsregistry import _class_resolver from .clsregistry import _ModNS + from .decl_base import _ClassScanMapperConfig from .dependency import DependencyProcessor from .mapper import Mapper from .query import Query @@ -1723,6 +1724,7 @@ class RelationshipProperty( def declarative_scan( self, + decl_scan: _ClassScanMapperConfig, registry: _RegistryType, cls: Type[Any], originating_module: Optional[str], |
