cubicweb/rset.py
changeset 11057 0b59724cb3f2
parent 10997 da712d3f0601
child 11168 dfa5f8879e8f
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     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')