[schema migration] import refactoring to fix #1109558 and enhances things on the way
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 05 Jul 2010 18:25:19 +0200
changeset 5891 99024ad59223
parent 5890 141b935a38fc
child 5892 214633a80961
[schema migration] import refactoring to fix #1109558 and enhances things on the way the main pb demonstrated by #1109558 was due to the fact that in-memory schema was updated in commit_event of operations. This is undesired in most cases, since we want the modification to be taken into account in the interval between the modification detection and the commit_event. I've fixed this by merging in-memory schema / database alteration operations for most important changes, doing in-memory schema changes as they are detected and implementing a revertcommit_event method to revert them if necessary (with exception for removal of stuff from the schema, where this is simply done in a postcommit_event methods). Also, I've benefited from this to support reverting of database alteration for some operations (more to be done there), and to move so system source alteration code to the native source code for a nicer design. There may be some more stuff in syncschema.py that would benefit from similar changes, but most important things are done (at least to close #1109558, w/ unittest_syncschema and unittest_migration green).
hooks/syncschema.py
hooks/test/unittest_syncschema.py
server/session.py
server/sources/native.py
--- 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 ############################################
--- a/hooks/test/unittest_syncschema.py	Mon Jul 05 18:00:33 2010 +0200
+++ b/hooks/test/unittest_syncschema.py	Mon Jul 05 18:25:19 2010 +0200
@@ -188,6 +188,9 @@
             self.failIf(self.index_exists('State', 'state_of'))
             rset = self.execute('Any X, Y WHERE X state_of Y')
             self.assertEquals(len(rset), 2) # user states
+        except:
+            import traceback
+            traceback.print_exc()
         finally:
             self.execute('SET X inlined TRUE WHERE X name "state_of"')
             self.failIf(self.schema['state_of'].inlined)
--- a/server/session.py	Mon Jul 05 18:00:33 2010 +0200
+++ b/server/session.py	Mon Jul 05 18:25:19 2010 +0200
@@ -747,7 +747,7 @@
                         self.pending_operations[:] = processed
                         self.debug('%s session %s done', trstate, self.id)
                     except:
-                        self.exception('error while %sing', trstate)
+                        self.critical('error while %sing', trstate, exc_info=True)
                         # if error on [pre]commit:
                         #
                         # * set .failed = True on the operation causing the failure
@@ -759,8 +759,12 @@
                         # instead of having to implements rollback, revertprecommit
                         # and revertcommit, that will be enough in mont case.
                         operation.failed = True
-                        for operation in processed:
-                            operation.handle_event('revert%s_event' % trstate)
+                        for operation in reversed(processed):
+                            try:
+                                operation.handle_event('revert%s_event' % trstate)
+                            except:
+                                self.critical('error while reverting %sing', trstate,
+                                              exc_info=True)
                         # XXX use slice notation since self.pending_operations is a
                         # read-only property.
                         self.pending_operations[:] = processed + self.pending_operations
--- a/server/sources/native.py	Mon Jul 05 18:00:33 2010 +0200
+++ b/server/sources/native.py	Mon Jul 05 18:25:19 2010 +0200
@@ -42,6 +42,8 @@
 from logilab.common.shellutils import getlogin
 from logilab.database import get_db_helper
 
+from yams import schema2sql as y2sql
+
 from cubicweb import UnknownEid, AuthenticationError, ValidationError, Binary
 from cubicweb import transaction as tx, server, neg_role
 from cubicweb.schema import VIRTUAL_RTYPES
@@ -127,6 +129,21 @@
         restr = '(%s)' % ' OR '.join(clauses)
     return '%s WHERE %s' % (select, restr)
 
+def rdef_table_column(rdef):
+    """return table and column used to store the given relation definition in
+    the database
+    """
+    return (SQL_PREFIX + str(rdef.subject),
+            SQL_PREFIX + str(rdef.rtype))
+
+def rdef_physical_info(dbhelper, rdef):
+    """return backend type and a boolean flag if NULL values should be allowed
+    for a given relation definition
+    """
+    coltype = y2sql.type_from_constraints(dbhelper, rdef.object,
+                                          rdef.constraints, creating=False)
+    allownull = rdef.cardinality[0] != '1'
+    return coltype, allownull
 
 class UndoException(Exception):
     """something went wrong during undoing"""
@@ -674,6 +691,47 @@
 
     # short cut to method requiring advanced db helper usage ##################
 
+    def update_rdef_column(self, session, rdef):
+        """update physical column for a relation definition (final or inlined)
+        """
+        table, column = rdef_table_column(rdef)
+        coltype, allownull = rdef_physical_info(self.dbhelper, rdef)
+        if not self.dbhelper.alter_column_support:
+            self.error("backend can't alter %s.%s to %s%s", table, column, coltype,
+                       not allownull and 'NOT NULL' or '')
+            return
+        self.dbhelper.change_col_type(LogCursor(session.pool[self.uri]),
+                                      table, column, coltype, allownull)
+        self.info('altered %s.%s: now %s%s', table, column, coltype,
+                  not allownull and 'NOT NULL' or '')
+
+    def update_rdef_null_allowed(self, session, rdef):
+        """update NULL / NOT NULL of physical column for a relation definition
+        (final or inlined)
+        """
+        if not self.dbhelper.alter_column_support:
+            # not supported (and NOT NULL not set by yams in that case, so no
+            # worry)
+            return
+        table, column = rdef_table_column(rdef)
+        coltype, allownull = rdef_physical_info(self.dbhelper, rdef)
+        self.dbhelper.set_null_allowed(LogCursor(session.pool[self.uri]),
+                                       table, column, coltype, allownull)
+
+    def update_rdef_indexed(self, session, rdef):
+        table, column = rdef_table_column(rdef)
+        if rdef.indexed:
+            self.create_index(session, table, column)
+        else:
+            self.drop_index(session, table, column)
+
+    def update_rdef_unique(self, session, rdef):
+        table, column = rdef_table_column(rdef)
+        if rdef.constraint_by_type('UniqueConstraint'):
+            self.create_index(session, table, column, unique=True)
+        else:
+            self.drop_index(session, table, column, unique=True)
+
     def create_index(self, session, table, column, unique=False):
         cursor = LogCursor(session.pool[self.uri])
         self.dbhelper.create_index(cursor, table, column, unique)
@@ -682,14 +740,6 @@
         cursor = LogCursor(session.pool[self.uri])
         self.dbhelper.drop_index(cursor, table, column, unique)
 
-    def change_col_type(self, session, table, column, coltype, null_allowed):
-        cursor = LogCursor(session.pool[self.uri])
-        self.dbhelper.change_col_type(cursor, table, column, coltype, null_allowed)
-
-    def set_null_allowed(self, session, table, column, coltype, null_allowed):
-        cursor = LogCursor(session.pool[self.uri])
-        self.dbhelper.set_null_allowed(cursor, table, column, coltype, null_allowed)
-
     # system source interface #################################################
 
     def eid_type_source(self, session, eid):