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