# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""The internals for the Unit Of Work system.
+"""The internals for the unit of work system.
-Includes hooks into the attributes package enabling the routing of
-change events to Unit Of Work objects, as well as the flush()
-mechanism which creates a dependency structure that executes change
-operations.
+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.
+
+Most of the code in this module is obsolete, and will
+be replaced by a much simpler and more efficient
+system in an upcoming release. See [ticket:1742] for
+details.
-A Unit of Work is essentially a system of maintaining a graph of
-in-memory objects and their modified state. Objects are maintained as
-unique against their primary key identity using an *identity map*
-pattern. The Unit of Work then maintains lists of objects that are
-new, dirty, or deleted and provides the capability to flush all those
-changes at once.
"""
self.key = key
def append(self, state, item, initiator):
- # process "save_update" cascade rules for when an instance is appended to the list of another instance
+ # process "save_update" cascade rules for when
+ # an instance is appended to the list of another instance
sess = _state_session(state)
if sess:
prop = _state_mapper(state).get_property(self.key)
class UOWTransaction(object):
- """Handles the details of organizing and executing transaction
- tasks during a UnitOfWork object's flush() operation.
-
- The central operation is to form a graph of nodes represented by the
- ``UOWTask`` class, which is then traversed by a ``UOWExecutor`` object
- that issues SQL and instance-synchronizing operations via the related
- packages.
- """
-
+ """Represent the state of a flush operation in progress."""
+
def __init__(self, session):
self.session = session
self.mapper_flush_opts = session._mapper_flush_opts
# dictionary of mappers to UOWTasks
self.tasks = {}
- # dictionary used by external actors to store arbitrary state
+ # dictionary used by external actors
+ # to store arbitrary state
# information.
self.attributes = {}
def get_task_by_mapper(self, mapper, dontcreate=False):
"""return UOWTask element corresponding to the given mapper.
- Will create a new UOWTask, including a UOWTask corresponding to the
- "base" inherited mapper, if needed, unless the dontcreate flag is True.
"""
try:
return task
def register_dependency(self, mapper, dependency):
- """register a dependency between two mappers.
-
- Called by ``mapper.PropertyLoader`` to register the objects
- handled by one mapper being dependent on the objects handled
- by another.
-
- """
+ """register a dependency between two mappers."""
+
# correct for primary mapper
- # also convert to the "base mapper", the parentmost task at the top of an inheritance chain
- # dependency sorting is done via non-inheriting mappers only, dependencies between mappers
+ # also convert to the "base mapper", the parentmost
+ # task at the top of an inheritance chain
+ # dependency sorting is done via non-inheriting
+ # mappers only, dependencies between mappers
# in the same inheritance chain is done at the per-object level
mapper = mapper.primary_mapper().base_mapper
dependency = dependency.primary_mapper().base_mapper
task.dependencies.add(up)
def execute(self):
- """Execute this UOWTransaction.
-
- This will organize all collected UOWTasks into a dependency-sorted
- list which is then traversed using the traversal scheme
- encoded in the UOWExecutor class. Operations to mappers and dependency
- processors are fired off in order to issue SQL to the database and
- synchronize instance attributes with database values and related
- foreign key values."""
+ """Execute this steps assembled into this UOWTransaction."""
# pre-execute dependency processors. this process may
# result in new tasks, objects and/or dependency processors being added,
self.session._register_newly_persistent(elem.state)
def _sort_dependencies(self):
- nodes = topological.sort_with_cycles(self.dependencies,
+ nodes = topological._sort_with_cycles(self.dependencies,
[t.mapper for t in self.tasks.itervalues() if t.base_task is t]
)
log.class_logger(UOWTransaction)
class UOWTask(object):
- """A collection of mapped states corresponding to a particular mapper."""
+ """A collection of mapped states corresponding to a particular mapper.
+
+ This object is deprecated.
+ """
def __init__(self, uowtransaction, mapper, base_task=None):
self.uowtransaction = uowtransaction
"""Return an iterator of UOWTask objects corresponding to the
inheritance sequence of this UOWTask's mapper.
- e.g. if mapper B and mapper C inherit from mapper A, and
- mapper D inherits from B:
-
- mapperA -> mapperB -> mapperD
- -> mapperC
-
- the inheritance sequence starting at mapper A is a depth-first
- traversal:
-
- [mapperA, mapperB, mapperD, mapperC]
-
- this method will therefore return
-
- [UOWTask(mapperA), UOWTask(mapperB), UOWTask(mapperD),
- UOWTask(mapperC)]
-
- The concept of "polymporphic iteration" is adapted into
- several property-based iterators which return object
- instances, UOWTaskElements and UOWDependencyProcessors in an
- order corresponding to this sequence of parent UOWTasks. This
- is used to issue operations related to inheritance-chains of
- mappers in the proper order based on dependencies between
- those mappers.
-
"""
for mapper in self.inheriting_mappers:
t = self.base_task._inheriting_tasks.get(mapper, None)
yield t
def is_empty(self):
- """return True if this UOWTask is 'empty', meaning it has no child items.
+ """return True if this UOWTask is 'empty',
+ meaning it has no child items.
- used only for debugging output.
"""
return not self._objects and not self.dependencies
rec.update(listonly, isdelete)
def append_postupdate(self, state, post_update_cols):
- """issue a 'post update' UPDATE statement via this object's mapper immediately.
+ """issue a 'post update' UPDATE statement via
+ this object's mapper immediately.
- this operation is used only with relationships that specify the `post_update=True`
- flag.
"""
- # postupdates are UPDATED immeditely (for now)
- # convert post_update_cols list to a Set so that __hash__() is used to compare columns
- # instead of __eq__()
- self.mapper._save_obj([state], self.uowtransaction, postupdate=True, post_update_cols=set(post_update_cols))
+ self.mapper._save_obj([state], self.uowtransaction,
+ postupdate=True, post_update_cols=set(post_update_cols))
def __contains__(self, state):
- """return True if the given object is contained within this UOWTask or inheriting tasks."""
+ """return True if the given object is contained
+ within this UOWTask or inheriting tasks."""
for task in self.polymorphic_tasks:
if state in task._objects:
return False
def is_deleted(self, state):
- """return True if the given object is marked as to be deleted within this UOWTask."""
+ """return True if the given object is marked
+ as to be deleted within this UOWTask."""
try:
return self._objects[state].isdelete
return self.cyclical_dependencies
def _sort_circular_dependencies(self, trans, cycles):
- """Topologically sort individual entities with row-level dependencies.
+ """sort row-level dependencies.
- Builds a modified UOWTask structure, and is invoked when the
- per-mapper topological structure is found to have cycles.
+ Note that this method is a total disaster, as it was
+ bolted onto the originally simple unit-of-work
+ system after more complex mappings revealed
+ the presence of inter-row rependencies - this occured
+ well within version 0.1 and despite many fixes
+ has remained the most legacy code within SQLAlchemy.
+ It is gone without a trace after [ticket:1742].
"""
if depprocessor not in tasks:
tasks[depprocessor] = UOWDependencyProcessor(
depprocessor.processor,
- UOWTask(self.uowtransaction, depprocessor.targettask.mapper)
+ UOWTask(self.uowtransaction,
+ depprocessor.targettask.mapper)
)
tasks[depprocessor].targettask.append(target_state, isdelete=isdelete)
isdelete = taskelement.isdelete
# list of dependent objects from this object
- (added, unchanged, deleted) = dep.get_object_dependencies(state, trans, passive=True)
+ (added, unchanged, deleted) = dep.get_object_dependencies(
+ state, trans, passive=True)
if not added and not unchanged and not deleted:
continue
tuples.append(whosdep)
if whosdep[0] is state:
- set_processor_for_state(whosdep[0], dep, whosdep[0], isdelete=isdelete)
+ set_processor_for_state(whosdep[0], dep, whosdep[0],
+ isdelete=isdelete)
else:
- set_processor_for_state(whosdep[0], dep, whosdep[1], isdelete=isdelete)
+ set_processor_for_state(whosdep[0], dep, whosdep[1],
+ isdelete=isdelete)
else:
# TODO: no test coverage here
set_processor_for_state(state, dep, state, isdelete=isdelete)
# dependency - keep non-dependent objects
# grouped together, so that insert ordering as determined
# by session.add() is maintained.
- # An alternative might be to represent the "insert order"
- # as part of the topological sort itself, which would
- # eliminate the need for this step (but may make the original
- # topological sort more expensive)
- head = topological.sort_as_tree(tuples, object_to_original_task.iterkeys())
+ head = topological._sort_as_tree(tuples, object_to_original_task.iterkeys())
if head is not None:
original_to_tasks = {}
stack = [(head, t)]
else:
task = original_to_tasks[(parenttask, originating_task)]
- task.append(state, originating_task._objects[state].listonly, isdelete=originating_task._objects[state].isdelete)
+ task.append(state, originating_task._objects[state].listonly,
+ isdelete=originating_task._objects[state].isdelete)
if state in dependencies:
task.cyclical_dependencies.update(dependencies[state].itervalues())
return ("UOWTask(%s) Mapper: '%r'" % (hex(id(self)), self.mapper))
class UOWTaskElement(object):
- """Corresponds to a single InstanceState to be saved, deleted,
- or otherwise marked as having dependencies. A collection of
- UOWTaskElements are held by a UOWTask.
-
+ """Represent a single state to be saved.
+
+ This object is deprecated.
"""
def __init__(self, state):
self.state = state
)
class UOWDependencyProcessor(object):
- """In between the saving and deleting of objects, process
- dependent data, such as filling in a foreign key on a child item
- from a new primary key, or deleting association rows before a
- delete. This object acts as a proxy to a DependencyProcessor.
+ """Represent tasks in between inserts/updates/deletes.
+ This object is deprecated.
"""
def __init__(self, processor, targettask):
self.processor = processor
return hash((self.processor, self.targettask))
def preexecute(self, trans):
- """preprocess all objects contained within this ``UOWDependencyProcessor``s target task.
-
- This may locate additional objects which should be part of the
- transaction, such as those affected deletes, orphans to be
- deleted, etc.
-
- Once an object is preprocessed, its ``UOWTaskElement`` is marked as processed. If subsequent
- changes occur to the ``UOWTaskElement``, its processed flag is reset, and will require processing
- again.
-
- Return True if any objects were preprocessed, or False if no
- objects were preprocessed. If True is returned, the parent ``UOWTransaction`` will
- ultimately call ``preexecute()`` again on all processors until no new objects are processed.
+ """preprocess all objects contained within
+ this ``UOWDependencyProcessor``s target task.
"""
def getobj(elem):
return ret
def execute(self, trans, delete):
- """process all objects contained within this ``UOWDependencyProcessor``s target task."""
+ """process all objects contained within this
+ ``UOWDependencyProcessor``s target task."""
elements = [e for e in
def whose_dependent_on_who(self, state1, state2):
"""establish which object is operationally dependent amongst a parent/child
using the semantics stated by the dependency processor.
-
- This method is used to establish a partial ordering (set of dependency tuples)
- when toplogically sorting on a per-instance basis.
-
"""
return self.processor.whose_dependent_on_who(state1, state2)
class UOWExecutor(object):
- """Encapsulates the execution traversal of a UOWTransaction structure."""
+ """Encapsulates the execution traversal
+ of a UOWTransaction structure.
+
+ This part of the approach is the core flaw that's
+ being removed with [ticket:1742], as it necessitates
+ deep levels of recursion.
+
+ """
def execute(self, trans, tasks, isdelete=None):
if isdelete is not True:
"""Topological sorting algorithms.
-The topological sort is an algorithm that receives this list of
-dependencies as a *partial ordering*, that is a list of pairs which
-might say, *X is dependent on Y*, *Q is dependent on Z*, but does not
-necessarily tell you anything about Q being dependent on X. Therefore,
-its not a straight sort where every element can be compared to
-another... only some of the elements have any sorting preference, and
-then only towards just some of the other elements. For a particular
-partial ordering, there can be many possible sorts that satisfy the
-conditions.
+All functions and classes in this module are currently deprecated,
+and will be replaced by a much simpler and more efficient
+system in an upcoming release. See [ticket:1742] for
+details.
"""
from sqlalchemy.exc import CircularDependencyError
from sqlalchemy import util
-__all__ = ['sort', 'sort_with_cycles', 'sort_as_tree']
-
-def sort(tuples, allitems):
+def _sort(tuples, allitems):
"""sort the given list of items by dependency.
- 'tuples' is a list of tuples representing a partial ordering.
+ deprecated. a new sort with slightly different
+ behavior will replace this method in an upcoming release.
"""
- return [n.item for n in _sort(tuples, allitems, allow_cycles=False, ignore_self_cycles=True)]
+ return [n.item for n in _sort_impl(tuples, allitems,
+ allow_cycles=False,
+ ignore_self_cycles=True)]
-def sort_with_cycles(tuples, allitems):
+def _sort_with_cycles(tuples, allitems):
"""sort the given list of items by dependency, cutting out cycles.
-
- returns results as an iterable of 2-tuples, containing the item,
- and a list containing items involved in a cycle with this item, if any.
-
- 'tuples' is a list of tuples representing a partial ordering.
+
+ deprecated. a new approach to cycle detection will
+ be introduced in an upcoming release.
"""
- return [(n.item, [n.item for n in n.cycles or []]) for n in _sort(tuples, allitems, allow_cycles=True)]
+ return [(n.item, [n.item for n in n.cycles or []])
+ for n in _sort_impl(tuples, allitems, allow_cycles=True)]
-def sort_as_tree(tuples, allitems, with_cycles=False):
+def _sort_as_tree(tuples, allitems, with_cycles=False):
"""sort the given list of items by dependency, and return results
as a hierarchical tree structure.
- returns results as an iterable of 3-tuples, containing the item,
- a list containing items involved in a cycle with this item, if any,
- and a list of child tuples.
-
- if with_cycles is False, the returned structure is of the same form
- but the second element of each tuple, i.e. the 'cycles', is an empty list.
-
- 'tuples' is a list of tuples representing a partial ordering.
+ deprecated. a new approach to "grouped" topological sorting
+ will be introduced in an upcoming release.
"""
- return _organize_as_tree(_sort(tuples, allitems, allow_cycles=with_cycles))
+ return _organize_as_tree(_sort_impl(tuples, allitems, allow_cycles=with_cycles))
class _Node(object):
def __repr__(self):
return repr(list(self))
-def _sort(tuples, allitems, allow_cycles=False, ignore_self_cycles=False):
+def _sort_impl(tuples, allitems, allow_cycles=False, ignore_self_cycles=False):
nodes = {}
edges = _EdgeCollection()
set as siblings to each other as possible.
returns nodes as 3-tuples (item, cycles, children).
+
+ this function is deprecated.
"""
if not nodes:
return (head.item, [n.item for n in head.cycles or []], head.children)
def _find_cycles(edges):
+ """
+ this function is deprecated.
+ """
cycles = {}
def traverse(node, cycle, goal):
-import sqlalchemy.topological as topological
+from sqlalchemy import topological
from sqlalchemy.test import TestBase
(node4, subnode3),
(node4, subnode4)
]
- head = topological.sort_as_tree(tuples, [])
+ head = topological._sort_as_tree(tuples, [])
self.assert_sort(tuples, head)
def testsort2(self):
(node5, node6),
(node6, node2)
]
- head = topological.sort_as_tree(tuples, [node7])
+ head = topological._sort_as_tree(tuples, [node7])
self.assert_sort(tuples, head, [node7])
def testsort3(self):
(node3, node2),
(node1,node3)
]
- head1 = topological.sort_as_tree(tuples, [node1, node2, node3])
- head2 = topological.sort_as_tree(tuples, [node3, node1, node2])
- head3 = topological.sort_as_tree(tuples, [node3, node2, node1])
+ head1 = topological._sort_as_tree(tuples, [node1, node2, node3])
+ head2 = topological._sort_as_tree(tuples, [node3, node1, node2])
+ head3 = topological._sort_as_tree(tuples, [node3, node2, node1])
# TODO: figure out a "node == node2" function
#self.assert_(str(head1) == str(head2) == str(head3))
(node1, node3),
(node3, node2)
]
- head = topological.sort_as_tree(tuples, [])
+ head = topological._sort_as_tree(tuples, [])
self.assert_sort(tuples, head)
def testsort5(self):
node3,
node4
]
- head = topological.sort_as_tree(tuples, allitems, with_cycles=True)
+ head = topological._sort_as_tree(tuples, allitems, with_cycles=True)
self.assert_sort(tuples, head)
def testcircular(self):
(node4, node1)
]
allitems = [node1, node2, node3, node4]
- head = topological.sort_as_tree(tuples, allitems, with_cycles=True)
+ head = topological._sort_as_tree(tuples, allitems, with_cycles=True)
self.assert_sort(tuples, head)
def testcircular2(self):
(node3, node2),
(node2, node3)
]
- head = topological.sort_as_tree(tuples, [], with_cycles=True)
+ head = topological._sort_as_tree(tuples, [], with_cycles=True)
self.assert_sort(tuples, head)
def testcircular3(self):
tuples = [(question, issue), (providerservice, issue), (provider, question), (question, provider), (providerservice, question), (provider, providerservice), (question, answer), (issue, question)]
- head = topological.sort_as_tree(tuples, [], with_cycles=True)
+ head = topological._sort_as_tree(tuples, [], with_cycles=True)
self.assert_sort(tuples, head)
def testbigsort(self):
tuples = [(i, i + 1) for i in range(0, 1500, 2)]
- head = topological.sort_as_tree(tuples, [])
+ head = topological._sort_as_tree(tuples, [])
def testids(self):
# ticket:1380 regression: would raise a KeyError
- topological.sort([(id(i), i) for i in range(3)], [])
+ topological._sort([(id(i), i) for i in range(3)], [])