rset.py
brancholdstable
changeset 4985 02b52bf9f5f8
parent 4939 349af486f5ed
child 4951 7dc54e12c606
equal deleted inserted replaced
4563:c25da7573ebd 4985:02b52bf9f5f8
     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(chr(65+i), i))
       
   290                        for i in xrange(len(rqlst.children[0].selection))]
       
   291             for vref in aliases:
       
   292                 newselect.append_selected(nodes.VariableRef(vref.variable))
       
   293             newselect.set_with([nodes.SubQuery(aliases, rqlst)], check=False)
       
   294             newunion = stmts.Union()
       
   295             newunion.append(newselect)
       
   296             rql = newunion.as_string(kwargs=self.args)
       
   297             rqlst.parent = None
       
   298         return rql
   254 
   299 
   255     def limit(self, limit, offset=0, inplace=False):
   300     def limit(self, limit, offset=0, inplace=False):
   256         """limit the result set to the given number of rows optionaly starting
   301         """limit the result set to the given number of rows optionaly starting
   257         from an index different than 0
   302         from an index different than 0
   258 
   303 
   280             if offset:
   325             if offset:
   281                 clear_cache(rset, 'get_entity')
   326                 clear_cache(rset, 'get_entity')
   282             # we also have to fix/remove from the request entity cache entities
   327             # we also have to fix/remove from the request entity cache entities
   283             # which get a wrong rset reference by this limit call
   328             # which get a wrong rset reference by this limit call
   284             for entity in self.req.cached_entities():
   329             for entity in self.req.cached_entities():
   285                 if entity.rset is self:
   330                 if entity.cw_rset is self:
   286                     if offset <= entity.row < stop:
   331                     if offset <= entity.cw_row < stop:
   287                         entity.row = entity.row - offset
   332                         entity.cw_row = entity.cw_row - offset
   288                     else:
   333                     else:
   289                         self.req.drop_entity_cache(entity.eid)
   334                         self.req.drop_entity_cache(entity.eid)
   290         else:
   335         else:
   291             rset = self.copy(rows, descr)
   336             rset = self.copy(rows, descr)
   292             if not offset:
   337             if not offset:
   319             # may have None values in case of outer join (or aggregat on eid
   364             # may have None values in case of outer join (or aggregat on eid
   320             # hacks)
   365             # hacks)
   321             if self.rows[i][col] is not None:
   366             if self.rows[i][col] is not None:
   322                 yield self.get_entity(i, col)
   367                 yield self.get_entity(i, col)
   323 
   368 
       
   369     def complete_entity(self, row, col=0, skip_bytes=True):
       
   370         """short cut to get an completed entity instance for a particular
       
   371         row (all instance's attributes have been fetched)
       
   372         """
       
   373         entity = self.get_entity(row, col)
       
   374         entity.complete(skip_bytes=skip_bytes)
       
   375         return entity
       
   376 
   324     @cached
   377     @cached
   325     def get_entity(self, row, col=None):
   378     def get_entity(self, row, col):
   326         """special method for query retreiving a single entity, returns a
   379         """special method for query retreiving a single entity, returns a
   327         partially initialized Entity instance.
   380         partially initialized Entity instance.
   328 
   381 
   329         WARNING: due to the cache wrapping this function, you should NEVER
   382         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)
   383                  give row as a named parameter (i.e. rset.get_entity(req, 0)
   334         :param row,col:
   387         :param row,col:
   335           row and col numbers localizing the entity among the result's table
   388           row and col numbers localizing the entity among the result's table
   336 
   389 
   337         :return: the partially initialized `Entity` instance
   390         :return: the partially initialized `Entity` instance
   338         """
   391         """
   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]
   392         etype = self.description[row][col]
   345         try:
   393         try:
   346             eschema = self.vreg.schema.eschema(etype)
   394             eschema = self.vreg.schema.eschema(etype)
   347             if eschema.final:
   395             if eschema.final:
   348                 raise NotAnEntity(etype)
   396                 raise NotAnEntity(etype)
   372         # return cached entity if exists. This also avoids potential recursion
   420         # return cached entity if exists. This also avoids potential recursion
   373         # XXX should we consider updating a cached entity with possible
   421         # XXX should we consider updating a cached entity with possible
   374         #     new attributes found in this resultset ?
   422         #     new attributes found in this resultset ?
   375         try:
   423         try:
   376             entity = req.entity_cache(eid)
   424             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:
   425         except KeyError:
   386             pass
   426             pass
       
   427         else:
       
   428             if entity.cw_rset is None:
       
   429                 # entity has no rset set, this means entity has been created by
       
   430                 # the querier (req is a repository session) and so jas no rset
       
   431                 # info. Add it.
       
   432                 entity.cw_rset = self
       
   433                 entity.cw_row = row
       
   434                 entity.cw_col = col
       
   435             return entity
   387         # build entity instance
   436         # build entity instance
   388         etype = self.description[row][col]
   437         etype = self.description[row][col]
   389         entity = self.vreg['etypes'].etype_class(etype)(req, rset=self,
   438         entity = self.vreg['etypes'].etype_class(etype)(req, rset=self,
   390                                                         row=row, col=col)
   439                                                         row=row, col=col)
   391         entity.set_eid(eid)
   440         entity.set_eid(eid)
   401                 select, col = rqlst.locate_subquery(col, etype, self.args)
   450                 select, col = rqlst.locate_subquery(col, etype, self.args)
   402             else:
   451             else:
   403                 select = rqlst
   452                 select = rqlst
   404             # take care, due to outer join support, we may find None
   453             # take care, due to outer join support, we may find None
   405             # values for non final relation
   454             # values for non final relation
   406             for i, attr, x in attr_desc_iterator(select, col):
   455             for i, attr, role in attr_desc_iterator(select, col):
   407                 outerselidx = rqlst.subquery_selection_index(select, i)
   456                 outerselidx = rqlst.subquery_selection_index(select, i)
   408                 if outerselidx is None:
   457                 if outerselidx is None:
   409                     continue
   458                     continue
   410                 if x == 'subject':
   459                 if role == 'subject':
   411                     rschema = eschema.subjrels[attr]
   460                     rschema = eschema.subjrels[attr]
   412                     if rschema.final:
   461                     if rschema.final:
   413                         entity[attr] = rowvalues[outerselidx]
   462                         entity[attr] = rowvalues[outerselidx]
   414                         continue
   463                         continue
   415                     tetype = rschema.objects(etype)[0]
       
   416                     card = rschema.rproperty(etype, tetype, 'cardinality')[0]
       
   417                 else:
   464                 else:
   418                     rschema = eschema.objrels[attr]
   465                     rschema = eschema.objrels[attr]
   419                     tetype = rschema.subjects(etype)[0]
   466                 rdef = eschema.rdef(attr, role)
   420                     card = rschema.rproperty(tetype, etype, 'cardinality')[1]
       
   421                 # only keep value if it can't be multivalued
   467                 # only keep value if it can't be multivalued
   422                 if card in '1?':
   468                 if rdef.role_cardinality(role) in '1?':
   423                     if rowvalues[outerselidx] is None:
   469                     if rowvalues[outerselidx] is None:
   424                         if x == 'subject':
   470                         if role == 'subject':
   425                             rql = 'Any Y WHERE X %s Y, X eid %s'
   471                             rql = 'Any Y WHERE X %s Y, X eid %s'
   426                         else:
   472                         else:
   427                             rql = 'Any Y WHERE Y %s X, X eid %s'
   473                             rql = 'Any Y WHERE Y %s X, X eid %s'
   428                         rrset = ResultSet([], rql % (attr, entity.eid))
   474                         rrset = ResultSet([], rql % (attr, entity.eid))
   429                         req.decorate_rset(rrset)
   475                         req.decorate_rset(rrset)
   430                     else:
   476                     else:
   431                         rrset = self._build_entity(row, outerselidx).as_rset()
   477                         rrset = self._build_entity(row, outerselidx).as_rset()
   432                     entity.set_related_cache(attr, x, rrset)
   478                     entity.set_related_cache(attr, role, rrset)
   433         return entity
   479         return entity
   434 
   480 
   435     @cached
   481     @cached
   436     def syntax_tree(self):
   482     def syntax_tree(self):
   437         """get the syntax tree for the source query.
   483         """get the syntax tree for the source query.
   479                 last = row
   525                 last = row
   480         if last is not None:
   526         if last is not None:
   481             result[-1][1] = i
   527             result[-1][1] = i
   482         return result
   528         return result
   483 
   529 
       
   530     def _locate_query_params(self, rqlst, row, col):
       
   531         locate_query_col = col
       
   532         etype = self.description[row][col]
       
   533         # final type, find a better one to locate the correct subquery
       
   534         # (ambiguous if possible)
       
   535         eschema = self.vreg.schema.eschema
       
   536         if eschema(etype).final:
       
   537             for select in rqlst.children:
       
   538                 try:
       
   539                     myvar = select.selection[col].variable
       
   540                 except AttributeError:
       
   541                     # not a variable
       
   542                     continue
       
   543                 for i in xrange(len(select.selection)):
       
   544                     if i == col:
       
   545                         continue
       
   546                     coletype = self.description[row][i]
       
   547                     # None description possible on column resulting from an outer join
       
   548                     if coletype is None or eschema(coletype).final:
       
   549                         continue
       
   550                     try:
       
   551                         ivar = select.selection[i].variable
       
   552                     except AttributeError:
       
   553                         # not a variable
       
   554                         continue
       
   555                     # check variables don't comes from a subquery or are both
       
   556                     # coming from the same subquery
       
   557                     if getattr(ivar, 'query', None) is getattr(myvar, 'query', None):
       
   558                         etype = coletype
       
   559                         locate_query_col = i
       
   560                         if len(self.column_types(i)) > 1:
       
   561                             return etype, locate_query_col
       
   562         return etype, locate_query_col
       
   563 
   484     @cached
   564     @cached
   485     def related_entity(self, row, col):
   565     def related_entity(self, row, col):
   486         """try to get the related entity to extract format information if any"""
   566         """try to get the related entity to extract format information if any"""
   487         locate_query_col = col
       
   488         rqlst = self.syntax_tree()
   567         rqlst = self.syntax_tree()
   489         etype = self.description[row][col]
   568         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
   569         # UNION query, find the subquery from which this entity has been found
   505         select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0]
   570         select = rqlst.locate_subquery(locate_query_col, etype, self.args)[0]
   506         col = rqlst.subquery_selection_index(select, col)
   571         col = rqlst.subquery_selection_index(select, col)
   507         if col is None:
   572         if col is None:
   508             # XXX unexpected, should fix subquery_selection_index ?
   573             # XXX unexpected, should fix subquery_selection_index ?