cubicweb/server/sources/native.py
changeset 11417 5e5e224239c3
parent 11413 c172fa18565e
child 11429 6a9a9ea1e9b9
equal deleted inserted replaced
11416:9c2fbb872e91 11417:5e5e224239c3
    68 
    68 
    69 ATTR_MAP = {}
    69 ATTR_MAP = {}
    70 NONSYSTEM_ETYPES = set()
    70 NONSYSTEM_ETYPES = set()
    71 NONSYSTEM_RELATIONS = set()
    71 NONSYSTEM_RELATIONS = set()
    72 
    72 
       
    73 
    73 class LogCursor(object):
    74 class LogCursor(object):
    74     def __init__(self, cursor):
    75     def __init__(self, cursor):
    75         self.cu = cursor
    76         self.cu = cursor
    76 
    77 
    77     def execute(self, query, args=None):
    78     def execute(self, query, args=None):
   140 
   141 
   141 def _undo_check_relation_target(tentity, rdef, role):
   142 def _undo_check_relation_target(tentity, rdef, role):
   142     """check linked entity has not been redirected for this relation"""
   143     """check linked entity has not been redirected for this relation"""
   143     card = rdef.role_cardinality(role)
   144     card = rdef.role_cardinality(role)
   144     if card in '?1' and tentity.related(rdef.rtype, role):
   145     if card in '?1' and tentity.related(rdef.rtype, role):
   145         raise _UndoException(tentity._cw._(
   146         msg = tentity._cw._(
   146             "Can't restore %(role)s relation %(rtype)s to entity %(eid)s which "
   147             "Can't restore %(role)s relation %(rtype)s to entity %(eid)s which "
   147             "is already linked using this relation.")
   148             "is already linked using this relation.")
   148                             % {'role': neg_role(role),
   149         raise _UndoException(msg % {'role': neg_role(role),
   149                                'rtype': rdef.rtype,
   150                                     'rtype': rdef.rtype,
   150                                'eid': tentity.eid})
   151                                     'eid': tentity.eid})
       
   152 
   151 
   153 
   152 def _undo_rel_info(cnx, subj, rtype, obj):
   154 def _undo_rel_info(cnx, subj, rtype, obj):
   153     entities = []
   155     entities = []
   154     for role, eid in (('subject', subj), ('object', obj)):
   156     for role, eid in (('subject', subj), ('object', obj)):
   155         try:
   157         try:
   156             entities.append(cnx.entity_from_eid(eid))
   158             entities.append(cnx.entity_from_eid(eid))
   157         except UnknownEid:
   159         except UnknownEid:
   158             raise _UndoException(cnx._(
   160             msg = cnx._(
   159                 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s"
   161                 "Can't restore relation %(rtype)s, %(role)s entity %(eid)s"
   160                 " doesn't exist anymore.")
   162                 " doesn't exist anymore.")
   161                                 % {'role': cnx._(role),
   163             raise _UndoException(msg % {'role': cnx._(role),
   162                                    'rtype': cnx._(rtype),
   164                                         'rtype': cnx._(rtype),
   163                                    'eid': eid})
   165                                         'eid': eid})
   164     sentity, oentity = entities
   166     sentity, oentity = entities
   165     try:
   167     try:
   166         rschema = cnx.vreg.schema.rschema(rtype)
   168         rschema = cnx.vreg.schema.rschema(rtype)
   167         rdef = rschema.rdefs[(sentity.cw_etype, oentity.cw_etype)]
   169         rdef = rschema.rdefs[(sentity.cw_etype, oentity.cw_etype)]
   168     except KeyError:
   170     except KeyError:
   169         raise _UndoException(cnx._(
   171         msg = cnx._(
   170             "Can't restore relation %(rtype)s between %(subj)s and "
   172             "Can't restore relation %(rtype)s between %(subj)s and "
   171             "%(obj)s, that relation does not exists anymore in the "
   173             "%(obj)s, that relation does not exists anymore in the "
   172             "schema.")
   174             "schema.")
   173                             % {'rtype': cnx._(rtype),
   175         raise _UndoException(msg % {'rtype': cnx._(rtype),
   174                                'subj': subj,
   176                                     'subj': subj,
   175                                'obj': obj})
   177                                     'obj': obj})
   176     return sentity, oentity, rdef
   178     return sentity, oentity, rdef
       
   179 
   177 
   180 
   178 def _undo_has_later_transaction(cnx, eid):
   181 def _undo_has_later_transaction(cnx, eid):
   179     return cnx.system_sql('''\
   182     return cnx.system_sql('''\
   180 SELECT T.tx_uuid FROM transactions AS TREF, transactions AS T
   183 SELECT T.tx_uuid FROM transactions AS TREF, transactions AS T
   181 WHERE TREF.tx_uuid='%(txuuid)s' AND T.tx_uuid!='%(txuuid)s'
   184 WHERE TREF.tx_uuid='%(txuuid)s' AND T.tx_uuid!='%(txuuid)s'
   268     """adapter for source using the native cubicweb schema (see below)
   271     """adapter for source using the native cubicweb schema (see below)
   269     """
   272     """
   270     sqlgen_class = SQLGenerator
   273     sqlgen_class = SQLGenerator
   271     options = (
   274     options = (
   272         ('db-driver',
   275         ('db-driver',
   273          {'type' : 'string',
   276          {'type': 'string',
   274           'default': 'postgres',
   277           'default': 'postgres',
   275           # XXX use choice type
   278           # XXX use choice type
   276           'help': 'database driver (postgres, sqlite, sqlserver2005)',
   279           'help': 'database driver (postgres, sqlite, sqlserver2005)',
   277           'group': 'native-source', 'level': 0,
   280           'group': 'native-source', 'level': 0,
   278           }),
   281           }),
   279         ('db-host',
   282         ('db-host',
   280          {'type' : 'string',
   283          {'type': 'string',
   281           'default': '',
   284           'default': '',
   282           'help': 'database host',
   285           'help': 'database host',
   283           'group': 'native-source', 'level': 1,
   286           'group': 'native-source', 'level': 1,
   284           }),
   287           }),
   285         ('db-port',
   288         ('db-port',
   286          {'type' : 'string',
   289          {'type': 'string',
   287           'default': '',
   290           'default': '',
   288           'help': 'database port',
   291           'help': 'database port',
   289           'group': 'native-source', 'level': 1,
   292           'group': 'native-source', 'level': 1,
   290           }),
   293           }),
   291         ('db-name',
   294         ('db-name',
   292          {'type' : 'string',
   295          {'type': 'string',
   293           'default': Method('default_instance_id'),
   296           'default': Method('default_instance_id'),
   294           'help': 'database name',
   297           'help': 'database name',
   295           'group': 'native-source', 'level': 0,
   298           'group': 'native-source', 'level': 0,
   296           }),
   299           }),
   297         ('db-namespace',
   300         ('db-namespace',
   298          {'type' : 'string',
   301          {'type': 'string',
   299           'default': '',
   302           'default': '',
   300           'help': 'database namespace (schema) name',
   303           'help': 'database namespace (schema) name',
   301           'group': 'native-source', 'level': 1,
   304           'group': 'native-source', 'level': 1,
   302           }),
   305           }),
   303         ('db-user',
   306         ('db-user',
   304          {'type' : 'string',
   307          {'type': 'string',
   305           'default': CubicWebNoAppConfiguration.mode == 'user' and getlogin() or 'cubicweb',
   308           'default': CubicWebNoAppConfiguration.mode == 'user' and getlogin() or 'cubicweb',
   306           'help': 'database user',
   309           'help': 'database user',
   307           'group': 'native-source', 'level': 0,
   310           'group': 'native-source', 'level': 0,
   308           }),
   311           }),
   309         ('db-password',
   312         ('db-password',
   310          {'type' : 'password',
   313          {'type': 'password',
   311           'default': '',
   314           'default': '',
   312           'help': 'database password',
   315           'help': 'database password',
   313           'group': 'native-source', 'level': 0,
   316           'group': 'native-source', 'level': 0,
   314           }),
   317           }),
   315         ('db-encoding',
   318         ('db-encoding',
   316          {'type' : 'string',
   319          {'type': 'string',
   317           'default': 'utf8',
   320           'default': 'utf8',
   318           'help': 'database encoding',
   321           'help': 'database encoding',
   319           'group': 'native-source', 'level': 1,
   322           'group': 'native-source', 'level': 1,
   320           }),
   323           }),
   321         ('db-extra-arguments',
   324         ('db-extra-arguments',
   322          {'type' : 'string',
   325          {'type': 'string',
   323           'default': '',
   326           'default': '',
   324           'help': 'set to "Trusted_Connection" if you are using SQLServer and '
   327           'help': 'set to "Trusted_Connection" if you are using SQLServer and '
   325                   'want trusted authentication for the database connection',
   328                   'want trusted authentication for the database connection',
   326           'group': 'native-source', 'level': 2,
   329           'group': 'native-source', 'level': 2,
   327           }),
   330           }),
   419             finally:
   422             finally:
   420                 self.open_source_connections()
   423                 self.open_source_connections()
   421         else:
   424         else:
   422             raise ValueError('Unknown format %r' % format)
   425             raise ValueError('Unknown format %r' % format)
   423 
   426 
   424 
       
   425     def restore(self, backupfile, confirm, drop, format='native'):
   427     def restore(self, backupfile, confirm, drop, format='native'):
   426         """method called to restore a backup of source's data"""
   428         """method called to restore a backup of source's data"""
   427         if self.repo.config.init_cnxset_pool:
   429         if self.repo.config.init_cnxset_pool:
   428             self.close_source_connections()
   430             self.close_source_connections()
   429         try:
   431         try:
   436                 raise ValueError('Unknown format %r' % format)
   438                 raise ValueError('Unknown format %r' % format)
   437         finally:
   439         finally:
   438             if self.repo.config.init_cnxset_pool:
   440             if self.repo.config.init_cnxset_pool:
   439                 self.open_source_connections()
   441                 self.open_source_connections()
   440 
   442 
   441 
       
   442     def init(self, activated, source_entity):
   443     def init(self, activated, source_entity):
   443         try:
   444         try:
   444             # test if 'asource' column exists
   445             # test if 'asource' column exists
   445             query = self.dbhelper.sql_add_limit_offset('SELECT asource FROM entities', 1)
   446             query = self.dbhelper.sql_add_limit_offset('SELECT asource FROM entities', 1)
   446             source_entity._cw.system_sql(query)
   447             source_entity._cw.system_sql(query)
   447         except Exception as ex:
   448         except Exception:
   448             self.eid_type_source = self.eid_type_source_pre_131
   449             self.eid_type_source = self.eid_type_source_pre_131
   449         super(NativeSQLSource, self).init(activated, source_entity)
   450         super(NativeSQLSource, self).init(activated, source_entity)
   450         self.init_creating(source_entity._cw.cnxset)
   451         self.init_creating(source_entity._cw.cnxset)
   451 
   452 
   452     def shutdown(self):
   453     def shutdown(self):
   497         self.cache_hit, self.cache_miss, self.no_cache = 0, 0, 0
   498         self.cache_hit, self.cache_miss, self.no_cache = 0, 0, 0
   498         self.schema = schema
   499         self.schema = schema
   499         try:
   500         try:
   500             self._rql_sqlgen.schema = schema
   501             self._rql_sqlgen.schema = schema
   501         except AttributeError:
   502         except AttributeError:
   502             pass # __init__
   503             pass  # __init__
   503         for authentifier in self.authentifiers:
   504         for authentifier in self.authentifiers:
   504             authentifier.set_schema(self.schema)
   505             authentifier.set_schema(self.schema)
   505         clear_cache(self, 'need_fti_indexation')
   506         clear_cache(self, 'need_fti_indexation')
   506 
   507 
   507     def support_entity(self, etype, write=False):
   508     def support_entity(self, etype, write=False):
   508         """return true if the given entity's type is handled by this adapter
   509         """return true if the given entity's type is handled by this adapter
   509         if write is true, return true only if it's a RW support
   510         if write is true, return true only if it's a RW support
   510         """
   511         """
   511         return not etype in NONSYSTEM_ETYPES
   512         return etype not in NONSYSTEM_ETYPES
   512 
   513 
   513     def support_relation(self, rtype, write=False):
   514     def support_relation(self, rtype, write=False):
   514         """return true if the given relation's type is handled by this adapter
   515         """return true if the given relation's type is handled by this adapter
   515         if write is true, return true only if it's a RW support
   516         if write is true, return true only if it's a RW support
   516         """
   517         """
   517         if write:
   518         if write:
   518             return not rtype in NONSYSTEM_RELATIONS
   519             return rtype not in NONSYSTEM_RELATIONS
   519         # due to current multi-sources implementation, the system source
   520         # due to current multi-sources implementation, the system source
   520         # can't claim not supporting a relation
   521         # can't claim not supporting a relation
   521         return True #not rtype == 'content_for'
   522         return True  #not rtype == 'content_for'
   522 
   523 
   523     @statsd_timeit
   524     @statsd_timeit
   524     def authenticate(self, cnx, login, **kwargs):
   525     def authenticate(self, cnx, login, **kwargs):
   525         """return CWUser eid for the given login and other authentication
   526         """return CWUser eid for the given login and other authentication
   526         information found in kwargs, else raise `AuthenticationError`
   527         information found in kwargs, else raise `AuthenticationError`
   594                         if attr in edited:
   595                         if attr in edited:
   595                             handler = getattr(storage, 'entity_%s' % event)
   596                             handler = getattr(storage, 'entity_%s' % event)
   596                             to_restore = handler(entity, attr)
   597                             to_restore = handler(entity, attr)
   597                             restore_values.append((entity, attr, to_restore))
   598                             restore_values.append((entity, attr, to_restore))
   598         try:
   599         try:
   599             yield # 2/ execute the source's instructions
   600             yield  # 2/ execute the source's instructions
   600         finally:
   601         finally:
   601             # 3/ restore original values
   602             # 3/ restore original values
   602             for entity, attr, value in restore_values:
   603             for entity, attr, value in restore_values:
   603                 entity.cw_edited.edited_attribute(attr, value)
   604                 entity.cw_edited.edited_attribute(attr, value)
   604 
   605 
   629         """delete an entity from the source"""
   630         """delete an entity from the source"""
   630         with self._storage_handler(cnx, entity, 'deleted'):
   631         with self._storage_handler(cnx, entity, 'deleted'):
   631             if cnx.ertype_supports_undo(entity.cw_etype):
   632             if cnx.ertype_supports_undo(entity.cw_etype):
   632                 attrs = [SQL_PREFIX + r.type
   633                 attrs = [SQL_PREFIX + r.type
   633                          for r in entity.e_schema.subject_relations()
   634                          for r in entity.e_schema.subject_relations()
   634                          if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
   635                          if (r.final or r.inlined) and r not in VIRTUAL_RTYPES]
   635                 changes = self._save_attrs(cnx, entity, attrs)
   636                 changes = self._save_attrs(cnx, entity, attrs)
   636                 self._record_tx_action(cnx, 'tx_entity_actions', u'D',
   637                 self._record_tx_action(cnx, 'tx_entity_actions', u'D',
   637                                        etype=text_type(entity.cw_etype), eid=entity.eid,
   638                                        etype=text_type(entity.cw_etype), eid=entity.eid,
   638                                        changes=self._binary(pickle.dumps(changes)))
   639                                        changes=self._binary(pickle.dumps(changes)))
   639             attrs = {'cw_eid': entity.eid}
   640             attrs = {'cw_eid': entity.eid}
   640             sql = self.sqlgen.delete(SQL_PREFIX + entity.cw_etype, attrs)
   641             sql = self.sqlgen.delete(SQL_PREFIX + entity.cw_etype, attrs)
   641             self.doexec(cnx, sql, attrs)
   642             self.doexec(cnx, sql, attrs)
   642 
   643 
   643     def add_relation(self, cnx, subject, rtype, object, inlined=False):
   644     def add_relation(self, cnx, subject, rtype, object, inlined=False):
   644         """add a relation to the source"""
   645         """add a relation to the source"""
   645         self._add_relations(cnx,  rtype, [(subject, object)], inlined)
   646         self._add_relations(cnx, rtype, [(subject, object)], inlined)
   646         if cnx.ertype_supports_undo(rtype):
   647         if cnx.ertype_supports_undo(rtype):
   647             self._record_tx_action(cnx, 'tx_relation_actions', u'A',
   648             self._record_tx_action(cnx, 'tx_relation_actions', u'A',
   648                                    eid_from=subject, rtype=text_type(rtype), eid_to=object)
   649                                    eid_from=subject, rtype=text_type(rtype), eid_to=object)
   649 
   650 
   650     def add_relations(self, cnx,  rtype, subj_obj_list, inlined=False):
   651     def add_relations(self, cnx, rtype, subj_obj_list, inlined=False):
   651         """add a relations to the source"""
   652         """add a relations to the source"""
   652         self._add_relations(cnx, rtype, subj_obj_list, inlined)
   653         self._add_relations(cnx, rtype, subj_obj_list, inlined)
   653         if cnx.ertype_supports_undo(rtype):
   654         if cnx.ertype_supports_undo(rtype):
   654             for subject, object in subj_obj_list:
   655             for subject, object in subj_obj_list:
   655                 self._record_tx_action(cnx, 'tx_relation_actions', u'A',
   656                 self._record_tx_action(cnx, 'tx_relation_actions', u'A',
   660         sql = []
   661         sql = []
   661         if inlined is False:
   662         if inlined is False:
   662             attrs = [{'eid_from': subject, 'eid_to': object}
   663             attrs = [{'eid_from': subject, 'eid_to': object}
   663                      for subject, object in subj_obj_list]
   664                      for subject, object in subj_obj_list]
   664             sql.append((self.sqlgen.insert('%s_relation' % rtype, attrs[0]), attrs))
   665             sql.append((self.sqlgen.insert('%s_relation' % rtype, attrs[0]), attrs))
   665         else: # used by data import
   666         else:  # used by data import
   666             etypes = {}
   667             etypes = {}
   667             for subject, object in subj_obj_list:
   668             for subject, object in subj_obj_list:
   668                 etype = cnx.entity_metas(subject)['type']
   669                 etype = cnx.entity_metas(subject)['type']
   669                 if etype in etypes:
   670                 if etype in etypes:
   670                     etypes[etype].append((subject, object))
   671                     etypes[etype].append((subject, object))
   672                     etypes[etype] = [(subject, object)]
   673                     etypes[etype] = [(subject, object)]
   673             for subj_etype, subj_obj_list in etypes.items():
   674             for subj_etype, subj_obj_list in etypes.items():
   674                 attrs = [{'cw_eid': subject, SQL_PREFIX + rtype: object}
   675                 attrs = [{'cw_eid': subject, SQL_PREFIX + rtype: object}
   675                          for subject, object in subj_obj_list]
   676                          for subject, object in subj_obj_list]
   676                 sql.append((self.sqlgen.update(SQL_PREFIX + etype, attrs[0],
   677                 sql.append((self.sqlgen.update(SQL_PREFIX + etype, attrs[0],
   677                                      ['cw_eid']),
   678                                                ['cw_eid']),
   678                             attrs))
   679                             attrs))
   679         for statement, attrs in sql:
   680         for statement, attrs in sql:
   680             self.doexecmany(cnx, statement, attrs)
   681             self.doexecmany(cnx, statement, attrs)
   681 
   682 
   682     def delete_relation(self, cnx, subject, rtype, object):
   683     def delete_relation(self, cnx, subject, rtype, object):
   692         if inlined:
   693         if inlined:
   693             table = SQL_PREFIX + cnx.entity_metas(subject)['type']
   694             table = SQL_PREFIX + cnx.entity_metas(subject)['type']
   694             column = SQL_PREFIX + rtype
   695             column = SQL_PREFIX + rtype
   695             sql = 'UPDATE %s SET %s=NULL WHERE %seid=%%(eid)s' % (table, column,
   696             sql = 'UPDATE %s SET %s=NULL WHERE %seid=%%(eid)s' % (table, column,
   696                                                                   SQL_PREFIX)
   697                                                                   SQL_PREFIX)
   697             attrs = {'eid' : subject}
   698             attrs = {'eid': subject}
   698         else:
   699         else:
   699             attrs = {'eid_from': subject, 'eid_to': object}
   700             attrs = {'eid_from': subject, 'eid_to': object}
   700             sql = self.sqlgen.delete('%s_relation' % rtype, attrs)
   701             sql = self.sqlgen.delete('%s_relation' % rtype, attrs)
   701         self.doexec(cnx, sql, attrs)
   702         self.doexec(cnx, sql, attrs)
   702 
   703 
   714         except Exception as ex:
   715         except Exception as ex:
   715             if self.repo.config.mode != 'test':
   716             if self.repo.config.mode != 'test':
   716                 # during test we get those message when trying to alter sqlite
   717                 # during test we get those message when trying to alter sqlite
   717                 # db schema
   718                 # db schema
   718                 self.info("sql: %r\n args: %s\ndbms message: %r",
   719                 self.info("sql: %r\n args: %s\ndbms message: %r",
   719                               query, args, ex.args[0])
   720                           query, args, ex.args[0])
   720             if rollback:
   721             if rollback:
   721                 try:
   722                 try:
   722                     cnx.cnxset.rollback()
   723                     cnx.cnxset.rollback()
   723                     if self.repo.config.mode != 'test':
   724                     if self.repo.config.mode != 'test':
   724                         self.debug('transaction has been rolled back')
   725                         self.debug('transaction has been rolled back')
   845                 return res
   846                 return res
   846         except Exception:
   847         except Exception:
   847             self.exception('failed to query entities table for eid %s', eid)
   848             self.exception('failed to query entities table for eid %s', eid)
   848         raise UnknownEid(eid)
   849         raise UnknownEid(eid)
   849 
   850 
   850     def eid_type_source(self, cnx, eid): # pylint: disable=E0202
   851     def eid_type_source(self, cnx, eid):  # pylint: disable=E0202
   851         """return a tuple (type, extid, source) for the entity with id <eid>"""
   852         """return a tuple (type, extid, source) for the entity with id <eid>"""
   852         sql = 'SELECT type, extid, asource FROM entities WHERE eid=%s' % eid
   853         sql = 'SELECT type, extid, asource FROM entities WHERE eid=%s' % eid
   853         res = self._eid_type_source(cnx, eid, sql)
   854         res = self._eid_type_source(cnx, eid, sql)
   854         if not isinstance(res, list):
   855         if not isinstance(res, list):
   855             res = list(res)
   856             res = list(res)
   914                  'asource': text_type(source.uri)}
   915                  'asource': text_type(source.uri)}
   915         self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs)
   916         self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs)
   916         # insert core relations: is, is_instance_of and cw_source
   917         # insert core relations: is, is_instance_of and cw_source
   917 
   918 
   918         if entity.e_schema.eid is not None:  # else schema has not yet been serialized
   919         if entity.e_schema.eid is not None:  # else schema has not yet been serialized
   919             self._handle_is_relation_sql(cnx, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)',
   920             self._handle_is_relation_sql(
   920                                          (entity.eid, entity.e_schema.eid))
   921                 cnx, 'INSERT INTO is_relation(eid_from,eid_to) VALUES (%s,%s)',
       
   922                 (entity.eid, entity.e_schema.eid))
   921             for eschema in entity.e_schema.ancestors() + [entity.e_schema]:
   923             for eschema in entity.e_schema.ancestors() + [entity.e_schema]:
   922                 self._handle_is_relation_sql(cnx,
   924                 self._handle_is_relation_sql(
   923                                              'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)',
   925                     cnx,
   924                                              (entity.eid, eschema.eid))
   926                     'INSERT INTO is_instance_of_relation(eid_from,eid_to) VALUES (%s,%s)',
       
   927                     (entity.eid, eschema.eid))
   925         if source.eid is not None:  # else the source has not yet been inserted
   928         if source.eid is not None:  # else the source has not yet been inserted
   926             self._handle_is_relation_sql(cnx, 'INSERT INTO cw_source_relation(eid_from,eid_to) VALUES (%s,%s)',
   929             self._handle_is_relation_sql(
   927                                          (entity.eid, source.eid))
   930                 cnx, 'INSERT INTO cw_source_relation(eid_from,eid_to) VALUES (%s,%s)',
       
   931                 (entity.eid, source.eid))
   928         # now we can update the full text index
   932         # now we can update the full text index
   929         if self.need_fti_indexation(entity.cw_etype):
   933         if self.need_fti_indexation(entity.cw_etype):
   930             self.index_entity(cnx, entity=entity)
   934             self.index_entity(cnx, entity=entity)
   931 
   935 
   932     def update_info(self, cnx, entity, need_fti_update):
   936     def update_info(self, cnx, entity, need_fti_update):
   967             restr['tx_user'] = ueid
   971             restr['tx_user'] = ueid
   968         sql = self.sqlgen.select('transactions', restr, ('tx_uuid', 'tx_time', 'tx_user'))
   972         sql = self.sqlgen.select('transactions', restr, ('tx_uuid', 'tx_time', 'tx_user'))
   969         if actionfilters:
   973         if actionfilters:
   970             # we will need subqueries to filter transactions according to
   974             # we will need subqueries to filter transactions according to
   971             # actions done
   975             # actions done
   972             tearestr = {} # filters on the tx_entity_actions table
   976             tearestr = {}  # filters on the tx_entity_actions table
   973             trarestr = {} # filters on the tx_relation_actions table
   977             trarestr = {}  # filters on the tx_relation_actions table
   974             genrestr = {} # generic filters, appliyable to both table
   978             genrestr = {}  # generic filters, appliyable to both table
   975             # unless public explicitly set to false, we only consider public
   979             # unless public explicitly set to false, we only consider public
   976             # actions
   980             # actions
   977             if actionfilters.pop('public', True):
   981             if actionfilters.pop('public', True):
   978                 genrestr['txa_public'] = True
   982                 genrestr['txa_public'] = True
   979             # put additional filters in trarestr and/or tearestr
   983             # put additional filters in trarestr and/or tearestr
   980             for key, val in actionfilters.items():
   984             for key, val in actionfilters.items():
   981                 if key == 'etype':
   985                 if key == 'etype':
   982                     # filtering on etype implies filtering on entity actions
   986                     # filtering on etype implies filtering on entity actions
   983                     # only, and with no eid specified
   987                     # only, and with no eid specified
   984                     assert actionfilters.get('action', 'C') in 'CUD'
   988                     assert actionfilters.get('action', 'C') in 'CUD'
   985                     assert not 'eid' in actionfilters
   989                     assert 'eid' not in actionfilters
   986                     tearestr['etype'] = text_type(val)
   990                     tearestr['etype'] = text_type(val)
   987                 elif key == 'eid':
   991                 elif key == 'eid':
   988                     # eid filter may apply to 'eid' of tx_entity_actions or to
   992                     # eid filter may apply to 'eid' of tx_entity_actions or to
   989                     # 'eid_from' OR 'eid_to' of tx_relation_actions
   993                     # 'eid_from' OR 'eid_to' of tx_relation_actions
   990                     if actionfilters.get('action', 'C') in 'CUD':
   994                     if actionfilters.get('action', 'C') in 'CUD':
  1044         sql = self.sqlgen.select('tx_entity_actions', restr,
  1048         sql = self.sqlgen.select('tx_entity_actions', restr,
  1045                                  ('txa_action', 'txa_public', 'txa_order',
  1049                                  ('txa_action', 'txa_public', 'txa_order',
  1046                                   'etype', 'eid', 'changes'))
  1050                                   'etype', 'eid', 'changes'))
  1047         with cnx.ensure_cnx_set:
  1051         with cnx.ensure_cnx_set:
  1048             cu = self.doexec(cnx, sql, restr)
  1052             cu = self.doexec(cnx, sql, restr)
  1049             actions = [tx.EntityAction(a,p,o,et,e,c and pickle.loads(self.binary_to_str(c)))
  1053             actions = [tx.EntityAction(a, p, o, et, e, c and pickle.loads(self.binary_to_str(c)))
  1050                        for a,p,o,et,e,c in cu.fetchall()]
  1054                        for a, p, o, et, e, c in cu.fetchall()]
  1051         sql = self.sqlgen.select('tx_relation_actions', restr,
  1055         sql = self.sqlgen.select('tx_relation_actions', restr,
  1052                                  ('txa_action', 'txa_public', 'txa_order',
  1056                                  ('txa_action', 'txa_public', 'txa_order',
  1053                                   'rtype', 'eid_from', 'eid_to'))
  1057                                   'rtype', 'eid_from', 'eid_to'))
  1054         with cnx.ensure_cnx_set:
  1058         with cnx.ensure_cnx_set:
  1055             cu = self.doexec(cnx, sql, restr)
  1059             cu = self.doexec(cnx, sql, restr)
  1144         eschema = entity.e_schema
  1148         eschema = entity.e_schema
  1145         getrschema = eschema.subjrels
  1149         getrschema = eschema.subjrels
  1146         for column, value in changes.items():
  1150         for column, value in changes.items():
  1147             rtype = column[len(SQL_PREFIX):]
  1151             rtype = column[len(SQL_PREFIX):]
  1148             if rtype == "eid":
  1152             if rtype == "eid":
  1149                 continue # XXX should even `eid` be stored in action changes?
  1153                 continue  # XXX should even `eid` be stored in action changes?
  1150             try:
  1154             try:
  1151                 rschema = getrschema[rtype]
  1155                 rschema = getrschema[rtype]
  1152             except KeyError:
  1156             except KeyError:
  1153                 err(cnx._("can't restore relation %(rtype)s of entity %(eid)s, "
  1157                 err(cnx._("can't restore relation %(rtype)s of entity %(eid)s, "
  1154                               "this relation does not exist in the schema anymore.")
  1158                           "this relation does not exist in the schema anymore.")
  1155                     % {'rtype': rtype, 'eid': eid})
  1159                     % {'rtype': rtype, 'eid': eid})
  1156             if not rschema.final:
  1160             if not rschema.final:
  1157                 if not rschema.inlined:
  1161                 if not rschema.inlined:
  1158                     assert value is None
  1162                     assert value is None
  1159                 # rschema is an inlined relation
  1163                 # rschema is an inlined relation
  1160                 elif value is not None:
  1164                 elif value is not None:
  1161                     # not a deletion: we must put something in edited
  1165                     # not a deletion: we must put something in edited
  1162                     try:
  1166                     try:
  1163                         entity._cw.entity_from_eid(value) # check target exists
  1167                         entity._cw.entity_from_eid(value)  # check target exists
  1164                         edited[rtype] = value
  1168                         edited[rtype] = value
  1165                     except UnknownEid:
  1169                     except UnknownEid:
  1166                         err(cnx._("can't restore entity %(eid)s of type %(eschema)s, "
  1170                         err(cnx._("can't restore entity %(eid)s of type %(eschema)s, "
  1167                                       "target of %(rtype)s (eid %(value)s) does not exist any longer")
  1171                                   "target of %(rtype)s (eid %(value)s) does not exist any longer")
  1168                             % locals())
  1172                             % locals())
  1169                         changes[column] = None
  1173                         changes[column] = None
  1170             elif eschema.destination(rtype) in ('Bytes', 'Password'):
  1174             elif eschema.destination(rtype) in ('Bytes', 'Password'):
  1171                 changes[column] = self._binary(value)
  1175                 changes[column] = self._binary(value)
  1172                 edited[rtype] = Binary(value)
  1176                 edited[rtype] = Binary(value)
  1181         """undo an entity deletion"""
  1185         """undo an entity deletion"""
  1182         errors = []
  1186         errors = []
  1183         err = errors.append
  1187         err = errors.append
  1184         eid = action.eid
  1188         eid = action.eid
  1185         etype = action.etype
  1189         etype = action.etype
  1186         _ = cnx._
       
  1187         # get an entity instance
  1190         # get an entity instance
  1188         try:
  1191         try:
  1189             entity = self.repo.vreg['etypes'].etype_class(etype)(cnx)
  1192             entity = self.repo.vreg['etypes'].etype_class(etype)(cnx)
  1190         except Exception:
  1193         except Exception:
  1191             err("can't restore entity %s of type %s, type no more supported"
  1194             err("can't restore entity %s of type %s, type no more supported"
  1237         eid = action.eid
  1240         eid = action.eid
  1238         # XXX done to avoid fetching all remaining relation for the entity
  1241         # XXX done to avoid fetching all remaining relation for the entity
  1239         # we should find an efficient way to do this (keeping current veolidf
  1242         # we should find an efficient way to do this (keeping current veolidf
  1240         # massive deletion performance)
  1243         # massive deletion performance)
  1241         if _undo_has_later_transaction(cnx, eid):
  1244         if _undo_has_later_transaction(cnx, eid):
  1242             msg = cnx._('some later transaction(s) touch entity, undo them '
  1245             msg = cnx._('some later transaction(s) touch entity, undo them first')
  1243                             'first')
       
  1244             raise ValidationError(eid, {None: msg})
  1246             raise ValidationError(eid, {None: msg})
  1245         etype = action.etype
  1247         etype = action.etype
  1246         # get an entity instance
  1248         # get an entity instance
  1247         try:
  1249         try:
  1248             entity = self.repo.vreg['etypes'].etype_class(etype)(cnx)
  1250             entity = self.repo.vreg['etypes'].etype_class(etype)(cnx)
  1275         err = errors.append
  1277         err = errors.append
  1276         try:
  1278         try:
  1277             entity = cnx.entity_from_eid(action.eid)
  1279             entity = cnx.entity_from_eid(action.eid)
  1278         except UnknownEid:
  1280         except UnknownEid:
  1279             err(cnx._("can't restore state of entity %s, it has been "
  1281             err(cnx._("can't restore state of entity %s, it has been "
  1280                           "deleted inbetween") % action.eid)
  1282                       "deleted inbetween") % action.eid)
  1281             return errors
  1283             return errors
  1282         self._reedit_entity(entity, action.changes, err)
  1284         self._reedit_entity(entity, action.changes, err)
  1283         entity.cw_edited.check()
  1285         entity.cw_edited.check()
  1284         self.repo.hm.call_hooks('before_update_entity', cnx, entity=entity)
  1286         self.repo.hm.call_hooks('before_update_entity', cnx, entity=entity)
  1285         sql = self.sqlgen.update(SQL_PREFIX + entity.cw_etype, action.changes,
  1287         sql = self.sqlgen.update(SQL_PREFIX + entity.cw_etype, action.changes,
  1344         cursor = cnx.cnxset.cu
  1346         cursor = cnx.cnxset.cu
  1345         cursor_unindex_object = self.dbhelper.cursor_unindex_object
  1347         cursor_unindex_object = self.dbhelper.cursor_unindex_object
  1346         try:
  1348         try:
  1347             for entity in entities:
  1349             for entity in entities:
  1348                 cursor_unindex_object(entity.eid, cursor)
  1350                 cursor_unindex_object(entity.eid, cursor)
  1349         except Exception: # let KeyboardInterrupt / SystemExit propagate
  1351         except Exception:  # let KeyboardInterrupt / SystemExit propagate
  1350             self.exception('error while unindexing %s', entity)
  1352             self.exception('error while unindexing %s', entity)
  1351 
       
  1352 
  1353 
  1353     def fti_index_entities(self, cnx, entities):
  1354     def fti_index_entities(self, cnx, entities):
  1354         """add text content of created/modified entities to the full text index
  1355         """add text content of created/modified entities to the full text index
  1355         """
  1356         """
  1356         cursor_index_object = self.dbhelper.cursor_index_object
  1357         cursor_index_object = self.dbhelper.cursor_index_object
  1360             # unindexing done in the FTIndexEntityOp
  1361             # unindexing done in the FTIndexEntityOp
  1361             for entity in entities:
  1362             for entity in entities:
  1362                 cursor_index_object(entity.eid,
  1363                 cursor_index_object(entity.eid,
  1363                                     entity.cw_adapt_to('IFTIndexable'),
  1364                                     entity.cw_adapt_to('IFTIndexable'),
  1364                                     cursor)
  1365                                     cursor)
  1365         except Exception: # let KeyboardInterrupt / SystemExit propagate
  1366         except Exception:  # let KeyboardInterrupt / SystemExit propagate
  1366             self.exception('error while indexing %s', entity)
  1367             self.exception('error while indexing %s', entity)
  1367 
  1368 
  1368 
  1369 
  1369 class FTIndexEntityOp(hook.DataOperationMixIn, hook.LateOperation):
  1370 class FTIndexEntityOp(hook.DataOperationMixIn, hook.LateOperation):
  1370     """operation to delay entity full text indexation to commit
  1371     """operation to delay entity full text indexation to commit
  1388             done.add(eid)
  1389             done.add(eid)
  1389             iftindexable = cnx.entity_from_eid(eid).cw_adapt_to('IFTIndexable')
  1390             iftindexable = cnx.entity_from_eid(eid).cw_adapt_to('IFTIndexable')
  1390             to_reindex |= set(iftindexable.fti_containers())
  1391             to_reindex |= set(iftindexable.fti_containers())
  1391         source.fti_unindex_entities(cnx, to_reindex)
  1392         source.fti_unindex_entities(cnx, to_reindex)
  1392         source.fti_index_entities(cnx, to_reindex)
  1393         source.fti_index_entities(cnx, to_reindex)
       
  1394 
  1393 
  1395 
  1394 def sql_schema(driver):
  1396 def sql_schema(driver):
  1395     """Yield SQL statements to create system tables in the database."""
  1397     """Yield SQL statements to create system tables in the database."""
  1396     helper = get_db_helper(driver)
  1398     helper = get_db_helper(driver)
  1397     typemap = helper.TYPE_MAPPING
  1399     typemap = helper.TYPE_MAPPING
  1486 
  1488 
  1487     def set_schema(self, schema):
  1489     def set_schema(self, schema):
  1488         """set the instance'schema"""
  1490         """set the instance'schema"""
  1489         pass
  1491         pass
  1490 
  1492 
       
  1493 
  1491 class LoginPasswordAuthentifier(BaseAuthentifier):
  1494 class LoginPasswordAuthentifier(BaseAuthentifier):
  1492     passwd_rql = 'Any P WHERE X is CWUser, X login %(login)s, X upassword P'
  1495     passwd_rql = 'Any P WHERE X is CWUser, X login %(login)s, X upassword P'
  1493     auth_rql = (u'Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s, '
  1496     auth_rql = (u'Any X WHERE X is CWUser, X login %(login)s, X upassword %(pwd)s, '
  1494                 'X cw_source S, S name "system"')
  1497                 'X cw_source S, S name "system"')
  1495     _sols = ({'X': 'CWUser', 'P': 'Password', 'S': 'CWSource'},)
  1498     _sols = ({'X': 'CWUser', 'P': 'Password', 'S': 'CWSource'},)
  1496 
  1499 
  1497     def set_schema(self, schema):
  1500     def set_schema(self, schema):
  1498         """set the instance'schema"""
  1501         """set the instance'schema"""
  1499         if 'CWUser' in schema: # probably an empty schema if not true...
  1502         if 'CWUser' in schema:  # probably an empty schema if not true...
  1500             # rql syntax trees used to authenticate users
  1503             # rql syntax trees used to authenticate users
  1501             self._passwd_rqlst = self.source.compile_rql(self.passwd_rql, self._sols)
  1504             self._passwd_rqlst = self.source.compile_rql(self.passwd_rql, self._sols)
  1502             self._auth_rqlst = self.source.compile_rql(self.auth_rql, self._sols)
  1505             self._auth_rqlst = self.source.compile_rql(self.auth_rql, self._sols)
  1503 
  1506 
  1504     def authenticate(self, cnx, login, password=None, **kwargs):
  1507     def authenticate(self, cnx, login, password=None, **kwargs):
  1506         defined in this source, else raise `AuthenticationError`
  1509         defined in this source, else raise `AuthenticationError`
  1507 
  1510 
  1508         two queries are needed since passwords are stored crypted, so we have
  1511         two queries are needed since passwords are stored crypted, so we have
  1509         to fetch the salt first
  1512         to fetch the salt first
  1510         """
  1513         """
  1511         args = {'login': login, 'pwd' : None}
  1514         args = {'login': login, 'pwd': None}
  1512         if password is not None:
  1515         if password is not None:
  1513             rset = self.source.syntax_tree_search(cnx, self._passwd_rqlst, args)
  1516             rset = self.source.syntax_tree_search(cnx, self._passwd_rqlst, args)
  1514             try:
  1517             try:
  1515                 pwd = rset[0][0]
  1518                 pwd = rset[0][0]
  1516             except IndexError:
  1519             except IndexError:
  1527             user = rset[0][0]
  1530             user = rset[0][0]
  1528             # If the stored hash uses a deprecated scheme (e.g. DES or MD5 used
  1531             # If the stored hash uses a deprecated scheme (e.g. DES or MD5 used
  1529             # before 3.14.7), update with a fresh one
  1532             # before 3.14.7), update with a fresh one
  1530             if pwd is not None and pwd.getvalue():
  1533             if pwd is not None and pwd.getvalue():
  1531                 verify, newhash = verify_and_update(password, pwd.getvalue())
  1534                 verify, newhash = verify_and_update(password, pwd.getvalue())
  1532                 if not verify: # should not happen, but...
  1535                 if not verify:  # should not happen, but...
  1533                     raise AuthenticationError('bad password')
  1536                     raise AuthenticationError('bad password')
  1534                 if newhash:
  1537                 if newhash:
  1535                     cnx.system_sql("UPDATE %s SET %s=%%(newhash)s WHERE %s=%%(login)s" % (
  1538                     cnx.system_sql("UPDATE %s SET %s=%%(newhash)s WHERE %s=%%(login)s"
  1536                                         SQL_PREFIX + 'CWUser',
  1539                                    % (SQL_PREFIX + 'CWUser',
  1537                                         SQL_PREFIX + 'upassword',
  1540                                       SQL_PREFIX + 'upassword',
  1538                                         SQL_PREFIX + 'login'),
  1541                                       SQL_PREFIX + 'login'),
  1539                                        {'newhash': self.source._binary(newhash.encode('ascii')),
  1542                                    {'newhash': self.source._binary(newhash.encode('ascii')),
  1540                                         'login': login})
  1543                                     'login': login})
  1541                     cnx.commit()
  1544                     cnx.commit()
  1542             return user
  1545             return user
  1543         except IndexError:
  1546         except IndexError:
  1544             raise AuthenticationError('bad password')
  1547             raise AuthenticationError('bad password')
  1545 
  1548 
  1546 
  1549 
  1547 class EmailPasswordAuthentifier(BaseAuthentifier):
  1550 class EmailPasswordAuthentifier(BaseAuthentifier):
  1548     def authenticate(self, cnx, login, **authinfo):
  1551     def authenticate(self, cnx, login, **authinfo):
  1549         # email_auth flag prevent from infinite recursion (call to
  1552         # email_auth flag prevent from infinite recursion (call to
  1550         # repo.check_auth_info at the end of this method may lead us here again)
  1553         # repo.check_auth_info at the end of this method may lead us here again)
  1551         if not '@' in login or authinfo.pop('email_auth', None):
  1554         if '@' not in login or authinfo.pop('email_auth', None):
  1552             raise AuthenticationError('not an email')
  1555             raise AuthenticationError('not an email')
  1553         rset = cnx.execute('Any L WHERE U login L, U primary_email M, '
  1556         rset = cnx.execute('Any L WHERE U login L, U primary_email M, '
  1554                                'M address %(login)s', {'login': login},
  1557                            'M address %(login)s', {'login': login},
  1555                                build_descr=False)
  1558                            build_descr=False)
  1556         if rset.rowcount != 1:
  1559         if rset.rowcount != 1:
  1557             raise AuthenticationError('unexisting email')
  1560             raise AuthenticationError('unexisting email')
  1558         login = rset.rows[0][0]
  1561         login = rset.rows[0][0]
  1559         authinfo['email_auth'] = True
  1562         authinfo['email_auth'] = True
  1560         return self.source.repo.check_auth_info(cnx, login, authinfo)
  1563         return self.source.repo.check_auth_info(cnx, login, authinfo)
  1635         prefix = 'cw_'
  1638         prefix = 'cw_'
  1636         for etype in self.schema.entities():
  1639         for etype in self.schema.entities():
  1637             eschema = self.schema.eschema(etype)
  1640             eschema = self.schema.eschema(etype)
  1638             if eschema.final:
  1641             if eschema.final:
  1639                 continue
  1642                 continue
  1640             etype_tables.append('%s%s'%(prefix, etype))
  1643             etype_tables.append('%s%s' % (prefix, etype))
  1641         for rtype in self.schema.relations():
  1644         for rtype in self.schema.relations():
  1642             rschema = self.schema.rschema(rtype)
  1645             rschema = self.schema.rschema(rtype)
  1643             if rschema.final or rschema.inlined or rschema in VIRTUAL_RTYPES:
  1646             if rschema.final or rschema.inlined or rschema in VIRTUAL_RTYPES:
  1644                 continue
  1647                 continue
  1645             relation_tables.append('%s_relation' % rtype)
  1648             relation_tables.append('%s_relation' % rtype)
  1687             for i, start in enumerate(range(0, rowcount, blocksize)):
  1690             for i, start in enumerate(range(0, rowcount, blocksize)):
  1688                 rows = list(itertools.islice(rows_iterator, blocksize))
  1691                 rows = list(itertools.islice(rows_iterator, blocksize))
  1689                 serialized = self._serialize(table, columns, rows)
  1692                 serialized = self._serialize(table, columns, rows)
  1690                 archive.writestr('tables/%s.%04d' % (table, i), serialized)
  1693                 archive.writestr('tables/%s.%04d' % (table, i), serialized)
  1691                 self.logger.debug('wrote rows %d to %d (out of %d) to %s.%04d',
  1694                 self.logger.debug('wrote rows %d to %d (out of %d) to %s.%04d',
  1692                                   start, start+len(rows)-1,
  1695                                   start, start + len(rows) - 1,
  1693                                   rowcount,
  1696                                   rowcount,
  1694                                   table, i)
  1697                                   table, i)
  1695         else:
  1698         else:
  1696             rows = []
  1699             rows = []
  1697             serialized = self._serialize(table, columns, rows)
  1700             serialized = self._serialize(table, columns, rows)
  1793                 self.cursor.execute(insert, merge_args(dict(zip(columns, row)), {}))
  1796                 self.cursor.execute(insert, merge_args(dict(zip(columns, row)), {}))
  1794             row_count += len(rows)
  1797             row_count += len(rows)
  1795             self.cnx.commit()
  1798             self.cnx.commit()
  1796         self.logger.info('inserted %d rows', row_count)
  1799         self.logger.info('inserted %d rows', row_count)
  1797 
  1800 
  1798 
       
  1799     def _parse_versions(self, version_str):
  1801     def _parse_versions(self, version_str):
  1800         versions = set()
  1802         versions = set()
  1801         for line in version_str.splitlines():
  1803         for line in version_str.splitlines():
  1802             versions.add(tuple(line.split()))
  1804             versions.add(tuple(line.split()))
  1803         return versions
  1805         return versions