1 # copyright 2003-2012 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 |
16 # You should have received a copy of the GNU Lesser General Public License along |
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/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """Functions to add additional annotations on a rql syntax tree to ease later |
18 """Functions to add additional annotations on a rql syntax tree to ease later |
19 code generation. |
19 code generation. |
20 """ |
20 """ |
|
21 |
21 from __future__ import print_function |
22 from __future__ import print_function |
22 |
23 |
23 __docformat__ = "restructuredtext en" |
|
24 |
|
25 from rql import BadRQLQuery |
24 from rql import BadRQLQuery |
26 from rql.nodes import Relation, VariableRef, Constant, Variable, Or, Exists |
25 from rql.nodes import Relation, VariableRef, Constant, Variable, Or |
27 from rql.utils import common_parent |
26 from rql.utils import common_parent |
|
27 |
28 |
28 |
29 def _annotate_select(annotator, rqlst): |
29 def _annotate_select(annotator, rqlst): |
30 has_text_query = False |
30 has_text_query = False |
31 for subquery in rqlst.with_: |
31 for subquery in rqlst.with_: |
32 if annotator._annotate_union(subquery.query): |
32 if annotator._annotate_union(subquery.query): |
33 has_text_query = True |
33 has_text_query = True |
34 #if server.DEBUG: |
|
35 # print '-------- sql annotate', repr(rqlst) |
|
36 getrschema = annotator.schema.rschema |
34 getrschema = annotator.schema.rschema |
37 for var in rqlst.defined_vars.values(): |
35 for var in rqlst.defined_vars.values(): |
38 stinfo = var.stinfo |
36 stinfo = var.stinfo |
39 if stinfo.get('ftirels'): |
37 if stinfo.get('ftirels'): |
40 has_text_query = True |
38 has_text_query = True |
47 # those particular queries should be executed using the system |
45 # those particular queries should be executed using the system |
48 # entities table unless there is some type restriction |
46 # entities table unless there is some type restriction |
49 stinfo['invariant'] = True |
47 stinfo['invariant'] = True |
50 stinfo['principal'] = None |
48 stinfo['principal'] = None |
51 continue |
49 continue |
52 if any(rel for rel in stinfo['relations'] if rel.r_type == 'eid' and rel.operator() != '=') and \ |
50 if (any(rel for rel in stinfo['relations'] if rel.r_type == 'eid' and rel.operator() != '=') |
53 not any(r for r in var.stinfo['relations'] - var.stinfo['rhsrelations'] |
51 and not any(r for r in var.stinfo['relations'] - var.stinfo['rhsrelations'] |
54 if r.r_type != 'eid' and (getrschema(r.r_type).inlined or getrschema(r.r_type).final)): |
52 if r.r_type != 'eid' |
|
53 and (getrschema(r.r_type).inlined or getrschema(r.r_type).final))): |
55 # Any X WHERE X eid > 2 |
54 # Any X WHERE X eid > 2 |
56 # those particular queries should be executed using the system entities table |
55 # those particular queries should be executed using the system entities table |
57 stinfo['invariant'] = True |
56 stinfo['invariant'] = True |
58 stinfo['principal'] = None |
57 stinfo['principal'] = None |
59 continue |
58 continue |
60 if stinfo['selected'] and var.valuable_references() == 1+bool(stinfo['constnode']): |
59 if stinfo['selected'] and var.valuable_references() == 1 + bool(stinfo['constnode']): |
61 # "Any X", "Any X, Y WHERE X attr Y" |
60 # "Any X", "Any X, Y WHERE X attr Y" |
62 stinfo['invariant'] = False |
61 stinfo['invariant'] = False |
63 continue |
62 continue |
64 joins = set() |
63 joins = set() |
65 invariant = False |
64 invariant = False |
72 role = 'subject' if onlhs else 'object' |
71 role = 'subject' if onlhs else 'object' |
73 if rel.r_type == 'eid': |
72 if rel.r_type == 'eid': |
74 if not (onlhs and len(stinfo['relations']) > 1): |
73 if not (onlhs and len(stinfo['relations']) > 1): |
75 break |
74 break |
76 if not stinfo['constnode']: |
75 if not stinfo['constnode']: |
77 joins.add( (rel, role) ) |
76 joins.add((rel, role)) |
78 continue |
77 continue |
79 elif rel.r_type == 'identity': |
78 elif rel.r_type == 'identity': |
80 # identity can't be used as principal, so check other relation are used |
79 # identity can't be used as principal, so check other relation are used |
81 # XXX explain rhs.operator == '=' |
80 # XXX explain rhs.operator == '=' |
82 if rhs.operator != '=' or len(stinfo['relations']) <= 1: #(stinfo['constnode'] and rhs.operator == '='): |
81 if rhs.operator != '=' or len(stinfo['relations']) <= 1: |
83 break |
82 break |
84 joins.add( (rel, role) ) |
83 joins.add((rel, role)) |
85 continue |
84 continue |
86 rschema = getrschema(rel.r_type) |
85 rschema = getrschema(rel.r_type) |
87 if rel.optional: |
86 if rel.optional: |
88 if rel in stinfo.get('optrelations', ()): |
87 if rel in stinfo.get('optrelations', ()): |
89 # optional variable can't be invariant if this is the lhs |
88 # optional variable can't be invariant if this is the lhs |
90 # variable of an inlined relation |
89 # variable of an inlined relation |
91 if not rel in stinfo['rhsrelations'] and rschema.inlined: |
90 if rel not in stinfo['rhsrelations'] and rschema.inlined: |
92 break |
91 break |
93 # variable used as main variable of an optional relation can't |
92 # variable used as main variable of an optional relation can't |
94 # be invariant, unless we can use some other relation as |
93 # be invariant, unless we can use some other relation as |
95 # reference for the outer join |
94 # reference for the outer join |
96 elif not stinfo['constnode']: |
95 elif not stinfo['constnode']: |
107 if rschema.final or (onlhs and rschema.inlined): |
106 if rschema.final or (onlhs and rschema.inlined): |
108 if rschema.type != 'has_text': |
107 if rschema.type != 'has_text': |
109 # need join anyway if the variable appears in a final or |
108 # need join anyway if the variable appears in a final or |
110 # inlined relation |
109 # inlined relation |
111 break |
110 break |
112 joins.add( (rel, role) ) |
111 joins.add((rel, role)) |
113 continue |
112 continue |
114 if not stinfo['constnode']: |
113 if not stinfo['constnode']: |
115 if rschema.inlined and rel.neged(strict=True): |
114 if rschema.inlined and rel.neged(strict=True): |
116 # if relation is inlined, can't be invariant if that |
115 # if relation is inlined, can't be invariant if that |
117 # variable is used anywhere else. |
116 # variable is used anywhere else. |
118 # see 'Any P WHERE NOT N ecrit_par P, N eid 512': |
117 # see 'Any P WHERE NOT N ecrit_par P, N eid 512': |
119 # sql for 'NOT N ecrit_par P' is 'N.ecrit_par is NULL' so P |
118 # sql for 'NOT N ecrit_par P' is 'N.ecrit_par is NULL' so P |
120 # can use N.ecrit_par as principal |
119 # can use N.ecrit_par as principal |
121 if (stinfo['selected'] or len(stinfo['relations']) > 1): |
120 if (stinfo['selected'] or len(stinfo['relations']) > 1): |
122 break |
121 break |
123 joins.add( (rel, role) ) |
122 joins.add((rel, role)) |
124 else: |
123 else: |
125 # if there is at least one ambigous relation and no other to |
124 # if there is at least one ambigous relation and no other to |
126 # restrict types, can't be invariant since we need to filter out |
125 # restrict types, can't be invariant since we need to filter out |
127 # other types |
126 # other types |
128 if not annotator.is_ambiguous(var): |
127 if not annotator.is_ambiguous(var): |
149 if col_alias.stinfo.get('ftirels'): |
148 if col_alias.stinfo.get('ftirels'): |
150 has_text_query = True |
149 has_text_query = True |
151 return has_text_query |
150 return has_text_query |
152 |
151 |
153 |
152 |
154 |
|
155 class CantSelectPrincipal(Exception): |
153 class CantSelectPrincipal(Exception): |
156 """raised when no 'principal' variable can be found""" |
154 """raised when no 'principal' variable can be found""" |
157 |
155 |
158 def _select_principal(scope, relations, _sort=lambda x:x): |
156 |
|
157 def _select_principal(scope, relations, _sort=lambda x: x): |
159 """given a list of rqlst relations, select one which will be used to |
158 """given a list of rqlst relations, select one which will be used to |
160 represent an invariant variable (e.g. using on extremity of the relation |
159 represent an invariant variable (e.g. using on extremity of the relation |
161 instead of the variable's type table |
160 instead of the variable's type table |
162 """ |
161 """ |
163 # _sort argument is there for test |
162 # _sort argument is there for test |
198 return next(iter(_sort(diffscope_rels))) |
197 return next(iter(_sort(diffscope_rels))) |
199 # XXX could use a relation from a different scope if it can't generate |
198 # XXX could use a relation from a different scope if it can't generate |
200 # duplicates, so we should have to check cardinality |
199 # duplicates, so we should have to check cardinality |
201 raise CantSelectPrincipal() |
200 raise CantSelectPrincipal() |
202 |
201 |
|
202 |
203 def _select_main_var(relations): |
203 def _select_main_var(relations): |
204 """given a list of rqlst relations, select one which will be used as main |
204 """given a list of rqlst relations, select one which will be used as main |
205 relation for the rhs variable |
205 relation for the rhs variable |
206 """ |
206 """ |
207 principal = None |
207 principal = None |
208 others = [] |
208 others = [] |
209 # sort for test predictability |
209 # sort for test predictability |
210 for rel in sorted(relations, key=lambda x: (x.children[0].name, x.r_type)): |
210 for rel in sorted(relations, key=lambda x: (x.children[0].name, x.r_type)): |
211 # only equality relation with a variable as rhs may be principal |
211 # only equality relation with a variable as rhs may be principal |
212 if rel.operator() not in ('=', 'IS') \ |
212 if (rel.operator() not in ('=', 'IS') |
213 or not isinstance(rel.children[1].children[0], VariableRef) or rel.neged(strict=True): |
213 or not isinstance(rel.children[1].children[0], VariableRef) |
|
214 or rel.neged(strict=True)): |
214 continue |
215 continue |
215 if rel.optional: |
216 if rel.optional: |
216 others.append(rel) |
217 others.append(rel) |
217 continue |
218 continue |
218 if rel.scope is rel.stmt: |
219 if rel.scope is rel.stmt: |
257 * it's not used as lhs in any final or inlined relation |
258 * it's not used as lhs in any final or inlined relation |
258 * there is no type restriction on this variable (either explicit in the |
259 * there is no type restriction on this variable (either explicit in the |
259 syntax tree or because a solution for this variable has been removed |
260 syntax tree or because a solution for this variable has been removed |
260 due to security filtering) |
261 due to security filtering) |
261 """ |
262 """ |
262 #assert rqlst.TYPE == 'select', rqlst |
263 # assert rqlst.TYPE == 'select', rqlst |
263 rqlst.has_text_query = self._annotate_union(rqlst) |
264 rqlst.has_text_query = self._annotate_union(rqlst) |
264 |
265 |
265 def _annotate_union(self, union): |
266 def _annotate_union(self, union): |
266 has_text_query = False |
267 has_text_query = False |
267 for select in union.children: |
268 for select in union.children: |
274 # This is expected by the rql2sql generator which will use the `entities` |
275 # This is expected by the rql2sql generator which will use the `entities` |
275 # table to filter out by type if necessary, This optimisation is very |
276 # table to filter out by type if necessary, This optimisation is very |
276 # interesting in multi-sources cases, as it may avoid a costly query |
277 # interesting in multi-sources cases, as it may avoid a costly query |
277 # on sources to get all entities of a given type to achieve this, while |
278 # on sources to get all entities of a given type to achieve this, while |
278 # we have all the necessary information. |
279 # we have all the necessary information. |
279 root = var.stmt.root # Union node |
280 root = var.stmt.root # Union node |
280 # rel.scope -> Select or Exists node, so add .parent to get Union from |
281 # rel.scope -> Select or Exists node, so add .parent to get Union from |
281 # Select node |
282 # Select node |
282 rels = [rel for rel in var.stinfo['relations'] if rel.scope.parent is root] |
283 rels = [rel for rel in var.stinfo['relations'] if rel.scope.parent is root] |
283 if len(rels) == 1 and rels[0].r_type == 'has_text': |
284 if len(rels) == 1 and rels[0].r_type == 'has_text': |
284 return False |
285 return False |
317 self.ambiguousvars.remove(var) |
318 self.ambiguousvars.remove(var) |
318 |
319 |
319 def compute(self, rqlst): |
320 def compute(self, rqlst): |
320 # set domains for each variable |
321 # set domains for each variable |
321 for varname, var in rqlst.defined_vars.items(): |
322 for varname, var in rqlst.defined_vars.items(): |
322 if var.stinfo['uidrel'] is not None or \ |
323 if (var.stinfo['uidrel'] is not None |
323 self.eschema(rqlst.solutions[0][varname]).final: |
324 or self.eschema(rqlst.solutions[0][varname]).final): |
324 ptypes = var.stinfo['possibletypes'] |
325 ptypes = var.stinfo['possibletypes'] |
325 else: |
326 else: |
326 ptypes = set(self.nfdomain) |
327 ptypes = set(self.nfdomain) |
327 self.ambiguousvars.add(var) |
328 self.ambiguousvars.add(var) |
328 self.varsols[var] = ptypes |
329 self.varsols[var] = ptypes |
354 # no relation to deambiguify |
355 # no relation to deambiguify |
355 continue |
356 continue |
356 |
357 |
357 def _debug_print(self): |
358 def _debug_print(self): |
358 print('varsols', dict((x, sorted(str(v) for v in values)) |
359 print('varsols', dict((x, sorted(str(v) for v in values)) |
359 for x, values in self.varsols.items())) |
360 for x, values in self.varsols.items())) |
360 print('ambiguous vars', sorted(self.ambiguousvars)) |
361 print('ambiguous vars', sorted(self.ambiguousvars)) |
361 |
362 |
362 def set_rel_constraint(self, term, rel, etypes_func): |
363 def set_rel_constraint(self, term, rel, etypes_func): |
363 if isinstance(term, VariableRef) and self.is_ambiguous(term.variable): |
364 if isinstance(term, VariableRef) and self.is_ambiguous(term.variable): |
364 var = term.variable |
365 var = term.variable |
365 if len(var.stinfo['relations']) == 1 \ |
366 if (len(var.stinfo['relations']) == 1 |
366 or rel.scope is var.scope or rel.r_type == 'identity': |
367 or rel.scope is var.scope |
|
368 or rel.r_type == 'identity'): |
367 self.restrict(var, frozenset(etypes_func())) |
369 self.restrict(var, frozenset(etypes_func())) |
368 try: |
370 try: |
369 self.maydeambrels[var].add(rel) |
371 self.maydeambrels[var].add(rel) |
370 except KeyError: |
372 except KeyError: |
371 self.maydeambrels[var] = set((rel,)) |
373 self.maydeambrels[var] = set((rel,)) |
376 other = onlhs and rhs or lhs |
378 other = onlhs and rhs or lhs |
377 otheretypes = None |
379 otheretypes = None |
378 # XXX isinstance(other.variable, Variable) to skip column alias |
380 # XXX isinstance(other.variable, Variable) to skip column alias |
379 if isinstance(other, VariableRef) and isinstance(other.variable, Variable): |
381 if isinstance(other, VariableRef) and isinstance(other.variable, Variable): |
380 deambiguifier = other.variable |
382 deambiguifier = other.variable |
381 if not var is self.deambification_map.get(deambiguifier): |
383 if var is not self.deambification_map.get(deambiguifier): |
382 if var.stinfo['typerel'] is None: |
384 if var.stinfo['typerel'] is None: |
383 otheretypes = deambiguifier.stinfo['possibletypes'] |
385 otheretypes = deambiguifier.stinfo['possibletypes'] |
384 elif not self.is_ambiguous(deambiguifier): |
386 elif not self.is_ambiguous(deambiguifier): |
385 otheretypes = self.varsols[deambiguifier] |
387 otheretypes = self.varsols[deambiguifier] |
386 elif deambiguifier in self.not_invariants: |
388 elif deambiguifier in self.not_invariants: |