hooks/syncschema.py
changeset 5891 99024ad59223
parent 5881 57387070f612
child 6002 0ce7052ce30b
--- a/hooks/syncschema.py	Mon Jul 05 18:00:33 2010 +0200
+++ b/hooks/syncschema.py	Mon Jul 05 18:25:19 2010 +0200
@@ -81,6 +81,11 @@
 
 def add_inline_relation_column(session, etype, rtype):
     """add necessary column and index for an inlined relation"""
+    attrkey = '%s.%s' % (etype, rtype)
+    createdattrs = session.transaction_data.setdefault('createdattrs', set())
+    if attrkey in createdattrs:
+        return
+    createdattrs.add(attrkey)
     table = SQL_PREFIX + etype
     column = SQL_PREFIX + rtype
     try:
@@ -97,8 +102,6 @@
     # is done by the dbhelper)
     session.pool.source('system').create_index(session, table, column)
     session.info('added index on %s(%s)', table, column)
-    session.transaction_data.setdefault('createdattrs', []).append(
-        '%s.%s' % (etype, rtype))
 
 
 def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
@@ -116,6 +119,14 @@
         raise ValidationError(entity.eid, errors)
 
 
+class SyncSchemaHook(hook.Hook):
+    """abstract class for schema synchronization hooks (in the `syncschema`
+    category)
+    """
+    __abstract__ = True
+    category = 'syncschema'
+
+
 # operations for low-level database alteration  ################################
 
 class DropTable(hook.Operation):
@@ -130,6 +141,8 @@
         self.session.system_sql('DROP TABLE %s' % self.table)
         self.info('dropped table %s', self.table)
 
+    # XXX revertprecommit_event
+
 
 class DropRelationTable(DropTable):
     def __init__(self, session, rtype):
@@ -157,6 +170,8 @@
             self.error('dropping column not supported by the backend, handle '
                        'it yourself (%s.%s)', table, column)
 
+    # XXX revertprecommit_event
+
 
 # base operations for in-memory schema synchronization  ########################
 
@@ -176,7 +191,7 @@
             if not eschema.final:
                 clear_cache(eschema, 'ordered_relations')
 
-    def commit_event(self):
+    def postcommit_event(self):
         rebuildinfered = self.session.data.get('rebuild-infered', True)
         repo = self.session.repo
         # commit event should not raise error, while set_schema has chances to
@@ -196,60 +211,88 @@
 
 class MemSchemaOperation(hook.Operation):
     """base class for schema operations"""
-    def __init__(self, session, kobj=None, **kwargs):
-        self.kobj = kobj
-        # once Operation.__init__ has been called, event may be triggered, so
-        # do this last !
+    def __init__(self, session, **kwargs):
         hook.Operation.__init__(self, session, **kwargs)
         # every schema operation is triggering a schema update
         MemSchemaNotifyChanges(session)
 
-    def prepare_constraints(self, rdef):
-        # if constraints is already a list, reuse it (we're updating multiple
-        # constraints of the same rdef in the same transactions)
-        if not isinstance(rdef.constraints, list):
-            rdef.constraints = list(rdef.constraints)
-        self.constraints = rdef.constraints
-
-
-class MemSchemaEarlyOperation(MemSchemaOperation):
-    def insert_index(self):
-        """schema operation which are inserted at the begining of the queue
-        (typically to add/remove entity or relation types)
-        """
-        i = -1
-        for i, op in enumerate(self.session.pending_operations):
-            if not isinstance(op, MemSchemaEarlyOperation):
-                return i
-        return i + 1
-
 
 # operations for high-level source database alteration  ########################
 
-class SourceDbCWETypeRename(hook.Operation):
+class CWETypeAddOp(MemSchemaOperation):
+    """after adding a CWEType entity:
+    * add it to the instance's schema
+    * create the necessary table
+    * set creation_date and modification_date by creating the necessary
+      CWAttribute entities
+    * add owned_by relation by creating the necessary CWRelation entity
+    """
+
+    def precommit_event(self):
+        session = self.session
+        entity = self.entity
+        schema = session.vreg.schema
+        etype = ybo.EntityType(eid=entity.eid, name=entity.name,
+                               description=entity.description)
+        eschema = schema.add_entity_type(etype)
+        # create the necessary table
+        tablesql = y2sql.eschema2sql(session.pool.source('system').dbhelper,
+                                     eschema, prefix=SQL_PREFIX)
+        for sql in tablesql.split(';'):
+            if sql.strip():
+                session.system_sql(sql)
+        # add meta relations
+        gmap = group_mapping(session)
+        cmap = ss.cstrtype_mapping(session)
+        for rtype in (META_RTYPES - VIRTUAL_RTYPES):
+            rschema = schema[rtype]
+            sampletype = rschema.subjects()[0]
+            desttype = rschema.objects()[0]
+            rdef = copy(rschema.rdef(sampletype, desttype))
+            rdef.subject = mock_object(eid=entity.eid)
+            mock = mock_object(eid=None)
+            ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
+
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.session.vreg.schema.del_entity_type(self.entity.name)
+        # revert changes on database
+        self.session.system_sql('DROP TABLE %s%s' % (SQL_PREFIX, self.entity.name))
+
+
+class CWETypeRenameOp(MemSchemaOperation):
     """this operation updates physical storage accordingly"""
     oldname = newname = None # make pylint happy
 
