hooks/integrity.py
brancholdstable
changeset 7074 e4580e5f0703
parent 6894 ba3f7e655414
child 6957 ffda12be2e9f
--- a/hooks/integrity.py	Fri Dec 10 12:17:18 2010 +0100
+++ b/hooks/integrity.py	Fri Mar 11 09:46:45 2011 +0100
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Core hooks: check for data integrity according to the instance'schema
 validity
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from threading import Lock
@@ -31,7 +31,6 @@
 from cubicweb.selectors import is_instance
 from cubicweb.uilib import soup2xhtml
 from cubicweb.server import hook
-from cubicweb.server.hook import set_operation
 
 # special relations that don't have to be checked for integrity, usually
 # because they are handled internally by hooks (so we trust ourselves)
@@ -62,27 +61,25 @@
         _UNIQUE_CONSTRAINTS_LOCK.release()
 
 class _ReleaseUniqueConstraintsOperation(hook.Operation):
-    def commit_event(self):
-        pass
     def postcommit_event(self):
         _release_unique_cstr_lock(self.session)
     def rollback_event(self):
         _release_unique_cstr_lock(self.session)
 
 
-class _CheckRequiredRelationOperation(hook.LateOperation):
-    """checking relation cardinality has to be done after commit in
-    case the relation is being replaced
+class _CheckRequiredRelationOperation(hook.DataOperationMixIn,
+                                      hook.LateOperation):
+    """checking relation cardinality has to be done after commit in case the
+    relation is being replaced
     """
+    containercls = list
     role = key = base_rql = None
 
     def precommit_event(self):
-        session =self.session
+        session = self.session
         pendingeids = session.transaction_data.get('pendingeids', ())
         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
-        # poping key is not optional: if further operation trigger new deletion
-        # of relation, we'll need a new operation
-        for eid, rtype in session.transaction_data.pop(self.key):
+        for eid, rtype in self.get_data():
             # recheck pending eids / relation types
             if eid in pendingeids:
                 continue
@@ -100,13 +97,11 @@
 class _CheckSRelationOp(_CheckRequiredRelationOperation):
     """check required subject relation"""
     role = 'subject'
-    key = '_cwisrel'
     base_rql = 'Any O WHERE S eid %%(x)s, S %s O'
 
 class _CheckORelationOp(_CheckRequiredRelationOperation):
     """check required object relation"""
     role = 'object'
-    key = '_cwiorel'
     base_rql = 'Any S WHERE O eid %%(x)s, S %s O'
 
 
@@ -115,15 +110,32 @@
     category = 'integrity'
 
 
-class CheckCardinalityHook(IntegrityHook):
+class CheckCardinalityHookBeforeDeleteRelation(IntegrityHook):
     """check cardinalities are satisfied"""
-    __regid__ = 'checkcard'
-    events = ('after_add_entity', 'before_delete_relation')
+    __regid__ = 'checkcard_before_delete_relation'
+    events = ('before_delete_relation',)
 
     def __call__(self):
-        getattr(self, self.event)()
+        rtype = self.rtype
+        if rtype in DONT_CHECK_RTYPES_ON_DEL:
+            return
+        session = self._cw
+        eidfrom, eidto = self.eidfrom, self.eidto
+        pendingrdefs = session.transaction_data.get('pendingrdefs', ())
+        if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
+            return
+        card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
+        if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
+            _CheckSRelationOp.get_instance(self._cw).add_data((eidfrom, rtype))
+        if card[1] in '1+' and not session.deleted_in_transaction(eidto):
+            _CheckORelationOp.get_instance(self._cw).add_data((eidto, rtype))
 
-    def after_add_entity(self):
+class CheckCardinalityHookAfterAddEntity(IntegrityHook):
+    """check cardinalities are satisfied"""
+    __regid__ = 'checkcard_after_add_entity'
+    events = ('after_add_entity',)
+
+    def __call__(self):
         eid = self.entity.eid
         eschema = self.entity.e_schema
         for rschema, targetschemas, role in eschema.relation_definitions():
@@ -133,11 +145,10 @@
             rdef = rschema.role_rdef(eschema, targetschemas[0], role)
             if rdef.role_cardinality(role) in '1+':
                 if role == 'subject':
-                    set_operation(self._cw, '_cwisrel', (eid, rschema.type),
-                                  _CheckSRelationOp, list)
+                    op = _CheckSRelationOp.get_instance(self._cw)
                 else:
-                    set_operation(self._cw, '_cwiorel', (eid, rschema.type),
-                                  _CheckORelationOp, list)
+                    op = _CheckORelationOp.get_instance(self._cw)
+                op.add_data((eid, rschema.type))
 
     def before_delete_relation(self):
         rtype = self.rtype
@@ -150,26 +161,24 @@
             return
         card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
         if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
-            set_operation(self._cw, '_cwisrel', (eidfrom, rtype),
-                          _CheckSRelationOp, list)
+            _CheckSRelationOp.get_instance(self._cw).add_data((eidfrom, rtype))
         if card[1] in '1+' and not session.deleted_in_transaction(eidto):
-            set_operation(self._cw, '_cwiorel', (eidto, rtype),
-                          _CheckORelationOp, list)
+            _CheckORelationOp.get_instance(self._cw).add_data((eidto, rtype))
 
 
-class _CheckConstraintsOp(hook.LateOperation):
+class _CheckConstraintsOp(hook.DataOperationMixIn, hook.LateOperation):
     """ check a new relation satisfy its constraints """
-
+    containercls = list
     def precommit_event(self):
         session = self.session
-        for values in session.transaction_data.pop('check_constraints_op'):
+        for values in self.get_data():
             eidfrom, rtype, eidto, constraints = values
             # first check related entities have not been deleted in the same
             # transaction
             if session.deleted_in_transaction(eidfrom):
-                return
+                continue
             if session.deleted_in_transaction(eidto):
-                return
+                continue
             for constraint in constraints:
                 # XXX
                 # * lock RQLConstraint as well?
@@ -183,9 +192,6 @@
                     self.critical('can\'t check constraint %s, not supported',
                                   constraint)
 
-    def commit_event(self):
-        pass
-
 
 class CheckConstraintHook(IntegrityHook):
     """check the relation satisfy its constraints
