1 # copyright 2003-2011 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 """The `ResultSet` class which is returned as result of an rql query""" |
|
19 __docformat__ = "restructuredtext en" |
|
20 |
|
21 from warnings import warn |
|
22 |
|
23 from six import PY3 |
|
24 from six.moves import range |
|
25 |
|
26 from logilab.common import nullobject |
|
27 from logilab.common.decorators import cached, clear_cache, copy_cache |
|
28 from rql import nodes, stmts |
|
29 |
|
30 from cubicweb import NotAnEntity, NoResultError, MultipleResultsError |
|
31 |
|
32 |
|
33 _MARKER = nullobject() |
|
34 |
|
35 |
|
36 class ResultSet(object): |
|
37 """A result set wraps a RQL query result. This object implements |
|
38 partially the list protocol to allow direct use as a list of |
|
39 result rows. |
|
40 |
|
41 :type rowcount: int |
|
42 :param rowcount: number of rows in the result |
|
43 |
|
44 :type rows: list |
|
45 :param rows: list of rows of result |
|
46 |
|
47 :type description: list |
|
48 :param description: |
|
49 result's description, using the same structure as the result itself |
|
50 |
|
51 :type rql: str or unicode |
|
52 :param rql: the original RQL query string |
|
53 """ |
|
54 |
|
55 def __init__(self, results, rql, args=None, description=None, rqlst=None): |
|
56 if rqlst is not None: |
|
57 warn('[3.20] rqlst parameter is deprecated', |
|
58 DeprecationWarning, stacklevel=2) |
|
59 self.rows = results |
|
60 self.rowcount = results and len(results) or 0 |
|
61 # original query and arguments |
|
62 self.rql = rql |
|
63 self.args = args |
|
64 # entity types for each cell (same shape as rows) |
|
65 # maybe discarded if specified when the query has been executed |
|
66 if description is None: |
|
67 self.description = [] |
|
68 else: |
|
69 self.description = description |
|
70 # set to (limit, offset) when a result set is limited using the |
|
71 # .limit method |
|
72 self.limited = None |
|
73 # set by the cursor which returned this resultset |
|
74 self.req = None |
|
75 # actions cache |
|
76 self._rsetactions = None |
|
77 |
|
78 def __str__(self): |
|
79 if not self.rows: |
|
80 return '<empty resultset %s>' % self.rql |
|
81 return '<resultset %s (%s rows)>' % (self.rql, len(self.rows)) |
|
82 |
|
83 def __repr__(self): |
|
84 if not self.rows: |
|
85 return '<empty resultset for %r>' % self.rql |
|
86 rows = self.rows |
|
87 if len(rows) > 10: |
|
88 rows = rows[:10] + ['...'] |
|
89 if len(rows) > 1: |
|
90 # add a line break before first entity if more that one. |
|
91 pattern = '<resultset %r (%s rows):\n%s>' |
|
92 else: |
|
93 pattern = '<resultset %r (%s rows): %s>' |
|
94 |
|
95 if not self.description: |
|
96 return pattern % (self.rql, len(self.rows), |
|
97 '\n'.join(str(r) for r in rows)) |
|
98 return pattern % (self.rql, len(self.rows), |
|
99 '\n'.join('%s (%s)' % (r, d) |
|
100 for r, d in zip(rows, self.description))) |
|
101 |
|
102 def possible_actions(self, **kwargs): |
|
103 if self._rsetactions is None: |
|
104 self._rsetactions = {} |
|
105 if kwargs: |
|
106 key = tuple(sorted(kwargs.items())) |
|
107 else: |
|
108 key = None |
|
109 try: |
|
110 return self._rsetactions[key] |
|
111 except KeyError: |
|
112 actions = self.req.vreg['actions'].poss_visible_objects( |
|
113 self.req, rset=self, **kwargs) |
|
114 self._rsetactions[key] = actions |
|
115 return actions |
|
116 |
|
117 def __len__(self): |
|
118 """returns the result set's size""" |
|
119 return self.rowcount |
|
120 |
|
121 def __getitem__(self, i): |
|
122 """returns the ith element of the result set""" |
|
123 return self.rows[i] #ResultSetRow(self.rows[i]) |
|
124 |
|
125 def __iter__(self): |
|
126 """Returns an iterator over rows""" |
|
127 return iter(self.rows) |
|
128 |
|
129 def __add__(self, rset): |
|
130 # XXX buggy implementation (.rql and .args attributes at least much |
|
131 # probably differ) |
|
132 # at least rql could be fixed now that we have union and sub-queries |
|
133 # but I tend to think that since we have that, we should not need this |
|
134 # method anymore (syt) |
|
135 rset = ResultSet(self.rows+rset.rows, self.rql, self.args, |
|
136 self.description + rset.description) |
|
137 rset.req = self.req |
|
138 return rset |
|
139 |
|
140 def copy(self, rows=None, descr=None): |
|
141 if rows is None: |
|
142 rows = self.rows[:] |
|
143 descr = self.description[:] |
|
144 rset = ResultSet(rows, self.rql, self.args, descr) |
|
145 rset.req = self.req |
|
146 return rset |
|
147 |
|
148 def transformed_rset(self, transformcb): |
|
149 """ the result set according to a given column types |
|
150 |
|
151 :type transormcb: callable(row, desc) |
|
152 :param transformcb: |
|
153 a callable which should take a row and its type description as |
|
154 parameters, and return the transformed row and type description. |
|
155 |
|
156 |
|
157 :type col: int |
|
158 :param col: the column index |
|
159 |
|
160 :rtype: `ResultSet` |
|
161 """ |
|
162 rows, descr = [], [] |
|
163 rset = self.copy(rows, descr) |
|
164 for row, desc in zip(self.rows, self.description): |
|
165 nrow, ndesc = transformcb(row, desc) |
|
166 if ndesc: # transformcb returns None for ndesc to skip that row |
|
167 rows.append(nrow) |
|
168 descr.append(ndesc) |
|
169 rset.rowcount = len(rows) |
|
170 return rset |
|
171 |
|
172 def filtered_rset(self, filtercb, col=0): |
|
173 """filter the result set according to a given filtercb |
|
174 |
|
175 :type filtercb: callable(entity) |
|
176 :param filtercb: |
|
177 a callable which should take an entity as argument and return |
|
178 False if it should be skipped, else True |
|
179 |
|
180 :type col: int |
|
181 :param col: the column index |
|
182 |
|
183 :rtype: `ResultSet` |
|
184 """ |
|
185 rows, descr = [], [] |
|
186 rset = self.copy(rows, descr) |
|
187 for i in range(len(self)): |
|
188 if not filtercb(self.get_entity(i, col)): |
|
189 continue |
|
190 rows.append(self.rows[i]) |
|
191 descr.append(self.description[i]) |
|
192 rset.rowcount = len(rows) |
|
193 return rset |
|
194 |
|
195 |
|
196 def sorted_rset(self, keyfunc, reverse=False, col=0): |
|
197 """sorts the result set according to a given keyfunc |
|
198 |
|
199 :type keyfunc: callable(entity) |
|
200 :param keyfunc: |
|
201 a callable which should take an entity as argument and return |
|
202 the value used to compare and sort |
|
203 |
|
204 :type reverse: bool |
|
205 :param reverse: if the result should be reversed |
|
206 |
|
207 :type col: int |
|
208 :param col: the column index. if col = -1, the whole row are used |
|
209 |
|
210 :rtype: `ResultSet` |
|
211 """ |
|
212 rows, descr = [], [] |
|
213 rset = self.copy(rows, descr) |
|
214 if col >= 0: |
|
215 entities = sorted(enumerate(self.entities(col)), |
|
216 key=lambda t: keyfunc(t[1]), reverse=reverse) |
|
217 else: |
|
218 entities = sorted(enumerate(self), |
|
219 key=lambda t: keyfunc(t[1]), reverse=reverse) |
|
220 for index, _ in entities: |
|
221 rows.append(self.rows[index]) |
|
222 descr.append(self.description[index]) |
|
223 rset.rowcount = len(rows) |
|
224 return rset |
|
225 |
|
226 def split_rset(self, keyfunc=None, col=0, return_dict=False): |
|
227 """splits the result set in multiple result sets according to |
|
228 a given key |
|
229 |
|
230 :type keyfunc: callable(entity or FinalType) |
|
231 :param keyfunc: |
|
232 a callable which should take a value of the rset in argument and |
|
233 return the value used to group the value. If not define, raw value |
|
234 of the specified columns is used. |
|
235 |
|
236 :type col: int |
|
237 :param col: the column index. if col = -1, the whole row are used |
|
238 |
|
239 :type return_dict: Boolean |
|
240 :param return_dict: If true, the function return a mapping |
|
241 (key -> rset) instead of a list of rset |
|
242 |
|
243 :rtype: List of `ResultSet` or mapping of `ResultSet` |
|
244 |
|
245 """ |
|
246 result = [] |
|
247 mapping = {} |
|
248 for idx, line in enumerate(self): |
|
249 if col >= 0: |
|
250 try: |
|
251 key = self.get_entity(idx, col) |
|
252 except NotAnEntity: |
|
253 key = line[col] |
|
254 else: |
|
255 key = line |
|
256 if keyfunc is not None: |
|
257 key = keyfunc(key) |
|
258 |
|
259 if key not in mapping: |
|
260 rows, descr = [], [] |
|
261 rset = self.copy(rows, descr) |
|
262 mapping[key] = rset |
|
263 result.append(rset) |
|
264 else: |
|
265 rset = mapping[key] |
|
266 rset.rows.append(self.rows[idx]) |
|
267 rset.description.append(self.description[idx]) |
|
268 for rset in result: |
|
269 rset.rowcount = len(rset.rows) |
|
270 if return_dict: |
|
271 return mapping |
|
272 else: |
|
273 return result |
|
274 |
|
275 def limited_rql(self): |
|
276 """returns a printable rql for the result set associated to the object, |
|
277 with limit/offset correctly set according to maximum page size and |
|
278 currently displayed page when necessary |
|
279 """ |
|
280 # try to get page boundaries from the navigation component |
|
281 # XXX we should probably not have a ref to this component here (eg in |
|
282 # cubicweb) |
|
283 nav = self.req.vreg['components'].select_or_none('navigation', self.req, |
|
284 rset=self) |
|
285 if nav: |
|
286 start, stop = nav.page_boundaries() |
|
287 rql = self._limit_offset_rql(stop - start, start) |
|
288 # result set may have be limited manually in which case navigation won't |
|
289 # apply |
|
290 elif self.limited: |
|
291 rql = self._limit_offset_rql(*self.limited) |
|
292 # navigation component doesn't apply and rset has not been limited, no |
|
293 # need to limit query |
|
294 else: |
|
295 rql = self.printable_rql() |
|
296 return rql |
|
297 |
|
298 def _limit_offset_rql(self, limit, offset): |
|
299 rqlst = self.syntax_tree() |
|
300 if len(rqlst.children) == 1: |
|
301 select = rqlst.children[0] |
|
302 olimit, ooffset = select.limit, select.offset |
|
303 select.limit, select.offset = limit, offset |
|
304 rql = rqlst.as_string(kwargs=self.args) |
|
305 # restore original limit/offset |
|
306 select.limit, select.offset = olimit, ooffset |
|
307 else: |
|
308 newselect = stmts.Select() |
|
309 newselect.limit = limit |
|
310 newselect.offset = offset |
|
311 aliases = [nodes.VariableRef(newselect.get_variable(chr(65+i), i)) |
|
312 for i in range(len(rqlst.children[0].selection))] |
|
313 for vref in aliases: |
|
314 newselect.append_selected(nodes.VariableRef(vref.variable)) |
|
315 newselect.set_with([nodes.SubQuery(aliases, rqlst)], check=False) |
|
316 newunion = stmts.Union() |
|
317 newunion.append(newselect) |
|
318 rql = newunion.as_string(kwargs=self.args) |
|
319 rqlst.parent = None |
|
320 return rql |
|
321 |
|
322 def limit(self, limit, offset=0, inplace=False): |
|
323 """limit the result set to the given number of rows optionally starting |
|
324 from an index different than 0 |
|
325 |
|
326 :type limit: int |
|
327 :param limit: the maximum number of results |
|
328 |
|
329 :type offset: int |
|
330 :param offset: the offset index |
|
331 |
|
332 :type inplace: bool |
|
333 :param inplace: |
|
334 if true, the result set is modified in place, else a new result set |
|
335 is returned and the original is left unmodified |
|
336 |
|
337 :rtype: `ResultSet` |
|
338 """ |
|
339 stop = limit+offset |
|
340 rows = self.rows[offset:stop] |
|
341 descr = self.description[offset:stop] |
|
342 if inplace: |
|
343 rset = self |
|
344 rset.rows, rset.description = rows, descr |
|
345 rset.rowcount = len(rows) |
|
346 clear_cache(rset, 'description_struct') |
|
347 if offset: |
|
348 clear_cache(rset, 'get_entity') |
|
349 # we also have to fix/remove from the request entity cache entities |
|
350 # which get a wrong rset reference by this limit call |
|
351 for entity in self.req.cached_entities(): |
|
352 if entity.cw_rset is self: |
|
353 if offset <= entity.cw_row < stop: |
|
354 entity.cw_row = entity.cw_row - offset |
|
355 else: |
|
356 entity.cw_rset = entity.as_rset() |
|
357 entity.cw_row = entity.cw_col = 0 |
|
358 else: |
|
359 rset = self.copy(rows, descr) |
|
360 if not offset: |
|
361 # can copy built entity caches |
|
362 copy_cache(rset, 'get_entity', self) |
|
363 rset.limited = (limit, offset) |
|
364 return rset |
|
365 |
|
366 def printable_rql(self, encoded=_MARKER): |
|
367 """return the result set's origin rql as a string, with arguments |
|
368 substitued |
|
369 """ |
|
370 if encoded is not _MARKER: |
|
371 warn('[3.21] the "encoded" argument is deprecated', DeprecationWarning) |
|
372 encoding = self.req.encoding |
|
373 rqlstr = self.syntax_tree().as_string(kwargs=self.args) |
|
374 if PY3: |
|
375 return rqlstr |
|
376 # sounds like we get encoded or unicode string due to a bug in as_string |
|
377 if not encoded: |
|
378 if isinstance(rqlstr, unicode): |
|
379 return rqlstr |
|
380 return unicode(rqlstr, encoding) |
|
381 else: |
|
382 if isinstance(rqlstr, unicode): |
|
383 return rqlstr.encode(encoding) |
|
384 return rqlstr |
|
385 |
|
386 # client helper methods ################################################### |
|
387 |
|
388 def entities(self, col=0): |
|
389 """iter on entities with eid in the `col` column of the result set""" |
|
390 for i in range(len(self)): |
|
391 # may have None values in case of outer join (or aggregat on eid |
|
392 # hacks) |
|
393 if self.rows[i][col] is not None: |
|
394 yield self.get_entity(i, col) |
|
395 |
|
396 def iter_rows_with_entities(self): |
|
397 """ iterates over rows, and for each row |
|
398 eids are converted to plain entities |
|
399 """ |
|
400 for i, row in enumerate(self): |
|
401 _row = [] |
|
402 for j, col in enumerate(row): |
|
403 try: |
|
404 _row.append(self.get_entity(i, j) if col is not None else col) |
|
405 except NotAnEntity: |
|
406 _row.append(col) |
|
407 yield _row |
|
408 |
|
409 def complete_entity(self, row, col=0, skip_bytes=True): |
|
410 """short cut to get an completed entity instance for a particular |
|
411 row (all instance's attributes have been fetched) |
|
412 """ |
|
413 entity = self.get_entity(row, col) |
|
414 entity.complete(skip_bytes=skip_bytes) |
|
415 return entity |
|
416 |
|
417 @cached |
|
418 def get_entity(self, row, col): |
|
419 """convenience method for query retrieving a single entity, returns a |
|
420 partially initialized Entity instance. |
|
421 |
|
422 .. warning:: |
|
423 |
|
424 Due to the cache wrapping this function, you should NEVER give row as |
|
425 a named parameter (i.e. `rset.get_entity(0, 1)` is OK but |
|
426 `rset.get_entity(row=0, col=1)` isn't) |
|
427 |
|
428 :type row,col: int, int |
|
429 :param row,col: |
|
430 row and col numbers localizing the entity among the result's table |
|
431 |
|
432 :return: the partially initialized `Entity` instance |
|
433 """ |
|
434 etype = self.description[row][col] |
|
435 try: |
|
436 eschema = self.req.vreg.schema.eschema(etype) |
|
437 if eschema.final: |
|
438 raise NotAnEntity(etype) |
|
439 except KeyError: |
|
440 raise NotAnEntity(etype) |
|
441 return self._build_entity(row, col) |
|
442 |
|
443 def one(self, col=0): |
|
444 """Retrieve exactly one entity from the query. |
|
445 |
|
446 If the result set is empty, raises :exc:`NoResultError`. |
|
447 If the result set has more than one row, raises |
|
448 :exc:`MultipleResultsError`. |
|
449 |
|
450 :type col: int |
|
451 :param col: The column localising the entity in the unique row |
|
452 |
|
453 :return: the partially initialized `Entity` instance |
|
454 """ |
|
455 if len(self) == 1: |
|
456 return self.get_entity(0, col) |
|
457 elif len(self) == 0: |
|
458 raise NoResultError("No row was found for one()") |
|
459 else: |
|
460 raise MultipleResultsError("Multiple rows were found for one()") |
|
461 |
|
462 def _build_entity(self, row, col): |
|
463 """internal method to get a single entity, returns a partially |
|
464 initialized Entity instance. |
|
465 |
|
466 partially means that only attributes selected in the RQL query will be |
|
467 directly assigned to the entity. |
|
468 |
|
469 :type row,col: int, int |
|
470 :param row,col: |
|
471 row and col numbers localizing the entity among the result's table |
|
472 |
|
473 :return: the partially initialized `Entity` instance |
|
474 """ |
|
475 req = self.req |
|
476 if req is None: |
|
477 raise AssertionError('dont call get_entity with no req on the result set') |
|
478 rowvalues = self.rows[row] |
|
479 eid = rowvalues[col] |
|
480 assert eid is not None |
|
481 # return cached entity if exists. This also avoids potential recursion |
|
482 # XXX should we consider updating a cached entity with possible |
|
483 # new attributes found in this resultset ? |
|
484 try: |
|
485 entity = req.entity_cache(eid) |
|
486 except KeyError: |
|
487 pass |
|
488 else: |
|
489 if entity.cw_rset is None: |
|
490 # entity has no rset set, this means entity has been created by |
|
491 # the querier (req is a repository session) and so jas no rset |
|
492 # info. Add it. |
|
493 entity.cw_rset = self |
|
494 entity.cw_row = row |
|
495 entity.cw_col = col |
|
496 return entity |
|
497 # build entity instance |
|
498 etype = self.description[row][col] |
|
499 entity = self.req.vreg['etypes'].etype_class(etype)(req, rset=self, |
|
500 row=row, col=col) |
|
501 entity.eid = eid |
|
502 # cache entity |
|
503 req.set_entity_cache(entity) |
|
504 # try to complete the entity if there are some additional columns |
|
505 if len(rowvalues) > 1: |
|
506 eschema = entity.e_schema |
|
507 eid_col, attr_cols, rel_cols = self._rset_structure(eschema, col) |
|
508 entity.eid = rowvalues[eid_col] |
|
509 for attr, col_idx in attr_cols.items(): |
|
510 entity.cw_attr_cache[attr] = rowvalues[col_idx] |
|
511 for (rtype, role), col_idx in rel_cols.items(): |
|
512 value = rowvalues[col_idx] |
|
513 if value is None: |
|
514 if role == 'subject': |
|
515 rql = 'Any Y WHERE X %s Y, X eid %s' |
|
516 else: |
|
517 rql = 'Any Y WHERE Y %s X, X eid %s' |
|
518 rrset = ResultSet([], rql % (rtype, entity.eid)) |
|
519 rrset.req = req |
|
520 else: |
|
521 rrset = self._build_entity(row, col_idx).as_rset() |
|
522 entity.cw_set_relation_cache(rtype, role, rrset) |
|
523 return entity |
|
524 |
|
525 @cached |
|
526 def _rset_structure(self, eschema, entity_col): |
|
527 eid_col = col = entity_col |
|
528 rqlst = self.syntax_tree() |
|
529 get_rschema = eschema.schema.rschema |
|
530 attr_cols = {} |
|
531 rel_cols = {} |
|
532 if rqlst.TYPE == 'select': |
|
533 # UNION query, find the subquery from which this entity has been |
|
534 # found |
|
535 select, col = rqlst.locate_subquery(entity_col, eschema.type, self.args) |
|
536 else: |
|
537 select = rqlst |
|
538 # take care, due to outer join support, we may find None |
|
539 # values for non final relation |
|
540 for i, attr, role in attr_desc_iterator(select, col, entity_col): |
|
541 rschema = get_rschema(attr) |
|
542 if rschema.final: |
|
543 if attr == 'eid': |
|
544 eid_col = i |
|
545 else: |
|
546 attr_cols[attr] = i |
|
547 else: |
|
548 # XXX takefirst=True to remove warning triggered by ambiguous relations |
|
549 rdef = eschema.rdef(attr, role, takefirst=True) |
|
550 # only keep value if it can't be multivalued |
|
551 if rdef.role_cardinality(role) in '1?': |
|
552 rel_cols[(attr, role)] = i |
|
553 return eid_col, attr_cols, rel_cols |
|
554 |
|
555 @cached |
|
556 def syntax_tree(self): |
|
557 """return the syntax tree (:class:`rql.stmts.Union`) for the |
|
558 originating query. You can expect it to have solutions |
|
559 computed and it will be properly annotated. |
|
560 """ |
|
561 return self.req.vreg.parse(self.req, self.rql, self.args) |
|
562 |
|
563 @cached |
|
564 def column_types(self, col): |
|
565 """return the list of different types in the column with the given col |
|
566 |
|
567 :type col: int |
|
568 :param col: the index of the desired column |
|
569 |
|
570 :rtype: list |
|
571 :return: the different entities type found in the column |
|
572 """ |
|
573 return frozenset(struc[-1][col] for struc in self.description_struct()) |
|
574 |
|
575 @cached |
|
576 def description_struct(self): |
|
577 """return a list describing sequence of results with the same |
|
578 description, e.g. : |
|
579 [[0, 4, ('Bug',)] |
|
580 [[0, 4, ('Bug',), [5, 8, ('Story',)] |
|
581 [[0, 3, ('Project', 'Version',)]] |
|
582 """ |
|
583 result = [] |
|
584 last = None |
|
585 for i, row in enumerate(self.description): |
|
586 if row != last: |
|
587 if last is not None: |
|
588 result[-1][1] = i - 1 |
|
589 result.append( [i, None, row] ) |
|
590 last = row |
|
591 if last is not None: |
|
592 result[-1][1] = i |
|
593 return result |
|
594 |
|
595 def _locate_query_params(self, rqlst, row, col): |
|
596 locate_query_col = col |
|
597 etype = self.description[row][col] |
|
598 # final type, find a better one to locate the correct subquery |
|
599 # (ambiguous if possible) |
|
600 eschema = self.req.vreg.schema.eschema |
|
601 if eschema(etype).final: |
|
602 for select in rqlst.children: |
|
603 try: |
|
604 myvar = select.selection[col].variable |
|
605 except AttributeError: |
|
606 # not a variable |
|
607 continue |
|
608 for i in range(len(select.selection)): |
|
609 if i == col: |
|
610 continue |
|
611 coletype = self.description[row][i] |
|
612 # None description possible on column resulting from an |
|
613 # outer join |
|
614 if coletype is None or eschema(coletype).final: |
|
615 continue |
|
616 try: |
|
617 ivar = select.selection[i].variable |
|
618 except AttributeError: |
|
619 # not a variable |
|
620 continue |
|
621 # check variables don't comes from a subquery or are both |
|
622 # coming from the same subquery |
|
623 if getattr(ivar, 'query', None) is getattr(myvar, 'query', None): |
|
624 etype = coletype |
|
625 locate_query_col = i |
|
626 if len(self.column_types(i)) > 1: |
|
627 return etype, locate_query_col |
|
628 return etype, locate_query_col |
|
629 |
|
630 @cached |
|
631 def related_entity(self, row, col): |
|
632 """given an cell of the result set, try to return a (entity, relation |
|
633 name) tuple to which this cell is linked. |
|
634 |
|
635 This is especially useful when the cell is an attribute of an entity, |
|
636 to get the entity to which this attribute belongs to. |
|
637 """ |
|
638 rqlst = self.syntax_tree() |
|
639 # UNION query, we've first to find a 'pivot' column to use to get the |
|
640 # actual query from which the row is coming |
|
641 etype, locate_query_col = self._locate_query_params(rqlst, row, col) |
|
642 # now find the query from which this entity has been found. Returned |
|
643 # select node may be a subquery with different column indexes. |
|
644 select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0] |
|
645 # then get the index of root query's col in the subquery |
|
646 col = rqlst.subquery_selection_index(select, col) |
|
647 if col is None: |
|
648 # XXX unexpected, should fix subquery_selection_index ? |
|
649 return None, None |
|
650 try: |
|
651 myvar = select.selection[col].variable |
|
652 except AttributeError: |
|
653 # not a variable |
|
654 return None, None |
|
655 rel = myvar.main_relation() |
|
656 if rel is not None: |
|
657 index = rel.children[0].root_selection_index() |
|
658 if index is not None and self.rows[row][index]: |
|
659 try: |
|
660 entity = self.get_entity(row, index) |
|
661 return entity, rel.r_type |
|
662 except NotAnEntity as exc: |
|
663 return None, None |
|
664 return None, None |
|
665 |
|
666 @cached |
|
667 def searched_text(self): |
|
668 """returns the searched text in case of full-text search |
|
669 |
|
670 :return: searched text or `None` if the query is not |
|
671 a full-text query |
|
672 """ |
|
673 rqlst = self.syntax_tree() |
|
674 for rel in rqlst.iget_nodes(nodes.Relation): |
|
675 if rel.r_type == 'has_text': |
|
676 __, rhs = rel.get_variable_parts() |
|
677 return rhs.eval(self.args) |
|
678 return None |
|
679 |
|
680 def _get_variable(term): |
|
681 # XXX rewritten const |
|
682 # use iget_nodes for (hack) case where we have things like MAX(V) |
|
683 for vref in term.iget_nodes(nodes.VariableRef): |
|
684 return vref.variable |
|
685 |
|
686 def attr_desc_iterator(select, selectidx, rootidx): |
|
687 """return an iterator on a list of 2-uple (index, attr_relation) |
|
688 localizing attribute relations of the main variable in a result's row |
|
689 |
|
690 :type rqlst: rql.stmts.Select |
|
691 :param rqlst: the RQL syntax tree to describe |
|
692 |
|
693 :return: |
|
694 a generator on (index, relation, target) describing column being |
|
695 attribute of the main variable |
|
696 """ |
|
697 rootselect = select |
|
698 while rootselect.parent.parent is not None: |
|
699 rootselect = rootselect.parent.parent.parent |
|
700 rootmain = rootselect.selection[selectidx] |
|
701 rootmainvar = _get_variable(rootmain) |
|
702 assert rootmainvar |
|
703 root = rootselect.parent |
|
704 selectmain = select.selection[selectidx] |
|
705 for i, term in enumerate(rootselect.selection): |
|
706 try: |
|
707 # don't use _get_variable here: if the term isn't a variable |
|
708 # (function...), we don't want it to be used as an entity attribute |
|
709 # or relation's value (XXX beside MAX/MIN trick?) |
|
710 rootvar = term.variable |
|
711 except AttributeError: |
|
712 continue |
|
713 if rootvar.name == rootmainvar.name: |
|
714 continue |
|
715 if select is not rootselect and isinstance(rootvar, nodes.ColumnAlias): |
|
716 term = select.selection[root.subquery_selection_index(select, i)] |
|
717 var = _get_variable(term) |
|
718 if var is None: |
|
719 continue |
|
720 for ref in var.references(): |
|
721 rel = ref.relation() |
|
722 if rel is None or rel.is_types_restriction(): |
|
723 continue |
|
724 lhs, rhs = rel.get_variable_parts() |
|
725 if selectmain.is_equivalent(lhs): |
|
726 if rhs.is_equivalent(term): |
|
727 yield (i, rel.r_type, 'subject') |
|
728 elif selectmain.is_equivalent(rhs): |
|
729 if lhs.is_equivalent(term): |
|
730 yield (i, rel.r_type, 'object') |
|