-    def precommit_event(self):
+    def rename(self, oldname, newname):
+        self.session.vreg.schema.rename_entity_type(oldname, newname)
         # we need sql to operate physical changes on the system database
         sqlexec = self.session.system_sql
-        sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname,
-                                                     SQL_PREFIX, self.newname))
-        self.info('renamed table %s to %s', self.oldname, self.newname)
+        sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, oldname,
+                                                     SQL_PREFIX, newname))
+        self.info('renamed table %s to %s', oldname, newname)
         sqlexec('UPDATE entities SET type=%s WHERE type=%s',
-                (self.newname, self.oldname))
+                (newname, oldname))
         sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
-                (self.newname, self.oldname))
+                (newname, oldname))
+        # XXX transaction records
+
+    def precommit_event(self):
+        self.rename(self.oldname, self.newname)
+
+    def revertprecommit_event(self):
+        self.rename(self.newname, self.oldname)
 
 
-class SourceDbCWRTypeUpdate(hook.Operation):
+class CWRTypeUpdateOp(MemSchemaOperation):
     """actually update some properties of a relation definition"""
     rschema = entity = values = None # make pylint happy
+    oldvalus = None
 
     def precommit_event(self):
         rschema = self.rschema
         if rschema.final:
-            return
+            return # watched changes to final relation type are unexpected
         session = self.session
         if 'fulltext_container' in self.values:
             for subjtype, objtype in rschema.rdefs:
@@ -257,10 +300,14 @@
                                    UpdateFTIndexOp)
                 hook.set_operation(session, 'fti_update_etypes', objtype,
                                    UpdateFTIndexOp)
+        # update the in-memory schema first
+        self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
+        self.rschema.__dict__.update(self.values)
+        # then make necessary changes to the system source database
         if not 'inlined' in self.values:
             return # nothing to do
         inlined = self.values['inlined']
-        # check in-lining is necessary / possible
+        # check in-lining is possible when inlined
         if inlined:
             self.entity.check_inlined_allowed()
         # inlined changed, make necessary physical changes!
@@ -296,7 +343,7 @@
                 except Exception, ex:
                     # the column probably already exists. this occurs when the
                     # entity's type has just been added or if the column has not
-                    # been previously dropped
+                    # been previously dropped (eg sqlite)
                     self.error('error while altering table %s: %s', etype, ex)
                 # copy existant data.
                 # XXX don't use, it's not supported by sqlite (at least at when i tried it)
@@ -316,8 +363,13 @@
                 # drop existant table
                 DropRelationTable(session, rtype)
 
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.rschema.__dict__.update(self.oldvalues)
+        # XXX revert changes on database
 
-class SourceDbCWAttributeAdd(hook.Operation):
+
+class CWAttributeAddOp(MemSchemaOperation):
     """an attribute relation (CWAttribute) has been added:
     * add the necessary column
     * set default on this column if any and possible
@@ -331,24 +383,18 @@
     def init_rdef(self, **kwargs):
         entity = self.entity
         fromentity = entity.stype
+        rdefdef = self.rdefdef = ybo.RelationDefinition(
+            str(fromentity.name), entity.rtype.name, str(entity.otype.name),
+            description=entity.description, cardinality=entity.cardinality,
+            constraints=get_constraints(self.session, entity),
+            order=entity.ordernum, eid=entity.eid, **kwargs)
+        self.session.vreg.schema.add_relation_def(rdefdef)
         self.session.execute('SET X ordernum Y+1 '
                              'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
                              'X ordernum >= %(order)s, NOT X eid %(x)s',
                              {'x': entity.eid, 'se': fromentity.eid,
                               'order': entity.ordernum or 0})
-        subj = str(fromentity.name)
-        rtype = entity.rtype.name
-        obj = str(entity.otype.name)
-        constraints = get_constraints(self.session, entity)
-        rdef = ybo.RelationDefinition(subj, rtype, obj,
-                                      description=entity.description,
-                                      cardinality=entity.cardinality,
-                                      constraints=constraints,
-                                      order=entity.ordernum,
-                                      eid=entity.eid,
-                                      **kwargs)
-        MemSchemaRDefAdd(self.session, rdef)
-        return rdef
+        return rdefdef
 
     def precommit_event(self):
         session = self.session
@@ -362,22 +408,24 @@
                  'indexed': entity.indexed,
                  'fulltextindexed': entity.fulltextindexed,
                  'internationalizable': entity.internationalizable}
-        rdef = self.init_rdef(**props)
-        sysource = session.pool.source('system')
+        # update the in-memory schema first
+        rdefdef = self.init_rdef(**props)
+        # then make necessary changes to the system source database
+        syssource = session.pool.source('system')
         attrtype = y2sql.type_from_constraints(
-            sysource.dbhelper, rdef.object, rdef.constraints)
+            syssource.dbhelper, rdefdef.object, rdefdef.constraints)
         # XXX should be moved somehow into lgdb: sqlite doesn't support to
         # add a new column with UNIQUE, it should be added after the ALTER TABLE
         # using ADD INDEX
-        if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
+        if syssource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
             extra_unique_index = True
             attrtype = attrtype.replace(' UNIQUE', '')
         else:
             extra_unique_index = False
         # added some str() wrapping query since some backend (eg psycopg) don't
         # allow unicode queries
-        table = SQL_PREFIX + rdef.subject
-        column = SQL_PREFIX + rdef.name
+        table = SQL_PREFIX + rdefdef.subject
+        column = SQL_PREFIX + rdefdef.name
         try:
             session.system_sql(str('ALTER TABLE %s ADD %s %s'
                                    % (table, column, attrtype)),
@@ -390,7 +438,7 @@
             self.error('error while altering table %s: %s', table, ex)
         if extra_unique_index or entity.indexed:
             try:
-                sysource.create_index(session, table, column,
+                syssource.create_index(session, table, column,
                                       unique=extra_unique_index)
             except Exception, ex:
                 self.error('error while creating index for %s.%s: %s',
@@ -398,28 +446,28 @@
         # final relations are not infered, propagate
         schema = session.vreg.schema
         try:
-            eschema = schema.eschema(rdef.subject)
+            eschema = schema.eschema(rdefdef.subject)
         except KeyError:
             return # entity type currently being added
         # propagate attribute to children classes
-        rschema = schema.rschema(rdef.name)
+        rschema = schema.rschema(rdefdef.name)
         # if relation type has been inserted in the same transaction, its final
         # attribute is still set to False, so we've to ensure it's False
         rschema.final = True
         # XXX 'infered': True/False, not clear actually
-        props.update({'constraints': rdef.constraints,
-                      'description': rdef.description,
-                      'cardinality': rdef.cardinality,
-                      'constraints': rdef.constraints,
-                      'permissions': rdef.get_permissions(),
-                      'order': rdef.order,
+        props.update({'constraints': rdefdef.constraints,
+                      'description': rdefdef.description,
+                      'cardinality': rdefdef.cardinality,
+                      'constraints': rdefdef.constraints,
+                      'permissions': rdefdef.get_permissions(),
+                      'order': rdefdef.order,
                       'infered': False, 'eid': None
                       })
         cstrtypemap = ss.cstrtype_mapping(session)
         groupmap = group_mapping(session)
-        object = schema.eschema(rdef.object)
+        object = schema.eschema(rdefdef.object)
         for specialization in eschema.specialized_by(False):
-            if (specialization, rdef.object) in rschema.rdefs:
+            if (specialization, rdefdef.object) in rschema.rdefs:
                 continue
             sperdef = RelationDefinitionSchema(specialization, rschema,
                                                object, props)
@@ -431,14 +479,21 @@
             session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
                                {'default': default})
 
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.session.vreg.schema.del_relation_def(
+            self.rdefdef.subject, self.rdefdef.name, self.rdefdef.object)
+        # XXX revert changes on database
 
-class SourceDbCWRelationAdd(SourceDbCWAttributeAdd):
+
+class CWRelationAddOp(CWAttributeAddOp):
     """an actual relation has been added:
