summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authormike bayer <mike_mp@zzzcomputing.com>2022-12-01 21:41:21 +0000
committerGerrit Code Review <gerrit@ci3.zzzcomputing.com>2022-12-01 21:41:21 +0000
commit123fffd7bf159d5b1c5a6a3e54f50945dc48ab2a (patch)
treee1560d41eb90b1d954ca46fe0c7bd44a3523d464 /lib/sqlalchemy
parent990663c732e5bde43ed05eba0ade6d96fc7a2b26 (diff)
parentde68627dd1ba9c2dd44bb3d11be1a3945b285205 (diff)
downloadsqlalchemy-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.py14
-rw-r--r--lib/sqlalchemy/orm/decl_base.py84
-rw-r--r--lib/sqlalchemy/orm/descriptor_props.py2
-rw-r--r--lib/sqlalchemy/orm/interfaces.py2
-rw-r--r--lib/sqlalchemy/orm/properties.py20
-rw-r--r--lib/sqlalchemy/orm/relationships.py2
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],