rset.py 28.4 KB
Newer Older
David Douard's avatar
David Douard committed
1
# copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2
3
4
5
6
7
8
9
10
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
11
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
12
13
14
15
16
17
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
18
"""The `ResultSet` class which is returned as result of an rql query"""
Sylvain Thénault's avatar
Sylvain Thénault committed
19

Adrien Di Mascio's avatar
Adrien Di Mascio committed
20
from logilab.common.decorators import cached, clear_cache, copy_cache
21
from rql import nodes, stmts
Adrien Di Mascio's avatar
Adrien Di Mascio committed
22

23
from cubicweb import NotAnEntity, NoResultError, MultipleResultsError, UnknownEid
24

Adrien Di Mascio's avatar
Adrien Di Mascio committed
25
26

class ResultSet(object):
27
28
29
    """A result set wraps a RQL query result. This object implements
    partially the list protocol to allow direct use as a list of
    result rows.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
30
31

    :type rowcount: int
32
    :param rowcount: number of rows in the result
Adrien Di Mascio's avatar
Adrien Di Mascio committed
33
34

    :type rows: list
35
    :param rows: list of rows of result
Adrien Di Mascio's avatar
Adrien Di Mascio committed
36
37

    :type description: list
38
    :param description:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
39
40
41
      result's description, using the same structure as the result itself

    :type rql: str or unicode
42
    :param rql: the original RQL query string
Adrien Di Mascio's avatar
Adrien Di Mascio committed
43
    """
44

45
    def __init__(self, results, rql, args=None, description=None, variables=None):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
46
47
48
49
50
51
52
        self.rows = results
        self.rowcount = results and len(results) or 0
        # original query and arguments
        self.rql = rql
        self.args = args
        # entity types for each cell (same shape as rows)
        # maybe discarded if specified when the query has been executed
53
54
55
56
        if description is None:
            self.description = []
        else:
            self.description = description
57
        self.variables = variables if variables is not None else []
Adrien Di Mascio's avatar
Adrien Di Mascio committed
58
59
60
61
62
        # set to (limit, offset) when a result set is limited using the
        # .limit method
        self.limited = None
        # set by the cursor which returned this resultset
        self.req = None
63
        # actions cache
64
        self._actions_cache = None
65

Adrien Di Mascio's avatar
Adrien Di Mascio committed
66
67
68
69
    def __str__(self):
        if not self.rows:
            return '<empty resultset %s>' % self.rql
        return '<resultset %s (%s rows)>' % (self.rql, len(self.rows))
70

Adrien Di Mascio's avatar
Adrien Di Mascio committed
71
72
    def __repr__(self):
        if not self.rows:
73
            return '<empty resultset for %r>' % self.rql
74
75
76
        rows = self.rows
        if len(rows) > 10:
            rows = rows[:10] + ['...']
77
78
        if len(rows) > 1:
            # add a line break before first entity if more that one.
79
            pattern = '<resultset %r (%s rows):\n%s>'
80
81
82
        else:
            pattern = '<resultset %r (%s rows): %s>'

Adrien Di Mascio's avatar
Adrien Di Mascio committed
83
        if not self.description:
84
            return pattern % (self.rql, len(self.rows),
85
                              '\n'.join(str(r) for r in rows))
86
        return pattern % (self.rql, len(self.rows),
87
88
                          '\n'.join('%s (%s)' % (r, d)
                                    for r, d in zip(rows, self.description)))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
89

90
    def possible_actions(self, **kwargs):
91
92
93
        """Return possible actions on this result set. Should always be called with the same
        arguments so it may be computed only once.
        """
94
95
96
97
98
        if not kwargs:
            raise ValueError("ResultSet.possible_actions is expecting to receive "
                             "keywords arguments to be able to compute access to "
                             "the cache only once but you provided none.")

99
100
        key = tuple(sorted(kwargs.items()))
        if self._actions_cache is None:
101
            actions = self.req.vreg['actions'].poss_visible_objects(
102
                self.req, rset=self, **kwargs)
103
            self._actions_cache = (key, actions)
104
            return actions
105
        else:
