summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/connectors/pyodbc.py5
-rw-r--r--lib/sqlalchemy/dialects/sqlite/base.py12
-rw-r--r--lib/sqlalchemy/ext/associationproxy.py10
-rw-r--r--lib/sqlalchemy/orm/_orm_constructors.py57
-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.py12
-rw-r--r--lib/sqlalchemy/orm/properties.py20
-rw-r--r--lib/sqlalchemy/orm/relationships.py2
9 files changed, 163 insertions, 41 deletions
diff --git a/lib/sqlalchemy/connectors/pyodbc.py b/lib/sqlalchemy/connectors/pyodbc.py
index f920860cd..e8fbed18e 100644
--- a/lib/sqlalchemy/connectors/pyodbc.py
+++ b/lib/sqlalchemy/connectors/pyodbc.py
@@ -209,7 +209,10 @@ class PyODBCConnector(Connector):
# omit the setinputsizes calls for .executemany() with
# fast_executemany=True
- if context.executemany and self.fast_executemany:
+ if (
+ context.execute_style is interfaces.ExecuteStyle.EXECUTEMANY
+ and self.fast_executemany
+ ):
return
cursor.setinputsizes(
diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py
index 11554fcc0..fb9a19c89 100644
--- a/lib/sqlalchemy/dialects/sqlite/base.py
+++ b/lib/sqlalchemy/dialects/sqlite/base.py
@@ -2462,6 +2462,8 @@ class SQLiteDialect(default.DefaultDialect):
r'REFERENCES +(?:(?:"(.+?)")|([a-z0-9_]+)) *\((.+?)\) *'
r"((?:ON (?:DELETE|UPDATE) "
r"(?:SET NULL|SET DEFAULT|CASCADE|RESTRICT|NO ACTION) *)*)"
+ r"((?:NOT +)?DEFERRABLE)?"
+ r"(?: +INITIALLY +(DEFERRED|IMMEDIATE))?"
)
for match in re.finditer(FK_PATTERN, table_data, re.I):
(
@@ -2471,7 +2473,9 @@ class SQLiteDialect(default.DefaultDialect):
referred_name,
referred_columns,
onupdatedelete,
- ) = match.group(1, 2, 3, 4, 5, 6)
+ deferrable,
+ initially,
+ ) = match.group(1, 2, 3, 4, 5, 6, 7, 8)
constrained_columns = list(
self._find_cols_in_sig(constrained_columns)
)
@@ -2493,6 +2497,12 @@ class SQLiteDialect(default.DefaultDialect):
onupdate = token[6:].strip()
if onupdate and onupdate != "NO ACTION":
options["onupdate"] = onupdate
+
+ if deferrable:
+ options["deferrable"] = "NOT" not in deferrable.upper()
+ if initially:
+ options["initially"] = initially.upper()
+
yield (
constraint_name,
constrained_columns,
diff --git a/lib/sqlalchemy/ext/associationproxy.py b/lib/sqlalchemy/ext/associationproxy.py
index 15193e563..4d38ac536 100644
--- a/lib/sqlalchemy/ext/associationproxy.py
+++ b/lib/sqlalchemy/ext/associationproxy.py
@@ -94,6 +94,7 @@ def association_proxy(
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
default: Optional[Any] = _NoArg.NO_ARG,
default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
) -> AssociationProxy[Any]:
r"""Return a Python property implementing a view of a target
@@ -174,6 +175,13 @@ def association_proxy(
.. versionadded:: 2.0.0b4
+ :param compare: Specific to
+ :ref:`orm_declarative_native_dataclasses`, indicates if this field
+ should be included in comparison operations when generating the
+ ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+ .. versionadded:: 2.0.0b4
+
:param kw_only: Specific to :ref:`orm_declarative_native_dataclasses`,
indicates if this field should be marked as keyword-only when generating
the ``__init__()`` method as generated by the dataclass process.
@@ -218,7 +226,7 @@ def association_proxy(
info=info,
cascade_scalar_deletes=cascade_scalar_deletes,
attribute_options=_AttributeOptions(
- init, repr, default, default_factory, kw_only
+ init, repr, default, default_factory, compare, kw_only
),
)
diff --git a/lib/sqlalchemy/orm/_orm_constructors.py b/lib/sqlalchemy/orm/_orm_constructors.py
index 2450d1e83..cb28ac060 100644
--- a/lib/sqlalchemy/orm/_orm_constructors.py
+++ b/lib/sqlalchemy/orm/_orm_constructors.py
@@ -104,6 +104,7 @@ def mapped_column(
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
default: Optional[Any] = _NoArg.NO_ARG,
default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
nullable: Optional[
Union[bool, Literal[SchemaConst.NULL_UNSPECIFIED]]
@@ -112,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",
@@ -208,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.
@@ -245,6 +259,13 @@ def mapped_column(
specifies a default-value generation function that will take place
as part of the ``__init__()``
method as generated by the dataclass process.
+ :param compare: Specific to
+ :ref:`orm_declarative_native_dataclasses`, indicates if this field
+ should be included in comparison operations when generating the
+ ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+ .. versionadded:: 2.0.0b4
+
:param kw_only: Specific to
:ref:`orm_declarative_native_dataclasses`, indicates if this field
should be marked as keyword-only when generating the ``__init__()``.
@@ -263,7 +284,7 @@ def mapped_column(
autoincrement=autoincrement,
insert_default=insert_default,
attribute_options=_AttributeOptions(
- init, repr, default, default_factory, kw_only
+ init, repr, default, default_factory, compare, kw_only
),
doc=doc,
key=key,
@@ -275,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,
@@ -296,6 +318,7 @@ def column_property(
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
default: Optional[Any] = _NoArg.NO_ARG,
default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
active_history: bool = False,
expire_on_flush: bool = True,
@@ -389,7 +412,7 @@ def column_property(
column,
*additional_columns,
attribute_options=_AttributeOptions(
- init, repr, default, default_factory, kw_only
+ init, repr, default, default_factory, compare, kw_only
),
group=group,
deferred=deferred,
@@ -415,6 +438,7 @@ def composite(
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
default: Optional[Any] = _NoArg.NO_ARG,
default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
info: Optional[_InfoType] = None,
doc: Optional[str] = None,
@@ -436,6 +460,7 @@ def composite(
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
default: Optional[Any] = _NoArg.NO_ARG,
default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
info: Optional[_InfoType] = None,
doc: Optional[str] = None,
@@ -458,6 +483,7 @@ def composite(
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
default: Optional[Any] = _NoArg.NO_ARG,
default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
info: Optional[_InfoType] = None,
doc: Optional[str] = None,
@@ -521,6 +547,14 @@ def composite(
specifies a default-value generation function that will take place
as part of the ``__init__()``
method as generated by the dataclass process.
+
+ :param compare: Specific to
+ :ref:`orm_declarative_native_dataclasses`, indicates if this field
+ should be included in comparison operations when generating the
+ ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+ .. versionadded:: 2.0.0b4
+
:param kw_only: Specific to
:ref:`orm_declarative_native_dataclasses`, indicates if this field
should be marked as keyword-only when generating the ``__init__()``.
@@ -533,7 +567,7 @@ def composite(
_class_or_attr,
*attrs,
attribute_options=_AttributeOptions(
- init, repr, default, default_factory, kw_only
+ init, repr, default, default_factory, compare, kw_only
),
group=group,
deferred=deferred,
@@ -756,6 +790,7 @@ def relationship(
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
default: Union[_NoArg, _T] = _NoArg.NO_ARG,
default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
lazy: _LazyLoadArgumentType = "select",
passive_deletes: Union[Literal["all"], bool] = False,
@@ -1593,6 +1628,13 @@ def relationship(
specifies a default-value generation function that will take place
as part of the ``__init__()``
method as generated by the dataclass process.
+ :param compare: Specific to
+ :ref:`orm_declarative_native_dataclasses`, indicates if this field
+ should be included in comparison operations when generating the
+ ``__eq__()`` and ``__ne__()`` methods for the mapped class.
+
+ .. versionadded:: 2.0.0b4
+
:param kw_only: Specific to
:ref:`orm_declarative_native_dataclasses`, indicates if this field
should be marked as keyword-only when generating the ``__init__()``.
@@ -1615,7 +1657,7 @@ def relationship(
cascade=cascade,
viewonly=viewonly,
attribute_options=_AttributeOptions(
- init, repr, default, default_factory, kw_only
+ init, repr, default, default_factory, compare, kw_only
),
lazy=lazy,
passive_deletes=passive_deletes,
@@ -1648,6 +1690,7 @@ def synonym(
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
default: Union[_NoArg, _T] = _NoArg.NO_ARG,
default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
info: Optional[_InfoType] = None,
doc: Optional[str] = None,
@@ -1761,7 +1804,7 @@ def synonym(
descriptor=descriptor,
comparator_factory=comparator_factory,
attribute_options=_AttributeOptions(
- init, repr, default, default_factory, kw_only
+ init, repr, default, default_factory, compare, kw_only
),
doc=doc,
info=info,
@@ -1890,6 +1933,7 @@ def deferred(
repr: Union[_NoArg, bool] = _NoArg.NO_ARG, # noqa: A002
default: Optional[Any] = _NoArg.NO_ARG,
default_factory: Union[_NoArg, Callable[[], _T]] = _NoArg.NO_ARG,
+ compare: Union[_NoArg, bool] = _NoArg.NO_ARG,
kw_only: Union[_NoArg, bool] = _NoArg.NO_ARG,
active_history: bool = False,
expire_on_flush: bool = True,
@@ -1925,7 +1969,7 @@ def deferred(
column,
*additional_columns,
attribute_options=_AttributeOptions(
- init, repr, default, default_factory, kw_only
+ init, repr, default, default_factory, compare, kw_only
),
group=group,
deferred=True,
@@ -1966,6 +2010,7 @@ def query_expression(
_NoArg.NO_ARG,
_NoArg.NO_ARG,
_NoArg.NO_ARG,
+ _NoArg.NO_ARG,
),
expire_on_flush=expire_on_flush,
info=info,
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 3d2f9708f..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],
@@ -195,6 +197,7 @@ class _AttributeOptions(NamedTuple):
dataclasses_repr: Union[_NoArg, bool]
dataclasses_default: Union[_NoArg, Any]
dataclasses_default_factory: Union[_NoArg, Callable[[], Any]]
+ dataclasses_compare: Union[_NoArg, bool]
dataclasses_kw_only: Union[_NoArg, bool]
def _as_dataclass_field(self) -> Any:
@@ -209,6 +212,8 @@ class _AttributeOptions(NamedTuple):
kw["init"] = self.dataclasses_init
if self.dataclasses_repr is not _NoArg.NO_ARG:
kw["repr"] = self.dataclasses_repr
+ if self.dataclasses_compare is not _NoArg.NO_ARG:
+ kw["compare"] = self.dataclasses_compare
if self.dataclasses_kw_only is not _NoArg.NO_ARG:
kw["kw_only"] = self.dataclasses_kw_only
@@ -256,7 +261,12 @@ class _AttributeOptions(NamedTuple):
_DEFAULT_ATTRIBUTE_OPTIONS = _AttributeOptions(
- _NoArg.NO_ARG, _NoArg.NO_ARG, _NoArg.NO_ARG, _NoArg.NO_ARG, _NoArg.NO_ARG
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
+ _NoArg.NO_ARG,
)
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],