-    * if this is an inlined relation, add the necessary column
-      else if it's the first instance of this relation type, add the
-      necessary table and set default permissions
-    * register an operation to add the relation definition to the
-      instance's schema on commit
+
+    * add the relation definition to the instance's schema
+
+    * if this is an inlined relation, add the necessary column else if it's the
+      first instance of this relation type, add the necessary table and set
+      default permissions
 
     constraints are handled by specific hooks
     """
@@ -447,280 +502,229 @@
     def precommit_event(self):
         session = self.session
         entity = self.entity
-        rdef = self.init_rdef(composite=entity.composite)
+        # update the in-memory schema first
+        rdefdef = self.init_rdef(composite=entity.composite)
+        # then make necessary changes to the system source database
         schema = session.vreg.schema
-        rtype = rdef.name
+        rtype = rdefdef.name
         rschema = schema.rschema(rtype)
         # this have to be done before permissions setting
         if rschema.inlined:
             # need to add a column if the relation is inlined and if this is the
             # first occurence of "Subject relation Something" whatever Something
-            # and if it has not been added during other event of the same
-            # transaction
-            key = '%s.%s' % (rdef.subject, rtype)
-            try:
-                alreadythere = bool(rschema.objects(rdef.subject))
-            except KeyError:
-                alreadythere = False
-            if not (alreadythere or
-                    key in session.transaction_data.get('createdattrs', ())):
-                add_inline_relation_column(session, rdef.subject, rtype)
+            if len(rschema.objects(rdefdef.subject)) == 1:
+                add_inline_relation_column(session, rdefdef.subject, rtype)
         else:
             # need to create the relation if no relation definition in the
             # schema and if it has not been added during other event of the same
             # transaction
-            if not (rschema.subjects() or
+            if not (len(rschema.rdefs) > 1 or
                     rtype in session.transaction_data.get('createdtables', ())):
-                try:
-                    rschema = schema.rschema(rtype)
-                    tablesql = y2sql.rschema2sql(rschema)
-                except KeyError:
-                    # fake we add it to the schema now to get a correctly
-                    # initialized schema but remove it before doing anything
-                    # more dangerous...
-                    rschema = schema.add_relation_type(rdef)
-                    tablesql = y2sql.rschema2sql(rschema)
-                    schema.del_relation_type(rtype)
+                rschema = schema.rschema(rtype)
                 # create the necessary table
-                for sql in tablesql.split(';'):
+                for sql in y2sql.rschema2sql(rschema).split(';'):
                     if sql.strip():
                         session.system_sql(sql)
                 session.transaction_data.setdefault('createdtables', []).append(
                     rtype)
 
+    # XXX revertprecommit_event
 
-class SourceDbRDefUpdate(hook.Operation):
+
+class RDefDelOp(MemSchemaOperation):
+    """an actual relation has been removed"""
+    rdef = None # make pylint happy
+
+    def precommit_event(self):
+        session = self.session
+        rdef = self.rdef
+        rschema = rdef.rtype
+        # make necessary changes to the system source database first
+        rdeftype = rschema.final and 'CWAttribute' or 'CWRelation'
+        execute = session.execute
+        rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
+                       'R eid %%(x)s' % rdeftype, {'x': rschema.eid})
+        lastrel = rset[0][0] == 0
+        # we have to update physical schema systematically for final and inlined
+        # relations, but only if it's the last instance for this relation type
+        # for other relations
+        if (rschema.final or rschema.inlined):
+            rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
+                           'R eid %%(r)s, X from_entity E, E eid %%(e)s'
+                           % rdeftype,
+                           {'r': rschema.eid, 'e': rdef.subject.eid})
+            if rset[0][0] == 0 and not session.deleted_in_transaction(rdef.subject.eid):
+                ptypes = session.transaction_data.setdefault('pendingrtypes', set())
+                ptypes.add(rschema.type)
+                DropColumn(session, table=SQL_PREFIX + str(rdef.subject),
+                           column=SQL_PREFIX + str(rschema))
+        elif lastrel:
+            DropRelationTable(session, str(rschema))
+        # then update the in-memory schema
+        rschema.del_relation_def(rdef.subject, rdef.object)
+        # if this is the last relation definition of this type, drop associated
+        # relation type
+        if lastrel and not session.deleted_in_transaction(rschema.eid):
+            execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rschema.eid})
+
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        #
+        # Note: add_relation_def takes a RelationDefinition, not a
+        # RelationDefinitionSchema, needs to fake it
+        self.rdef.name = str(self.rdef.rtype)
+        self.session.vreg.schema.add_relation_def(self.rdef)
+
+
+
+class RDefUpdateOp(MemSchemaOperation):
     """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
