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