closes #601987
1) sqlutils.restore_from_file have to use its confirm argument when
a command fail, to propose to continue there (this can't be handled
by the caller)
2) source.restore method hence needs to take this confirmation callback
as argument
3) properly fix places where 'drop' was given instead of 'confirm'
"""provide an abstract class for external sources using a sqlite database helper:organization: Logilab:copyright: 2007-2009 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"fromos.pathimportjoin,existsfromcubicwebimportserverfromcubicweb.server.sqlutilsimportSQL_PREFIX,SQLAdapterMixIn,sqlexecfromcubicweb.server.sourcesimportnative,rql2sqlfromcubicweb.server.sourcesimportAbstractSource,dbg_st_search,dbg_resultsclassConnectionWrapper(object):def__init__(self,source=None):self.source=sourceself._cnx=None@propertydeflogged_user(self):ifself._cnxisNone:self._cnx=self.source._sqlcnxreturnself._cnx.logged_userdefcursor(self):ifself._cnxisNone:self._cnx=self.source._sqlcnxifserver.DEBUG&server.DBG_SQL:print'sql cnx OPEN',self._cnxreturnself._cnx.cursor()defcommit(self):ifself._cnxisnotNone:ifserver.DEBUG&(server.DBG_SQL|server.DBG_RQL):print'sql cnx COMMIT',self._cnxself._cnx.commit()defrollback(self):ifself._cnxisnotNone:ifserver.DEBUG&(server.DBG_SQL|server.DBG_RQL):print'sql cnx ROLLBACK',self._cnxself._cnx.rollback()defclose(self):ifself._cnxisnotNone:ifserver.DEBUG&server.DBG_SQL:print'sql cnx CLOSE',self._cnxself._cnx.close()self._cnx=NoneclassSQLiteAbstractSource(AbstractSource):"""an abstract class for external sources using a sqlite database helper """sqlgen_class=rql2sql.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 contentdbpath=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)defbackup(self,backupfile):"""method called to create a backup of the source's data"""self.close_pool_connections()try:self.sqladapter.backup_to_file(backupfile)finally:self.open_pool_connections()defrestore(self,backupfile,confirm,drop):"""method called to restore a backup of source's data"""self.close_pool_connections()try:self.sqladapter.restore_from_file(backupfile,confirm,drop)finally:self.open_pool_connections()@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',),prefix=SQL_PREFIX)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()andself.dbpath: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 """# reset _cnx to ensure next thread using cnx will get a new# connectioncnx.close()defsyntax_tree_search(self,session,union,args=None,cachekey=None,varmap=None):"""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[]assertdbg_st_search(self.uri,union,varmap,args,cachekey)sql,query_args=self.rqlsqlgen.generate(union,args)args=self.sqladapter.merge_args(args,query_args)results=self.sqladapter.process_result(self.doexec(session,sql,args))assertdbg_results(results)returnresultsdeflocal_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 """attrs=self.sqladapter.preprocess_entity(entity)sql=self.sqladapter.sqlgen.insert(SQL_PREFIX+str(entity.e_schema),attrs)self.doexec(session,sql,attrs)defadd_entity(self,session,entity):"""add a new entity to the source"""raiseNotImplementedError()deflocal_update_entity(self,session,entity,attrs=None):"""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 """ifattrsisNone:attrs=self.sqladapter.preprocess_entity(entity)sql=self.sqladapter.sqlgen.update(SQL_PREFIX+str(entity.e_schema),attrs,[SQL_PREFIX+'eid'])self.doexec(session,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. """attrs={SQL_PREFIX+'eid':eid}sql=self.sqladapter.sqlgen.delete(SQL_PREFIX+etype,attrs)self.doexec(session,sql,attrs)deflocal_add_relation(self,session,subject,rtype,object):"""add a relation to the source This is not provided as add_relation 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 """attrs={'eid_from':subject,'eid_to':object}sql=self.sqladapter.sqlgen.insert('%s_relation'%rtype,attrs)self.doexec(session,sql,attrs)defadd_relation(self,session,subject,rtype,object):"""add a relation to the source"""raiseNotImplementedError()defdelete_relation(self,session,subject,rtype,object):"""delete a relation from the source"""rschema=self.schema.rschema(rtype)ifrschema.inlined:ifsubjectinsession.transaction_data.get('pendingeids',()):returntable=SQL_PREFIX+session.describe(subject)[0]column=SQL_PREFIX+rtypesql='UPDATE %s SET %s=NULL WHERE %seid=%%(eid)s'%(table,column,SQL_PREFIX)attrs={'eid':subject}else:attrs={'eid_from':subject,'eid_to':object}sql=self.sqladapter.sqlgen.delete('%s_relation'%rtype,attrs)self.doexec(session,sql,attrs)defdoexec(self,session,query,args=None):"""Execute a query. it's a function just so that it shows up in profiling """ifserver.DEBUG:print'exec',query,argscursor=session.pool[self.uri]try:# str(query) to avoid error if it's an unicode stringcursor.execute(str(query),args)exceptException,ex:self.critical("sql: %r\n args: %s\ndbms message: %r",query,args,ex.args[0])try:session.pool.connection(self.uri).rollback()self.critical('transaction has been rollbacked')except:passraisereturncursor