hooks/synccomputed.py
changeset 9967 e65873ad0371
child 10192 365e5a0287d6
equal deleted inserted replaced
9966:6c2d57d1b6de 9967:e65873ad0371
       
     1 # copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """Hooks for synchronizing computed attributes"""
       
    19 
       
    20 __docformat__ = "restructuredtext en"
       
    21 _ = unicode
       
    22 
       
    23 from collections import defaultdict
       
    24 
       
    25 from rql import nodes
       
    26 
       
    27 from cubicweb.server import hook
       
    28 
       
    29 
       
    30 class RecomputeAttributeOperation(hook.DataOperationMixIn, hook.Operation):
       
    31     """Operation to recompute caches of computed attribute at commit time,
       
    32     depending on what's have been modified in the transaction and avoiding to
       
    33     recompute twice the same attribute
       
    34     """
       
    35     containercls = dict
       
    36     def add_data(self, computed_attribute, eid=None):
       
    37         try:
       
    38             self._container[computed_attribute].add(eid)
       
    39         except KeyError:
       
    40             self._container[computed_attribute] = set((eid,))
       
    41 
       
    42     def precommit_event(self):
       
    43         for computed_attribute_rdef, eids in self.get_data().iteritems():
       
    44             attr = computed_attribute_rdef.rtype
       
    45             formula  = computed_attribute_rdef.formula
       
    46             rql = formula.replace('Any ', 'Any X, ', 1)
       
    47             kwargs = None
       
    48             # add constraint on X to the formula
       
    49             if None in eids : # recompute for all etype if None is found
       
    50                 rql += ', X is %s' % computed_attribute_rdef.subject
       
    51             elif len(eids) == 1:
       
    52                 rql += ', X eid %(x)s'
       
    53                 kwargs = {'x': eids.pop()}
       
    54             else:
       
    55                 rql += ', X eid IN (%s)' % ', '.join((str(eid) for eid in eids))
       
    56             update_rql = 'SET X %s %%(value)s WHERE X eid %%(x)s' % attr
       
    57             for eid, value in self.cnx.execute(rql, kwargs):
       
    58                 self.cnx.execute(update_rql, {'value': value, 'x': eid})
       
    59 
       
    60 
       
    61 class EntityWithCACreatedHook(hook.Hook):
       
    62     """When creating an entity that has some computed attribute, those
       
    63     attributes have to be computed.
       
    64 
       
    65     Concret class of this hook are generated at registration time by
       
    66     introspecting the schema.
       
    67     """
       
    68     __abstract__ = True
       
    69     events = ('after_add_entity',)
       
    70     # list of computed attribute rdefs that have to be recomputed
       
    71     computed_attributes = None
       
    72 
       
    73     def __call__(self):
       
    74         for rdef in self.computed_attributes:
       
    75             RecomputeAttributeOperation.get_instance(self._cw).add_data(
       
    76                 rdef, self.entity.eid)
       
    77 
       
    78 
       
    79 class RelationInvolvedInCAModifiedHook(hook.Hook):
       
    80     """When some relation used in a computed attribute is updated, those
       
    81     attributes have to be recomputed.
       
    82 
       
    83     Concret class of this hook are generated at registration time by
       
    84     introspecting the schema.
       
    85     """
       
    86     __abstract__ = True
       
    87     events = ('after_add_relation', 'before_delete_relation')
       
    88     # list of (computed attribute rdef, optimize_on) that have to be recomputed
       
    89     optimized_computed_attributes = None
       
    90 
       
    91     def __call__(self):
       
    92         for rdef, optimize_on in self.optimized_computed_attributes:
       
    93             if optimize_on is None:
       
    94                 eid = None
       
    95             else:
       
    96                 eid = getattr(self, optimize_on)
       
    97             RecomputeAttributeOperation.get_instance(self._cw).add_data(rdef, eid)
       
    98 
       
    99 
       
   100 class AttributeInvolvedInCAModifiedHook(hook.Hook):
       
   101     """When some attribute used in a computed attribute is updated, those
       
   102     attributes have to be recomputed.
       
   103 
       
   104     Concret class of this hook are generated at registration time by
       
   105     introspecting the schema.
       
   106     """
       
   107     __abstract__ = True
       
   108     events = ('after_update_entity',)
       
   109     # list of (computed attribute rdef, attributes of this entity type involved)
       
   110     # that may have to be recomputed
       
   111     attributes_computed_attributes = None
       
   112 
       
   113     def __call__(self):
       
   114         edited_attributes = frozenset(self.entity.cw_edited)
       
   115         for rdef, used_attributes in self.attributes_computed_attributes.iteritems():
       
   116             if edited_attributes.intersection(used_attributes):
       
   117                 # XXX optimize if the modified attributes belong to the same
       
   118                 # entity as the computed attribute
       
   119                 RecomputeAttributeOperation.get_instance(self._cw).add_data(rdef)
       
   120 
       
   121 
       
   122 # code generation at registration time #########################################
       
   123 
       
   124 def _optimize_on(formula_select, rtype):
       
   125     """Given a formula and some rtype, tells whether on update of the given
       
   126     relation, formula may be recomputed only for rhe relation's subject
       
   127     ('eidfrom' returned), object ('eidto' returned) or None.
       
   128 
       
   129     Optimizing is only possible when X is used as direct subject/object of this
       
   130     relation, else we may miss some necessary update.
       
   131     """
       
   132     for rel in formula_select.get_nodes(nodes.Relation):
       
   133         if rel.r_type == rtype:
       
   134             sub = rel.get_variable_parts()[0]
       
   135             obj = rel.get_variable_parts()[1]
       
   136             if sub.name == 'X':
       
   137                 return 'eidfrom'
       
   138             elif obj.name == 'X':
       
   139                 return 'eidto'
       
   140             else:
       
   141                 return None
       
   142 
       
   143 
       
   144 class _FormulaDependenciesMatrix(object):
       
   145     """This class computes and represents the dependencies of computed attributes
       
   146     towards relations and attributes
       
   147     """
       
   148 
       
   149     def __init__(self, schema):
       
   150         """Analyzes the schema to compute the dependencies"""
       
   151         # entity types holding some computed attribute {etype: [computed rdefs]}
       
   152         self.computed_attribute_by_etype = defaultdict(list)
       
   153         # depending entity types {dep. etype: {computed rdef: dep. etype attributes}}
       
   154         self.computed_attribute_by_etype_attrs = defaultdict(lambda: defaultdict(set))
       
   155         # depending relations def {dep. rdef: [computed rdefs]
       
   156         self.computed_attribute_by_relation = defaultdict(list) # by rdef
       
   157         # Walk through all attributes definitions
       
   158         for rdef in schema.iter_computed_attributes():
       
   159             self.computed_attribute_by_etype[rdef.subject.type].append(rdef)
       
   160             # extract the relations it depends upon - `rdef.formula_select` is
       
   161             # expected to have been set by finalize_computed_attributes
       
   162             select = rdef.formula_select
       
   163             for rel_node in select.get_nodes(nodes.Relation):
       
   164                 rschema = schema.rschema(rel_node.r_type)
       
   165                 lhs, rhs = rel_node.get_variable_parts()
       
   166                 for sol in select.solutions:
       
   167                     subject_etype = sol[lhs.name]
       
   168                     if isinstance(rhs, nodes.VariableRef):
       
   169                         object_etypes = set(sol[rhs.name] for sol in select.solutions)
       
   170                     else:
       
   171                         object_etypes = rschema.objects(subject_etype)
       
   172                     for object_etype in object_etypes:
       
   173                         if rschema.final:
       
   174                             attr_for_computations = self.computed_attribute_by_etype_attrs[subject_etype]
       
   175                             attr_for_computations[rdef].add(rschema.type)
       
   176                         else:
       
   177                             depend_on_rdef = rschema.rdefs[subject_etype, object_etype]
       
   178                             self.computed_attribute_by_relation[depend_on_rdef].append(rdef)
       
   179 
       
   180     def generate_entity_creation_hooks(self):
       
   181         for etype, computed_attributes in self.computed_attribute_by_etype.iteritems():
       
   182             regid = 'computed_attribute.%s_created' % etype
       
   183             selector = hook.is_instance(etype)
       
   184             yield type('%sCreatedHook' % etype,
       
   185                        (EntityWithCACreatedHook,),
       
   186                        {'__regid__': regid,
       
   187                         '__select__':  hook.Hook.__select__ & selector,
       
   188                         'computed_attributes': computed_attributes})
       
   189 
       
   190     def generate_relation_change_hooks(self):
       
   191         for rdef, computed_attributes in self.computed_attribute_by_relation.iteritems():
       
   192             regid = 'computed_attribute.%s_modified' % rdef.rtype
       
   193             selector = hook.match_rtype(rdef.rtype.type,
       
   194                                         frometypes=(rdef.subject.type,),
       
   195                                         toetypes=(rdef.object.type,))
       
   196             optimized_computed_attributes = []
       
   197             for computed_rdef in computed_attributes:
       
   198                 optimized_computed_attributes.append(
       
   199                     (computed_rdef,
       
   200                      _optimize_on(computed_rdef.formula_select, rdef.rtype))
       
   201                      )
       
   202             yield type('%sModifiedHook' % rdef.rtype,
       
   203                        (RelationInvolvedInCAModifiedHook,),
       
   204                        {'__regid__': regid,
       
   205                         '__select__':  hook.Hook.__select__ & selector,
       
   206                         'optimized_computed_attributes': optimized_computed_attributes})
       
   207 
       
   208     def generate_entity_update_hooks(self):
       
   209         for etype, attributes_computed_attributes in self.computed_attribute_by_etype_attrs.iteritems():
       
   210             regid = 'computed_attribute.%s_updated' % etype
       
   211             selector = hook.is_instance(etype)
       
   212             yield type('%sModifiedHook' % etype,
       
   213                        (AttributeInvolvedInCAModifiedHook,),
       
   214                        {'__regid__': regid,
       
   215                         '__select__':  hook.Hook.__select__ & selector,
       
   216                         'attributes_computed_attributes': attributes_computed_attributes})
       
   217 
       
   218 
       
   219 def registration_callback(vreg):
       
   220     vreg.register_all(globals().values(), __name__)
       
   221     dependencies = _FormulaDependenciesMatrix(vreg.schema)
       
   222     for hook_class in dependencies.generate_entity_creation_hooks():
       
   223         vreg.register(hook_class)
       
   224     for hook_class in dependencies.generate_relation_change_hooks():
       
   225         vreg.register(hook_class)
       
   226     for hook_class in dependencies.generate_entity_update_hooks():
       
   227         vreg.register(hook_class)