106
107
108
109
110
111
            if key != self._actions_cache[0]:
                raise ValueError("ResultSet.possible_actions expects to always "
                                 "receive the same arguments to compute the "
                                 "cache once, but you've call it with the "
                                 "arguments: '%s' that aren't the same as the "
                                 "previously used combinaison '%s'" % (key, self._actions_cache[0]))
112
            return self._actions_cache[1]
113

Adrien Di Mascio's avatar
Adrien Di Mascio committed
114
115
116
117
118
119
    def __len__(self):
        """returns the result set's size"""
        return self.rowcount

    def __getitem__(self, i):
        """returns the ith element of the result set"""
120
        return self.rows[i]
121

Adrien Di Mascio's avatar
Adrien Di Mascio committed
122
123
124
125
126
127
128
129
130
131
    def __iter__(self):
        """Returns an iterator over rows"""
        return iter(self.rows)

    def __add__(self, rset):
        # XXX buggy implementation (.rql and .args attributes at least much
        # probably differ)
        # at least rql could be fixed now that we have union and sub-queries
        # but I tend to think that since we have that, we should not need this
        # method anymore (syt)
132
        rset = ResultSet(self.rows + rset.rows, self.rql, self.args,
133
                         self.description + rset.description)
134
135
        rset.req = self.req
        return rset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
136

137
138
139
140
    def copy(self, rows=None, descr=None):
        if rows is None:
            rows = self.rows[:]
            descr = self.description[:]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
141
        rset = ResultSet(rows, self.rql, self.args, descr)
142
143
        rset.req = self.req
        return rset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
144
145
146
147
148
149
150
151

    def transformed_rset(self, transformcb):
        """ the result set according to a given column types

        :type transormcb: callable(row, desc)
        :param transformcb:
          a callable which should take a row and its type description as
          parameters, and return the transformed row and type description.
152

Adrien Di Mascio's avatar
Adrien Di Mascio committed
153
154
155
156
157
158
159

        :type col: int
        :param col: the column index

        :rtype: `ResultSet`
        """
        rows, descr = [], []
