From: Mike Bayer Date: Sun, 9 Sep 2007 17:01:38 +0000 (+0000) Subject: - added 'passive_deletes="all"' flag to relation(), disables all X-Git-Tag: rel_0_4beta6~28 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=9d5264f9f99f75187b3f95d08ebabbaea8529901;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - added 'passive_deletes="all"' flag to relation(), disables all nulling-out of foreign key attributes during a flush where the parent object is deleted. - fix to FK compile fix from yesterday --- diff --git a/CHANGES b/CHANGES index 8a8b897a2a..9cbd3be8d8 100644 --- a/CHANGES +++ b/CHANGES @@ -21,6 +21,10 @@ CHANGES - 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 diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 0d4dec4bc4..5a2c414497 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -168,14 +168,21 @@ def relation(argument, secondary=None, **kwargs): 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 + 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 diff --git a/lib/sqlalchemy/orm/dependency.py b/lib/sqlalchemy/orm/dependency.py index 3b837d275a..1e461e6bf7 100644 --- a/lib/sqlalchemy/orm/dependency.py +++ b/lib/sqlalchemy/orm/dependency.py @@ -188,7 +188,7 @@ class OneToManyDP(DependencyProcessor): # 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: @@ -217,7 +217,7 @@ class OneToManyDP(DependencyProcessor): 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: @@ -265,7 +265,7 @@ class ManyToOneDP(DependencyProcessor): 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: diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 20cbcb2351..90a7ba3a42 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -161,6 +161,9 @@ class PropertyLoader(StrategizedProperty): 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: diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index 212fdac82a..3b7f08f79c 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -509,6 +509,8 @@ class Column(SchemaItem, expression._ColumnClause): 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: @@ -555,6 +557,7 @@ class Column(SchemaItem, expression._ColumnClause): 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: diff --git a/test/orm/unitofwork.py b/test/orm/unitofwork.py index 63456b6385..c202abf99b 100644 --- a/test/orm/unitofwork.py +++ b/test/orm/unitofwork.py @@ -579,6 +579,72 @@ class PassiveDeletesTest(ORMTest): 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,