"""provide an abstract class for external sources using a sqlite database helper:organization: Logilab:copyright: 2007-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr"""__docformat__="restructuredtext en"importtimeimportthreadingfromos.pathimportjoin,existsfromcubicwebimportserverfromcubicweb.server.sqlutilsimportsqlexec,SQLAdapterMixInfromcubicweb.server.sourcesimportAbstractSource,nativefromcubicweb.server.sources.rql2sqlimportSQLGeneratordeftimeout_acquire(lock,timeout):whilenotlock.acquire(False):time.sleep(0.2)timeout-=0.2iftimeout<=0:raiseRuntimeError("svn source is busy, can't acquire connection lock")classConnectionWrapper(object):def__init__(self,source=None):self.source=sourceself._cnx=None@propertydefcnx(self):ifself._cnxisNone:timeout_acquire(self.source._cnxlock,5)self._cnx=self.source._sqlcnxreturnself._cnxdefcommit(self):ifself._cnxisnotNone:self._cnx.commit()defrollback(self):ifself._cnxisnotNone:self._cnx.rollback()defcursor(self):returnself.cnx.cursor()classSQLiteAbstractSource(AbstractSource):"""an abstract class for external sources using a sqlite database helper """sqlgen_class=SQLGenerator@classmethoddefset_nonsystem_types(cls):# those entities are only in this source, we don't want them in the# system sourceforetypeincls.support_entities:native.NONSYSTEM_ETYPES.add(etype)forrtypeincls.support_relations:native.NONSYSTEM_RELATIONS.add(rtype)options=(('helper-db-path',{'type':'string','default':None,'help':'path to the sqlite database file used to do queries on the \repository.','inputlevel':2,}),)def__init__(self,repo,appschema,source_config,*args,**kwargs):# the helper db is used to easy querying and will store everything but# actual file content dbpath=source_config.get('helper-db-path')ifdbpathisNone:dbpath=join(repo.config.appdatahome,'%(uri)s.sqlite'%source_config)self.dbpath=dbpathself.sqladapter=SQLAdapterMixIn({'db-driver':'sqlite','db-name':dbpath})# those attributes have to be initialized before ancestor's __init__# which will call set_schemaself._need_sql_create=notexists(dbpath)self._need_full_import=self._need_sql_createAbstractSource.__init__(self,repo,appschema,source_config,*args,**kwargs)# sql database can only be accessed by one connection at a time, and a# connection can only be used by the thread which created it so:# * create the connection when needed# * use a lock to be sure only one connection is usedself._cnxlock=threading.Lock()@propertydef_sqlcnx(self):# XXX: sqlite connections can only be used in the same thread, so# create a new one each time necessary. If it appears to be time# consuming, find another wayreturnself.sqladapter.get_connection()def_is_schema_complete(self):foretypeinself.support_entities:ifnotetypeinself.schema:self.warning('not ready to generate %s database, %s support missing from schema',self.uri,etype)returnFalseforrtypeinself.support_relations:ifnotrtypeinself.schema:self.warning('not ready to generate %s database, %s support missing from schema',self.uri,rtype)returnFalsereturnTruedef_create_database(self):fromyams.schema2sqlimporteschema2sql,rschema2sqlfromcubicweb.toolsutilsimportrestrict_perms_to_userself.warning('initializing sqlite database for %s source'%self.uri)cnx=self._sqlcnxcu=cnx.cursor()schema=self.schemaforetypeinself.support_entities:eschema=schema.eschema(etype)createsqls=eschema2sql(self.sqladapter.dbhelper,eschema,skip_relations=('data',))sqlexec(createsqls,cu,withpb=False)forrtypeinself.support_relations:rschema=schema.rschema(rtype)ifnotrschema.inlined:sqlexec(rschema2sql(rschema),cu,withpb=False)cnx.commit()cnx.close()self._need_sql_create=Falseifself.repo.config['uid']:fromlogilab.common.shellutilsimportchown# database file must be owned by the uid of the server processself.warning('set %s as owner of the database file',self.repo.config['uid'])chown(self.dbpath,self.repo.config['uid'])restrict_perms_to_user(self.dbpath,self.info)defset_schema(self,schema):super(SQLiteAbstractSource,self).set_schema(schema)ifself._need_sql_createandself._is_schema_complete():self._create_database()self.rqlsqlgen=self.sqlgen_class(schema,self.sqladapter.dbhelper)defget_connection(self):returnConnectionWrapper(self)defcheck_connection(self,cnx):"""check connection validity, return None if the connection is still valid else a new connection (called when the pool using the given connection is being attached to a session) always return the connection to reset eventually cached cursor """returncnxdefpool_reset(self,cnx):"""the pool using the given connection is being reseted from its current attached session: release the connection lock if the connection wrapper has a connection set """ifcnx._cnxisnotNone:try:cnx._cnx.close()cnx._cnx=Nonefinally:self._cnxlock.release()defsyntax_tree_search(self,session,union,args=None,cachekey=None,varmap=None,debug=0):"""return result from this source for a rql query (actually from a rql syntax tree and a solution dictionary mapping each used variable to a possible type). If cachekey is given, the query necessary to fetch the results (but not the results themselves) may be cached using this key. """ifself._need_sql_create:return[]sql,query_args=self.rqlsqlgen.generate(union,args)ifserver.DEBUG:printself.uri,'SOURCE RQL',union.as_string()print'GENERATED SQL',sqlargs=self.sqladapter.merge_args(args,query_args)cursor=session.pool[self.uri]cursor.execute(sql,args)returnself.sqladapter.process_result(cursor)deflocal_add_entity(self,session,entity):"""insert the entity in the local database. This is not provided as add_entity implementation since usually source don't want to simply do this, so let raise NotImplementedError and the source implementor may use this method if necessary """cu=session.pool[self.uri]attrs=self.sqladapter.preprocess_entity(entity)sql=self.sqladapter.sqlgen.insert(str(entity.e_schema),attrs)cu.execute(sql,attrs)defadd_entity(self,session,entity):"""add a new entity to the source"""raiseNotImplementedError()deflocal_update_entity(self,session,entity):"""update an entity in the source This is not provided as update_entity implementation since usually source don't want to simply do this, so let raise NotImplementedError and the source implementor may use this method if necessary """cu=session.pool[self.uri]attrs=self.sqladapter.preprocess_entity(entity)sql=self.sqladapter.sqlgen.update(str(entity.e_schema),attrs,['eid'])cu.execute(sql,attrs)defupdate_entity(self,session,entity):"""update an entity in the source"""raiseNotImplementedError()defdelete_entity(self,session,etype,eid):"""delete an entity from the source this is not deleting a file in the svn but deleting entities from the source. Main usage is to delete repository content when a Repository entity is deleted. """sqlcursor=session.pool[self.uri]attrs={'eid':eid}sql=self.sqladapter.sqlgen.delete(etype,attrs)sqlcursor.execute(sql,attrs)defdelete_relation(self,session,subject,rtype,object):"""delete a relation from the source"""rschema=self.schema.rschema(rtype)ifrschema.inlined:ifsubjectinsession.query_data('pendingeids',()):returnetype=session.describe(subject)[0]sql='UPDATE %s SET %s=NULL WHERE eid=%%(eid)s'%(etype,rtype)attrs={'eid':subject}else:attrs={'eid_from':subject,'eid_to':object}sql=self.sqladapter.sqlgen.delete('%s_relation'%rtype,attrs)sqlcursor=session.pool[self.uri]sqlcursor.execute(sql,attrs)