160
        rset = self.copy(rows, descr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
161
162
        for row, desc in zip(self.rows, self.description):
            nrow, ndesc = transformcb(row, desc)
163
            if ndesc:  # transformcb returns None for ndesc to skip that row
Adrien Di Mascio's avatar
Adrien Di Mascio committed
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
                rows.append(nrow)
                descr.append(ndesc)
        rset.rowcount = len(rows)
        return rset

    def filtered_rset(self, filtercb, col=0):
        """filter the result set according to a given filtercb

        :type filtercb: callable(entity)
        :param filtercb:
          a callable which should take an entity as argument and return
          False if it should be skipped, else True

        :type col: int
        :param col: the column index

        :rtype: `ResultSet`
        """
        rows, descr = [], []
183
        rset = self.copy(rows, descr)
184
        for i in range(len(self)):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
            if not filtercb(self.get_entity(i, col)):
                continue
            rows.append(self.rows[i])
            descr.append(self.description[i])
        rset.rowcount = len(rows)
        return rset

    def sorted_rset(self, keyfunc, reverse=False, col=0):
        """sorts the result set according to a given keyfunc

        :type keyfunc: callable(entity)
        :param keyfunc:
          a callable which should take an entity as argument and return
          the value used to compare and sort

        :type reverse: bool
        :param reverse: if the result should be reversed

        :type col: int
        :param col: the column index. if col = -1, the whole row are used

        :rtype: `ResultSet`
        """
        rows, descr = [], []
209
        rset = self.copy(rows, descr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
210
211
        if col >= 0:
            entities = sorted(enumerate(self.entities(col)),
212
                              key=lambda t: keyfunc(t[1]), reverse=reverse)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
213
214
        else:
            entities = sorted(enumerate(self),
215
                              key=lambda t: keyfunc(t[1]), reverse=reverse)
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
216
        for index, _ in entities:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
217
218
219
220
221
222
            rows.append(self.rows[index])
            descr.append(self.description[index])
        rset.rowcount = len(rows)
        return rset

    def split_rset(self, keyfunc=None, col=0, return_dict=False):
223
224
        """splits the result set in multiple result sets according to
        a given key
225

Adrien Di Mascio's avatar
Adrien Di Mascio committed
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
        :type keyfunc: callable(entity or FinalType)
        :param keyfunc:
          a callable which should take a value of the rset in argument and
          return the value used to group the value. If not define, raw value
          of the specified columns is used.

        :type col: int
        :param col: the column index. if col = -1, the whole row are used

        :type return_dict: Boolean
        :param return_dict: If true, the function return a mapping
            (key -> rset) instead of a list of rset

        :rtype: List of `ResultSet` or mapping of  `ResultSet`

        """
        result = []
        mapping = {}
        for idx, line in enumerate(self):
            if col >= 0:
                try:
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
247
                    key = self.get_entity(idx, col)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
248
249
250
251
252
253
254
255
256
                except NotAnEntity:
                    key = line[col]
            else:
                key = line
            if keyfunc is not None:
                key = keyfunc(key)

            if key not in mapping:
                rows, descr = [], []
257
                rset = self.copy(rows, descr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
258
259
260
261
262
263
264
265
266
267
268
269
270
                mapping[key] = rset
                result.append(rset)
            else:
                rset = mapping[key]
            rset.rows.append(self.rows[idx])
            rset.description.append(self.description[idx])
        for rset in result:
            rset.rowcount = len(rset.rows)
        if return_dict:
            return mapping
        else:
            return result

271
    def limited_rql(self):
272
        """returns a printable rql for the result set associated to the object,
273
274
275
276
277
        with limit/offset correctly set according to maximum page size and
        currently displayed page when necessary
        """
        # try to get page boundaries from the navigation component
        # XXX we should probably not have a ref to this component here (eg in
Sylvain Thénault's avatar
Sylvain Thénault committed
278
        #     cubicweb)
279
280
        nav = self.req.vreg['components'].select_or_none('navigation', self.req,
                                                         rset=self)
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
        if nav:
            start, stop = nav.page_boundaries()
            rql = self._limit_offset_rql(stop - start, start)
        # result set may have be limited manually in which case navigation won't
        # apply
        elif self.limited:
            rql = self._limit_offset_rql(*self.limited)
        # navigation component doesn't apply and rset has not been limited, no
        # need to limit query
        else:
            rql = self.printable_rql()
        return rql

    def _limit_offset_rql(self, limit, offset):
        rqlst = self.syntax_tree()
        if len(rqlst.children) == 1:
            select = rqlst.children[0]
            olimit, ooffset = select.limit, select.offset
            select.limit, select.offset = limit, offset
            rql = rqlst.as_string(kwargs=self.args)
            # restore original limit/offset
            select.limit, select.offset = olimit, ooffset
        else:
            newselect = stmts.Select()
            newselect.limit = limit
            newselect.offset = offset
307
            aliases = [nodes.VariableRef(newselect.get_variable(chr(65 + i), i))
308
                       for i in range(len(rqlst.children[0].selection))]
309
310
            for vref in aliases:
                newselect.append_selected(nodes.VariableRef(vref.variable))
311
312
313
            newselect.set_with([nodes.SubQuery(aliases, rqlst)], check=False)
            newunion = stmts.Union()
            newunion.append(newselect)
314
            rql = newunion.as_string(kwargs=self.args)
315
316
317
            rqlst.parent = None
        return rql

Adrien Di Mascio's avatar
Adrien Di Mascio committed
318
    def limit(self, limit, offset=0, inplace=False):
Rémi Cardona's avatar
Rémi Cardona committed
319
        """limit the result set to the given number of rows optionally starting
Adrien Di Mascio's avatar
Adrien Di Mascio committed
320
321
322
323
324
325
326
        from an index different than 0

        :type limit: int
        :param limit: the maximum number of results

        :type offset: int
        :param offset: the offset index
327

Adrien Di Mascio's avatar
Adrien Di Mascio committed
328
329
330
331
332
333
334
        :type inplace: bool
        :param inplace:
          if true, the result set is modified in place, else a new result set
          is returned and the original is left unmodified

        :rtype: `ResultSet`
        """
335
        stop = limit + offset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
336
337
338
339
340
341
342
343
344
345
346
347
        rows = self.rows[offset:stop]
        descr = self.description[offset:stop]
        if inplace:
            rset = self
            rset.rows, rset.description = rows, descr
            rset.rowcount = len(rows)
            clear_cache(rset, 'description_struct')
            if offset:
                clear_cache(rset, 'get_entity')
            # we also have to fix/remove from the request entity cache entities
            # which get a wrong rset reference by this limit call
            for entity in self.req.cached_entities():
348
                if entity.cw_rset is self:
349
350
                    if offset <= entity.cw_row < stop:
                        entity.cw_row = entity.cw_row - offset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
351
                    else:
352
353
                        entity.cw_rset = entity.as_rset()
                        entity.cw_row = entity.cw_col = 0
Adrien Di Mascio's avatar
Adrien Di Mascio committed
354
        else:
355
            rset = self.copy(rows, descr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
356
357
358
359
360
            if not offset:
                # can copy built entity caches
                copy_cache(rset, 'get_entity', self)
        rset.limited = (limit, offset)
        return rset
361

Denis Laxalde's avatar
Denis Laxalde committed
362
    def printable_rql(self):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
363
364
365
        """return the result set's origin rql as a string, with arguments
        substitued
        """
Denis Laxalde's avatar
Denis Laxalde committed
366
        return self.syntax_tree().as_string(kwargs=self.args)
367

Adrien Di Mascio's avatar
Adrien Di Mascio committed
368
369
370
371
    # client helper methods ###################################################

    def entities(self, col=0):
        """iter on entities with eid in the `col` column of the result set"""
372
        for i in range(len(self)):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
373
374
375
376
377
            # may have None values in case of outer join (or aggregat on eid
            # hacks)
            if self.rows[i][col] is not None:
                yield self.get_entity(i, col)

378
379
    all = entities

380
381
382
383
384
385
386
387
388
389
390
391
392
    def iter_rows_with_entities(self):
        """ iterates over rows, and for each row
        eids are converted to plain entities
        """
        for i, row in enumerate(self):
            _row = []
            for j, col in enumerate(row):
                try:
                    _row.append(self.get_entity(i, j) if col is not None else col)
                except NotAnEntity:
                    _row.append(col)
            yield _row

393
394
395
396
397
398
399
400
    def complete_entity(self, row, col=0, skip_bytes=True):
        """short cut to get an completed entity instance for a particular
        row (all instance's attributes have been fetched)
        """
        entity = self.get_entity(row, col)
        entity.complete(skip_bytes=skip_bytes)
        return entity

Adrien Di Mascio's avatar
Adrien Di Mascio committed
401
    @cached
402
    def get_entity(self, row, col):
403
        """convenience method for query retrieving a single entity, returns a
Adrien Di Mascio's avatar
Adrien Di Mascio committed
404
        partially initialized Entity instance.
405

406
        .. warning::
407

408
409
410
          Due to the cache wrapping this function, you should NEVER give row as
          a named parameter (i.e. `rset.get_entity(0, 1)` is OK but
          `rset.get_entity(row=0, col=1)` isn't)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
411
412
413
414
415
416
417
418
419

        :type row,col: int, int
        :param row,col:
          row and col numbers localizing the entity among the result's table

        :return: the partially initialized `Entity` instance
        """
        etype = self.description[row][col]
        try:
420
            eschema = self.req.vreg.schema.eschema(etype)
421
            if eschema.final:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
422
423
424
425
426
                raise NotAnEntity(etype)
        except KeyError:
            raise NotAnEntity(etype)
        return self._build_entity(row, col)

427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
    def one(self, col=0):
        """Retrieve exactly one entity from the query.

        If the result set is empty, raises :exc:`NoResultError`.
        If the result set has more than one row, raises
        :exc:`MultipleResultsError`.

        :type col: int
        :param col: The column localising the entity in the unique row

        :return: the partially initialized `Entity` instance
        """
        if len(self) == 1:
            return self.get_entity(0, col)
        elif len(self) == 0:
            raise NoResultError("No row was found for one()")
        else:
            raise MultipleResultsError("Multiple rows were found for one()")

446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
    def first(self, col=0):
        """Retrieve the first entity from the query.

        If the result set is empty, raises :exc:`NoResultError`.

        :type col: int
        :param col: The column localising the entity in the unique row

        :return: the partially initialized `Entity` instance
        """
        if len(self) == 0:
            raise NoResultError("No row was found for first()")
        return self.get_entity(0, col)

    def last(self, col=0):
        """Retrieve the last entity from the query.

        If the result set is empty, raises :exc:`NoResultError`.

        :type col: int
        :param col: The column localising the entity in the unique row

        :return: the partially initialized `Entity` instance
        """
        if len(self) == 0:
            raise NoResultError("No row was found for last()")
        return self.get_entity(-1, col)

474
475
476
477
478
479
480
481
482
483
484
    def _make_entity(self, row, col):
        """Instantiate an entity, and store it in the entity cache"""
        # build entity instance
        etype = self.description[row][col]
        entity = self.req.vreg['etypes'].etype_class(etype)(self.req, rset=self,
                                                            row=row, col=col)
        entity.eid = self.rows[row][col]
        # cache entity
        self.req.set_entity_cache(entity)
        return entity

485
    def _build_entity(self, row, col, seen=None):
486
487
        """internal method to get a single entity, returns a partially
        initialized Entity instance.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
488

489
490
        partially means that only attributes selected in the RQL query will be
        directly assigned to the entity.
491

Adrien Di Mascio's avatar
Adrien Di Mascio committed
492
493
494
495
496
497
498
        :type row,col: int, int
        :param row,col:
          row and col numbers localizing the entity among the result's table

        :return: the partially initialized `Entity` instance
        """
        req = self.req
499
500
        assert req is not None, 'do not call get_entity with no req on the result set'

Adrien Di Mascio's avatar
Adrien Di Mascio committed
501
502
503
504
        rowvalues = self.rows[row]
        eid = rowvalues[col]
        assert eid is not None
        try:
505
            entity = req.entity_cache(eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
506
        except KeyError:
507
            entity = self._make_entity(row, col)
508
        else:
509
            if entity.cw_rset is None:
510
                # entity has no rset set, this means entity has been created by
511
                # the querier (req is a repository session) and so has no rset
512
513
514
515
                # info. Add it.
                entity.cw_rset = self
                entity.cw_row = row
                entity.cw_col = col
516
517
518
519
520
521
        # avoid recursion
        if seen is None:
            seen = set()
        if col in seen:
            return entity
        seen.add(col)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
522
523
        # try to complete the entity if there are some additional columns
        if len(rowvalues) > 1:
524
525
526
            eschema = entity.e_schema
            eid_col, attr_cols, rel_cols = self._rset_structure(eschema, col)
            entity.eid = rowvalues[eid_col]
527
            for attr, col_idx in attr_cols.items():
528
                entity.cw_attr_cache[attr] = rowvalues[col_idx]
529
            for (rtype, role), col_idx in rel_cols.items():
530
531
532
533
534
535
536
537
538
                value = rowvalues[col_idx]
                if value is None:
                    if role == 'subject':
                        rql = 'Any Y WHERE X %s Y, X eid %s'
                    else:
                        rql = 'Any Y WHERE Y %s X, X eid %s'
                    rrset = ResultSet([], rql % (rtype, entity.eid))
                    rrset.req = req
                else:
539
                    rrset = self._build_entity(row, col_idx, seen).as_rset()
540
541
542
543
544
545
546
                entity.cw_set_relation_cache(rtype, role, rrset)
        return entity

    @cached
    def _rset_structure(self, eschema, entity_col):
        eid_col = col = entity_col
        rqlst = self.syntax_tree()
547
        get_rschema = eschema.schema.rschema
548
549
550
551
552
553
554
555
556
557
558
        attr_cols = {}
        rel_cols = {}
        if rqlst.TYPE == 'select':
            # UNION query, find the subquery from which this entity has been
            # found
            select, col = rqlst.locate_subquery(entity_col, eschema.type, self.args)
        else:
            select = rqlst
        # take care, due to outer join support, we may find None
        # values for non final relation
        for i, attr, role in attr_desc_iterator(select, col, entity_col):
559
            rschema = get_rschema(attr)
560
561
562
            if rschema.final:
                if attr == 'eid':
                    eid_col = i
Adrien Di Mascio's avatar
Adrien Di Mascio committed
563
                else:
564
565
                    attr_cols[attr] = i
            else:
566
567
                # XXX takefirst=True to remove warning triggered by ambiguous relations
                rdef = eschema.rdef(attr, role, takefirst=True)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
568
                # only keep value if it can't be multivalued
Sylvain Thénault's avatar
Sylvain Thénault committed
569
                if rdef.role_cardinality(role) in '1?':
570
571
                    rel_cols[(attr, role)] = i
        return eid_col, attr_cols, rel_cols
Adrien Di Mascio's avatar
Adrien Di Mascio committed
572
573
574

    @cached
    def syntax_tree(self):
575
576
577
578
579
        """Return the **cached** syntax tree (:class:`rql.stmts.Union`) for the
        originating query.

        You can expect it to have solutions computed and it will be properly annotated.
        Since this is a cached shared object, **you must not modify it**.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
580
        """
581
582
583
584
585
586
587
588
589
        cnx = getattr(self.req, 'cnx', self.req)
        try:
            rqlst = cnx.repo.querier.rql_cache.get(cnx, self.rql, self.args)[0]
            if not rqlst.annotated:
                self.req.vreg.rqlhelper.annotate(rqlst)
            return rqlst
        except UnknownEid:
            # unknown eid in args prevent usage of rql cache, but we still need a rql st
            return self.req.vreg.parse(self.req, self.rql, self.args)
590

Adrien Di Mascio's avatar
Adrien Di Mascio committed
591
592
593
    @cached
    def column_types(self, col):
        """return the list of different types in the column with the given col
594

Adrien Di Mascio's avatar
Adrien Di Mascio committed
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
        :type col: int
        :param col: the index of the desired column

        :rtype: list
        :return: the different entities type found in the column
        """
        return frozenset(struc[-1][col] for struc in self.description_struct())

    @cached
    def description_struct(self):
        """return a list describing sequence of results with the same
        description, e.g. :
        [[0, 4, ('Bug',)]
        [[0, 4, ('Bug',), [5, 8, ('Story',)]
        [[0, 3, ('Project', 'Version',)]]
        """
        result = []
        last = None
        for i, row in enumerate(self.description):
            if row != last:
                if last is not None:
                    result[-1][1] = i - 1
617
                result.append([i, None, row])
Adrien Di Mascio's avatar
Adrien Di Mascio committed
618
619
620
621
622
                last = row
        if last is not None:
            result[-1][1] = i
        return result

623
624
625
626
627
    def _locate_query_params(self, rqlst, row, col):
        locate_query_col = col
        etype = self.description[row][col]
        # final type, find a better one to locate the correct subquery
        # (ambiguous if possible)
628
        eschema = self.req.vreg.schema.eschema
629
630
631
632
633
634
635
        if eschema(etype).final:
            for select in rqlst.children:
                try:
                    myvar = select.selection[col].variable
                except AttributeError:
                    # not a variable
                    continue
636
                for i in range(len(select.selection)):
637
638
639
                    if i == col:
                        continue
                    coletype = self.description[row][i]
Sylvain Thénault's avatar
Sylvain Thénault committed
640
641
                    # None description possible on column resulting from an
                    # outer join
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
                    if coletype is None or eschema(coletype).final:
                        continue
                    try:
                        ivar = select.selection[i].variable
                    except AttributeError:
                        # not a variable
                        continue
                    # check variables don't comes from a subquery or are both
                    # coming from the same subquery
                    if getattr(ivar, 'query', None) is getattr(myvar, 'query', None):
                        etype = coletype
                        locate_query_col = i
                        if len(self.column_types(i)) > 1:
                            return etype, locate_query_col
        return etype, locate_query_col

Adrien Di Mascio's avatar
Adrien Di Mascio committed
658
659
    @cached
    def related_entity(self, row, col):
660
661
662
663
664
665
        """given an cell of the result set, try to return a (entity, relation
        name) tuple to which this cell is linked.

        This is especially useful when the cell is an attribute of an entity,
        to get the entity to which this attribute belongs to.
        """
Adrien Di Mascio's avatar
Adrien Di Mascio committed
666
        rqlst = self.syntax_tree()
667
668
        # UNION query, we've first to find a 'pivot' column to use to get the
        # actual query from which the row is coming
669
        etype, locate_query_col = self._locate_query_params(rqlst, row, col)
670
671
        # now find the query from which this entity has been found. Returned
        # select node may be a subquery with different column indexes.
672
        select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0]
673
        # then get the index of root query's col in the subquery
674
        col = rqlst.subquery_selection_index(select, col)
675
676
677
        if col is None:
            # XXX unexpected, should fix subquery_selection_index ?
            return None, None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
678
679
680
        try:
            myvar = select.selection[col].variable
        except AttributeError:
681
            # not a variable
Adrien Di Mascio's avatar
Adrien Di Mascio committed
682
683
684
            return None, None
        rel = myvar.main_relation()
        if rel is not None:
685
            index = rel.children[0].root_selection_index()
686
            if index is not None and self.rows[row][index]:
687
688
689
                try:
                    entity = self.get_entity(row, index)
                    return entity, rel.r_type
690
                except NotAnEntity:
691
                    return None, None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
        return None, None

    @cached
    def searched_text(self):
        """returns the searched text in case of full-text search

        :return: searched text or `None` if the query is not
                 a full-text query
        """
        rqlst = self.syntax_tree()
        for rel in rqlst.iget_nodes(nodes.Relation):
            if rel.r_type == 'has_text':
                __, rhs = rel.get_variable_parts()
                return rhs.eval(self.args)
        return None
707

708

709
710
711
712
713
def _get_variable(term):
    # XXX rewritten const
    # use iget_nodes for (hack) case where we have things like MAX(V)
    for vref in term.iget_nodes(nodes.VariableRef):
        return vref.variable
Adrien Di Mascio's avatar
Adrien Di Mascio committed
714

715

716
def attr_desc_iterator(select, selectidx, rootidx):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
717
718
719
720
721
722
723
724
725
726
    """return an iterator on a list of 2-uple (index, attr_relation)
    localizing attribute relations of the main variable in a result's row

    :type rqlst: rql.stmts.Select
    :param rqlst: the RQL syntax tree to describe

    :return:
      a generator on (index, relation, target) describing column being
      attribute of the main variable
    """
727
728
729
730
731
732
733
734
735
    rootselect = select
    while rootselect.parent.parent is not None:
        rootselect = rootselect.parent.parent.parent
    rootmain = rootselect.selection[selectidx]
    rootmainvar = _get_variable(rootmain)
    assert rootmainvar
    root = rootselect.parent
    selectmain = select.selection[selectidx]
    for i, term in enumerate(rootselect.selection):
736
737
738
739
740
741
        try:
            # don't use _get_variable here: if the term isn't a variable
            # (function...), we don't want it to be used as an entity attribute
            # or relation's value (XXX beside MAX/MIN trick?)
            rootvar = term.variable
        except AttributeError:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
742
            continue
743
744
        if rootvar.name == rootmainvar.name:
            continue
745
        if select is not rootselect and isinstance(rootvar, nodes.ColumnAlias):
746
747
748
            term = select.selection[root.subquery_selection_index(select, i)]
        var = _get_variable(term)
        if var is None:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
749
750
751
752
753
754
            continue
        for ref in var.references():
            rel = ref.relation()
            if rel is None or rel.is_types_restriction():
                continue
            lhs, rhs = rel.get_variable_parts()
755
            if selectmain.is_equivalent(lhs):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
756
757
                if rhs.is_equivalent(term):
                    yield (i, rel.r_type, 'subject')
758
            elif selectmain.is_equivalent(rhs):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
759
760
                if lhs.is_equivalent(term):
                    yield (i, rel.r_type, 'object')