cubicweb/server/repository.py
changeset 11348 70337ad23145
parent 11204 71057a8bb19a
child 11477 3b4d41566de3
equal deleted inserted replaced
11347:b4dcfd734686 11348:70337ad23145
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    27 """
    27 """
    28 from __future__ import print_function
    28 from __future__ import print_function
    29 
    29 
    30 __docformat__ = "restructuredtext en"
    30 __docformat__ = "restructuredtext en"
    31 
    31 
    32 import threading
       
    33 from warnings import warn
    32 from warnings import warn
    34 from itertools import chain
    33 from itertools import chain
    35 from time import time, localtime, strftime
    34 from time import time, localtime, strftime
    36 from contextlib import contextmanager
    35 from contextlib import contextmanager
    37 
    36 
    49                       UniqueTogetherError, onevent, ViolatedConstraint)
    48                       UniqueTogetherError, onevent, ViolatedConstraint)
    50 from cubicweb import cwvreg, schema, server
    49 from cubicweb import cwvreg, schema, server
    51 from cubicweb.server import ShuttingDown, utils, hook, querier, sources
    50 from cubicweb.server import ShuttingDown, utils, hook, querier, sources
    52 from cubicweb.server.session import Session, InternalManager
    51 from cubicweb.server.session import Session, InternalManager
    53 
    52 
    54 NO_CACHE_RELATIONS = set( [('owned_by', 'object'),
    53 
    55                            ('created_by', 'object'),
    54 NO_CACHE_RELATIONS = set([
    56                            ('cw_source', 'object'),
    55     ('owned_by', 'object'),
    57                            ])
    56     ('created_by', 'object'),
       
    57     ('cw_source', 'object'),
       
    58 ])
       
    59 
    58 
    60 
    59 def prefill_entity_caches(entity):
    61 def prefill_entity_caches(entity):
    60     cnx = entity._cw
    62     cnx = entity._cw
    61     # prefill entity relation caches
    63     # prefill entity relation caches
    62     for rschema in entity.e_schema.subject_relations():
    64     for rschema in entity.e_schema.subject_relations():
    71     for rschema in entity.e_schema.object_relations():
    73     for rschema in entity.e_schema.object_relations():
    72         rtype = str(rschema)
    74         rtype = str(rschema)
    73         if rtype in schema.VIRTUAL_RTYPES or (rtype, 'object') in NO_CACHE_RELATIONS:
    75         if rtype in schema.VIRTUAL_RTYPES or (rtype, 'object') in NO_CACHE_RELATIONS:
    74             continue
    76             continue
    75         entity.cw_set_relation_cache(rtype, 'object', cnx.empty_rset())
    77         entity.cw_set_relation_cache(rtype, 'object', cnx.empty_rset())
       
    78 
    76 
    79 
    77 def del_existing_rel_if_needed(cnx, eidfrom, rtype, eidto):
    80 def del_existing_rel_if_needed(cnx, eidfrom, rtype, eidto):
    78     """delete existing relation when adding a new one if card is 1 or ?
    81     """delete existing relation when adding a new one if card is 1 or ?
    79 
    82 
    80     have to be done once the new relation has been inserted to avoid having
    83     have to be done once the new relation has been inserted to avoid having
   118     relations = []
   121     relations = []
   119     activeintegrity = cnx.is_hook_category_activated('activeintegrity')
   122     activeintegrity = cnx.is_hook_category_activated('activeintegrity')
   120     eschema = entity.e_schema
   123     eschema = entity.e_schema
   121     for attr in entity.cw_edited:
   124     for attr in entity.cw_edited:
   122         rschema = eschema.subjrels[attr]
   125         rschema = eschema.subjrels[attr]
   123         if not rschema.final: # inlined relation
   126         if not rschema.final:  # inlined relation
   124             value = entity.cw_edited[attr]
   127             value = entity.cw_edited[attr]
   125             relations.append((attr, value))
   128             relations.append((attr, value))
   126             cnx.update_rel_cache_add(entity.eid, attr, value)
   129             cnx.update_rel_cache_add(entity.eid, attr, value)
   127             rdef = cnx.rtype_eids_rdef(attr, entity.eid, value)
   130             rdef = cnx.rtype_eids_rdef(attr, entity.eid, value)
   128             if rdef.cardinality[1] in '1?' and activeintegrity:
   131             if rdef.cardinality[1] in '1?' and activeintegrity:
   129                 with cnx.security_enabled(read=False):
   132                 with cnx.security_enabled(read=False):
   130                     cnx.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
   133                     cnx.execute('DELETE X %s Y WHERE Y eid %%(y)s' % attr,
   131                                     {'x': entity.eid, 'y': value})
   134                                 {'x': entity.eid, 'y': value})
   132     return relations
   135     return relations
   133 
   136 
   134 
   137 
   135 class NullEventBus(object):
   138 class NullEventBus(object):
   136     def publish(self, msg):
   139     def publish(self, msg):
   166         # list of functions to be called at regular interval
   169         # list of functions to be called at regular interval
   167         # list of running threads
   170         # list of running threads
   168         self._running_threads = []
   171         self._running_threads = []
   169         # initial schema, should be build or replaced latter
   172         # initial schema, should be build or replaced latter
   170         self.schema = schema.CubicWebSchema(config.appid)
   173         self.schema = schema.CubicWebSchema(config.appid)
   171         self.vreg.schema = self.schema # until actual schema is loaded...
   174         self.vreg.schema = self.schema  # until actual schema is loaded...
   172         # shutdown flag
   175         # shutdown flag
   173         self.shutting_down = False
   176         self.shutting_down = False
   174         # sources (additional sources info in the system database)
   177         # sources (additional sources info in the system database)
   175         self.system_source = self.get_source('native', 'system',
   178         self.system_source = self.get_source('native', 'system',
   176                                              config.system_source_config.copy())
   179                                              config.system_source_config.copy())
   184         # open some connection sets
   187         # open some connection sets
   185         if config.init_cnxset_pool:
   188         if config.init_cnxset_pool:
   186             self.init_cnxset_pool()
   189             self.init_cnxset_pool()
   187         # the hooks manager
   190         # the hooks manager
   188         self.hm = hook.HooksManager(self.vreg)
   191         self.hm = hook.HooksManager(self.vreg)
       
   192 
   189         # registry hook to fix user class on registry reload
   193         # registry hook to fix user class on registry reload
   190         @onevent('after-registry-reload', self)
   194         @onevent('after-registry-reload', self)
   191         def fix_user_classes(self):
   195         def fix_user_classes(self):
   192             # After registry reload the 'CWUser' class used for CWEtype
   196             # After registry reload the 'CWUser' class used for CWEtype
   193             # changed.  So any existing user object have a different class than
   197             # changed.  So any existing user object have a different class than
   245             if 'CWProperty' in self.schema:
   249             if 'CWProperty' in self.schema:
   246                 self.vreg.init_properties(self.properties())
   250                 self.vreg.init_properties(self.properties())
   247         # 4. close initialization connection set and reopen fresh ones for
   251         # 4. close initialization connection set and reopen fresh ones for
   248         #    proper initialization
   252         #    proper initialization
   249         self._get_cnxset().close(True)
   253         self._get_cnxset().close(True)
   250         self.cnxsets = [] # list of available cnxsets (can't iterate on a Queue)
   254         # list of available cnxsets (can't iterate on a Queue)
       
   255         self.cnxsets = []
   251         for i in range(config['connections-pool-size']):
   256         for i in range(config['connections-pool-size']):
   252             self.cnxsets.append(self.system_source.wrapped_connection())
   257             self.cnxsets.append(self.system_source.wrapped_connection())
   253             self._cnxsets_pool.put_nowait(self.cnxsets[-1])
   258             self._cnxsets_pool.put_nowait(self.cnxsets[-1])
   254 
   259 
   255     # internals ###############################################################
   260     # internals ###############################################################
   256 
   261 
   257     def init_sources_from_database(self):
   262     def init_sources_from_database(self):
   258         self.sources_by_eid = {}
   263         self.sources_by_eid = {}
   259         if self.config.quick_start \
   264         if self.config.quick_start or 'CWSource' not in self.schema:  # 3.10 migration
   260                or not 'CWSource' in self.schema: # # 3.10 migration
       
   261             self.system_source.init_creating()
   265             self.system_source.init_creating()
   262             return
   266             return
   263         with self.internal_cnx() as cnx:
   267         with self.internal_cnx() as cnx:
   264             # FIXME: sources should be ordered (add_entity priority)
   268             # FIXME: sources should be ordered (add_entity priority)
   265             for sourceent in cnx.execute(
   269             for sourceent in cnx.execute(
   266                 'Any S, SN, SA, SC WHERE S is_instance_of CWSource, '
   270                     'Any S, SN, SA, SC WHERE S is_instance_of CWSource, '
   267                 'S name SN, S type SA, S config SC').entities():
   271                     'S name SN, S type SA, S config SC').entities():
   268                 if sourceent.name == 'system':
   272                 if sourceent.name == 'system':
   269                     self.system_source.eid = sourceent.eid
   273                     self.system_source.eid = sourceent.eid
   270                     self.sources_by_eid[sourceent.eid] = self.system_source
   274                     self.sources_by_eid[sourceent.eid] = self.system_source
   271                     self.system_source.init(True, sourceent)
   275                     self.system_source.init(True, sourceent)
   272                     continue
   276                     continue
   347             # call instance level initialisation hooks
   351             # call instance level initialisation hooks
   348             self.hm.call_hooks('server_startup', repo=self)
   352             self.hm.call_hooks('server_startup', repo=self)
   349             # register a task to cleanup expired session
   353             # register a task to cleanup expired session
   350             self.cleanup_session_time = self.config['cleanup-session-time'] or 60 * 60 * 24
   354             self.cleanup_session_time = self.config['cleanup-session-time'] or 60 * 60 * 24
   351             assert self.cleanup_session_time > 0
   355             assert self.cleanup_session_time > 0
   352             cleanup_session_interval = min(60*60, self.cleanup_session_time / 3)
   356             cleanup_session_interval = min(60 * 60, self.cleanup_session_time / 3)
   353             assert self._tasks_manager is not None, "This Repository is not intended to be used as a server"
   357             assert self._tasks_manager is not None, \
       
   358                 "This Repository is not intended to be used as a server"
   354             self._tasks_manager.add_looping_task(cleanup_session_interval,
   359             self._tasks_manager.add_looping_task(cleanup_session_interval,
   355                                                  self.clean_sessions)
   360                                                  self.clean_sessions)
   356 
   361 
   357     def start_looping_tasks(self):
   362     def start_looping_tasks(self):
   358         """Actual "Repository as a server" startup.
   363         """Actual "Repository as a server" startup.
   363 
   368 
   364         XXX Other startup related stuffs are done elsewhere. In Repository
   369         XXX Other startup related stuffs are done elsewhere. In Repository
   365         XXX __init__ or in external codes (various server managers).
   370         XXX __init__ or in external codes (various server managers).
   366         """
   371         """
   367         self._prepare_startup()
   372         self._prepare_startup()
   368         assert self._tasks_manager is not None, "This Repository is not intended to be used as a server"
   373         assert self._tasks_manager is not None,\
       
   374             "This Repository is not intended to be used as a server"
   369         self._tasks_manager.start()
   375         self._tasks_manager.start()
   370 
   376 
   371     def looping_task(self, interval, func, *args):
   377     def looping_task(self, interval, func, *args):
   372         """register a function to be called every `interval` seconds.
   378         """register a function to be called every `interval` seconds.
   373 
   379 
   374         looping tasks can only be registered during repository initialization,
   380         looping tasks can only be registered during repository initialization,
   375         once done this method will fail.
   381         once done this method will fail.
   376         """
   382         """
   377         assert self._tasks_manager is not None, "This Repository is not intended to be used as a server"
   383         assert self._tasks_manager is not None,\
       
   384             "This Repository is not intended to be used as a server"
   378         self._tasks_manager.add_looping_task(interval, func, *args)
   385         self._tasks_manager.add_looping_task(interval, func, *args)
   379 
   386 
   380     def threaded_task(self, func):
   387     def threaded_task(self, func):
   381         """start function in a separated thread"""
   388         """start function in a separated thread"""
   382         utils.RepoThread(func, self._running_threads).start()
   389         utils.RepoThread(func, self._running_threads).start()
   383 
   390 
   384     #@locked
       
   385     def _get_cnxset(self):
   391     def _get_cnxset(self):
   386         try:
   392         try:
   387             return self._cnxsets_pool.get(True, timeout=5)
   393             return self._cnxsets_pool.get(True, timeout=5)
   388         except queue.Empty:
   394         except queue.Empty:
   389             raise Exception('no connections set available after 5 secs, probably either a '
   395             raise Exception('no connections set available after 5 secs, probably either a '
   428             self.info('rql st cache hit/miss: %s/%s (%s%% hits)', hits, misses,
   434             self.info('rql st cache hit/miss: %s/%s (%s%% hits)', hits, misses,
   429                       (hits * 100) / (hits + misses))
   435                       (hits * 100) / (hits + misses))
   430             hits, misses = self.system_source.cache_hit, self.system_source.cache_miss
   436             hits, misses = self.system_source.cache_hit, self.system_source.cache_miss
   431             self.info('sql cache hit/miss: %s/%s (%s%% hits)', hits, misses,
   437             self.info('sql cache hit/miss: %s/%s (%s%% hits)', hits, misses,
   432                       (hits * 100) / (hits + misses))
   438                       (hits * 100) / (hits + misses))
   433             nocache  = self.system_source.no_cache
   439             nocache = self.system_source.no_cache
   434             self.info('sql cache usage: %s/%s (%s%%)', hits+ misses, nocache,
   440             self.info('sql cache usage: %s/%s (%s%%)', hits + misses, nocache,
   435                       ((hits + misses) * 100) / (hits + misses + nocache))
   441                       ((hits + misses) * 100) / (hits + misses + nocache))
   436         except ZeroDivisionError:
   442         except ZeroDivisionError:
   437             pass
   443             pass
   438 
   444 
   439     def check_auth_info(self, cnx, login, authinfo):
   445     def check_auth_info(self, cnx, login, authinfo):
   456         return associated CWUser instance on success
   462         return associated CWUser instance on success
   457         """
   463         """
   458         eid = self.check_auth_info(cnx, login, authinfo)
   464         eid = self.check_auth_info(cnx, login, authinfo)
   459         cwuser = self._build_user(cnx, eid)
   465         cwuser = self._build_user(cnx, eid)
   460         if self.config.consider_user_state and \
   466         if self.config.consider_user_state and \
   461                not cwuser.cw_adapt_to('IWorkflowable').state in cwuser.AUTHENTICABLE_STATES:
   467            not cwuser.cw_adapt_to('IWorkflowable').state in cwuser.AUTHENTICABLE_STATES:
   462             raise AuthenticationError('user is not in authenticable state')
   468             raise AuthenticationError('user is not in authenticable state')
   463         return cwuser
   469         return cwuser
   464 
   470 
   465     def _build_user(self, cnx, eid):
   471     def _build_user(self, cnx, eid):
   466         """return a CWUser entity for user with the given eid"""
   472         """return a CWUser entity for user with the given eid"""
   478         return cwuser
   484         return cwuser
   479 
   485 
   480     # public (dbapi) interface ################################################
   486     # public (dbapi) interface ################################################
   481 
   487 
   482     @deprecated("[3.19] use _cw.call_service('repo_stats')")
   488     @deprecated("[3.19] use _cw.call_service('repo_stats')")
   483     def stats(self): # XXX restrict to managers session?
   489     def stats(self):  # XXX restrict to managers session?
   484         """Return a dictionary containing some statistics about the repository
   490         """Return a dictionary containing some statistics about the repository
   485         resources usage.
   491         resources usage.
   486 
   492 
   487         This is a public method, not requiring a session id.
   493         This is a public method, not requiring a session id.
   488 
   494 
   546         """
   552         """
   547         from logilab.common.changelog import Version
   553         from logilab.common.changelog import Version
   548         vcconf = {}
   554         vcconf = {}
   549         with self.internal_cnx() as cnx:
   555         with self.internal_cnx() as cnx:
   550             for pk, version in cnx.execute(
   556             for pk, version in cnx.execute(
   551                 'Any K,V WHERE P is CWProperty, P value V, P pkey K, '
   557                     'Any K,V WHERE P is CWProperty, P value V, P pkey K, '
   552                 'P pkey ~="system.version.%"', build_descr=False):
   558                     'P pkey ~="system.version.%"', build_descr=False):
   553                 cube = pk.split('.')[-1]
   559                 cube = pk.split('.')[-1]
   554                 # XXX cubicweb migration
   560                 # XXX cubicweb migration
   555                 if cube in CW_MIGRATION_MAP:
   561                 if cube in CW_MIGRATION_MAP:
   556                     cube = CW_MIGRATION_MAP[cube]
   562                     cube = CW_MIGRATION_MAP[cube]
   557                 version = Version(version)
   563                 version = Version(version)
   690 
   696 
   691         Beware that unlike the older :meth:`internal_session`, internal
   697         Beware that unlike the older :meth:`internal_session`, internal
   692         connections have all hooks beside security enabled.
   698         connections have all hooks beside security enabled.
   693         """
   699         """
   694         with Session(InternalManager(), self).new_cnx() as cnx:
   700         with Session(InternalManager(), self).new_cnx() as cnx:
   695             cnx.user._cw = cnx  # XXX remove when "vreg = user._cw.vreg"
   701             cnx.user._cw = cnx  # XXX remove when "vreg = user._cw.vreg" hack in entity.py is gone
   696                                 # hack in entity.py is gone
       
   697             with cnx.security_enabled(read=False, write=False):
   702             with cnx.security_enabled(read=False, write=False):
   698                 yield cnx
   703                 yield cnx
   699 
   704 
   700     def _get_session(self, sessionid, txid=None, checkshuttingdown=True):
   705     def _get_session(self, sessionid, txid=None, checkshuttingdown=True):
   701         """return the session associated with the given session identifier"""
   706         """return the session associated with the given session identifier"""
   730         etcache = self._type_source_cache
   735         etcache = self._type_source_cache
   731         extidcache = self._extid_cache
   736         extidcache = self._extid_cache
   732         rqlcache = self.querier._rql_cache
   737         rqlcache = self.querier._rql_cache
   733         for eid in eids:
   738         for eid in eids:
   734             try:
   739             try:
   735                 etype, extid, auri = etcache.pop(int(eid)) # may be a string in some cases
   740                 etype, extid, auri = etcache.pop(int(eid))  # may be a string in some cases
   736                 rqlcache.pop( ('%s X WHERE X eid %s' % (etype, eid),), None)
   741                 rqlcache.pop(('%s X WHERE X eid %s' % (etype, eid),), None)
   737                 extidcache.pop(extid, None)
   742                 extidcache.pop(extid, None)
   738             except KeyError:
   743             except KeyError:
   739                 etype = None
   744                 etype = None
   740             rqlcache.pop( ('Any X WHERE X eid %s' % eid,), None)
   745             rqlcache.pop(('Any X WHERE X eid %s' % eid,), None)
   741             self.system_source.clear_eid_cache(eid, etype)
   746             self.system_source.clear_eid_cache(eid, etype)
   742 
   747 
   743     def type_from_eid(self, eid, cnx):
   748     def type_from_eid(self, eid, cnx):
   744         """return the type of the entity with id <eid>"""
   749         """return the type of the entity with id <eid>"""
   745         return self.type_and_source_from_eid(eid, cnx)[0]
   750         return self.type_and_source_from_eid(eid, cnx)[0]
   844         with cnx.security_enabled(read=False, write=False):
   849         with cnx.security_enabled(read=False, write=False):
   845             in_eids = ','.join([str(_e.eid) for _e in entities])
   850             in_eids = ','.join([str(_e.eid) for _e in entities])
   846             with cnx.running_hooks_ops():
   851             with cnx.running_hooks_ops():
   847                 for rschema, _, role in entities[0].e_schema.relation_definitions():
   852                 for rschema, _, role in entities[0].e_schema.relation_definitions():
   848                     if rschema.rule:
   853                     if rschema.rule:
   849                         continue # computed relation
   854                         continue  # computed relation
   850                     rtype = rschema.type
   855                     rtype = rschema.type
   851                     if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
   856                     if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
   852                         continue
   857                         continue
   853                     if role == 'subject':
   858                     if role == 'subject':
   854                         # don't skip inlined relation so they are regularly
   859                         # don't skip inlined relation so they are regularly
   859                     try:
   864                     try:
   860                         cnx.execute(rql, build_descr=False)
   865                         cnx.execute(rql, build_descr=False)
   861                     except ValidationError:
   866                     except ValidationError:
   862                         raise
   867                         raise
   863                     except Unauthorized:
   868                     except Unauthorized:
   864                         self.exception('Unauthorized exception while cascading delete for entity %s. '
   869                         self.exception(
   865                                        'RQL: %s.\nThis should not happen since security is disabled here.',
   870                             'Unauthorized exception while cascading delete for entity %s. '
   866                                        entities, rql)
   871                             'RQL: %s.\nThis should not happen since security is disabled here.',
       
   872                             entities, rql)
   867                         raise
   873                         raise
   868                     except Exception:
   874                     except Exception:
   869                         if self.config.mode == 'test':
   875                         if self.config.mode == 'test':
   870                             raise
   876                             raise
   871                         self.exception('error while cascading delete for entity %s. RQL: %s',
   877                         self.exception('error while cascading delete for entity %s. RQL: %s',
   889 
   895 
   890         the entity eid should originally be None and a unique eid is assigned to
   896         the entity eid should originally be None and a unique eid is assigned to
   891         the entity instance
   897         the entity instance
   892         """
   898         """
   893         entity = edited.entity
   899         entity = edited.entity
   894         entity._cw_is_saved = False # entity has an eid but is not yet saved
   900         entity._cw_is_saved = False  # entity has an eid but is not yet saved
   895         # init edited_attributes before calling before_add_entity hooks
   901         # init edited_attributes before calling before_add_entity hooks
   896         entity.cw_edited = edited
   902         entity.cw_edited = edited
   897         source = self.system_source
   903         source = self.system_source
   898         # allocate an eid to the entity before calling hooks
   904         # allocate an eid to the entity before calling hooks
   899         entity.eid = self.system_source.create_eid(cnx)
   905         entity.eid = self.system_source.create_eid(cnx)
   918         # trigger after_add_entity after after_add_relation
   924         # trigger after_add_entity after after_add_relation
   919         self.hm.call_hooks('after_add_entity', cnx, entity=entity)
   925         self.hm.call_hooks('after_add_entity', cnx, entity=entity)
   920         # call hooks for inlined relations
   926         # call hooks for inlined relations
   921         for attr, value in relations:
   927         for attr, value in relations:
   922             self.hm.call_hooks('before_add_relation', cnx,
   928             self.hm.call_hooks('before_add_relation', cnx,
   923                                 eidfrom=entity.eid, rtype=attr, eidto=value)
   929                                eidfrom=entity.eid, rtype=attr, eidto=value)
   924             self.hm.call_hooks('after_add_relation', cnx,
   930             self.hm.call_hooks('after_add_relation', cnx,
   925                                 eidfrom=entity.eid, rtype=attr, eidto=value)
   931                                eidfrom=entity.eid, rtype=attr, eidto=value)
   926         return entity.eid
   932         return entity.eid
   927 
   933 
   928     def glob_update_entity(self, cnx, edited):
   934     def glob_update_entity(self, cnx, edited):
   929         """replace an entity in the repository
   935         """replace an entity in the repository
   930         the type and the eid of an entity must not be changed
   936         the type and the eid of an entity must not be changed
   952                     only_inline_rels = False
   958                     only_inline_rels = False
   953                 else:
   959                 else:
   954                     # inlined relation
   960                     # inlined relation
   955                     previous_value = entity.related(attr) or None
   961                     previous_value = entity.related(attr) or None
   956                     if previous_value is not None:
   962                     if previous_value is not None:
   957                         previous_value = previous_value[0][0] # got a result set
   963                         previous_value = previous_value[0][0]  # got a result set
   958                         if previous_value == entity.cw_attr_cache[attr]:
   964                         if previous_value == entity.cw_attr_cache[attr]:
   959                             previous_value = None
   965                             previous_value = None
   960                         else:
   966                         else:
   961                             hm.call_hooks('before_delete_relation', cnx,
   967                             hm.call_hooks('before_delete_relation', cnx,
   962                                           eidfrom=entity.eid, rtype=attr,
   968                                           eidfrom=entity.eid, rtype=attr,
   994                               eidfrom=entity.eid, rtype=attr, eidto=value)
  1000                               eidfrom=entity.eid, rtype=attr, eidto=value)
   995         finally:
  1001         finally:
   996             if orig_edited is not None:
  1002             if orig_edited is not None:
   997                 entity.cw_edited = orig_edited
  1003                 entity.cw_edited = orig_edited
   998 
  1004 
   999 
       
  1000     def glob_delete_entities(self, cnx, eids):
  1005     def glob_delete_entities(self, cnx, eids):
  1001         """delete a list of  entities and all related entities from the repository"""
  1006         """delete a list of  entities and all related entities from the repository"""
  1002         # mark eids as being deleted in cnx info and setup cache update
  1007         # mark eids as being deleted in cnx info and setup cache update
  1003         # operation (register pending eids before actual deletion to avoid
  1008         # operation (register pending eids before actual deletion to avoid
  1004         # multiple call to glob_delete_entities)
  1009         # multiple call to glob_delete_entities)
  1007             warn('[3.13] eids should be given as a set', DeprecationWarning,
  1012             warn('[3.13] eids should be given as a set', DeprecationWarning,
  1008                  stacklevel=2)
  1013                  stacklevel=2)
  1009             eids = frozenset(eids)
  1014             eids = frozenset(eids)
  1010         eids = eids - op._container
  1015         eids = eids - op._container
  1011         op._container |= eids
  1016         op._container |= eids
  1012         data_by_etype = {} # values are [list of entities]
  1017         data_by_etype = {}  # values are [list of entities]
  1013         #
  1018         #
  1014         # WARNING: the way this dictionary is populated is heavily optimized
  1019         # WARNING: the way this dictionary is populated is heavily optimized
  1015         # and does not use setdefault on purpose. Unless a new release
  1020         # and does not use setdefault on purpose. Unless a new release
  1016         # of the Python interpreter advertises large perf improvements
  1021         # of the Python interpreter advertises large perf improvements
  1017         # in setdefault, this should not be changed without profiling.
  1022         # in setdefault, this should not be changed without profiling.
  1024             except KeyError:
  1029             except KeyError:
  1025                 data_by_etype[etype] = [entity]
  1030                 data_by_etype[etype] = [entity]
  1026         source = self.system_source
  1031         source = self.system_source
  1027         for etype, entities in data_by_etype.items():
  1032         for etype, entities in data_by_etype.items():
  1028             if server.DEBUG & server.DBG_REPO:
  1033             if server.DEBUG & server.DBG_REPO:
  1029                 print('DELETE entities', etype, [entity.eid for entity in entities])
  1034                 print('DELETE entities', etype, [e.eid for e in entities])
  1030             self.hm.call_hooks('before_delete_entity', cnx, entities=entities)
  1035             self.hm.call_hooks('before_delete_entity', cnx, entities=entities)
  1031             self._delete_cascade_multi(cnx, entities)
  1036             self._delete_cascade_multi(cnx, entities)
  1032             source.delete_entities(cnx, entities)
  1037             source.delete_entities(cnx, entities)
  1033             source.delete_info_multi(cnx, entities)
  1038             source.delete_info_multi(cnx, entities)
  1034             self.hm.call_hooks('after_delete_entity', cnx, entities=entities)
  1039             self.hm.call_hooks('after_delete_entity', cnx, entities=entities)
  1109         rschema = self.schema.rschema(rtype)
  1114         rschema = self.schema.rschema(rtype)
  1110         cnx.update_rel_cache_del(subject, rtype, object, rschema.symmetric)
  1115         cnx.update_rel_cache_del(subject, rtype, object, rschema.symmetric)
  1111         self.hm.call_hooks('after_delete_relation', cnx,
  1116         self.hm.call_hooks('after_delete_relation', cnx,
  1112                            eidfrom=subject, rtype=rtype, eidto=object)
  1117                            eidfrom=subject, rtype=rtype, eidto=object)
  1113 
  1118 
  1114 
       
  1115 
       
  1116 
       
  1117     # these are overridden by set_log_methods below
  1119     # these are overridden by set_log_methods below
  1118     # only defining here to prevent pylint from complaining
  1120     # only defining here to prevent pylint from complaining
  1119     info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
  1121     info = warning = error = critical = exception = debug = lambda msg, *a, **kw: None
  1120 
  1122 
       
  1123 
  1121 from logging import getLogger
  1124 from logging import getLogger
  1122 from cubicweb import set_log_methods
  1125 from cubicweb import set_log_methods
  1123 set_log_methods(Repository, getLogger('cubicweb.repository'))
  1126 set_log_methods(Repository, getLogger('cubicweb.repository'))