"""a class implementing basic actions used in migration scripts.The following schema actions are supported for now:* add/drop/rename attribute* add/drop entity/relation type* rename entity typeThe following data actions are supported for now:* add an entity* execute raw RQL queries:organization: Logilab:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"importsysimportosfromos.pathimportjoin,existsfromdatetimeimportdatetimefromlogilab.common.decoratorsimportcachedfromlogilab.common.adbhimportget_adv_func_helperfromyams.constraintsimportSizeConstraintfromyams.schema2sqlimporteschema2sql,rschema2sqlfromcubicwebimportAuthenticationErrorfromcubicweb.dbapiimportget_repository,repo_connectfromcubicweb.common.migrationimportMigrationHelper,yestry:fromcubicweb.serverimportschemaserialasssfromcubicweb.server.utilsimportmanager_userpasswdfromcubicweb.server.sqlutilsimportsqlexec,SQL_PREFIXexceptImportError:# LAXpassdefset_sql_prefix(prefix):"""3.1.5 migration function: allow to unset/reset SQL_PREFIX"""formodulein('checkintegrity','migractions','schemahooks','sources.rql2sql','sources.native'):try:sys.modules['cubicweb.server.%s'%module].SQL_PREFIX=prefixprint'changed SQL_PREFIX for %s'%moduleexceptKeyError:passdefupdate_database(repo):"""3.1.3 migration function: update database schema by adding SQL_PREFIX to entity type tables and columns """pool=repo._get_pool()source=repo.system_sourcesqlcu=pool['system']foretypeinrepo.schema.entities():ifetype.is_final():continuetry:sqlcu.execute('ALTER TABLE %s RENAME TO cw_%s'%(etype,etype))print'renamed %s table for source %s'%(etype,uri)except:passforrschemainetype.subject_relations():ifrschema=='has_text':continueifrschema.is_final()orrschema.inlined:sqlcu.execute('ALTER TABLE cw_%s RENAME %s TO cw_%s'%(etype,rschema,rschema))print'renamed %s.%s column for source %s'%(etype,rschema,uri)pool.commit()repo._free_pool(pool)classServerMigrationHelper(MigrationHelper):"""specific migration helper for server side migration scripts, providind actions related to schema/data migration """def__init__(self,config,schema,interactive=True,repo=None,cnx=None,verbosity=1,connect=True):MigrationHelper.__init__(self,config,interactive,verbosity)ifnotinteractive:assertcnxassertrepoifcnxisnotNone:assertrepoself._cnx=cnxself.repo=repoelifconnect:self.repo_connect()ifnotschema:schema=config.load_schema(expand_cubes=True)self.fs_schema=schemaself._synchronized=set()@cacheddefrepo_connect(self):try:self.repo=get_repository(method='inmemory',config=self.config)except:importtracebacktraceback.print_exc()print'3.1.5 migration'# XXX 3.1.5 migrationset_sql_prefix('')self.repo=get_repository(method='inmemory',config=self.config)update_database(self.repo)set_sql_prefix('cw_')returnself.repodefshutdown(self):ifself.repoisnotNone:self.repo.shutdown()defrewrite_vcconfiguration(self):"""write current installed versions (of cubicweb software and of each used cube) into the database """self.cmd_set_property('system.version.cubicweb',self.config.cubicweb_version())forpkginself.config.cubes():pkgversion=self.config.cube_version(pkg)self.cmd_set_property('system.version.%s'%pkg.lower(),pkgversion)self.commit()defbackup_database(self,backupfile=None,askconfirm=True):config=self.configsource=config.sources()['system']helper=get_adv_func_helper(source['db-driver'])date=datetime.now().strftime('%Y-%m-%d_%H:%M:%S')app=config.appidbackupfile=backupfileorjoin(config.backup_dir(),'%s-%s.dump'%(app,date))ifexists(backupfile):ifnotself.confirm('a backup already exists for %s, overwrite it?'%app):returnelifaskconfirmandnotself.confirm('backup %s database?'%app):returncmd=helper.backup_command(source['db-name'],source.get('db-host'),source.get('db-user'),backupfile,keepownership=False)whileTrue:printcmdifos.system(cmd):print'error while backuping the base'answer=self.confirm('continue anyway?',shell=False,abort=False,retry=True)ifnotanswer:raiseSystemExit(1)ifanswer==1:# 1: continue, 2: retrybreakelse:fromcubicweb.toolsutilsimportrestrict_perms_to_userprint'database backup:',backupfilerestrict_perms_to_user(backupfile,self.info)breakdefrestore_database(self,backupfile,drop=True):config=self.configsource=config.sources()['system']helper=get_adv_func_helper(source['db-driver'])app=config.appidifnotexists(backupfile):raiseException("backup file %s doesn't exist"%backupfile)ifself.confirm('restore %s database from %s ?'%(app,backupfile)):forcmdinhelper.restore_commands(source['db-name'],source.get('db-host'),source.get('db-user'),backupfile,source['db-encoding'],keepownership=False,drop=drop):whileTrue:printcmdifos.system(cmd):print'error while restoring the base'answer=self.confirm('continue anyway?',shell=False,abort=False,retry=True)ifnotanswer:raiseSystemExit(1)ifanswer==1:# 1: continue, 2: retrybreakelse:breakprint'database restored'defmigrate(self,vcconf,toupgrade,options):ifnotoptions.fs_only:ifoptions.backup_dbisNone:self.backup_database()elifoptions.backup_db:self.backup_database(askconfirm=False)super(ServerMigrationHelper,self).migrate(vcconf,toupgrade,options)defprocess_script(self,migrscript,funcname=None,*args,**kwargs):"""execute a migration script in interactive mode, display the migration script path, ask for confirmation and execute it if confirmed """ifmigrscript.endswith('.sql'):ifself.execscript_confirm(migrscript):sqlexec(open(migrscript).read(),self.session.system_sql)else:returnsuper(ServerMigrationHelper,self).process_script(migrscript,funcname,*args,**kwargs)@propertydefcnx(self):"""lazy connection"""try:returnself._cnxexceptAttributeError:sourcescfg=self.repo.config.sources()try:login=sourcescfg['admin']['login']pwd=sourcescfg['admin']['password']exceptKeyError:login,pwd=manager_userpasswd()whileTrue:try:self._cnx=repo_connect(self.repo,login,pwd)ifnot'managers'inself._cnx.user(self.session).groups:print'migration need an account in the managers group'else:breakexceptAuthenticationError:print'wrong user/password'except(KeyboardInterrupt,EOFError):print'aborting...'sys.exit(0)try:login,pwd=manager_userpasswd()except(KeyboardInterrupt,EOFError):print'aborting...'sys.exit(0)returnself._cnx@propertydefsession(self):returnself.repo._get_session(self.cnx.sessionid)@property@cacheddefrqlcursor(self):"""lazy rql cursor"""# should not give session as cnx.cursor(), else we may try to execute# some query while no pool is set on the session (eg on entity attribute# access for instance)returnself.cnx.cursor()defcommit(self):ifhasattr(self,'_cnx'):self._cnx.commit()defrollback(self):ifhasattr(self,'_cnx'):self._cnx.rollback()defrqlexecall(self,rqliter,cachekey=None,ask_confirm=True):forrql,kwargsinrqliter:self.rqlexec(rql,kwargs,cachekey,ask_confirm)@cacheddef_create_context(self):"""return a dictionary to use as migration script execution context"""context=super(ServerMigrationHelper,self)._create_context()context.update({'checkpoint':self.checkpoint,'sql':self.sqlexec,'rql':self.rqlexec,'rqliter':self.rqliter,'schema':self.repo.schema,# XXX deprecate'newschema':self.fs_schema,'fsschema':self.fs_schema,'cnx':self.cnx,'session':self.session,'repo':self.repo,})returncontext@cacheddefgroup_mapping(self):"""cached group mapping"""returnss.group_mapping(self.rqlcursor)defexec_event_script(self,event,cubepath=None,funcname=None,*args,**kwargs):ifcubepath:apc=join(cubepath,'migration','%s.py'%event)else:apc=join(self.config.migration_scripts_dir(),'%s.py'%event)ifexists(apc):ifself.config.free_wheel:fromcubicweb.server.hooksimportsetowner_after_add_entityself.repo.hm.unregister_hook(setowner_after_add_entity,'after_add_entity','')self.deactivate_verification_hooks()self.info('executing %s',apc)confirm=self.confirmexecscript_confirm=self.execscript_confirmself.confirm=yesself.execscript_confirm=yestry:returnself.process_script(apc,funcname,*args,**kwargs)finally:self.confirm=confirmself.execscript_confirm=execscript_confirmifself.config.free_wheel:self.repo.hm.register_hook(setowner_after_add_entity,'after_add_entity','')self.reactivate_verification_hooks()# base actions ############################################################defcheckpoint(self):"""checkpoint action"""ifself.confirm('commit now ?',shell=False):self.commit()defcmd_add_cube(self,cube,update_database=True):self.cmd_add_cubes((cube,),update_database)defcmd_add_cubes(self,cubes,update_database=True):"""update_database is telling if the database schema should be updated or if only the relevant eproperty should be inserted (for the case where a cube has been extracted from an existing application, so the cube schema is already in there) """newcubes=super(ServerMigrationHelper,self).cmd_add_cubes(cubes)ifnotnewcubes:returnforpackinnewcubes:self.cmd_set_property('system.version.'+pack,self.config.cube_version(pack))ifnotupdate_database:self.commit()returnnewcubes_schema=self.config.load_schema(construction_mode='non-strict')new=set()# execute pre-create filesforpackinreversed(newcubes):self.exec_event_script('precreate',self.config.cube_dir(pack))# add new entity and relation typesforrschemainnewcubes_schema.relations():ifnotrschemainself.repo.schema:self.cmd_add_relation_type(rschema.type)new.add(rschema.type)foreschemainnewcubes_schema.entities():ifnoteschemainself.repo.schema:self.cmd_add_entity_type(eschema.type)new.add(eschema.type)# check if attributes has been added to existing entitiesforrschemainnewcubes_schema.relations():existingschema=self.repo.schema.rschema(rschema.type)for(fromtype,totype)inrschema.iter_rdefs():ifexistingschema.has_rdef(fromtype,totype):continue# check we should actually add the relation definitionifnot(fromtypeinnewortotypeinneworrschemainnew):continueself.cmd_add_relation_definition(str(fromtype),rschema.type,str(totype))# execute post-create filesforpackinreversed(newcubes):self.exec_event_script('postcreate',self.config.cube_dir(pack))self.commit()defcmd_remove_cube(self,cube):removedcubes=super(ServerMigrationHelper,self).cmd_remove_cube(cube)ifnotremovedcubes:returnfsschema=self.fs_schemaremovedcubes_schema=self.config.load_schema(construction_mode='non-strict')reposchema=self.repo.schema# execute pre-remove filesforpackinreversed(removedcubes):self.exec_event_script('preremove',self.config.cube_dir(pack))# remove cubes'entity and relation typesforrschemainfsschema.relations():ifnotrschemainremovedcubes_schemaandrschemainreposchema:self.cmd_drop_relation_type(rschema.type)foreschemainfsschema.entities():ifnoteschemainremovedcubes_schemaandeschemainreposchema:self.cmd_drop_entity_type(eschema.type)forrschemainfsschema.relations():ifrschemainremovedcubes_schemaandrschemainreposchema:# check if attributes/relations has been added to entities from # other cubesforfromtype,totypeinrschema.iter_rdefs():ifnotremovedcubes_schema[rschema.type].has_rdef(fromtype,totype)and \reposchema[rschema.type].has_rdef(fromtype,totype):self.cmd_drop_relation_definition(str(fromtype),rschema.type,str(totype))# execute post-remove filesforpackinreversed(removedcubes):self.exec_event_script('postremove',self.config.cube_dir(pack))self.rqlexec('DELETE EProperty X WHERE X pkey %(pk)s',{'pk':u'system.version.'+pack},ask_confirm=False)self.commit()# schema migration actions ################################################defcmd_add_attribute(self,etype,attrname,attrtype=None,commit=True):"""add a new attribute on the given entity type"""ifattrtypeisNone:rschema=self.fs_schema.rschema(attrname)attrtype=rschema.objects(etype)[0]self.cmd_add_relation_definition(etype,attrname,attrtype,commit=commit)defcmd_drop_attribute(self,etype,attrname,commit=True):"""drop an existing attribute from the given entity type `attrname` is a string giving the name of the attribute to drop """rschema=self.repo.schema.rschema(attrname)attrtype=rschema.objects(etype)[0]self.cmd_drop_relation_definition(etype,attrname,attrtype,commit=commit)defcmd_rename_attribute(self,etype,oldname,newname,commit=True):"""rename an existing attribute of the given entity type `oldname` is a string giving the name of the existing attribute `newname` is a string giving the name of the renamed attribute """eschema=self.fs_schema.eschema(etype)attrtype=eschema.destination(newname)# have to commit this first step anyway to get the definition# actually in the schemaself.cmd_add_attribute(etype,newname,attrtype,commit=True)# skipp NULL values if the attribute is requiredrql='SET X %s VAL WHERE X is %s, X %s VAL'%(newname,etype,oldname)card=eschema.rproperty(newname,'cardinality')[0]ifcard=='1':rql+=', NOT X %s NULL'%oldnameself.rqlexec(rql,ask_confirm=self.verbosity>=2)self.cmd_drop_attribute(etype,oldname,commit=commit)defcmd_add_entity_type(self,etype,auto=True,commit=True):"""register a new entity type in auto mode, automatically register entity's relation where the targeted type is known """applschema=self.repo.schemaifetypeinapplschema:eschema=applschema[etype]ifeschema.is_final():applschema.del_entity_type(etype)else:eschema=self.fs_schema.eschema(etype)confirm=self.verbosity>=2# register the entity into EETypeself.rqlexecall(ss.eschema2rql(eschema),ask_confirm=confirm)# add specializes relation if neededself.rqlexecall(ss.eschemaspecialize2rql(eschema),ask_confirm=confirm)# register groups / permissions for the entityself.rqlexecall(ss.erperms2rql(eschema,self.group_mapping()),ask_confirm=confirm)# register entity's attributesforrschema,attrschemaineschema.attribute_definitions():# ignore those meta relations, they will be automatically addedifrschema.typein('eid','creation_date','modification_date'):continueifnotrschema.typeinapplschema:# need to add the relation type and to commit to get it# actually in the schemaself.cmd_add_relation_type(rschema.type,False,commit=True)# register relation definitionself.rqlexecall(ss.rdef2rql(rschema,etype,attrschema.type),ask_confirm=confirm)ifauto:# we have commit here to get relation types actually in the schemaself.commit()added=[]forrschemaineschema.subject_relations():# attribute relation have already been processed and# 'owned_by'/'created_by' will be automatically addedifrschema.finalorrschema.typein('owned_by','created_by','is','is_instance_of'):continuertypeadded=rschema.typeinapplschemafortargetschemainrschema.objects(etype):# ignore relations where the targeted type is not in the# current application schematargettype=targetschema.typeifnottargettypeinapplschemaandtargettype!=etype:continueifnotrtypeadded:# need to add the relation type and to commit to get it# actually in the schemaadded.append(rschema.type)self.cmd_add_relation_type(rschema.type,False,commit=True)rtypeadded=True# register relation definition# remember this two avoid adding twice non symetric relation# such as "Emailthread forked_from Emailthread"added.append((etype,rschema.type,targettype))self.rqlexecall(ss.rdef2rql(rschema,etype,targettype),ask_confirm=confirm)forrschemaineschema.object_relations():rtypeadded=rschema.typeinapplschemaorrschema.typeinaddedfortargetschemainrschema.subjects(etype):# ignore relations where the targeted type is not in the# current application schematargettype=targetschema.type# don't check targettype != etype since in this case the# relation has already been added as a subject relationifnottargettypeinapplschema:continueifnotrtypeadded:# need to add the relation type and to commit to get it# actually in the schemaself.cmd_add_relation_type(rschema.type,False,commit=True)rtypeadded=Trueelif(targettype,rschema.type,etype)inadded:continue# register relation definitionself.rqlexecall(ss.rdef2rql(rschema,targettype,etype),ask_confirm=confirm)ifcommit:self.commit()defcmd_drop_entity_type(self,etype,commit=True):"""unregister an existing entity type This will trigger deletion of necessary relation types and definitions """# XXX what if we delete an entity type which is specialized by other types# unregister the entity from EETypeself.rqlexec('DELETE EEType X WHERE X name %(etype)s',{'etype':etype},ask_confirm=self.verbosity>=2)ifcommit:self.commit()defcmd_rename_entity_type(self,oldname,newname,commit=True):"""rename an existing entity type in the persistent schema `oldname` is a string giving the name of the existing entity type `newname` is a string giving the name of the renamed entity type """self.rqlexec('SET ET name %(newname)s WHERE ET is EEType, ET name %(oldname)s',{'newname':unicode(newname),'oldname':oldname})ifcommit:self.commit()defcmd_add_relation_type(self,rtype,addrdef=True,commit=True):"""register a new relation type named `rtype`, as described in the schema description file. `addrdef` is a boolean value; when True, it will also add all relations of the type just added found in the schema definition file. Note that it implies an intermediate "commit" which commits the relation type creation (but not the relation definitions themselves, for which committing depends on the `commit` argument value). """rschema=self.fs_schema.rschema(rtype)# register the relation into ERType and insert necessary relation# definitionsself.rqlexecall(ss.rschema2rql(rschema,addrdef=False),ask_confirm=self.verbosity>=2)# register groups / permissions for the relationself.rqlexecall(ss.erperms2rql(rschema,self.group_mapping()),ask_confirm=self.verbosity>=2)ifaddrdef:self.commit()self.rqlexecall(ss.rdef2rql(rschema),ask_confirm=self.verbosity>=2)ifcommit:self.commit()defcmd_drop_relation_type(self,rtype,commit=True):"""unregister an existing relation type"""# unregister the relation from ERTypeself.rqlexec('DELETE ERType X WHERE X name %r'%rtype,ask_confirm=self.verbosity>=2)ifcommit:self.commit()defcmd_rename_relation(self,oldname,newname,commit=True):"""rename an existing relation `oldname` is a string giving the name of the existing relation `newname` is a string giving the name of the renamed relation """self.cmd_add_relation_type(newname,commit=True)self.rqlexec('SET X %s Y WHERE X %s Y'%(newname,oldname),ask_confirm=self.verbosity>=2)self.cmd_drop_relation_type(oldname,commit=commit)defcmd_add_relation_definition(self,subjtype,rtype,objtype,commit=True):"""register a new relation definition, from its definition found in the schema definition file """rschema=self.fs_schema.rschema(rtype)ifnotrtypeinself.repo.schema:self.cmd_add_relation_type(rtype,addrdef=False,commit=True)self.rqlexecall(ss.rdef2rql(rschema,subjtype,objtype),ask_confirm=self.verbosity>=2)ifcommit:self.commit()defcmd_drop_relation_definition(self,subjtype,rtype,objtype,commit=True):"""unregister an existing relation definition"""rschema=self.repo.schema.rschema(rtype)# unregister the definition from EFRDef or ENFRDefifrschema.is_final():etype='EFRDef'else:etype='ENFRDef'rql=('DELETE %s X WHERE X from_entity FE, FE name "%s",''X relation_type RT, RT name "%s", X to_entity TE, TE name "%s"')self.rqlexec(rql%(etype,subjtype,rtype,objtype),ask_confirm=self.verbosity>=2)ifcommit:self.commit()defcmd_synchronize_permissions(self,ertype,commit=True):"""permission synchronization for an entity or relation type"""ifertypein('eid','has_text','identity'):returnnewrschema=self.fs_schema[ertype]teid=self.repo.schema[ertype].eidif'update'innewrschema.ACTIONSornewrschema.is_final():# entity typeexprtype=u'ERQLExpression'else:# relation typeexprtype=u'RRQLExpression'assertteid,ertypegm=self.group_mapping()confirm=self.verbosity>=2# * remove possibly deprecated permission (eg in the persistent schema# but not in the new schema)# * synchronize existing expressions# * add new groups/expressionsforactioninnewrschema.ACTIONS:perm='%s_permission'%action# handle groupsnewgroups=list(newrschema.get_groups(action))forgeid,gnameinself.rqlexec('Any G, GN WHERE T %s G, G name GN, ''T eid %%(x)s'%perm,{'x':teid},'x',ask_confirm=False):ifnotgnameinnewgroups:ifnotconfirmorself.confirm('remove %s permission of %s to %s?'%(action,ertype,gname)):self.rqlexec('DELETE T %s G WHERE G eid %%(x)s, T eid %s'%(perm,teid),{'x':geid},'x',ask_confirm=False)else:newgroups.remove(gname)forgnameinnewgroups:ifnotconfirmorself.confirm('grant %s permission of %s to %s?'%(action,ertype,gname)):self.rqlexec('SET T %s G WHERE G eid %%(x)s, T eid %s'%(perm,teid),{'x':gm[gname]},'x',ask_confirm=False)# handle rql expressionsnewexprs=dict((expr.expression,expr)forexprinnewrschema.get_rqlexprs(action))forexpreid,expressioninself.rqlexec('Any E, EX WHERE T %s E, E expression EX, ''T eid %s'%(perm,teid),ask_confirm=False):ifnotexpressioninnewexprs:ifnotconfirmorself.confirm('remove %s expression for %s permission of %s?'%(expression,action,ertype)):# deleting the relation will delete the expression entityself.rqlexec('DELETE T %s E WHERE E eid %%(x)s, T eid %s'%(perm,teid),{'x':expreid},'x',ask_confirm=False)else:newexprs.pop(expression)forexpressioninnewexprs.values():expr=expression.expressionifnotconfirmorself.confirm('add %s expression for %s permission of %s?'%(expr,action,ertype)):self.rqlexec('INSERT RQLExpression X: X exprtype %%(exprtype)s, ''X expression %%(expr)s, X mainvars %%(vars)s, T %s X ''WHERE T eid %%(x)s'%perm,{'expr':expr,'exprtype':exprtype,'vars':expression.mainvars,'x':teid},'x',ask_confirm=False)ifcommit:self.commit()defcmd_synchronize_rschema(self,rtype,syncrdefs=True,syncperms=True,commit=True):"""synchronize properties of the persistent relation schema against its current definition: * description * symetric, meta * inlined * relation definitions if `syncrdefs` * permissions if `syncperms` physical schema changes should be handled by repository's schema hooks """rtype=str(rtype)ifrtypeinself._synchronized:returnself._synchronized.add(rtype)rschema=self.fs_schema.rschema(rtype)self.rqlexecall(ss.updaterschema2rql(rschema),ask_confirm=self.verbosity>=2)reporschema=self.repo.schema.rschema(rtype)ifsyncrdefs:forsubj,objinrschema.iter_rdefs():ifnotreporschema.has_rdef(subj,obj):continueself.cmd_synchronize_rdef_schema(subj,rschema,obj,commit=False)ifsyncperms:self.cmd_synchronize_permissions(rtype,commit=False)ifcommit:self.commit()defcmd_synchronize_eschema(self,etype,syncperms=True,commit=True):"""synchronize properties of the persistent entity schema against its current definition: * description * internationalizable, fulltextindexed, indexed, meta * relations from/to this entity * permissions if `syncperms` """etype=str(etype)ifetypeinself._synchronized:returnself._synchronized.add(etype)repoeschema=self.repo.schema.eschema(etype)try:eschema=self.fs_schema.eschema(etype)exceptKeyError:returnrepospschema=repoeschema.specializes()espschema=eschema.specializes()ifrepospschemaandnotespschema:self.rqlexec('DELETE X specializes Y WHERE X is EEType, X name %(x)s',{'x':str(repoeschema)})elifnotrepospschemaandespschema:self.rqlexec('SET X specializes Y WHERE X is EEType, X name %(x)s, ''Y is EEType, Y name %(y)s',{'x':str(repoeschema),'y':str(espschema)})self.rqlexecall(ss.updateeschema2rql(eschema),ask_confirm=self.verbosity>=2)forrschema,targettypes,xineschema.relation_definitions(True):ifx=='subject':ifnotrschemainrepoeschema.subject_relations():continuesubjtypes,objtypes=[etype],targettypeselse:# x == 'object'ifnotrschemainrepoeschema.object_relations():continuesubjtypes,objtypes=targettypes,[etype]self.cmd_synchronize_rschema(rschema,syncperms=syncperms,syncrdefs=False,commit=False)reporschema=self.repo.schema.rschema(rschema)forsubjinsubjtypes:forobjinobjtypes:ifnotreporschema.has_rdef(subj,obj):continueself.cmd_synchronize_rdef_schema(subj,rschema,obj,commit=False)ifsyncperms:self.cmd_synchronize_permissions(etype,commit=False)ifcommit:self.commit()defcmd_synchronize_rdef_schema(self,subjtype,rtype,objtype,commit=True):"""synchronize properties of the persistent relation definition schema against its current definition: * order and other properties * constraints """subjtype,objtype=str(subjtype),str(objtype)rschema=self.fs_schema.rschema(rtype)reporschema=self.repo.schema.rschema(rschema)if(subjtype,rschema,objtype)inself._synchronized:returnself._synchronized.add((subjtype,rschema,objtype))ifrschema.symetric:self._synchronized.add((objtype,rschema,subjtype))confirm=self.verbosity>=2# propertiesself.rqlexecall(ss.updaterdef2rql(rschema,subjtype,objtype),ask_confirm=confirm)# constraintsnewconstraints=list(rschema.rproperty(subjtype,objtype,'constraints'))# 1. remove old constraints and update constraints of the same type# NOTE: don't use rschema.constraint_by_type because it may be# out of sync with newconstraints when multiple# constraints of the same type are usedforcstrinreporschema.rproperty(subjtype,objtype,'constraints'):fornewcstrinnewconstraints:ifnewcstr.type()==cstr.type():breakelse:newcstr=NoneifnewcstrisNone:self.rqlexec('DELETE X constrained_by C WHERE C eid %(x)s',{'x':cstr.eid},'x',ask_confirm=confirm)self.rqlexec('DELETE EConstraint C WHERE C eid %(x)s',{'x':cstr.eid},'x',ask_confirm=confirm)else:newconstraints.remove(newcstr)values={'x':cstr.eid,'v':unicode(newcstr.serialize())}self.rqlexec('SET X value %(v)s WHERE X eid %(x)s',values,'x',ask_confirm=confirm)# 2. add new constraintsfornewcstrinnewconstraints:self.rqlexecall(ss.constraint2rql(rschema,subjtype,objtype,newcstr),ask_confirm=confirm)ifcommit:self.commit()defcmd_synchronize_schema(self,syncperms=True,commit=True):"""synchronize the persistent schema against the current definition schema. It will synch common stuff between the definition schema and the actual persistent schema, it won't add/remove any entity or relation. """foretypeinself.repo.schema.entities():self.cmd_synchronize_eschema(etype,syncperms=syncperms,commit=False)ifcommit:self.commit()defcmd_change_relation_props(self,subjtype,rtype,objtype,commit=True,**kwargs):"""change some properties of a relation definition"""assertkwargsrestriction=[]ifsubjtypeandsubjtype!='Any':restriction.append('X from_entity FE, FE name "%s"'%subjtype)ifobjtypeandobjtype!='Any':restriction.append('X to_entity TE, TE name "%s"'%objtype)ifrtypeandrtype!='Any':restriction.append('X relation_type RT, RT name "%s"'%rtype)assertrestrictionvalues=[]fork,vinkwargs.items():values.append('X %s%%(%s)s'%(k,k))ifisinstance(v,str):kwargs[k]=unicode(v)rql='SET %s WHERE %s'%(','.join(values),','.join(restriction))self.rqlexec(rql,kwargs,ask_confirm=self.verbosity>=2)ifcommit:self.commit()defcmd_set_size_constraint(self,etype,rtype,size,commit=True):"""set change size constraint of a string attribute if size is None any size constraint will be removed """oldvalue=Noneforconstrinself.repo.schema.eschema(etype).constraints(rtype):ifisinstance(constr,SizeConstraint):oldvalue=constr.maxifoldvalue==size:returnifoldvalueisNoneandnotsizeisNone:ceid=self.rqlexec('INSERT EConstraint C: C value %(v)s, C cstrtype CT ''WHERE CT name "SizeConstraint"',{'v':SizeConstraint(size).serialize()},ask_confirm=self.verbosity>=2)[0][0]self.rqlexec('SET X constrained_by C WHERE X from_entity S, X relation_type R, ''S name "%s", R name "%s", C eid %s'%(etype,rtype,ceid),ask_confirm=self.verbosity>=2)elifnotoldvalueisNone:ifnotsizeisNone:self.rqlexec('SET C value %%(v)s WHERE X from_entity S, X relation_type R,''X constrained_by C, C cstrtype CT, CT name "SizeConstraint",''S name "%s", R name "%s"'%(etype,rtype),{'v':unicode(SizeConstraint(size).serialize())},ask_confirm=self.verbosity>=2)else:self.rqlexec('DELETE X constrained_by C WHERE X from_entity S, X relation_type R,''X constrained_by C, C cstrtype CT, CT name "SizeConstraint",''S name "%s", R name "%s"'%(etype,rtype),ask_confirm=self.verbosity>=2)# cleanup unused constraintsself.rqlexec('DELETE EConstraint C WHERE NOT X constrained_by C')ifcommit:self.commit()# Workflows handling ######################################################defcmd_add_state(self,name,stateof,initial=False,commit=False,**kwargs):"""method to ease workflow definition: add a state for one or more entity type(s) """stateeid=self.cmd_add_entity('State',name=name,**kwargs)ifnotisinstance(stateof,(list,tuple)):stateof=(stateof,)foretypeinstateof:# XXX ensure etype validityself.rqlexec('SET X state_of Y WHERE X eid %(x)s, Y name %(et)s',{'x':stateeid,'et':etype},'x',ask_confirm=False)ifinitial:self.rqlexec('SET ET initial_state S WHERE ET name %(et)s, S eid %(x)s',{'x':stateeid,'et':etype},'x',ask_confirm=False)ifcommit:self.commit()returnstateeiddefcmd_add_transition(self,name,transitionof,fromstates,tostate,requiredgroups=(),conditions=(),commit=False,**kwargs):"""method to ease workflow definition: add a transition for one or more entity type(s), from one or more state and to a single state """treid=self.cmd_add_entity('Transition',name=name,**kwargs)ifnotisinstance(transitionof,(list,tuple)):transitionof=(transitionof,)foretypeintransitionof:# XXX ensure etype validityself.rqlexec('SET X transition_of Y WHERE X eid %(x)s, Y name %(et)s',{'x':treid,'et':etype},'x',ask_confirm=False)forstateeidinfromstates:self.rqlexec('SET X allowed_transition Y WHERE X eid %(x)s, Y eid %(y)s',{'x':stateeid,'y':treid},'x',ask_confirm=False)self.rqlexec('SET X destination_state Y WHERE X eid %(x)s, Y eid %(y)s',{'x':treid,'y':tostate},'x',ask_confirm=False)self.cmd_set_transition_permissions(treid,requiredgroups,conditions,reset=False)ifcommit:self.commit()returntreiddefcmd_set_transition_permissions(self,treid,requiredgroups=(),conditions=(),reset=True,commit=False):"""set or add (if `reset` is False) groups and conditions for a transition """ifreset:self.rqlexec('DELETE T require_group G WHERE T eid %(x)s',{'x':treid},'x',ask_confirm=False)self.rqlexec('DELETE T condition R WHERE T eid %(x)s',{'x':treid},'x',ask_confirm=False)forgnameinrequiredgroups:### XXX ensure gname validityself.rqlexec('SET T require_group G WHERE T eid %(x)s, G name %(gn)s',{'x':treid,'gn':gname},'x',ask_confirm=False)ifisinstance(conditions,basestring):conditions=(conditions,)forexprinconditions:ifisinstance(expr,str):expr=unicode(expr)self.rqlexec('INSERT RQLExpression X: X exprtype "ERQLExpression", ''X expression %(expr)s, T condition X ''WHERE T eid %(x)s',{'x':treid,'expr':expr},'x',ask_confirm=False)ifcommit:self.commit()defcmd_set_state(self,eid,statename,commit=False):self.session.set_pool()# ensure pool is setentity=self.session.eid_rset(eid).get_entity(0,0)entity.change_state(entity.wf_state(statename).eid)ifcommit:self.commit()# EProperty handling ######################################################defcmd_property_value(self,pkey):rql='Any V WHERE X is EProperty, X pkey %(k)s, X value V'rset=self.rqlexec(rql,{'k':pkey},ask_confirm=False)returnrset[0][0]defcmd_set_property(self,pkey,value):value=unicode(value)try:prop=self.rqlexec('EProperty X WHERE X pkey %(k)s',{'k':pkey},ask_confirm=False).get_entity(0,0)except:self.cmd_add_entity('EProperty',pkey=unicode(pkey),value=value)else:self.rqlexec('SET X value %(v)s WHERE X pkey %(k)s',{'k':pkey,'v':value},ask_confirm=False)# other data migration commands ###########################################defcmd_add_entity(self,etype,*args,**kwargs):"""add a new entity of the given type"""rql='INSERT %s X'%etyperelations=[]restrictions=[]forrtype,rvarinargs:relations.append('X %s%s'%(rtype,rvar))restrictions.append('%s eid %s'%(rvar,kwargs.pop(rvar)))commit=kwargs.pop('commit',False)forattrinkwargs:relations.append('X %s%%(%s)s'%(attr,attr))ifrelations:rql='%s: %s'%(rql,', '.join(relations))ifrestrictions:rql='%s WHERE %s'%(rql,', '.join(restrictions))eid=self.rqlexec(rql,kwargs,ask_confirm=self.verbosity>=2).rows[0][0]ifcommit:self.commit()returneiddefsqlexec(self,sql,args=None,ask_confirm=True):"""execute the given sql if confirmed should only be used for low level stuff undoable with existing higher level actions """ifnotask_confirmorself.confirm('execute sql: %s ?'%sql):self.session.set_pool()# ensure pool is settry:cu=self.session.system_sql(sql,args)except:ex=sys.exc_info()[1]ifself.confirm('error: %s\nabort?'%ex):raisereturntry:returncu.fetchall()except:# no result to fetchreturndefrqlexec(self,rql,kwargs=None,cachekey=None,ask_confirm=True):"""rql action"""ifnotisinstance(rql,(tuple,list)):rql=((rql,kwargs),)res=Noneforrql,kwargsinrql:ifkwargs:msg='%s (%s)'%(rql,kwargs)else:msg=rqlifnotask_confirmorself.confirm('execute rql: %s ?'%msg):try:res=self.rqlcursor.execute(rql,kwargs,cachekey)exceptException,ex:ifself.confirm('error: %s\nabort?'%ex):raisereturnresdefrqliter(self,rql,kwargs=None,ask_confirm=True):returnForRqlIterator(self,rql,None,ask_confirm)defcmd_deactivate_verification_hooks(self):self.repo.hm.deactivate_verification_hooks()defcmd_reactivate_verification_hooks(self):self.repo.hm.reactivate_verification_hooks()# broken db commands ######################################################defcmd_change_attribute_type(self,etype,attr,newtype,commit=True):"""low level method to change the type of an entity attribute. This is a quick hack which has some drawback: * only works when the old type can be changed to the new type by the underlying rdbms (eg using ALTER TABLE) * the actual schema won't be updated until next startup """rschema=self.repo.schema.rschema(attr)oldtype=rschema.objects(etype)[0]rdefeid=rschema.rproperty(etype,oldtype,'eid')sql=("UPDATE EFRDef ""SET to_entity=(SELECT eid FROM EEType WHERE name='%s')""WHERE eid=%s")%(newtype,rdefeid)self.sqlexec(sql,ask_confirm=False)dbhelper=self.repo.system_source.dbhelpersqltype=dbhelper.TYPE_MAPPING[newtype]sql='ALTER TABLE %s ALTER COLUMN %s TYPE %s'%(etype,attr,sqltype)self.sqlexec(sql,ask_confirm=False)ifcommit:self.commit()defcmd_add_entity_type_table(self,etype,commit=True):"""low level method to create the sql table for an existing entity. This may be useful on accidental desync between the repository schema and a sql database """dbhelper=self.repo.system_source.dbhelpertablesql=eschema2sql(dbhelper,self.repo.schema.eschema(etype),prefix=SQL_PREFIX)forsqlintablesql.split(';'):ifsql.strip():self.sqlexec(sql)ifcommit:self.commit()defcmd_add_relation_type_table(self,rtype,commit=True):"""low level method to create the sql table for an existing relation. This may be useful on accidental desync between the repository schema and a sql database """dbhelper=self.repo.system_source.dbhelpertablesql=rschema2sql(dbhelper,self.repo.schema.rschema(rtype))forsqlintablesql.split(';'):ifsql.strip():self.sqlexec(sql)ifcommit:self.commit()classForRqlIterator:"""specific rql iterator to make the loop skipable"""def__init__(self,helper,rql,kwargs,ask_confirm):self._h=helperself.rql=rqlself.kwargs=kwargsself.ask_confirm=ask_confirmself._rsetit=Nonedef__iter__(self):returnselfdefnext(self):ifself._rsetitisnotNone:returnself._rsetit.next()rql,kwargs=self.rql,self.kwargsifkwargs:msg='%s (%s)'%(rql,kwargs)else:msg=rqlifself.ask_confirm:ifnotself._h.confirm('execute rql: %s ?'%msg):raiseStopIterationtry:rset=self._h.rqlcursor.execute(rql,kwargs)exceptException,ex:ifself._h.confirm('error: %s\nabort?'%ex):raiseelse:raiseStopIterationself._rsetit=iter(rset)returnself._rsetit.next()