Загрузить файлы в «venv/Lib/site-packages/sqlalchemy/orm»
This commit is contained in:
164
venv/Lib/site-packages/sqlalchemy/orm/sync.py
Normal file
164
venv/Lib/site-packages/sqlalchemy/orm/sync.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# orm/sync.py
|
||||
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||
# <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||
# mypy: allow-untyped-defs, allow-untyped-calls
|
||||
|
||||
|
||||
"""private module containing functions used for copying data
|
||||
between instances based on join conditions.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import exc
|
||||
from . import util as orm_util
|
||||
from .base import PassiveFlag
|
||||
|
||||
|
||||
def populate(
|
||||
source,
|
||||
source_mapper,
|
||||
dest,
|
||||
dest_mapper,
|
||||
synchronize_pairs,
|
||||
uowcommit,
|
||||
flag_cascaded_pks,
|
||||
):
|
||||
source_dict = source.dict
|
||||
dest_dict = dest.dict
|
||||
|
||||
for l, r in synchronize_pairs:
|
||||
try:
|
||||
# inline of source_mapper._get_state_attr_by_column
|
||||
prop = source_mapper._columntoproperty[l]
|
||||
value = source.manager[prop.key].impl.get(
|
||||
source, source_dict, PassiveFlag.PASSIVE_OFF
|
||||
)
|
||||
except exc.UnmappedColumnError as err:
|
||||
_raise_col_to_prop(False, source_mapper, l, dest_mapper, r, err)
|
||||
|
||||
try:
|
||||
# inline of dest_mapper._set_state_attr_by_column
|
||||
prop = dest_mapper._columntoproperty[r]
|
||||
dest.manager[prop.key].impl.set(dest, dest_dict, value, None)
|
||||
except exc.UnmappedColumnError as err:
|
||||
_raise_col_to_prop(True, source_mapper, l, dest_mapper, r, err)
|
||||
|
||||
# technically the "r.primary_key" check isn't
|
||||
# needed here, but we check for this condition to limit
|
||||
# how often this logic is invoked for memory/performance
|
||||
# reasons, since we only need this info for a primary key
|
||||
# destination.
|
||||
if (
|
||||
flag_cascaded_pks
|
||||
and l.primary_key
|
||||
and r.primary_key
|
||||
and r.references(l)
|
||||
):
|
||||
uowcommit.attributes[("pk_cascaded", dest, r)] = True
|
||||
|
||||
|
||||
def bulk_populate_inherit_keys(source_dict, source_mapper, synchronize_pairs):
|
||||
# a simplified version of populate() used by bulk insert mode
|
||||
for l, r in synchronize_pairs:
|
||||
try:
|
||||
prop = source_mapper._columntoproperty[l]
|
||||
value = source_dict[prop.key]
|
||||
except exc.UnmappedColumnError as err:
|
||||
_raise_col_to_prop(False, source_mapper, l, source_mapper, r, err)
|
||||
|
||||
try:
|
||||
prop = source_mapper._columntoproperty[r]
|
||||
source_dict[prop.key] = value
|
||||
except exc.UnmappedColumnError as err:
|
||||
_raise_col_to_prop(True, source_mapper, l, source_mapper, r, err)
|
||||
|
||||
|
||||
def clear(dest, dest_mapper, synchronize_pairs):
|
||||
for l, r in synchronize_pairs:
|
||||
if (
|
||||
r.primary_key
|
||||
and dest_mapper._get_state_attr_by_column(dest, dest.dict, r)
|
||||
not in orm_util._none_set
|
||||
):
|
||||
raise AssertionError(
|
||||
f"Dependency rule on column '{l}' "
|
||||
"tried to blank-out primary key "
|
||||
f"column '{r}' on instance '{orm_util.state_str(dest)}'"
|
||||
)
|
||||
try:
|
||||
dest_mapper._set_state_attr_by_column(dest, dest.dict, r, None)
|
||||
except exc.UnmappedColumnError as err:
|
||||
_raise_col_to_prop(True, None, l, dest_mapper, r, err)
|
||||
|
||||
|
||||
def update(source, source_mapper, dest, old_prefix, synchronize_pairs):
|
||||
for l, r in synchronize_pairs:
|
||||
try:
|
||||
oldvalue = source_mapper._get_committed_attr_by_column(
|
||||
source.obj(), l
|
||||
)
|
||||
value = source_mapper._get_state_attr_by_column(
|
||||
source, source.dict, l, passive=PassiveFlag.PASSIVE_OFF
|
||||
)
|
||||
except exc.UnmappedColumnError as err:
|
||||
_raise_col_to_prop(False, source_mapper, l, None, r, err)
|
||||
dest[r.key] = value
|
||||
dest[old_prefix + r.key] = oldvalue
|
||||
|
||||
|
||||
def populate_dict(source, source_mapper, dict_, synchronize_pairs):
|
||||
for l, r in synchronize_pairs:
|
||||
try:
|
||||
value = source_mapper._get_state_attr_by_column(
|
||||
source, source.dict, l, passive=PassiveFlag.PASSIVE_OFF
|
||||
)
|
||||
except exc.UnmappedColumnError as err:
|
||||
_raise_col_to_prop(False, source_mapper, l, None, r, err)
|
||||
|
||||
dict_[r.key] = value
|
||||
|
||||
|
||||
def source_modified(uowcommit, source, source_mapper, synchronize_pairs):
|
||||
"""return true if the source object has changes from an old to a
|
||||
new value on the given synchronize pairs
|
||||
|
||||
"""
|
||||
for l, r in synchronize_pairs:
|
||||
try:
|
||||
prop = source_mapper._columntoproperty[l]
|
||||
except exc.UnmappedColumnError as err:
|
||||
_raise_col_to_prop(False, source_mapper, l, None, r, err)
|
||||
history = uowcommit.get_attribute_history(
|
||||
source, prop.key, PassiveFlag.PASSIVE_NO_INITIALIZE
|
||||
)
|
||||
if bool(history.deleted):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _raise_col_to_prop(
|
||||
isdest, source_mapper, source_column, dest_mapper, dest_column, err
|
||||
):
|
||||
if isdest:
|
||||
raise exc.UnmappedColumnError(
|
||||
"Can't execute sync rule for "
|
||||
"destination column '%s'; mapper '%s' does not map "
|
||||
"this column. Try using an explicit `foreign_keys` "
|
||||
"collection which does not include this column (or use "
|
||||
"a viewonly=True relation)." % (dest_column, dest_mapper)
|
||||
) from err
|
||||
else:
|
||||
raise exc.UnmappedColumnError(
|
||||
"Can't execute sync rule for "
|
||||
"source column '%s'; mapper '%s' does not map this "
|
||||
"column. Try using an explicit `foreign_keys` "
|
||||
"collection which does not include destination column "
|
||||
"'%s' (or use a viewonly=True relation)."
|
||||
% (source_column, source_mapper, dest_column)
|
||||
) from err
|
||||
795
venv/Lib/site-packages/sqlalchemy/orm/unitofwork.py
Normal file
795
venv/Lib/site-packages/sqlalchemy/orm/unitofwork.py
Normal file
@@ -0,0 +1,795 @@
|
||||
# orm/unitofwork.py
|
||||
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||
# <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||
# mypy: ignore-errors
|
||||
|
||||
|
||||
"""The internals for the unit of work system.
|
||||
|
||||
The session's flush() process passes objects to a contextual object
|
||||
here, which assembles flush tasks based on mappers and their properties,
|
||||
organizes them in order of dependency, and executes.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import attributes
|
||||
from . import exc as orm_exc
|
||||
from . import util as orm_util
|
||||
from .. import event
|
||||
from .. import util
|
||||
from ..util import topological
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .dependency import DependencyProcessor
|
||||
from .interfaces import MapperProperty
|
||||
from .mapper import Mapper
|
||||
from .session import Session
|
||||
from .session import SessionTransaction
|
||||
from .state import InstanceState
|
||||
|
||||
|
||||
def track_cascade_events(descriptor, prop):
|
||||
"""Establish event listeners on object attributes which handle
|
||||
cascade-on-set/append.
|
||||
|
||||
"""
|
||||
key = prop.key
|
||||
|
||||
def append(state, item, initiator, **kw):
|
||||
# process "save_update" cascade rules for when
|
||||
# an instance is appended to the list of another instance
|
||||
|
||||
if item is None:
|
||||
return
|
||||
|
||||
sess = state.session
|
||||
if sess:
|
||||
if sess._warn_on_events:
|
||||
sess._flush_warning("collection append")
|
||||
|
||||
prop = state.manager.mapper._props[key]
|
||||
item_state = attributes.instance_state(item)
|
||||
|
||||
if (
|
||||
prop._cascade.save_update
|
||||
and (key == initiator.key)
|
||||
and not sess._contains_state(item_state)
|
||||
):
|
||||
sess._save_or_update_state(item_state)
|
||||
return item
|
||||
|
||||
def remove(state, item, initiator, **kw):
|
||||
if item is None:
|
||||
return
|
||||
|
||||
sess = state.session
|
||||
|
||||
prop = state.manager.mapper._props[key]
|
||||
|
||||
if sess and sess._warn_on_events:
|
||||
sess._flush_warning(
|
||||
"collection remove"
|
||||
if prop.uselist
|
||||
else "related attribute delete"
|
||||
)
|
||||
|
||||
if (
|
||||
item is not None
|
||||
and item is not attributes.NEVER_SET
|
||||
and item is not attributes.PASSIVE_NO_RESULT
|
||||
and prop._cascade.delete_orphan
|
||||
):
|
||||
# expunge pending orphans
|
||||
item_state = attributes.instance_state(item)
|
||||
|
||||
if prop.mapper._is_orphan(item_state):
|
||||
if sess and item_state in sess._new:
|
||||
sess.expunge(item)
|
||||
else:
|
||||
# the related item may or may not itself be in a
|
||||
# Session, however the parent for which we are catching
|
||||
# the event is not in a session, so memoize this on the
|
||||
# item
|
||||
item_state._orphaned_outside_of_session = True
|
||||
|
||||
def set_(state, newvalue, oldvalue, initiator, **kw):
|
||||
# process "save_update" cascade rules for when an instance
|
||||
# is attached to another instance
|
||||
if oldvalue is newvalue:
|
||||
return newvalue
|
||||
|
||||
sess = state.session
|
||||
if sess:
|
||||
if sess._warn_on_events:
|
||||
sess._flush_warning("related attribute set")
|
||||
|
||||
prop = state.manager.mapper._props[key]
|
||||
if newvalue is not None:
|
||||
newvalue_state = attributes.instance_state(newvalue)
|
||||
if (
|
||||
prop._cascade.save_update
|
||||
and (key == initiator.key)
|
||||
and not sess._contains_state(newvalue_state)
|
||||
):
|
||||
sess._save_or_update_state(newvalue_state)
|
||||
|
||||
if (
|
||||
oldvalue is not None
|
||||
and oldvalue is not attributes.NEVER_SET
|
||||
and oldvalue is not attributes.PASSIVE_NO_RESULT
|
||||
and prop._cascade.delete_orphan
|
||||
):
|
||||
# possible to reach here with attributes.NEVER_SET ?
|
||||
oldvalue_state = attributes.instance_state(oldvalue)
|
||||
|
||||
if oldvalue_state in sess._new and prop.mapper._is_orphan(
|
||||
oldvalue_state
|
||||
):
|
||||
sess.expunge(oldvalue)
|
||||
return newvalue
|
||||
|
||||
event.listen(
|
||||
descriptor, "append_wo_mutation", append, raw=True, include_key=True
|
||||
)
|
||||
event.listen(
|
||||
descriptor, "append", append, raw=True, retval=True, include_key=True
|
||||
)
|
||||
event.listen(
|
||||
descriptor, "remove", remove, raw=True, retval=True, include_key=True
|
||||
)
|
||||
event.listen(
|
||||
descriptor, "set", set_, raw=True, retval=True, include_key=True
|
||||
)
|
||||
|
||||
|
||||
class UOWTransaction:
|
||||
session: Session
|
||||
transaction: SessionTransaction
|
||||
attributes: Dict[str, Any]
|
||||
deps: util.defaultdict[Mapper[Any], Set[DependencyProcessor]]
|
||||
mappers: util.defaultdict[Mapper[Any], Set[InstanceState[Any]]]
|
||||
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
# dictionary used by external actors to
|
||||
# store arbitrary state information.
|
||||
self.attributes = {}
|
||||
|
||||
# dictionary of mappers to sets of
|
||||
# DependencyProcessors, which are also
|
||||
# set to be part of the sorted flush actions,
|
||||
# which have that mapper as a parent.
|
||||
self.deps = util.defaultdict(set)
|
||||
|
||||
# dictionary of mappers to sets of InstanceState
|
||||
# items pending for flush which have that mapper
|
||||
# as a parent.
|
||||
self.mappers = util.defaultdict(set)
|
||||
|
||||
# a dictionary of Preprocess objects, which gather
|
||||
# additional states impacted by the flush
|
||||
# and determine if a flush action is needed
|
||||
self.presort_actions = {}
|
||||
|
||||
# dictionary of PostSortRec objects, each
|
||||
# one issues work during the flush within
|
||||
# a certain ordering.
|
||||
self.postsort_actions = {}
|
||||
|
||||
# a set of 2-tuples, each containing two
|
||||
# PostSortRec objects where the second
|
||||
# is dependent on the first being executed
|
||||
# first
|
||||
self.dependencies = set()
|
||||
|
||||
# dictionary of InstanceState-> (isdelete, listonly)
|
||||
# tuples, indicating if this state is to be deleted
|
||||
# or insert/updated, or just refreshed
|
||||
self.states = {}
|
||||
|
||||
# tracks InstanceStates which will be receiving
|
||||
# a "post update" call. Keys are mappers,
|
||||
# values are a set of states and a set of the
|
||||
# columns which should be included in the update.
|
||||
self.post_update_states = util.defaultdict(lambda: (set(), set()))
|
||||
|
||||
@property
|
||||
def has_work(self):
|
||||
return bool(self.states)
|
||||
|
||||
def was_already_deleted(self, state):
|
||||
"""Return ``True`` if the given state is expired and was deleted
|
||||
previously.
|
||||
"""
|
||||
if state.expired:
|
||||
try:
|
||||
state._load_expired(state, attributes.PASSIVE_OFF)
|
||||
except orm_exc.ObjectDeletedError:
|
||||
self.session._remove_newly_deleted([state])
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_deleted(self, state):
|
||||
"""Return ``True`` if the given state is marked as deleted
|
||||
within this uowtransaction."""
|
||||
|
||||
return state in self.states and self.states[state][0]
|
||||
|
||||
def memo(self, key, callable_):
|
||||
if key in self.attributes:
|
||||
return self.attributes[key]
|
||||
else:
|
||||
self.attributes[key] = ret = callable_()
|
||||
return ret
|
||||
|
||||
def remove_state_actions(self, state):
|
||||
"""Remove pending actions for a state from the uowtransaction."""
|
||||
|
||||
isdelete = self.states[state][0]
|
||||
|
||||
self.states[state] = (isdelete, True)
|
||||
|
||||
def get_attribute_history(
|
||||
self, state, key, passive=attributes.PASSIVE_NO_INITIALIZE
|
||||
):
|
||||
"""Facade to attributes.get_state_history(), including
|
||||
caching of results."""
|
||||
|
||||
hashkey = ("history", state, key)
|
||||
|
||||
# cache the objects, not the states; the strong reference here
|
||||
# prevents newly loaded objects from being dereferenced during the
|
||||
# flush process
|
||||
|
||||
if hashkey in self.attributes:
|
||||
history, state_history, cached_passive = self.attributes[hashkey]
|
||||
# if the cached lookup was "passive" and now
|
||||
# we want non-passive, do a non-passive lookup and re-cache
|
||||
|
||||
if (
|
||||
not cached_passive & attributes.SQL_OK
|
||||
and passive & attributes.SQL_OK
|
||||
):
|
||||
impl = state.manager[key].impl
|
||||
history = impl.get_history(
|
||||
state,
|
||||
state.dict,
|
||||
attributes.PASSIVE_OFF
|
||||
| attributes.LOAD_AGAINST_COMMITTED
|
||||
| attributes.NO_RAISE,
|
||||
)
|
||||
if history and impl.uses_objects:
|
||||
state_history = history.as_state()
|
||||
else:
|
||||
state_history = history
|
||||
self.attributes[hashkey] = (history, state_history, passive)
|
||||
else:
|
||||
impl = state.manager[key].impl
|
||||
# TODO: store the history as (state, object) tuples
|
||||
# so we don't have to keep converting here
|
||||
history = impl.get_history(
|
||||
state,
|
||||
state.dict,
|
||||
passive
|
||||
| attributes.LOAD_AGAINST_COMMITTED
|
||||
| attributes.NO_RAISE,
|
||||
)
|
||||
if history and impl.uses_objects:
|
||||
state_history = history.as_state()
|
||||
else:
|
||||
state_history = history
|
||||
self.attributes[hashkey] = (history, state_history, passive)
|
||||
|
||||
return state_history
|
||||
|
||||
def has_dep(self, processor):
|
||||
return (processor, True) in self.presort_actions
|
||||
|
||||
def register_preprocessor(self, processor, fromparent):
|
||||
key = (processor, fromparent)
|
||||
if key not in self.presort_actions:
|
||||
self.presort_actions[key] = Preprocess(processor, fromparent)
|
||||
|
||||
def register_object(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
isdelete: bool = False,
|
||||
listonly: bool = False,
|
||||
cancel_delete: bool = False,
|
||||
operation: Optional[str] = None,
|
||||
prop: Optional[MapperProperty] = None,
|
||||
) -> bool:
|
||||
if not self.session._contains_state(state):
|
||||
# this condition is normal when objects are registered
|
||||
# as part of a relationship cascade operation. it should
|
||||
# not occur for the top-level register from Session.flush().
|
||||
if not state.deleted and operation is not None:
|
||||
util.warn(
|
||||
"Object of type %s not in session, %s operation "
|
||||
"along '%s' will not proceed"
|
||||
% (orm_util.state_class_str(state), operation, prop)
|
||||
)
|
||||
return False
|
||||
|
||||
if state not in self.states:
|
||||
mapper = state.manager.mapper
|
||||
|
||||
if mapper not in self.mappers:
|
||||
self._per_mapper_flush_actions(mapper)
|
||||
|
||||
self.mappers[mapper].add(state)
|
||||
self.states[state] = (isdelete, listonly)
|
||||
else:
|
||||
if not listonly and (isdelete or cancel_delete):
|
||||
self.states[state] = (isdelete, False)
|
||||
return True
|
||||
|
||||
def register_post_update(self, state, post_update_cols):
|
||||
mapper = state.manager.mapper.base_mapper
|
||||
states, cols = self.post_update_states[mapper]
|
||||
states.add(state)
|
||||
cols.update(post_update_cols)
|
||||
|
||||
def _per_mapper_flush_actions(self, mapper):
|
||||
saves = SaveUpdateAll(self, mapper.base_mapper)
|
||||
deletes = DeleteAll(self, mapper.base_mapper)
|
||||
self.dependencies.add((saves, deletes))
|
||||
|
||||
for dep in mapper._dependency_processors:
|
||||
dep.per_property_preprocessors(self)
|
||||
|
||||
for prop in mapper.relationships:
|
||||
if prop.viewonly:
|
||||
continue
|
||||
dep = prop._dependency_processor
|
||||
dep.per_property_preprocessors(self)
|
||||
|
||||
@util.memoized_property
|
||||
def _mapper_for_dep(self):
|
||||
"""return a dynamic mapping of (Mapper, DependencyProcessor) to
|
||||
True or False, indicating if the DependencyProcessor operates
|
||||
on objects of that Mapper.
|
||||
|
||||
The result is stored in the dictionary persistently once
|
||||
calculated.
|
||||
|
||||
"""
|
||||
return util.PopulateDict(
|
||||
lambda tup: tup[0]._props.get(tup[1].key) is tup[1].prop
|
||||
)
|
||||
|
||||
def filter_states_for_dep(self, dep, states):
|
||||
"""Filter the given list of InstanceStates to those relevant to the
|
||||
given DependencyProcessor.
|
||||
|
||||
"""
|
||||
mapper_for_dep = self._mapper_for_dep
|
||||
return [s for s in states if mapper_for_dep[(s.manager.mapper, dep)]]
|
||||
|
||||
def states_for_mapper_hierarchy(self, mapper, isdelete, listonly):
|
||||
checktup = (isdelete, listonly)
|
||||
for mapper in mapper.base_mapper.self_and_descendants:
|
||||
for state in self.mappers[mapper]:
|
||||
if self.states[state] == checktup:
|
||||
yield state
|
||||
|
||||
def _generate_actions(self):
|
||||
"""Generate the full, unsorted collection of PostSortRecs as
|
||||
well as dependency pairs for this UOWTransaction.
|
||||
|
||||
"""
|
||||
# execute presort_actions, until all states
|
||||
# have been processed. a presort_action might
|
||||
# add new states to the uow.
|
||||
while True:
|
||||
ret = False
|
||||
for action in list(self.presort_actions.values()):
|
||||
if action.execute(self):
|
||||
ret = True
|
||||
if not ret:
|
||||
break
|
||||
|
||||
# see if the graph of mapper dependencies has cycles.
|
||||
self.cycles = cycles = topological.find_cycles(
|
||||
self.dependencies, list(self.postsort_actions.values())
|
||||
)
|
||||
|
||||
if cycles:
|
||||
# if yes, break the per-mapper actions into
|
||||
# per-state actions
|
||||
convert = {
|
||||
rec: set(rec.per_state_flush_actions(self)) for rec in cycles
|
||||
}
|
||||
|
||||
# rewrite the existing dependencies to point to
|
||||
# the per-state actions for those per-mapper actions
|
||||
# that were broken up.
|
||||
for edge in list(self.dependencies):
|
||||
if (
|
||||
None in edge
|
||||
or edge[0].disabled
|
||||
or edge[1].disabled
|
||||
or cycles.issuperset(edge)
|
||||
):
|
||||
self.dependencies.remove(edge)
|
||||
elif edge[0] in cycles:
|
||||
self.dependencies.remove(edge)
|
||||
for dep in convert[edge[0]]:
|
||||
self.dependencies.add((dep, edge[1]))
|
||||
elif edge[1] in cycles:
|
||||
self.dependencies.remove(edge)
|
||||
for dep in convert[edge[1]]:
|
||||
self.dependencies.add((edge[0], dep))
|
||||
|
||||
return {
|
||||
a for a in self.postsort_actions.values() if not a.disabled
|
||||
}.difference(cycles)
|
||||
|
||||
def execute(self) -> None:
|
||||
postsort_actions = self._generate_actions()
|
||||
|
||||
postsort_actions = sorted(
|
||||
postsort_actions,
|
||||
key=lambda item: item.sort_key,
|
||||
)
|
||||
# sort = topological.sort(self.dependencies, postsort_actions)
|
||||
# print "--------------"
|
||||
# print "\ndependencies:", self.dependencies
|
||||
# print "\ncycles:", self.cycles
|
||||
# print "\nsort:", list(sort)
|
||||
# print "\nCOUNT OF POSTSORT ACTIONS", len(postsort_actions)
|
||||
|
||||
# execute
|
||||
if self.cycles:
|
||||
for subset in topological.sort_as_subsets(
|
||||
self.dependencies, postsort_actions
|
||||
):
|
||||
set_ = set(subset)
|
||||
while set_:
|
||||
n = set_.pop()
|
||||
n.execute_aggregate(self, set_)
|
||||
else:
|
||||
for rec in topological.sort(self.dependencies, postsort_actions):
|
||||
rec.execute(self)
|
||||
|
||||
def finalize_flush_changes(self) -> None:
|
||||
"""Mark processed objects as clean / deleted after a successful
|
||||
flush().
|
||||
|
||||
This method is called within the flush() method after the
|
||||
execute() method has succeeded and the transaction has been committed.
|
||||
|
||||
"""
|
||||
if not self.states:
|
||||
return
|
||||
|
||||
states = set(self.states)
|
||||
isdel = {
|
||||
s for (s, (isdelete, listonly)) in self.states.items() if isdelete
|
||||
}
|
||||
other = states.difference(isdel)
|
||||
if isdel:
|
||||
self.session._remove_newly_deleted(isdel)
|
||||
if other:
|
||||
self.session._register_persistent(other)
|
||||
|
||||
|
||||
class IterateMappersMixin:
|
||||
__slots__ = ()
|
||||
|
||||
def _mappers(self, uow):
|
||||
if self.fromparent:
|
||||
return iter(
|
||||
m
|
||||
for m in self.dependency_processor.parent.self_and_descendants
|
||||
if uow._mapper_for_dep[(m, self.dependency_processor)]
|
||||
)
|
||||
else:
|
||||
return self.dependency_processor.mapper.self_and_descendants
|
||||
|
||||
|
||||
class Preprocess(IterateMappersMixin):
|
||||
__slots__ = (
|
||||
"dependency_processor",
|
||||
"fromparent",
|
||||
"processed",
|
||||
"setup_flush_actions",
|
||||
)
|
||||
|
||||
def __init__(self, dependency_processor, fromparent):
|
||||
self.dependency_processor = dependency_processor
|
||||
self.fromparent = fromparent
|
||||
self.processed = set()
|
||||
self.setup_flush_actions = False
|
||||
|
||||
def execute(self, uow):
|
||||
delete_states = set()
|
||||
save_states = set()
|
||||
|
||||
for mapper in self._mappers(uow):
|
||||
for state in uow.mappers[mapper].difference(self.processed):
|
||||
isdelete, listonly = uow.states[state]
|
||||
if not listonly:
|
||||
if isdelete:
|
||||
delete_states.add(state)
|
||||
else:
|
||||
save_states.add(state)
|
||||
|
||||
if delete_states:
|
||||
self.dependency_processor.presort_deletes(uow, delete_states)
|
||||
self.processed.update(delete_states)
|
||||
if save_states:
|
||||
self.dependency_processor.presort_saves(uow, save_states)
|
||||
self.processed.update(save_states)
|
||||
|
||||
if delete_states or save_states:
|
||||
if not self.setup_flush_actions and (
|
||||
self.dependency_processor.prop_has_changes(
|
||||
uow, delete_states, True
|
||||
)
|
||||
or self.dependency_processor.prop_has_changes(
|
||||
uow, save_states, False
|
||||
)
|
||||
):
|
||||
self.dependency_processor.per_property_flush_actions(uow)
|
||||
self.setup_flush_actions = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class PostSortRec:
|
||||
__slots__ = ("disabled",)
|
||||
|
||||
def __new__(cls, uow, *args):
|
||||
key = (cls,) + args
|
||||
if key in uow.postsort_actions:
|
||||
return uow.postsort_actions[key]
|
||||
else:
|
||||
uow.postsort_actions[key] = ret = object.__new__(cls)
|
||||
ret.disabled = False
|
||||
return ret
|
||||
|
||||
def execute_aggregate(self, uow, recs):
|
||||
self.execute(uow)
|
||||
|
||||
|
||||
class ProcessAll(IterateMappersMixin, PostSortRec):
|
||||
__slots__ = "dependency_processor", "isdelete", "fromparent", "sort_key"
|
||||
|
||||
def __init__(self, uow, dependency_processor, isdelete, fromparent):
|
||||
self.dependency_processor = dependency_processor
|
||||
self.sort_key = (
|
||||
"ProcessAll",
|
||||
self.dependency_processor.sort_key,
|
||||
isdelete,
|
||||
)
|
||||
self.isdelete = isdelete
|
||||
self.fromparent = fromparent
|
||||
uow.deps[dependency_processor.parent.base_mapper].add(
|
||||
dependency_processor
|
||||
)
|
||||
|
||||
def execute(self, uow):
|
||||
states = self._elements(uow)
|
||||
if self.isdelete:
|
||||
self.dependency_processor.process_deletes(uow, states)
|
||||
else:
|
||||
self.dependency_processor.process_saves(uow, states)
|
||||
|
||||
def per_state_flush_actions(self, uow):
|
||||
# this is handled by SaveUpdateAll and DeleteAll,
|
||||
# since a ProcessAll should unconditionally be pulled
|
||||
# into per-state if either the parent/child mappers
|
||||
# are part of a cycle
|
||||
return iter([])
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s, isdelete=%s)" % (
|
||||
self.__class__.__name__,
|
||||
self.dependency_processor,
|
||||
self.isdelete,
|
||||
)
|
||||
|
||||
def _elements(self, uow):
|
||||
for mapper in self._mappers(uow):
|
||||
for state in uow.mappers[mapper]:
|
||||
isdelete, listonly = uow.states[state]
|
||||
if isdelete == self.isdelete and not listonly:
|
||||
yield state
|
||||
|
||||
|
||||
class PostUpdateAll(PostSortRec):
|
||||
__slots__ = "mapper", "isdelete", "sort_key"
|
||||
|
||||
def __init__(self, uow, mapper, isdelete):
|
||||
self.mapper = mapper
|
||||
self.isdelete = isdelete
|
||||
self.sort_key = ("PostUpdateAll", mapper._sort_key, isdelete)
|
||||
|
||||
@util.preload_module("sqlalchemy.orm.persistence")
|
||||
def execute(self, uow):
|
||||
persistence = util.preloaded.orm_persistence
|
||||
states, cols = uow.post_update_states[self.mapper]
|
||||
states = [s for s in states if uow.states[s][0] == self.isdelete]
|
||||
|
||||
persistence.post_update(self.mapper, states, uow, cols)
|
||||
|
||||
|
||||
class SaveUpdateAll(PostSortRec):
|
||||
__slots__ = ("mapper", "sort_key")
|
||||
|
||||
def __init__(self, uow, mapper):
|
||||
self.mapper = mapper
|
||||
self.sort_key = ("SaveUpdateAll", mapper._sort_key)
|
||||
assert mapper is mapper.base_mapper
|
||||
|
||||
@util.preload_module("sqlalchemy.orm.persistence")
|
||||
def execute(self, uow):
|
||||
util.preloaded.orm_persistence.save_obj(
|
||||
self.mapper,
|
||||
uow.states_for_mapper_hierarchy(self.mapper, False, False),
|
||||
uow,
|
||||
)
|
||||
|
||||
def per_state_flush_actions(self, uow):
|
||||
states = list(
|
||||
uow.states_for_mapper_hierarchy(self.mapper, False, False)
|
||||
)
|
||||
base_mapper = self.mapper.base_mapper
|
||||
delete_all = DeleteAll(uow, base_mapper)
|
||||
for state in states:
|
||||
# keep saves before deletes -
|
||||
# this ensures 'row switch' operations work
|
||||
action = SaveUpdateState(uow, state)
|
||||
uow.dependencies.add((action, delete_all))
|
||||
yield action
|
||||
|
||||
for dep in uow.deps[self.mapper]:
|
||||
states_for_prop = uow.filter_states_for_dep(dep, states)
|
||||
dep.per_state_flush_actions(uow, states_for_prop, False)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, self.mapper)
|
||||
|
||||
|
||||
class DeleteAll(PostSortRec):
|
||||
__slots__ = ("mapper", "sort_key")
|
||||
|
||||
def __init__(self, uow, mapper):
|
||||
self.mapper = mapper
|
||||
self.sort_key = ("DeleteAll", mapper._sort_key)
|
||||
assert mapper is mapper.base_mapper
|
||||
|
||||
@util.preload_module("sqlalchemy.orm.persistence")
|
||||
def execute(self, uow):
|
||||
util.preloaded.orm_persistence.delete_obj(
|
||||
self.mapper,
|
||||
uow.states_for_mapper_hierarchy(self.mapper, True, False),
|
||||
uow,
|
||||
)
|
||||
|
||||
def per_state_flush_actions(self, uow):
|
||||
states = list(
|
||||
uow.states_for_mapper_hierarchy(self.mapper, True, False)
|
||||
)
|
||||
base_mapper = self.mapper.base_mapper
|
||||
save_all = SaveUpdateAll(uow, base_mapper)
|
||||
for state in states:
|
||||
# keep saves before deletes -
|
||||
# this ensures 'row switch' operations work
|
||||
action = DeleteState(uow, state)
|
||||
uow.dependencies.add((save_all, action))
|
||||
yield action
|
||||
|
||||
for dep in uow.deps[self.mapper]:
|
||||
states_for_prop = uow.filter_states_for_dep(dep, states)
|
||||
dep.per_state_flush_actions(uow, states_for_prop, True)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, self.mapper)
|
||||
|
||||
|
||||
class ProcessState(PostSortRec):
|
||||
__slots__ = "dependency_processor", "isdelete", "state", "sort_key"
|
||||
|
||||
def __init__(self, uow, dependency_processor, isdelete, state):
|
||||
self.dependency_processor = dependency_processor
|
||||
self.sort_key = ("ProcessState", dependency_processor.sort_key)
|
||||
self.isdelete = isdelete
|
||||
self.state = state
|
||||
|
||||
def execute_aggregate(self, uow, recs):
|
||||
cls_ = self.__class__
|
||||
dependency_processor = self.dependency_processor
|
||||
isdelete = self.isdelete
|
||||
our_recs = [
|
||||
r
|
||||
for r in recs
|
||||
if r.__class__ is cls_
|
||||
and r.dependency_processor is dependency_processor
|
||||
and r.isdelete is isdelete
|
||||
]
|
||||
recs.difference_update(our_recs)
|
||||
states = [self.state] + [r.state for r in our_recs]
|
||||
if isdelete:
|
||||
dependency_processor.process_deletes(uow, states)
|
||||
else:
|
||||
dependency_processor.process_saves(uow, states)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s, %s, delete=%s)" % (
|
||||
self.__class__.__name__,
|
||||
self.dependency_processor,
|
||||
orm_util.state_str(self.state),
|
||||
self.isdelete,
|
||||
)
|
||||
|
||||
|
||||
class SaveUpdateState(PostSortRec):
|
||||
__slots__ = "state", "mapper", "sort_key"
|
||||
|
||||
def __init__(self, uow, state):
|
||||
self.state = state
|
||||
self.mapper = state.mapper.base_mapper
|
||||
self.sort_key = ("ProcessState", self.mapper._sort_key)
|
||||
|
||||
@util.preload_module("sqlalchemy.orm.persistence")
|
||||
def execute_aggregate(self, uow, recs):
|
||||
persistence = util.preloaded.orm_persistence
|
||||
cls_ = self.__class__
|
||||
mapper = self.mapper
|
||||
our_recs = [
|
||||
r for r in recs if r.__class__ is cls_ and r.mapper is mapper
|
||||
]
|
||||
recs.difference_update(our_recs)
|
||||
persistence.save_obj(
|
||||
mapper, [self.state] + [r.state for r in our_recs], uow
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (
|
||||
self.__class__.__name__,
|
||||
orm_util.state_str(self.state),
|
||||
)
|
||||
|
||||
|
||||
class DeleteState(PostSortRec):
|
||||
__slots__ = "state", "mapper", "sort_key"
|
||||
|
||||
def __init__(self, uow, state):
|
||||
self.state = state
|
||||
self.mapper = state.mapper.base_mapper
|
||||
self.sort_key = ("DeleteState", self.mapper._sort_key)
|
||||
|
||||
@util.preload_module("sqlalchemy.orm.persistence")
|
||||
def execute_aggregate(self, uow, recs):
|
||||
persistence = util.preloaded.orm_persistence
|
||||
cls_ = self.__class__
|
||||
mapper = self.mapper
|
||||
our_recs = [
|
||||
r for r in recs if r.__class__ is cls_ and r.mapper is mapper
|
||||
]
|
||||
recs.difference_update(our_recs)
|
||||
states = [self.state] + [r.state for r in our_recs]
|
||||
persistence.delete_obj(
|
||||
mapper, [s for s in states if uow.states[s][0]], uow
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (
|
||||
self.__class__.__name__,
|
||||
orm_util.state_str(self.state),
|
||||
)
|
||||
2407
venv/Lib/site-packages/sqlalchemy/orm/util.py
Normal file
2407
venv/Lib/site-packages/sqlalchemy/orm/util.py
Normal file
File diff suppressed because it is too large
Load Diff
674
venv/Lib/site-packages/sqlalchemy/orm/writeonly.py
Normal file
674
venv/Lib/site-packages/sqlalchemy/orm/writeonly.py
Normal file
@@ -0,0 +1,674 @@
|
||||
# orm/writeonly.py
|
||||
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
||||
# <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
"""Write-only collection API.
|
||||
|
||||
This is an alternate mapped attribute style that only supports single-item
|
||||
collection mutation operations. To read the collection, a select()
|
||||
object must be executed each time.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import Collection
|
||||
from typing import Dict
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import NoReturn
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
from sqlalchemy.sql import bindparam
|
||||
from . import attributes
|
||||
from . import interfaces
|
||||
from . import relationships
|
||||
from . import strategies
|
||||
from .base import NEVER_SET
|
||||
from .base import object_mapper
|
||||
from .base import PassiveFlag
|
||||
from .base import RelationshipDirection
|
||||
from .. import exc
|
||||
from .. import inspect
|
||||
from .. import log
|
||||
from .. import util
|
||||
from ..sql import delete
|
||||
from ..sql import insert
|
||||
from ..sql import select
|
||||
from ..sql import update
|
||||
from ..sql.dml import Delete
|
||||
from ..sql.dml import Insert
|
||||
from ..sql.dml import Update
|
||||
from ..util.typing import Literal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import QueryableAttribute
|
||||
from ._typing import _InstanceDict
|
||||
from .attributes import AttributeEventToken
|
||||
from .base import LoaderCallableStatus
|
||||
from .collections import _AdaptedCollectionProtocol
|
||||
from .collections import CollectionAdapter
|
||||
from .mapper import Mapper
|
||||
from .relationships import _RelationshipOrderByArg
|
||||
from .state import InstanceState
|
||||
from .util import AliasedClass
|
||||
from ..event import _Dispatch
|
||||
from ..sql.selectable import FromClause
|
||||
from ..sql.selectable import Select
|
||||
|
||||
_T = TypeVar("_T", bound=Any)
|
||||
|
||||
|
||||
class WriteOnlyHistory(Generic[_T]):
|
||||
"""Overrides AttributeHistory to receive append/remove events directly."""
|
||||
|
||||
unchanged_items: util.OrderedIdentitySet
|
||||
added_items: util.OrderedIdentitySet
|
||||
deleted_items: util.OrderedIdentitySet
|
||||
_reconcile_collection: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attr: WriteOnlyAttributeImpl,
|
||||
state: InstanceState[_T],
|
||||
passive: PassiveFlag,
|
||||
apply_to: Optional[WriteOnlyHistory[_T]] = None,
|
||||
) -> None:
|
||||
if apply_to:
|
||||
if passive & PassiveFlag.SQL_OK:
|
||||
raise exc.InvalidRequestError(
|
||||
f"Attribute {attr} can't load the existing state from the "
|
||||
"database for this operation; full iteration is not "
|
||||
"permitted. If this is a delete operation, configure "
|
||||
f"passive_deletes=True on the {attr} relationship in "
|
||||
"order to resolve this error."
|
||||
)
|
||||
|
||||
self.unchanged_items = apply_to.unchanged_items
|
||||
self.added_items = apply_to.added_items
|
||||
self.deleted_items = apply_to.deleted_items
|
||||
self._reconcile_collection = apply_to._reconcile_collection
|
||||
else:
|
||||
self.deleted_items = util.OrderedIdentitySet()
|
||||
self.added_items = util.OrderedIdentitySet()
|
||||
self.unchanged_items = util.OrderedIdentitySet()
|
||||
self._reconcile_collection = False
|
||||
|
||||
@property
|
||||
def added_plus_unchanged(self) -> List[_T]:
|
||||
return list(self.added_items.union(self.unchanged_items))
|
||||
|
||||
@property
|
||||
def all_items(self) -> List[_T]:
|
||||
return list(
|
||||
self.added_items.union(self.unchanged_items).union(
|
||||
self.deleted_items
|
||||
)
|
||||
)
|
||||
|
||||
def as_history(self) -> attributes.History:
|
||||
if self._reconcile_collection:
|
||||
added = self.added_items.difference(self.unchanged_items)
|
||||
deleted = self.deleted_items.intersection(self.unchanged_items)
|
||||
unchanged = self.unchanged_items.difference(deleted)
|
||||
else:
|
||||
added, unchanged, deleted = (
|
||||
self.added_items,
|
||||
self.unchanged_items,
|
||||
self.deleted_items,
|
||||
)
|
||||
return attributes.History(list(added), list(unchanged), list(deleted))
|
||||
|
||||
def indexed(self, index: Union[int, slice]) -> Union[List[_T], _T]:
|
||||
return list(self.added_items)[index]
|
||||
|
||||
def add_added(self, value: _T) -> None:
|
||||
self.added_items.add(value)
|
||||
|
||||
def add_removed(self, value: _T) -> None:
|
||||
if value in self.added_items:
|
||||
self.added_items.remove(value)
|
||||
else:
|
||||
self.deleted_items.add(value)
|
||||
|
||||
|
||||
class WriteOnlyAttributeImpl(
|
||||
attributes.HasCollectionAdapter, attributes.AttributeImpl
|
||||
):
|
||||
uses_objects: bool = True
|
||||
default_accepts_scalar_loader: bool = False
|
||||
supports_population: bool = False
|
||||
_supports_dynamic_iteration: bool = False
|
||||
collection: bool = False
|
||||
dynamic: bool = True
|
||||
order_by: _RelationshipOrderByArg = ()
|
||||
collection_history_cls: Type[WriteOnlyHistory[Any]] = WriteOnlyHistory
|
||||
|
||||
query_class: Type[WriteOnlyCollection[Any]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
class_: Union[Type[Any], AliasedClass[Any]],
|
||||
key: str,
|
||||
dispatch: _Dispatch[QueryableAttribute[Any]],
|
||||
target_mapper: Mapper[_T],
|
||||
order_by: _RelationshipOrderByArg,
|
||||
**kw: Any,
|
||||
):
|
||||
super().__init__(class_, key, None, dispatch, **kw)
|
||||
self.target_mapper = target_mapper
|
||||
self.query_class = WriteOnlyCollection
|
||||
if order_by:
|
||||
self.order_by = tuple(order_by)
|
||||
|
||||
def get(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
|
||||
) -> Union[util.OrderedIdentitySet, WriteOnlyCollection[Any]]:
|
||||
if not passive & PassiveFlag.SQL_OK:
|
||||
return self._get_collection_history(
|
||||
state, PassiveFlag.PASSIVE_NO_INITIALIZE
|
||||
).added_items
|
||||
else:
|
||||
return self.query_class(self, state)
|
||||
|
||||
@overload
|
||||
def get_collection(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
user_data: Literal[None] = ...,
|
||||
passive: Literal[PassiveFlag.PASSIVE_OFF] = ...,
|
||||
) -> CollectionAdapter: ...
|
||||
|
||||
@overload
|
||||
def get_collection(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
user_data: _AdaptedCollectionProtocol = ...,
|
||||
passive: PassiveFlag = ...,
|
||||
) -> CollectionAdapter: ...
|
||||
|
||||
@overload
|
||||
def get_collection(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
user_data: Optional[_AdaptedCollectionProtocol] = ...,
|
||||
passive: PassiveFlag = ...,
|
||||
) -> Union[
|
||||
Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
|
||||
]: ...
|
||||
|
||||
def get_collection(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
user_data: Optional[_AdaptedCollectionProtocol] = None,
|
||||
passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
|
||||
) -> Union[
|
||||
Literal[LoaderCallableStatus.PASSIVE_NO_RESULT], CollectionAdapter
|
||||
]:
|
||||
data: Collection[Any]
|
||||
if not passive & PassiveFlag.SQL_OK:
|
||||
data = self._get_collection_history(state, passive).added_items
|
||||
else:
|
||||
history = self._get_collection_history(state, passive)
|
||||
data = history.added_plus_unchanged
|
||||
return DynamicCollectionAdapter(data) # type: ignore[return-value]
|
||||
|
||||
@util.memoized_property
|
||||
def _append_token(self) -> attributes.AttributeEventToken:
|
||||
return attributes.AttributeEventToken(self, attributes.OP_APPEND)
|
||||
|
||||
@util.memoized_property
|
||||
def _remove_token(self) -> attributes.AttributeEventToken:
|
||||
return attributes.AttributeEventToken(self, attributes.OP_REMOVE)
|
||||
|
||||
def fire_append_event(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
value: Any,
|
||||
initiator: Optional[AttributeEventToken],
|
||||
collection_history: Optional[WriteOnlyHistory[Any]] = None,
|
||||
) -> None:
|
||||
if collection_history is None:
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
|
||||
collection_history.add_added(value)
|
||||
|
||||
for fn in self.dispatch.append:
|
||||
value = fn(state, value, initiator or self._append_token)
|
||||
|
||||
if self.trackparent and value is not None:
|
||||
self.sethasparent(attributes.instance_state(value), state, True)
|
||||
|
||||
def fire_remove_event(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
value: Any,
|
||||
initiator: Optional[AttributeEventToken],
|
||||
collection_history: Optional[WriteOnlyHistory[Any]] = None,
|
||||
) -> None:
|
||||
if collection_history is None:
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
|
||||
collection_history.add_removed(value)
|
||||
|
||||
if self.trackparent and value is not None:
|
||||
self.sethasparent(attributes.instance_state(value), state, False)
|
||||
|
||||
for fn in self.dispatch.remove:
|
||||
fn(state, value, initiator or self._remove_token)
|
||||
|
||||
def _modified_event(
|
||||
self, state: InstanceState[Any], dict_: _InstanceDict
|
||||
) -> WriteOnlyHistory[Any]:
|
||||
if self.key not in state.committed_state:
|
||||
state.committed_state[self.key] = self.collection_history_cls(
|
||||
self, state, PassiveFlag.PASSIVE_NO_FETCH
|
||||
)
|
||||
|
||||
state._modified_event(dict_, self, NEVER_SET)
|
||||
|
||||
# this is a hack to allow the entities.ComparableEntity fixture
|
||||
# to work
|
||||
dict_[self.key] = True
|
||||
return state.committed_state[self.key] # type: ignore[no-any-return]
|
||||
|
||||
def set(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
value: Any,
|
||||
initiator: Optional[AttributeEventToken] = None,
|
||||
passive: PassiveFlag = PassiveFlag.PASSIVE_OFF,
|
||||
check_old: Any = None,
|
||||
pop: bool = False,
|
||||
_adapt: bool = True,
|
||||
) -> None:
|
||||
if initiator and initiator.parent_token is self.parent_token:
|
||||
return
|
||||
|
||||
if pop and value is None:
|
||||
return
|
||||
|
||||
iterable = value
|
||||
new_values = list(iterable)
|
||||
if state.has_identity:
|
||||
if not self._supports_dynamic_iteration:
|
||||
raise exc.InvalidRequestError(
|
||||
f'Collection "{self}" does not support implicit '
|
||||
"iteration; collection replacement operations "
|
||||
"can't be used"
|
||||
)
|
||||
old_collection = util.IdentitySet(
|
||||
self.get(state, dict_, passive=passive)
|
||||
)
|
||||
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
if not state.has_identity:
|
||||
old_collection = collection_history.added_items
|
||||
else:
|
||||
old_collection = old_collection.union(
|
||||
collection_history.added_items
|
||||
)
|
||||
|
||||
constants = old_collection.intersection(new_values)
|
||||
additions = util.IdentitySet(new_values).difference(constants)
|
||||
removals = old_collection.difference(constants)
|
||||
|
||||
for member in new_values:
|
||||
if member in additions:
|
||||
self.fire_append_event(
|
||||
state,
|
||||
dict_,
|
||||
member,
|
||||
None,
|
||||
collection_history=collection_history,
|
||||
)
|
||||
|
||||
for member in removals:
|
||||
self.fire_remove_event(
|
||||
state,
|
||||
dict_,
|
||||
member,
|
||||
None,
|
||||
collection_history=collection_history,
|
||||
)
|
||||
|
||||
def delete(self, *args: Any, **kwargs: Any) -> NoReturn:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_committed_value(
|
||||
self, state: InstanceState[Any], dict_: _InstanceDict, value: Any
|
||||
) -> NoReturn:
|
||||
raise NotImplementedError(
|
||||
"Dynamic attributes don't support collection population."
|
||||
)
|
||||
|
||||
def get_history(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
|
||||
) -> attributes.History:
|
||||
c = self._get_collection_history(state, passive)
|
||||
return c.as_history()
|
||||
|
||||
def get_all_pending(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
passive: PassiveFlag = PassiveFlag.PASSIVE_NO_INITIALIZE,
|
||||
) -> List[Tuple[InstanceState[Any], Any]]:
|
||||
c = self._get_collection_history(state, passive)
|
||||
return [(attributes.instance_state(x), x) for x in c.all_items]
|
||||
|
||||
def _get_collection_history(
|
||||
self, state: InstanceState[Any], passive: PassiveFlag
|
||||
) -> WriteOnlyHistory[Any]:
|
||||
c: WriteOnlyHistory[Any]
|
||||
if self.key in state.committed_state:
|
||||
c = state.committed_state[self.key]
|
||||
else:
|
||||
c = self.collection_history_cls(
|
||||
self, state, PassiveFlag.PASSIVE_NO_FETCH
|
||||
)
|
||||
|
||||
if state.has_identity and (passive & PassiveFlag.INIT_OK):
|
||||
return self.collection_history_cls(
|
||||
self, state, passive, apply_to=c
|
||||
)
|
||||
else:
|
||||
return c
|
||||
|
||||
def append(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
value: Any,
|
||||
initiator: Optional[AttributeEventToken],
|
||||
passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
|
||||
) -> None:
|
||||
if initiator is not self: # type: ignore[comparison-overlap]
|
||||
self.fire_append_event(state, dict_, value, initiator)
|
||||
|
||||
def remove(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
value: Any,
|
||||
initiator: Optional[AttributeEventToken],
|
||||
passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
|
||||
) -> None:
|
||||
if initiator is not self: # type: ignore[comparison-overlap]
|
||||
self.fire_remove_event(state, dict_, value, initiator)
|
||||
|
||||
def pop(
|
||||
self,
|
||||
state: InstanceState[Any],
|
||||
dict_: _InstanceDict,
|
||||
value: Any,
|
||||
initiator: Optional[AttributeEventToken],
|
||||
passive: PassiveFlag = PassiveFlag.PASSIVE_NO_FETCH,
|
||||
) -> None:
|
||||
self.remove(state, dict_, value, initiator, passive=passive)
|
||||
|
||||
|
||||
@log.class_logger
|
||||
@relationships.RelationshipProperty.strategy_for(lazy="write_only")
|
||||
class WriteOnlyLoader(strategies.AbstractRelationshipLoader, log.Identified):
|
||||
impl_class = WriteOnlyAttributeImpl
|
||||
|
||||
def init_class_attribute(self, mapper: Mapper[Any]) -> None:
|
||||
self.is_class_level = True
|
||||
if not self.uselist or self.parent_property.direction not in (
|
||||
interfaces.ONETOMANY,
|
||||
interfaces.MANYTOMANY,
|
||||
):
|
||||
raise exc.InvalidRequestError(
|
||||
"On relationship %s, 'dynamic' loaders cannot be used with "
|
||||
"many-to-one/one-to-one relationships and/or "
|
||||
"uselist=False." % self.parent_property
|
||||
)
|
||||
|
||||
strategies._register_attribute( # type: ignore[no-untyped-call]
|
||||
self.parent_property,
|
||||
mapper,
|
||||
useobject=True,
|
||||
impl_class=self.impl_class,
|
||||
target_mapper=self.parent_property.mapper,
|
||||
order_by=self.parent_property.order_by,
|
||||
query_class=self.parent_property.query_class,
|
||||
)
|
||||
|
||||
|
||||
class DynamicCollectionAdapter:
|
||||
"""simplified CollectionAdapter for internal API consistency"""
|
||||
|
||||
data: Collection[Any]
|
||||
|
||||
def __init__(self, data: Collection[Any]):
|
||||
self.data = data
|
||||
|
||||
def __iter__(self) -> Iterator[Any]:
|
||||
return iter(self.data)
|
||||
|
||||
def _reset_empty(self) -> None:
|
||||
pass
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self.data)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class AbstractCollectionWriter(Generic[_T]):
|
||||
"""Virtual collection which includes append/remove methods that synchronize
|
||||
into the attribute event system.
|
||||
|
||||
"""
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
__slots__ = ()
|
||||
|
||||
instance: _T
|
||||
_from_obj: Tuple[FromClause, ...]
|
||||
|
||||
def __init__(self, attr: WriteOnlyAttributeImpl, state: InstanceState[_T]):
|
||||
instance = state.obj()
|
||||
if TYPE_CHECKING:
|
||||
assert instance
|
||||
self.instance = instance
|
||||
self.attr = attr
|
||||
|
||||
mapper = object_mapper(instance)
|
||||
prop = mapper._props[self.attr.key]
|
||||
|
||||
if prop.secondary is not None:
|
||||
# this is a hack right now. The Query only knows how to
|
||||
# make subsequent joins() without a given left-hand side
|
||||
# from self._from_obj[0]. We need to ensure prop.secondary
|
||||
# is in the FROM. So we purposely put the mapper selectable
|
||||
# in _from_obj[0] to ensure a user-defined join() later on
|
||||
# doesn't fail, and secondary is then in _from_obj[1].
|
||||
|
||||
# note also, we are using the official ORM-annotated selectable
|
||||
# from __clause_element__(), see #7868
|
||||
self._from_obj = (prop.mapper.__clause_element__(), prop.secondary)
|
||||
else:
|
||||
self._from_obj = ()
|
||||
|
||||
self._where_criteria = (
|
||||
prop._with_parent(instance, alias_secondary=False),
|
||||
)
|
||||
|
||||
if self.attr.order_by:
|
||||
self._order_by_clauses = self.attr.order_by
|
||||
else:
|
||||
self._order_by_clauses = ()
|
||||
|
||||
def _add_all_impl(self, iterator: Iterable[_T]) -> None:
|
||||
for item in iterator:
|
||||
self.attr.append(
|
||||
attributes.instance_state(self.instance),
|
||||
attributes.instance_dict(self.instance),
|
||||
item,
|
||||
None,
|
||||
)
|
||||
|
||||
def _remove_impl(self, item: _T) -> None:
|
||||
self.attr.remove(
|
||||
attributes.instance_state(self.instance),
|
||||
attributes.instance_dict(self.instance),
|
||||
item,
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
class WriteOnlyCollection(AbstractCollectionWriter[_T]):
|
||||
"""Write-only collection which can synchronize changes into the
|
||||
attribute event system.
|
||||
|
||||
The :class:`.WriteOnlyCollection` is used in a mapping by
|
||||
using the ``"write_only"`` lazy loading strategy with
|
||||
:func:`_orm.relationship`. For background on this configuration,
|
||||
see :ref:`write_only_relationship`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`write_only_relationship`
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"instance",
|
||||
"attr",
|
||||
"_where_criteria",
|
||||
"_from_obj",
|
||||
"_order_by_clauses",
|
||||
)
|
||||
|
||||
def __iter__(self) -> NoReturn:
|
||||
raise TypeError(
|
||||
"WriteOnly collections don't support iteration in-place; "
|
||||
"to query for collection items, use the select() method to "
|
||||
"produce a SQL statement and execute it with session.scalars()."
|
||||
)
|
||||
|
||||
def select(self) -> Select[Tuple[_T]]:
|
||||
"""Produce a :class:`_sql.Select` construct that represents the
|
||||
rows within this instance-local :class:`_orm.WriteOnlyCollection`.
|
||||
|
||||
"""
|
||||
stmt = select(self.attr.target_mapper).where(*self._where_criteria)
|
||||
if self._from_obj:
|
||||
stmt = stmt.select_from(*self._from_obj)
|
||||
if self._order_by_clauses:
|
||||
stmt = stmt.order_by(*self._order_by_clauses)
|
||||
return stmt
|
||||
|
||||
def insert(self) -> Insert:
|
||||
"""For one-to-many collections, produce a :class:`_dml.Insert` which
|
||||
will insert new rows in terms of this this instance-local
|
||||
:class:`_orm.WriteOnlyCollection`.
|
||||
|
||||
This construct is only supported for a :class:`_orm.Relationship`
|
||||
that does **not** include the :paramref:`_orm.relationship.secondary`
|
||||
parameter. For relationships that refer to a many-to-many table,
|
||||
use ordinary bulk insert techniques to produce new objects, then
|
||||
use :meth:`_orm.AbstractCollectionWriter.add_all` to associate them
|
||||
with the collection.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
state = inspect(self.instance)
|
||||
mapper = state.mapper
|
||||
prop = mapper._props[self.attr.key]
|
||||
|
||||
if prop.direction is not RelationshipDirection.ONETOMANY:
|
||||
raise exc.InvalidRequestError(
|
||||
"Write only bulk INSERT only supported for one-to-many "
|
||||
"collections; for many-to-many, use a separate bulk "
|
||||
"INSERT along with add_all()."
|
||||
)
|
||||
|
||||
dict_: Dict[str, Any] = {}
|
||||
|
||||
for l, r in prop.synchronize_pairs:
|
||||
fn = prop._get_attr_w_warn_on_none(
|
||||
mapper,
|
||||
state,
|
||||
state.dict,
|
||||
l,
|
||||
)
|
||||
|
||||
dict_[r.key] = bindparam(None, callable_=fn)
|
||||
|
||||
return insert(self.attr.target_mapper).values(**dict_)
|
||||
|
||||
def update(self) -> Update:
|
||||
"""Produce a :class:`_dml.Update` which will refer to rows in terms
|
||||
of this instance-local :class:`_orm.WriteOnlyCollection`.
|
||||
|
||||
"""
|
||||
return update(self.attr.target_mapper).where(*self._where_criteria)
|
||||
|
||||
def delete(self) -> Delete:
|
||||
"""Produce a :class:`_dml.Delete` which will refer to rows in terms
|
||||
of this instance-local :class:`_orm.WriteOnlyCollection`.
|
||||
|
||||
"""
|
||||
return delete(self.attr.target_mapper).where(*self._where_criteria)
|
||||
|
||||
def add_all(self, iterator: Iterable[_T]) -> None:
|
||||
"""Add an iterable of items to this :class:`_orm.WriteOnlyCollection`.
|
||||
|
||||
The given items will be persisted to the database in terms of
|
||||
the parent instance's collection on the next flush.
|
||||
|
||||
"""
|
||||
self._add_all_impl(iterator)
|
||||
|
||||
def add(self, item: _T) -> None:
|
||||
"""Add an item to this :class:`_orm.WriteOnlyCollection`.
|
||||
|
||||
The given item will be persisted to the database in terms of
|
||||
the parent instance's collection on the next flush.
|
||||
|
||||
"""
|
||||
self._add_all_impl([item])
|
||||
|
||||
def remove(self, item: _T) -> None:
|
||||
"""Remove an item from this :class:`_orm.WriteOnlyCollection`.
|
||||
|
||||
The given item will be removed from the parent instance's collection on
|
||||
the next flush.
|
||||
|
||||
"""
|
||||
self._remove_impl(item)
|
||||
Reference in New Issue
Block a user