+    rschema = rdefkey = values = None # make pylint happy
+    oldvalues = None
+    indexed_changed = null_allowed_changed = False
 
     def precommit_event(self):
         session = self.session
-        etype = self.kobj[0]
-        table = SQL_PREFIX + etype
-        column = SQL_PREFIX + self.rschema.type
+        rdef = self.rdef = self.rschema.rdefs[self.rdefkey]
+        # update the in-memory schema first
+        self.oldvalues = dict( (attr, getattr(rdef, attr)) for attr in self.values)
+        rdef.update(self.values)
+        # then make necessary changes to the system source database
+        syssource = session.pool.source('system')
         if 'indexed' in self.values:
-            sysource = session.pool.source('system')
-            if self.values['indexed']:
-                sysource.create_index(session, table, column)
-            else:
-                sysource.drop_index(session, table, column)
-        if 'cardinality' in self.values and self.rschema.final:
-            syssource = session.pool.source('system')
-            if not syssource.dbhelper.alter_column_support:
-                # not supported (and NOT NULL not set by yams in that case, so
-                # no worry) XXX (syt) then should we set NOT NULL below ??
-                return
-            atype = self.rschema.objects(etype)[0]
-            constraints = self.rschema.rdef(etype, atype).constraints
-            coltype = y2sql.type_from_constraints(syssource.dbhelper, atype, constraints,
-                                                  creating=False)
-            # XXX check self.values['cardinality'][0] actually changed?
-            syssource.set_null_allowed(self.session, table, column, coltype,
-                                       self.values['cardinality'][0] != '1')
+            syssource.update_rdef_indexed(session, rdef)
+            self.indexed_changed = True
+        if 'cardinality' in self.values and (rdef.rtype.final or
+                                             rdef.rtype.inlined) \
+              and self.values['cardinality'][0] != self.oldvalues['cardinality'][0]:
+            syssource.update_rdef_null_allowed(self.session, rdef)
+            self.null_allowed_changed = True
         if 'fulltextindexed' in self.values:
