22 from __future__ import print_function |
22 from __future__ import print_function |
23 |
23 |
24 from rql import BadRQLQuery |
24 from rql import BadRQLQuery |
25 from rql.nodes import Relation, VariableRef, Constant, Variable, Or |
25 from rql.nodes import Relation, VariableRef, Constant, Variable, Or |
26 from rql.utils import common_parent |
26 from rql.utils import common_parent |
27 |
|
28 |
|
29 def _annotate_select(annotator, rqlst): |
|
30 has_text_query = False |
|
31 for subquery in rqlst.with_: |
|
32 if annotator._annotate_union(subquery.query): |
|
33 has_text_query = True |
|
34 getrschema = annotator.schema.rschema |
|
35 for var in rqlst.defined_vars.values(): |
|
36 stinfo = var.stinfo |
|
37 if stinfo.get('ftirels'): |
|
38 has_text_query = True |
|
39 if stinfo['attrvar']: |
|
40 stinfo['invariant'] = False |
|
41 stinfo['principal'] = _select_main_var(stinfo['rhsrelations']) |
|
42 continue |
|
43 if stinfo['typerel'] is None: |
|
44 # those particular queries should be executed using the system |
|
45 # entities table unless there is some type restriction |
|
46 if not stinfo['relations']: |
|
47 # Any X, Any MAX(X)... |
|
48 stinfo['invariant'] = True |
|
49 stinfo['principal'] = None |
|
50 continue |
|
51 if (any(rel for rel in stinfo['relations'] |
|
52 if rel.r_type == 'eid' and rel.operator() != '=') |
|
53 and not any(r for r in var.stinfo['relations'] - var.stinfo['rhsrelations'] |
|
54 if r.r_type != 'eid' |
|
55 and (getrschema(r.r_type).inlined or getrschema(r.r_type).final))): |
|
56 # Any X WHERE X eid > 2 |
|
57 stinfo['invariant'] = True |
|
58 stinfo['principal'] = None |
|
59 continue |
|
60 if stinfo['selected'] and var.valuable_references() == 1 + bool(stinfo['constnode']): |
|
61 # "Any X", "Any X, Y WHERE X attr Y" |
|
62 stinfo['invariant'] = False |
|
63 continue |
|
64 joins = set() |
|
65 invariant = False |
|
66 for ref in var.references(): |
|
67 rel = ref.relation() |
|
68 if rel is None or rel.is_types_restriction(): |
|
69 continue |
|
70 lhs, rhs = rel.get_parts() |
|
71 onlhs = ref is lhs |
|
72 role = 'subject' if onlhs else 'object' |
|
73 if rel.r_type == 'eid': |
|
74 if not (onlhs and len(stinfo['relations']) > 1): |
|
75 break |
|
76 if not stinfo['constnode']: |
|
77 joins.add((rel, role)) |
|
78 continue |
|
79 elif rel.r_type == 'identity': |
|
80 # identity can't be used as principal, so check other relation are used |
|
81 # XXX explain rhs.operator == '=' |
|
82 if rhs.operator != '=' or len(stinfo['relations']) <= 1: |
|
83 break |
|
84 joins.add((rel, role)) |
|
85 continue |
|
86 rschema = getrschema(rel.r_type) |
|
87 if rel.optional: |
|
88 if rel in stinfo.get('optrelations', ()): |
|
89 # optional variable can't be invariant if this is the lhs |
|
90 # variable of an inlined relation |
|
91 if rel not in stinfo['rhsrelations'] and rschema.inlined: |
|
92 break |
|
93 # variable used as main variable of an optional relation can't |
|
94 # be invariant, unless we can use some other relation as |
|
95 # reference for the outer join |
|
96 elif not stinfo['constnode']: |
|
97 break |
|
98 elif len(stinfo['relations']) == 2: |
|
99 if onlhs: |
|
100 ostinfo = rhs.children[0].variable.stinfo |
|
101 else: |
|
102 ostinfo = lhs.variable.stinfo |
|
103 if not (ostinfo.get('optcomparisons') |
|
104 or any(orel for orel in ostinfo['relations'] |
|
105 if orel.optional and orel is not rel)): |
|
106 break |
|
107 if rschema.final or (onlhs and rschema.inlined): |
|
108 if rschema.type != 'has_text': |
|
109 # need join anyway if the variable appears in a final or |
|
110 # inlined relation |
|
111 break |
|
112 joins.add((rel, role)) |
|
113 continue |
|
114 if not stinfo['constnode']: |
|
115 if rschema.inlined and rel.neged(strict=True): |
|
116 # if relation is inlined, can't be invariant if that |
|
117 # variable is used anywhere else. |
|
118 # 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 |
|
120 # can use N.ecrit_par as principal |
|
121 if (stinfo['selected'] or len(stinfo['relations']) > 1): |
|
122 break |
|
123 joins.add((rel, role)) |
|
124 else: |
|
125 # 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 |
|
127 # other types |
|
128 if not annotator.is_ambiguous(var): |
|
129 invariant = True |
|
130 stinfo['invariant'] = invariant |
|
131 if invariant and joins: |
|
132 # remember rqlst/solutions analyze information |
|
133 # we have to select a kindof "main" relation which will "extrajoins" |
|
134 # the other |
|
135 # priority should be given to relation which are not in inner queries |
|
136 # (eg exists) |
|
137 try: |
|
138 stinfo['principal'] = principal = _select_principal(var.scope, joins) |
|
139 if getrschema(principal.r_type).inlined: |
|
140 # the scope of the lhs variable must be equal or outer to the |
|
141 # rhs variable's scope (since it's retrieved from lhs's table) |
|
142 sstinfo = principal.children[0].variable.stinfo |
|
143 sstinfo['scope'] = common_parent(sstinfo['scope'], stinfo['scope']).scope |
|
144 except CantSelectPrincipal: |
|
145 stinfo['invariant'] = False |
|
146 # see unittest_rqlannotation. test_has_text_security_cache_bug |
|
147 # XXX probably more to do, but yet that work without more... |
|
148 for col_alias in rqlst.aliases.values(): |
|
149 if col_alias.stinfo.get('ftirels'): |
|
150 has_text_query = True |
|
151 return has_text_query |
|
152 |
27 |
153 |
28 |
154 class CantSelectPrincipal(Exception): |
29 class CantSelectPrincipal(Exception): |
155 """raised when no 'principal' variable can be found""" |
30 """raised when no 'principal' variable can be found""" |
156 |
31 |
243 else: |
118 else: |
244 var._q_invariant = False |
119 var._q_invariant = False |
245 |
120 |
246 |
121 |
247 class SQLGenAnnotator(object): |
122 class SQLGenAnnotator(object): |
|
123 |
248 def __init__(self, schema): |
124 def __init__(self, schema): |
249 self.schema = schema |
125 self.schema = schema |
250 self.nfdomain = frozenset(eschema.type for eschema in schema.entities() |
126 self.nfdomain = frozenset(eschema.type for eschema in schema.entities() |
251 if not eschema.final) |
127 if not eschema.final) |
252 |
128 |
253 def annotate(self, rqlst): |
129 def annotate(self, rqlst): |
254 """add information to the rql syntax tree to help sources to do their |
130 """add information to the rql syntax tree to help sources to do their |
255 job (read sql generation) |
131 job (read sql generation) |
256 |
132 |
257 a variable is tagged as invariant if: |
133 a variable is tagged as invariant if: |
258 * it's a non final variable |
134 * it is a non final variable |
259 * it's not used as lhs in any final or inlined relation |
135 * it is not used as lhs in any final or inlined relation |
260 * there is no type restriction on this variable (either explicit in the |
136 * there is no type restriction on this variable (either explicit in the |
261 syntax tree or because a solution for this variable has been removed |
137 syntax tree or because a solution for this variable has been removed |
262 due to security filtering) |
138 due to security filtering) |
263 """ |
139 """ |
264 # assert rqlst.TYPE == 'select', rqlst |
140 # assert rqlst.TYPE == 'select', rqlst |
265 rqlst.has_text_query = self._annotate_union(rqlst) |
141 rqlst.has_text_query = self._annotate_union(rqlst) |
266 |
142 |
267 def _annotate_union(self, union): |
143 def _annotate_union(self, union): |
268 has_text_query = False |
144 has_text_query = False |
269 for select in union.children: |
145 for select in union.children: |
270 if _annotate_select(self, select): |
146 if self._annotate_select(select): |
|
147 has_text_query = True |
|
148 return has_text_query |
|
149 |
|
150 def _annotate_select(self, rqlst): |
|
151 has_text_query = False |
|
152 for subquery in rqlst.with_: |
|
153 if self._annotate_union(subquery.query): |
|
154 has_text_query = True |
|
155 getrschema = self.schema.rschema |
|
156 for var in rqlst.defined_vars.values(): |
|
157 stinfo = var.stinfo |
|
158 if stinfo.get('ftirels'): |
|
159 has_text_query = True |
|
160 if stinfo['attrvar']: |
|
161 stinfo['invariant'] = False |
|
162 stinfo['principal'] = _select_main_var(stinfo['rhsrelations']) |
|
163 continue |
|
164 if stinfo['typerel'] is None: |
|
165 # those particular queries should be executed using the system |
|
166 # entities table unless there is some type restriction |
|
167 if not stinfo['relations']: |
|
168 # Any X, Any MAX(X)... |
|
169 stinfo['invariant'] = True |
|
170 stinfo['principal'] = None |
|
171 continue |
|
172 if (any(rel for rel in stinfo['relations'] |
|
173 if rel.r_type == 'eid' and rel.operator() != '=') |
|
174 and not any(r for r in var.stinfo['relations'] - var.stinfo['rhsrelations'] |
|
175 if r.r_type != 'eid' |
|
176 and (getrschema(r.r_type).inlined |
|
177 or getrschema(r.r_type).final))): |
|
178 # Any X WHERE X eid > 2 |
|
179 stinfo['invariant'] = True |
|
180 stinfo['principal'] = None |
|
181 continue |
|
182 if stinfo['selected'] and var.valuable_references() == 1 + bool(stinfo['constnode']): |
|
183 # "Any X", "Any X, Y WHERE X attr Y" |
|
184 stinfo['invariant'] = False |
|
185 continue |
|
186 joins = set() |
|
187 invariant = False |
|
188 for ref in var.references(): |
|
189 rel = ref.relation() |
|
190 if rel is None or rel.is_types_restriction(): |
|
191 continue |
|
192 lhs, rhs = rel.get_parts() |
|
193 onlhs = ref is lhs |
|
194 role = 'subject' if onlhs else 'object' |
|
195 if rel.r_type == 'eid': |
|
196 if not (onlhs and len(stinfo['relations']) > 1): |
|
197 break |
|
198 if not stinfo['constnode']: |
|
199 joins.add((rel, role)) |
|
200 continue |
|
201 elif rel.r_type == 'identity': |
|
202 # identity can't be used as principal, so check other relation are used |
|
203 # XXX explain rhs.operator == '=' |
|
204 if rhs.operator != '=' or len(stinfo['relations']) <= 1: |
|
205 break |
|
206 joins.add((rel, role)) |
|
207 continue |
|
208 rschema = getrschema(rel.r_type) |
|
209 if rel.optional: |
|
210 if rel in stinfo.get('optrelations', ()): |
|
211 # optional variable can't be invariant if this is the lhs |
|
212 # variable of an inlined relation |
|
213 if rel not in stinfo['rhsrelations'] and rschema.inlined: |
|
214 break |
|
215 # variable used as main variable of an optional relation can't |
|
216 # be invariant, unless we can use some other relation as |
|
217 # reference for the outer join |
|
218 elif not stinfo['constnode']: |
|
219 break |
|
220 elif len(stinfo['relations']) == 2: |
|
221 if onlhs: |
|
222 ostinfo = rhs.children[0].variable.stinfo |
|
223 else: |
|
224 ostinfo = lhs.variable.stinfo |
|
225 if not (ostinfo.get('optcomparisons') |
|
226 or any(orel for orel in ostinfo['relations'] |
|
227 if orel.optional and orel is not rel)): |
|
228 break |
|
229 if rschema.final or (onlhs and rschema.inlined): |
|
230 if rschema.type != 'has_text': |
|
231 # need join anyway if the variable appears in a final or |
|
232 # inlined relation |
|
233 break |
|
234 joins.add((rel, role)) |
|
235 continue |
|
236 if not stinfo['constnode']: |
|
237 if rschema.inlined and rel.neged(strict=True): |
|
238 # if relation is inlined, can't be invariant if that |
|
239 # variable is used anywhere else. |
|
240 # see 'Any P WHERE NOT N ecrit_par P, N eid 512': |
|
241 # sql for 'NOT N ecrit_par P' is 'N.ecrit_par is NULL' so P |
|
242 # can use N.ecrit_par as principal |
|
243 if (stinfo['selected'] or len(stinfo['relations']) > 1): |
|
244 break |
|
245 joins.add((rel, role)) |
|
246 else: |
|
247 # if there is at least one ambigous relation and no other to |
|
248 # restrict types, can't be invariant since we need to filter out |
|
249 # other types |
|
250 if not self.is_ambiguous(var): |
|
251 invariant = True |
|
252 stinfo['invariant'] = invariant |
|
253 if invariant and joins: |
|
254 # remember rqlst/solutions analyze information |
|
255 # we have to select a kindof "main" relation which will "extrajoins" |
|
256 # the other |
|
257 # priority should be given to relation which are not in inner queries |
|
258 # (eg exists) |
|
259 try: |
|
260 stinfo['principal'] = principal = _select_principal(var.scope, joins) |
|
261 if getrschema(principal.r_type).inlined: |
|
262 # the scope of the lhs variable must be equal or outer to the |
|
263 # rhs variable's scope (since it's retrieved from lhs's table) |
|
264 sstinfo = principal.children[0].variable.stinfo |
|
265 sstinfo['scope'] = common_parent(sstinfo['scope'], stinfo['scope']).scope |
|
266 except CantSelectPrincipal: |
|
267 stinfo['invariant'] = False |
|
268 # see unittest_rqlannotation. test_has_text_security_cache_bug |
|
269 # XXX probably more to do, but yet that work without more... |
|
270 for col_alias in rqlst.aliases.values(): |
|
271 if col_alias.stinfo.get('ftirels'): |
271 has_text_query = True |
272 has_text_query = True |
272 return has_text_query |
273 return has_text_query |
273 |
274 |
274 def is_ambiguous(self, var): |
275 def is_ambiguous(self, var): |
275 # ignore has_text relation when we know it will be used as principal. |
276 # ignore has_text relation when we know it will be used as principal. |