server/sources/extlite.py
changeset 257 4c7d3af7e94d
child 983 cf1caf460081
child 1137 9ce0ac82f94f
equal deleted inserted replaced
256:3dbee583526c 257:4c7d3af7e94d
       
     1 """provide an abstract class for external sources using a sqlite database helper
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2007-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 
       
    10 import time
       
    11 import threading
       
    12 from os.path import join, exists
       
    13 
       
    14 from cubicweb import server
       
    15 from cubicweb.server.sqlutils import sqlexec, SQLAdapterMixIn
       
    16 from cubicweb.server.sources import AbstractSource, native
       
    17 from cubicweb.server.sources.rql2sql import SQLGenerator
       
    18 
       
    19 def timeout_acquire(lock, timeout):
       
    20     while not lock.acquire(False):
       
    21         time.sleep(0.2)
       
    22         timeout -= 0.2
       
    23         if timeout <= 0:
       
    24             raise RuntimeError("svn source is busy, can't acquire connection lock")
       
    25         
       
    26 class ConnectionWrapper(object):
       
    27     def __init__(self, source=None):
       
    28         self.source = source
       
    29         self._cnx = None
       
    30 
       
    31     @property
       
    32     def cnx(self):
       
    33         if self._cnx is None:
       
    34             timeout_acquire(self.source._cnxlock, 5)
       
    35             self._cnx = self.source._sqlcnx
       
    36         return self._cnx
       
    37     
       
    38     def commit(self):
       
    39         if self._cnx is not None:
       
    40             self._cnx.commit()
       
    41         
       
    42     def rollback(self):
       
    43         if self._cnx is not None:
       
    44             self._cnx.rollback()
       
    45         
       
    46     def cursor(self):
       
    47         return self.cnx.cursor()
       
    48 
       
    49     
       
    50 class SQLiteAbstractSource(AbstractSource):
       
    51     """an abstract class for external sources using a sqlite database helper
       
    52     """
       
    53     sqlgen_class = SQLGenerator
       
    54     @classmethod
       
    55     def set_nonsystem_types(cls):
       
    56         # those entities are only in this source, we don't want them in the
       
    57         # system source
       
    58         for etype in cls.support_entities:
       
    59             native.NONSYSTEM_ETYPES.add(etype)
       
    60         for rtype in cls.support_relations:
       
    61             native.NONSYSTEM_RELATIONS.add(rtype)
       
    62         
       
    63     options = (
       
    64         ('helper-db-path',
       
    65          {'type' : 'string',
       
    66           'default': None,
       
    67           'help': 'path to the sqlite database file used to do queries on the \
       
    68 repository.',
       
    69           'inputlevel': 2,
       
    70           }),
       
    71     )
       
    72             
       
    73     def __init__(self, repo, appschema, source_config, *args, **kwargs):
       
    74         # the helper db is used to easy querying and will store everything but
       
    75         # actual file content 
       
    76         dbpath = source_config.get('helper-db-path')
       
    77         if dbpath is None:
       
    78             dbpath = join(repo.config.appdatahome,
       
    79                           '%(uri)s.sqlite' % source_config)
       
    80         self.dbpath = dbpath
       
    81         self.sqladapter = SQLAdapterMixIn({'db-driver': 'sqlite',
       
    82                                            'db-name': dbpath})
       
    83         # those attributes have to be initialized before ancestor's __init__
       
    84         # which will call set_schema
       
    85         self._need_sql_create = not exists(dbpath)
       
    86         self._need_full_import = self._need_sql_create
       
    87         AbstractSource.__init__(self, repo, appschema, source_config,
       
    88                                 *args, **kwargs)
       
    89         # sql database can only be accessed by one connection at a time, and a
       
    90         # connection can only be used by the thread which created it so:
       
    91         # * create the connection when needed
       
    92         # * use a lock to be sure only one connection is used
       
    93         self._cnxlock = threading.Lock()
       
    94         
       
    95     @property
       
    96     def _sqlcnx(self):
       
    97         # XXX: sqlite connections can only be used in the same thread, so
       
    98         #      create a new one each time necessary. If it appears to be time
       
    99         #      consuming, find another way
       
   100         return self.sqladapter.get_connection()
       
   101 
       
   102     def _is_schema_complete(self):
       
   103         for etype in self.support_entities:
       
   104             if not etype in self.schema:
       
   105                 self.warning('not ready to generate %s database, %s support missing from schema',
       
   106                              self.uri, etype)
       
   107                 return False
       
   108         for rtype in self.support_relations:
       
   109             if not rtype in self.schema:
       
   110                 self.warning('not ready to generate %s database, %s support missing from schema',
       
   111                              self.uri, rtype)
       
   112                 return False
       
   113         return True
       
   114 
       
   115     def _create_database(self):
       
   116         from yams.schema2sql import eschema2sql, rschema2sql
       
   117         from cubicweb.toolsutils import restrict_perms_to_user
       
   118         self.warning('initializing sqlite database for %s source' % self.uri)
       
   119         cnx = self._sqlcnx
       
   120         cu = cnx.cursor()
       
   121         schema = self.schema
       
   122         for etype in self.support_entities:
       
   123             eschema = schema.eschema(etype)
       
   124             createsqls = eschema2sql(self.sqladapter.dbhelper, eschema,
       
   125                                      skip_relations=('data',))
       
   126             sqlexec(createsqls, cu, withpb=False)
       
   127         for rtype in self.support_relations:
       
   128             rschema = schema.rschema(rtype)
       
   129             if not rschema.inlined:
       
   130                 sqlexec(rschema2sql(rschema), cu, withpb=False)
       
   131         cnx.commit()
       
   132         cnx.close()
       
   133         self._need_sql_create = False
       
   134         if self.repo.config['uid']:
       
   135             from logilab.common.shellutils import chown
       
   136             # database file must be owned by the uid of the server process
       
   137             self.warning('set %s as owner of the database file',
       
   138                          self.repo.config['uid'])
       
   139             chown(self.dbpath, self.repo.config['uid'])
       
   140         restrict_perms_to_user(self.dbpath, self.info)
       
   141         
       
   142     def set_schema(self, schema):
       
   143         super(SQLiteAbstractSource, self).set_schema(schema)
       
   144         if self._need_sql_create and self._is_schema_complete():
       
   145             self._create_database()
       
   146         self.rqlsqlgen = self.sqlgen_class(schema, self.sqladapter.dbhelper)
       
   147                 
       
   148     def get_connection(self):
       
   149         return ConnectionWrapper(self)
       
   150 
       
   151     def check_connection(self, cnx):
       
   152         """check connection validity, return None if the connection is still valid
       
   153         else a new connection (called when the pool using the given connection is
       
   154         being attached to a session)
       
   155 
       
   156         always return the connection to reset eventually cached cursor
       
   157         """
       
   158         return cnx
       
   159 
       
   160     def pool_reset(self, cnx):
       
   161         """the pool using the given connection is being reseted from its current
       
   162         attached session: release the connection lock if the connection wrapper
       
   163         has a connection set
       
   164         """
       
   165         if cnx._cnx is not None:
       
   166             try:
       
   167                 cnx._cnx.close()
       
   168                 cnx._cnx = None
       
   169             finally:
       
   170                 self._cnxlock.release()
       
   171         
       
   172     def syntax_tree_search(self, session, union,
       
   173                            args=None, cachekey=None, varmap=None, debug=0):
       
   174         """return result from this source for a rql query (actually from a rql 
       
   175         syntax tree and a solution dictionary mapping each used variable to a 
       
   176         possible type). If cachekey is given, the query necessary to fetch the
       
   177         results (but not the results themselves) may be cached using this key.
       
   178         """
       
   179         if self._need_sql_create:
       
   180             return []
       
   181         sql, query_args = self.rqlsqlgen.generate(union, args)
       
   182         if server.DEBUG:
       
   183             print self.uri, 'SOURCE RQL', union.as_string()
       
   184             print 'GENERATED SQL', sql
       
   185         args = self.sqladapter.merge_args(args, query_args)
       
   186         cursor = session.pool[self.uri]
       
   187         cursor.execute(sql, args)
       
   188         return self.sqladapter.process_result(cursor) 
       
   189 
       
   190     def local_add_entity(self, session, entity):
       
   191         """insert the entity in the local database.
       
   192 
       
   193         This is not provided as add_entity implementation since usually source
       
   194         don't want to simply do this, so let raise NotImplementedError and the
       
   195         source implementor may use this method if necessary
       
   196         """
       
   197         cu = session.pool[self.uri]
       
   198         attrs = self.sqladapter.preprocess_entity(entity)
       
   199         sql = self.sqladapter.sqlgen.insert(str(entity.e_schema), attrs)
       
   200         cu.execute(sql, attrs)
       
   201         
       
   202     def add_entity(self, session, entity):
       
   203         """add a new entity to the source"""
       
   204         raise NotImplementedError()
       
   205 
       
   206     def local_update_entity(self, session, entity):
       
   207         """update an entity in the source
       
   208 
       
   209         This is not provided as update_entity implementation since usually
       
   210         source don't want to simply do this, so let raise NotImplementedError
       
   211         and the source implementor may use this method if necessary
       
   212         """
       
   213         cu = session.pool[self.uri]
       
   214         attrs = self.sqladapter.preprocess_entity(entity)
       
   215         sql = self.sqladapter.sqlgen.update(str(entity.e_schema), attrs, ['eid'])
       
   216         cu.execute(sql, attrs)
       
   217         
       
   218     def update_entity(self, session, entity):
       
   219         """update an entity in the source"""
       
   220         raise NotImplementedError()
       
   221         
       
   222     def delete_entity(self, session, etype, eid):
       
   223         """delete an entity from the source
       
   224 
       
   225         this is not deleting a file in the svn but deleting entities from the
       
   226         source. Main usage is to delete repository content when a Repository
       
   227         entity is deleted.
       
   228         """
       
   229         sqlcursor = session.pool[self.uri]        
       
   230         attrs = {'eid': eid}
       
   231         sql = self.sqladapter.sqlgen.delete(etype, attrs)
       
   232         sqlcursor.execute(sql, attrs)
       
   233     
       
   234     def delete_relation(self, session, subject, rtype, object):
       
   235         """delete a relation from the source"""
       
   236         rschema = self.schema.rschema(rtype)
       
   237         if rschema.inlined:
       
   238             if subject in session.query_data('pendingeids', ()):
       
   239                 return
       
   240             etype = session.describe(subject)[0]
       
   241             sql = 'UPDATE %s SET %s=NULL WHERE eid=%%(eid)s' % (etype, rtype)
       
   242             attrs = {'eid' : subject}
       
   243         else:
       
   244             attrs = {'eid_from': subject, 'eid_to': object}
       
   245             sql = self.sqladapter.sqlgen.delete('%s_relation' % rtype, attrs)
       
   246         sqlcursor = session.pool[self.uri]        
       
   247         sqlcursor.execute(sql, attrs)