-            hook.set_operation(session, 'fti_update_etypes', etype,
+            hook.set_operation(session, 'fti_update_etypes', rdef.subject,
                                UpdateFTIndexOp)
 
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        self.rdef.update(self.oldvalues)
+        # revert changes on database
+        syssource = self.session.pool.source('system')
+        if self.indexed_changed:
+            syssource.update_rdef_indexed(self.session, self.rdef)
+        if self.null_allowed_changed:
+            syssource.update_rdef_null_allowed(self.session, self.rdef)
 
-class SourceDbCWConstraintAdd(hook.Operation):
+
+def _set_modifiable_constraints(rdef):
+    # for proper in-place modification of in-memory schema: if rdef.constraints
+    # is already a list, reuse it (we're updating multiple constraints of the
+    # same rdef in the same transactions)
+    if not isinstance(rdef.constraints, list):
+        rdef.constraints = list(rdef.constraints)
+
+
+class CWConstraintDelOp(MemSchemaOperation):
+    """actually remove a constraint of a relation definition"""
+    rdef = oldcstr = newcstr = None # make pylint happy
+    size_cstr_changed = unique_changed = False
+
+    def precommit_event(self):
+        session = self.session
+        rdef = self.rdef
+        # in-place modification of in-memory schema first
+        _set_modifiable_constraints(rdef)
+        rdef.constraints.remove(self.oldcstr)
+        # then update database: alter the physical schema on size/unique
+        # constraint changes
+        syssource = session.pool.source('system')
+        cstrtype = self.oldcstr.type()
+        if cstrtype == 'SizeConstraint':
+            syssource.update_rdef_column(session, rdef)
+            self.size_cstr_changed = True
+        elif cstrtype == 'UniqueConstraint':
+            syssource.update_rdef_unique(session, rdef)
+            self.unique_changed = True
+
+    def revertprecommit_event(self):
+        # revert changes on in memory schema
+        if self.newcstr is not None:
+            self.rdef.constraints.remove(self.newcstr)
+        if self.oldcstr is not None:
+            self.rdef.constraints.append(self.oldcstr)
+        # revert changes on database
+        syssource = self.session.pool.source('system')
+        if self.size_cstr_changed:
+            syssource.update_rdef_column(self.session, self.rdef)
+        if self.unique_changed:
+            syssource.update_rdef_unique(self.session, self.rdef)
+
+
+class CWConstraintAddOp(CWConstraintDelOp):
     """actually update constraint of a relation definition"""
     entity = None # make pylint happy
-    cancelled = False
 
     def precommit_event(self):
-        rdef = self.entity.reverse_constrained_by[0]
         session = self.session
+        rdefentity = self.entity.reverse_constrained_by[0]
         # when the relation is added in the same transaction, the constraint
         # object is created by the operation adding the attribute or relation,
         # so there is nothing to do here
-        if session.added_in_transaction(rdef.eid):
+        if session.added_in_transaction(rdefentity.eid):
             return
-        rdefschema = session.vreg.schema.schema_by_eid(rdef.eid)
-        subjtype, rtype, objtype = rdefschema.as_triple()
+        rdef = self.rdef = session.vreg.schema.schema_by_eid(rdefentity.eid)
         cstrtype = self.entity.type
-        oldcstr = rtype.rdef(subjtype, objtype).constraint_by_type(cstrtype)
-        newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
-        table = SQL_PREFIX + str(subjtype)
-        column = SQL_PREFIX + str(rtype)
-        # alter the physical schema on size constraint changes
-        if newcstr.type() == 'SizeConstraint' and (
-            oldcstr is None or oldcstr.max != newcstr.max):
-            syssource = self.session.pool.source('system')
-            card = rtype.rdef(subjtype, objtype).cardinality
-            coltype = y2sql.type_from_constraints(syssource.dbhelper, objtype,
-                                                  [newcstr], creating=False)
-            try:
-                syssource.change_col_type(session, table, column, coltype, card[0] != '1')
-                self.info('altered column %s of table %s: now %s',
-                          column, table, coltype)
-            except Exception, ex:
-                # not supported by sqlite for instance
-                self.error('error while altering table %s: %s', table, ex)
+        oldcstr = self.oldcstr = rdef.constraint_by_type(cstrtype)
+        newcstr = self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
+        # in-place modification of in-memory schema first
+        _set_modifiable_constraints(rdef)
+        newcstr.eid = self.entity.eid
+        if oldcstr is not None:
+            rdef.constraints.remove(oldcstr)
+        rdef.constraints.append(newcstr)
+        # then update database: alter the physical schema on size/unique
+        # constraint changes
+        syssource = session.pool.source('system')
+        if cstrtype == 'SizeConstraint' and (oldcstr is None or
+                                             oldcstr.max != newcstr.max):
+            syssource.update_rdef_column(session, rdef)
+            self.size_cstr_changed = True
         elif cstrtype == 'UniqueConstraint' and oldcstr is None:
-            session.pool.source('system').create_index(
-                self.session, table, column, unique=True)
-
-
-class SourceDbCWConstraintDel(hook.Operation):
-    """actually remove a constraint of a relation definition"""
-    rtype = subjtype = None # make pylint happy
-
-    def precommit_event(self):
-        cstrtype = self.cstr.type()
-        table = SQL_PREFIX + str(self.rdef.subject)
-        column = SQL_PREFIX + str(self.rdef.rtype)
-        # alter the physical schema on size/unique constraint changes
-        if cstrtype == 'SizeConstraint':
-            syssource = self.session.pool.source('system')
-            coltype = y2sql.type_from_constraints(syssource.dbhelper,
-                                                  self.rdef.object, [],
-                                                  creating=False)
-            try:
-                syssource.change_col_type(session, table, column, coltype,
-                                          self.rdef.cardinality[0] != '1')
-                self.info('altered column %s of table %s: now %s',
-                          column, table, coltype)
-            except Exception, ex:
-                # not supported by sqlite for instance
-                self.error('error while altering table %s: %s', table, ex)
-        elif cstrtype == 'UniqueConstraint':
-            self.session.pool.source('system').drop_index(
-                self.session, table, column, unique=True)
+            syssource.update_rdef_unique(session, rdef)
+            self.unique_changed = True
 
 
 # operations for in-memory schema synchronization  #############################
 
-class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
-    """actually add the entity type to the instance's schema"""
-    eid = None # make pylint happy
-    def commit_event(self):
-        self.session.vreg.schema.add_entity_type(self.kobj)
-
-
-class MemSchemaCWETypeRename(MemSchemaOperation):
-    """this operation updates physical storage accordingly"""
-    oldname = newname = None # make pylint happy
-
-    def commit_event(self):
-        self.session.vreg.schema.rename_entity_type(self.oldname, self.newname)
-
-
 class MemSchemaCWETypeDel(MemSchemaOperation):
     """actually remove the entity type from the instance's schema"""
-    def commit_event(self):
-        try:
-            # del_entity_type also removes entity's relations
-            self.session.vreg.schema.del_entity_type(self.kobj)
-        except KeyError:
-            # s/o entity type have already been deleted
-            pass
+    def postcommit_event(self):
+        # del_entity_type also removes entity's relations
+        self.session.vreg.schema.del_entity_type(self.etype)
 
 
-class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
+class MemSchemaCWRTypeAdd(MemSchemaOperation):
     """actually add the relation type to the instance's schema"""
-    eid = None # make pylint happy
-    def commit_event(self):
-        self.session.vreg.schema.add_relation_type(self.kobj)
-
+    def precommit_event(self):
+        self.session.vreg.schema.add_relation_type(self.rtypedef)
 
-class MemSchemaCWRTypeUpdate(MemSchemaOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def commit_event(self):
-        # structure should be clean, not need to remove entity's relations
-        # at this point
-        self.rschema.__dict__.update(self.values)
+    def revertprecommit_event(self):
+        self.session.vreg.schema.del_relation_type(self.rtypedef.name)
 
 
 class MemSchemaCWRTypeDel(MemSchemaOperation):
     """actually remove the relation type from the instance's schema"""
-    def commit_event(self):
+    def postcommit_event(self):
         try:
-            self.session.vreg.schema.del_relation_type(self.kobj)
+            self.session.vreg.schema.del_relation_type(self.rtype)
         except KeyError:
             # s/o entity type have already been deleted
             pass
 
 
-class MemSchemaRDefAdd(MemSchemaEarlyOperation):
-    """actually add the attribute relation definition to the instance's
-    schema
-    """
-    def commit_event(self):
-        self.session.vreg.schema.add_relation_def(self.kobj)
-
-
-class MemSchemaRDefUpdate(MemSchemaOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def commit_event(self):
-        # structure should be clean, not need to remove entity's relations
-        # at this point
-        self.rschema.rdefs[self.kobj].update(self.values)
-
-
-class MemSchemaRDefDel(MemSchemaOperation):
-    """actually remove the relation definition from the instance's schema"""
-    def commit_event(self):
-        subjtype, rtype, objtype = self.kobj
-        try:
-            self.session.vreg.schema.del_relation_def(subjtype, rtype, objtype)
-        except KeyError:
-            # relation type may have been already deleted
-            pass
-
-
-class MemSchemaCWConstraintAdd(MemSchemaOperation):
-    """actually update constraint of a relation definition
-
-    has to be called before SourceDbCWConstraintAdd
-    """
-    cancelled = False
-
-    def precommit_event(self):
-        rdef = self.entity.reverse_constrained_by[0]
-        # when the relation is added in the same transaction, the constraint
-        # object is created by the operation adding the attribute or relation,
-        # so there is nothing to do here
-        if self.session.added_in_transaction(rdef.eid):
-            self.cancelled = True
-            return
-        rdef = self.session.vreg.schema.schema_by_eid(rdef.eid)
-        self.prepare_constraints(rdef)
-        cstrtype = self.entity.type
-        self.cstr = rdef.constraint_by_type(cstrtype)
-        self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
-        self.newcstr.eid = self.entity.eid
-
-    def commit_event(self):
-        if self.cancelled:
-            return
-        # in-place modification
-        if not self.cstr is None:
-            self.constraints.remove(self.cstr)
-        self.constraints.append(self.newcstr)
-
-
-class MemSchemaCWConstraintDel(MemSchemaOperation):
-    """actually remove a constraint of a relation definition
-
-    has to be called before SourceDbCWConstraintDel
-    """
-    rtype = subjtype = objtype = None # make pylint happy
-    def precommit_event(self):
-        self.prepare_constraints(self.rdef)
-
-    def commit_event(self):
-        self.constraints.remove(self.cstr)
-
-
 class MemSchemaPermissionAdd(MemSchemaOperation):
     """synchronize schema when a *_permission relation has been added on a group
     """
 
-    def commit_event(self):
+    def precommit_event(self):
         """the observed connections pool has been commited"""
         try:
             erschema = self.session.vreg.schema.schema_by_eid(self.eid)
@@ -741,13 +745,15 @@
             perms.append(perm)
             erschema.set_action_permissions(self.action, perms)
 
+    # XXX revertprecommit_event
+
 
 class MemSchemaPermissionDel(MemSchemaPermissionAdd):
     """synchronize schema when a *_permission relation has been deleted from a
     group
     """
 
-    def commit_event(self):
+    def precommit_event(self):
         """the observed connections pool has been commited"""
         try:
             erschema = self.session.vreg.schema.schema_by_eid(self.eid)
@@ -772,19 +778,23 @@
             self.error('can\'t remove permission %s for %s on %s',
                        perm, self.action, erschema)
 
+    # XXX revertprecommit_event
+
 
 class MemSchemaSpecializesAdd(MemSchemaOperation):
 
-    def commit_event(self):
+    def precommit_event(self):
         eschema = self.session.vreg.schema.schema_by_eid(self.etypeeid)
         parenteschema = self.session.vreg.schema.schema_by_eid(self.parentetypeeid)
         eschema._specialized_type = parenteschema.type
         parenteschema._specialized_by.append(eschema.type)
 
+    # XXX revertprecommit_event
+
 
 class MemSchemaSpecializesDel(MemSchemaOperation):
 
-    def commit_event(self):
+    def precommit_event(self):
         try:
             eschema = self.session.vreg.schema.schema_by_eid(self.etypeeid)
             parenteschema = self.session.vreg.schema.schema_by_eid(self.parentetypeeid)
@@ -794,10 +804,7 @@
         eschema._specialized_type = None
         parenteschema._specialized_by.remove(eschema.type)
 
-
-class SyncSchemaHook(hook.Hook):
-    __abstract__ = True
-    category = 'syncschema'
+    # XXX revertprecommit_event
 
 
 # CWEType hooks ################################################################
@@ -820,7 +827,7 @@
         # delete every entities of this type
         if not name in ETYPE_NAME_MAP:
             self._cw.execute('DELETE %s X' % name)
-            MemSchemaCWETypeDel(self._cw, name)
+            MemSchemaCWETypeDel(self._cw, etype=name)
         DropTable(self._cw, table=SQL_PREFIX + name)
 
 
@@ -849,42 +856,7 @@
         entity = self.entity
         if entity.get('final'):
             return
-        schema = self._cw.vreg.schema
-        name = entity['name']
-        etype = ybo.EntityType(name=name, description=entity.get('description'),
-                               meta=entity.get('meta')) # don't care about final
-        # fake we add it to the schema now to get a correctly initialized schema
-        # but remove it before doing anything more dangerous...
-        schema = self._cw.vreg.schema
-        eschema = schema.add_entity_type(etype)
-        # generate table sql and rql to add metadata
-        tablesql = y2sql.eschema2sql(self._cw.pool.source('system').dbhelper,
-                                     eschema, prefix=SQL_PREFIX)
-        rdefrqls = []
-        gmap = group_mapping(self._cw)
-        cmap = ss.cstrtype_mapping(self._cw)
-        for rtype in (META_RTYPES - VIRTUAL_RTYPES):
-            rschema = schema[rtype]
-            sampletype = rschema.subjects()[0]
-            desttype = rschema.objects()[0]
-            rdef = copy(rschema.rdef(sampletype, desttype))
-            rdef.subject = mock_object(eid=entity.eid)
-            mock = mock_object(eid=None)
-            rdefrqls.append( (mock, tuple(ss.rdef2rql(rdef, cmap, gmap))) )
-        # now remove it !
-        schema.del_entity_type(name)
-        # create the necessary table
-        for sql in tablesql.split(';'):
-            if sql.strip():
-                self._cw.system_sql(sql)
-        # register operation to modify the schema on commit
-        # this have to be done before adding other relations definitions
-        # or permission settings
-        etype.eid = entity.eid
-        MemSchemaCWETypeAdd(self._cw, etype)
-        # add meta relations
-        for rdef, relrqls in rdefrqls:
-            ss.execschemarql(self._cw.execute, rdef, relrqls)
+        CWETypeAddOp(self._cw, entity=entity)
 
 
 class BeforeUpdateCWETypeHook(DelCWETypeHook):
@@ -897,12 +869,9 @@
         check_valid_changes(self._cw, entity, ro_attrs=('final',))
         # don't use getattr(entity, attr), we would get the modified value if any
         if 'name' in entity.edited_attributes:
-            newname = entity.pop('name')
-            oldname = entity.name
+            oldname, newname = hook.entity_oldnewvalue(entity, 'name')
             if newname.lower() != oldname.lower():
-                SourceDbCWETypeRename(self._cw, oldname=oldname, newname=newname)
-                MemSchemaCWETypeRename(self._cw, oldname=oldname, newname=newname)
-            entity['name'] = newname
+                CWETypeRenameOp(self._cw, oldname=oldname, newname=newname)
 
 
 # CWRType hooks ################################################################
@@ -926,7 +895,7 @@
                         {'x': self.entity.eid})
         self._cw.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
                         {'x': self.entity.eid})
-        MemSchemaCWRTypeDel(self._cw, name)
+        MemSchemaCWRTypeDel(self._cw, rtype=name)
 
 
 class AfterAddCWRTypeHook(DelCWRTypeHook):
@@ -941,13 +910,12 @@
 
     def __call__(self):
         entity = self.entity
-        rtype = ybo.RelationType(name=entity.name,
-                                 description=entity.get('description'),
-                                 meta=entity.get('meta', False),
-                                 inlined=entity.get('inlined', False),
-                                 symmetric=entity.get('symmetric', False),
-                                 eid=entity.eid)
-        MemSchemaCWRTypeAdd(self._cw, rtype)
+        rtypedef = ybo.RelationType(name=entity.name,
+                                    description=entity.description,
+                                    inlined=entity.get('inlined', False),
+                                    symmetric=entity.get('symmetric', False),
+                                    eid=entity.eid)
+        MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
 
 
 class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
@@ -966,9 +934,8 @@
                     newvalues[prop] = entity[prop]
         if newvalues:
             rschema = self._cw.vreg.schema.rschema(entity.name)
-            SourceDbCWRTypeUpdate(self._cw, rschema=rschema, entity=entity,
-                                  values=newvalues)
-            MemSchemaCWRTypeUpdate(self._cw, rschema=rschema, values=newvalues)
+            CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity,
+                            values=newvalues)
 
 
 class AfterDelRelationTypeHook(SyncSchemaHook):
