summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/sqlalchemy/orm/dependency.py27
-rw-r--r--lib/sqlalchemy/orm/unitofwork.py21
-rw-r--r--lib/sqlalchemy/test/assertsql.py15
-rw-r--r--lib/sqlalchemy/topological.py3
-rw-r--r--test/orm/test_naturalpks.py28
-rw-r--r--test/orm/test_unitofworkv2.py155
6 files changed, 213 insertions, 36 deletions
diff --git a/lib/sqlalchemy/orm/dependency.py b/lib/sqlalchemy/orm/dependency.py
index 1018c2029..d02776dce 100644
--- a/lib/sqlalchemy/orm/dependency.py
+++ b/lib/sqlalchemy/orm/dependency.py
@@ -110,7 +110,8 @@ class DependencyProcessor(object):
"""
if self.post_update and self._check_reverse(uow):
- return
+ # TODO: coverage here
+ return iter([])
# locate and disable the aggregate processors
# for this dependency
@@ -684,7 +685,8 @@ class DetectKeySwitch(DependencyProcessor):
])
def per_state_flush_actions(self, uow, states, isdelete):
- pass
+ # TODO: coverage here
+ return iter([])
def presort_deletes(self, uowcommit, states):
assert False
@@ -741,9 +743,9 @@ class ManyToManyDP(DependencyProcessor):
def per_state_flush_actions(self, uow, states, isdelete):
if self._check_reverse(uow):
- return
+ return iter([])
else:
- DependencyProcessor.\
+ return DependencyProcessor.\
per_state_flush_actions(self, uow, states, isdelete)
def per_property_dependencies(self, uow, parent_saves,
@@ -777,15 +779,20 @@ class ManyToManyDP(DependencyProcessor):
after_save, before_delete,
isdelete, childisdelete):
if not isdelete:
- uow.dependencies.update([
- (save_parent, after_save),
- (after_save, child_action),
- (save_parent, child_action)
- ])
+ if childisdelete:
+ uow.dependencies.update([
+ (save_parent, after_save),
+ (after_save, child_action),
+ ])
+ else:
+ uow.dependencies.update([
+ (save_parent, after_save),
+ (child_action, after_save),
+ ])
else:
uow.dependencies.update([
(before_delete, child_action),
- (child_action, delete_parent)
+ (before_delete, delete_parent)
])
def presort_deletes(self, uowcommit, states):
diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py
index ca8c31e86..85ed790d9 100644
--- a/lib/sqlalchemy/orm/unitofwork.py
+++ b/lib/sqlalchemy/orm/unitofwork.py
@@ -4,19 +4,11 @@
# 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.
-
-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.
+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.
"""
@@ -79,11 +71,6 @@ class UOWEventHandler(interfaces.AttributeExtension):
class UOWTransaction(object):
- """Handles the details of organizing and executing transaction
- tasks during a UnitOfWork object's flush() operation.
-
- """
-
def __init__(self, session):
self.session = session
self.mapper_flush_opts = session._mapper_flush_opts
diff --git a/lib/sqlalchemy/test/assertsql.py b/lib/sqlalchemy/test/assertsql.py
index 1417c2e43..81a6191a1 100644
--- a/lib/sqlalchemy/test/assertsql.py
+++ b/lib/sqlalchemy/test/assertsql.py
@@ -156,12 +156,15 @@ class CompiledSQL(SQLMatchRule):
if not isinstance(params, list):
params = [params]
- # do a positive compare only
- for param, received in zip(params, _received_parameters):
- for k, v in param.iteritems():
- if k not in received or received[k] != v:
- equivalent = False
- break
+ while params:
+ param = params.pop(0)
+ if param not in _received_parameters:
+ equivalent = False
+ break
+ else:
+ _received_parameters.remove(param)
+ if _received_parameters:
+ equivalent = False
else:
params = {}
diff --git a/lib/sqlalchemy/topological.py b/lib/sqlalchemy/topological.py
index fbde7c601..a6328a5e4 100644
--- a/lib/sqlalchemy/topological.py
+++ b/lib/sqlalchemy/topological.py
@@ -9,6 +9,9 @@
from sqlalchemy.exc import CircularDependencyError
from sqlalchemy import util
+# this enables random orderings for iterated subsets
+# of non-dependent items.
+#from sqlalchemy.test.util import RandomSet as set
__all__ = ['sort', 'sort_as_subsets', 'find_cycles']
diff --git a/test/orm/test_naturalpks.py b/test/orm/test_naturalpks.py
index 216c10f1a..f81167062 100644
--- a/test/orm/test_naturalpks.py
+++ b/test/orm/test_naturalpks.py
@@ -420,7 +420,7 @@ class ReversePKsTest(_base.MappedTest):
assert session.query(User).get([1, EDITABLE]) is a_editable
-class SelfRefTest(_base.MappedTest):
+class SelfReferentialTest(_base.MappedTest):
__unsupported_on__ = ('mssql',) # mssql doesn't allow ON UPDATE on self-referential keys
@classmethod
@@ -441,7 +441,7 @@ class SelfRefTest(_base.MappedTest):
pass
@testing.resolve_artifact_names
- def test_onetomany(self):
+ def test_one_to_many(self):
mapper(Node, nodes, properties={
'children': relationship(Node,
backref=sa.orm.backref('parentnode',
@@ -465,6 +465,30 @@ class SelfRefTest(_base.MappedTest):
for n in sess.query(Node).filter(
Node.name.in_(['n11', 'n12', 'n13']))])
+ @testing.resolve_artifact_names
+ def test_many_to_one(self):
+ mapper(Node, nodes, properties={
+ 'parentnode':relationship(Node,
+ remote_side=nodes.c.name,
+ passive_updates=True)
+ }
+ )
+
+ sess = create_session()
+ n1 = Node(name='n1')
+ n11 = Node(name='n11', parentnode=n1)
+ n12 = Node(name='n12', parentnode=n1)
+ n13 = Node(name='n13', parentnode=n1)
+ sess.add_all([n1, n11, n12, n13])
+ sess.flush()
+
+ n1.name = 'new n1'
+ sess.flush()
+ eq_(['new n1', 'new n1', 'new n1'],
+ [n.parent
+ for n in sess.query(Node).filter(
+ Node.name.in_(['n11', 'n12', 'n13']))])
+
class NonPKCascadeTest(_base.MappedTest):
@classmethod
diff --git a/test/orm/test_unitofworkv2.py b/test/orm/test_unitofworkv2.py
index 42d9fd90f..e8b7d9837 100644
--- a/test/orm/test_unitofworkv2.py
+++ b/test/orm/test_unitofworkv2.py
@@ -1,6 +1,8 @@
from sqlalchemy.test.testing import eq_, assert_raises, assert_raises_message
from sqlalchemy.test import testing
-from test.orm import _fixtures
+from sqlalchemy.test.schema import Table, Column
+from sqlalchemy import Integer, String, ForeignKey
+from test.orm import _fixtures, _base
from sqlalchemy.orm import mapper, relationship, backref, create_session
from sqlalchemy.test.assertsql import AllOf, CompiledSQL
@@ -385,4 +387,155 @@ class SingleCycleTest(UOWTest):
# testing.db,
# sess.flush,
# )
+
+class SingleCycleM2MTest(_base.MappedTest, testing.AssertsExecutionResults):
+
+ @classmethod
+ def define_tables(cls, metadata):
+ nodes = Table('nodes', metadata,
+ Column('id', Integer,
+ primary_key=True,
+ test_needs_autoincrement=True),
+ Column('data', String(30)),
+ Column('favorite_node_id', Integer, ForeignKey('nodes.id'))
+ )
+
+ node_to_nodes =Table('node_to_nodes', metadata,
+ Column('left_node_id', Integer,
+ ForeignKey('nodes.id'),primary_key=True),
+ Column('right_node_id', Integer,
+ ForeignKey('nodes.id'),primary_key=True),
+ )
+
+ @testing.resolve_artifact_names
+ def test_many_to_many_one(self):
+ class Node(Base):
+ pass
+
+ mapper(Node, nodes, properties={
+ 'children':relationship(Node, secondary=node_to_nodes,
+ primaryjoin=nodes.c.id==node_to_nodes.c.left_node_id,
+ secondaryjoin=nodes.c.id==node_to_nodes.c.right_node_id,
+ backref='parents'
+ ),
+ 'favorite':relationship(Node, remote_side=nodes.c.id)
+ })
+
+ sess = create_session()
+ n1 = Node(data='n1')
+ n2 = Node(data='n2')
+ n3 = Node(data='n3')
+ n4 = Node(data='n4')
+ n5 = Node(data='n5')
+
+ n4.favorite = n3
+ n1.favorite = n5
+ n5.favorite = n2
+
+ n1.children = [n2, n3, n4]
+ n2.children = [n3, n5]
+ n3.children = [n5, n4]
+
+ sess.add_all([n1, n2, n3, n4, n5])
+ self.assert_sql_execution(
+ testing.db,
+ sess.flush,
+
+ CompiledSQL(
+ "INSERT INTO nodes (data, favorite_node_id) "
+ "VALUES (:data, :favorite_node_id)",
+ {'data': 'n2', 'favorite_node_id': None}
+ ),
+ CompiledSQL(
+ "INSERT INTO nodes (data, favorite_node_id) "
+ "VALUES (:data, :favorite_node_id)",
+ {'data': 'n3', 'favorite_node_id': None}),
+ CompiledSQL("INSERT INTO nodes (data, favorite_node_id) "
+ "VALUES (:data, :favorite_node_id)",
+ lambda ctx:{'data': 'n5', 'favorite_node_id': n2.id}),
+ CompiledSQL(
+ "INSERT INTO nodes (data, favorite_node_id) "
+ "VALUES (:data, :favorite_node_id)",
+ lambda ctx:{'data': 'n4', 'favorite_node_id': n3.id}),
+ CompiledSQL(
+ "INSERT INTO node_to_nodes (left_node_id, right_node_id) "
+ "VALUES (:left_node_id, :right_node_id)",
+ lambda ctx:[
+ {'right_node_id': n5.id, 'left_node_id': n3.id},
+ {'right_node_id': n4.id, 'left_node_id': n3.id},
+ {'right_node_id': n3.id, 'left_node_id': n2.id},
+ {'right_node_id': n5.id, 'left_node_id': n2.id}
+ ]
+ ),
+ CompiledSQL(
+ "INSERT INTO nodes (data, favorite_node_id) "
+ "VALUES (:data, :favorite_node_id)",
+ lambda ctx:[{'data': 'n1', 'favorite_node_id': n5.id}]
+ ),
+ CompiledSQL(
+ "INSERT INTO node_to_nodes (left_node_id, right_node_id) "
+ "VALUES (:left_node_id, :right_node_id)",
+ lambda ctx:[
+ {'right_node_id': n2.id, 'left_node_id': n1.id},
+ {'right_node_id': n3.id, 'left_node_id': n1.id},
+ {'right_node_id': n4.id, 'left_node_id': n1.id}
+ ])
+ )
+
+ sess.delete(n1)
+
+ self.assert_sql_execution(
+ testing.db,
+ sess.flush,
+ CompiledSQL(
+ "DELETE FROM node_to_nodes WHERE "
+ "node_to_nodes.left_node_id = :left_node_id AND "
+ "node_to_nodes.right_node_id = :right_node_id",
+ lambda ctx:[
+ {'right_node_id': n2.id, 'left_node_id': n1.id},
+ {'right_node_id': n3.id, 'left_node_id': n1.id},
+ {'right_node_id': n4.id, 'left_node_id': n1.id}
+ ]
+ ),
+ CompiledSQL(
+ "DELETE FROM nodes WHERE nodes.id = :id",
+ lambda ctx:{'id': n1.id}
+ ),
+ )
+
+ for n in [n2, n3, n4, n5]:
+ sess.delete(n)
+
+ # load these collections
+ # outside of the flush() below
+ n4.children
+ n5.children
+
+ self.assert_sql_execution(
+ testing.db,
+ sess.flush,
+ CompiledSQL(
+ "DELETE FROM node_to_nodes WHERE node_to_nodes.left_node_id "
+ "= :left_node_id AND node_to_nodes.right_node_id = "
+ ":right_node_id",
+ lambda ctx:[
+ {'right_node_id': n5.id, 'left_node_id': n3.id},
+ {'right_node_id': n4.id, 'left_node_id': n3.id},
+ {'right_node_id': n3.id, 'left_node_id': n2.id},
+ {'right_node_id': n5.id, 'left_node_id': n2.id}
+ ]
+ ),
+ CompiledSQL(
+ "DELETE FROM nodes WHERE nodes.id = :id",
+ lambda ctx:[{'id': n4.id}, {'id': n5.id}]
+ ),
+ CompiledSQL(
+ "DELETE FROM nodes WHERE nodes.id = :id",
+ lambda ctx:[{'id': n2.id}, {'id': n3.id}]
+ ),
+ )
+
+
+
+
\ No newline at end of file