server/schemahooks.py
changeset 2835 04034421b072
parent 2834 7df3494ae657
child 2839 6419af16faa0
equal deleted inserted replaced
2834:7df3494ae657 2835:04034421b072
     1 """schema hooks:
       
     2 
       
     3 - synchronize the living schema object with the persistent schema
       
     4 - perform physical update on the source when necessary
       
     5 
       
     6 checking for schema consistency is done in hooks.py
       
     7 
       
     8 :organization: Logilab
       
     9 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
       
    10 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
    11 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
       
    12 """
       
    13 __docformat__ = "restructuredtext en"
       
    14 
       
    15 from yams.schema import BASE_TYPES
       
    16 from yams.buildobjs import EntityType, RelationType, RelationDefinition
       
    17 from yams.schema2sql import eschema2sql, rschema2sql, type_from_constraints
       
    18 
       
    19 
       
    20 from cubicweb import ValidationError, RepositoryError
       
    21 from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS
       
    22 from cubicweb.server import schemaserial as ss
       
    23 from cubicweb.server.sqlutils import SQL_PREFIX
       
    24 from cubicweb.server.pool import Operation, SingleLastOperation, PreCommitOperation
       
    25 from cubicweb.server.hookhelper import (entity_attr, entity_name,
       
    26                                         check_internal_entity)
       
    27 
       
    28 
       
    29 TYPE_CONVERTER = { # XXX
       
    30     'Boolean': bool,
       
    31     'Int': int,
       
    32     'Float': float,
       
    33     'Password': str,
       
    34     'String': unicode,
       
    35     'Date' : unicode,
       
    36     'Datetime' : unicode,
       
    37     'Time' : unicode,
       
    38     }
       
    39 
       
    40 # core entity and relation types which can't be removed
       
    41 CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup',
       
    42                                   'CWConstraint', 'CWAttribute', 'CWRelation']
       
    43 CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri',
       
    44                'login', 'upassword', 'name',
       
    45                'is', 'instanceof', 'owned_by', 'created_by', 'in_group',
       
    46                'relation_type', 'from_entity', 'to_entity',
       
    47                'constrainted_by',
       
    48                'read_permission', 'add_permission',
       
    49                'delete_permission', 'updated_permission',
       
    50                ]
       
    51 
       
    52 def get_constraints(session, entity):
       
    53     constraints = []
       
    54     for cstreid in session.transaction_data.get(entity.eid, ()):
       
    55         cstrent = session.entity_from_eid(cstreid)
       
    56         cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value)
       
    57         cstr.eid = cstreid
       
    58         constraints.append(cstr)
       
    59     return constraints
       
    60 
       
    61 def add_inline_relation_column(session, etype, rtype):
       
    62     """add necessary column and index for an inlined relation"""
       
    63     table = SQL_PREFIX + etype
       
    64     column = SQL_PREFIX + rtype
       
    65     try:
       
    66         session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer'
       
    67                                % (table, column)), rollback_on_failure=False)
       
    68         session.info('added column %s to table %s', column, table)
       
    69     except:
       
    70         # silent exception here, if this error has not been raised because the
       
    71         # column already exists, index creation will fail anyway
       
    72         session.exception('error while adding column %s to table %s',
       
    73                           table, column)
       
    74     # create index before alter table which may expectingly fail during test
       
    75     # (sqlite) while index creation should never fail (test for index existence
       
    76     # is done by the dbhelper)
       
    77     session.pool.source('system').create_index(session, table, column)
       
    78     session.info('added index on %s(%s)', table, column)
       
    79     session.transaction_data.setdefault('createdattrs', []).append(
       
    80         '%s.%s' % (etype, rtype))
       
    81 
       
    82 
       
    83 # operations for low-level database alteration  ################################
       
    84 
       
    85 class DropTable(PreCommitOperation):
       
    86     """actually remove a database from the instance's schema"""
       
    87     table = None # make pylint happy
       
    88     def precommit_event(self):
       
    89         dropped = self.session.transaction_data.setdefault('droppedtables',
       
    90                                                            set())
       
    91         if self.table in dropped:
       
    92             return # already processed
       
    93         dropped.add(self.table)
       
    94         self.session.system_sql('DROP TABLE %s' % self.table)
       
    95         self.info('dropped table %s', self.table)
       
    96 
       
    97 
       
    98 class DropRelationTable(DropTable):
       
    99     def __init__(self, session, rtype):
       
   100         super(DropRelationTable, self).__init__(
       
   101             session, table='%s_relation' % rtype)
       
   102         session.transaction_data.setdefault('pendingrtypes', set()).add(rtype)
       
   103 
       
   104 
       
   105 class DropColumn(PreCommitOperation):
       
   106     """actually remove the attribut's column from entity table in the system
       
   107     database
       
   108     """
       
   109     table = column = None # make pylint happy
       
   110     def precommit_event(self):
       
   111         session, table, column = self.session, self.table, self.column
       
   112         # drop index if any
       
   113         session.pool.source('system').drop_index(session, table, column)
       
   114         try:
       
   115             session.system_sql('ALTER TABLE %s DROP COLUMN %s'
       
   116                                % (table, column), rollback_on_failure=False)
       
   117             self.info('dropped column %s from table %s', column, table)
       
   118         except Exception, ex:
       
   119             # not supported by sqlite for instance
       
   120             self.error('error while altering table %s: %s', table, ex)
       
   121 
       
   122 
       
   123 # base operations for in-memory schema synchronization  ########################
       
   124 
       
   125 class MemSchemaNotifyChanges(SingleLastOperation):
       
   126     """the update schema operation:
       
   127 
       
   128     special operation which should be called once and after all other schema
       
   129     operations. It will trigger internal structures rebuilding to consider
       
   130     schema changes
       
   131     """
       
   132 
       
   133     def __init__(self, session):
       
   134         self.repo = session.repo
       
   135         SingleLastOperation.__init__(self, session)
       
   136 
       
   137     def commit_event(self):
       
   138         self.repo.set_schema(self.repo.schema)
       
   139 
       
   140 
       
   141 class MemSchemaOperation(Operation):
       
   142     """base class for schema operations"""
       
   143     def __init__(self, session, kobj=None, **kwargs):
       
   144         self.schema = session.schema
       
   145         self.kobj = kobj
       
   146         # once Operation.__init__ has been called, event may be triggered, so
       
   147         # do this last !
       
   148         Operation.__init__(self, session, **kwargs)
       
   149         # every schema operation is triggering a schema update
       
   150         MemSchemaNotifyChanges(session)
       
   151 
       
   152     def prepare_constraints(self, subjtype, rtype, objtype):
       
   153         constraints = rtype.rproperty(subjtype, objtype, 'constraints')
       
   154         self.constraints = list(constraints)
       
   155         rtype.set_rproperty(subjtype, objtype, 'constraints', self.constraints)
       
   156 
       
   157 
       
   158 class MemSchemaEarlyOperation(MemSchemaOperation):
       
   159     def insert_index(self):
       
   160         """schema operation which are inserted at the begining of the queue
       
   161         (typically to add/remove entity or relation types)
       
   162         """
       
   163         i = -1
       
   164         for i, op in enumerate(self.session.pending_operations):
       
   165             if not isinstance(op, MemSchemaEarlyOperation):
       
   166                 return i
       
   167         return i + 1
       
   168 
       
   169 
       
   170 class MemSchemaPermissionOperation(MemSchemaOperation):
       
   171     """base class to synchronize schema permission definitions"""
       
   172     def __init__(self, session, perm, etype_eid):
       
   173         self.perm = perm
       
   174         try:
       
   175             self.name = entity_name(session, etype_eid)
       
   176         except IndexError:
       
   177             self.error('changing permission of a no more existant type #%s',
       
   178                 etype_eid)
       
   179         else:
       
   180             Operation.__init__(self, session)
       
   181 
       
   182 
       
   183 # operations for high-level source database alteration  ########################
       
   184 
       
   185 class SourceDbCWETypeRename(PreCommitOperation):
       
   186     """this operation updates physical storage accordingly"""
       
   187     oldname = newname = None # make pylint happy
       
   188 
       
   189     def precommit_event(self):
       
   190         # we need sql to operate physical changes on the system database
       
   191         sqlexec = self.session.system_sql
       
   192         sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname,
       
   193                                                      SQL_PREFIX, self.newname))
       
   194         self.info('renamed table %s to %s', self.oldname, self.newname)
       
   195         sqlexec('UPDATE entities SET type=%s WHERE type=%s',
       
   196                 (self.newname, self.oldname))
       
   197         sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
       
   198                 (self.newname, self.oldname))
       
   199 
       
   200 
       
   201 class SourceDbCWRTypeUpdate(PreCommitOperation):
       
   202     """actually update some properties of a relation definition"""
       
   203     rschema = values = entity = None # make pylint happy
       
   204 
       
   205     def precommit_event(self):
       
   206         session = self.session
       
   207         rschema = self.rschema
       
   208         if rschema.is_final() or not 'inlined' in self.values:
       
   209             return # nothing to do
       
   210         inlined = self.values['inlined']
       
   211         entity = self.entity
       
   212         # check in-lining is necessary / possible
       
   213         if not entity.inlined_changed(inlined):
       
   214             return # nothing to do
       
   215         # inlined changed, make necessary physical changes!
       
   216         sqlexec = self.session.system_sql
       
   217         rtype = rschema.type
       
   218         eidcolumn = SQL_PREFIX + 'eid'
       
   219         if not inlined:
       
   220             # need to create the relation if it has not been already done by
       
   221             # another event of the same transaction
       
   222             if not rschema.type in session.transaction_data.get('createdtables', ()):
       
   223                 tablesql = rschema2sql(rschema)
       
   224                 # create the necessary table
       
   225                 for sql in tablesql.split(';'):
       
   226                     if sql.strip():
       
   227                         sqlexec(sql)
       
   228                 session.transaction_data.setdefault('createdtables', []).append(
       
   229                     rschema.type)
       
   230             # copy existant data
       
   231             column = SQL_PREFIX + rtype
       
   232             for etype in rschema.subjects():
       
   233                 table = SQL_PREFIX + str(etype)
       
   234                 sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL'
       
   235                         % (rtype, eidcolumn, column, table, column))
       
   236             # drop existant columns
       
   237             for etype in rschema.subjects():
       
   238                 DropColumn(session, table=SQL_PREFIX + str(etype),
       
   239                              column=SQL_PREFIX + rtype)
       
   240         else:
       
   241             for etype in rschema.subjects():
       
   242                 try:
       
   243                     add_inline_relation_column(session, str(etype), rtype)
       
   244                 except Exception, ex:
       
   245                     # the column probably already exists. this occurs when the
       
   246                     # entity's type has just been added or if the column has not
       
   247                     # been previously dropped
       
   248                     self.error('error while altering table %s: %s', etype, ex)
       
   249                 # copy existant data.
       
   250                 # XXX don't use, it's not supported by sqlite (at least at when i tried it)
       
   251                 #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
       
   252                 #        'FROM %(rtype)s_relation '
       
   253                 #        'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'
       
   254                 #        % locals())
       
   255                 table = SQL_PREFIX + str(etype)
       
   256                 cursor = sqlexec('SELECT eid_from, eid_to FROM %(table)s, '
       
   257                                  '%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s='
       
   258                                  '%(rtype)s_relation.eid_from' % locals())
       
   259                 args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()]
       
   260                 if args:
       
   261                     column = SQL_PREFIX + rtype
       
   262                     cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
       
   263                                        % (table, column, eidcolumn), args)
       
   264                 # drop existant table
       
   265                 DropRelationTable(session, rtype)
       
   266 
       
   267 
       
   268 class SourceDbCWAttributeAdd(PreCommitOperation):
       
   269     """an attribute relation (CWAttribute) has been added:
       
   270     * add the necessary column
       
   271     * set default on this column if any and possible
       
   272     * register an operation to add the relation definition to the
       
   273       instance's schema on commit
       
   274 
       
   275     constraints are handled by specific hooks
       
   276     """
       
   277     entity = None # make pylint happy
       
   278 
       
   279     def init_rdef(self, **kwargs):
       
   280         entity = self.entity
       
   281         fromentity = entity.stype
       
   282         self.session.execute('SET X ordernum Y+1 '
       
   283                              'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
       
   284                              'X ordernum >= %(order)s, NOT X eid %(x)s',
       
   285                              {'x': entity.eid, 'se': fromentity.eid,
       
   286                               'order': entity.ordernum or 0})
       
   287         subj = str(fromentity.name)
       
   288         rtype = entity.rtype.name
       
   289         obj = str(entity.otype.name)
       
   290         constraints = get_constraints(self.session, entity)
       
   291         rdef = RelationDefinition(subj, rtype, obj,
       
   292                                   description=entity.description,
       
   293                                   cardinality=entity.cardinality,
       
   294                                   constraints=constraints,
       
   295                                   order=entity.ordernum,
       
   296                                   eid=entity.eid,
       
   297                                   **kwargs)
       
   298         MemSchemaRDefAdd(self.session, rdef)
       
   299         return rdef
       
   300 
       
   301     def precommit_event(self):
       
   302         session = self.session
       
   303         entity = self.entity
       
   304         # entity.defaultval is a string or None, but we need a correctly typed
       
   305         # value
       
   306         default = entity.defaultval
       
   307         if default is not None:
       
   308             default = TYPE_CONVERTER[entity.otype.name](default)
       
   309         rdef = self.init_rdef(default=default,
       
   310                               indexed=entity.indexed,
       
   311                               fulltextindexed=entity.fulltextindexed,
       
   312                               internationalizable=entity.internationalizable)
       
   313         sysource = session.pool.source('system')
       
   314         attrtype = type_from_constraints(sysource.dbhelper, rdef.object,
       
   315                                          rdef.constraints)
       
   316         # XXX should be moved somehow into lgc.adbh: sqlite doesn't support to
       
   317         # add a new column with UNIQUE, it should be added after the ALTER TABLE
       
   318         # using ADD INDEX
       
   319         if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
       
   320             extra_unique_index = True
       
   321             attrtype = attrtype.replace(' UNIQUE', '')
       
   322         else:
       
   323             extra_unique_index = False
       
   324         # added some str() wrapping query since some backend (eg psycopg) don't
       
   325         # allow unicode queries
       
   326         table = SQL_PREFIX + rdef.subject
       
   327         column = SQL_PREFIX + rdef.name
       
   328         try:
       
   329             session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s'
       
   330                                    % (table, column, attrtype)),
       
   331                                rollback_on_failure=False)
       
   332             self.info('added column %s to table %s', table, column)
       
   333         except Exception, ex:
       
   334             # the column probably already exists. this occurs when
       
   335             # the entity's type has just been added or if the column
       
   336             # has not been previously dropped
       
   337             self.error('error while altering table %s: %s', table, ex)
       
   338         if extra_unique_index or entity.indexed:
       
   339             try:
       
   340                 sysource.create_index(session, table, column,
       
   341                                       unique=extra_unique_index)
       
   342             except Exception, ex:
       
   343                 self.error('error while creating index for %s.%s: %s',
       
   344                            table, column, ex)
       
   345 
       
   346 
       
   347 class SourceDbCWRelationAdd(SourceDbCWAttributeAdd):
       
   348     """an actual relation has been added:
       
   349     * if this is an inlined relation, add the necessary column
       
   350       else if it's the first instance of this relation type, add the
       
   351       necessary table and set default permissions
       
   352     * register an operation to add the relation definition to the
       
   353       instance's schema on commit
       
   354 
       
   355     constraints are handled by specific hooks
       
   356     """
       
   357     entity = None # make pylint happy
       
   358 
       
   359     def precommit_event(self):
       
   360         session = self.session
       
   361         entity = self.entity
       
   362         rdef = self.init_rdef(composite=entity.composite)
       
   363         schema = session.schema
       
   364         rtype = rdef.name
       
   365         rschema = session.schema.rschema(rtype)
       
   366         # this have to be done before permissions setting
       
   367         if rschema.inlined:
       
   368             # need to add a column if the relation is inlined and if this is the
       
   369             # first occurence of "Subject relation Something" whatever Something
       
   370             # and if it has not been added during other event of the same
       
   371             # transaction
       
   372             key = '%s.%s' % (rdef.subject, rtype)
       
   373             try:
       
   374                 alreadythere = bool(rschema.objects(rdef.subject))
       
   375             except KeyError:
       
   376                 alreadythere = False
       
   377             if not (alreadythere or
       
   378                     key in session.transaction_data.get('createdattrs', ())):
       
   379                 add_inline_relation_column(session, rdef.subject, rtype)
       
   380         else:
       
   381             # need to create the relation if no relation definition in the
       
   382             # schema and if it has not been added during other event of the same
       
   383             # transaction
       
   384             if not (rschema.subjects() or
       
   385                     rtype in session.transaction_data.get('createdtables', ())):
       
   386                 try:
       
   387                     rschema = session.schema.rschema(rtype)
       
   388                     tablesql = rschema2sql(rschema)
       
   389                 except KeyError:
       
   390                     # fake we add it to the schema now to get a correctly
       
   391                     # initialized schema but remove it before doing anything
       
   392                     # more dangerous...
       
   393                     rschema = session.schema.add_relation_type(rdef)
       
   394                     tablesql = rschema2sql(rschema)
       
   395                     session.schema.del_relation_type(rtype)
       
   396                 # create the necessary table
       
   397                 for sql in tablesql.split(';'):
       
   398                     if sql.strip():
       
   399                         session.system_sql(sql)
       
   400                 session.transaction_data.setdefault('createdtables', []).append(
       
   401                     rtype)
       
   402 
       
   403 
       
   404 class SourceDbRDefUpdate(PreCommitOperation):
       
   405     """actually update some properties of a relation definition"""
       
   406     rschema = values = None # make pylint happy
       
   407 
       
   408     def precommit_event(self):
       
   409         etype = self.kobj[0]
       
   410         table = SQL_PREFIX + etype
       
   411         column = SQL_PREFIX + self.rschema.type
       
   412         if 'indexed' in self.values:
       
   413             sysource = self.session.pool.source('system')
       
   414             if self.values['indexed']:
       
   415                 sysource.create_index(self.session, table, column)
       
   416             else:
       
   417                 sysource.drop_index(self.session, table, column)
       
   418         if 'cardinality' in self.values and self.rschema.is_final():
       
   419             adbh = self.session.pool.source('system').dbhelper
       
   420             if not adbh.alter_column_support:
       
   421                 # not supported (and NOT NULL not set by yams in that case, so
       
   422                 # no worry)
       
   423                 return
       
   424             atype = self.rschema.objects(etype)[0]
       
   425             constraints = self.rschema.rproperty(etype, atype, 'constraints')
       
   426             coltype = type_from_constraints(adbh, atype, constraints,
       
   427                                             creating=False)
       
   428             # XXX check self.values['cardinality'][0] actually changed?
       
   429             sql = adbh.sql_set_null_allowed(table, column, coltype,
       
   430                                             self.values['cardinality'][0] != '1')
       
   431             self.session.system_sql(sql)
       
   432 
       
   433 
       
   434 class SourceDbCWConstraintAdd(PreCommitOperation):
       
   435     """actually update constraint of a relation definition"""
       
   436     entity = None # make pylint happy
       
   437     cancelled = False
       
   438 
       
   439     def precommit_event(self):
       
   440         rdef = self.entity.reverse_constrained_by[0]
       
   441         session = self.session
       
   442         # when the relation is added in the same transaction, the constraint
       
   443         # object is created by the operation adding the attribute or relation,
       
   444         # so there is nothing to do here
       
   445         if rdef.eid in session.transaction_data.get('neweids', ()):
       
   446             return
       
   447         subjtype, rtype, objtype = session.schema.schema_by_eid(rdef.eid)
       
   448         cstrtype = self.entity.type
       
   449         oldcstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
       
   450         newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
       
   451         table = SQL_PREFIX + str(subjtype)
       
   452         column = SQL_PREFIX + str(rtype)
       
   453         # alter the physical schema on size constraint changes
       
   454         if newcstr.type() == 'SizeConstraint' and (
       
   455             oldcstr is None or oldcstr.max != newcstr.max):
       
   456             adbh = self.session.pool.source('system').dbhelper
       
   457             card = rtype.rproperty(subjtype, objtype, 'cardinality')
       
   458             coltype = type_from_constraints(adbh, objtype, [newcstr],
       
   459                                             creating=False)
       
   460             sql = adbh.sql_change_col_type(table, column, coltype, card != '1')
       
   461             try:
       
   462                 session.system_sql(sql, rollback_on_failure=False)
       
   463                 self.info('altered column %s of table %s: now VARCHAR(%s)',
       
   464                           column, table, newcstr.max)
       
   465             except Exception, ex:
       
   466                 # not supported by sqlite for instance
       
   467                 self.error('error while altering table %s: %s', table, ex)
       
   468         elif cstrtype == 'UniqueConstraint' and oldcstr is None:
       
   469             session.pool.source('system').create_index(
       
   470                 self.session, table, column, unique=True)
       
   471 
       
   472 
       
   473 class SourceDbCWConstraintDel(PreCommitOperation):
       
   474     """actually remove a constraint of a relation definition"""
       
   475     rtype = subjtype = objtype = None # make pylint happy
       
   476 
       
   477     def precommit_event(self):
       
   478         cstrtype = self.cstr.type()
       
   479         table = SQL_PREFIX + str(self.subjtype)
       
   480         column = SQL_PREFIX + str(self.rtype)
       
   481         # alter the physical schema on size/unique constraint changes
       
   482         if cstrtype == 'SizeConstraint':
       
   483             try:
       
   484                 self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT'
       
   485                                         % (table, column),
       
   486                                         rollback_on_failure=False)
       
   487                 self.info('altered column %s of table %s: now TEXT',
       
   488                           column, table)
       
   489             except Exception, ex:
       
   490                 # not supported by sqlite for instance
       
   491                 self.error('error while altering table %s: %s', table, ex)
       
   492         elif cstrtype == 'UniqueConstraint':
       
   493             self.session.pool.source('system').drop_index(
       
   494                 self.session, table, column, unique=True)
       
   495 
       
   496 
       
   497 # operations for in-memory schema synchronization  #############################
       
   498 
       
   499 class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
       
   500     """actually add the entity type to the instance's schema"""
       
   501     eid = None # make pylint happy
       
   502     def commit_event(self):
       
   503         self.schema.add_entity_type(self.kobj)
       
   504 
       
   505 
       
   506 class MemSchemaCWETypeRename(MemSchemaOperation):
       
   507     """this operation updates physical storage accordingly"""
       
   508     oldname = newname = None # make pylint happy
       
   509 
       
   510     def commit_event(self):
       
   511         self.session.schema.rename_entity_type(self.oldname, self.newname)
       
   512 
       
   513 
       
   514 class MemSchemaCWETypeDel(MemSchemaOperation):
       
   515     """actually remove the entity type from the instance's schema"""
       
   516     def commit_event(self):
       
   517         try:
       
   518             # del_entity_type also removes entity's relations
       
   519             self.schema.del_entity_type(self.kobj)
       
   520         except KeyError:
       
   521             # s/o entity type have already been deleted
       
   522             pass
       
   523 
       
   524 
       
   525 class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
       
   526     """actually add the relation type to the instance's schema"""
       
   527     eid = None # make pylint happy
       
   528     def commit_event(self):
       
   529         rschema = self.schema.add_relation_type(self.kobj)
       
   530         rschema.set_default_groups()
       
   531 
       
   532 
       
   533 class MemSchemaCWRTypeUpdate(MemSchemaOperation):
       
   534     """actually update some properties of a relation definition"""
       
   535     rschema = values = None # make pylint happy
       
   536 
       
   537     def commit_event(self):
       
   538         # structure should be clean, not need to remove entity's relations
       
   539         # at this point
       
   540         self.rschema.__dict__.update(self.values)
       
   541 
       
   542 
       
   543 class MemSchemaCWRTypeDel(MemSchemaOperation):
       
   544     """actually remove the relation type from the instance's schema"""
       
   545     def commit_event(self):
       
   546         try:
       
   547             self.schema.del_relation_type(self.kobj)
       
   548         except KeyError:
       
   549             # s/o entity type have already been deleted
       
   550             pass
       
   551 
       
   552 
       
   553 class MemSchemaRDefAdd(MemSchemaEarlyOperation):
       
   554     """actually add the attribute relation definition to the instance's
       
   555     schema
       
   556     """
       
   557     def commit_event(self):
       
   558         self.schema.add_relation_def(self.kobj)
       
   559 
       
   560 
       
   561 class MemSchemaRDefUpdate(MemSchemaOperation):
       
   562     """actually update some properties of a relation definition"""
       
   563     rschema = values = None # make pylint happy
       
   564 
       
   565     def commit_event(self):
       
   566         # structure should be clean, not need to remove entity's relations
       
   567         # at this point
       
   568         self.rschema._rproperties[self.kobj].update(self.values)
       
   569 
       
   570 
       
   571 class MemSchemaRDefDel(MemSchemaOperation):
       
   572     """actually remove the relation definition from the instance's schema"""
       
   573     def commit_event(self):
       
   574         subjtype, rtype, objtype = self.kobj
       
   575         try:
       
   576             self.schema.del_relation_def(subjtype, rtype, objtype)
       
   577         except KeyError:
       
   578             # relation type may have been already deleted
       
   579             pass
       
   580 
       
   581 
       
   582 class MemSchemaCWConstraintAdd(MemSchemaOperation):
       
   583     """actually update constraint of a relation definition
       
   584 
       
   585     has to be called before SourceDbCWConstraintAdd
       
   586     """
       
   587     cancelled = False
       
   588 
       
   589     def precommit_event(self):
       
   590         rdef = self.entity.reverse_constrained_by[0]
       
   591         # when the relation is added in the same transaction, the constraint
       
   592         # object is created by the operation adding the attribute or relation,
       
   593         # so there is nothing to do here
       
   594         if rdef.eid in self.session.transaction_data.get('neweids', ()):
       
   595             self.cancelled = True
       
   596             return
       
   597         subjtype, rtype, objtype = self.session.schema.schema_by_eid(rdef.eid)
       
   598         self.prepare_constraints(subjtype, rtype, objtype)
       
   599         cstrtype = self.entity.type
       
   600         self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
       
   601         self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
       
   602         self.newcstr.eid = self.entity.eid
       
   603 
       
   604     def commit_event(self):
       
   605         if self.cancelled:
       
   606             return
       
   607         # in-place modification
       
   608         if not self.cstr is None:
       
   609             self.constraints.remove(self.cstr)
       
   610         self.constraints.append(self.newcstr)
       
   611 
       
   612 
       
   613 class MemSchemaCWConstraintDel(MemSchemaOperation):
       
   614     """actually remove a constraint of a relation definition
       
   615 
       
   616     has to be called before SourceDbCWConstraintDel
       
   617     """
       
   618     rtype = subjtype = objtype = None # make pylint happy
       
   619     def precommit_event(self):
       
   620         self.prepare_constraints(self.subjtype, self.rtype, self.objtype)
       
   621 
       
   622     def commit_event(self):
       
   623         self.constraints.remove(self.cstr)
       
   624 
       
   625 
       
   626 class MemSchemaPermissionCWGroupAdd(MemSchemaPermissionOperation):
       
   627     """synchronize schema when a *_permission relation has been added on a group
       
   628     """
       
   629     def __init__(self, session, perm, etype_eid, group_eid):
       
   630         self.group = entity_name(session, group_eid)
       
   631         super(MemSchemaPermissionCWGroupAdd, self).__init__(
       
   632             session, perm, etype_eid)
       
   633 
       
   634     def commit_event(self):
       
   635         """the observed connections pool has been commited"""
       
   636         try:
       
   637             erschema = self.schema[self.name]
       
   638         except KeyError:
       
   639             # duh, schema not found, log error and skip operation
       
   640             self.error('no schema for %s', self.name)
       
   641             return
       
   642         groups = list(erschema.get_groups(self.perm))
       
   643         try:
       
   644             groups.index(self.group)
       
   645             self.warning('group %s already have permission %s on %s',
       
   646                          self.group, self.perm, erschema.type)
       
   647         except ValueError:
       
   648             groups.append(self.group)
       
   649             erschema.set_groups(self.perm, groups)
       
   650 
       
   651 
       
   652 class MemSchemaPermissionCWGroupDel(MemSchemaPermissionCWGroupAdd):
       
   653     """synchronize schema when a *_permission relation has been deleted from a
       
   654     group
       
   655     """
       
   656 
       
   657     def commit_event(self):
       
   658         """the observed connections pool has been commited"""
       
   659         try:
       
   660             erschema = self.schema[self.name]
       
   661         except KeyError:
       
   662             # duh, schema not found, log error and skip operation
       
   663             self.error('no schema for %s', self.name)
       
   664             return
       
   665         groups = list(erschema.get_groups(self.perm))
       
   666         try:
       
   667             groups.remove(self.group)
       
   668             erschema.set_groups(self.perm, groups)
       
   669         except ValueError:
       
   670             self.error('can\'t remove permission %s on %s to group %s',
       
   671                 self.perm, erschema.type, self.group)
       
   672 
       
   673 
       
   674 class MemSchemaPermissionRQLExpressionAdd(MemSchemaPermissionOperation):
       
   675     """synchronize schema when a *_permission relation has been added on a rql
       
   676     expression
       
   677     """
       
   678     def __init__(self, session, perm, etype_eid, expression):
       
   679         self.expr = expression
       
   680         super(MemSchemaPermissionRQLExpressionAdd, self).__init__(
       
   681             session, perm, etype_eid)
       
   682 
       
   683     def commit_event(self):
       
   684         """the observed connections pool has been commited"""
       
   685         try:
       
   686             erschema = self.schema[self.name]
       
   687         except KeyError:
       
   688             # duh, schema not found, log error and skip operation
       
   689             self.error('no schema for %s', self.name)
       
   690             return
       
   691         exprs = list(erschema.get_rqlexprs(self.perm))
       
   692         exprs.append(erschema.rql_expression(self.expr))
       
   693         erschema.set_rqlexprs(self.perm, exprs)
       
   694 
       
   695 
       
   696 class MemSchemaPermissionRQLExpressionDel(MemSchemaPermissionRQLExpressionAdd):
       
   697     """synchronize schema when a *_permission relation has been deleted from an
       
   698     rql expression
       
   699     """
       
   700 
       
   701     def commit_event(self):
       
   702         """the observed connections pool has been commited"""
       
   703         try:
       
   704             erschema = self.schema[self.name]
       
   705         except KeyError:
       
   706             # duh, schema not found, log error and skip operation
       
   707             self.error('no schema for %s', self.name)
       
   708             return
       
   709         rqlexprs = list(erschema.get_rqlexprs(self.perm))
       
   710         for i, rqlexpr in enumerate(rqlexprs):
       
   711             if rqlexpr.expression == self.expr:
       
   712                 rqlexprs.pop(i)
       
   713                 break
       
   714         else:
       
   715             self.error('can\'t remove permission %s on %s for expression %s',
       
   716                 self.perm, erschema.type, self.expr)
       
   717             return
       
   718         erschema.set_rqlexprs(self.perm, rqlexprs)
       
   719 
       
   720 
       
   721 # deletion hooks ###############################################################
       
   722 
       
   723 def before_del_eetype(session, eid):
       
   724     """before deleting a CWEType entity:
       
   725     * check that we don't remove a core entity type
       
   726     * cascade to delete related CWAttribute and CWRelation entities
       
   727     * instantiate an operation to delete the entity type on commit
       
   728     """
       
   729     # final entities can't be deleted, don't care about that
       
   730     name = check_internal_entity(session, eid, CORE_ETYPES)
       
   731     # delete every entities of this type
       
   732     session.unsafe_execute('DELETE %s X' % name)
       
   733     DropTable(session, table=SQL_PREFIX + name)
       
   734     MemSchemaCWETypeDel(session, name)
       
   735 
       
   736 
       
   737 def after_del_eetype(session, eid):
       
   738     # workflow cleanup
       
   739     session.execute('DELETE State X WHERE NOT X state_of Y')
       
   740     session.execute('DELETE Transition X WHERE NOT X transition_of Y')
       
   741 
       
   742 
       
   743 def before_del_ertype(session, eid):
       
   744     """before deleting a CWRType entity:
       
   745     * check that we don't remove a core relation type
       
   746     * cascade to delete related CWAttribute and CWRelation entities
       
   747     * instantiate an operation to delete the relation type on commit
       
   748     """
       
   749     name = check_internal_entity(session, eid, CORE_RTYPES)
       
   750     # delete relation definitions using this relation type
       
   751     session.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
       
   752                     {'x': eid})
       
   753     session.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
       
   754                     {'x': eid})
       
   755     MemSchemaCWRTypeDel(session, name)
       
   756 
       
   757 
       
   758 def after_del_relation_type(session, rdefeid, rtype, rteid):
       
   759     """before deleting a CWAttribute or CWRelation entity:
       
   760     * if this is a final or inlined relation definition, instantiate an
       
   761       operation to drop necessary column, else if this is the last instance
       
   762       of a non final relation, instantiate an operation to drop necessary
       
   763       table
       
   764     * instantiate an operation to delete the relation definition on commit
       
   765     * delete the associated relation type when necessary
       
   766     """
       
   767     subjschema, rschema, objschema = session.schema.schema_by_eid(rdefeid)
       
   768     pendings = session.transaction_data.get('pendingeids', ())
       
   769     # first delete existing relation if necessary
       
   770     if rschema.is_final():
       
   771         rdeftype = 'CWAttribute'
       
   772     else:
       
   773         rdeftype = 'CWRelation'
       
   774         if not (subjschema.eid in pendings or objschema.eid in pendings):
       
   775             pending = session.transaction_data.setdefault('pendingrdefs', set())
       
   776             pending.add((subjschema, rschema, objschema))
       
   777             session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
       
   778                             % (rschema, subjschema, objschema))
       
   779     execute = session.unsafe_execute
       
   780     rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
       
   781                    'R eid %%(x)s' % rdeftype, {'x': rteid})
       
   782     lastrel = rset[0][0] == 0
       
   783     # we have to update physical schema systematically for final and inlined
       
   784     # relations, but only if it's the last instance for this relation type
       
   785     # for other relations
       
   786 
       
   787     if (rschema.is_final() or rschema.inlined):
       
   788         rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
       
   789                        'R eid %%(x)s, X from_entity E, E name %%(name)s'
       
   790                        % rdeftype, {'x': rteid, 'name': str(subjschema)})
       
   791         if rset[0][0] == 0 and not subjschema.eid in pendings:
       
   792             ptypes = session.transaction_data.setdefault('pendingrtypes', set())
       
   793             ptypes.add(rschema.type)
       
   794             DropColumn(session, table=SQL_PREFIX + subjschema.type,
       
   795                          column=SQL_PREFIX + rschema.type)
       
   796     elif lastrel:
       
   797         DropRelationTable(session, rschema.type)
       
   798     # if this is the last instance, drop associated relation type
       
   799     if lastrel and not rteid in pendings:
       
   800         execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rteid}, 'x')
       
   801     MemSchemaRDefDel(session, (subjschema, rschema, objschema))
       
   802 
       
   803 
       
   804 # addition hooks ###############################################################
       
   805 
       
   806 def before_add_eetype(session, entity):
       
   807     """before adding a CWEType entity:
       
   808     * check that we are not using an existing entity type,
       
   809     """
       
   810     name = entity['name']
       
   811     schema = session.schema
       
   812     if name in schema and schema[name].eid is not None:
       
   813         raise RepositoryError('an entity type %s already exists' % name)
       
   814 
       
   815 def after_add_eetype(session, entity):
       
   816     """after adding a CWEType entity:
       
   817     * create the necessary table
       
   818     * set creation_date and modification_date by creating the necessary
       
   819       CWAttribute entities
       
   820     * add owned_by relation by creating the necessary CWRelation entity
       
   821     * register an operation to add the entity type to the instance's
       
   822       schema on commit
       
   823     """
       
   824     if entity.get('final'):
       
   825         return
       
   826     schema = session.schema
       
   827     name = entity['name']
       
   828     etype = EntityType(name=name, description=entity.get('description'),
       
   829                        meta=entity.get('meta')) # don't care about final
       
   830     # fake we add it to the schema now to get a correctly initialized schema
       
   831     # but remove it before doing anything more dangerous...
       
   832     schema = session.schema
       
   833     eschema = schema.add_entity_type(etype)
       
   834     eschema.set_default_groups()
       
   835     # generate table sql and rql to add metadata
       
   836     tablesql = eschema2sql(session.pool.source('system').dbhelper, eschema,
       
   837                            prefix=SQL_PREFIX)
       
   838     relrqls = []
       
   839     for rtype in (META_RTYPES - VIRTUAL_RTYPES):
       
   840         rschema = schema[rtype]
       
   841         sampletype = rschema.subjects()[0]
       
   842         desttype = rschema.objects()[0]
       
   843         props = rschema.rproperties(sampletype, desttype)
       
   844         relrqls += list(ss.rdef2rql(rschema, name, desttype, props))
       
   845     # now remove it !
       
   846     schema.del_entity_type(name)
       
   847     # create the necessary table
       
   848     for sql in tablesql.split(';'):
       
   849         if sql.strip():
       
   850             session.system_sql(sql)
       
   851     # register operation to modify the schema on commit
       
   852     # this have to be done before adding other relations definitions
       
   853     # or permission settings
       
   854     etype.eid = entity.eid
       
   855     MemSchemaCWETypeAdd(session, etype)
       
   856     # add meta relations
       
   857     for rql, kwargs in relrqls:
       
   858         session.execute(rql, kwargs)
       
   859 
       
   860 
       
   861 def before_add_ertype(session, entity):
       
   862     """before adding a CWRType entity:
       
   863     * check that we are not using an existing relation type,
       
   864     * register an operation to add the relation type to the instance's
       
   865       schema on commit
       
   866 
       
   867     We don't know yeat this point if a table is necessary
       
   868     """
       
   869     name = entity['name']
       
   870     if name in session.schema.relations():
       
   871         raise RepositoryError('a relation type %s already exists' % name)
       
   872 
       
   873 
       
   874 def after_add_ertype(session, entity):
       
   875     """after a CWRType entity has been added:
       
   876     * register an operation to add the relation type to the instance's
       
   877       schema on commit
       
   878     We don't know yeat this point if a table is necessary
       
   879     """
       
   880     rtype = RelationType(name=entity['name'],
       
   881                          description=entity.get('description'),
       
   882                          meta=entity.get('meta', False),
       
   883                          inlined=entity.get('inlined', False),
       
   884                          symetric=entity.get('symetric', False))
       
   885     rtype.eid = entity.eid
       
   886     MemSchemaCWRTypeAdd(session, rtype)
       
   887 
       
   888 
       
   889 def after_add_efrdef(session, entity):
       
   890     SourceDbCWAttributeAdd(session, entity=entity)
       
   891 
       
   892 def after_add_enfrdef(session, entity):
       
   893     SourceDbCWRelationAdd(session, entity=entity)
       
   894 
       
   895 
       
   896 # update hooks #################################################################
       
   897 
       
   898 def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
       
   899     errors = {}
       
   900     # don't use getattr(entity, attr), we would get the modified value if any
       
   901     for attr in ro_attrs:
       
   902         origval = entity_attr(session, entity.eid, attr)
       
   903         if entity.get(attr, origval) != origval:
       
   904             errors[attr] = session._("can't change the %s attribute") % \
       
   905                            display_name(session, attr)
       
   906     if errors:
       
   907         raise ValidationError(entity.eid, errors)
       
   908 
       
   909 def before_update_eetype(session, entity):
       
   910     """check name change, handle final"""
       
   911     check_valid_changes(session, entity, ro_attrs=('final',))
       
   912     # don't use getattr(entity, attr), we would get the modified value if any
       
   913     oldname = entity_attr(session, entity.eid, 'name')
       
   914     newname = entity.get('name', oldname)
       
   915     if newname.lower() != oldname.lower():
       
   916         SourceDbCWETypeRename(session, oldname=oldname, newname=newname)
       
   917         MemSchemaCWETypeRename(session, oldname=oldname, newname=newname)
       
   918 
       
   919 def before_update_ertype(session, entity):
       
   920     """check name change, handle final"""
       
   921     check_valid_changes(session, entity)
       
   922 
       
   923 
       
   924 def after_update_erdef(session, entity):
       
   925     if entity.eid in session.transaction_data.get('pendingeids', ()):
       
   926         return
       
   927     desttype = entity.otype.name
       
   928     rschema = session.schema[entity.rtype.name]
       
   929     newvalues = {}
       
   930     for prop in rschema.rproperty_defs(desttype):
       
   931         if prop == 'constraints':
       
   932             continue
       
   933         if prop == 'order':
       
   934             prop = 'ordernum'
       
   935         if prop in entity.edited_attributes:
       
   936             newvalues[prop] = entity[prop]
       
   937     if newvalues:
       
   938         subjtype = entity.stype.name
       
   939         MemSchemaRDefUpdate(session, kobj=(subjtype, desttype),
       
   940                             rschema=rschema, values=newvalues)
       
   941         SourceDbRDefUpdate(session, kobj=(subjtype, desttype),
       
   942                            rschema=rschema, values=newvalues)
       
   943 
       
   944 def after_update_ertype(session, entity):
       
   945     rschema = session.schema.rschema(entity.name)
       
   946     newvalues = {}
       
   947     for prop in ('meta', 'symetric', 'inlined'):
       
   948         if prop in entity:
       
   949             newvalues[prop] = entity[prop]
       
   950     if newvalues:
       
   951         MemSchemaCWRTypeUpdate(session, rschema=rschema, values=newvalues)
       
   952         SourceDbCWRTypeUpdate(session, rschema=rschema, values=newvalues,
       
   953                               entity=entity)
       
   954 
       
   955 # constraints synchronization hooks ############################################
       
   956 
       
   957 def after_add_econstraint(session, entity):
       
   958     MemSchemaCWConstraintAdd(session, entity=entity)
       
   959     SourceDbCWConstraintAdd(session, entity=entity)
       
   960 
       
   961 
       
   962 def after_update_econstraint(session, entity):
       
   963     MemSchemaCWConstraintAdd(session, entity=entity)
       
   964     SourceDbCWConstraintAdd(session, entity=entity)
       
   965 
       
   966 
       
   967 def before_delete_constrained_by(session, fromeid, rtype, toeid):
       
   968     if not fromeid in session.transaction_data.get('pendingeids', ()):
       
   969         schema = session.schema
       
   970         entity = session.entity_from_eid(toeid)
       
   971         subjtype, rtype, objtype = schema.schema_by_eid(fromeid)
       
   972         try:
       
   973             cstr = rtype.constraint_by_type(subjtype, objtype,
       
   974                                             entity.cstrtype[0].name)
       
   975         except IndexError:
       
   976             session.critical('constraint type no more accessible')
       
   977         else:
       
   978             SourceDbCWConstraintDel(session, subjtype=subjtype, rtype=rtype,
       
   979                                     objtype=objtype, cstr=cstr)
       
   980             MemSchemaCWConstraintDel(session, subjtype=subjtype, rtype=rtype,
       
   981                                      objtype=objtype, cstr=cstr)
       
   982 
       
   983 
       
   984 def after_add_constrained_by(session, fromeid, rtype, toeid):
       
   985     if fromeid in session.transaction_data.get('neweids', ()):
       
   986         session.transaction_data.setdefault(fromeid, []).append(toeid)
       
   987 
       
   988 
       
   989 # permissions synchronization hooks ############################################
       
   990 
       
   991 def after_add_permission(session, subject, rtype, object):
       
   992     """added entity/relation *_permission, need to update schema"""
       
   993     perm = rtype.split('_', 1)[0]
       
   994     if session.describe(object)[0] == 'CWGroup':
       
   995         MemSchemaPermissionCWGroupAdd(session, perm, subject, object)
       
   996     else: # RQLExpression
       
   997         expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR',
       
   998                                {'x': object}, 'x')[0][0]
       
   999         MemSchemaPermissionRQLExpressionAdd(session, perm, subject, expr)
       
  1000 
       
  1001 
       
  1002 def before_del_permission(session, subject, rtype, object):
       
  1003     """delete entity/relation *_permission, need to update schema
       
  1004 
       
  1005     skip the operation if the related type is being deleted
       
  1006     """
       
  1007     if subject in session.transaction_data.get('pendingeids', ()):
       
  1008         return
       
  1009     perm = rtype.split('_', 1)[0]
       
  1010     if session.describe(object)[0] == 'CWGroup':
       
  1011         MemSchemaPermissionCWGroupDel(session, perm, subject, object)
       
  1012     else: # RQLExpression
       
  1013         expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR',
       
  1014                                {'x': object}, 'x')[0][0]
       
  1015         MemSchemaPermissionRQLExpressionDel(session, perm, subject, expr)
       
  1016 
       
  1017 
       
  1018 def rebuild_infered_relations(session, subject, rtype, object):
       
  1019     # registering a schema operation will trigger a call to
       
  1020     # repo.set_schema() on commit which will in turn rebuild
       
  1021     # infered relation definitions
       
  1022     MemSchemaNotifyChanges(session)
       
  1023 
       
  1024 
       
  1025 def _register_schema_hooks(hm):
       
  1026     """register schema related hooks on the hooks manager"""
       
  1027     # schema synchronisation #####################
       
  1028     # before/after add
       
  1029     hm.register_hook(before_add_eetype, 'before_add_entity', 'CWEType')
       
  1030     hm.register_hook(before_add_ertype, 'before_add_entity', 'CWRType')
       
  1031     hm.register_hook(after_add_eetype, 'after_add_entity', 'CWEType')
       
  1032     hm.register_hook(after_add_ertype, 'after_add_entity', 'CWRType')
       
  1033     hm.register_hook(after_add_efrdef, 'after_add_entity', 'CWAttribute')
       
  1034     hm.register_hook(after_add_enfrdef, 'after_add_entity', 'CWRelation')
       
  1035     # before/after update
       
  1036     hm.register_hook(before_update_eetype, 'before_update_entity', 'CWEType')
       
  1037     hm.register_hook(before_update_ertype, 'before_update_entity', 'CWRType')
       
  1038     hm.register_hook(after_update_ertype, 'after_update_entity', 'CWRType')
       
  1039     hm.register_hook(after_update_erdef, 'after_update_entity', 'CWAttribute')
       
  1040     hm.register_hook(after_update_erdef, 'after_update_entity', 'CWRelation')
       
  1041     # before/after delete
       
  1042     hm.register_hook(before_del_eetype, 'before_delete_entity', 'CWEType')
       
  1043     hm.register_hook(after_del_eetype, 'after_delete_entity', 'CWEType')
       
  1044     hm.register_hook(before_del_ertype, 'before_delete_entity', 'CWRType')
       
  1045     hm.register_hook(after_del_relation_type, 'after_delete_relation', 'relation_type')
       
  1046     hm.register_hook(rebuild_infered_relations, 'after_add_relation', 'specializes')
       
  1047     hm.register_hook(rebuild_infered_relations, 'after_delete_relation', 'specializes')
       
  1048     # constraints synchronization hooks
       
  1049     hm.register_hook(after_add_econstraint, 'after_add_entity', 'CWConstraint')
       
  1050     hm.register_hook(after_update_econstraint, 'after_update_entity', 'CWConstraint')
       
  1051     hm.register_hook(before_delete_constrained_by, 'before_delete_relation', 'constrained_by')
       
  1052     hm.register_hook(after_add_constrained_by, 'after_add_relation', 'constrained_by')
       
  1053     # permissions synchronisation ################
       
  1054     for perm in ('read_permission', 'add_permission',
       
  1055                  'delete_permission', 'update_permission'):
       
  1056         hm.register_hook(after_add_permission, 'after_add_relation', perm)
       
  1057         hm.register_hook(before_del_permission, 'before_delete_relation', perm)