@@ -992,7 +959,6 @@
             self.critical('cant get schema rdef associated to %s', self.eidfrom)
             return
         subjschema, rschema, objschema = rdef.as_triple()
-        pendings = session.transaction_data.get('pendingeids', ())
         pendingrdefs = session.transaction_data.setdefault('pendingrdefs', set())
         # first delete existing relation if necessary
         if rschema.final:
@@ -1001,31 +967,11 @@
         else:
             rdeftype = 'CWRelation'
             pendingrdefs.add((subjschema, rschema, objschema))
-            if not (subjschema.eid in pendings or objschema.eid in pendings):
+            if not (session.deleted_in_transaction(subjschema.eid) or
+                    session.deleted_in_transaction(objschema.eid)):
                 session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
                                 % (rschema, subjschema, objschema))
-        execute = session.execute
-        rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
-                       'R eid %%(x)s' % rdeftype, {'x': self.eidto})
-        lastrel = rset[0][0] == 0
-        # we have to update physical schema systematically for final and inlined
-        # relations, but only if it's the last instance for this relation type
-        # for other relations
-        if (rschema.final or rschema.inlined):
-            rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
-                           'R eid %%(x)s, X from_entity E, E name %%(name)s'
-                           % rdeftype, {'x': self.eidto, 'name': str(subjschema)})
-            if rset[0][0] == 0 and not subjschema.eid in pendings:
-                ptypes = session.transaction_data.setdefault('pendingrtypes', set())
-                ptypes.add(rschema.type)
-                DropColumn(session, table=SQL_PREFIX + subjschema.type,
-                           column=SQL_PREFIX + rschema.type)
-        elif lastrel:
-            DropRelationTable(session, rschema.type)
-        # if this is the last instance, drop associated relation type
-        if lastrel and not self.eidto in pendings:
-            execute('DELETE CWRType X WHERE X eid %(x)s', {'x': self.eidto})
-        MemSchemaRDefDel(session, (subjschema, rschema, objschema))
+        RDefDelOp(session, rdef=rdef)
 
 
 # CWAttribute / CWRelation hooks ###############################################
