[entity] introduce a new 'adapters' registry
This changeset introduces the notion in adapters (as in Zope Component Architecture)
in a cubicweb way, eg using a specific registry of appobjects.
This allows nicer code structure, by avoid clutering entity classes and moving
code usually specific to a place of the ui (or something else) together with the
code that use the interface.
We don't use actual interface anymore, they are implied by adapters (which
may be abstract), whose reg id is an interface name.
Appobjects that used to 'implements(IFace)' should now be rewritten by:
* coding an IFaceAdapter(EntityAdapter) defining (implementing if desired)
the interface, usually with __regid__ = 'IFace'
* use "adaptable('IFace')" as selector instead
Also, the implements_adapter_compat decorator eases backward compatibility
with adapter's methods that may still be found on entities implementing
the interface.
Notice that unlike ZCA, we don't support automatic adapters chain (yagni?).
All interfaces defined in cubicweb have been turned into adapters, also
some new ones have been introduced to cleanup Entity / AnyEntity classes
namespace. At the end, the pluggable mixins mecanism should disappear in
favor of adapters as well.
# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""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"""__docformat__="restructuredtext en"fromcopyimportcopyfromyams.schemaimportBASE_TYPES,RelationSchema,RelationDefinitionSchemafromyamsimportbuildobjsasybo,schema2sqlasy2sqlfromlogilab.common.decoratorsimportclear_cachefromlogilab.common.testlibimportmock_objectfromcubicwebimportValidationErrorfromcubicweb.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,rdef):# if constraints is already a list, reuse it (we're updating multiple# constraints of the same rdef in the same transactions)ifnotisinstance(rdef.constraints,list):rdef.constraints=list(rdef.constraints)self.constraints=rdef.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=entity=values=None# make pylint happydefprecommit_event(self):rschema=self.rschemaifrschema.final:returnsession=self.sessionif'fulltext_container'inself.values:ftiupdates=session.transaction_data.setdefault('fti_update_etypes',set())forsubjtype,objtypeinrschema.rdefs:ftiupdates.add(subjtype)ftiupdates.add(objtype)UpdateFTIndexOp(session)ifnot'inlined'inself.values:return# nothing to doinlined=self.values['inlined']# check in-lining is necessary / possibleifinlined:self.entity.check_inlined_allowed()# 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=y2sql.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=ybo.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=y2sql.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, propagateschema=session.vreg.schematry:eschema=schema.eschema(rdef.subject)exceptKeyError:return# entity type currently being added# propagate attribute to children classesrschema=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,'infered':False,'eid':None})cstrtypemap=ss.cstrtype_mapping(session)groupmap=group_mapping(session)object=schema.eschema(rdef.object)forspecializationineschema.specialized_by(False):if(specialization,rdef.object)inrschema.rdefs:continuesperdef=RelationDefinitionSchema(specialization,rschema,object,props)ss.execschemarql(session.execute,sperdef,ss.rdef2rql(sperdef,cstrtypemap,groupmap))# 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=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=schema.rschema(rtype)tablesql=y2sql.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=schema.add_relation_type(rdef)tablesql=y2sql.rschema2sql(rschema)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):session=self.sessionetype=self.kobj[0]table=SQL_PREFIX+etypecolumn=SQL_PREFIX+self.rschema.typeif'indexed'inself.values:sysource=session.pool.source('system')ifself.values['indexed']:sysource.create_index(session,table,column)else:sysource.drop_index(session,table,column)if'cardinality'inself.valuesandself.rschema.final:adbh=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=y2sql.type_from_constraints(adbh,atype,constraints,creating=False)# XXX check self.values['cardinality'][0] actually changed?notnull=self.values['cardinality'][0]!='1'sql=adbh.sql_set_null_allowed(table,column,coltype,notnull)session.system_sql(sql)if'fulltextindexed'inself.values:UpdateFTIndexOp(session)session.transaction_data.setdefault('fti_update_etypes',set()).add(etype)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=y2sql.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=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)self.prepare_constraints(rdef)cstrtype=self.entity.typeself.cstr=rdef.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.rdef)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.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=ybo.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=y2sql.eschema2sql(self._cw.pool.source('system').dbhelper,eschema,prefix=SQL_PREFIX)rdefrqls=[]gmap=group_mapping(self._cw)cmap=ss.cstrtype_mapping(self._cw)forrtypein(META_RTYPES-VIRTUAL_RTYPES):rschema=schema[rtype]sampletype=rschema.subjects()[0]desttype=rschema.objects()[0]rdef=copy(rschema.rdef(sampletype,desttype))rdef.subject=mock_object(eid=entity.eid)mock=mock_object(eid=None)rdefrqls.append((mock,tuple(ss.rdef2rql(rdef,cmap,gmap))))# 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 relationsforrdef,relrqlsinrdefrqls:ss.execschemarql(self._cw.execute,rdef,relrqls)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=ybo.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__='syncupdatecwrtype'events=('before_update_entity',)def__call__(self):entity=self.entitycheck_valid_changes(self._cw,entity)newvalues={}forpropin('symmetric','inlined','fulltext_container'):ifpropinentity.edited_attributes:old,new=hook.entity_oldnewvalue(entity,prop)ifold!=new:newvalues[prop]=entity[prop]ifnewvalues:rschema=self._cw.vreg.schema.rschema(entity.name)SourceDbCWRTypeUpdate(self._cw,rschema=rschema,entity=entity,values=newvalues)MemSchemaCWRTypeUpdate(self._cw,rschema=rschema,values=newvalues)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.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})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=('before_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:old,new=hook.entity_oldnewvalue(entity,prop)ifold!=new: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:SourceDbCWConstraintDel(self._cw,cstr=cstr,subjtype=rdef.subject,rtype=rdef.rtype)MemSchemaCWConstraintDel(self._cw,rdef=rdef,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)classUpdateFTIndexOp(hook.SingleLastOperation):"""operation to update full text indexation of entity whose schema change We wait after the commit to as the schema in memory is only updated after the commit. """defpostcommit_event(self):session=self.sessionsource=session.repo.system_sourceto_reindex=session.transaction_data.get('fti_update_etypes',())self.info('%i etypes need full text indexed reindexation',len(to_reindex))schema=self.session.repo.vreg.schemaforetypeinto_reindex:rset=session.execute('Any X WHERE X is %s'%etype)self.info('Reindexing full text index for %i entity of type %s',len(rset),etype)still_fti=list(schema[etype].indexable_attributes())forentityinrset.entities():source.fti_unindex_entity(session,entity.eid)forcontainerinentity.cw_adapt_to('IFTIndexable').fti_containers():ifstill_ftiorcontainerisnotentity:source.fti_unindex_entity(session,entity.eid)source.fti_index_entity(session,container)iflen(to_reindex):# Transaction have already been committedsession.pool.commit()# 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)