This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# orm/relationships.py
|
||||
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
|
||||
# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
|
||||
# <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
@@ -19,7 +19,6 @@ import collections
|
||||
from collections import abc
|
||||
import dataclasses
|
||||
import inspect as _py_inspect
|
||||
import itertools
|
||||
import re
|
||||
import typing
|
||||
from typing import Any
|
||||
@@ -27,7 +26,6 @@ from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Collection
|
||||
from typing import Dict
|
||||
from typing import FrozenSet
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
@@ -181,10 +179,7 @@ _ORMOrderByArgument = Union[
|
||||
ORMBackrefArgument = Union[str, Tuple[str, Dict[str, Any]]]
|
||||
|
||||
_ORMColCollectionElement = Union[
|
||||
ColumnClause[Any],
|
||||
_HasClauseElement[Any],
|
||||
roles.DMLColumnRole,
|
||||
"Mapped[Any]",
|
||||
ColumnClause[Any], _HasClauseElement, roles.DMLColumnRole, "Mapped[Any]"
|
||||
]
|
||||
_ORMColCollectionArgument = Union[
|
||||
str,
|
||||
@@ -486,7 +481,8 @@ class RelationshipProperty(
|
||||
else:
|
||||
self._overlaps = ()
|
||||
|
||||
self.cascade = cascade
|
||||
# mypy ignoring the @property setter
|
||||
self.cascade = cascade # type: ignore
|
||||
|
||||
self.back_populates = back_populates
|
||||
|
||||
@@ -708,16 +704,12 @@ class RelationshipProperty(
|
||||
def __eq__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
|
||||
"""Implement the ``==`` operator.
|
||||
|
||||
In a many-to-one context, such as:
|
||||
|
||||
.. sourcecode:: text
|
||||
In a many-to-one context, such as::
|
||||
|
||||
MyClass.some_prop == <some object>
|
||||
|
||||
this will typically produce a
|
||||
clause such as:
|
||||
|
||||
.. sourcecode:: text
|
||||
clause such as::
|
||||
|
||||
mytable.related_id == <some id>
|
||||
|
||||
@@ -880,12 +872,11 @@ class RelationshipProperty(
|
||||
An expression like::
|
||||
|
||||
session.query(MyClass).filter(
|
||||
MyClass.somereference.any(SomeRelated.x == 2)
|
||||
MyClass.somereference.any(SomeRelated.x==2)
|
||||
)
|
||||
|
||||
Will produce a query like:
|
||||
|
||||
.. sourcecode:: sql
|
||||
Will produce a query like::
|
||||
|
||||
SELECT * FROM my_table WHERE
|
||||
EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id
|
||||
@@ -899,11 +890,11 @@ class RelationshipProperty(
|
||||
:meth:`~.Relationship.Comparator.any` is particularly
|
||||
useful for testing for empty collections::
|
||||
|
||||
session.query(MyClass).filter(~MyClass.somereference.any())
|
||||
session.query(MyClass).filter(
|
||||
~MyClass.somereference.any()
|
||||
)
|
||||
|
||||
will produce:
|
||||
|
||||
.. sourcecode:: sql
|
||||
will produce::
|
||||
|
||||
SELECT * FROM my_table WHERE
|
||||
NOT (EXISTS (SELECT 1 FROM related WHERE
|
||||
@@ -934,12 +925,11 @@ class RelationshipProperty(
|
||||
An expression like::
|
||||
|
||||
session.query(MyClass).filter(
|
||||
MyClass.somereference.has(SomeRelated.x == 2)
|
||||
MyClass.somereference.has(SomeRelated.x==2)
|
||||
)
|
||||
|
||||
Will produce a query like:
|
||||
|
||||
.. sourcecode:: sql
|
||||
Will produce a query like::
|
||||
|
||||
SELECT * FROM my_table WHERE
|
||||
EXISTS (SELECT 1 FROM related WHERE
|
||||
@@ -958,7 +948,7 @@ class RelationshipProperty(
|
||||
"""
|
||||
if self.property.uselist:
|
||||
raise sa_exc.InvalidRequestError(
|
||||
"'has()' not implemented for collections. Use any()."
|
||||
"'has()' not implemented for collections. " "Use any()."
|
||||
)
|
||||
return self._criterion_exists(criterion, **kwargs)
|
||||
|
||||
@@ -978,9 +968,7 @@ class RelationshipProperty(
|
||||
|
||||
MyClass.contains(other)
|
||||
|
||||
Produces a clause like:
|
||||
|
||||
.. sourcecode:: sql
|
||||
Produces a clause like::
|
||||
|
||||
mytable.id == <some id>
|
||||
|
||||
@@ -1000,9 +988,7 @@ class RelationshipProperty(
|
||||
|
||||
query(MyClass).filter(MyClass.contains(other))
|
||||
|
||||
Produces a query like:
|
||||
|
||||
.. sourcecode:: sql
|
||||
Produces a query like::
|
||||
|
||||
SELECT * FROM my_table, my_association_table AS
|
||||
my_association_table_1 WHERE
|
||||
@@ -1098,15 +1084,11 @@ class RelationshipProperty(
|
||||
def __ne__(self, other: Any) -> ColumnElement[bool]: # type: ignore[override] # noqa: E501
|
||||
"""Implement the ``!=`` operator.
|
||||
|
||||
In a many-to-one context, such as:
|
||||
|
||||
.. sourcecode:: text
|
||||
In a many-to-one context, such as::
|
||||
|
||||
MyClass.some_prop != <some object>
|
||||
|
||||
This will typically produce a clause such as:
|
||||
|
||||
.. sourcecode:: sql
|
||||
This will typically produce a clause such as::
|
||||
|
||||
mytable.related_id != <some id>
|
||||
|
||||
@@ -1322,11 +1304,9 @@ class RelationshipProperty(
|
||||
state,
|
||||
dict_,
|
||||
column,
|
||||
passive=(
|
||||
PassiveFlag.PASSIVE_OFF
|
||||
if state.persistent
|
||||
else PassiveFlag.PASSIVE_NO_FETCH ^ PassiveFlag.INIT_OK
|
||||
),
|
||||
passive=PassiveFlag.PASSIVE_OFF
|
||||
if state.persistent
|
||||
else PassiveFlag.PASSIVE_NO_FETCH ^ PassiveFlag.INIT_OK,
|
||||
)
|
||||
|
||||
if current_value is LoaderCallableStatus.NEVER_SET:
|
||||
@@ -1757,6 +1737,8 @@ class RelationshipProperty(
|
||||
extracted_mapped_annotation: Optional[_AnnotationScanType],
|
||||
is_dataclass_field: bool,
|
||||
) -> None:
|
||||
argument = extracted_mapped_annotation
|
||||
|
||||
if extracted_mapped_annotation is None:
|
||||
if self.argument is None:
|
||||
self._raise_for_required(key, cls)
|
||||
@@ -1766,17 +1748,19 @@ class RelationshipProperty(
|
||||
argument = extracted_mapped_annotation
|
||||
assert originating_module is not None
|
||||
|
||||
if mapped_container is not None:
|
||||
is_write_only = issubclass(mapped_container, WriteOnlyMapped)
|
||||
is_dynamic = issubclass(mapped_container, DynamicMapped)
|
||||
if is_write_only:
|
||||
self.lazy = "write_only"
|
||||
self.strategy_key = (("lazy", self.lazy),)
|
||||
elif is_dynamic:
|
||||
self.lazy = "dynamic"
|
||||
self.strategy_key = (("lazy", self.lazy),)
|
||||
else:
|
||||
is_write_only = is_dynamic = False
|
||||
is_write_only = mapped_container is not None and issubclass(
|
||||
mapped_container, WriteOnlyMapped
|
||||
)
|
||||
if is_write_only:
|
||||
self.lazy = "write_only"
|
||||
self.strategy_key = (("lazy", self.lazy),)
|
||||
|
||||
is_dynamic = mapped_container is not None and issubclass(
|
||||
mapped_container, DynamicMapped
|
||||
)
|
||||
if is_dynamic:
|
||||
self.lazy = "dynamic"
|
||||
self.strategy_key = (("lazy", self.lazy),)
|
||||
|
||||
argument = de_optionalize_union_types(argument)
|
||||
|
||||
@@ -1827,12 +1811,15 @@ class RelationshipProperty(
|
||||
argument, originating_module
|
||||
)
|
||||
|
||||
if (
|
||||
self.collection_class is None
|
||||
and not is_write_only
|
||||
and not is_dynamic
|
||||
):
|
||||
self.uselist = False
|
||||
# we don't allow the collection class to be a
|
||||
# __forward_arg__ right now, so if we see a forward arg here,
|
||||
# we know there was no collection class either
|
||||
if (
|
||||
self.collection_class is None
|
||||
and not is_write_only
|
||||
and not is_dynamic
|
||||
):
|
||||
self.uselist = False
|
||||
|
||||
# ticket #8759
|
||||
# if a lead argument was given to relationship(), like
|
||||
@@ -2012,11 +1999,9 @@ class RelationshipProperty(
|
||||
"the single_parent=True flag."
|
||||
% {
|
||||
"rel": self,
|
||||
"direction": (
|
||||
"many-to-one"
|
||||
if self.direction is MANYTOONE
|
||||
else "many-to-many"
|
||||
),
|
||||
"direction": "many-to-one"
|
||||
if self.direction is MANYTOONE
|
||||
else "many-to-many",
|
||||
"clsname": self.parent.class_.__name__,
|
||||
"relatedcls": self.mapper.class_.__name__,
|
||||
},
|
||||
@@ -2909,6 +2894,9 @@ class JoinCondition:
|
||||
) -> None:
|
||||
"""Check the foreign key columns collected and emit error
|
||||
messages."""
|
||||
|
||||
can_sync = False
|
||||
|
||||
foreign_cols = self._gather_columns_with_annotation(
|
||||
join_condition, "foreign"
|
||||
)
|
||||
@@ -3064,9 +3052,9 @@ class JoinCondition:
|
||||
|
||||
def _setup_pairs(self) -> None:
|
||||
sync_pairs: _MutableColumnPairs = []
|
||||
lrp: util.OrderedSet[Tuple[ColumnElement[Any], ColumnElement[Any]]] = (
|
||||
util.OrderedSet([])
|
||||
)
|
||||
lrp: util.OrderedSet[
|
||||
Tuple[ColumnElement[Any], ColumnElement[Any]]
|
||||
] = util.OrderedSet([])
|
||||
secondary_sync_pairs: _MutableColumnPairs = []
|
||||
|
||||
def go(
|
||||
@@ -3143,9 +3131,9 @@ class JoinCondition:
|
||||
# level configuration that benefits from this warning.
|
||||
|
||||
if to_ not in self._track_overlapping_sync_targets:
|
||||
self._track_overlapping_sync_targets[to_] = (
|
||||
weakref.WeakKeyDictionary({self.prop: from_})
|
||||
)
|
||||
self._track_overlapping_sync_targets[
|
||||
to_
|
||||
] = weakref.WeakKeyDictionary({self.prop: from_})
|
||||
else:
|
||||
other_props = []
|
||||
prop_to_from = self._track_overlapping_sync_targets[to_]
|
||||
@@ -3243,15 +3231,6 @@ class JoinCondition:
|
||||
if annotation_set.issubset(col._annotations)
|
||||
}
|
||||
|
||||
@util.memoized_property
|
||||
def _secondary_lineage_set(self) -> FrozenSet[ColumnElement[Any]]:
|
||||
if self.secondary is not None:
|
||||
return frozenset(
|
||||
itertools.chain(*[c.proxy_set for c in self.secondary.c])
|
||||
)
|
||||
else:
|
||||
return util.EMPTY_SET
|
||||
|
||||
def join_targets(
|
||||
self,
|
||||
source_selectable: Optional[FromClause],
|
||||
@@ -3302,25 +3281,23 @@ class JoinCondition:
|
||||
|
||||
if extra_criteria:
|
||||
|
||||
def mark_exclude_cols(
|
||||
def mark_unrelated_columns_as_ok_to_adapt(
|
||||
elem: SupportsAnnotations, annotations: _AnnotationDict
|
||||
) -> SupportsAnnotations:
|
||||
"""note unrelated columns in the "extra criteria" as either
|
||||
should be adapted or not adapted, even though they are not
|
||||
part of our "local" or "remote" side.
|
||||
"""note unrelated columns in the "extra criteria" as OK
|
||||
to adapt, even though they are not part of our "local"
|
||||
or "remote" side.
|
||||
|
||||
see #9779 for this case, as well as #11010 for a follow up
|
||||
see #9779 for this case
|
||||
|
||||
"""
|
||||
|
||||
parentmapper_for_element = elem._annotations.get(
|
||||
"parentmapper", None
|
||||
)
|
||||
|
||||
if (
|
||||
parentmapper_for_element is not self.prop.parent
|
||||
and parentmapper_for_element is not self.prop.mapper
|
||||
and elem not in self._secondary_lineage_set
|
||||
):
|
||||
return _safe_annotate(elem, annotations)
|
||||
else:
|
||||
@@ -3329,8 +3306,8 @@ class JoinCondition:
|
||||
extra_criteria = tuple(
|
||||
_deep_annotate(
|
||||
elem,
|
||||
{"should_not_adapt": True},
|
||||
annotate_callable=mark_exclude_cols,
|
||||
{"ok_to_adapt_in_join_condition": True},
|
||||
annotate_callable=mark_unrelated_columns_as_ok_to_adapt,
|
||||
)
|
||||
for elem in extra_criteria
|
||||
)
|
||||
@@ -3344,16 +3321,14 @@ class JoinCondition:
|
||||
if secondary is not None:
|
||||
secondary = secondary._anonymous_fromclause(flat=True)
|
||||
primary_aliasizer = ClauseAdapter(
|
||||
secondary,
|
||||
exclude_fn=_local_col_exclude,
|
||||
secondary, exclude_fn=_ColInAnnotations("local")
|
||||
)
|
||||
secondary_aliasizer = ClauseAdapter(
|
||||
dest_selectable, equivalents=self.child_equivalents
|
||||
).chain(primary_aliasizer)
|
||||
if source_selectable is not None:
|
||||
primary_aliasizer = ClauseAdapter(
|
||||
secondary,
|
||||
exclude_fn=_local_col_exclude,
|
||||
secondary, exclude_fn=_ColInAnnotations("local")
|
||||
).chain(
|
||||
ClauseAdapter(
|
||||
source_selectable,
|
||||
@@ -3365,14 +3340,14 @@ class JoinCondition:
|
||||
else:
|
||||
primary_aliasizer = ClauseAdapter(
|
||||
dest_selectable,
|
||||
exclude_fn=_local_col_exclude,
|
||||
exclude_fn=_ColInAnnotations("local"),
|
||||
equivalents=self.child_equivalents,
|
||||
)
|
||||
if source_selectable is not None:
|
||||
primary_aliasizer.chain(
|
||||
ClauseAdapter(
|
||||
source_selectable,
|
||||
exclude_fn=_remote_col_exclude,
|
||||
exclude_fn=_ColInAnnotations("remote"),
|
||||
equivalents=self.parent_equivalents,
|
||||
)
|
||||
)
|
||||
@@ -3391,7 +3366,9 @@ class JoinCondition:
|
||||
dest_selectable,
|
||||
)
|
||||
|
||||
def create_lazy_clause(self, reverse_direction: bool = False) -> Tuple[
|
||||
def create_lazy_clause(
|
||||
self, reverse_direction: bool = False
|
||||
) -> Tuple[
|
||||
ColumnElement[bool],
|
||||
Dict[str, ColumnElement[Any]],
|
||||
Dict[ColumnElement[Any], ColumnElement[Any]],
|
||||
@@ -3451,29 +3428,25 @@ class JoinCondition:
|
||||
|
||||
|
||||
class _ColInAnnotations:
|
||||
"""Serializable object that tests for names in c._annotations.
|
||||
"""Serializable object that tests for a name in c._annotations."""
|
||||
|
||||
TODO: does this need to be serializable anymore? can we find what the
|
||||
use case was for that?
|
||||
__slots__ = ("name",)
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("names",)
|
||||
|
||||
def __init__(self, *names: str):
|
||||
self.names = frozenset(names)
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
def __call__(self, c: ClauseElement) -> bool:
|
||||
return bool(self.names.intersection(c._annotations))
|
||||
return (
|
||||
self.name in c._annotations
|
||||
or "ok_to_adapt_in_join_condition" in c._annotations
|
||||
)
|
||||
|
||||
|
||||
_local_col_exclude = _ColInAnnotations("local", "should_not_adapt")
|
||||
_remote_col_exclude = _ColInAnnotations("remote", "should_not_adapt")
|
||||
|
||||
|
||||
class Relationship(
|
||||
class Relationship( # type: ignore
|
||||
RelationshipProperty[_T],
|
||||
_DeclarativeMapped[_T],
|
||||
WriteOnlyMapped[_T], # not compatible with Mapped[_T]
|
||||
DynamicMapped[_T], # not compatible with Mapped[_T]
|
||||
):
|
||||
"""Describes an object property that holds a single item or list
|
||||
of items that correspond to a related database table.
|
||||
@@ -3491,18 +3464,3 @@ class Relationship(
|
||||
|
||||
inherit_cache = True
|
||||
""":meta private:"""
|
||||
|
||||
|
||||
class _RelationshipDeclared( # type: ignore[misc]
|
||||
Relationship[_T],
|
||||
WriteOnlyMapped[_T], # not compatible with Mapped[_T]
|
||||
DynamicMapped[_T], # not compatible with Mapped[_T]
|
||||
):
|
||||
"""Relationship subclass used implicitly for declarative mapping."""
|
||||
|
||||
inherit_cache = True
|
||||
""":meta private:"""
|
||||
|
||||
@classmethod
|
||||
def _mapper_property_name(cls) -> str:
|
||||
return "Relationship"
|
||||
|
||||
Reference in New Issue
Block a user