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