entity.py
changeset 7794 aed065b97f12
parent 7702 73cadb5d0097
child 7798 8930f7a284dd
equal deleted inserted replaced
7792:163d25c9fdd2 7794:aed065b97f12
    25 from logilab.common.decorators import cached
    25 from logilab.common.decorators import cached
    26 from logilab.common.deprecation import deprecated
    26 from logilab.common.deprecation import deprecated
    27 from logilab.mtconverter import TransformData, TransformError, xml_escape
    27 from logilab.mtconverter import TransformData, TransformError, xml_escape
    28 
    28 
    29 from rql.utils import rqlvar_maker
    29 from rql.utils import rqlvar_maker
       
    30 from rql.stmts import Select
       
    31 from rql.nodes import (Not, VariableRef, Constant, make_relation,
       
    32                        Relation as RqlRelation)
    30 
    33 
    31 from cubicweb import Unauthorized, typed_eid, neg_role
    34 from cubicweb import Unauthorized, typed_eid, neg_role
    32 from cubicweb.rset import ResultSet
    35 from cubicweb.rset import ResultSet
    33 from cubicweb.selectors import yes
    36 from cubicweb.selectors import yes
    34 from cubicweb.appobject import AppObject
    37 from cubicweb.appobject import AppObject
   173         return None
   176         return None
   174 
   177 
   175     @classmethod
   178     @classmethod
   176     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
   179     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
   177                   settype=True, ordermethod='fetch_order'):
   180                   settype=True, ordermethod='fetch_order'):
   178         """return a rql to fetch all entities of the class type"""
   181         st = cls.fetch_rqlst(user, mainvar=mainvar, fetchattrs=fetchattrs,
   179         # XXX update api and implementation to AST manipulation (see unrelated rql)
   182                              settype=settype, ordermethod=ordermethod)
   180         restrictions = restriction or []
   183         rql = st.as_string()
       
   184         if restriction:
       
   185             # cannot use RQLRewriter API to insert 'X rtype %(x)s' restriction
       
   186             warn('[3.14] fetch_rql: use of `restriction` parameter is '
       
   187                  'deprecated, please use fetch_rqlst and supply a syntax'
       
   188                  'tree with your restriction instead', DeprecationWarning)
       
   189             insert = ' WHERE ' + ','.join(restriction)
       
   190             if ' WHERE ' in rql:
       
   191                 select, where = rql.split(' WHERE ', 1)
       
   192                 rql = select + insert + ',' + where
       
   193             else:
       
   194                 rql += insert
       
   195         return rql
       
   196 
       
   197     @classmethod
       
   198     def fetch_rqlst(cls, user, select=None, mainvar='X', fetchattrs=None,
       
   199                     settype=True, ordermethod='fetch_order'):
       
   200         if select is None:
       
   201             select = Select()
       
   202             mainvar = select.get_variable(mainvar)
       
   203             select.add_selected(mainvar)
       
   204         elif isinstance(mainvar, basestring):
       
   205             assert mainvar in select.defined_vars
       
   206             mainvar = select.get_variable(mainvar)
       
   207         # eases string -> syntax tree test transition: please remove once stable
       
   208         select._varmaker = rqlvar_maker(defined=select.defined_vars,
       
   209                                         aliases=select.aliases, index=26)
   181         if settype:
   210         if settype:
   182             restrictions.append('%s is %s' % (mainvar, cls.__regid__))
   211             select.add_type_restriction(mainvar, cls.__regid__)
   183         if fetchattrs is None:
   212         if fetchattrs is None:
   184             fetchattrs = cls.fetch_attrs
   213             fetchattrs = cls.fetch_attrs
   185         selection = [mainvar]
   214         cls._fetch_restrictions(mainvar, select, fetchattrs, user, ordermethod)
   186         orderby = []
   215         return select
   187         # start from 26 to avoid possible conflicts with X
       
   188         # XXX not enough to be sure it'll be no conflicts
       
   189         varmaker = rqlvar_maker(index=26)
       
   190         cls._fetch_restrictions(mainvar, varmaker, fetchattrs, selection,
       
   191                                 orderby, restrictions, user, ordermethod)
       
   192         rql = 'Any %s' % ','.join(selection)
       
   193         if orderby:
       
   194             rql +=  ' ORDERBY %s' % ','.join(orderby)
       
   195         rql += ' WHERE %s' % ', '.join(restrictions)
       
   196         return rql
       
   197 
   216 
   198     @classmethod
   217     @classmethod
   199     def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs,
   218     def _fetch_restrictions(cls, mainvar, select, fetchattrs,
   200                             selection, orderby, restrictions, user,
   219                             user, ordermethod='fetch_order', visited=None):
   201                             ordermethod='fetch_order', visited=None):
       
   202         eschema = cls.e_schema
   220         eschema = cls.e_schema
   203         if visited is None:
   221         if visited is None:
   204             visited = set((eschema.type,))
   222             visited = set((eschema.type,))
   205         elif eschema.type in visited:
   223         elif eschema.type in visited:
   206             # avoid infinite recursion
   224             # avoid infinite recursion
   216                             attr, cls.__regid__)
   234                             attr, cls.__regid__)
   217                 continue
   235                 continue
   218             rdef = eschema.rdef(attr)
   236             rdef = eschema.rdef(attr)
   219             if not user.matching_groups(rdef.get_groups('read')):
   237             if not user.matching_groups(rdef.get_groups('read')):
   220                 continue
   238                 continue
   221             var = varmaker.next()
   239             if rschema.final or rdef.cardinality[0] in '?1':
   222             selection.append(var)
   240                 var = select.make_variable()
   223             restriction = '%s %s %s' % (mainvar, attr, var)
   241                 select.add_selected(var)
   224             restrictions.append(restriction)
   242                 rel = make_relation(mainvar, attr, (var,), VariableRef)
       
   243                 select.add_restriction(rel)
       
   244             else:
       
   245                 cls.warning('bad relation %s specified in fetch attrs for %s',
       
   246                             attr, cls)
       
   247                 continue
   225             if not rschema.final:
   248             if not rschema.final:
   226                 card = rdef.cardinality[0]
       
   227                 if card not in '?1':
       
   228                     cls.warning('bad relation %s specified in fetch attrs for %s',
       
   229                                  attr, cls)
       
   230                     selection.pop()
       
   231                     restrictions.pop()
       
   232                     continue
       
   233                 # XXX we need outer join in case the relation is not mandatory
   249                 # XXX we need outer join in case the relation is not mandatory
   234                 # (card == '?')  *or if the entity is being added*, since in
   250                 # (card == '?')  *or if the entity is being added*, since in
   235                 # that case the relation may still be missing. As we miss this
   251                 # that case the relation may still be missing. As we miss this
   236                 # later information here, systematically add it.
   252                 # later information here, systematically add it.
   237                 restrictions[-1] += '?'
   253                 rel.change_optional('right')
   238                 targettypes = rschema.objects(eschema.type)
   254                 targettypes = rschema.objects(eschema.type)
   239                 # XXX user._cw.vreg iiiirk
   255                 # XXX user._cw.vreg iiiirk
   240                 etypecls = user._cw.vreg['etypes'].etype_class(targettypes[0])
   256                 etypecls = user._cw.vreg['etypes'].etype_class(targettypes[0])
   241                 if len(targettypes) > 1:
   257                 if len(targettypes) > 1:
   242                     # find fetch_attrs common to all destination types
   258                     # find fetch_attrs common to all destination types
   243                     fetchattrs = user._cw.vreg['etypes'].fetch_attrs(targettypes)
   259                     fetchattrs = user._cw.vreg['etypes'].fetch_attrs(targettypes)
   244                     remove_ambiguous_rels(fetchattrs, targettypes, user._cw.vreg.schema)
   260                     remove_ambiguous_rels(fetchattrs, targettypes, user._cw.vreg.schema)
   245                 else:
   261                 else:
   246                     fetchattrs = etypecls.fetch_attrs
   262                     fetchattrs = etypecls.fetch_attrs
   247                 etypecls._fetch_restrictions(var, varmaker, fetchattrs,
   263                 etypecls._fetch_restrictions(var, select, fetchattrs,
   248                                              selection, orderby, restrictions,
       
   249                                              user, ordermethod, visited=visited)
   264                                              user, ordermethod, visited=visited)
   250             if ordermethod is not None:
   265             if ordermethod is not None:
   251                 orderterm = getattr(cls, ordermethod)(attr, var)
   266                 orderterm = getattr(cls, ordermethod)(attr, var.name)
   252                 if orderterm:
   267                 if orderterm:
   253                     orderby.append(orderterm)
   268                     var, order = orderterm.split()
   254         return selection, orderby, restrictions
   269                     select.add_sort_var(select.get_variable(var), order=='ASC')
   255 
   270 
   256     @classmethod
   271     @classmethod
   257     @cached
   272     @cached
   258     def _rest_attr_info(cls):
   273     def _rest_attr_info(cls):
   259         mainattr, needcheck = 'eid', True
   274         mainattr, needcheck = 'eid', True
   740 
   755 
   741         :param role: is the role played by 'self' in the relation ('subject' or 'object')
   756         :param role: is the role played by 'self' in the relation ('subject' or 'object')
   742         :param limit: resultset's maximum size
   757         :param limit: resultset's maximum size
   743         :param entities: if True, the entites are returned; if False, a result set is returned
   758         :param entities: if True, the entites are returned; if False, a result set is returned
   744         """
   759         """
       
   760         rtype = str(rtype)
   745         try:
   761         try:
   746             return self._cw_relation_cache(rtype, role, entities, limit)
   762             return self._cw_relation_cache(rtype, role, entities, limit)
   747         except KeyError:
   763         except KeyError:
   748             pass
   764             pass
   749         if not self.has_eid():
   765         if not self.has_eid():
   755         self.cw_set_relation_cache(rtype, role, rset)
   771         self.cw_set_relation_cache(rtype, role, rset)
   756         return self.related(rtype, role, limit, entities)
   772         return self.related(rtype, role, limit, entities)
   757 
   773 
   758     def cw_related_rql(self, rtype, role='subject', targettypes=None):
   774     def cw_related_rql(self, rtype, role='subject', targettypes=None):
   759         rschema = self._cw.vreg.schema[rtype]
   775         rschema = self._cw.vreg.schema[rtype]
       
   776         select = Select()
       
   777         mainvar, evar = select.get_variable('X'), select.get_variable('E')
       
   778         select.add_selected(mainvar)
       
   779         select.add_eid_restriction(evar, 'x', 'Substitute')
   760         if role == 'subject':
   780         if role == 'subject':
   761             restriction = 'E eid %%(x)s, E %s X' % rtype
   781             rel = make_relation(evar, rtype, (mainvar,), VariableRef)
       
   782             select.add_restriction(rel)
   762             if targettypes is None:
   783             if targettypes is None:
   763                 targettypes = rschema.objects(self.e_schema)
   784                 targettypes = rschema.objects(self.e_schema)
   764             else:
   785             else:
   765                 restriction += ', X is IN (%s)' % ','.join(targettypes)
   786                 select.add_constant_restriction(mainvar, 'is',
   766             card = greater_card(rschema, (self.e_schema,), targettypes, 0)
   787                                                 targettypes, 'etype')
   767         else:
   788             gcard = greater_card(rschema, (self.e_schema,), targettypes, 0)
   768             restriction = 'E eid %%(x)s, X %s E' % rtype
   789         else:
       
   790             rel = make_relation(mainvar, rtype, (evar,), VariableRef)
       
   791             select.add_restriction(rel)
   769             if targettypes is None:
   792             if targettypes is None:
   770                 targettypes = rschema.subjects(self.e_schema)
   793                 targettypes = rschema.subjects(self.e_schema)
   771             else:
   794             else:
   772                 restriction += ', X is IN (%s)' % ','.join(targettypes)
   795                 select.add_constant_restriction(mainvar, 'is', targettypes,
   773             card = greater_card(rschema, targettypes, (self.e_schema,), 1)
   796                                                 'String')
       
   797             gcard = greater_card(rschema, targettypes, (self.e_schema,), 1)
   774         etypecls = self._cw.vreg['etypes'].etype_class(targettypes[0])
   798         etypecls = self._cw.vreg['etypes'].etype_class(targettypes[0])
   775         if len(targettypes) > 1:
   799         if len(targettypes) > 1:
   776             fetchattrs = self._cw.vreg['etypes'].fetch_attrs(targettypes)
   800             fetchattrs = self._cw.vreg['etypes'].fetch_attrs(targettypes)
   777             # XXX we should fetch ambiguous relation objects too but not
   801             # XXX we should fetch ambiguous relation objects too but not
   778             # recurse on them in _fetch_restrictions; it is easier to remove
   802             # recurse on them in _fetch_restrictions; it is easier to remove
   779             # them completely for now, as it would require an deeper api rewrite
   803             # them completely for now, as it would require a deeper api rewrite
   780             remove_ambiguous_rels(fetchattrs, targettypes, self._cw.vreg.schema)
   804             remove_ambiguous_rels(fetchattrs, targettypes, self._cw.vreg.schema)
   781         else:
   805         else:
   782             fetchattrs = etypecls.fetch_attrs
   806             fetchattrs = etypecls.fetch_attrs
   783         rql = etypecls.fetch_rql(self._cw.user, [restriction], fetchattrs,
   807         etypecls.fetch_rqlst(self._cw.user, select, mainvar, fetchattrs,
   784                                  settype=False)
   808                              settype=False)
   785         # optimisation: remove ORDERBY if cardinality is 1 or ? (though
   809         # optimisation: remove ORDERBY if cardinality is 1 or ? (though
   786         # greater_card return 1 for those both cases)
   810         # greater_card return 1 for those both cases)
   787         if card == '1':
   811         if gcard == '1':
   788             if ' ORDERBY ' in rql:
   812             select.remove_sort_terms()
   789                 rql = '%s WHERE %s' % (rql.split(' ORDERBY ', 1)[0],
   813         elif not select.orderby:
   790                                        rql.split(' WHERE ', 1)[1])
   814             # if modification_date is already retrieved, we use it instead
   791         elif not ' ORDERBY ' in rql:
   815             # of adding another variable for sorting. This should not be
   792             args = rql.split(' WHERE ', 1)
   816             # problematic, but it is with sqlserver, see ticket #694445
   793             # if modification_date already retreived, we should use it instead
   817             for rel in select.where.get_nodes(RqlRelation):
   794             # of adding another variable for sort. This should be be problematic
   818                 if (rel.r_type == 'modification_date'
   795             # but it's actually with sqlserver, see ticket #694445
   819                     and rel.children[0].variable == mainvar
   796             if 'X modification_date ' in args[1]:
   820                     and rel.children[1].operator == '='):
   797                 var = args[1].split('X modification_date ', 1)[1].split(',', 1)[0]
   821                     var = rel.children[1].children[0].variable
   798                 args.insert(1, var.strip())
   822                     select.add_sort_var(var, asc=False)
   799                 rql = '%s ORDERBY %s DESC WHERE %s' % tuple(args)
   823                     break
   800             else:
   824             else:
   801                 rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % \
   825                 mdvar = select.make_variable()
   802                       tuple(args)
   826                 rel = make_relation(mainvar, 'modification_date',
   803         return rql
   827                                     (mdvar,), VariableRef)
       
   828                 select.add_restriction(rel)
       
   829                 select.add_sort_var(mdvar, asc=False)
       
   830         return select.as_string()
   804 
   831 
   805     # generic vocabulary methods ##############################################
   832     # generic vocabulary methods ##############################################
   806 
   833 
   807     def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
   834     def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
   808                          vocabconstraints=True):
   835                          vocabconstraints=True):
   815         ordermethod = ordermethod or 'fetch_unrelated_order'
   842         ordermethod = ordermethod or 'fetch_unrelated_order'
   816         if isinstance(rtype, basestring):
   843         if isinstance(rtype, basestring):
   817             rtype = self._cw.vreg.schema.rschema(rtype)
   844             rtype = self._cw.vreg.schema.rschema(rtype)
   818         rdef = rtype.role_rdef(self.e_schema, targettype, role)
   845         rdef = rtype.role_rdef(self.e_schema, targettype, role)
   819         rewriter = RQLRewriter(self._cw)
   846         rewriter = RQLRewriter(self._cw)
       
   847         select = Select()
   820         # initialize some variables according to the `role` of `self` in the
   848         # initialize some variables according to the `role` of `self` in the
   821         # relation:
   849         # relation (variable names must respect constraints conventions):
   822         # * variable for myself (`evar`) and searched entities (`searchvedvar`)
   850         # * variable for myself (`evar`)
   823         # * entity type of the subject (`subjtype`) and of the object
   851         # * variable for searched entities (`searchvedvar`)
   824         #   (`objtype`) of the relation
       
   825         if role == 'subject':
   852         if role == 'subject':
   826             evar, searchedvar = 'S', 'O'
   853             evar = subjvar = select.get_variable('S')
   827             subjtype, objtype = self.e_schema, targettype
   854             searchedvar = objvar = select.get_variable('O')
   828         else:
   855         else:
   829             searchedvar, evar = 'S', 'O'
   856             searchedvar = subjvar = select.get_variable('S')
   830             objtype, subjtype = self.e_schema, targettype
   857             evar = objvar = select.get_variable('O')
       
   858         select.add_selected(searchedvar)
   831         # initialize some variables according to `self` existance
   859         # initialize some variables according to `self` existance
   832         if rdef.role_cardinality(neg_role(role)) in '?1':
   860         if rdef.role_cardinality(neg_role(role)) in '?1':
   833             # if cardinality in '1?', we want a target entity which isn't
   861             # if cardinality in '1?', we want a target entity which isn't
   834             # already linked using this relation
   862             # already linked using this relation
   835             if searchedvar == 'S':
   863             var = select.get_variable('ZZ') # XXX unname when tests pass
   836                 restriction = ['NOT S %s ZZ' % rtype]
   864             if role == 'subject':
   837             else:
   865                 rel = make_relation(var, rtype.type, (searchedvar,), VariableRef)
   838                 restriction = ['NOT ZZ %s O' % rtype]
   866             else:
       
   867                 rel = make_relation(searchedvar, rtype.type, (var,), VariableRef)
       
   868             select.add_restriction(Not(rel))
   839         elif self.has_eid():
   869         elif self.has_eid():
   840             # elif we have an eid, we don't want a target entity which is
   870             # elif we have an eid, we don't want a target entity which is
   841             # already linked to ourself through this relation
   871             # already linked to ourself through this relation
   842             restriction = ['NOT S %s O' % rtype]
   872             rel = make_relation(subjvar, rtype.type, (objvar,), VariableRef)
   843         else:
   873             select.add_restriction(Not(rel))
   844             restriction = []
       
   845         if self.has_eid():
   874         if self.has_eid():
   846             restriction += ['%s eid %%(x)s' % evar]
   875             rel = make_relation(evar, 'eid', ('x', 'Substitute'), Constant)
       
   876             select.add_restriction(rel)
   847             args = {'x': self.eid}
   877             args = {'x': self.eid}
   848             if role == 'subject':
   878             if role == 'subject':
   849                 sec_check_args = {'fromeid': self.eid}
   879                 sec_check_args = {'fromeid': self.eid}
   850             else:
   880             else:
   851                 sec_check_args = {'toeid': self.eid}
   881                 sec_check_args = {'toeid': self.eid}
   852             existant = None # instead of 'SO', improve perfs
   882             existant = None # instead of 'SO', improve perfs
   853         else:
   883         else:
   854             args = {}
   884             args = {}
   855             sec_check_args = {}
   885             sec_check_args = {}
   856             existant = searchedvar
   886             existant = searchedvar.name
   857         # retreive entity class for targettype to compute base rql
   887             # undefine unused evar, or the type resolver will consider it
       
   888             select.undefine_variable(evar)
       
   889         # retrieve entity class for targettype to compute base rql
   858         etypecls = self._cw.vreg['etypes'].etype_class(targettype)
   890         etypecls = self._cw.vreg['etypes'].etype_class(targettype)
   859         rql = etypecls.fetch_rql(self._cw.user, restriction,
   891         etypecls.fetch_rqlst(self._cw.user, select, searchedvar,
   860                                  mainvar=searchedvar, ordermethod=ordermethod)
   892                              ordermethod=ordermethod)
   861         select = self._cw.vreg.parse(self._cw, rql, args).children[0]
   893         # from now on, we need variable type resolving
       
   894         self._cw.vreg.solutions(self._cw, select, args)
   862         # insert RQL expressions for schema constraints into the rql syntax tree
   895         # insert RQL expressions for schema constraints into the rql syntax tree
   863         if vocabconstraints:
   896         if vocabconstraints:
   864             # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
   897             # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
   865             # will be included as well
   898             # will be included as well
   866             cstrcls = RQLVocabularyConstraint
   899             cstrcls = RQLVocabularyConstraint
   867         else:
   900         else:
   868             cstrcls = RQLConstraint
   901             cstrcls = RQLConstraint
   869         for cstr in rdef.constraints:
   902         for cstr in rdef.constraints:
   870             # consider constraint.mainvars to check if constraint apply
   903             # consider constraint.mainvars to check if constraint apply
   871             if isinstance(cstr, cstrcls) and searchedvar in cstr.mainvars:
   904             if isinstance(cstr, cstrcls) and searchedvar.name in cstr.mainvars:
   872                 if not self.has_eid() and evar in cstr.mainvars:
   905                 if not self.has_eid() and evar.name in cstr.mainvars:
   873                     continue
   906                     continue
   874                 # compute a varmap suitable to RQLRewriter.rewrite argument
   907                 # compute a varmap suitable to RQLRewriter.rewrite argument
   875                 varmap = dict((v, v) for v in 'SO' if v in select.defined_vars
   908                 varmap = dict((v, v) for v in (searchedvar.name, evar.name)
   876                               and v in cstr.mainvars)
   909                               if v in select.defined_vars and v in cstr.mainvars)
   877                 # rewrite constraint by constraint since we want a AND between
   910                 # rewrite constraint by constraint since we want a AND between
   878                 # expressions.
   911                 # expressions.
   879                 rewriter.rewrite(select, [(varmap, (cstr,))], select.solutions,
   912                 rewriter.rewrite(select, [(varmap, (cstr,))], select.solutions,
   880                                  args, existant)
   913                                  args, existant)
   881         # insert security RQL expressions granting the permission to 'add' the
   914         # insert security RQL expressions granting the permission to 'add' the
   882         # relation into the rql syntax tree, if necessary
   915         # relation into the rql syntax tree, if necessary
   883         rqlexprs = rdef.get_rqlexprs('add')
   916         rqlexprs = rdef.get_rqlexprs('add')
   884         if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
   917         if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
   885             # compute a varmap suitable to RQLRewriter.rewrite argument
   918             # compute a varmap suitable to RQLRewriter.rewrite argument
   886             varmap = dict((v, v) for v in 'SO' if v in select.defined_vars)
   919             varmap = dict((v, v) for v in (searchedvar.name, evar.name)
       
   920                           if v in select.defined_vars)
   887             # rewrite all expressions at once since we want a OR between them.
   921             # rewrite all expressions at once since we want a OR between them.
   888             rewriter.rewrite(select, [(varmap, rqlexprs)], select.solutions,
   922             rewriter.rewrite(select, [(varmap, rqlexprs)], select.solutions,
   889                              args, existant)
   923                              args, existant)
   890         # ensure we have an order defined
   924         # ensure we have an order defined
   891         if not select.orderby:
   925         if not select.orderby:
   892             select.add_sort_var(select.defined_vars[searchedvar])
   926             select.add_sort_var(select.defined_vars[searchedvar.name])
   893         # we're done, turn the rql syntax tree as a string
   927         # we're done, turn the rql syntax tree as a string
   894         rql = select.as_string()
   928         rql = select.as_string()
   895         return rql, args
   929         return rql, args
   896 
   930 
   897     def unrelated(self, rtype, targettype, role='subject', limit=None,
   931     def unrelated(self, rtype, targettype, role='subject', limit=None,