hooks/synccomputed.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     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 from cubicweb import _
       
    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().items():
       
    44             attr = computed_attribute_rdef.rtype
       
    45             formula  = computed_attribute_rdef.formula
       
    46             select = self.cnx.repo.vreg.rqlhelper.parse(formula).children[0]
       
    47             xvar = select.get_variable('X')
       
    48             select.add_selected(xvar, index=0)
       
    49             select.add_group_var(xvar, index=0)
       
    50             if None in eids:
       
    51                 select.add_type_restriction(xvar, computed_attribute_rdef.subject)
       
    52             else:
       
    53                 select.add_eid_restriction(xvar, eids)
       
    54             update_rql = 'SET X %s %%(value)s WHERE X eid %%(x)s' % attr
       
    55             for eid, value in self.cnx.execute(select.as_string()):
       
    56                 self.cnx.execute(update_rql, {'value': value, 'x': eid})
       
    57 
       
    58 
       
    59 class EntityWithCACreatedHook(hook.Hook):
       
    60     """When creating an entity that has some computed attribute, those
       
    61     attributes have to be computed.
       
    62 
       
    63     Concret class of this hook are generated at registration time by
       
    64     introspecting the schema.
       
    65     """
       
    66     __abstract__ = True
       
    67     events = ('after_add_entity',)
       
    68     # list of computed attribute rdefs that have to be recomputed
       
    69     computed_attributes = None
       
    70 
       
    71     def __call__(self):
       
    72         for rdef in self.computed_attributes:
       
    73             RecomputeAttributeOperation.get_instance(self._cw).add_data(
       
    74                 rdef, self.entity.eid)
       
    75 
       
    76 
       
    77 class RelationInvolvedInCAModifiedHook(hook.Hook):
       
    78     """When some relation used in a computed attribute is updated, those
       
    79     attributes have to be recomputed.
       
    80 
       
    81     Concret class of this hook are generated at registration time by
       
    82     introspecting the schema.
       
    83     """
       
    84     __abstract__ = True
       
    85     events = ('after_add_relation', 'before_delete_relation')
       
    86     # list of (computed attribute rdef, optimize_on) that have to be recomputed
       
    87     optimized_computed_attributes = None
       
    88 
       
    89     def __call__(self):
       
    90         for rdef, optimize_on in self.optimized_computed_attributes:
       
    91             if optimize_on is None:
       
    92                 eid = None
       
    93             else:
       
    94                 eid = getattr(self, optimize_on)
       
    95             RecomputeAttributeOperation.get_instance(self._cw).add_data(rdef, eid)
       
    96 
       
    97 
       
    98 class AttributeInvolvedInCAModifiedHook(hook.Hook):
       
    99     """When some attribute used in a computed attribute is updated, those
       
   100     attributes have to be recomputed.
       
   101 
       
   102     Concret class of this hook are generated at registration time by
       
   103     introspecting the schema.
       
   104     """
       
   105     __abstract__ = True
       
   106     events = ('after_update_entity',)
       
   107     # list of (computed attribute rdef, attributes of this entity type involved)
       
   108     # that may have to be recomputed
       
   109     attributes_computed_attributes = None
       
   110 
       
   111     def __call__(self):
       
   112         edited_attributes = frozenset(self.entity.cw_edited)
       
   113         for rdef, used_attributes in self.attributes_computed_attributes.items():
       
   114             if edited_attributes.intersection(used_attributes):
       
   115                 # XXX optimize if the modified attributes belong to the same
       
   116                 # entity as the computed attribute
       
   117                 RecomputeAttributeOperation.get_instance(self._cw).add_data(rdef)
       
   118 
       
   119 
       
   120 # code generation at registration time #########################################
       
   121 
       
   122 def _optimize_on(formula_select, rtype):
       
   123     """Given a formula and some rtype, tells whether on update of the given
       
   124     relation, formula may be recomputed only for rhe relation's subject
       
   125     ('eidfrom' returned), object ('eidto' returned) or None.
       
   126 
       
   127     Optimizing is only possible when X is used as direct subject/object of this
       
   128     relation, else we may miss some necessary update.
       
   129     """
       
   130     for rel in formula_select.get_nodes(nodes.Relation):
       
   131         if rel.r_type == rtype:
       
   132             sub = rel.get_variable_parts()[0]
       
   133             obj = rel.get_variable_parts()[1]
       
   134             if sub.name == 'X':
       
   135                 return 'eidfrom'
       
   136             elif obj.name == 'X':
       
   137                 return 'eidto'
       
   138             else:
       
   139                 return None
       
   140 
       
   141 
       
   142 class _FormulaDependenciesMatrix(object):
       
   143     """This class computes and represents the dependencies of computed attributes
       
   144     towards relations and attributes
       
   145     """
       
   146 
       
   147     def __init__(self, schema):
       
   148         """Analyzes the schema to compute the dependencies"""
       
   149         # entity types holding some computed attribute {etype: [computed rdefs]}
       
   150         self.computed_attribute_by_etype = defaultdict(list)
       
   151         # depending entity types {dep. etype: {computed rdef: dep. etype attributes}}
       
   152         self.computed_attribute_by_etype_attrs = defaultdict(lambda: defaultdict(set))
       
   153         # depending relations def {dep. rdef: [computed rdefs]
       
   154         self.computed_attribute_by_relation = defaultdict(list) # by rdef
       
   155         # Walk through all attributes definitions
       
   156         for rdef in schema.iter_computed_attributes():
       
   157             self.computed_attribute_by_etype[rdef.subject.type].append(rdef)
       
   158             # extract the relations it depends upon - `rdef.formula_select` is
       
   159             # expected to have been set by finalize_computed_attributes
       
   160             select = rdef.formula_select
       
   161             for rel_node in select.get_nodes(nodes.Relation):
       
   162                 if rel_node.is_types_restriction():
       
   163                     continue
       
   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.items():
       
   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.items():
       
   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.items():
       
   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)