@@ -1036,7 +982,7 @@
     events = ('after_add_entity',)
 
     def __call__(self):
-        SourceDbCWAttributeAdd(self._cw, entity=self.entity)
+        CWAttributeAddOp(self._cw, entity=self.entity)
 
 
 class AfterAddCWRelationHook(AfterAddCWAttributeHook):
@@ -1044,7 +990,7 @@
     __select__ = SyncSchemaHook.__select__ & is_instance('CWRelation')
 
     def __call__(self):
-        SourceDbCWRelationAdd(self._cw, entity=self.entity)
+        CWRelationAddOp(self._cw, entity=self.entity)
 
 
 class AfterUpdateCWRDefHook(SyncSchemaHook):
@@ -1057,24 +1003,26 @@
         entity = self.entity
         if self._cw.deleted_in_transaction(entity.eid):
             return
-        desttype = entity.otype.name
+        subjtype = entity.stype.name
+        objtype = entity.otype.name
         rschema = self._cw.vreg.schema[entity.rtype.name]
+        # note: do not access schema rdef here, it may be added later by an
+        # operation
         newvalues = {}
-        for prop in RelationDefinitionSchema.rproperty_defs(desttype):
+        for prop in RelationDefinitionSchema.rproperty_defs(objtype):
             if prop == 'constraints':
                 continue
             if prop == 'order':
