hooks/syncschema.py
brancholdstable
changeset 6665 90f2f20367bc
parent 6294 a1535abe6ab2
child 6333 e3994fcc21c3
child 6734 ec9a5efdc451
equal deleted inserted replaced
6018:f4d1d5d9ccbb 6665:90f2f20367bc
    31 
    31 
    32 from logilab.common.decorators import clear_cache
    32 from logilab.common.decorators import clear_cache
    33 from logilab.common.testlib import mock_object
    33 from logilab.common.testlib import mock_object
    34 
    34 
    35 from cubicweb import ValidationError
    35 from cubicweb import ValidationError
    36 from cubicweb.selectors import implements
    36 from cubicweb.selectors import is_instance
    37 from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS, display_name
    37 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
       
    38                              CONSTRAINTS, ETYPE_NAME_MAP, display_name)
    38 from cubicweb.server import hook, schemaserial as ss
    39 from cubicweb.server import hook, schemaserial as ss
    39 from cubicweb.server.sqlutils import SQL_PREFIX
    40 from cubicweb.server.sqlutils import SQL_PREFIX
    40 
    41 
    41 
    42 
    42 TYPE_CONVERTER = { # XXX
    43 TYPE_CONVERTER = { # XXX
    49     'Datetime' : unicode,
    50     'Datetime' : unicode,
    50     'Time' : unicode,
    51     'Time' : unicode,
    51     }
    52     }
    52 
    53 
    53 # core entity and relation types which can't be removed
    54 # core entity and relation types which can't be removed
    54 CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup',
    55 CORE_TYPES = BASE_TYPES | SCHEMA_TYPES | META_RTYPES | set(
    55                                   'CWConstraint', 'CWAttribute', 'CWRelation']
    56     ('CWUser', 'CWGroup','login', 'upassword', 'name', 'in_group'))
    56 CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri',
    57 
    57                'login', 'upassword', 'name',
       
    58                'is', 'instanceof', 'owned_by', 'created_by', 'in_group',
       
    59                'relation_type', 'from_entity', 'to_entity',
       
    60                'constrainted_by',
       
    61                'read_permission', 'add_permission',
       
    62                'delete_permission', 'updated_permission',
       
    63                ]
       
    64 
    58 
    65 def get_constraints(session, entity):
    59 def get_constraints(session, entity):
    66     constraints = []
    60     constraints = []
    67     for cstreid in session.transaction_data.get(entity.eid, ()):
    61     for cstreid in session.transaction_data.get(entity.eid, ()):
    68         cstrent = session.entity_from_eid(cstreid)
    62         cstrent = session.entity_from_eid(cstreid)
    78         cw.transaction_data['groupmap'] = gmap = ss.group_mapping(cw)
    72         cw.transaction_data['groupmap'] = gmap = ss.group_mapping(cw)
    79         return gmap
    73         return gmap
    80 
    74 
    81 def add_inline_relation_column(session, etype, rtype):
    75 def add_inline_relation_column(session, etype, rtype):
    82     """add necessary column and index for an inlined relation"""
    76     """add necessary column and index for an inlined relation"""
       
    77     attrkey = '%s.%s' % (etype, rtype)
       
    78     createdattrs = session.transaction_data.setdefault('createdattrs', set())
       
    79     if attrkey in createdattrs:
       
    80         return
       
    81     createdattrs.add(attrkey)
    83     table = SQL_PREFIX + etype
    82     table = SQL_PREFIX + etype
    84     column = SQL_PREFIX + rtype
    83     column = SQL_PREFIX + rtype
    85     try:
    84     try:
    86         session.system_sql(str('ALTER TABLE %s ADD %s integer'
    85         session.system_sql(str('ALTER TABLE %s ADD %s integer'
    87                                % (table, column)), rollback_on_failure=False)
    86                                % (table, column)), rollback_on_failure=False)
    94     # create index before alter table which may expectingly fail during test
    93     # create index before alter table which may expectingly fail during test
    95     # (sqlite) while index creation should never fail (test for index existence
    94     # (sqlite) while index creation should never fail (test for index existence
    96     # is done by the dbhelper)
    95     # is done by the dbhelper)
    97     session.pool.source('system').create_index(session, table, column)
    96     session.pool.source('system').create_index(session, table, column)
    98     session.info('added index on %s(%s)', table, column)
    97     session.info('added index on %s(%s)', table, column)
    99     session.transaction_data.setdefault('createdattrs', []).append(
    98 
   100         '%s.%s' % (etype, rtype))
    99 
       
   100 def insert_rdef_on_subclasses(session, eschema, rschema, rdefdef, props):
       
   101     # XXX 'infered': True/False, not clear actually
       
   102     props.update({'constraints': rdefdef.constraints,
       
   103                   'description': rdefdef.description,
       
   104                   'cardinality': rdefdef.cardinality,
       
   105                   'permissions': rdefdef.get_permissions(),
       
   106                   'order': rdefdef.order,
       
   107                   'infered': False, 'eid': None
       
   108                   })
       
   109     cstrtypemap = ss.cstrtype_mapping(session)
       
   110     groupmap = group_mapping(session)
       
   111     object = rschema.schema.eschema(rdefdef.object)
       
   112     for specialization in eschema.specialized_by(False):
       
   113         if (specialization, rdefdef.object) in rschema.rdefs:
       
   114             continue
       
   115         sperdef = RelationDefinitionSchema(specialization, rschema,
       
   116                                            object, props)
       
   117         ss.execschemarql(session.execute, sperdef,
       
   118                          ss.rdef2rql(sperdef, cstrtypemap, groupmap))
   101 
   119 
   102 
   120 
   103 def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
   121 def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
   104     errors = {}
   122     errors = {}
   105     # don't use getattr(entity, attr), we would get the modified value if any
   123     # don't use getattr(entity, attr), we would get the modified value if any
   113             entity[attr] = newval
   131             entity[attr] = newval
   114     if errors:
   132     if errors:
   115         raise ValidationError(entity.eid, errors)
   133         raise ValidationError(entity.eid, errors)
   116 
   134 
   117 
   135 
       
   136 class SyncSchemaHook(hook.Hook):
       
   137     """abstract class for schema synchronization hooks (in the `syncschema`
       
   138     category)
       
   139     """
       
   140     __abstract__ = True
       
   141     category = 'syncschema'
       
   142 
       
   143 
   118 # operations for low-level database alteration  ################################
   144 # operations for low-level database alteration  ################################
   119 
   145 
   120 class DropTable(hook.Operation):
   146 class DropTable(hook.Operation):
   121     """actually remove a database from the instance's schema"""
   147     """actually remove a database from the instance's schema"""
   122     table = None # make pylint happy
   148     table = None # make pylint happy
   126         if self.table in dropped:
   152         if self.table in dropped:
   127             return # already processed
   153             return # already processed
   128         dropped.add(self.table)
   154         dropped.add(self.table)
   129         self.session.system_sql('DROP TABLE %s' % self.table)
   155         self.session.system_sql('DROP TABLE %s' % self.table)
   130         self.info('dropped table %s', self.table)
   156         self.info('dropped table %s', self.table)
       
   157 
       
   158     # XXX revertprecommit_event
   131 
   159 
   132 
   160 
   133 class DropRelationTable(DropTable):
   161 class DropRelationTable(DropTable):
   134     def __init__(self, session, rtype):
   162     def __init__(self, session, rtype):
   135         super(DropRelationTable, self).__init__(
   163         super(DropRelationTable, self).__init__(
   154         else:
   182         else:
   155             # not supported by sqlite for instance
   183             # not supported by sqlite for instance
   156             self.error('dropping column not supported by the backend, handle '
   184             self.error('dropping column not supported by the backend, handle '
   157                        'it yourself (%s.%s)', table, column)
   185                        'it yourself (%s.%s)', table, column)
   158 
   186 
       
   187     # XXX revertprecommit_event
       
   188 
   159 
   189 
   160 # base operations for in-memory schema synchronization  ########################
   190 # base operations for in-memory schema synchronization  ########################
   161 
   191 
   162 class MemSchemaNotifyChanges(hook.SingleLastOperation):
   192 class MemSchemaNotifyChanges(hook.SingleLastOperation):
   163     """the update schema operation:
   193     """the update schema operation:
   173     def precommit_event(self):
   203     def precommit_event(self):
   174         for eschema in self.session.repo.schema.entities():
   204         for eschema in self.session.repo.schema.entities():
   175             if not eschema.final:
   205             if not eschema.final:
   176                 clear_cache(eschema, 'ordered_relations')
   206                 clear_cache(eschema, 'ordered_relations')
   177 
   207 
   178     def commit_event(self):
   208     def postcommit_event(self):
   179         rebuildinfered = self.session.data.get('rebuild-infered', True)
   209         rebuildinfered = self.session.data.get('rebuild-infered', True)
   180         repo = self.session.repo
   210         repo = self.session.repo
   181         # commit event should not raise error, while set_schema has chances to
   211         # commit event should not raise error, while set_schema has chances to
   182         # do so because it triggers full vreg reloading
   212         # do so because it triggers full vreg reloading
   183         try:
   213         try:
   193         self.precommit_event()
   223         self.precommit_event()
   194 
   224 
   195 
   225 
   196 class MemSchemaOperation(hook.Operation):
   226 class MemSchemaOperation(hook.Operation):
   197     """base class for schema operations"""
   227     """base class for schema operations"""
   198     def __init__(self, session, kobj=None, **kwargs):
   228     def __init__(self, session, **kwargs):
   199         self.kobj = kobj
       
   200         # once Operation.__init__ has been called, event may be triggered, so
       
   201         # do this last !
       
   202         hook.Operation.__init__(self, session, **kwargs)
   229         hook.Operation.__init__(self, session, **kwargs)
   203         # every schema operation is triggering a schema update
   230         # every schema operation is triggering a schema update
   204         MemSchemaNotifyChanges(session)
   231         MemSchemaNotifyChanges(session)
   205 
   232 
   206     def prepare_constraints(self, rdef):
       
   207         # if constraints is already a list, reuse it (we're updating multiple
       
   208         # constraints of the same rdef in the same transactions)
       
   209         if not isinstance(rdef.constraints, list):
       
   210             rdef.constraints = list(rdef.constraints)
       
   211         self.constraints = rdef.constraints
       
   212 
       
   213 
       
   214 class MemSchemaEarlyOperation(MemSchemaOperation):
       
   215     def insert_index(self):
       
   216         """schema operation which are inserted at the begining of the queue
       
   217         (typically to add/remove entity or relation types)
       
   218         """
       
   219         i = -1
       
   220         for i, op in enumerate(self.session.pending_operations):
       
   221             if not isinstance(op, MemSchemaEarlyOperation):
       
   222                 return i
       
   223         return i + 1
       
   224 
       
   225 
   233 
   226 # operations for high-level source database alteration  ########################
   234 # operations for high-level source database alteration  ########################
   227 
   235 
   228 class SourceDbCWETypeRename(hook.Operation):
   236 class CWETypeAddOp(MemSchemaOperation):
       
   237     """after adding a CWEType entity:
       
   238     * add it to the instance's schema
       
   239     * create the necessary table
       
   240     * set creation_date and modification_date by creating the necessary
       
   241       CWAttribute entities
       
   242     * add owned_by relation by creating the necessary CWRelation entity
       
   243     """
       
   244 
       
   245     def precommit_event(self):
       
   246         session = self.session
       
   247         entity = self.entity
       
   248         schema = session.vreg.schema
       
   249         etype = ybo.EntityType(eid=entity.eid, name=entity.name,
       
   250                                description=entity.description)
       
   251         eschema = schema.add_entity_type(etype)
       
   252         # create the necessary table
       
   253         tablesql = y2sql.eschema2sql(session.pool.source('system').dbhelper,
       
   254                                      eschema, prefix=SQL_PREFIX)
       
   255         for sql in tablesql.split(';'):
       
   256             if sql.strip():
       
   257                 session.system_sql(sql)
       
   258         # add meta relations
       
   259         gmap = group_mapping(session)
       
   260         cmap = ss.cstrtype_mapping(session)
       
   261         for rtype in (META_RTYPES - VIRTUAL_RTYPES):
       
   262             rschema = schema[rtype]
       
   263             sampletype = rschema.subjects()[0]
       
   264             desttype = rschema.objects()[0]
       
   265             rdef = copy(rschema.rdef(sampletype, desttype))
       
   266             rdef.subject = mock_object(eid=entity.eid)
       
   267             mock = mock_object(eid=None)
       
   268             ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
       
   269 
       
   270     def revertprecommit_event(self):
       
   271         # revert changes on in memory schema
       
   272         self.session.vreg.schema.del_entity_type(self.entity.name)
       
   273         # revert changes on database
       
   274         self.session.system_sql('DROP TABLE %s%s' % (SQL_PREFIX, self.entity.name))
       
   275 
       
   276 
       
   277 class CWETypeRenameOp(MemSchemaOperation):
   229     """this operation updates physical storage accordingly"""
   278     """this operation updates physical storage accordingly"""
   230     oldname = newname = None # make pylint happy
   279     oldname = newname = None # make pylint happy
   231 
   280 
   232     def precommit_event(self):
   281     def rename(self, oldname, newname):
       
   282         self.session.vreg.schema.rename_entity_type(oldname, newname)
   233         # we need sql to operate physical changes on the system database
   283         # we need sql to operate physical changes on the system database
   234         sqlexec = self.session.system_sql
   284         sqlexec = self.session.system_sql
   235         sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname,
   285         sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, oldname,
   236                                                      SQL_PREFIX, self.newname))
   286                                                      SQL_PREFIX, newname))
   237         self.info('renamed table %s to %s', self.oldname, self.newname)
   287         self.info('renamed table %s to %s', oldname, newname)
   238         sqlexec('UPDATE entities SET type=%s WHERE type=%s',
   288         sqlexec('UPDATE entities SET type=%s WHERE type=%s',
   239                 (self.newname, self.oldname))
   289                 (newname, oldname))
   240         sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
   290         sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
   241                 (self.newname, self.oldname))
   291                 (newname, oldname))
   242 
   292         # XXX transaction records
   243 
   293 
   244 class SourceDbCWRTypeUpdate(hook.Operation):
   294     def precommit_event(self):
       
   295         self.rename(self.oldname, self.newname)
       
   296 
       
   297     def revertprecommit_event(self):
       
   298         self.rename(self.newname, self.oldname)
       
   299 
       
   300 
       
   301 class CWRTypeUpdateOp(MemSchemaOperation):
   245     """actually update some properties of a relation definition"""
   302     """actually update some properties of a relation definition"""
   246     rschema = entity = values = None # make pylint happy
   303     rschema = entity = values = None # make pylint happy
       
   304     oldvalus = None
   247 
   305 
   248     def precommit_event(self):
   306     def precommit_event(self):
   249         rschema = self.rschema
   307         rschema = self.rschema
   250         if rschema.final:
   308         if rschema.final:
   251             return
   309             return # watched changes to final relation type are unexpected
   252         session = self.session
   310         session = self.session
   253         if 'fulltext_container' in self.values:
   311         if 'fulltext_container' in self.values:
   254             for subjtype, objtype in rschema.rdefs:
   312             for subjtype, objtype in rschema.rdefs:
   255                 hook.set_operation(session, 'fti_update_etypes', subjtype,
   313                 hook.set_operation(session, 'fti_update_etypes', subjtype,
   256                                    UpdateFTIndexOp)
   314                                    UpdateFTIndexOp)
   257                 hook.set_operation(session, 'fti_update_etypes', objtype,
   315                 hook.set_operation(session, 'fti_update_etypes', objtype,
   258                                    UpdateFTIndexOp)
   316                                    UpdateFTIndexOp)
       
   317         # update the in-memory schema first
       
   318         self.oldvalues = dict( (attr, getattr(rschema, attr)) for attr in self.values)
       
   319         self.rschema.__dict__.update(self.values)
       
   320         # then make necessary changes to the system source database
   259         if not 'inlined' in self.values:
   321         if not 'inlined' in self.values:
   260             return # nothing to do
   322             return # nothing to do
   261         inlined = self.values['inlined']
   323         inlined = self.values['inlined']
   262         # check in-lining is necessary / possible
   324         # check in-lining is possible when inlined
   263         if inlined:
   325         if inlined:
   264             self.entity.check_inlined_allowed()
   326             self.entity.check_inlined_allowed()
   265         # inlined changed, make necessary physical changes!
   327         # inlined changed, make necessary physical changes!
   266         sqlexec = self.session.system_sql
   328         sqlexec = self.session.system_sql
   267         rtype = rschema.type
   329         rtype = rschema.type
   293                 try:
   355                 try:
   294                     add_inline_relation_column(session, str(etype), rtype)
   356                     add_inline_relation_column(session, str(etype), rtype)
   295                 except Exception, ex:
   357                 except Exception, ex:
   296                     # the column probably already exists. this occurs when the
   358                     # the column probably already exists. this occurs when the
   297                     # entity's type has just been added or if the column has not
   359                     # entity's type has just been added or if the column has not
   298                     # been previously dropped
   360                     # been previously dropped (eg sqlite)
   299                     self.error('error while altering table %s: %s', etype, ex)
   361                     self.error('error while altering table %s: %s', etype, ex)
   300                 # copy existant data.
   362                 # copy existant data.
   301                 # XXX don't use, it's not supported by sqlite (at least at when i tried it)
   363                 # XXX don't use, it's not supported by sqlite (at least at when i tried it)
   302                 #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
   364                 #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
   303                 #        'FROM %(rtype)s_relation '
   365                 #        'FROM %(rtype)s_relation '
   313                     cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
   375                     cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
   314                                        % (table, column, eidcolumn), args)
   376                                        % (table, column, eidcolumn), args)
   315                 # drop existant table
   377                 # drop existant table
   316                 DropRelationTable(session, rtype)
   378                 DropRelationTable(session, rtype)
   317 
   379 
   318 
   380     def revertprecommit_event(self):
   319 class SourceDbCWAttributeAdd(hook.Operation):
   381         # revert changes on in memory schema
       
   382         self.rschema.__dict__.update(self.oldvalues)
       
   383         # XXX revert changes on database
       
   384 
       
   385 
       
   386 class CWAttributeAddOp(MemSchemaOperation):
   320     """an attribute relation (CWAttribute) has been added:
   387     """an attribute relation (CWAttribute) has been added:
   321     * add the necessary column
   388     * add the necessary column
   322     * set default on this column if any and possible
   389     * set default on this column if any and possible
   323     * register an operation to add the relation definition to the
   390     * register an operation to add the relation definition to the
   324       instance's schema on commit
   391       instance's schema on commit
   328     entity = None # make pylint happy
   395     entity = None # make pylint happy
   329 
   396 
   330     def init_rdef(self, **kwargs):
   397     def init_rdef(self, **kwargs):
   331         entity = self.entity
   398         entity = self.entity
   332         fromentity = entity.stype
   399         fromentity = entity.stype
       
   400         rdefdef = self.rdefdef = ybo.RelationDefinition(
       
   401             str(fromentity.name), entity.rtype.name, str(entity.otype.name),
       
   402             description=entity.description, cardinality=entity.cardinality,
       
   403             constraints=get_constraints(self.session, entity),
       
   404             order=entity.ordernum, eid=entity.eid, **kwargs)
       
   405         self.session.vreg.schema.add_relation_def(rdefdef)
   333         self.session.execute('SET X ordernum Y+1 '
   406         self.session.execute('SET X ordernum Y+1 '
   334                              'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
   407                              'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
   335                              'X ordernum >= %(order)s, NOT X eid %(x)s',
   408                              'X ordernum >= %(order)s, NOT X eid %(x)s',
   336                              {'x': entity.eid, 'se': fromentity.eid,
   409                              {'x': entity.eid, 'se': fromentity.eid,
   337                               'order': entity.ordernum or 0})
   410                               'order': entity.ordernum or 0})
   338         subj = str(fromentity.name)
   411         return rdefdef
   339         rtype = entity.rtype.name
       
   340         obj = str(entity.otype.name)
       
   341         constraints = get_constraints(self.session, entity)
       
   342         rdef = ybo.RelationDefinition(subj, rtype, obj,
       
   343                                       description=entity.description,
       
   344                                       cardinality=entity.cardinality,
       
   345                                       constraints=constraints,
       
   346                                       order=entity.ordernum,
       
   347                                       eid=entity.eid,
       
   348                                       **kwargs)
       
   349         MemSchemaRDefAdd(self.session, rdef)
       
   350         return rdef
       
   351 
   412 
   352     def precommit_event(self):
   413     def precommit_event(self):
   353         session = self.session
   414         session = self.session
   354         entity = self.entity
   415         entity = self.entity
   355         # entity.defaultval is a string or None, but we need a correctly typed
   416         # entity.defaultval is a string or None, but we need a correctly typed
   359             default = TYPE_CONVERTER[entity.otype.name](default)
   420             default = TYPE_CONVERTER[entity.otype.name](default)
   360         props = {'default': default,
   421         props = {'default': default,
   361                  'indexed': entity.indexed,
   422                  'indexed': entity.indexed,
   362                  'fulltextindexed': entity.fulltextindexed,
   423                  'fulltextindexed': entity.fulltextindexed,
   363                  'internationalizable': entity.internationalizable}
   424                  'internationalizable': entity.internationalizable}
   364         rdef = self.init_rdef(**props)
   425         # update the in-memory schema first
   365         sysource = session.pool.source('system')
   426         rdefdef = self.init_rdef(**props)
       
   427         # then make necessary changes to the system source database
       
   428         syssource = session.pool.source('system')
   366         attrtype = y2sql.type_from_constraints(
   429         attrtype = y2sql.type_from_constraints(
   367             sysource.dbhelper, rdef.object, rdef.constraints)
   430             syssource.dbhelper, rdefdef.object, rdefdef.constraints)
   368         # XXX should be moved somehow into lgdb: sqlite doesn't support to
   431         # XXX should be moved somehow into lgdb: sqlite doesn't support to
   369         # add a new column with UNIQUE, it should be added after the ALTER TABLE
   432         # add a new column with UNIQUE, it should be added after the ALTER TABLE
   370         # using ADD INDEX
   433         # using ADD INDEX
   371         if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
   434         if syssource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
   372             extra_unique_index = True
   435             extra_unique_index = True
   373             attrtype = attrtype.replace(' UNIQUE', '')
   436             attrtype = attrtype.replace(' UNIQUE', '')
   374         else:
   437         else:
   375             extra_unique_index = False
   438             extra_unique_index = False
   376         # added some str() wrapping query since some backend (eg psycopg) don't
   439         # added some str() wrapping query since some backend (eg psycopg) don't
   377         # allow unicode queries
   440         # allow unicode queries
   378         table = SQL_PREFIX + rdef.subject
   441         table = SQL_PREFIX + rdefdef.subject
   379         column = SQL_PREFIX + rdef.name
   442         column = SQL_PREFIX + rdefdef.name
   380         try:
   443         try:
   381             session.system_sql(str('ALTER TABLE %s ADD %s %s'
   444             session.system_sql(str('ALTER TABLE %s ADD %s %s'
   382                                    % (table, column, attrtype)),
   445                                    % (table, column, attrtype)),
   383                                rollback_on_failure=False)
   446                                rollback_on_failure=False)
   384             self.info('added column %s to table %s', table, column)
   447             self.info('added column %s to table %s', table, column)
   387             # the entity's type has just been added or if the column
   450             # the entity's type has just been added or if the column
   388             # has not been previously dropped
   451             # has not been previously dropped
   389             self.error('error while altering table %s: %s', table, ex)
   452             self.error('error while altering table %s: %s', table, ex)
   390         if extra_unique_index or entity.indexed:
   453         if extra_unique_index or entity.indexed:
   391             try:
   454             try:
   392                 sysource.create_index(session, table, column,
   455                 syssource.create_index(session, table, column,
   393                                       unique=extra_unique_index)
   456                                       unique=extra_unique_index)
   394             except Exception, ex:
   457             except Exception, ex:
   395                 self.error('error while creating index for %s.%s: %s',
   458                 self.error('error while creating index for %s.%s: %s',
   396                            table, column, ex)
   459                            table, column, ex)
   397         # final relations are not infered, propagate
   460         # final relations are not infered, propagate
   398         schema = session.vreg.schema
   461         schema = session.vreg.schema
   399         try:
   462         try:
   400             eschema = schema.eschema(rdef.subject)
   463             eschema = schema.eschema(rdefdef.subject)
   401         except KeyError:
   464         except KeyError:
   402             return # entity type currently being added
   465             return # entity type currently being added
   403         # propagate attribute to children classes
   466         # propagate attribute to children classes
   404         rschema = schema.rschema(rdef.name)
   467         rschema = schema.rschema(rdefdef.name)
   405         # if relation type has been inserted in the same transaction, its final
   468         # if relation type has been inserted in the same transaction, its final
   406         # attribute is still set to False, so we've to ensure it's False
   469         # attribute is still set to False, so we've to ensure it's False
   407         rschema.final = True
   470         rschema.final = True
   408         # XXX 'infered': True/False, not clear actually
   471         insert_rdef_on_subclasses(session, eschema, rschema, rdefdef, props)
   409         props.update({'constraints': rdef.constraints,
       
   410                       'description': rdef.description,
       
   411                       'cardinality': rdef.cardinality,
       
   412                       'constraints': rdef.constraints,
       
   413                       'permissions': rdef.get_permissions(),
       
   414                       'order': rdef.order,
       
   415                       'infered': False, 'eid': None
       
   416                       })
       
   417         cstrtypemap = ss.cstrtype_mapping(session)
       
   418         groupmap = group_mapping(session)
       
   419         object = schema.eschema(rdef.object)
       
   420         for specialization in eschema.specialized_by(False):
       
   421             if (specialization, rdef.object) in rschema.rdefs:
       
   422                 continue
       
   423             sperdef = RelationDefinitionSchema(specialization, rschema,
       
   424                                                object, props)
       
   425             ss.execschemarql(session.execute, sperdef,
       
   426                              ss.rdef2rql(sperdef, cstrtypemap, groupmap))
       
   427         # set default value, using sql for performance and to avoid
   472         # set default value, using sql for performance and to avoid
   428         # modification_date update
   473         # modification_date update
   429         if default:
   474         if default:
   430             session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
   475             session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
   431                                {'default': default})
   476                                {'default': default})
   432 
   477 
   433 
   478     def revertprecommit_event(self):
   434 class SourceDbCWRelationAdd(SourceDbCWAttributeAdd):
   479         # revert changes on in memory schema
       
   480         self.session.vreg.schema.del_relation_def(
       
   481             self.rdefdef.subject, self.rdefdef.name, self.rdefdef.object)
       
   482         # XXX revert changes on database
       
   483 
       
   484 
       
   485 class CWRelationAddOp(CWAttributeAddOp):
   435     """an actual relation has been added:
   486     """an actual relation has been added:
   436     * if this is an inlined relation, add the necessary column
   487 
   437       else if it's the first instance of this relation type, add the
   488     * add the relation definition to the instance's schema
   438       necessary table and set default permissions
   489 
   439     * register an operation to add the relation definition to the
   490     * if this is an inlined relation, add the necessary column else if it's the
   440       instance's schema on commit
   491       first instance of this relation type, add the necessary table and set
       
   492       default permissions
   441 
   493 
   442     constraints are handled by specific hooks
   494     constraints are handled by specific hooks
   443     """
   495     """
   444     entity = None # make pylint happy
   496     entity = None # make pylint happy
   445 
   497 
   446     def precommit_event(self):
   498     def precommit_event(self):
   447         session = self.session
   499         session = self.session
   448         entity = self.entity
   500         entity = self.entity
   449         rdef = self.init_rdef(composite=entity.composite)
   501         # update the in-memory schema first
       
   502         rdefdef = self.init_rdef(composite=entity.composite)
       
   503         # then make necessary changes to the system source database
   450         schema = session.vreg.schema
   504         schema = session.vreg.schema
   451         rtype = rdef.name
   505         rtype = rdefdef.name
   452         rschema = schema.rschema(rtype)
   506         rschema = schema.rschema(rtype)
   453         # this have to be done before permissions setting
   507         # this have to be done before permissions setting
   454         if rschema.inlined:
   508         if rschema.inlined:
   455             # need to add a column if the relation is inlined and if this is the
   509             # need to add a column if the relation is inlined and if this is the
   456             # first occurence of "Subject relation Something" whatever Something
   510             # first occurence of "Subject relation Something" whatever Something
   457             # and if it has not been added during other event of the same
   511             if len(rschema.objects(rdefdef.subject)) == 1:
   458             # transaction
   512                 add_inline_relation_column(session, rdefdef.subject, rtype)
   459             key = '%s.%s' % (rdef.subject, rtype)
   513             eschema = schema[rdefdef.subject]
   460             try:
   514             insert_rdef_on_subclasses(session, eschema, rschema, rdefdef,
   461                 alreadythere = bool(rschema.objects(rdef.subject))
   515                                       {'composite': entity.composite})
   462             except KeyError:
       
   463                 alreadythere = False
       
   464             if not (alreadythere or
       
   465                     key in session.transaction_data.get('createdattrs', ())):
       
   466                 add_inline_relation_column(session, rdef.subject, rtype)
       
   467         else:
   516         else:
       
   517             if rschema.symmetric:
       
   518                 # for symmetric relations, rdefs will store relation definitions
       
   519                 # in both ways (i.e. (subj -> obj) and (obj -> subj))
       
   520                 relation_already_defined = len(rschema.rdefs) > 2
       
   521             else:
       
   522                 relation_already_defined = len(rschema.rdefs) > 1
   468             # need to create the relation if no relation definition in the
   523             # need to create the relation if no relation definition in the
   469             # schema and if it has not been added during other event of the same
   524             # schema and if it has not been added during other event of the same
   470             # transaction
   525             # transaction
   471             if not (rschema.subjects() or
   526             if not (relation_already_defined or
   472                     rtype in session.transaction_data.get('createdtables', ())):
   527                     rtype in session.transaction_data.get('createdtables', ())):
   473                 try:
   528                 rschema = schema.rschema(rtype)
   474                     rschema = schema.rschema(rtype)
       
   475                     tablesql = y2sql.rschema2sql(rschema)
       
   476                 except KeyError:
       
   477                     # fake we add it to the schema now to get a correctly
       
   478                     # initialized schema but remove it before doing anything
       
   479                     # more dangerous...
       
   480                     rschema = schema.add_relation_type(rdef)
       
   481                     tablesql = y2sql.rschema2sql(rschema)
       
   482                     schema.del_relation_type(rtype)
       
   483                 # create the necessary table
   529                 # create the necessary table
   484                 for sql in tablesql.split(';'):
   530                 for sql in y2sql.rschema2sql(rschema).split(';'):
   485                     if sql.strip():
   531                     if sql.strip():
   486                         session.system_sql(sql)
   532                         session.system_sql(sql)
   487                 session.transaction_data.setdefault('createdtables', []).append(
   533                 session.transaction_data.setdefault('createdtables', []).append(
   488                     rtype)
   534                     rtype)
   489 
   535 
   490 
   536     # XXX revertprecommit_event
   491 class SourceDbRDefUpdate(hook.Operation):
   537 
       
   538 
       
   539 class RDefDelOp(MemSchemaOperation):
       
   540     """an actual relation has been removed"""
       
   541     rdef = None # make pylint happy
       
   542 
       
   543     def precommit_event(self):
       
   544         session = self.session
       
   545         rdef = self.rdef
       
   546         rschema = rdef.rtype
       
   547         # make necessary changes to the system source database first
       
   548         rdeftype = rschema.final and 'CWAttribute' or 'CWRelation'
       
   549         execute = session.execute
       
   550         rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
       
   551                        'R eid %%(x)s' % rdeftype, {'x': rschema.eid})
       
   552         lastrel = rset[0][0] == 0
       
   553         # we have to update physical schema systematically for final and inlined
       
   554         # relations, but only if it's the last instance for this relation type
       
   555         # for other relations
       
   556         if (rschema.final or rschema.inlined):
       
   557             rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
       
   558                            'R eid %%(r)s, X from_entity E, E eid %%(e)s'
       
   559                            % rdeftype,
       
   560                            {'r': rschema.eid, 'e': rdef.subject.eid})
       
   561             if rset[0][0] == 0 and not session.deleted_in_transaction(rdef.subject.eid):
       
   562                 ptypes = session.transaction_data.setdefault('pendingrtypes', set())
       
   563                 ptypes.add(rschema.type)
       
   564                 DropColumn(session, table=SQL_PREFIX + str(rdef.subject),
       
   565                            column=SQL_PREFIX + str(rschema))
       
   566         elif lastrel:
       
   567             DropRelationTable(session, str(rschema))
       
   568         # then update the in-memory schema
       
   569         rschema.del_relation_def(rdef.subject, rdef.object)
       
   570         # if this is the last relation definition of this type, drop associated
       
   571         # relation type
       
   572         if lastrel and not session.deleted_in_transaction(rschema.eid):
       
   573             execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rschema.eid})
       
   574 
       
   575     def revertprecommit_event(self):
       
   576         # revert changes on in memory schema
       
   577         #
       
   578         # Note: add_relation_def takes a RelationDefinition, not a
       
   579         # RelationDefinitionSchema, needs to fake it
       
   580         self.rdef.name = str(self.rdef.rtype)
       
   581         self.session.vreg.schema.add_relation_def(self.rdef)
       
   582 
       
   583 
       
   584 
       
   585 class RDefUpdateOp(MemSchemaOperation):
   492     """actually update some properties of a relation definition"""
   586     """actually update some properties of a relation definition"""
   493     rschema = values = None # make pylint happy
   587     rschema = rdefkey = values = None # make pylint happy
       
   588     rdef = oldvalues = None
       
   589     indexed_changed = null_allowed_changed = False
   494 
   590 
   495     def precommit_event(self):
   591     def precommit_event(self):
   496         session = self.session
   592         session = self.session
   497         etype = self.kobj[0]
   593         rdef = self.rdef = self.rschema.rdefs[self.rdefkey]
   498         table = SQL_PREFIX + etype
   594         # update the in-memory schema first
   499         column = SQL_PREFIX + self.rschema.type
   595         self.oldvalues = dict( (attr, getattr(rdef, attr)) for attr in self.values)
       
   596         rdef.update(self.values)
       
   597         # then make necessary changes to the system source database
       
   598         syssource = session.pool.source('system')
   500         if 'indexed' in self.values:
   599         if 'indexed' in self.values:
   501             sysource = session.pool.source('system')
   600             syssource.update_rdef_indexed(session, rdef)
   502             if self.values['indexed']:
   601             self.indexed_changed = True
   503                 sysource.create_index(session, table, column)
   602         if 'cardinality' in self.values and (rdef.rtype.final or
   504             else:
   603                                              rdef.rtype.inlined) \
   505                 sysource.drop_index(session, table, column)
   604               and self.values['cardinality'][0] != self.oldvalues['cardinality'][0]:
   506         if 'cardinality' in self.values and self.rschema.final:
   605             syssource.update_rdef_null_allowed(self.session, rdef)
   507             syssource = session.pool.source('system')
   606             self.null_allowed_changed = True
   508             if not syssource.dbhelper.alter_column_support:
       
   509                 # not supported (and NOT NULL not set by yams in that case, so
       
   510                 # no worry) XXX (syt) then should we set NOT NULL below ??
       
   511                 return
       
   512             atype = self.rschema.objects(etype)[0]
       
   513             constraints = self.rschema.rdef(etype, atype).constraints
       
   514             coltype = y2sql.type_from_constraints(syssource.dbhelper, atype, constraints,
       
   515                                                   creating=False)
       
   516             # XXX check self.values['cardinality'][0] actually changed?
       
   517             syssource.set_null_allowed(self.session, table, column, coltype,
       
   518                                        self.values['cardinality'][0] != '1')
       
   519         if 'fulltextindexed' in self.values:
   607         if 'fulltextindexed' in self.values:
   520             hook.set_operation(session, 'fti_update_etypes', etype,
   608             hook.set_operation(session, 'fti_update_etypes', rdef.subject,
   521                                UpdateFTIndexOp)
   609                                UpdateFTIndexOp)
   522 
   610 
   523 
   611     def revertprecommit_event(self):
   524 class SourceDbCWConstraintAdd(hook.Operation):
   612         if self.rdef is None:
       
   613             return
       
   614         # revert changes on in memory schema
       
   615         self.rdef.update(self.oldvalues)
       
   616         # revert changes on database
       
   617         syssource = self.session.pool.source('system')
       
   618         if self.indexed_changed:
       
   619             syssource.update_rdef_indexed(self.session, self.rdef)
       
   620         if self.null_allowed_changed:
       
   621             syssource.update_rdef_null_allowed(self.session, self.rdef)
       
   622 
       
   623 
       
   624 def _set_modifiable_constraints(rdef):
       
   625     # for proper in-place modification of in-memory schema: if rdef.constraints
       
   626     # is already a list, reuse it (we're updating multiple constraints of the
       
   627     # same rdef in the same transactions)
       
   628     if not isinstance(rdef.constraints, list):
       
   629         rdef.constraints = list(rdef.constraints)
       
   630 
       
   631 
       
   632 class CWConstraintDelOp(MemSchemaOperation):
       
   633     """actually remove a constraint of a relation definition"""
       
   634     rdef = oldcstr = newcstr = None # make pylint happy
       
   635     size_cstr_changed = unique_changed = False
       
   636 
       
   637     def precommit_event(self):
       
   638         session = self.session
       
   639         rdef = self.rdef
       
   640         # in-place modification of in-memory schema first
       
   641         _set_modifiable_constraints(rdef)
       
   642         rdef.constraints.remove(self.oldcstr)
       
   643         # then update database: alter the physical schema on size/unique
       
   644         # constraint changes
       
   645         syssource = session.pool.source('system')
       
   646         cstrtype = self.oldcstr.type()
       
   647         if cstrtype == 'SizeConstraint':
       
   648             syssource.update_rdef_column(session, rdef)
       
   649             self.size_cstr_changed = True
       
   650         elif cstrtype == 'UniqueConstraint':
       
   651             syssource.update_rdef_unique(session, rdef)
       
   652             self.unique_changed = True
       
   653 
       
   654     def revertprecommit_event(self):
       
   655         # revert changes on in memory schema
       
   656         if self.newcstr is not None:
       
   657             self.rdef.constraints.remove(self.newcstr)
       
   658         if self.oldcstr is not None:
       
   659             self.rdef.constraints.append(self.oldcstr)
       
   660         # revert changes on database
       
   661         syssource = self.session.pool.source('system')
       
   662         if self.size_cstr_changed:
       
   663             syssource.update_rdef_column(self.session, self.rdef)
       
   664         if self.unique_changed:
       
   665             syssource.update_rdef_unique(self.session, self.rdef)
       
   666 
       
   667 
       
   668 class CWConstraintAddOp(CWConstraintDelOp):
   525     """actually update constraint of a relation definition"""
   669     """actually update constraint of a relation definition"""
   526     entity = None # make pylint happy
   670     entity = None # make pylint happy
   527     cancelled = False
   671 
   528 
   672     def precommit_event(self):
   529     def precommit_event(self):
       
   530         rdef = self.entity.reverse_constrained_by[0]
       
   531         session = self.session
   673         session = self.session
       
   674         rdefentity = self.entity.reverse_constrained_by[0]
   532         # when the relation is added in the same transaction, the constraint
   675         # when the relation is added in the same transaction, the constraint
   533         # object is created by the operation adding the attribute or relation,
   676         # object is created by the operation adding the attribute or relation,
   534         # so there is nothing to do here
   677         # so there is nothing to do here
   535         if session.added_in_transaction(rdef.eid):
   678         if session.added_in_transaction(rdefentity.eid):
   536             return
   679             return
   537         rdefschema = session.vreg.schema.schema_by_eid(rdef.eid)
   680         rdef = self.rdef = session.vreg.schema.schema_by_eid(rdefentity.eid)
   538         subjtype, rtype, objtype = rdefschema.as_triple()
       
   539         cstrtype = self.entity.type
   681         cstrtype = self.entity.type
   540         oldcstr = rtype.rdef(subjtype, objtype).constraint_by_type(cstrtype)
   682         oldcstr = self.oldcstr = rdef.constraint_by_type(cstrtype)
   541         newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
   683         newcstr = self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
   542         table = SQL_PREFIX + str(subjtype)
   684         # in-place modification of in-memory schema first
   543         column = SQL_PREFIX + str(rtype)
   685         _set_modifiable_constraints(rdef)
   544         # alter the physical schema on size constraint changes
   686         newcstr.eid = self.entity.eid
   545         if newcstr.type() == 'SizeConstraint' and (
   687         if oldcstr is not None:
   546             oldcstr is None or oldcstr.max != newcstr.max):
   688             rdef.constraints.remove(oldcstr)
   547             syssource = self.session.pool.source('system')
   689         rdef.constraints.append(newcstr)
   548             card = rtype.rdef(subjtype, objtype).cardinality
   690         # then update database: alter the physical schema on size/unique
   549             coltype = y2sql.type_from_constraints(syssource.dbhelper, objtype,
   691         # constraint changes
   550                                                   [newcstr], creating=False)
   692         syssource = session.pool.source('system')
   551             try:
   693         if cstrtype == 'SizeConstraint' and (oldcstr is None or
   552                 syssource.change_col_type(session, table, column, coltype, card[0] != '1')
   694                                              oldcstr.max != newcstr.max):
   553                 self.info('altered column %s of table %s: now %s',
   695             syssource.update_rdef_column(session, rdef)
   554                           column, table, coltype)
   696             self.size_cstr_changed = True
   555             except Exception, ex:
       
   556                 # not supported by sqlite for instance
       
   557                 self.error('error while altering table %s: %s', table, ex)
       
   558         elif cstrtype == 'UniqueConstraint' and oldcstr is None:
   697         elif cstrtype == 'UniqueConstraint' and oldcstr is None:
   559             session.pool.source('system').create_index(
   698             syssource.update_rdef_unique(session, rdef)
   560                 self.session, table, column, unique=True)
   699             self.unique_changed = True
   561 
   700 
   562 
   701 class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
   563 class SourceDbCWConstraintDel(hook.Operation):
   702     entity = None # make pylint happy
   564     """actually remove a constraint of a relation definition"""
   703     def precommit_event(self):
   565     rtype = subjtype = None # make pylint happy
   704         session = self.session
   566 
   705         prefix = SQL_PREFIX
   567     def precommit_event(self):
   706         table = '%s%s' % (prefix, self.entity.constraint_of[0].name)
   568         cstrtype = self.cstr.type()
   707         cols = ['%s%s' % (prefix, r.rtype.name)
   569         table = SQL_PREFIX + str(self.rdef.subject)
   708                 for r in self.entity.relations]
   570         column = SQL_PREFIX + str(self.rdef.rtype)
   709         dbhelper= session.pool.source('system').dbhelper
   571         # alter the physical schema on size/unique constraint changes
   710         sqls = dbhelper.sqls_create_multicol_unique_index(table, cols)
   572         if cstrtype == 'SizeConstraint':
   711         for sql in sqls:
   573             syssource = self.session.pool.source('system')
   712             session.system_sql(sql)
   574             coltype = y2sql.type_from_constraints(syssource.dbhelper,
   713 
   575                                                   self.rdef.object, [],
   714     # XXX revertprecommit_event
   576                                                   creating=False)
   715 
   577             try:
   716     def postcommit_event(self):
   578                 syssource.change_col_type(session, table, column, coltype,
   717         eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid)
   579                                           self.rdef.cardinality[0] != '1')
   718         attrs = [r.rtype.name for r in self.entity.relations]
   580                 self.info('altered column %s of table %s: now %s',
   719         eschema._unique_together.append(attrs)
   581                           column, table, coltype)
   720 
   582             except Exception, ex:
   721 class CWUniqueTogetherConstraintDelOp(MemSchemaOperation):
   583                 # not supported by sqlite for instance
   722     entity = oldcstr = None # for pylint
   584                 self.error('error while altering table %s: %s', table, ex)
   723     cols = [] # for pylint
   585         elif cstrtype == 'UniqueConstraint':
   724     def precommit_event(self):
   586             self.session.pool.source('system').drop_index(
   725         session = self.session
   587                 self.session, table, column, unique=True)
   726         prefix = SQL_PREFIX
   588 
   727         table = '%s%s' % (prefix, self.entity.type)
       
   728         dbhelper= session.pool.source('system').dbhelper
       
   729         cols = ['%s%s' % (prefix, c) for c in self.cols]
       
   730         sqls = dbhelper.sqls_drop_multicol_unique_index(table, cols)
       
   731         for sql in sqls:
       
   732             session.system_sql(sql)
       
   733 
       
   734     # XXX revertprecommit_event
       
   735 
       
   736     def postcommit_event(self):
       
   737         eschema = self.session.vreg.schema.schema_by_eid(self.entity.eid)
       
   738         cols = set(self.cols)
       
   739         unique_together = [ut for ut in eschema._unique_together
       
   740                            if set(ut) != cols]
       
   741         eschema._unique_together = unique_together
   589 
   742 
   590 # operations for in-memory schema synchronization  #############################
   743 # operations for in-memory schema synchronization  #############################
   591 
       
   592 class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
       
   593     """actually add the entity type to the instance's schema"""
       
   594     eid = None # make pylint happy
       
   595     def commit_event(self):
       
   596         self.session.vreg.schema.add_entity_type(self.kobj)
       
   597 
       
   598 
       
   599 class MemSchemaCWETypeRename(MemSchemaOperation):
       
   600     """this operation updates physical storage accordingly"""
       
   601     oldname = newname = None # make pylint happy
       
   602 
       
   603     def commit_event(self):
       
   604         self.session.vreg.schema.rename_entity_type(self.oldname, self.newname)
       
   605 
       
   606 
   744 
   607 class MemSchemaCWETypeDel(MemSchemaOperation):
   745 class MemSchemaCWETypeDel(MemSchemaOperation):
   608     """actually remove the entity type from the instance's schema"""
   746     """actually remove the entity type from the instance's schema"""
   609     def commit_event(self):
   747     def postcommit_event(self):
       
   748         # del_entity_type also removes entity's relations
       
   749         self.session.vreg.schema.del_entity_type(self.etype)
       
   750 
       
   751 
       
   752 class MemSchemaCWRTypeAdd(MemSchemaOperation):
       
   753     """actually add the relation type to the instance's schema"""
       
   754     def precommit_event(self):
       
   755         self.session.vreg.schema.add_relation_type(self.rtypedef)
       
   756 
       
   757     def revertprecommit_event(self):
       
   758         self.session.vreg.schema.del_relation_type(self.rtypedef.name)
       
   759 
       
   760 
       
   761 class MemSchemaCWRTypeDel(MemSchemaOperation):
       
   762     """actually remove the relation type from the instance's schema"""
       
   763     def postcommit_event(self):
   610         try:
   764         try:
   611             # del_entity_type also removes entity's relations
   765             self.session.vreg.schema.del_relation_type(self.rtype)
   612             self.session.vreg.schema.del_entity_type(self.kobj)
       
   613         except KeyError:
   766         except KeyError:
   614             # s/o entity type have already been deleted
   767             # s/o entity type have already been deleted
   615             pass
   768             pass
   616 
   769 
   617 
   770 
   618 class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
       
   619     """actually add the relation type to the instance's schema"""
       
   620     eid = None # make pylint happy
       
   621     def commit_event(self):
       
   622         self.session.vreg.schema.add_relation_type(self.kobj)
       
   623 
       
   624 
       
   625 class MemSchemaCWRTypeUpdate(MemSchemaOperation):
       
   626     """actually update some properties of a relation definition"""
       
   627     rschema = values = None # make pylint happy
       
   628 
       
   629     def commit_event(self):
       
   630         # structure should be clean, not need to remove entity's relations
       
   631         # at this point
       
   632         self.rschema.__dict__.update(self.values)
       
   633 
       
   634 
       
   635 class MemSchemaCWRTypeDel(MemSchemaOperation):
       
   636     """actually remove the relation type from the instance's schema"""
       
   637     def commit_event(self):
       
   638         try:
       
   639             self.session.vreg.schema.del_relation_type(self.kobj)
       
   640         except KeyError:
       
   641             # s/o entity type have already been deleted
       
   642             pass
       
   643 
       
   644 
       
   645 class MemSchemaRDefAdd(MemSchemaEarlyOperation):
       
   646     """actually add the attribute relation definition to the instance's
       
   647     schema
       
   648     """
       
   649     def commit_event(self):
       
   650         self.session.vreg.schema.add_relation_def(self.kobj)
       
   651 
       
   652 
       
   653 class MemSchemaRDefUpdate(MemSchemaOperation):
       
   654     """actually update some properties of a relation definition"""
       
   655     rschema = values = None # make pylint happy
       
   656 
       
   657     def commit_event(self):
       
   658         # structure should be clean, not need to remove entity's relations
       
   659         # at this point
       
   660         self.rschema.rdefs[self.kobj].update(self.values)
       
   661 
       
   662 
       
   663 class MemSchemaRDefDel(MemSchemaOperation):
       
   664     """actually remove the relation definition from the instance's schema"""
       
   665     def commit_event(self):
       
   666         subjtype, rtype, objtype = self.kobj
       
   667         try:
       
   668             self.session.vreg.schema.del_relation_def(subjtype, rtype, objtype)
       
   669         except KeyError:
       
   670             # relation type may have been already deleted
       
   671             pass
       
   672 
       
   673 
       
   674 class MemSchemaCWConstraintAdd(MemSchemaOperation):
       
   675     """actually update constraint of a relation definition
       
   676 
       
   677     has to be called before SourceDbCWConstraintAdd
       
   678     """
       
   679     cancelled = False
       
   680 
       
   681     def precommit_event(self):
       
   682         rdef = self.entity.reverse_constrained_by[0]
       
   683         # when the relation is added in the same transaction, the constraint
       
   684         # object is created by the operation adding the attribute or relation,
       
   685         # so there is nothing to do here
       
   686         if self.session.added_in_transaction(rdef.eid):
       
   687             self.cancelled = True
       
   688             return
       
   689         rdef = self.session.vreg.schema.schema_by_eid(rdef.eid)
       
   690         self.prepare_constraints(rdef)
       
   691         cstrtype = self.entity.type
       
   692         self.cstr = rdef.constraint_by_type(cstrtype)
       
   693         self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
       
   694         self.newcstr.eid = self.entity.eid
       
   695 
       
   696     def commit_event(self):
       
   697         if self.cancelled:
       
   698             return
       
   699         # in-place modification
       
   700         if not self.cstr is None:
       
   701             self.constraints.remove(self.cstr)
       
   702         self.constraints.append(self.newcstr)
       
   703 
       
   704 
       
   705 class MemSchemaCWConstraintDel(MemSchemaOperation):
       
   706     """actually remove a constraint of a relation definition
       
   707 
       
   708     has to be called before SourceDbCWConstraintDel
       
   709     """
       
   710     rtype = subjtype = objtype = None # make pylint happy
       
   711     def precommit_event(self):
       
   712         self.prepare_constraints(self.rdef)
       
   713 
       
   714     def commit_event(self):
       
   715         self.constraints.remove(self.cstr)
       
   716 
       
   717 
       
   718 class MemSchemaPermissionAdd(MemSchemaOperation):
   771 class MemSchemaPermissionAdd(MemSchemaOperation):
   719     """synchronize schema when a *_permission relation has been added on a group
   772     """synchronize schema when a *_permission relation has been added on a group
   720     """
   773     """
   721 
   774 
   722     def commit_event(self):
   775     def precommit_event(self):
   723         """the observed connections pool has been commited"""
   776         """the observed connections pool has been commited"""
   724         try:
   777         try:
   725             erschema = self.session.vreg.schema.schema_by_eid(self.eid)
   778             erschema = self.session.vreg.schema.schema_by_eid(self.eid)
   726         except KeyError:
   779         except KeyError:
   727             # duh, schema not found, log error and skip operation
   780             # duh, schema not found, log error and skip operation
   738                          perm, self.action, erschema)
   791                          perm, self.action, erschema)
   739         except ValueError:
   792         except ValueError:
   740             perms.append(perm)
   793             perms.append(perm)
   741             erschema.set_action_permissions(self.action, perms)
   794             erschema.set_action_permissions(self.action, perms)
   742 
   795 
       
   796     # XXX revertprecommit_event
       
   797 
   743 
   798 
   744 class MemSchemaPermissionDel(MemSchemaPermissionAdd):
   799 class MemSchemaPermissionDel(MemSchemaPermissionAdd):
   745     """synchronize schema when a *_permission relation has been deleted from a
   800     """synchronize schema when a *_permission relation has been deleted from a
   746     group
   801     group
   747     """
   802     """
   748 
   803 
   749     def commit_event(self):
   804     def precommit_event(self):
   750         """the observed connections pool has been commited"""
   805         """the observed connections pool has been commited"""
   751         try:
   806         try:
   752             erschema = self.session.vreg.schema.schema_by_eid(self.eid)
   807             erschema = self.session.vreg.schema.schema_by_eid(self.eid)
   753         except KeyError:
   808         except KeyError:
   754             # duh, schema not found, log error and skip operation
   809             # duh, schema not found, log error and skip operation
   769             erschema.set_action_permissions(self.action, perms)
   824             erschema.set_action_permissions(self.action, perms)
   770         except ValueError:
   825         except ValueError:
   771             self.error('can\'t remove permission %s for %s on %s',
   826             self.error('can\'t remove permission %s for %s on %s',
   772                        perm, self.action, erschema)
   827                        perm, self.action, erschema)
   773 
   828 
       
   829     # XXX revertprecommit_event
       
   830 
   774 
   831 
   775 class MemSchemaSpecializesAdd(MemSchemaOperation):
   832 class MemSchemaSpecializesAdd(MemSchemaOperation):
   776 
   833 
   777     def commit_event(self):
   834     def precommit_event(self):
   778         eschema = self.session.vreg.schema.schema_by_eid(self.etypeeid)
   835         eschema = self.session.vreg.schema.schema_by_eid(self.etypeeid)
   779         parenteschema = self.session.vreg.schema.schema_by_eid(self.parentetypeeid)
   836         parenteschema = self.session.vreg.schema.schema_by_eid(self.parentetypeeid)
   780         eschema._specialized_type = parenteschema.type
   837         eschema._specialized_type = parenteschema.type
   781         parenteschema._specialized_by.append(eschema.type)
   838         parenteschema._specialized_by.append(eschema.type)
   782 
   839 
       
   840     # XXX revertprecommit_event
       
   841 
   783 
   842 
   784 class MemSchemaSpecializesDel(MemSchemaOperation):
   843 class MemSchemaSpecializesDel(MemSchemaOperation):
   785 
   844 
   786     def commit_event(self):
   845     def precommit_event(self):
   787         try:
   846         try:
   788             eschema = self.session.vreg.schema.schema_by_eid(self.etypeeid)
   847             eschema = self.session.vreg.schema.schema_by_eid(self.etypeeid)
   789             parenteschema = self.session.vreg.schema.schema_by_eid(self.parentetypeeid)
   848             parenteschema = self.session.vreg.schema.schema_by_eid(self.parentetypeeid)
   790         except KeyError:
   849         except KeyError:
   791             # etype removed, nothing to do
   850             # etype removed, nothing to do
   792             return
   851             return
   793         eschema._specialized_type = None
   852         eschema._specialized_type = None
   794         parenteschema._specialized_by.remove(eschema.type)
   853         parenteschema._specialized_by.remove(eschema.type)
   795 
   854 
   796 
   855     # XXX revertprecommit_event
   797 class SyncSchemaHook(hook.Hook):
       
   798     __abstract__ = True
       
   799     category = 'syncschema'
       
   800 
   856 
   801 
   857 
   802 # CWEType hooks ################################################################
   858 # CWEType hooks ################################################################
   803 
   859 
   804 class DelCWETypeHook(SyncSchemaHook):
   860 class DelCWETypeHook(SyncSchemaHook):
   806     * check that we don't remove a core entity type
   862     * check that we don't remove a core entity type
   807     * cascade to delete related CWAttribute and CWRelation entities
   863     * cascade to delete related CWAttribute and CWRelation entities
   808     * instantiate an operation to delete the entity type on commit
   864     * instantiate an operation to delete the entity type on commit
   809     """
   865     """
   810     __regid__ = 'syncdelcwetype'
   866     __regid__ = 'syncdelcwetype'
   811     __select__ = SyncSchemaHook.__select__ & implements('CWEType')
   867     __select__ = SyncSchemaHook.__select__ & is_instance('CWEType')
   812     events = ('before_delete_entity',)
   868     events = ('before_delete_entity',)
   813 
   869 
   814     def __call__(self):
   870     def __call__(self):
   815         # final entities can't be deleted, don't care about that
   871         # final entities can't be deleted, don't care about that
   816         name = self.entity.name
   872         name = self.entity.name
   817         if name in CORE_ETYPES:
   873         if name in CORE_TYPES:
   818             raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
   874             raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
   819         # delete every entities of this type
   875         # delete every entities of this type
   820         self._cw.execute('DELETE %s X' % name)
   876         if not name in ETYPE_NAME_MAP:
       
   877             self._cw.execute('DELETE %s X' % name)
       
   878             MemSchemaCWETypeDel(self._cw, etype=name)
   821         DropTable(self._cw, table=SQL_PREFIX + name)
   879         DropTable(self._cw, table=SQL_PREFIX + name)
   822         MemSchemaCWETypeDel(self._cw, name)
       
   823 
   880 
   824 
   881 
   825 class AfterDelCWETypeHook(DelCWETypeHook):
   882 class AfterDelCWETypeHook(DelCWETypeHook):
   826     __regid__ = 'wfcleanup'
   883     __regid__ = 'wfcleanup'
   827     events = ('after_delete_entity',)
   884     events = ('after_delete_entity',)
   845 
   902 
   846     def __call__(self):
   903     def __call__(self):
   847         entity = self.entity
   904         entity = self.entity
   848         if entity.get('final'):
   905         if entity.get('final'):
   849             return
   906             return
   850         schema = self._cw.vreg.schema
   907         CWETypeAddOp(self._cw, entity=entity)
   851         name = entity['name']
       
   852         etype = ybo.EntityType(name=name, description=entity.get('description'),
       
   853                                meta=entity.get('meta')) # don't care about final
       
   854         # fake we add it to the schema now to get a correctly initialized schema
       
   855         # but remove it before doing anything more dangerous...
       
   856         schema = self._cw.vreg.schema
       
   857         eschema = schema.add_entity_type(etype)
       
   858         # generate table sql and rql to add metadata
       
   859         tablesql = y2sql.eschema2sql(self._cw.pool.source('system').dbhelper,
       
   860                                      eschema, prefix=SQL_PREFIX)
       
   861         rdefrqls = []
       
   862         gmap = group_mapping(self._cw)
       
   863         cmap = ss.cstrtype_mapping(self._cw)
       
   864         for rtype in (META_RTYPES - VIRTUAL_RTYPES):
       
   865             rschema = schema[rtype]
       
   866             sampletype = rschema.subjects()[0]
       
   867             desttype = rschema.objects()[0]
       
   868             rdef = copy(rschema.rdef(sampletype, desttype))
       
   869             rdef.subject = mock_object(eid=entity.eid)
       
   870             mock = mock_object(eid=None)
       
   871             rdefrqls.append( (mock, tuple(ss.rdef2rql(rdef, cmap, gmap))) )
       
   872         # now remove it !
       
   873         schema.del_entity_type(name)
       
   874         # create the necessary table
       
   875         for sql in tablesql.split(';'):
       
   876             if sql.strip():
       
   877                 self._cw.system_sql(sql)
       
   878         # register operation to modify the schema on commit
       
   879         # this have to be done before adding other relations definitions
       
   880         # or permission settings
       
   881         etype.eid = entity.eid
       
   882         MemSchemaCWETypeAdd(self._cw, etype)
       
   883         # add meta relations
       
   884         for rdef, relrqls in rdefrqls:
       
   885             ss.execschemarql(self._cw.execute, rdef, relrqls)
       
   886 
   908 
   887 
   909 
   888 class BeforeUpdateCWETypeHook(DelCWETypeHook):
   910 class BeforeUpdateCWETypeHook(DelCWETypeHook):
   889     """check name change, handle final"""
   911     """check name change, handle final"""
   890     __regid__ = 'syncupdatecwetype'
   912     __regid__ = 'syncupdatecwetype'
   893     def __call__(self):
   915     def __call__(self):
   894         entity = self.entity
   916         entity = self.entity
   895         check_valid_changes(self._cw, entity, ro_attrs=('final',))
   917         check_valid_changes(self._cw, entity, ro_attrs=('final',))
   896         # don't use getattr(entity, attr), we would get the modified value if any
   918         # don't use getattr(entity, attr), we would get the modified value if any
   897         if 'name' in entity.edited_attributes:
   919         if 'name' in entity.edited_attributes:
   898             newname = entity.pop('name')
   920             oldname, newname = hook.entity_oldnewvalue(entity, 'name')
   899             oldname = entity.name
       
   900             if newname.lower() != oldname.lower():
   921             if newname.lower() != oldname.lower():
   901                 SourceDbCWETypeRename(self._cw, oldname=oldname, newname=newname)
   922                 CWETypeRenameOp(self._cw, oldname=oldname, newname=newname)
   902                 MemSchemaCWETypeRename(self._cw, oldname=oldname, newname=newname)
       
   903             entity['name'] = newname
       
   904 
   923 
   905 
   924 
   906 # CWRType hooks ################################################################
   925 # CWRType hooks ################################################################
   907 
   926 
   908 class DelCWRTypeHook(SyncSchemaHook):
   927 class DelCWRTypeHook(SyncSchemaHook):
   910     * check that we don't remove a core relation type
   929     * check that we don't remove a core relation type
   911     * cascade to delete related CWAttribute and CWRelation entities
   930     * cascade to delete related CWAttribute and CWRelation entities
   912     * instantiate an operation to delete the relation type on commit
   931     * instantiate an operation to delete the relation type on commit
   913     """
   932     """
   914     __regid__ = 'syncdelcwrtype'
   933     __regid__ = 'syncdelcwrtype'
   915     __select__ = SyncSchemaHook.__select__ & implements('CWRType')
   934     __select__ = SyncSchemaHook.__select__ & is_instance('CWRType')
   916     events = ('before_delete_entity',)
   935     events = ('before_delete_entity',)
   917 
   936 
   918     def __call__(self):
   937     def __call__(self):
   919         name = self.entity.name
   938         name = self.entity.name
   920         if name in CORE_RTYPES:
   939         if name in CORE_TYPES:
   921             raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
   940             raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
   922         # delete relation definitions using this relation type
   941         # delete relation definitions using this relation type
   923         self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
   942         self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
   924                         {'x': self.entity.eid})
   943                         {'x': self.entity.eid})
   925         self._cw.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
   944         self._cw.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
   926                         {'x': self.entity.eid})
   945                         {'x': self.entity.eid})
   927         MemSchemaCWRTypeDel(self._cw, name)
   946         MemSchemaCWRTypeDel(self._cw, rtype=name)
   928 
   947 
   929 
   948 
   930 class AfterAddCWRTypeHook(DelCWRTypeHook):
   949 class AfterAddCWRTypeHook(DelCWRTypeHook):
   931     """after a CWRType entity has been added:
   950     """after a CWRType entity has been added:
   932     * register an operation to add the relation type to the instance's
   951     * register an operation to add the relation type to the instance's
   937     __regid__ = 'syncaddcwrtype'
   956     __regid__ = 'syncaddcwrtype'
   938     events = ('after_add_entity',)
   957     events = ('after_add_entity',)
   939 
   958 
   940     def __call__(self):
   959     def __call__(self):
   941         entity = self.entity
   960         entity = self.entity
   942         rtype = ybo.RelationType(name=entity.name,
   961         rtypedef = ybo.RelationType(name=entity.name,
   943                                  description=entity.get('description'),
   962                                     description=entity.description,
   944                                  meta=entity.get('meta', False),
   963                                     inlined=entity.get('inlined', False),
   945                                  inlined=entity.get('inlined', False),
   964                                     symmetric=entity.get('symmetric', False),
   946                                  symmetric=entity.get('symmetric', False),
   965                                     eid=entity.eid)
   947                                  eid=entity.eid)
   966         MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
   948         MemSchemaCWRTypeAdd(self._cw, rtype)
       
   949 
   967 
   950 
   968 
   951 class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
   969 class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
   952     """check name change, handle final"""
   970     """check name change, handle final"""
   953     __regid__ = 'syncupdatecwrtype'
   971     __regid__ = 'syncupdatecwrtype'
   962                 old, new = hook.entity_oldnewvalue(entity, prop)
   980                 old, new = hook.entity_oldnewvalue(entity, prop)
   963                 if old != new:
   981                 if old != new:
   964                     newvalues[prop] = entity[prop]
   982                     newvalues[prop] = entity[prop]
   965         if newvalues:
   983         if newvalues:
   966             rschema = self._cw.vreg.schema.rschema(entity.name)
   984             rschema = self._cw.vreg.schema.rschema(entity.name)
   967             SourceDbCWRTypeUpdate(self._cw, rschema=rschema, entity=entity,
   985             CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity,
   968                                   values=newvalues)
   986                             values=newvalues)
   969             MemSchemaCWRTypeUpdate(self._cw, rschema=rschema, values=newvalues)
       
   970 
   987 
   971 
   988 
   972 class AfterDelRelationTypeHook(SyncSchemaHook):
   989 class AfterDelRelationTypeHook(SyncSchemaHook):
   973     """before deleting a CWAttribute or CWRelation entity:
   990     """before deleting a CWAttribute or CWRelation entity:
   974     * if this is a final or inlined relation definition, instantiate an
   991     * if this is a final or inlined relation definition, instantiate an
   982     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('relation_type')
   999     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('relation_type')
   983     events = ('after_delete_relation',)
  1000     events = ('after_delete_relation',)
   984 
  1001 
   985     def __call__(self):
  1002     def __call__(self):
   986         session = self._cw
  1003         session = self._cw
   987         rdef = session.vreg.schema.schema_by_eid(self.eidfrom)
  1004         try:
       
  1005             rdef = session.vreg.schema.schema_by_eid(self.eidfrom)
       
  1006         except KeyError:
       
  1007             self.critical('cant get schema rdef associated to %s', self.eidfrom)
       
  1008             return
   988         subjschema, rschema, objschema = rdef.as_triple()
  1009         subjschema, rschema, objschema = rdef.as_triple()
   989         pendings = session.transaction_data.get('pendingeids', ())
       
   990         pendingrdefs = session.transaction_data.setdefault('pendingrdefs', set())
  1010         pendingrdefs = session.transaction_data.setdefault('pendingrdefs', set())
   991         # first delete existing relation if necessary
  1011         # first delete existing relation if necessary
   992         if rschema.final:
  1012         if rschema.final:
   993             rdeftype = 'CWAttribute'
  1013             rdeftype = 'CWAttribute'
   994             pendingrdefs.add((subjschema, rschema))
  1014             pendingrdefs.add((subjschema, rschema))
   995         else:
  1015         else:
   996             rdeftype = 'CWRelation'
  1016             rdeftype = 'CWRelation'
   997             pendingrdefs.add((subjschema, rschema, objschema))
  1017             pendingrdefs.add((subjschema, rschema, objschema))
   998             if not (subjschema.eid in pendings or objschema.eid in pendings):
  1018             if not (session.deleted_in_transaction(subjschema.eid) or
       
  1019                     session.deleted_in_transaction(objschema.eid)):
   999                 session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
  1020                 session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
  1000                                 % (rschema, subjschema, objschema))
  1021                                 % (rschema, subjschema, objschema))
  1001         execute = session.execute
  1022         RDefDelOp(session, rdef=rdef)
  1002         rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
       
  1003                        'R eid %%(x)s' % rdeftype, {'x': self.eidto})
       
  1004         lastrel = rset[0][0] == 0
       
  1005         # we have to update physical schema systematically for final and inlined
       
  1006         # relations, but only if it's the last instance for this relation type
       
  1007         # for other relations
       
  1008 
       
  1009         if (rschema.final or rschema.inlined):
       
  1010             rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
       
  1011                            'R eid %%(x)s, X from_entity E, E name %%(name)s'
       
  1012                            % rdeftype, {'x': self.eidto, 'name': str(subjschema)})
       
  1013             if rset[0][0] == 0 and not subjschema.eid in pendings:
       
  1014                 ptypes = session.transaction_data.setdefault('pendingrtypes', set())
       
  1015                 ptypes.add(rschema.type)
       
  1016                 DropColumn(session, table=SQL_PREFIX + subjschema.type,
       
  1017                            column=SQL_PREFIX + rschema.type)
       
  1018         elif lastrel:
       
  1019             DropRelationTable(session, rschema.type)
       
  1020         # if this is the last instance, drop associated relation type
       
  1021         if lastrel and not self.eidto in pendings:
       
  1022             execute('DELETE CWRType X WHERE X eid %(x)s', {'x': self.eidto})
       
  1023         MemSchemaRDefDel(session, (subjschema, rschema, objschema))
       
  1024 
  1023 
  1025 
  1024 
  1026 # CWAttribute / CWRelation hooks ###############################################
  1025 # CWAttribute / CWRelation hooks ###############################################
  1027 
  1026 
  1028 class AfterAddCWAttributeHook(SyncSchemaHook):
  1027 class AfterAddCWAttributeHook(SyncSchemaHook):
  1029     __regid__ = 'syncaddcwattribute'
  1028     __regid__ = 'syncaddcwattribute'
  1030     __select__ = SyncSchemaHook.__select__ & implements('CWAttribute')
  1029     __select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute')
  1031     events = ('after_add_entity',)
  1030     events = ('after_add_entity',)
  1032 
  1031 
  1033     def __call__(self):
  1032     def __call__(self):
  1034         SourceDbCWAttributeAdd(self._cw, entity=self.entity)
  1033         CWAttributeAddOp(self._cw, entity=self.entity)
  1035 
  1034 
  1036 
  1035 
  1037 class AfterAddCWRelationHook(AfterAddCWAttributeHook):
  1036 class AfterAddCWRelationHook(AfterAddCWAttributeHook):
  1038     __regid__ = 'syncaddcwrelation'
  1037     __regid__ = 'syncaddcwrelation'
  1039     __select__ = SyncSchemaHook.__select__ & implements('CWRelation')
  1038     __select__ = SyncSchemaHook.__select__ & is_instance('CWRelation')
  1040 
  1039 
  1041     def __call__(self):
  1040     def __call__(self):
  1042         SourceDbCWRelationAdd(self._cw, entity=self.entity)
  1041         CWRelationAddOp(self._cw, entity=self.entity)
  1043 
  1042 
  1044 
  1043 
  1045 class AfterUpdateCWRDefHook(SyncSchemaHook):
  1044 class AfterUpdateCWRDefHook(SyncSchemaHook):
  1046     __regid__ = 'syncaddcwattribute'
  1045     __regid__ = 'syncaddcwattribute'
  1047     __select__ = SyncSchemaHook.__select__ & implements('CWAttribute',
  1046     __select__ = SyncSchemaHook.__select__ & is_instance('CWAttribute',
  1048                                                         'CWRelation')
  1047                                                          'CWRelation')
  1049     events = ('before_update_entity',)
  1048     events = ('before_update_entity',)
  1050 
  1049 
  1051     def __call__(self):
  1050     def __call__(self):
  1052         entity = self.entity
  1051         entity = self.entity
  1053         if self._cw.deleted_in_transaction(entity.eid):
  1052         if self._cw.deleted_in_transaction(entity.eid):
  1054             return
  1053             return
  1055         desttype = entity.otype.name
  1054         subjtype = entity.stype.name
       
  1055         objtype = entity.otype.name
  1056         rschema = self._cw.vreg.schema[entity.rtype.name]
  1056         rschema = self._cw.vreg.schema[entity.rtype.name]
       
  1057         # note: do not access schema rdef here, it may be added later by an
       
  1058         # operation
  1057         newvalues = {}
  1059         newvalues = {}
  1058         for prop in RelationDefinitionSchema.rproperty_defs(desttype):
  1060         for prop in RelationDefinitionSchema.rproperty_defs(objtype):
  1059             if prop == 'constraints':
  1061             if prop == 'constraints':
  1060                 continue
  1062                 continue
  1061             if prop == 'order':
  1063             if prop == 'order':
  1062                 prop = 'ordernum'
  1064                 attr = 'ordernum'
  1063             if prop in entity.edited_attributes:
  1065             else:
  1064                 old, new = hook.entity_oldnewvalue(entity, prop)
  1066                 attr = prop
       
  1067             if attr in entity.edited_attributes:
       
  1068                 old, new = hook.entity_oldnewvalue(entity, attr)
  1065                 if old != new:
  1069                 if old != new:
  1066                     newvalues[prop] = entity[prop]
  1070                     newvalues[prop] = new
  1067         if newvalues:
  1071         if newvalues:
  1068             subjtype = entity.stype.name
  1072             RDefUpdateOp(self._cw, rschema=rschema, rdefkey=(subjtype, objtype),
  1069             MemSchemaRDefUpdate(self._cw, kobj=(subjtype, desttype),
  1073                          values=newvalues)
  1070                                 rschema=rschema, values=newvalues)
       
  1071             SourceDbRDefUpdate(self._cw, kobj=(subjtype, desttype),
       
  1072                                rschema=rschema, values=newvalues)
       
  1073 
  1074 
  1074 
  1075 
  1075 # constraints synchronization hooks ############################################
  1076 # constraints synchronization hooks ############################################
  1076 
  1077 
  1077 class AfterAddCWConstraintHook(SyncSchemaHook):
  1078 class AfterAddCWConstraintHook(SyncSchemaHook):
  1078     __regid__ = 'syncaddcwconstraint'
  1079     __regid__ = 'syncaddcwconstraint'
  1079     __select__ = SyncSchemaHook.__select__ & implements('CWConstraint')
  1080     __select__ = SyncSchemaHook.__select__ & is_instance('CWConstraint')
  1080     events = ('after_add_entity', 'after_update_entity')
  1081     events = ('after_add_entity', 'after_update_entity')
  1081 
  1082 
  1082     def __call__(self):
  1083     def __call__(self):
  1083         MemSchemaCWConstraintAdd(self._cw, entity=self.entity)
  1084         CWConstraintAddOp(self._cw, entity=self.entity)
  1084         SourceDbCWConstraintAdd(self._cw, entity=self.entity)
       
  1085 
  1085 
  1086 
  1086 
  1087 class AfterAddConstrainedByHook(SyncSchemaHook):
  1087 class AfterAddConstrainedByHook(SyncSchemaHook):
       
  1088     __regid__ = 'syncaddconstrainedby'
       
  1089     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by')
       
  1090     events = ('after_add_relation',)
       
  1091 
       
  1092     def __call__(self):
       
  1093         if self._cw.added_in_transaction(self.eidfrom):
       
  1094             # used by get_constraints() which is called in CWAttributeAddOp
       
  1095             self._cw.transaction_data.setdefault(self.eidfrom, []).append(self.eidto)
       
  1096 
       
  1097 
       
  1098 class BeforeDeleteConstrainedByHook(SyncSchemaHook):
  1088     __regid__ = 'syncdelconstrainedby'
  1099     __regid__ = 'syncdelconstrainedby'
  1089     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by')
  1100     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrained_by')
  1090     events = ('after_add_relation',)
       
  1091 
       
  1092     def __call__(self):
       
  1093         if self._cw.added_in_transaction(self.eidfrom):
       
  1094             self._cw.transaction_data.setdefault(self.eidfrom, []).append(self.eidto)
       
  1095 
       
  1096 
       
  1097 class BeforeDeleteConstrainedByHook(AfterAddConstrainedByHook):
       
  1098     __regid__ = 'syncdelconstrainedby'
       
  1099     events = ('before_delete_relation',)
  1101     events = ('before_delete_relation',)
  1100 
  1102 
  1101     def __call__(self):
  1103     def __call__(self):
  1102         if self._cw.deleted_in_transaction(self.eidfrom):
  1104         if self._cw.deleted_in_transaction(self.eidfrom):
  1103             return
  1105             return
  1107         try:
  1109         try:
  1108             cstr = rdef.constraint_by_type(entity.type)
  1110             cstr = rdef.constraint_by_type(entity.type)
  1109         except IndexError:
  1111         except IndexError:
  1110             self._cw.critical('constraint type no more accessible')
  1112             self._cw.critical('constraint type no more accessible')
  1111         else:
  1113         else:
  1112             SourceDbCWConstraintDel(self._cw, rdef=rdef, cstr=cstr)
  1114             CWConstraintDelOp(self._cw, rdef=rdef, oldcstr=cstr)
  1113             MemSchemaCWConstraintDel(self._cw, rdef=rdef, cstr=cstr)
  1115 
       
  1116 # unique_together constraints
       
  1117 # XXX: use setoperations and before_add_relation here (on constraint_of and relations)
       
  1118 class AfterAddCWUniqueTogetherConstraintHook(SyncSchemaHook):
       
  1119     __regid__ = 'syncadd_cwuniquetogether_constraint'
       
  1120     __select__ = SyncSchemaHook.__select__ & is_instance('CWUniqueTogetherConstraint')
       
  1121     events = ('after_add_entity', 'after_update_entity')
       
  1122 
       
  1123     def __call__(self):
       
  1124         CWUniqueTogetherConstraintAddOp(self._cw, entity=self.entity)
       
  1125 
       
  1126 
       
  1127 class BeforeDeleteConstraintOfHook(SyncSchemaHook):
       
  1128     __regid__ = 'syncdelconstraintof'
       
  1129     __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constraint_of')
       
  1130     events = ('before_delete_relation',)
       
  1131 
       
  1132     def __call__(self):
       
  1133         if self._cw.deleted_in_transaction(self.eidto):
       
  1134             return
       
  1135         schema = self._cw.vreg.schema
       
  1136         cstr = self._cw.entity_from_eid(self.eidfrom)
       
  1137         entity = schema.schema_by_eid(self.eidto)
       
  1138         cols = [r.rtype.name
       
  1139                 for r in cstr.relations]
       
  1140         CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, oldcstr=cstr, cols=cols)
  1114 
  1141 
  1115 
  1142 
  1116 # permissions synchronization hooks ############################################
  1143 # permissions synchronization hooks ############################################
  1117 
  1144 
  1118 class AfterAddPermissionHook(SyncSchemaHook):
  1145 class AfterAddPermissionHook(SyncSchemaHook):
  1174             self.info('Reindexing full text index for %i entity of type %s',
  1201             self.info('Reindexing full text index for %i entity of type %s',
  1175                       len(rset), etype)
  1202                       len(rset), etype)
  1176             still_fti = list(schema[etype].indexable_attributes())
  1203             still_fti = list(schema[etype].indexable_attributes())
  1177             for entity in rset.entities():
  1204             for entity in rset.entities():
  1178                 source.fti_unindex_entity(session, entity.eid)
  1205                 source.fti_unindex_entity(session, entity.eid)
  1179                 for container in entity.fti_containers():
  1206                 for container in entity.cw_adapt_to('IFTIndexable').fti_containers():
  1180                     if still_fti or container is not entity:
  1207                     if still_fti or container is not entity:
  1181                         source.fti_unindex_entity(session, container.eid)
  1208                         source.fti_unindex_entity(session, container.eid)
  1182                         source.fti_index_entity(session, container)
  1209                         source.fti_index_entity(session, container)
  1183         if len(to_reindex):
  1210         if len(to_reindex):
  1184             # Transaction have already been committed
  1211             # Transaction have already been committed