- Fixed reflection of the empty string for mysql enums.
+- added 'passive_deletes="all"' flag to relation(), disables all
+ nulling-out of foreign key attributes during a flush where the parent
+ object is deleted.
+
- column defaults and onupdates, executing inline, will add parenthesis
for subqueries and other parenthesis-requiring expressions
indicates the ordering that should be applied when loading these items.
passive_deletes=False
- Indicates if lazy-loaders should not be executed during the ``flush()``
- process, which normally occurs in order to locate all existing child
- items when a parent item is to be deleted. Setting this flag to True is
- appropriate when ``ON DELETE CASCADE`` rules have been set up on the
- actual tables so that the database may handle cascading deletes
- automatically. This strategy is useful particularly for handling the
- deletion of objects that have very large (and/or deep) child-object
- collections.
+ Indicates the behavior of delete operations.
+ A value of True indicates that unloaded child items should not be loaded
+ during a delete operation on the parent. Normally, when a parent
+ item is deleted, all child items are loaded so that they can either be
+ marked as deleted, or have their foreign key to the parent set to NULL.
+ Marking this flag as True usually implies an ON DELETE <CASCADE|SET NULL>
+ rule is in place which will handle updating/deleting child rows on the
+ database side.
+
+ Additionally, setting the flag to the string value 'all' will disable
+ the "nulling out" of the child foreign keys, when there is no delete or
+ delete-orphan cascade enabled. This is typically used when a triggering
+ or error raise scenario is in place on the database side. Note that
+ the foreign key attributes on in-session child objects will not be changed
+ after a flush occurs so this is a very special use-case setting.
post_update
this indicates that the relationship should be handled by a second
# the child objects have to have their foreign key to the parent set to NULL
# this phase can be called safely for any cascade but is unnecessary if delete cascade
# is on.
- if not self.cascade.delete or self.post_update:
+ if (not self.cascade.delete or self.post_update) and not self.passive_deletes=='all':
for obj in deplist:
childlist = self.get_object_dependencies(obj, uowcommit, passive=self.passive_deletes)
if childlist is not None:
if delete:
# head object is being deleted, and we manage its list of child objects
# the child objects have to have their foreign key to the parent set to NULL
- if not self.post_update and not self.cascade.delete:
+ if not self.post_update and not self.cascade.delete and not self.passive_deletes=='all':
for obj in deplist:
childlist = self.get_object_dependencies(obj, uowcommit, passive=self.passive_deletes)
if childlist is not None:
def process_dependencies(self, task, deplist, uowcommit, delete = False):
#print self.mapper.mapped_table.name + " " + self.key + " " + repr(len(deplist)) + " process_dep isdelete " + repr(delete) + " direction " + repr(self.direction)
if delete:
- if self.post_update and not self.cascade.delete_orphan:
+ if self.post_update and not self.cascade.delete_orphan and not self.passive_deletes=='all':
# post_update means we have to update our row to not reference the child object
# before we can DELETE the row
for obj in deplist:
self.cascade = mapperutil.CascadeOptions("all, delete-orphan")
else:
self.cascade = mapperutil.CascadeOptions("save-update, merge")
+
+ if self.passive_deletes == 'all' and ("delete" in self.cascade or "delete-orphan" in self.cascade):
+ raise exceptions.ArgumentError("Can't set passive_deletes='all' in conjunction with 'delete' or 'delete-orphan' cascade")
self.association = association
if association:
if not self._is_oid:
self._pre_existing_column = table._columns.get(self.key)
table._columns.add(self)
+ else:
+ self._pre_existing_column = None
if self.primary_key:
table.primary_key.add(self)
elif self.key in table.primary_key:
c.orig_set = self.orig_set
c.__originating_column = self.__originating_column
c._distance = self._distance + 1
+ c._pre_existing_column = self._pre_existing_column
if not c._is_oid:
selectable.columns.add(c)
if self.primary_key:
sess.commit()
assert mytable.count().scalar() == 0
assert myothertable.count().scalar() == 0
+
+class ExtraPassiveDeletesTest(ORMTest):
+ def define_tables(self, metadata):
+ global mytable,myothertable
+
+ mytable = Table('mytable', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('data', String(30)),
+ test_needs_fk=True,
+ )
+
+ myothertable = Table('myothertable', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('parent_id', Integer),
+ Column('data', String(30)),
+ ForeignKeyConstraint(['parent_id'],['mytable.id']), # no CASCADE, the same as ON DELETE RESTRICT
+ test_needs_fk=True,
+ )
+
+ def test_assertions(self):
+ class MyClass(object):
+ pass
+ class MyOtherClass(object):
+ pass
+
+ mapper(MyOtherClass, myothertable)
+
+ try:
+ mapper(MyClass, mytable, properties={
+ 'children':relation(MyOtherClass, passive_deletes='all', cascade="all")
+ })
+ assert False
+ except exceptions.ArgumentError, e:
+ assert str(e) == "Can't set passive_deletes='all' in conjunction with 'delete' or 'delete-orphan' cascade"
+
+ @testing.unsupported('sqlite')
+ def test_extra_passive(self):
+ class MyClass(object):
+ pass
+ class MyOtherClass(object):
+ pass
+
+ mapper(MyOtherClass, myothertable)
+
+ mapper(MyClass, mytable, properties={
+ 'children':relation(MyOtherClass, passive_deletes='all', cascade="save-update")
+ })
+
+ sess = Session
+ mc = MyClass()
+ mc.children.append(MyOtherClass())
+ mc.children.append(MyOtherClass())
+ mc.children.append(MyOtherClass())
+ mc.children.append(MyOtherClass())
+ sess.save(mc)
+ sess.commit()
+
+ assert myothertable.count().scalar() == 4
+ mc = sess.query(MyClass).get(mc.id)
+ sess.delete(mc)
+ try:
+ sess.commit()
+ assert False
+ except (exceptions.IntegrityError, exceptions.OperationalError):
+ assert True
+
class DefaultTest(ORMTest):
"""tests that when saving objects whose table contains DefaultGenerators, either python-side, preexec or database-side,