rset.py
branchstable
changeset 4556 43c14e0e8972
parent 4475 37c413a07216
child 4850 bd640b137f50
child 4939 349af486f5ed
equal deleted inserted replaced
4555:8968c50818db 4556:43c14e0e8972
     7 """
     7 """
     8 __docformat__ = "restructuredtext en"
     8 __docformat__ = "restructuredtext en"
     9 
     9 
    10 from logilab.common.decorators import cached, clear_cache, copy_cache
    10 from logilab.common.decorators import cached, clear_cache, copy_cache
    11 
    11 
    12 from rql import nodes
    12 from rql import nodes, stmts
    13 
    13 
    14 from cubicweb import NotAnEntity
    14 from cubicweb import NotAnEntity
    15 
    15 
    16 
    16 
    17 class ResultSet(object):
    17 class ResultSet(object):
    81         else:
    81         else:
    82             key = None
    82             key = None
    83         try:
    83         try:
    84             return self._rsetactions[key]
    84             return self._rsetactions[key]
    85         except KeyError:
    85         except KeyError:
    86             actions = self.vreg['actions'].possible_vobjects(
    86             actions = self.vreg['actions'].poss_visible_objects(
    87                 self.req, rset=self, **kwargs)
    87                 self.req, rset=self, **kwargs)
    88             self._rsetactions[key] = actions
    88             self._rsetactions[key] = actions
    89             return actions
    89             return actions
    90 
    90 
    91     def __len__(self):
    91     def __len__(self):
   241                 result.append(rset)
   241                 result.append(rset)
   242             else:
   242             else:
   243                 rset = mapping[key]
   243                 rset = mapping[key]
   244             rset.rows.append(self.rows[idx])
   244             rset.rows.append(self.rows[idx])
   245             rset.description.append(self.description[idx])
   245             rset.description.append(self.description[idx])
   246 
       
   247 
       
   248         for rset in result:
   246         for rset in result:
   249             rset.rowcount = len(rset.rows)
   247             rset.rowcount = len(rset.rows)
   250         if return_dict:
   248         if return_dict:
   251             return mapping
   249             return mapping
   252         else:
   250         else:
   253             return result
   251             return result
       
   252 
       
   253     def limited_rql(self):
       
   254         """return a printable rql for the result set associated to the object,
       
   255         with limit/offset correctly set according to maximum page size and
       
   256         currently displayed page when necessary
       
   257         """
       
   258         # try to get page boundaries from the navigation component
       
   259         # XXX we should probably not have a ref to this component here (eg in
       
   260         #     cubicweb)
       
   261         nav = self.vreg['components'].select_or_none('navigation', self.req,
       
   262                                                      rset=self)
       
   263         if nav:
       
   264             start, stop = nav.page_boundaries()
       
   265             rql = self._limit_offset_rql(stop - start, start)
       
   266         # result set may have be limited manually in which case navigation won't
       
   267         # apply
       
   268         elif self.limited:
       
   269             rql = self._limit_offset_rql(*self.limited)
       
   270         # navigation component doesn't apply and rset has not been limited, no
       
   271         # need to limit query
       
   272         else:
       
   273             rql = self.printable_rql()
       
   274         return rql
       
   275 
       
   276     def _limit_offset_rql(self, limit, offset):
       
   277         rqlst = self.syntax_tree()
       
   278         if len(rqlst.children) == 1:
       
   279             select = rqlst.children[0]
       
   280             olimit, ooffset = select.limit, select.offset
       
   281             select.limit, select.offset = limit, offset
       
   282             rql = rqlst.as_string(kwargs=self.args)
       
   283             # restore original limit/offset
       
   284             select.limit, select.offset = olimit, ooffset
       
   285         else:
       
   286             newselect = stmts.Select()
       
   287             newselect.limit = limit
       
   288             newselect.offset = offset
       
   289             aliases = [nodes.VariableRef(newselect.get_variable(vref.name, i))
       
   290                        for i, vref in enumerate(rqlst.selection)]
       
   291             newselect.set_with([nodes.SubQuery(aliases, rqlst)], check=False)
       
   292             newunion = stmts.Union()
       
   293             newunion.append(newselect)
       
   294             rql = rqlst.as_string(kwargs=self.args)
       
   295             rqlst.parent = None
       
   296         return rql
   254 
   297 
   255     def limit(self, limit, offset=0, inplace=False):
   298     def limit(self, limit, offset=0, inplace=False):
   256         """limit the result set to the given number of rows optionaly starting
   299         """limit the result set to the given number of rows optionaly starting
   257         from an index different than 0
   300         from an index different than 0
   258 
   301 
   280             if offset:
   323             if offset:
   281                 clear_cache(rset, 'get_entity')
   324                 clear_cache(rset, 'get_entity')
   282             # we also have to fix/remove from the request entity cache entities
   325             # we also have to fix/remove from the request entity cache entities
   283             # which get a wrong rset reference by this limit call
   326             # which get a wrong rset reference by this limit call
   284             for entity in self.req.cached_entities():
   327             for entity in self.req.cached_entities():
   285                 if entity.rset is self:
   328                 if entity.cw_rset is self:
   286                     if offset <= entity.row < stop:
   329                     if offset <= entity.cw_row < stop:
   287                         entity.row = entity.row - offset
   330                         entity.cw_row = entity.cw_row - offset
   288                     else:
   331                     else:
   289                         self.req.drop_entity_cache(entity.eid)
   332                         self.req.drop_entity_cache(entity.eid)
   290         else:
   333         else:
   291             rset = self.copy(rows, descr)
   334             rset = self.copy(rows, descr)
   292             if not offset:
   335             if not offset:
   319             # may have None values in case of outer join (or aggregat on eid
   362             # may have None values in case of outer join (or aggregat on eid
   320             # hacks)
   363             # hacks)
   321             if self.rows[i][col] is not None:
   364             if self.rows[i][col] is not None:
   322                 yield self.get_entity(i, col)
   365                 yield self.get_entity(i, col)
   323 
   366 
       
   367     def complete_entity(self, row, col=0, skip_bytes=True):
       
   368         """short cut to get an completed entity instance for a particular
       
   369         row (all instance's attributes have been fetched)
       
   370         """
       
   371         entity = self.get_entity(row, col)
       
   372         entity.complete(skip_bytes=skip_bytes)
       
   373         return entity
       
   374 
   324     @cached
   375     @cached
   325     def get_entity(self, row, col=None):
   376     def get_entity(self, row, col):
   326         """special method for query retreiving a single entity, returns a
   377         """special method for query retreiving a single entity, returns a
   327         partially initialized Entity instance.
   378         partially initialized Entity instance.
   328 
   379 
   329         WARNING: due to the cache wrapping this function, you should NEVER
   380         WARNING: due to the cache wrapping this function, you should NEVER
   330                  give row as a named parameter (i.e. rset.get_entity(req, 0)
   381                  give row as a named parameter (i.e. rset.get_entity(req, 0)
   334         :param row,col:
   385         :param row,col:
   335           row and col numbers localizing the entity among the result's table
   386           row and col numbers localizing the entity among the result's table
   336 
   387 
   337         :return: the partially initialized `Entity` instance
   388         :return: the partially initialized `Entity` instance
   338         """
   389         """
   339         if col is None:
       
   340             from warnings import warn
       
   341             msg = 'col parameter will become mandatory in future version'
       
   342             warn(msg, DeprecationWarning, stacklevel=3)
       
   343             col = 0
       
   344         etype = self.description[row][col]
   390         etype = self.description[row][col]
   345         try:
   391         try:
   346             eschema = self.vreg.schema.eschema(etype)
   392             eschema = self.vreg.schema.eschema(etype)
   347             if eschema.final:
   393             if eschema.final:
   348                 raise NotAnEntity(etype)
   394                 raise NotAnEntity(etype)
   372         # return cached entity if exists. This also avoids potential recursion
   418         # return cached entity if exists. This also avoids potential recursion
   373         # XXX should we consider updating a cached entity with possible
   419         # XXX should we consider updating a cached entity with possible
   374         #     new attributes found in this resultset ?
   420         #     new attributes found in this resultset ?
   375         try:
   421         try:
   376             entity = req.entity_cache(eid)
   422             entity = req.entity_cache(eid)
   377             if entity.rset is None:
       
   378                 # entity has no rset set, this means entity has been cached by
       
   379                 # the repository (req is a repository session) which had no rset
       
   380                 # info. Add id.
       
   381                 entity.rset = self
       
   382                 entity.row = row
       
   383                 entity.col = col
       
   384             return entity
       
   385         except KeyError:
   423         except KeyError:
   386             pass
   424             pass
       
   425         else:
       
   426             if entity.cw_rset is None:
       
   427                 # entity has no rset set, this means entity has been created by
       
   428                 # the querier (req is a repository session) and so jas no rset
       
   429                 # info. Add it.
       
   430                 entity.cw_rset = self
       
   431                 entity.cw_row = row
       
   432                 entity.cw_col = col
       
   433             return entity
   387         # build entity instance
   434         # build entity instance
   388         etype = self.description[row][col]
   435         etype = self.description[row][col]
   389         entity = self.vreg['etypes'].etype_class(etype)(req, rset=self,
   436         entity = self.vreg['etypes'].etype_class(etype)(req, rset=self,
   390                                                         row=row, col=col)
   437                                                         row=row, col=col)
   391         entity.set_eid(eid)
   438         entity.set_eid(eid)
   401                 select, col = rqlst.locate_subquery(col, etype, self.args)
   448                 select, col = rqlst.locate_subquery(col, etype, self.args)
   402             else:
   449             else:
   403                 select = rqlst
   450                 select = rqlst
   404             # take care, due to outer join support, we may find None
   451             # take care, due to outer join support, we may find None
   405             # values for non final relation
   452             # values for non final relation
   406             for i, attr, x in attr_desc_iterator(select, col):
   453             for i, attr, role in attr_desc_iterator(select, col):
   407                 outerselidx = rqlst.subquery_selection_index(select, i)
   454                 outerselidx = rqlst.subquery_selection_index(select, i)
   408                 if outerselidx is None:
   455                 if outerselidx is None:
   409                     continue
   456                     continue
   410                 if x == 'subject':
   457                 if role == 'subject':
   411                     rschema = eschema.subjrels[attr]
   458                     rschema = eschema.subjrels[attr]
   412                     if rschema.final:
   459                     if rschema.final:
   413                         entity[attr] = rowvalues[outerselidx]
   460                         entity[attr] = rowvalues[outerselidx]
   414                         continue
   461                         continue
   415                     tetype = rschema.objects(etype)[0]
       
   416                     card = rschema.rproperty(etype, tetype, 'cardinality')[0]
       
   417                 else:
   462                 else:
   418                     rschema = eschema.objrels[attr]
   463                     rschema = eschema.objrels[attr]
   419                     tetype = rschema.subjects(etype)[0]
   464                 rdef = eschema.rdef(attr, role)
   420                     card = rschema.rproperty(tetype, etype, 'cardinality')[1]
       
   421                 # only keep value if it can't be multivalued
   465                 # only keep value if it can't be multivalued
   422                 if card in '1?':
   466                 if rdef.role_cardinality(role) in '1?':
   423                     if rowvalues[outerselidx] is None:
   467                     if rowvalues[outerselidx] is None:
   424                         if x == 'subject':
   468                         if role == 'subject':
   425                             rql = 'Any Y WHERE X %s Y, X eid %s'
   469                             rql = 'Any Y WHERE X %s Y, X eid %s'
   426                         else:
   470                         else:
   427                             rql = 'Any Y WHERE Y %s X, X eid %s'
   471                             rql = 'Any Y WHERE Y %s X, X eid %s'
   428                         rrset = ResultSet([], rql % (attr, entity.eid))
   472                         rrset = ResultSet([], rql % (attr, entity.eid))
   429                         req.decorate_rset(rrset)
   473                         req.decorate_rset(rrset)
   430                     else:
   474                     else:
   431                         rrset = self._build_entity(row, outerselidx).as_rset()
   475                         rrset = self._build_entity(row, outerselidx).as_rset()
   432                     entity.set_related_cache(attr, x, rrset)
   476                     entity.set_related_cache(attr, role, rrset)
   433         return entity
   477         return entity
   434 
   478 
   435     @cached
   479     @cached
   436     def syntax_tree(self):
   480     def syntax_tree(self):
   437         """get the syntax tree for the source query.
   481         """get the syntax tree for the source query.
   479                 last = row
   523                 last = row
   480         if last is not None:
   524         if last is not None:
   481             result[-1][1] = i
   525             result[-1][1] = i
   482         return result
   526         return result
   483 
   527 
       
   528     def _locate_query_params(self, rqlst, row, col):
       
   529         locate_query_col = col
       
   530         etype = self.description[row][col]
       
   531         # final type, find a better one to locate the correct subquery
       
   532         # (ambiguous if possible)
       
   533         eschema = self.vreg.schema.eschema
       
   534         if eschema(etype).final:
       
   535             for select in rqlst.children:
       
   536                 try:
       
   537                     myvar = select.selection[col].variable
       
   538                 except AttributeError:
       
   539                     # not a variable
       
   540                     continue
       
   541                 for i in xrange(len(select.selection)):
       
   542                     if i == col:
       
   543                         continue
       
   544                     coletype = self.description[row][i]
       
   545                     # None description possible on column resulting from an outer join
       
   546                     if coletype is None or eschema(coletype).final:
       
   547                         continue
       
   548                     try:
       
   549                         ivar = select.selection[i].variable
       
   550                     except AttributeError:
       
   551                         # not a variable
       
   552                         continue
       
   553                     # check variables don't comes from a subquery or are both
       
   554                     # coming from the same subquery
       
   555                     if getattr(ivar, 'query', None) is getattr(myvar, 'query', None):
       
   556                         etype = coletype
       
   557                         locate_query_col = i
       
   558                         if len(self.column_types(i)) > 1:
       
   559                             return etype, locate_query_col
       
   560         return etype, locate_query_col
       
   561 
   484     @cached
   562     @cached
   485     def related_entity(self, row, col):
   563     def related_entity(self, row, col):
   486         """try to get the related entity to extract format information if any"""
   564         """try to get the related entity to extract format information if any"""
   487         locate_query_col = col
       
   488         rqlst = self.syntax_tree()
   565         rqlst = self.syntax_tree()
   489         etype = self.description[row][col]
   566         etype, locate_query_col = self._locate_query_params(rqlst, row, col)
   490         if self.vreg.schema.eschema(etype).final:
       
   491             # final type, find a better one to locate the correct subquery
       
   492             # (ambiguous if possible)
       
   493             for i in xrange(len(rqlst.children[0].selection)):
       
   494                 if i == col:
       
   495                     continue
       
   496                 coletype = self.description[row][i]
       
   497                 if coletype is None:
       
   498                     continue
       
   499                 if not self.vreg.schema.eschema(coletype).final:
       
   500                     etype = coletype
       
   501                     locate_query_col = i
       
   502                     if len(self.column_types(i)) > 1:
       
   503                         break
       
   504         # UNION query, find the subquery from which this entity has been found
   567         # UNION query, find the subquery from which this entity has been found
   505         select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0]
   568         select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0]
   506         col = rqlst.subquery_selection_index(select, col)
   569         col = rqlst.subquery_selection_index(select, col)
   507         if col is None:
   570         if col is None:
   508             # XXX unexpected, should fix subquery_selection_index ?
   571             # XXX unexpected, should fix subquery_selection_index ?