|
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) |