server/sources/native.py
changeset 4913 083b4d454192
parent 4902 4e67a538e476
child 4943 7f5b83578fec
equal deleted inserted replaced
4912:9767cc516b4f 4913:083b4d454192
     9 :organization: Logilab
     9 :organization: Logilab
    10 :copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
    10 :copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
    11 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
    11 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
    12 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
    12 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
    13 """
    13 """
       
    14 from __future__ import with_statement
       
    15 
    14 __docformat__ = "restructuredtext en"
    16 __docformat__ = "restructuredtext en"
    15 
    17 
       
    18 from pickle import loads, dumps
    16 from threading import Lock
    19 from threading import Lock
    17 from datetime import datetime
    20 from datetime import datetime
    18 from base64 import b64decode, b64encode
    21 from base64 import b64decode, b64encode
    19 
    22 
    20 from logilab.common.compat import any
    23 from logilab.common.compat import any
    22 from logilab.common.decorators import cached, clear_cache
    25 from logilab.common.decorators import cached, clear_cache
    23 from logilab.common.configuration import Method
    26 from logilab.common.configuration import Method
    24 from logilab.common.shellutils import getlogin
    27 from logilab.common.shellutils import getlogin
    25 from logilab.database import get_db_helper
    28 from logilab.database import get_db_helper
    26 
    29 
    27 from cubicweb import UnknownEid, AuthenticationError, Binary, server
    30 from cubicweb import UnknownEid, AuthenticationError, Binary, server, neg_role
       
    31 from cubicweb import transaction as tx
       
    32 from cubicweb.schema import VIRTUAL_RTYPES
    28 from cubicweb.cwconfig import CubicWebNoAppConfiguration
    33 from cubicweb.cwconfig import CubicWebNoAppConfiguration
    29 from cubicweb.server import hook
    34 from cubicweb.server import hook
    30 from cubicweb.server.utils import crypt_password
    35 from cubicweb.server.utils import crypt_password
    31 from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
    36 from cubicweb.server.sqlutils import SQL_PREFIX, SQLAdapterMixIn
    32 from cubicweb.server.rqlannotation import set_qdata
    37 from cubicweb.server.rqlannotation import set_qdata
       
    38 from cubicweb.server.session import hooks_control
    33 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
    39 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
    34 from cubicweb.server.sources.rql2sql import SQLGenerator
    40 from cubicweb.server.sources.rql2sql import SQLGenerator
    35 
    41 
    36 
    42 
    37 ATTR_MAP = {}
    43 ATTR_MAP = {}
    89         attr = 'mtime'
    95         attr = 'mtime'
    90     else:
    96     else:
    91         attr = 'dtime'
    97         attr = 'dtime'
    92     return 'SELECT type, eid FROM %s WHERE %s AND %s > %%(time)s' % (
    98     return 'SELECT type, eid FROM %s WHERE %s AND %s > %%(time)s' % (
    93         table, restr, attr)
    99         table, restr, attr)
       
   100 
       
   101 
       
   102 def sql_or_clauses(sql, clauses):
       
   103     select, restr = sql.split(' WHERE ', 1)
       
   104     restrclauses = restr.split(' AND ')
       
   105     for clause in clauses:
       
   106         restrclauses.remove(clause)
       
   107     if restrclauses:
       
   108         restr = '%s AND (%s)' % (' AND '.join(restrclauses),
       
   109                                  ' OR '.join(clauses))
       
   110     else:
       
   111         restr = '(%s)' % ' OR '.join(clauses)
       
   112     return '%s WHERE %s' % (select, restr)
       
   113 
       
   114 
       
   115 class UndoException(Exception):
       
   116     """something went wrong during undoing"""
       
   117 
       
   118 
       
   119 def _undo_check_relation_target(tentity, rdef, role):
       
   120     """check linked entity has not been redirected for this relation"""
       
   121     card = rdef.role_cardinality(role)
       
   122     if card in '?1' and tentity.related(rdef.rtype, role):
       
   123         raise UndoException(tentity._cw._(
       
   124             "Can't restore %(role)s relation %(rtype)s to entity %(eid)s which "
       
   125             "is already linked using this relation.")
       
   126                             % {'role': neg_role(role),
       
   127                                'rtype': rdef.rtype,
       
   128                                'eid': tentity.eid})
    94 
   129 
    95 
   130 
    96 class NativeSQLSource(SQLAdapterMixIn, AbstractSource):
   131 class NativeSQLSource(SQLAdapterMixIn, AbstractSource):
    97     """adapter for source using the native cubicweb schema (see below)
   132     """adapter for source using the native cubicweb schema (see below)
    98     """
   133     """
   368                     continue
   403                     continue
   369 
   404 
   370     def add_entity(self, session, entity):
   405     def add_entity(self, session, entity):
   371         """add a new entity to the source"""
   406         """add a new entity to the source"""
   372         attrs = self.preprocess_entity(entity)
   407         attrs = self.preprocess_entity(entity)
   373         sql = self.sqlgen.insert(SQL_PREFIX + str(entity.e_schema), attrs)
   408         sql = self.sqlgen.insert(SQL_PREFIX + entity.__regid__, attrs)
   374         self.doexec(session, sql, attrs)
   409         self.doexec(session, sql, attrs)
       
   410         if session.undoable_action('C', entity.__regid__):
       
   411             self._record_tx_action(session, 'tx_entity_actions', 'C',
       
   412                                    etype=entity.__regid__, eid=entity.eid)
   375 
   413 
   376     def update_entity(self, session, entity):
   414     def update_entity(self, session, entity):
   377         """replace an entity in the source"""
   415         """replace an entity in the source"""
   378         attrs = self.preprocess_entity(entity)
   416         attrs = self.preprocess_entity(entity)
   379         sql = self.sqlgen.update(SQL_PREFIX + str(entity.e_schema), attrs,
   417         if session.undoable_action('U', entity.__regid__):
   380                                  [SQL_PREFIX + 'eid'])
   418             changes = self._save_attrs(session, entity, attrs)
       
   419             self._record_tx_action(session, 'tx_entity_actions', 'U',
       
   420                                    etype=entity.__regid__, eid=entity.eid,
       
   421                                    changes=self._binary(dumps(changes)))
       
   422         sql = self.sqlgen.update(SQL_PREFIX + entity.__regid__, attrs,
       
   423                                  ['cw_eid'])
   381         self.doexec(session, sql, attrs)
   424         self.doexec(session, sql, attrs)
   382 
   425 
   383     def delete_entity(self, session, etype, eid):
   426     def delete_entity(self, session, entity):
   384         """delete an entity from the source"""
   427         """delete an entity from the source"""
   385         attrs = {SQL_PREFIX + 'eid': eid}
   428         if session.undoable_action('D', entity.__regid__):
   386         sql = self.sqlgen.delete(SQL_PREFIX + etype, attrs)
   429             attrs = [SQL_PREFIX + r.type
       
   430                      for r in entity.e_schema.subject_relations()
       
   431                      if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
       
   432             changes = self._save_attrs(session, entity, attrs)
       
   433             self._record_tx_action(session, 'tx_entity_actions', 'D',
       
   434                                    etype=entity.__regid__, eid=entity.eid,
       
   435                                    changes=self._binary(dumps(changes)))
       
   436         attrs = {'cw_eid': entity.eid}
       
   437         sql = self.sqlgen.delete(SQL_PREFIX + entity.__regid__, attrs)
   387         self.doexec(session, sql, attrs)
   438         self.doexec(session, sql, attrs)
   388 
   439 
   389     def add_relation(self, session, subject, rtype, object, inlined=False):
   440     def _add_relation(self, session, subject, rtype, object, inlined=False):
   390         """add a relation to the source"""
   441         """add a relation to the source"""
   391         if inlined is False:
   442         if inlined is False:
   392             attrs = {'eid_from': subject, 'eid_to': object}
   443             attrs = {'eid_from': subject, 'eid_to': object}
   393             sql = self.sqlgen.insert('%s_relation' % rtype, attrs)
   444             sql = self.sqlgen.insert('%s_relation' % rtype, attrs)
   394         else: # used by data import
   445         else: # used by data import
   395             etype = session.describe(subject)[0]
   446             etype = session.describe(subject)[0]
   396             attrs = {SQL_PREFIX + 'eid': subject, SQL_PREFIX + rtype: object}
   447             attrs = {'cw_eid': subject, SQL_PREFIX + rtype: object}
   397             sql = self.sqlgen.update(SQL_PREFIX + etype, attrs,
   448             sql = self.sqlgen.update(SQL_PREFIX + etype, attrs,
   398                                      [SQL_PREFIX + 'eid'])
   449                                      ['cw_eid'])
   399         self.doexec(session, sql, attrs)
   450         self.doexec(session, sql, attrs)
       
   451 
       
   452     def add_relation(self, session, subject, rtype, object, inlined=False):
       
   453         """add a relation to the source"""
       
   454         self._add_relation(session, subject, rtype, object, inlined)
       
   455         if session.undoable_action('A', rtype):
       
   456             self._record_tx_action(session, 'tx_relation_actions', 'A',
       
   457                                    eid_from=subject, rtype=rtype, eid_to=object)
   400 
   458 
   401     def delete_relation(self, session, subject, rtype, object):
   459     def delete_relation(self, session, subject, rtype, object):
   402         """delete a relation from the source"""
   460         """delete a relation from the source"""
   403         rschema = self.schema.rschema(rtype)
   461         rschema = self.schema.rschema(rtype)
   404         if rschema.inlined:
   462         if rschema.inlined:
   409             attrs = {'eid' : subject}
   467             attrs = {'eid' : subject}
   410         else:
   468         else:
   411             attrs = {'eid_from': subject, 'eid_to': object}
   469             attrs = {'eid_from': subject, 'eid_to': object}
   412             sql = self.sqlgen.delete('%s_relation' % rtype, attrs)
   470             sql = self.sqlgen.delete('%s_relation' % rtype, attrs)
   413         self.doexec(session, sql, attrs)
   471         self.doexec(session, sql, attrs)
       
   472         if session.undoable_action('R', rtype):
       
   473             self._record_tx_action(session, 'tx_relation_actions', 'R',
       
   474                                    eid_from=subject, rtype=rtype, eid_to=object)
   414 
   475 
   415     def doexec(self, session, query, args=None, rollback=True):
   476     def doexec(self, session, query, args=None, rollback=True):
   416         """Execute a query.
   477         """Execute a query.
   417         it's a function just so that it shows up in profiling
   478         it's a function just so that it shows up in profiling
   418         """
   479         """
   465                 pass
   526                 pass
   466             raise
   527             raise
   467 
   528 
   468     # short cut to method requiring advanced db helper usage ##################
   529     # short cut to method requiring advanced db helper usage ##################
   469 
   530 
       
   531     def binary_to_str(self, value):
       
   532         return self.dbhelper.dbapi_module.binary_to_str(value)
       
   533 
   470     def create_index(self, session, table, column, unique=False):
   534     def create_index(self, session, table, column, unique=False):
   471         cursor = LogCursor(session.pool[self.uri])
   535         cursor = LogCursor(session.pool[self.uri])
   472         self.dbhelper.create_index(cursor, table, column, unique)
   536         self.dbhelper.create_index(cursor, table, column, unique)
   473 
   537 
   474     def drop_index(self, session, table, column, unique=False):
   538     def drop_index(self, session, table, column, unique=False):
   479 
   543 
   480     def eid_type_source(self, session, eid):
   544     def eid_type_source(self, session, eid):
   481         """return a tuple (type, source, extid) for the entity with id <eid>"""
   545         """return a tuple (type, source, extid) for the entity with id <eid>"""
   482         sql = 'SELECT type, source, extid FROM entities WHERE eid=%s' % eid
   546         sql = 'SELECT type, source, extid FROM entities WHERE eid=%s' % eid
   483         try:
   547         try:
   484             res = session.system_sql(sql).fetchone()
   548             res = self.doexec(session, sql).fetchone()
   485         except:
   549         except:
   486             assert session.pool, 'session has no pool set'
   550             assert session.pool, 'session has no pool set'
   487             raise UnknownEid(eid)
   551             raise UnknownEid(eid)
   488         if res is None:
   552         if res is None:
   489             raise UnknownEid(eid)
   553             raise UnknownEid(eid)
   494         return res
   558         return res
   495 
   559 
   496     def extid2eid(self, session, source, extid):
   560     def extid2eid(self, session, source, extid):
   497         """get eid from an external id. Return None if no record found."""
   561         """get eid from an external id. Return None if no record found."""
   498         assert isinstance(extid, str)
   562         assert isinstance(extid, str)
   499         cursor = session.system_sql('SELECT eid FROM entities WHERE '
   563         cursor = self.doexec(session,
   500                                     'extid=%(x)s AND source=%(s)s',
   564                              'SELECT eid FROM entities '
   501                                     {'x': b64encode(extid), 's': source.uri})
   565                              'WHERE extid=%(x)s AND source=%(s)s',
       
   566                              {'x': b64encode(extid), 's': source.uri})
   502         # XXX testing rowcount cause strange bug with sqlite, results are there
   567         # XXX testing rowcount cause strange bug with sqlite, results are there
   503         #     but rowcount is 0
   568         #     but rowcount is 0
   504         #if cursor.rowcount > 0:
   569         #if cursor.rowcount > 0:
   505         try:
   570         try:
   506             result = cursor.fetchone()
   571             result = cursor.fetchone()
   527                 cursor = self.doexec(session, sql)
   592                 cursor = self.doexec(session, sql)
   528             return cursor.fetchone()[0]
   593             return cursor.fetchone()[0]
   529         finally:
   594         finally:
   530             self._eid_creation_lock.release()
   595             self._eid_creation_lock.release()
   531 
   596 
   532     def add_info(self, session, entity, source, extid=None, complete=True):
   597     def add_info(self, session, entity, source, extid, complete):
   533         """add type and source info for an eid into the system table"""
   598         """add type and source info for an eid into the system table"""
   534         # begin by inserting eid/type/source/extid into the entities table
   599         # begin by inserting eid/type/source/extid into the entities table
   535         if extid is not None:
   600         if extid is not None:
   536             assert isinstance(extid, str)
   601             assert isinstance(extid, str)
   537             extid = b64encode(extid)
   602             extid = b64encode(extid)
   538         attrs = {'type': entity.__regid__, 'eid': entity.eid, 'extid': extid,
   603         attrs = {'type': entity.__regid__, 'eid': entity.eid, 'extid': extid,
   539                  'source': source.uri, 'mtime': datetime.now()}
   604                  'source': source.uri, 'mtime': datetime.now()}
   540         session.system_sql(self.sqlgen.insert('entities', attrs), attrs)
   605         self.doexec(session, self.sqlgen.insert('entities', attrs), attrs)
   541         # now we can update the full text index
   606         # now we can update the full text index
   542         if self.do_fti and self.need_fti_indexation(entity.__regid__):
   607         if self.do_fti and self.need_fti_indexation(entity.__regid__):
   543             if complete:
   608             if complete:
   544                 entity.complete(entity.e_schema.indexable_attributes())
   609                 entity.complete(entity.e_schema.indexable_attributes())
   545             FTIndexEntityOp(session, entity=entity)
   610             FTIndexEntityOp(session, entity=entity)
   546 
   611 
   547     def update_info(self, session, entity, need_fti_update):
   612     def update_info(self, session, entity, need_fti_update):
       
   613         """mark entity as being modified, fulltext reindex if needed"""
   548         if self.do_fti and need_fti_update:
   614         if self.do_fti and need_fti_update:
   549             # reindex the entity only if this query is updating at least
   615             # reindex the entity only if this query is updating at least
   550             # one indexable attribute
   616             # one indexable attribute
   551             FTIndexEntityOp(session, entity=entity)
   617             FTIndexEntityOp(session, entity=entity)
   552         # update entities.mtime
   618         # update entities.mtime
   553         attrs = {'eid': entity.eid, 'mtime': datetime.now()}
   619         attrs = {'eid': entity.eid, 'mtime': datetime.now()}
   554         session.system_sql(self.sqlgen.update('entities', attrs, ['eid']), attrs)
   620         self.doexec(session, self.sqlgen.update('entities', attrs, ['eid']), attrs)
   555 
   621 
   556     def delete_info(self, session, eid, etype, uri, extid):
   622     def delete_info(self, session, entity, uri, extid):
   557         """delete system information on deletion of an entity by transfering
   623         """delete system information on deletion of an entity by transfering
   558         record from the entities table to the deleted_entities table
   624         record from the entities table to the deleted_entities table
   559         """
   625         """
   560         attrs = {'eid': eid}
   626         attrs = {'eid': entity.eid}
   561         session.system_sql(self.sqlgen.delete('entities', attrs), attrs)
   627         self.doexec(session, self.sqlgen.delete('entities', attrs), attrs)
   562         if extid is not None:
   628         if extid is not None:
   563             assert isinstance(extid, str), type(extid)
   629             assert isinstance(extid, str), type(extid)
   564             extid = b64encode(extid)
   630             extid = b64encode(extid)
   565         attrs = {'type': etype, 'eid': eid, 'extid': extid,
   631         attrs = {'type': entity.__regid__, 'eid': entity.eid, 'extid': extid,
   566                  'source': uri, 'dtime': datetime.now()}
   632                  'source': uri, 'dtime': datetime.now(),
   567         session.system_sql(self.sqlgen.insert('deleted_entities', attrs), attrs)
   633                  }
       
   634         self.doexec(session, self.sqlgen.insert('deleted_entities', attrs), attrs)
   568 
   635 
   569     def modified_entities(self, session, etypes, mtime):
   636     def modified_entities(self, session, etypes, mtime):
   570         """return a 2-uple:
   637         """return a 2-uple:
   571         * list of (etype, eid) of entities of the given types which have been
   638         * list of (etype, eid) of entities of the given types which have been
   572           modified since the given timestamp (actually entities whose full text
   639           modified since the given timestamp (actually entities whose full text
   573           index content has changed)
   640           index content has changed)
   574         * list of (etype, eid) of entities of the given types which have been
   641         * list of (etype, eid) of entities of the given types which have been
   575           deleted since the given timestamp
   642           deleted since the given timestamp
   576         """
   643         """
   577         modsql = _modified_sql('entities', etypes)
   644         modsql = _modified_sql('entities', etypes)
   578         cursor = session.system_sql(modsql, {'time': mtime})
   645         cursor = self.doexec(session, modsql, {'time': mtime})
   579         modentities = cursor.fetchall()
   646         modentities = cursor.fetchall()
   580         delsql = _modified_sql('deleted_entities', etypes)
   647         delsql = _modified_sql('deleted_entities', etypes)
   581         cursor = session.system_sql(delsql, {'time': mtime})
   648         cursor = self.doexec(session, delsql, {'time': mtime})
   582         delentities = cursor.fetchall()
   649         delentities = cursor.fetchall()
   583         return modentities, delentities
   650         return modentities, delentities
       
   651 
       
   652     # undo support #############################################################
       
   653 
       
   654     def undoable_transactions(self, session, ueid=None, **actionfilters):
       
   655         """See :class:`cubicweb.dbapi.Connection.undoable_transactions`"""
       
   656         # force filtering to session's user if not a manager
       
   657         if not session.user.is_in_group('managers'):
       
   658             ueid = session.user.eid
       
   659         restr = {}
       
   660         if ueid is not None:
       
   661             restr['tx_user'] = ueid
       
   662         sql = self.sqlgen.select('transactions', restr, ('tx_uuid', 'tx_time', 'tx_user'))
       
   663         if actionfilters:
       
   664             # we will need subqueries to filter transactions according to
       
   665             # actions done
       
   666             tearestr = {} # filters on the tx_entity_actions table
       
   667             trarestr = {} # filters on the tx_relation_actions table
       
   668             genrestr = {} # generic filters, appliyable to both table
       
   669             # unless public explicitly set to false, we only consider public
       
   670             # actions
       
   671             if actionfilters.pop('public', True):
       
   672                 genrestr['txa_public'] = True
       
   673             # put additional filters in trarestr and/or tearestr
       
   674             for key, val in actionfilters.iteritems():
       
   675                 if key == 'etype':
       
   676                     # filtering on etype implies filtering on entity actions
       
   677                     # only, and with no eid specified
       
   678                     assert actionfilters.get('action', 'C') in 'CUD'
       
   679                     assert not 'eid' in actionfilters
       
   680                     tearestr['etype'] = val
       
   681                 elif key == 'eid':
       
   682                     # eid filter may apply to 'eid' of tx_entity_actions or to
       
   683                     # 'eid_from' OR 'eid_to' of tx_relation_actions
       
   684                     if actionfilters.get('action', 'C') in 'CUD':
       
   685                         tearestr['eid'] = val
       
   686                     if actionfilters.get('action', 'A') in 'AR':
       
   687                         trarestr['eid_from'] = val
       
   688                         trarestr['eid_to'] = val
       
   689                 elif key == 'action':
       
   690                     if val in 'CUD':
       
   691                         tearestr['txa_action'] = val
       
   692                     else:
       
   693                         assert val in 'AR'
       
   694                         trarestr['txa_action'] = val
       
   695                 else:
       
   696                     raise AssertionError('unknow filter %s' % key)
       
   697             assert trarestr or tearestr, "can't only filter on 'public'"
       
   698             subqsqls = []
       
   699             # append subqueries to the original query, using EXISTS()
       
   700             if trarestr or (genrestr and not tearestr):
       
   701                 trarestr.update(genrestr)
       
   702                 trasql = self.sqlgen.select('tx_relation_actions', trarestr, ('1',))
       
   703                 if 'eid_from' in trarestr:
       
   704                     # replace AND by OR between eid_from/eid_to restriction
       
   705                     trasql = sql_or_clauses(trasql, ['eid_from = %(eid_from)s',
       
   706                                                      'eid_to = %(eid_to)s'])
       
   707                 trasql += ' AND transactions.tx_uuid=tx_relation_actions.tx_uuid'
       
   708                 subqsqls.append('EXISTS(%s)' % trasql)
       
   709             if tearestr or (genrestr and not trarestr):
       
   710                 tearestr.update(genrestr)
       
   711                 teasql = self.sqlgen.select('tx_entity_actions', tearestr, ('1',))
       
   712                 teasql += ' AND transactions.tx_uuid=tx_entity_actions.tx_uuid'
       
   713                 subqsqls.append('EXISTS(%s)' % teasql)
       
   714             if restr:
       
   715                 sql += ' AND %s' % ' OR '.join(subqsqls)
       
   716             else:
       
   717                 sql += ' WHERE %s' % ' OR '.join(subqsqls)
       
   718             restr.update(trarestr)
       
   719             restr.update(tearestr)
       
   720         # we want results ordered by transaction's time descendant
       
   721         sql += ' ORDER BY tx_time DESC'
       
   722         cu = self.doexec(session, sql, restr)
       
   723         # turn results into transaction objects
       
   724         return [tx.Transaction(*args) for args in cu.fetchall()]
       
   725 
       
   726     def tx_info(self, session, txuuid):
       
   727         """See :class:`cubicweb.dbapi.Connection.transaction_info`"""
       
   728         return tx.Transaction(txuuid, *self._tx_info(session, txuuid))
       
   729 
       
   730     def tx_actions(self, session, txuuid, public):
       
   731         """See :class:`cubicweb.dbapi.Connection.transaction_actions`"""
       
   732         self._tx_info(session, txuuid)
       
   733         restr = {'tx_uuid': txuuid}
       
   734         if public:
       
   735             restr['txa_public'] = True
       
   736         sql = self.sqlgen.select('tx_entity_actions', restr,
       
   737                                  ('txa_action', 'txa_public', 'txa_order',
       
   738                                   'etype', 'eid', 'changes'))
       
   739         cu = self.doexec(session, sql, restr)
       
   740         actions = [tx.EntityAction(a,p,o,et,e,c and loads(self.binary_to_str(c)))
       
   741                    for a,p,o,et,e,c in cu.fetchall()]
       
   742         sql = self.sqlgen.select('tx_relation_actions', restr,
       
   743                                  ('txa_action', 'txa_public', 'txa_order',
       
   744                                   'rtype', 'eid_from', 'eid_to'))
       
   745         cu = self.doexec(session, sql, restr)
       
   746         actions += [tx.RelationAction(*args) for args in cu.fetchall()]
       
   747         return sorted(actions, key=lambda x: x.order)
       
   748 
       
   749     def undo_transaction(self, session, txuuid):
       
   750         """See :class:`cubicweb.dbapi.Connection.undo_transaction`"""
       
   751         # set mode so pool isn't released subsquently until commit/rollback
       
   752         session.mode = 'write'
       
   753         errors = []
       
   754         with hooks_control(session, session.HOOKS_DENY_ALL, 'integrity'):
       
   755             for action in reversed(self.tx_actions(session, txuuid, False)):
       
   756                 undomethod = getattr(self, '_undo_%s' % action.action.lower())
       
   757                 errors += undomethod(session, action)
       
   758         # remove the transactions record
       
   759         self.doexec(session,
       
   760                     "DELETE FROM transactions WHERE tx_uuid='%s'" % txuuid)
       
   761         return errors
       
   762 
       
   763     def start_undoable_transaction(self, session, uuid):
       
   764         """session callback to insert a transaction record in the transactions
       
   765         table when some undoable transaction is started
       
   766         """
       
   767         ueid = session.user.eid
       
   768         attrs = {'tx_uuid': uuid, 'tx_user': ueid, 'tx_time': datetime.now()}
       
   769         self.doexec(session, self.sqlgen.insert('transactions', attrs), attrs)
       
   770 
       
   771     def _save_attrs(self, session, entity, attrs):
       
   772         """return a pickleable dictionary containing current values for given
       
   773         attributes of the entity
       
   774         """
       
   775         restr = {'cw_eid': entity.eid}
       
   776         sql = self.sqlgen.select(SQL_PREFIX + entity.__regid__, restr, attrs)
       
   777         cu = self.doexec(session, sql, restr)
       
   778         values = dict(zip(attrs, cu.fetchone()))
       
   779         # ensure backend specific binary are converted back to string
       
   780         eschema = entity.e_schema
       
   781         for column in attrs:
       
   782             # [3:] remove 'cw_' prefix
       
   783             attr = column[3:]
       
   784             if not eschema.subjrels[attr].final:
       
   785                 continue
       
   786             if eschema.destination(attr) in ('Password', 'Bytes'):
       
   787                 value = values[column]
       
   788                 if value is not None:
       
   789                     values[column] = self.binary_to_str(value)
       
   790         return values
       
   791 
       
   792     def _record_tx_action(self, session, table, action, **kwargs):
       
   793         """record a transaction action in the given table (either
       
   794         'tx_entity_actions' or 'tx_relation_action')
       
   795         """
       
   796         kwargs['tx_uuid'] = session.transaction_uuid()
       
   797         kwargs['txa_action'] = action
       
   798         kwargs['txa_order'] = session.transaction_inc_action_counter()
       
   799         kwargs['txa_public'] = session.running_dbapi_query
       
   800         self.doexec(session, self.sqlgen.insert(table, kwargs), kwargs)
       
   801 
       
   802     def _tx_info(self, session, txuuid):
       
   803         """return transaction's time and user of the transaction with the given uuid.
       
   804 
       
   805         raise `NoSuchTransaction` if there is no such transaction of if the
       
   806         session's user isn't allowed to see it.
       
   807         """
       
   808         restr = {'tx_uuid': txuuid}
       
   809         sql = self.sqlgen.select('transactions', restr, ('tx_time', 'tx_user'))
       
   810         cu = self.doexec(session, sql, restr)
       
   811         try:
       
   812             time, ueid = cu.fetchone()
       
   813         except TypeError:
       
   814             raise tx.NoSuchTransaction()
       
   815         if not (session.user.is_in_group('managers')
       
   816                 or session.user.eid == ueid):
       
   817             raise tx.NoSuchTransaction()
       
   818         return time, ueid
       
   819 
       
   820     def _undo_d(self, session, action):
       
   821         """undo an entity deletion"""
       
   822         errors = []
       
   823         err = errors.append
       
   824         eid = action.eid
       
   825         etype = action.etype
       
   826         _ = session._
       
   827         # get an entity instance
       
   828         try:
       
   829             entity = self.repo.vreg['etypes'].etype_class(etype)(session)
       
   830         except Exception:
       
   831             err("can't restore entity %s of type %s, type no more supported"
       
   832                 % (eid, etype))
       
   833             return errors
       
   834         # check for schema changes, entities linked through inlined relation
       
   835         # still exists, rewrap binary values
       
   836         eschema = entity.e_schema
       
   837         getrschema = eschema.subjrels
       
   838         for column, value in action.changes.items():
       
   839             rtype = column[3:] # remove cw_ prefix
       
   840             try:
       
   841                 rschema = getrschema[rtype]
       
   842             except KeyError:
       
   843                 err(_("Can't restore relation %(rtype)s of entity %(eid)s, "
       
   844                       "this relation does not exists anymore in the schema.")
       
   845                     % {'rtype': rtype, 'eid': eid})
       
   846             if not rschema.final:
       
   847                 assert value is None
       
   848                     # try:
       
   849                     #     tentity = session.entity_from_eid(eid)
       
   850                     # except UnknownEid:
       
   851                     #     err(_("Can't restore %(role)s relation %(rtype)s to "
       
   852                     #           "entity %(eid)s which doesn't exist anymore.")
       
   853                     #         % {'role': _('subject'),
       
   854                     #            'rtype': _(rtype),
       
   855                     #            'eid': eid})
       
   856                     #     continue
       
   857                     # rdef = rdefs[(eschema, tentity.__regid__)]
       
   858                     # try:
       
   859                     #     _undo_check_relation_target(tentity, rdef, 'object')
       
   860                     # except UndoException, ex:
       
   861                     #     err(unicode(ex))
       
   862                     #     continue
       
   863                     # if rschema.inlined:
       
   864                     #     entity[rtype] = value
       
   865                     # else:
       
   866                     #     # restore relation where inlined changed since the deletion
       
   867                     #     del action.changes[column]
       
   868                     #     self._add_relation(session, subject, rtype, object)
       
   869                     # # set related cache
       
   870                     # session.update_rel_cache_add(eid, rtype, value,
       
   871                     #                              rschema.symmetric)
       
   872             elif eschema.destination(rtype) in ('Bytes', 'Password'):
       
   873                 action.changes[column] = self._binary(value)
       
   874                 entity[rtype] = Binary(value)
       
   875             elif isinstance(value, str):
       
   876                 entity[rtype] = unicode(value, session.encoding, 'replace')
       
   877             else:
       
   878                 entity[rtype] = value
       
   879         entity.set_eid(eid)
       
   880         entity.edited_attributes = set(entity)
       
   881         entity.check()
       
   882         self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
       
   883         # restore the entity
       
   884         action.changes['cw_eid'] = eid
       
   885         sql = self.sqlgen.insert(SQL_PREFIX + etype, action.changes)
       
   886         self.doexec(session, sql, action.changes)
       
   887         # restore record in entities (will update fti if needed)
       
   888         self.add_info(session, entity, self, None, True)
       
   889         # remove record from deleted_entities
       
   890         self.doexec(session, 'DELETE FROM deleted_entities WHERE eid=%s' % eid)
       
   891         self.repo.hm.call_hooks('after_add_entity', session, entity=entity)
       
   892         return errors
       
   893 
       
   894     def _undo_r(self, session, action):
       
   895         """undo a relation removal"""
       
   896         errors = []
       
   897         err = errors.append
       
   898         _ = session._
       
   899         subj, rtype, obj = action.eid_from, action.rtype, action.eid_to
       
   900         entities = []
       
   901         for role, eid in (('subject', subj), ('object', obj)):
       
   902             try:
       
   903                 entities.append(session.entity_from_eid(eid))
       
   904             except UnknownEid:
       
   905                 err(_("Can't restore relation %(rtype)s, %(role)s entity %(eid)s"
       
   906                       " doesn't exist anymore.")
       
   907                     % {'role': _(role),
       
   908                        'rtype': _(rtype),
       
   909                        'eid': eid})
       
   910         if not len(entities) == 2:
       
   911             return errors
       
   912         sentity, oentity = entities
       
   913         try:
       
   914             rschema = self.schema.rschema(rtype)
       
   915             rdef = rschema.rdefs[(sentity.__regid__, oentity.__regid__)]
       
   916         except KeyError:
       
   917             err(_("Can't restore relation %(rtype)s between %(subj)s and "
       
   918                   "%(obj)s, that relation does not exists anymore in the "
       
   919                   "schema.")
       
   920                 % {'rtype': rtype,
       
   921                    'subj': subj,
       
   922                    'obj': obj})
       
   923         else:
       
   924             for role, entity in (('subject', sentity),
       
   925                                  ('object', oentity)):
       
   926                 try:
       
   927                     _undo_check_relation_target(entity, rdef, role)
       
   928                 except UndoException, ex:
       
   929                     err(unicode(ex))
       
   930                     continue
       
   931         if not errors:
       
   932             self.repo.hm.call_hooks('before_add_relation', session,
       
   933                                     eidfrom=subj, rtype=rtype, eidto=obj)
       
   934             # add relation in the database
       
   935             self._add_relation(session, subj, rtype, obj, rschema.inlined)
       
   936             # set related cache
       
   937             session.update_rel_cache_add(subj, rtype, obj, rschema.symmetric)
       
   938             self.repo.hm.call_hooks('after_add_relation', session,
       
   939                                     eidfrom=subj, rtype=rtype, eidto=obj)
       
   940         return errors
       
   941 
       
   942     def _undo_c(self, session, action):
       
   943         """undo an entity creation"""
       
   944         return ['undoing of entity creation not yet supported.']
       
   945 
       
   946     def _undo_u(self, session, action):
       
   947         """undo an entity update"""
       
   948         return ['undoing of entity updating not yet supported.']
       
   949 
       
   950     def _undo_a(self, session, action):
       
   951         """undo a relation addition"""
       
   952         return ['undoing of relation addition not yet supported.']
   584 
   953 
   585     # full text index handling #################################################
   954     # full text index handling #################################################
   586 
   955 
   587     @cached
   956     @cached
   588     def need_fti_indexation(self, etype):
   957     def need_fti_indexation(self, etype):
   648         pass
  1017         pass
   649 
  1018 
   650 
  1019 
   651 def sql_schema(driver):
  1020 def sql_schema(driver):
   652     helper = get_db_helper(driver)
  1021     helper = get_db_helper(driver)
   653     tstamp_col_type = helper.TYPE_MAPPING['Datetime']
  1022     typemap = helper.TYPE_MAPPING
   654     schema = """
  1023     schema = """
   655 /* Create the repository's system database */
  1024 /* Create the repository's system database */
   656 
  1025 
   657 %s
  1026 %s
   658 
  1027 
   660   eid INTEGER PRIMARY KEY NOT NULL,
  1029   eid INTEGER PRIMARY KEY NOT NULL,
   661   type VARCHAR(64) NOT NULL,
  1030   type VARCHAR(64) NOT NULL,
   662   source VARCHAR(64) NOT NULL,
  1031   source VARCHAR(64) NOT NULL,
   663   mtime %s NOT NULL,
  1032   mtime %s NOT NULL,
   664   extid VARCHAR(256)
  1033   extid VARCHAR(256)
   665 );
  1034 );;
   666 CREATE INDEX entities_type_idx ON entities(type);
  1035 CREATE INDEX entities_type_idx ON entities(type);;
   667 CREATE INDEX entities_mtime_idx ON entities(mtime);
  1036 CREATE INDEX entities_mtime_idx ON entities(mtime);;
   668 CREATE INDEX entities_extid_idx ON entities(extid);
  1037 CREATE INDEX entities_extid_idx ON entities(extid);;
   669 
  1038 
   670 CREATE TABLE deleted_entities (
  1039 CREATE TABLE deleted_entities (
   671   eid INTEGER PRIMARY KEY NOT NULL,
  1040   eid INTEGER PRIMARY KEY NOT NULL,
   672   type VARCHAR(64) NOT NULL,
  1041   type VARCHAR(64) NOT NULL,
   673   source VARCHAR(64) NOT NULL,
  1042   source VARCHAR(64) NOT NULL,
   674   dtime %s NOT NULL,
  1043   dtime %s NOT NULL,
   675   extid VARCHAR(256)
  1044   extid VARCHAR(256)
   676 );
  1045 );;
   677 CREATE INDEX deleted_entities_type_idx ON deleted_entities(type);
  1046 CREATE INDEX deleted_entities_type_idx ON deleted_entities(type);;
   678 CREATE INDEX deleted_entities_dtime_idx ON deleted_entities(dtime);
  1047 CREATE INDEX deleted_entities_dtime_idx ON deleted_entities(dtime);;
   679 CREATE INDEX deleted_entities_extid_idx ON deleted_entities(extid);
  1048 CREATE INDEX deleted_entities_extid_idx ON deleted_entities(extid);;
   680 """ % (helper.sql_create_sequence('entities_id_seq'), tstamp_col_type, tstamp_col_type)
  1049 
       
  1050 CREATE TABLE transactions (
       
  1051   tx_uuid CHAR(32) PRIMARY KEY NOT NULL,
       
  1052   tx_user INTEGER NOT NULL,
       
  1053   tx_time %s NOT NULL
       
  1054 );;
       
  1055 CREATE INDEX transactions_tx_user_idx ON transactions(tx_user);;
       
  1056 
       
  1057 CREATE TABLE tx_entity_actions (
       
  1058   tx_uuid CHAR(32) REFERENCES transactions(tx_uuid) ON DELETE CASCADE,
       
  1059   txa_action CHAR(1) NOT NULL,
       
  1060   txa_public %s NOT NULL,
       
  1061   txa_order INTEGER,
       
  1062   eid INTEGER NOT NULL,
       
  1063   etype VARCHAR(64) NOT NULL,
       
  1064   changes %s
       
  1065 );;
       
  1066 CREATE INDEX tx_entity_actions_txa_action_idx ON tx_entity_actions(txa_action);;
       
  1067 CREATE INDEX tx_entity_actions_txa_public_idx ON tx_entity_actions(txa_public);;
       
  1068 CREATE INDEX tx_entity_actions_eid_idx ON tx_entity_actions(eid);;
       
  1069 CREATE INDEX tx_entity_actions_etype_idx ON tx_entity_actions(etype);;
       
  1070 
       
  1071 CREATE TABLE tx_relation_actions (
       
  1072   tx_uuid CHAR(32) REFERENCES transactions(tx_uuid) ON DELETE CASCADE,
       
  1073   txa_action CHAR(1) NOT NULL,
       
  1074   txa_public %s NOT NULL,
       
  1075   txa_order INTEGER,
       
  1076   eid_from INTEGER NOT NULL,
       
  1077   eid_to INTEGER NOT NULL,
       
  1078   rtype VARCHAR(256) NOT NULL
       
  1079 );;
       
  1080 CREATE INDEX tx_relation_actions_txa_action_idx ON tx_relation_actions(txa_action);;
       
  1081 CREATE INDEX tx_relation_actions_txa_public_idx ON tx_relation_actions(txa_public);;
       
  1082 CREATE INDEX tx_relation_actions_eid_from_idx ON tx_relation_actions(eid_from);;
       
  1083 CREATE INDEX tx_relation_actions_eid_to_idx ON tx_relation_actions(eid_to);;
       
  1084 """ % (helper.sql_create_sequence('entities_id_seq').replace(';', ';;'),
       
  1085        typemap['Datetime'], typemap['Datetime'], typemap['Datetime'],
       
  1086        typemap['Boolean'], typemap['Bytes'], typemap['Boolean'])
       
  1087     if helper.backend_name == 'sqlite':
       
  1088         # sqlite support the ON DELETE CASCADE syntax but do nothing
       
  1089         schema += '''
       
  1090 CREATE TRIGGER fkd_transactions
       
  1091 BEFORE DELETE ON transactions
       
  1092 FOR EACH ROW BEGIN
       
  1093     DELETE FROM tx_entity_actions WHERE tx_uuid=OLD.tx_uuid;
       
  1094     DELETE FROM tx_relation_actions WHERE tx_uuid=OLD.tx_uuid;
       
  1095 END;;
       
  1096 '''
   681     return schema
  1097     return schema
   682 
  1098 
   683 
  1099 
   684 def sql_drop_schema(driver):
  1100 def sql_drop_schema(driver):
   685     helper = get_db_helper(driver)
  1101     helper = get_db_helper(driver)
   686     return """
  1102     return """
   687 %s
  1103 %s
   688 DROP TABLE entities;
  1104 DROP TABLE entities;
   689 DROP TABLE deleted_entities;
  1105 DROP TABLE deleted_entities;
       
  1106 DROP TABLE transactions;
       
  1107 DROP TABLE tx_entity_actions;
       
  1108 DROP TABLE tx_relation_actions;
   690 """ % helper.sql_drop_sequence('entities_id_seq')
  1109 """ % helper.sql_drop_sequence('entities_id_seq')
   691 
  1110 
   692 
  1111 
   693 def grant_schema(user, set_owner=True):
  1112 def grant_schema(user, set_owner=True):
   694     result = ''
  1113     result = ''
   695     if set_owner:
  1114     for table in ('entities', 'deleted_entities', 'entities_id_seq',
   696         result = 'ALTER TABLE entities OWNER TO %s;\n' % user
  1115                   'transactions', 'tx_entity_actions', 'tx_relation_actions'):
   697         result += 'ALTER TABLE deleted_entities OWNER TO %s;\n' % user
  1116         if set_owner:
   698         result += 'ALTER TABLE entities_id_seq OWNER TO %s;\n' % user
  1117             result = 'ALTER TABLE %s OWNER TO %s;\n' % (table, user)
   699     result += 'GRANT ALL ON entities TO %s;\n' % user
  1118         result += 'GRANT ALL ON %s TO %s;\n' % (table, user)
   700     result += 'GRANT ALL ON deleted_entities TO %s;\n' % user
       
   701     result += 'GRANT ALL ON entities_id_seq TO %s;\n' % user
       
   702     return result
  1119     return result
   703 
  1120 
   704 
  1121 
   705 class BaseAuthentifier(object):
  1122 class BaseAuthentifier(object):
   706 
  1123