turn default logging threshold to warning (we usually want them), and log 'no schema for eid' pb using warning instead of error, so we see them in logs but not during migration
"""schema hooks:- synchronize the living schema object with the persistent schema- perform physical update on the source when necessarychecking for schema consistency is done in hooks.py:organization: Logilab:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"fromyams.schemaimportBASE_TYPES,RelationSchema,RelationDefinitionSchemafromyams.buildobjsimportEntityType,RelationType,RelationDefinitionfromyams.schema2sqlimporteschema2sql,rschema2sql,type_from_constraintsfromlogilab.common.decoratorsimportclear_cachefromcubicwebimportValidationError,RepositoryErrorfromcubicweb.selectorsimportimplementsfromcubicweb.schemaimportMETA_RTYPES,VIRTUAL_RTYPES,CONSTRAINTS,display_namefromcubicweb.serverimporthook,schemaserialasssfromcubicweb.server.sqlutilsimportSQL_PREFIXTYPE_CONVERTER={# XXX'Boolean':bool,'Int':int,'Float':float,'Password':str,'String':unicode,'Date':unicode,'Datetime':unicode,'Time':unicode,}# core entity and relation types which can't be removedCORE_ETYPES=list(BASE_TYPES)+['CWEType','CWRType','CWUser','CWGroup','CWConstraint','CWAttribute','CWRelation']CORE_RTYPES=['eid','creation_date','modification_date','cwuri','login','upassword','name','is','instanceof','owned_by','created_by','in_group','relation_type','from_entity','to_entity','constrainted_by','read_permission','add_permission','delete_permission','updated_permission',]defget_constraints(session,entity):constraints=[]forcstreidinsession.transaction_data.get(entity.eid,()):cstrent=session.entity_from_eid(cstreid)cstr=CONSTRAINTS[cstrent.type].deserialize(cstrent.value)cstr.eid=cstreidconstraints.append(cstr)returnconstraintsdefgroup_mapping(cw):try:returncw.transaction_data['groupmap']exceptKeyError:cw.transaction_data['groupmap']=gmap=ss.group_mapping(cw)returngmapdefadd_inline_relation_column(session,etype,rtype):"""add necessary column and index for an inlined relation"""table=SQL_PREFIX+etypecolumn=SQL_PREFIX+rtypetry:session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer'%(table,column)),rollback_on_failure=False)session.info('added column %s to table %s',column,table)except:# silent exception here, if this error has not been raised because the# column already exists, index creation will fail anywaysession.exception('error while adding column %s to table %s',table,column)# create index before alter table which may expectingly fail during test# (sqlite) while index creation should never fail (test for index existence# is done by the dbhelper)session.pool.source('system').create_index(session,table,column)session.info('added index on %s(%s)',table,column)session.transaction_data.setdefault('createdattrs',[]).append('%s.%s'%(etype,rtype))defcheck_valid_changes(session,entity,ro_attrs=('name','final')):errors={}# don't use getattr(entity, attr), we would get the modified value if anyforattrinentity.edited_attributes:ifattrinro_attrs:newval=entity.pop(attr)origval=getattr(entity,attr)ifnewval!=origval:errors[attr]=session._("can't change the %s attribute")% \display_name(session,attr)entity[attr]=newvaliferrors:raiseValidationError(entity.eid,errors)# operations for low-level database alteration ################################classDropTable(hook.Operation):"""actually remove a database from the instance's schema"""table=None# make pylint happydefprecommit_event(self):dropped=self.session.transaction_data.setdefault('droppedtables',set())ifself.tableindropped:return# already processeddropped.add(self.table)self.session.system_sql('DROP TABLE %s'%self.table)self.info('dropped table %s',self.table)classDropRelationTable(DropTable):def__init__(self,session,rtype):super(DropRelationTable,self).__init__(session,table='%s_relation'%rtype)session.transaction_data.setdefault('pendingrtypes',set()).add(rtype)classDropColumn(hook.Operation):"""actually remove the attribut's column from entity table in the system database """table=column=None# make pylint happydefprecommit_event(self):session,table,column=self.session,self.table,self.column# drop index if anysession.pool.source('system').drop_index(session,table,column)try:session.system_sql('ALTER TABLE %s DROP COLUMN %s'%(table,column),rollback_on_failure=False)self.info('dropped column %s from table %s',column,table)exceptException,ex:# not supported by sqlite for instanceself.error('error while altering table %s: %s',table,ex)# base operations for in-memory schema synchronization ########################classMemSchemaNotifyChanges(hook.SingleLastOperation):"""the update schema operation: special operation which should be called once and after all other schema operations. It will trigger internal structures rebuilding to consider schema changes. """def__init__(self,session):hook.SingleLastOperation.__init__(self,session)defprecommit_event(self):foreschemainself.session.repo.schema.entities():ifnoteschema.final:clear_cache(eschema,'ordered_relations')defcommit_event(self):rebuildinfered=self.session.data.get('rebuild-infered',True)repo=self.session.repo# commit event should not raise error, while set_schema has chances to# do so because it triggers full vreg reloadingtry:repo.set_schema(repo.schema,rebuildinfered=rebuildinfered)# CWUser class might have changed, update current session userscwuser_cls=self.session.vreg['etypes'].etype_class('CWUser')forsessioninrepo._sessions.values():session.user.__class__=cwuser_clsexcept:self.critical('error while setting schmea',exc_info=True)defrollback_event(self):self.precommit_event()classMemSchemaOperation(hook.Operation):"""base class for schema operations"""def__init__(self,session,kobj=None,**kwargs):self.kobj=kobj# once Operation.__init__ has been called, event may be triggered, so# do this last !hook.Operation.__init__(self,session,**kwargs)# every schema operation is triggering a schema updateMemSchemaNotifyChanges(session)defprepare_constraints(self,subjtype,rtype,objtype):rdef=rtype.rdef(subjtype,objtype)constraints=rdef.constraintsself.constraints=list(constraints)rdef.constraints=self.constraintsclassMemSchemaEarlyOperation(MemSchemaOperation):definsert_index(self):"""schema operation which are inserted at the begining of the queue (typically to add/remove entity or relation types) """i=-1fori,opinenumerate(self.session.pending_operations):ifnotisinstance(op,MemSchemaEarlyOperation):returnireturni+1# operations for high-level source database alteration ########################classSourceDbCWETypeRename(hook.Operation):"""this operation updates physical storage accordingly"""oldname=newname=None# make pylint happydefprecommit_event(self):# we need sql to operate physical changes on the system databasesqlexec=self.session.system_sqlsqlexec('ALTER TABLE %s%s RENAME TO %s%s'%(SQL_PREFIX,self.oldname,SQL_PREFIX,self.newname))self.info('renamed table %s to %s',self.oldname,self.newname)sqlexec('UPDATE entities SET type=%s WHERE type=%s',(self.newname,self.oldname))sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',(self.newname,self.oldname))classSourceDbCWRTypeUpdate(hook.Operation):"""actually update some properties of a relation definition"""rschema=values=entity=None# make pylint happydefprecommit_event(self):session=self.sessionrschema=self.rschemaifrschema.finalornot'inlined'inself.values:return# nothing to doinlined=self.values['inlined']entity=self.entity# check in-lining is necessary / possibleifnotentity.inlined_changed(inlined):return# nothing to do# inlined changed, make necessary physical changes!sqlexec=self.session.system_sqlrtype=rschema.typeeidcolumn=SQL_PREFIX+'eid'ifnotinlined:# need to create the relation if it has not been already done by# another event of the same transactionifnotrschema.typeinsession.transaction_data.get('createdtables',()):tablesql=rschema2sql(rschema)# create the necessary tableforsqlintablesql.split(';'):ifsql.strip():sqlexec(sql)session.transaction_data.setdefault('createdtables',[]).append(rschema.type)# copy existant datacolumn=SQL_PREFIX+rtypeforetypeinrschema.subjects():table=SQL_PREFIX+str(etype)sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL'%(rtype,eidcolumn,column,table,column))# drop existant columnsforetypeinrschema.subjects():DropColumn(session,table=SQL_PREFIX+str(etype),column=SQL_PREFIX+rtype)else:foretypeinrschema.subjects():try:add_inline_relation_column(session,str(etype),rtype)exceptException,ex:# the column probably already exists. this occurs when the# entity's type has just been added or if the column has not# been previously droppedself.error('error while altering table %s: %s',etype,ex)# copy existant data.# XXX don't use, it's not supported by sqlite (at least at when i tried it)#sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '# 'FROM %(rtype)s_relation '# 'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'# % locals())table=SQL_PREFIX+str(etype)cursor=sqlexec('SELECT eid_from, eid_to FROM %(table)s, ''%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s=''%(rtype)s_relation.eid_from'%locals())args=[{'val':eid_to,'x':eid}foreid,eid_toincursor.fetchall()]ifargs:column=SQL_PREFIX+rtypecursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'%(table,column,eidcolumn),args)# drop existant tableDropRelationTable(session,rtype)classSourceDbCWAttributeAdd(hook.Operation):"""an attribute relation (CWAttribute) has been added: * add the necessary column * set default on this column if any and possible * register an operation to add the relation definition to the instance's schema on commit constraints are handled by specific hooks """entity=None# make pylint happydefinit_rdef(self,**kwargs):entity=self.entityfromentity=entity.stypeself.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',{'x':entity.eid,'se':fromentity.eid,'order':entity.ordernumor0})subj=str(fromentity.name)rtype=entity.rtype.nameobj=str(entity.otype.name)constraints=get_constraints(self.session,entity)rdef=RelationDefinition(subj,rtype,obj,description=entity.description,cardinality=entity.cardinality,constraints=constraints,order=entity.ordernum,eid=entity.eid,**kwargs)MemSchemaRDefAdd(self.session,rdef)returnrdefdefprecommit_event(self):session=self.sessionentity=self.entity# entity.defaultval is a string or None, but we need a correctly typed# valuedefault=entity.defaultvalifdefaultisnotNone:default=TYPE_CONVERTER[entity.otype.name](default)props={'default':default,'indexed':entity.indexed,'fulltextindexed':entity.fulltextindexed,'internationalizable':entity.internationalizable}rdef=self.init_rdef(**props)sysource=session.pool.source('system')attrtype=type_from_constraints(sysource.dbhelper,rdef.object,rdef.constraints)# XXX should be moved somehow into lgc.adbh: sqlite doesn't support to# add a new column with UNIQUE, it should be added after the ALTER TABLE# using ADD INDEXifsysource.dbdriver=='sqlite'and'UNIQUE'inattrtype:extra_unique_index=Trueattrtype=attrtype.replace(' UNIQUE','')else:extra_unique_index=False# added some str() wrapping query since some backend (eg psycopg) don't# allow unicode queriestable=SQL_PREFIX+rdef.subjectcolumn=SQL_PREFIX+rdef.nametry:session.system_sql(str('ALTER TABLE %s ADD COLUMN %s%s'%(table,column,attrtype)),rollback_on_failure=False)self.info('added column %s to table %s',table,column)exceptException,ex:# the column probably already exists. this occurs when# the entity's type has just been added or if the column# has not been previously droppedself.error('error while altering table %s: %s',table,ex)ifextra_unique_indexorentity.indexed:try:sysource.create_index(session,table,column,unique=extra_unique_index)exceptException,ex:self.error('error while creating index for %s.%s: %s',table,column,ex)# final relations are not infered, propagatetry:eschema=session.vreg.schema.eschema(rdef.subject)exceptKeyError:return# entity type currently being added# propagate attribute to children classesrschema=session.vreg.schema.rschema(rdef.name)# if relation type has been inserted in the same transaction, its final# attribute is still set to False, so we've to ensure it's Falserschema.final=True# XXX 'infered': True/False, not clear actuallyprops.update({'constraints':rdef.constraints,'description':rdef.description,'cardinality':rdef.cardinality,'constraints':rdef.constraints,'permissions':rdef.get_permissions(),'order':rdef.order})groupmap=group_mapping(session)forspecializationineschema.specialized_by(False):if(specialization,rdef.object)inrschema.rdefs:continuesperdef=RelationDefinitionSchema(specialization,rschema,rdef.object,props)forrql,argsinss.rdef2rql(rschema,str(specialization),rdef.object,sperdef,groupmap=groupmap):session.execute(rql,args)# set default value, using sql for performance and to avoid# modification_date updateifdefault:session.system_sql('UPDATE %s SET %s=%%(default)s'%(table,column),{'default':default})classSourceDbCWRelationAdd(SourceDbCWAttributeAdd):"""an actual relation has been added: * if this is an inlined relation, add the necessary column else if it's the first instance of this relation type, add the necessary table and set default permissions * register an operation to add the relation definition to the instance's schema on commit constraints are handled by specific hooks """entity=None# make pylint happydefprecommit_event(self):session=self.sessionentity=self.entityrdef=self.init_rdef(composite=entity.composite)schema=session.vreg.schemartype=rdef.namerschema=session.vreg.schema.rschema(rtype)# this have to be done before permissions settingifrschema.inlined:# need to add a column if the relation is inlined and if this is the# first occurence of "Subject relation Something" whatever Something# and if it has not been added during other event of the same# transactionkey='%s.%s'%(rdef.subject,rtype)try:alreadythere=bool(rschema.objects(rdef.subject))exceptKeyError:alreadythere=Falseifnot(alreadythereorkeyinsession.transaction_data.get('createdattrs',())):add_inline_relation_column(session,rdef.subject,rtype)else:# need to create the relation if no relation definition in the# schema and if it has not been added during other event of the same# transactionifnot(rschema.subjects()orrtypeinsession.transaction_data.get('createdtables',())):try:rschema=session.vreg.schema.rschema(rtype)tablesql=rschema2sql(rschema)exceptKeyError:# fake we add it to the schema now to get a correctly# initialized schema but remove it before doing anything# more dangerous...rschema=session.vreg.schema.add_relation_type(rdef)tablesql=rschema2sql(rschema)session.vreg.schema.del_relation_type(rtype)# create the necessary tableforsqlintablesql.split(';'):ifsql.strip():session.system_sql(sql)session.transaction_data.setdefault('createdtables',[]).append(rtype)classSourceDbRDefUpdate(hook.Operation):"""actually update some properties of a relation definition"""rschema=values=None# make pylint happydefprecommit_event(self):etype=self.kobj[0]table=SQL_PREFIX+etypecolumn=SQL_PREFIX+self.rschema.typeif'indexed'inself.values:sysource=self.session.pool.source('system')ifself.values['indexed']:sysource.create_index(self.session,table,column)else:sysource.drop_index(self.session,table,column)if'cardinality'inself.valuesandself.rschema.final:adbh=self.session.pool.source('system').dbhelperifnotadbh.alter_column_support:# not supported (and NOT NULL not set by yams in that case, so# no worry)returnatype=self.rschema.objects(etype)[0]constraints=self.rschema.rdef(etype,atype).constraintscoltype=type_from_constraints(adbh,atype,constraints,creating=False)# XXX check self.values['cardinality'][0] actually changed?sql=adbh.sql_set_null_allowed(table,column,coltype,self.values['cardinality'][0]!='1')self.session.system_sql(sql)classSourceDbCWConstraintAdd(hook.Operation):"""actually update constraint of a relation definition"""entity=None# make pylint happycancelled=Falsedefprecommit_event(self):rdef=self.entity.reverse_constrained_by[0]session=self.session# when the relation is added in the same transaction, the constraint# object is created by the operation adding the attribute or relation,# so there is nothing to do hereifsession.added_in_transaction(rdef.eid):returnrdefschema=session.vreg.schema.schema_by_eid(rdef.eid)subjtype,rtype,objtype=rdefschema.as_triple()cstrtype=self.entity.typeoldcstr=rtype.rdef(subjtype,objtype).constraint_by_type(cstrtype)newcstr=CONSTRAINTS[cstrtype].deserialize(self.entity.value)table=SQL_PREFIX+str(subjtype)column=SQL_PREFIX+str(rtype)# alter the physical schema on size constraint changesifnewcstr.type()=='SizeConstraint'and(oldcstrisNoneoroldcstr.max!=newcstr.max):adbh=self.session.pool.source('system').dbhelpercard=rtype.rdef(subjtype,objtype).cardinalitycoltype=type_from_constraints(adbh,objtype,[newcstr],creating=False)sql=adbh.sql_change_col_type(table,column,coltype,card!='1')try:session.system_sql(sql,rollback_on_failure=False)self.info('altered column %s of table %s: now VARCHAR(%s)',column,table,newcstr.max)exceptException,ex:# not supported by sqlite for instanceself.error('error while altering table %s: %s',table,ex)elifcstrtype=='UniqueConstraint'andoldcstrisNone:session.pool.source('system').create_index(self.session,table,column,unique=True)classSourceDbCWConstraintDel(hook.Operation):"""actually remove a constraint of a relation definition"""rtype=subjtype=objtype=None# make pylint happydefprecommit_event(self):cstrtype=self.cstr.type()table=SQL_PREFIX+str(self.subjtype)column=SQL_PREFIX+str(self.rtype)# alter the physical schema on size/unique constraint changesifcstrtype=='SizeConstraint':try:self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT'%(table,column),rollback_on_failure=False)self.info('altered column %s of table %s: now TEXT',column,table)exceptException,ex:# not supported by sqlite for instanceself.error('error while altering table %s: %s',table,ex)elifcstrtype=='UniqueConstraint':self.session.pool.source('system').drop_index(self.session,table,column,unique=True)# operations for in-memory schema synchronization #############################classMemSchemaCWETypeAdd(MemSchemaEarlyOperation):"""actually add the entity type to the instance's schema"""eid=None# make pylint happydefcommit_event(self):self.session.vreg.schema.add_entity_type(self.kobj)classMemSchemaCWETypeRename(MemSchemaOperation):"""this operation updates physical storage accordingly"""oldname=newname=None# make pylint happydefcommit_event(self):self.session.vreg.schema.rename_entity_type(self.oldname,self.newname)classMemSchemaCWETypeDel(MemSchemaOperation):"""actually remove the entity type from the instance's schema"""defcommit_event(self):try:# del_entity_type also removes entity's relationsself.session.vreg.schema.del_entity_type(self.kobj)exceptKeyError:# s/o entity type have already been deletedpassclassMemSchemaCWRTypeAdd(MemSchemaEarlyOperation):"""actually add the relation type to the instance's schema"""eid=None# make pylint happydefcommit_event(self):self.session.vreg.schema.add_relation_type(self.kobj)classMemSchemaCWRTypeUpdate(MemSchemaOperation):"""actually update some properties of a relation definition"""rschema=values=None# make pylint happydefcommit_event(self):# structure should be clean, not need to remove entity's relations# at this pointself.rschema.__dict__.update(self.values)classMemSchemaCWRTypeDel(MemSchemaOperation):"""actually remove the relation type from the instance's schema"""defcommit_event(self):try:self.session.vreg.schema.del_relation_type(self.kobj)exceptKeyError:# s/o entity type have already been deletedpassclassMemSchemaRDefAdd(MemSchemaEarlyOperation):"""actually add the attribute relation definition to the instance's schema """defcommit_event(self):self.session.vreg.schema.add_relation_def(self.kobj)classMemSchemaRDefUpdate(MemSchemaOperation):"""actually update some properties of a relation definition"""rschema=values=None# make pylint happydefcommit_event(self):# structure should be clean, not need to remove entity's relations# at this pointself.rschema.rdefs[self.kobj].update(self.values)classMemSchemaRDefDel(MemSchemaOperation):"""actually remove the relation definition from the instance's schema"""defcommit_event(self):subjtype,rtype,objtype=self.kobjtry:self.session.vreg.schema.del_relation_def(subjtype,rtype,objtype)exceptKeyError:# relation type may have been already deletedpassclassMemSchemaCWConstraintAdd(MemSchemaOperation):"""actually update constraint of a relation definition has to be called before SourceDbCWConstraintAdd """cancelled=Falsedefprecommit_event(self):rdef=self.entity.reverse_constrained_by[0]# when the relation is added in the same transaction, the constraint# object is created by the operation adding the attribute or relation,# so there is nothing to do hereifself.session.added_in_transaction(rdef.eid):self.cancelled=Truereturnrdef=self.session.vreg.schema.schema_by_eid(rdef.eid)subjtype,rtype,objtype=rdef.as_triple()self.prepare_constraints(subjtype,rtype,objtype)cstrtype=self.entity.typeself.cstr=rtype.rdef(subjtype,objtype).constraint_by_type(cstrtype)self.newcstr=CONSTRAINTS[cstrtype].deserialize(self.entity.value)self.newcstr.eid=self.entity.eiddefcommit_event(self):ifself.cancelled:return# in-place modificationifnotself.cstrisNone:self.constraints.remove(self.cstr)self.constraints.append(self.newcstr)classMemSchemaCWConstraintDel(MemSchemaOperation):"""actually remove a constraint of a relation definition has to be called before SourceDbCWConstraintDel """rtype=subjtype=objtype=None# make pylint happydefprecommit_event(self):self.prepare_constraints(self.subjtype,self.rtype,self.objtype)defcommit_event(self):self.constraints.remove(self.cstr)classMemSchemaPermissionAdd(MemSchemaOperation):"""synchronize schema when a *_permission relation has been added on a group """defcommit_event(self):"""the observed connections pool has been commited"""try:erschema=self.session.vreg.schema.schema_by_eid(self.eid)exceptKeyError:# duh, schema not found, log error and skip operationself.warning('no schema for %s',self.eid)returnperms=list(erschema.action_permissions(self.action))ifhasattr(self,'group_eid'):perm=self.session.entity_from_eid(self.group_eid).nameelse:perm=erschema.rql_expression(self.expr)try:perms.index(perm)self.warning('%s already in permissions for %s on %s',perm,self.action,erschema)exceptValueError:perms.append(perm)erschema.set_action_permissions(self.action,perms)classMemSchemaPermissionDel(MemSchemaPermissionAdd):"""synchronize schema when a *_permission relation has been deleted from a group """defcommit_event(self):"""the observed connections pool has been commited"""try:erschema=self.session.vreg.schema.schema_by_eid(self.eid)exceptKeyError:# duh, schema not found, log error and skip operationself.warning('no schema for %s',self.eid)returnifisinstance(erschema,RelationSchema):# XXX 3.6 migrationreturnifisinstance(erschema,RelationDefinitionSchema)and \self.actionin('delete','add'):# XXX 3.6.1 migrationreturnperms=list(erschema.action_permissions(self.action))ifhasattr(self,'group_eid'):perm=self.session.entity_from_eid(self.group_eid).nameelse:perm=erschema.rql_expression(self.expr)try:perms.remove(perm)erschema.set_action_permissions(self.action,perms)exceptValueError:self.error('can\'t remove permission %s for %s on %s',perm,self.action,erschema)classMemSchemaSpecializesAdd(MemSchemaOperation):defcommit_event(self):eschema=self.session.vreg.schema.schema_by_eid(self.etypeeid)parenteschema=self.session.vreg.schema.schema_by_eid(self.parentetypeeid)eschema._specialized_type=parenteschema.typeparenteschema._specialized_by.append(eschema.type)classMemSchemaSpecializesDel(MemSchemaOperation):defcommit_event(self):try:eschema=self.session.vreg.schema.schema_by_eid(self.etypeeid)parenteschema=self.session.vreg.schema.schema_by_eid(self.parentetypeeid)exceptKeyError:# etype removed, nothing to doreturneschema._specialized_type=Noneparenteschema._specialized_by.remove(eschema.type)classSyncSchemaHook(hook.Hook):__abstract__=Truecategory='syncschema'# CWEType hooks ################################################################classDelCWETypeHook(SyncSchemaHook):"""before deleting a CWEType entity: * check that we don't remove a core entity type * cascade to delete related CWAttribute and CWRelation entities * instantiate an operation to delete the entity type on commit """__regid__='syncdelcwetype'__select__=SyncSchemaHook.__select__&implements('CWEType')events=('before_delete_entity',)def__call__(self):# final entities can't be deleted, don't care about thatname=self.entity.nameifnameinCORE_ETYPES:raiseValidationError(self.entity.eid,{None:self._cw._('can\'t be deleted')})# delete every entities of this typeself._cw.unsafe_execute('DELETE %s X'%name)DropTable(self._cw,table=SQL_PREFIX+name)MemSchemaCWETypeDel(self._cw,name)classAfterDelCWETypeHook(DelCWETypeHook):__regid__='wfcleanup'events=('after_delete_entity',)def__call__(self):# workflow cleanupself._cw.execute('DELETE Workflow X WHERE NOT X workflow_of Y')classAfterAddCWETypeHook(DelCWETypeHook):"""after adding a CWEType entity: * create the necessary table * set creation_date and modification_date by creating the necessary CWAttribute entities * add owned_by relation by creating the necessary CWRelation entity * register an operation to add the entity type to the instance's schema on commit """__regid__='syncaddcwetype'events=('after_add_entity',)def__call__(self):entity=self.entityifentity.get('final'):returnschema=self._cw.vreg.schemaname=entity['name']etype=EntityType(name=name,description=entity.get('description'),meta=entity.get('meta'))# don't care about final# fake we add it to the schema now to get a correctly initialized schema# but remove it before doing anything more dangerous...schema=self._cw.vreg.schemaeschema=schema.add_entity_type(etype)# generate table sql and rql to add metadatatablesql=eschema2sql(self._cw.pool.source('system').dbhelper,eschema,prefix=SQL_PREFIX)relrqls=[]forrtypein(META_RTYPES-VIRTUAL_RTYPES):rschema=schema[rtype]sampletype=rschema.subjects()[0]desttype=rschema.objects()[0]props=rschema.rdef(sampletype,desttype)relrqls+=list(ss.rdef2rql(rschema,name,desttype,props,groupmap=group_mapping(self._cw)))# now remove it !schema.del_entity_type(name)# create the necessary tableforsqlintablesql.split(';'):ifsql.strip():self._cw.system_sql(sql)# register operation to modify the schema on commit# this have to be done before adding other relations definitions# or permission settingsetype.eid=entity.eidMemSchemaCWETypeAdd(self._cw,etype)# add meta relationsforrql,kwargsinrelrqls:self._cw.execute(rql,kwargs)classBeforeUpdateCWETypeHook(DelCWETypeHook):"""check name change, handle final"""__regid__='syncupdatecwetype'events=('before_update_entity',)def__call__(self):entity=self.entitycheck_valid_changes(self._cw,entity,ro_attrs=('final',))# don't use getattr(entity, attr), we would get the modified value if anyif'name'inentity.edited_attributes:newname=entity.pop('name')oldname=entity.nameifnewname.lower()!=oldname.lower():SourceDbCWETypeRename(self._cw,oldname=oldname,newname=newname)MemSchemaCWETypeRename(self._cw,oldname=oldname,newname=newname)entity['name']=newname# CWRType hooks ################################################################classDelCWRTypeHook(SyncSchemaHook):"""before deleting a CWRType entity: * check that we don't remove a core relation type * cascade to delete related CWAttribute and CWRelation entities * instantiate an operation to delete the relation type on commit """__regid__='syncdelcwrtype'__select__=SyncSchemaHook.__select__&implements('CWRType')events=('before_delete_entity',)def__call__(self):name=self.entity.nameifnameinCORE_RTYPES:raiseValidationError(self.entity.eid,{None:self._cw._('can\'t be deleted')})# delete relation definitions using this relation typeself._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',{'x':self.entity.eid})self._cw.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',{'x':self.entity.eid})MemSchemaCWRTypeDel(self._cw,name)classAfterAddCWRTypeHook(DelCWRTypeHook):"""after a CWRType entity has been added: * register an operation to add the relation type to the instance's schema on commit We don't know yet this point if a table is necessary """__regid__='syncaddcwrtype'events=('after_add_entity',)def__call__(self):entity=self.entityrtype=RelationType(name=entity.name,description=entity.get('description'),meta=entity.get('meta',False),inlined=entity.get('inlined',False),symmetric=entity.get('symmetric',False),eid=entity.eid)MemSchemaCWRTypeAdd(self._cw,rtype)classBeforeUpdateCWRTypeHook(DelCWRTypeHook):"""check name change, handle final"""__regid__='checkupdatecwrtype'events=('before_update_entity',)def__call__(self):check_valid_changes(self._cw,self.entity)classAfterUpdateCWRTypeHook(DelCWRTypeHook):__regid__='syncupdatecwrtype'events=('after_update_entity',)def__call__(self):entity=self.entityrschema=self._cw.vreg.schema.rschema(entity.name)newvalues={}forpropin('meta','symmetric','inlined'):ifpropinentity:newvalues[prop]=entity[prop]ifnewvalues:MemSchemaCWRTypeUpdate(self._cw,rschema=rschema,values=newvalues)SourceDbCWRTypeUpdate(self._cw,rschema=rschema,values=newvalues,entity=entity)defcheck_valid_changes(session,entity,ro_attrs=('name','final')):errors={}# don't use getattr(entity, attr), we would get the modified value if anyforattrinro_attrs:ifattrinentity.edited_attributes:origval,newval=hook.entity_oldnewvalue(entity,attr)ifnewval!=origval:errors[attr]=session._("can't change the %s attribute")% \display_name(session,attr)iferrors:raiseValidationError(entity.eid,errors)classAfterDelRelationTypeHook(SyncSchemaHook):"""before deleting a CWAttribute or CWRelation entity: * if this is a final or inlined relation definition, instantiate an operation to drop necessary column, else if this is the last instance of a non final relation, instantiate an operation to drop necessary table * instantiate an operation to delete the relation definition on commit * delete the associated relation type when necessary """__regid__='syncdelrelationtype'__select__=SyncSchemaHook.__select__&hook.match_rtype('relation_type')events=('after_delete_relation',)def__call__(self):session=self._cwrdef=session.vreg.schema.schema_by_eid(self.eidfrom)subjschema,rschema,objschema=rdef.as_triple()pendings=session.transaction_data.get('pendingeids',())pendingrdefs=session.transaction_data.setdefault('pendingrdefs',set())# first delete existing relation if necessaryifrschema.final:rdeftype='CWAttribute'pendingrdefs.add((subjschema,rschema))else:rdeftype='CWRelation'pendingrdefs.add((subjschema,rschema,objschema))ifnot(subjschema.eidinpendingsorobjschema.eidinpendings):session.execute('DELETE X %s Y WHERE X is %s, Y is %s'%(rschema,subjschema,objschema))execute=session.unsafe_executerset=execute('Any COUNT(X) WHERE X is %s, X relation_type R,''R eid %%(x)s'%rdeftype,{'x':self.eidto})lastrel=rset[0][0]==0# we have to update physical schema systematically for final and inlined# relations, but only if it's the last instance for this relation type# for other relationsif(rschema.finalorrschema.inlined):rset=execute('Any COUNT(X) WHERE X is %s, X relation_type R, ''R eid %%(x)s, X from_entity E, E name %%(name)s'%rdeftype,{'x':self.eidto,'name':str(subjschema)})ifrset[0][0]==0andnotsubjschema.eidinpendings:ptypes=session.transaction_data.setdefault('pendingrtypes',set())ptypes.add(rschema.type)DropColumn(session,table=SQL_PREFIX+subjschema.type,column=SQL_PREFIX+rschema.type)eliflastrel:DropRelationTable(session,rschema.type)# if this is the last instance, drop associated relation typeiflastrelandnotself.eidtoinpendings:execute('DELETE CWRType X WHERE X eid %(x)s',{'x':self.eidto},'x')MemSchemaRDefDel(session,(subjschema,rschema,objschema))# CWAttribute / CWRelation hooks ###############################################classAfterAddCWAttributeHook(SyncSchemaHook):__regid__='syncaddcwattribute'__select__=SyncSchemaHook.__select__&implements('CWAttribute')events=('after_add_entity',)def__call__(self):SourceDbCWAttributeAdd(self._cw,entity=self.entity)classAfterAddCWRelationHook(AfterAddCWAttributeHook):__regid__='syncaddcwrelation'__select__=SyncSchemaHook.__select__&implements('CWRelation')def__call__(self):SourceDbCWRelationAdd(self._cw,entity=self.entity)classAfterUpdateCWRDefHook(SyncSchemaHook):__regid__='syncaddcwattribute'__select__=SyncSchemaHook.__select__&implements('CWAttribute','CWRelation')events=('after_update_entity',)def__call__(self):entity=self.entityifself._cw.deleted_in_transaction(entity.eid):returndesttype=entity.otype.namerschema=self._cw.vreg.schema[entity.rtype.name]newvalues={}forpropinRelationDefinitionSchema.rproperty_defs(desttype):ifprop=='constraints':continueifprop=='order':prop='ordernum'ifpropinentity.edited_attributes:newvalues[prop]=entity[prop]ifnewvalues:subjtype=entity.stype.nameMemSchemaRDefUpdate(self._cw,kobj=(subjtype,desttype),rschema=rschema,values=newvalues)SourceDbRDefUpdate(self._cw,kobj=(subjtype,desttype),rschema=rschema,values=newvalues)# constraints synchronization hooks ############################################classAfterAddCWConstraintHook(SyncSchemaHook):__regid__='syncaddcwconstraint'__select__=SyncSchemaHook.__select__&implements('CWConstraint')events=('after_add_entity','after_update_entity')def__call__(self):MemSchemaCWConstraintAdd(self._cw,entity=self.entity)SourceDbCWConstraintAdd(self._cw,entity=self.entity)classAfterAddConstrainedByHook(SyncSchemaHook):__regid__='syncdelconstrainedby'__select__=SyncSchemaHook.__select__&hook.match_rtype('constrained_by')events=('after_add_relation',)def__call__(self):ifself._cw.added_in_transaction(self.eidfrom):self._cw.transaction_data.setdefault(self.eidfrom,[]).append(self.eidto)classBeforeDeleteConstrainedByHook(AfterAddConstrainedByHook):__regid__='syncdelconstrainedby'events=('before_delete_relation',)def__call__(self):ifself._cw.deleted_in_transaction(self.eidfrom):returnschema=self._cw.vreg.schemaentity=self._cw.entity_from_eid(self.eidto)rdef=schema.schema_by_eid(self.eidfrom)try:cstr=rdef.constraint_by_type(entity.type)exceptIndexError:self._cw.critical('constraint type no more accessible')else:subjtype,rtype,objtype=rdef.as_triple()SourceDbCWConstraintDel(self._cw,subjtype=subjtype,rtype=rtype,objtype=objtype,cstr=cstr)MemSchemaCWConstraintDel(self._cw,subjtype=subjtype,rtype=rtype,objtype=objtype,cstr=cstr)# permissions synchronization hooks ############################################classAfterAddPermissionHook(SyncSchemaHook):"""added entity/relation *_permission, need to update schema"""__regid__='syncaddperm'__select__=SyncSchemaHook.__select__&hook.match_rtype('read_permission','add_permission','delete_permission','update_permission')events=('after_add_relation',)def__call__(self):action=self.rtype.split('_',1)[0]ifself._cw.describe(self.eidto)[0]=='CWGroup':MemSchemaPermissionAdd(self._cw,action=action,eid=self.eidfrom,group_eid=self.eidto)else:# RQLExpressionexpr=self._cw.entity_from_eid(self.eidto).expressionMemSchemaPermissionAdd(self._cw,action=action,eid=self.eidfrom,expr=expr)classBeforeDelPermissionHook(AfterAddPermissionHook):"""delete entity/relation *_permission, need to update schema skip the operation if the related type is being deleted """__regid__='syncdelperm'events=('before_delete_relation',)def__call__(self):ifself._cw.deleted_in_transaction(self.eidfrom):returnaction=self.rtype.split('_',1)[0]ifself._cw.describe(self.eidto)[0]=='CWGroup':MemSchemaPermissionDel(self._cw,action=action,eid=self.eidfrom,group_eid=self.eidto)else:# RQLExpressionexpr=self._cw.entity_from_eid(self.eidto).expressionMemSchemaPermissionDel(self._cw,action=action,eid=self.eidfrom,expr=expr)# specializes synchronization hooks ############################################classAfterAddSpecializesHook(SyncSchemaHook):__regid__='syncaddspecializes'__select__=SyncSchemaHook.__select__&hook.match_rtype('specializes')events=('after_add_relation',)def__call__(self):MemSchemaSpecializesAdd(self._cw,etypeeid=self.eidfrom,parentetypeeid=self.eidto)classAfterDelSpecializesHook(SyncSchemaHook):__regid__='syncdelspecializes'__select__=SyncSchemaHook.__select__&hook.match_rtype('specializes')events=('after_delete_relation',)def__call__(self):MemSchemaSpecializesDel(self._cw,etypeeid=self.eidfrom,parentetypeeid=self.eidto)