-                prop = 'ordernum'
-            if prop in entity.edited_attributes:
-                old, new = hook.entity_oldnewvalue(entity, prop)
+                attr = 'ordernum'
+            else:
+                attr = prop
+            if attr in entity.edited_attributes:
+                old, new = hook.entity_oldnewvalue(entity, attr)
                 if old != new:
-                    newvalues[prop] = entity[prop]
+                    newvalues[prop] = new
         if newvalues:
-            subjtype = entity.stype.name
-            MemSchemaRDefUpdate(self._cw, kobj=(subjtype, desttype),
-                                rschema=rschema, values=newvalues)
-            SourceDbRDefUpdate(self._cw, kobj=(subjtype, desttype),
-                               rschema=rschema, values=newvalues)
+            RDefUpdateOp(self._cw, rschema=rschema, rdefkey=(subjtype, objtype),
+                         values=newvalues)
 
 
 # constraints synchronization hooks ############################################
@@ -1085,8 +1033,7 @@
     events = ('after_add_entity', 'after_update_entity')
 
     def __call__(self):
-        MemSchemaCWConstraintAdd(self._cw, entity=self.entity)
-        SourceDbCWConstraintAdd(self._cw, entity=self.entity)
+        CWConstraintAddOp(self._cw, entity=self.entity)
 
 
 class AfterAddConstrainedByHook(SyncSchemaHook):
@@ -1114,8 +1061,7 @@
         except IndexError:
             self._cw.critical('constraint type no more accessible')
         else:
-            SourceDbCWConstraintDel(self._cw, rdef=rdef, cstr=cstr)
-            MemSchemaCWConstraintDel(self._cw, rdef=rdef, cstr=cstr)
+            CWConstraintDelOp(self._cw, rdef=rdef, oldcstr=cstr)
 
 
 # permissions synchronization hooks ############################################