@@ -201,9 +207,8 @@
         constraints = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
                                                 'constraints')
         if constraints:
-            hook.set_operation(self._cw, 'check_constraints_op',
-                               (self.eidfrom, self.rtype, self.eidto, tuple(constraints)),
-                               _CheckConstraintsOp, list)
+            _CheckConstraintsOp.get_instance(self._cw).add_data(
+                (self.eidfrom, self.rtype, self.eidto, constraints))
 
 
 class CheckAttributeConstraintHook(IntegrityHook):
@@ -217,14 +222,13 @@
 
     def __call__(self):
         eschema = self.entity.e_schema
-        for attr in self.entity.edited_attributes:
+        for attr in self.entity.cw_edited:
             if eschema.subjrels[attr].final:
                 constraints = [c for c in eschema.rdef(attr).constraints
                                if isinstance(c, (RQLUniqueConstraint, RQLConstraint))]
                 if constraints:
-                    hook.set_operation(self._cw, 'check_constraints_op',
-                                       (self.entity.eid, attr, None, tuple(constraints)),
-                                       _CheckConstraintsOp, list)
+                    _CheckConstraintsOp.get_instance(self._cw).add_data(
+                        (self.entity.eid, attr, None, constraints))
 
 
 class CheckUniqueHook(IntegrityHook):
@@ -234,9 +238,8 @@
     def __call__(self):
         entity = self.entity
         eschema = entity.e_schema
-        for attr in entity.edited_attributes:
+        for attr, val in entity.cw_edited.iteritems():
             if eschema.subjrels[attr].final and eschema.has_unique_values(attr):
-                val = entity[attr]
                 if val is None:
                     continue
                 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
@@ -255,18 +258,17 @@
     events = ('before_delete_entity', 'before_update_entity')
 
     def __call__(self):
-        if self.event == 'before_delete_entity' and self.entity.name == 'owners':
+        entity = self.entity
+        if self.event == 'before_delete_entity' and entity.name == 'owners':
             msg = self._cw._('can\'t be deleted')
-            raise ValidationError(self.entity.eid, {None: msg})
-        elif self.event == 'before_update_entity' and \
-                 'name' in self.entity.edited_attributes:
-            newname = self.entity.pop('name')
-            oldname = self.entity.name
+            raise ValidationError(entity.eid, {None: msg})
+        elif self.event == 'before_update_entity' \
+                 and 'name' in entity.cw_edited:
+            oldname, newname = entity.cw_edited.oldnewvalue('name')
             if oldname == 'owners' and newname != oldname:
                 qname = role_name('name', 'subject')
                 msg = self._cw._('can\'t be changed')
-                raise ValidationError(self.entity.eid, {qname: msg})
-            self.entity['name'] = newname
+                raise ValidationError(entity.eid, {qname: msg})
 
 
 class TidyHtmlFields(IntegrityHook):
@@ -277,15 +279,16 @@
     def __call__(self):
         entity = self.entity
         metaattrs = entity.e_schema.meta_attributes()
+        edited = entity.cw_edited
         for metaattr, (metadata, attr) in metaattrs.iteritems():
-            if metadata == 'format' and attr in entity.edited_attributes:
+            if metadata == 'format' and attr in edited:
                 try:
-                    value = entity[attr]
+                    value = edited[attr]
                 except KeyError:
                     continue # no text to tidy
                 if isinstance(value, unicode): # filter out None and Binary
                     if getattr(entity, str(metaattr)) == 'text/html':
-                        entity[attr] = soup2xhtml(value, self._cw.encoding)
+                        edited[attr] = soup2xhtml(value, self._cw.encoding)
 
 
 class StripCWUserLoginHook(IntegrityHook):
@@ -295,41 +298,51 @@
     events = ('before_add_entity', 'before_update_entity',)
 
     def __call__(self):
-        user = self.entity
-        if 'login' in user.edited_attributes and user.login:
-            user.login = user.login.strip()
+        login = self.entity.cw_edited.get('login')
+        if login:
+            self.entity.cw_edited['login'] = login.strip()
 
 
 # 'active' integrity hooks: you usually don't want to deactivate them, they are
 # not really integrity check, they maintain consistency on changes
 
-class _DelayedDeleteOp(hook.Operation):
+class _DelayedDeleteOp(hook.DataOperationMixIn, hook.Operation):
     """delete the object of composite relation except if the relation has
     actually been redirected to another composite
     """
-    key = base_rql = None
+    base_rql = None
 
     def precommit_event(self):
         session = self.session
         pendingeids = session.transaction_data.get('pendingeids', ())
         neweids = session.transaction_data.get('neweids', ())
-        # poping key is not optional: if further operation trigger new deletion
-        # of composite relation, we'll need a new operation
-        for eid, rtype in session.transaction_data.pop(self.key):
+        eids_by_etype_rtype = {}
+        for eid, rtype in self.get_data():
             # don't do anything if the entity is being created or deleted
             if not (eid in pendingeids or eid in neweids):
                 etype = session.describe(eid)[0]
-                session.execute(self.base_rql % (etype, rtype), {'x': eid})
+                key = (etype, rtype)
+                if key not in eids_by_etype_rtype:
+                    eids_by_etype_rtype[key] = [str(eid)]
+                else:
+                    eids_by_etype_rtype[key].append(str(eid))
+        for (etype, rtype), eids in eids_by_etype_rtype.iteritems():
+            # quite unexpectedly, not deleting too many entities at a time in
+            # this operation benefits to the exec speed (possibly on the RQL
+            # parsing side)
+            start = 0
+            incr = 500
+            while start < len(eids):
+                session.execute(self.base_rql % (etype, ','.join(eids[start:start+incr]), rtype))
+                start += incr
 
 class _DelayedDeleteSEntityOp(_DelayedDeleteOp):
     """delete orphan subject entity of a composite relation"""
-    key = '_cwiscomp'
-    base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT X %s Y'
+    base_rql = 'DELETE %s X WHERE X eid IN (%s), NOT X %s Y'
 
 class _DelayedDeleteOEntityOp(_DelayedDeleteOp):
     """check required object relation"""
-    key = '_cwiocomp'
-    base_rql = 'DELETE %s X WHERE X eid %%(x)s, NOT Y %s X'
+    base_rql = 'DELETE %s X WHERE X eid IN (%s), NOT Y %s X'
 
 
 class DeleteCompositeOrphanHook(hook.Hook):
@@ -349,8 +362,8 @@
         composite = self._cw.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
                                               'composite')
         if composite == 'subject':
-            set_operation(self._cw, '_cwiocomp', (self.eidto, self.rtype),
-                          _DelayedDeleteOEntityOp)
+            _DelayedDeleteOEntityOp.get_instance(self._cw).add_data(
+                (self.eidto, self.rtype))
         elif composite == 'object':
-            set_operation(self._cw, '_cwiscomp', (self.eidfrom, self.rtype),
-                          _DelayedDeleteSEntityOp)
+            _DelayedDeleteSEntityOp.get_instance(self._cw).add_data(
+                (self.eidfrom, self.rtype))