cubicweb/server/querier.py
changeset 11057 0b59724cb3f2
parent 10903 da30851f9706
child 11237 f32134dd0067
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2014 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 """Helper classes to execute RQL queries on a set of sources, performing
       
    19 security checking and data aggregation.
       
    20 """
       
    21 from __future__ import print_function
       
    22 
       
    23 __docformat__ = "restructuredtext en"
       
    24 
       
    25 from itertools import repeat
       
    26 
       
    27 from six import text_type, string_types, integer_types
       
    28 from six.moves import range
       
    29 
       
    30 from rql import RQLSyntaxError, CoercionError
       
    31 from rql.stmts import Union
       
    32 from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj, Relation, Exists, Not
       
    33 from yams import BASE_TYPES
       
    34 
       
    35 from cubicweb import ValidationError, Unauthorized, UnknownEid
       
    36 from cubicweb.rqlrewrite import RQLRelationRewriter
       
    37 from cubicweb import Binary, server
       
    38 from cubicweb.rset import ResultSet
       
    39 
       
    40 from cubicweb.utils import QueryCache, RepeatList
       
    41 from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
       
    42 from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
       
    43 from cubicweb.server.edition import EditedEntity
       
    44 from cubicweb.server.ssplanner import SSPlanner
       
    45 from cubicweb.statsd_logger import statsd_timeit, statsd_c
       
    46 
       
    47 ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
       
    48 
       
    49 
       
    50 def empty_rset(rql, args, rqlst=None):
       
    51     """build an empty result set object"""
       
    52     return ResultSet([], rql, args, rqlst=rqlst)
       
    53 
       
    54 def update_varmap(varmap, selected, table):
       
    55     """return a sql schema to store RQL query result"""
       
    56     for i, term in enumerate(selected):
       
    57         key = term.as_string()
       
    58         value = '%s.C%s' % (table, i)
       
    59         if varmap.get(key, value) != value:
       
    60             raise Exception('variable name conflict on %s: got %s / %s'
       
    61                             % (key, value, varmap))
       
    62         varmap[key] = value
       
    63 
       
    64 # permission utilities ########################################################
       
    65 
       
    66 def check_no_password_selected(rqlst):
       
    67     """check that Password entities are not selected"""
       
    68     for solution in rqlst.solutions:
       
    69         for var, etype in solution.items():
       
    70             if etype == 'Password':
       
    71                 raise Unauthorized('Password selection is not allowed (%s)' % var)
       
    72 
       
    73 def term_etype(cnx, term, solution, args):
       
    74     """return the entity type for the given term (a VariableRef or a Constant
       
    75     node)
       
    76     """
       
    77     try:
       
    78         return solution[term.name]
       
    79     except AttributeError:
       
    80         return cnx.entity_metas(term.eval(args))['type']
       
    81 
       
    82 def check_relations_read_access(cnx, select, args):
       
    83     """Raise :exc:`Unauthorized` if the given user doesn't have credentials to
       
    84     read relations used in the given syntax tree
       
    85     """
       
    86     # use `term_etype` since we've to deal with rewritten constants here,
       
    87     # when used as an external source by another repository.
       
    88     # XXX what about local read security w/ those rewritten constants...
       
    89     # XXX constants can also happen in some queries generated by req.find()
       
    90     DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS
       
    91     schema = cnx.repo.schema
       
    92     user = cnx.user
       
    93     if select.where is not None:
       
    94         for rel in select.where.iget_nodes(Relation):
       
    95             for solution in select.solutions:
       
    96                 # XXX has_text may have specific perm ?
       
    97                 if rel.r_type in READ_ONLY_RTYPES:
       
    98                     continue
       
    99                 rschema = schema.rschema(rel.r_type)
       
   100                 if rschema.final:
       
   101                     eschema = schema.eschema(term_etype(cnx, rel.children[0],
       
   102                                              solution, args))
       
   103                     rdef = eschema.rdef(rschema)
       
   104                 else:
       
   105                     rdef = rschema.rdef(term_etype(cnx, rel.children[0],
       
   106                                                    solution, args),
       
   107                                         term_etype(cnx, rel.children[1].children[0],
       
   108                                                    solution, args))
       
   109                 if not user.matching_groups(rdef.get_groups('read')):
       
   110                     if DBG:
       
   111                         print('check_read_access: %s %s does not match %s' %
       
   112                               (rdef, user.groups, rdef.get_groups('read')))
       
   113                     # XXX rqlexpr not allowed
       
   114                     raise Unauthorized('read', rel.r_type)
       
   115                 if DBG:
       
   116                     print('check_read_access: %s %s matches %s' %
       
   117                           (rdef, user.groups, rdef.get_groups('read')))
       
   118 
       
   119 def get_local_checks(cnx, rqlst, solution):
       
   120     """Check that the given user has credentials to access data read by the
       
   121     query and return a dict defining necessary "local checks" (i.e. rql
       
   122     expression in read permission defined in the schema) where no group grants
       
   123     him the permission.
       
   124 
       
   125     Returned dictionary's keys are variable names and values the rql expressions
       
   126     for this variable (with the given solution).
       
   127 
       
   128     Raise :exc:`Unauthorized` if access is known to be defined, i.e. if there is
       
   129     no matching group and no local permissions.
       
   130     """
       
   131     DBG = (server.DEBUG & server.DBG_SEC) and 'read' in server._SECURITY_CAPS
       
   132     schema = cnx.repo.schema
       
   133     user = cnx.user
       
   134     localchecks = {}
       
   135     # iterate on defined_vars and not on solutions to ignore column aliases
       
   136     for varname in rqlst.defined_vars:
       
   137         eschema = schema.eschema(solution[varname])
       
   138         if eschema.final:
       
   139             continue
       
   140         if not user.matching_groups(eschema.get_groups('read')):
       
   141             erqlexprs = eschema.get_rqlexprs('read')
       
   142             if not erqlexprs:
       
   143                 ex = Unauthorized('read', solution[varname])
       
   144                 ex.var = varname
       
   145                 if DBG:
       
   146                     print('check_read_access: %s %s %s %s' %
       
   147                           (varname, eschema, user.groups, eschema.get_groups('read')))
       
   148                 raise ex
       
   149             # don't insert security on variable only referenced by 'NOT X relation Y' or
       
   150             # 'NOT EXISTS(X relation Y)'
       
   151             varinfo = rqlst.defined_vars[varname].stinfo
       
   152             if varinfo['selected'] or (
       
   153                 len([r for r in varinfo['relations']
       
   154                      if (not schema.rschema(r.r_type).final
       
   155                          and ((isinstance(r.parent, Exists) and r.parent.neged(strict=True))
       
   156                               or isinstance(r.parent, Not)))])
       
   157                 !=
       
   158                 len(varinfo['relations'])):
       
   159                 localchecks[varname] = erqlexprs
       
   160     return localchecks
       
   161 
       
   162 
       
   163 # Plans #######################################################################
       
   164 
       
   165 class ExecutionPlan(object):
       
   166     """the execution model of a rql query, composed of querier steps"""
       
   167 
       
   168     def __init__(self, querier, rqlst, args, cnx):
       
   169         # original rql syntax tree
       
   170         self.rqlst = rqlst
       
   171         self.args = args or {}
       
   172         # cnx executing the query
       
   173         self.cnx = cnx
       
   174         # quick reference to the system source
       
   175         self.syssource = cnx.repo.system_source
       
   176         # execution steps
       
   177         self.steps = []
       
   178         # various resource accesors
       
   179         self.querier = querier
       
   180         self.schema = querier.schema
       
   181         self.sqlannotate = querier.sqlgen_annotate
       
   182         self.rqlhelper = cnx.vreg.rqlhelper
       
   183 
       
   184     def annotate_rqlst(self):
       
   185         if not self.rqlst.annotated:
       
   186             self.rqlhelper.annotate(self.rqlst)
       
   187 
       
   188     def add_step(self, step):
       
   189         """add a step to the plan"""
       
   190         self.steps.append(step)
       
   191 
       
   192     def sqlexec(self, sql, args=None):
       
   193         return self.syssource.sqlexec(self.cnx, sql, args)
       
   194 
       
   195     def execute(self):
       
   196         """execute a plan and return resulting rows"""
       
   197         for step in self.steps:
       
   198             result = step.execute()
       
   199         # the latest executed step contains the full query result
       
   200         return result
       
   201 
       
   202     def preprocess(self, union, security=True):
       
   203         """insert security when necessary then annotate rql st for sql generation
       
   204 
       
   205         return rqlst to actually execute
       
   206         """
       
   207         cached = None
       
   208         if security and self.cnx.read_security:
       
   209             # ensure security is turned of when security is inserted,
       
   210             # else we may loop for ever...
       
   211             if self.cnx.transaction_data.get('security-rqlst-cache'):
       
   212                 key = self.cache_key
       
   213             else:
       
   214                 key = None
       
   215             if key is not None and key in self.cnx.transaction_data:
       
   216                 cachedunion, args = self.cnx.transaction_data[key]
       
   217                 union.children[:] = []
       
   218                 for select in cachedunion.children:
       
   219                     union.append(select)
       
   220                 union.has_text_query = cachedunion.has_text_query
       
   221                 args.update(self.args)
       
   222                 self.args = args
       
   223                 cached = True
       
   224             else:
       
   225                 with self.cnx.security_enabled(read=False):
       
   226                     noinvariant = self._insert_security(union)
       
   227                 if key is not None:
       
   228                     self.cnx.transaction_data[key] = (union, self.args)
       
   229         else:
       
   230             noinvariant = ()
       
   231         if cached is None:
       
   232             self.rqlhelper.simplify(union)
       
   233             self.sqlannotate(union)
       
   234             set_qdata(self.schema.rschema, union, noinvariant)
       
   235         if union.has_text_query:
       
   236             self.cache_key = None
       
   237 
       
   238     def _insert_security(self, union):
       
   239         noinvariant = set()
       
   240         for select in union.children[:]:
       
   241             for subquery in select.with_:
       
   242                 self._insert_security(subquery.query)
       
   243             localchecks, restricted = self._check_permissions(select)
       
   244             if any(localchecks):
       
   245                 self.cnx.rql_rewriter.insert_local_checks(
       
   246                     select, self.args, localchecks, restricted, noinvariant)
       
   247         return noinvariant
       
   248 
       
   249     def _check_permissions(self, rqlst):
       
   250         """Return a dict defining "local checks", i.e. RQLExpression defined in
       
   251         the schema that should be inserted in the original query, together with
       
   252         a set of variable names which requires some security to be inserted.
       
   253 
       
   254         Solutions where a variable has a type which the user can't definitly
       
   255         read are removed, else if the user *may* read it (i.e. if an rql
       
   256         expression is defined for the "read" permission of the related type),
       
   257         the local checks dict is updated.
       
   258 
       
   259         The local checks dict has entries for each different local check
       
   260         necessary, with associated solutions as value, a local check being
       
   261         defined by a list of 2-uple (variable name, rql expressions) for each
       
   262         variable which has to be checked. Solutions which don't require local
       
   263         checks will be associated to the empty tuple key.
       
   264 
       
   265         Note rqlst should not have been simplified at this point.
       
   266         """
       
   267         cnx = self.cnx
       
   268         msgs = []
       
   269         # dict(varname: eid), allowing to check rql expression for variables
       
   270         # which have a known eid
       
   271         varkwargs = {}
       
   272         if not cnx.transaction_data.get('security-rqlst-cache'):
       
   273             for var in rqlst.defined_vars.values():
       
   274                 if var.stinfo['constnode'] is not None:
       
   275                     eid = var.stinfo['constnode'].eval(self.args)
       
   276                     varkwargs[var.name] = int(eid)
       
   277         # dictionary of variables restricted for security reason
       
   278         localchecks = {}
       
   279         restricted_vars = set()
       
   280         newsolutions = []
       
   281         for solution in rqlst.solutions:
       
   282             try:
       
   283                 localcheck = get_local_checks(cnx, rqlst, solution)
       
   284             except Unauthorized as ex:
       
   285                 msg = 'remove %s from solutions since %s has no %s access to %s'
       
   286                 msg %= (solution, cnx.user.login, ex.args[0], ex.args[1])
       
   287                 msgs.append(msg)
       
   288                 LOGGER.info(msg)
       
   289             else:
       
   290                 newsolutions.append(solution)
       
   291                 # try to benefit of rqlexpr.check cache for entities which
       
   292                 # are specified by eid in query'args
       
   293                 for varname, eid in varkwargs.items():
       
   294                     try:
       
   295                         rqlexprs = localcheck.pop(varname)
       
   296                     except KeyError:
       
   297                         continue
       
   298                     # if entity has been added in the current transaction, the
       
   299                     # user can read it whatever rql expressions are associated
       
   300                     # to its type
       
   301                     if cnx.added_in_transaction(eid):
       
   302                         continue
       
   303                     for rqlexpr in rqlexprs:
       
   304                         if rqlexpr.check(cnx, eid):
       
   305                             break
       
   306                     else:
       
   307                         raise Unauthorized('No read acces on %r with eid %i.' % (var, eid))
       
   308                 # mark variables protected by an rql expression
       
   309                 restricted_vars.update(localcheck)
       
   310                 # turn local check into a dict key
       
   311                 localcheck = tuple(sorted(localcheck.items()))
       
   312                 localchecks.setdefault(localcheck, []).append(solution)
       
   313         # raise Unautorized exception if the user can't access to any solution
       
   314         if not newsolutions:
       
   315             raise Unauthorized('\n'.join(msgs))
       
   316         # if there is some message, solutions have been modified and must be
       
   317         # reconsidered by the syntax treee
       
   318         if msgs:
       
   319             rqlst.set_possible_types(newsolutions)
       
   320         return localchecks, restricted_vars
       
   321 
       
   322     def finalize(self, select, solutions, insertedvars):
       
   323         rqlst = Union()
       
   324         rqlst.append(select)
       
   325         for mainvarname, rschema, newvarname in insertedvars:
       
   326             nvartype = str(rschema.objects(solutions[0][mainvarname])[0])
       
   327             for sol in solutions:
       
   328                 sol[newvarname] = nvartype
       
   329         select.clean_solutions(solutions)
       
   330         add_types_restriction(self.schema, select)
       
   331         self.rqlhelper.annotate(rqlst)
       
   332         self.preprocess(rqlst, security=False)
       
   333         return rqlst
       
   334 
       
   335 
       
   336 class InsertPlan(ExecutionPlan):
       
   337     """an execution model specific to the INSERT rql query
       
   338     """
       
   339 
       
   340     def __init__(self, querier, rqlst, args, cnx):
       
   341         ExecutionPlan.__init__(self, querier, rqlst, args, cnx)
       
   342         # save originally selected variable, we may modify this
       
   343         # dictionary for substitution (query parameters)
       
   344         self.selected = rqlst.selection
       
   345         # list of rows of entities definition (ssplanner.EditedEntity)
       
   346         self.e_defs = [[]]
       
   347         # list of new relation definition (3-uple (from_eid, r_type, to_eid)
       
   348         self.r_defs = set()
       
   349         # indexes to track entity definitions bound to relation definitions
       
   350         self._r_subj_index = {}
       
   351         self._r_obj_index = {}
       
   352         self._expanded_r_defs = {}
       
   353 
       
   354     def add_entity_def(self, edef):
       
   355         """add an entity definition to build"""
       
   356         self.e_defs[-1].append(edef)
       
   357 
       
   358     def add_relation_def(self, rdef):
       
   359         """add an relation definition to build"""
       
   360         self.r_defs.add(rdef)
       
   361         if not isinstance(rdef[0], int):
       
   362             self._r_subj_index.setdefault(rdef[0], []).append(rdef)
       
   363         if not isinstance(rdef[2], int):
       
   364             self._r_obj_index.setdefault(rdef[2], []).append(rdef)
       
   365 
       
   366     def substitute_entity_def(self, edef, edefs):
       
   367         """substitute an incomplete entity definition by a list of complete
       
   368         equivalents
       
   369 
       
   370         e.g. on queries such as ::
       
   371           INSERT Personne X, Societe Y: X nom N, Y nom 'toto', X travaille Y
       
   372           WHERE U login 'admin', U login N
       
   373 
       
   374         X will be inserted as many times as U exists, and so the X travaille Y
       
   375         relations as to be added as many time as X is inserted
       
   376         """
       
   377         if not edefs or not self.e_defs:
       
   378             # no result, no entity will be created
       
   379             self.e_defs = ()
       
   380             return
       
   381         # first remove the incomplete entity definition
       
   382         colidx = self.e_defs[0].index(edef)
       
   383         for i, row in enumerate(self.e_defs[:]):
       
   384             self.e_defs[i][colidx] = edefs[0]
       
   385             samplerow = self.e_defs[i]
       
   386             for edef_ in edefs[1:]:
       
   387                 row = [ed.clone() for i, ed in enumerate(samplerow)
       
   388                        if i != colidx]
       
   389                 row.insert(colidx, edef_)
       
   390                 self.e_defs.append(row)
       
   391         # now, see if this entity def is referenced as subject in some relation
       
   392         # definition
       
   393         if edef in self._r_subj_index:
       
   394             for rdef in self._r_subj_index[edef]:
       
   395                 expanded = self._expanded(rdef)
       
   396                 result = []
       
   397                 for exp_rdef in expanded:
       
   398                     for edef_ in edefs:
       
   399                         result.append( (edef_, exp_rdef[1], exp_rdef[2]) )
       
   400                 self._expanded_r_defs[rdef] = result
       
   401         # and finally, see if this entity def is referenced as object in some
       
   402         # relation definition
       
   403         if edef in self._r_obj_index:
       
   404             for rdef in self._r_obj_index[edef]:
       
   405                 expanded = self._expanded(rdef)
       
   406                 result = []
       
   407                 for exp_rdef in expanded:
       
   408                     for edef_ in edefs:
       
   409                         result.append( (exp_rdef[0], exp_rdef[1], edef_) )
       
   410                 self._expanded_r_defs[rdef] = result
       
   411 
       
   412     def _expanded(self, rdef):
       
   413         """return expanded value for the given relation definition"""
       
   414         try:
       
   415             return self._expanded_r_defs[rdef]
       
   416         except KeyError:
       
   417             self.r_defs.remove(rdef)
       
   418             return [rdef]
       
   419 
       
   420     def relation_defs(self):
       
   421         """return the list for relation definitions to insert"""
       
   422         for rdefs in self._expanded_r_defs.values():
       
   423             for rdef in rdefs:
       
   424                 yield rdef
       
   425         for rdef in self.r_defs:
       
   426             yield rdef
       
   427 
       
   428     def insert_entity_defs(self):
       
   429         """return eids of inserted entities in a suitable form for the resulting
       
   430         result set, e.g.:
       
   431 
       
   432         e.g. on queries such as ::
       
   433           INSERT Personne X, Societe Y: X nom N, Y nom 'toto', X travaille Y
       
   434           WHERE U login 'admin', U login N
       
   435 
       
   436         if there is two entities matching U, the result set will look like
       
   437         [(eidX1, eidY1), (eidX2, eidY2)]
       
   438         """
       
   439         cnx = self.cnx
       
   440         repo = cnx.repo
       
   441         results = []
       
   442         for row in self.e_defs:
       
   443             results.append([repo.glob_add_entity(cnx, edef)
       
   444                             for edef in row])
       
   445         return results
       
   446 
       
   447     def insert_relation_defs(self):
       
   448         cnx = self.cnx
       
   449         repo = cnx.repo
       
   450         edited_entities = {}
       
   451         relations = {}
       
   452         for subj, rtype, obj in self.relation_defs():
       
   453             # if a string is given into args instead of an int, we get it here
       
   454             if isinstance(subj, string_types):
       
   455                 subj = int(subj)
       
   456             elif not isinstance(subj, integer_types):
       
   457                 subj = subj.entity.eid
       
   458             if isinstance(obj, string_types):
       
   459                 obj = int(obj)
       
   460             elif not isinstance(obj, integer_types):
       
   461                 obj = obj.entity.eid
       
   462             if repo.schema.rschema(rtype).inlined:
       
   463                 if subj not in edited_entities:
       
   464                     entity = cnx.entity_from_eid(subj)
       
   465                     edited = EditedEntity(entity)
       
   466                     edited_entities[subj] = edited
       
   467                 else:
       
   468                     edited = edited_entities[subj]
       
   469                 edited.edited_attribute(rtype, obj)
       
   470             else:
       
   471                 if rtype in relations:
       
   472                     relations[rtype].append((subj, obj))
       
   473                 else:
       
   474                     relations[rtype] = [(subj, obj)]
       
   475         repo.glob_add_relations(cnx, relations)
       
   476         for edited in edited_entities.values():
       
   477             repo.glob_update_entity(cnx, edited)
       
   478 
       
   479 
       
   480 class QuerierHelper(object):
       
   481     """helper class to execute rql queries, putting all things together"""
       
   482 
       
   483     def __init__(self, repo, schema):
       
   484         # system info helper
       
   485         self._repo = repo
       
   486         # instance schema
       
   487         self.set_schema(schema)
       
   488 
       
   489     def set_schema(self, schema):
       
   490         self.schema = schema
       
   491         repo = self._repo
       
   492         # rql st and solution cache.
       
   493         self._rql_cache = QueryCache(repo.config['rql-cache-size'])
       
   494         # rql cache key cache. Don't bother using a Cache instance: we should
       
   495         # have a limited number of queries in there, since there are no entries
       
   496         # in this cache for user queries (which have no args)
       
   497         self._rql_ck_cache = {}
       
   498         # some cache usage stats
       
   499         self.cache_hit, self.cache_miss = 0, 0
       
   500         # rql parsing / analysing helper
       
   501         self.solutions = repo.vreg.solutions
       
   502         rqlhelper = repo.vreg.rqlhelper
       
   503         # set backend on the rql helper, will be used for function checking
       
   504         rqlhelper.backend = repo.config.system_source_config['db-driver']
       
   505         self._parse = rqlhelper.parse
       
   506         self._annotate = rqlhelper.annotate
       
   507         # rql planner
       
   508         self._planner = SSPlanner(schema, rqlhelper)
       
   509         # sql generation annotator
       
   510         self.sqlgen_annotate = SQLGenAnnotator(schema).annotate
       
   511 
       
   512     def parse(self, rql, annotate=False):
       
   513         """return a rql syntax tree for the given rql"""
       
   514         try:
       
   515             return self._parse(text_type(rql), annotate=annotate)
       
   516         except UnicodeError:
       
   517             raise RQLSyntaxError(rql)
       
   518 
       
   519     def plan_factory(self, rqlst, args, cnx):
       
   520         """create an execution plan for an INSERT RQL query"""
       
   521         if rqlst.TYPE == 'insert':
       
   522             return InsertPlan(self, rqlst, args, cnx)
       
   523         return ExecutionPlan(self, rqlst, args, cnx)
       
   524 
       
   525     @statsd_timeit
       
   526     def execute(self, cnx, rql, args=None, build_descr=True):
       
   527         """execute a rql query, return resulting rows and their description in
       
   528         a `ResultSet` object
       
   529 
       
   530         * `rql` should be a Unicode string or a plain ASCII string
       
   531         * `args` the optional parameters dictionary associated to the query
       
   532         * `build_descr` is a boolean flag indicating if the description should
       
   533           be built on select queries (if false, the description will be en empty
       
   534           list)
       
   535 
       
   536         on INSERT queries, there will be one row with the eid of each inserted
       
   537         entity
       
   538 
       
   539         result for DELETE and SET queries is undefined yet
       
   540 
       
   541         to maximize the rql parsing/analyzing cache performance, you should
       
   542         always use substitute arguments in queries (i.e. avoid query such as
       
   543         'Any X WHERE X eid 123'!)
       
   544         """
       
   545         if server.DEBUG & (server.DBG_RQL | server.DBG_SQL):
       
   546             if server.DEBUG & (server.DBG_MORE | server.DBG_SQL):
       
   547                 print('*'*80)
       
   548             print('querier input', repr(rql), repr(args))
       
   549         # parse the query and binds variables
       
   550         cachekey = (rql,)
       
   551         try:
       
   552             if args:
       
   553                 # search for named args in query which are eids (hence
       
   554                 # influencing query's solutions)
       
   555                 eidkeys = self._rql_ck_cache[rql]
       
   556                 if eidkeys:
       
   557                     # if there are some, we need a better cache key, eg (rql +
       
   558                     # entity type of each eid)
       
   559                     try:
       
   560                         cachekey = self._repo.querier_cache_key(cnx, rql,
       
   561                                                                 args, eidkeys)
       
   562                     except UnknownEid:
       
   563                         # we want queries such as "Any X WHERE X eid 9999"
       
   564                         # return an empty result instead of raising UnknownEid
       
   565                         return empty_rset(rql, args)
       
   566             rqlst = self._rql_cache[cachekey]
       
   567             self.cache_hit += 1
       
   568             statsd_c('cache_hit')
       
   569         except KeyError:
       
   570             self.cache_miss += 1
       
   571             statsd_c('cache_miss')
       
   572             rqlst = self.parse(rql)
       
   573             try:
       
   574                 # compute solutions for rqlst and return named args in query
       
   575                 # which are eids. Notice that if you may not need `eidkeys`, we
       
   576                 # have to compute solutions anyway (kept as annotation on the
       
   577                 # tree)
       
   578                 eidkeys = self.solutions(cnx, rqlst, args)
       
   579             except UnknownEid:
       
   580                 # we want queries such as "Any X WHERE X eid 9999" return an
       
   581                 # empty result instead of raising UnknownEid
       
   582                 return empty_rset(rql, args)
       
   583             if args and rql not in self._rql_ck_cache:
       
   584                 self._rql_ck_cache[rql] = eidkeys
       
   585                 if eidkeys:
       
   586                     cachekey = self._repo.querier_cache_key(cnx, rql, args,
       
   587                                                             eidkeys)
       
   588             self._rql_cache[cachekey] = rqlst
       
   589         if rqlst.TYPE != 'select':
       
   590             if cnx.read_security:
       
   591                 check_no_password_selected(rqlst)
       
   592             cachekey = None
       
   593         else:
       
   594             if cnx.read_security:
       
   595                 for select in rqlst.children:
       
   596                     check_no_password_selected(select)
       
   597                     check_relations_read_access(cnx, select, args)
       
   598             # on select query, always copy the cached rqlst so we don't have to
       
   599             # bother modifying it. This is not necessary on write queries since
       
   600             # a new syntax tree is built from them.
       
   601             rqlst = rqlst.copy()
       
   602             # Rewrite computed relations
       
   603             rewriter = RQLRelationRewriter(cnx)
       
   604             rewriter.rewrite(rqlst, args)
       
   605             self._annotate(rqlst)
       
   606             if args:
       
   607                 # different SQL generated when some argument is None or not (IS
       
   608                 # NULL). This should be considered when computing sql cache key
       
   609                 cachekey += tuple(sorted([k for k, v in args.items()
       
   610                                           if v is None]))
       
   611         # make an execution plan
       
   612         plan = self.plan_factory(rqlst, args, cnx)
       
   613         plan.cache_key = cachekey
       
   614         self._planner.build_plan(plan)
       
   615         # execute the plan
       
   616         try:
       
   617             results = plan.execute()
       
   618         except (Unauthorized, ValidationError):
       
   619             # getting an Unauthorized/ValidationError exception means the
       
   620             # transaction must be rolled back
       
   621             #
       
   622             # notes:
       
   623             # * we should not reset the connections set here, since we don't want the
       
   624             #   connection to loose it during processing
       
   625             # * don't rollback if we're in the commit process, will be handled
       
   626             #   by the connection
       
   627             if cnx.commit_state is None:
       
   628                 cnx.commit_state = 'uncommitable'
       
   629             raise
       
   630         # build a description for the results if necessary
       
   631         descr = ()
       
   632         if build_descr:
       
   633             if rqlst.TYPE == 'select':
       
   634                 # sample selection
       
   635                 if len(rqlst.children) == 1 and len(rqlst.children[0].solutions) == 1:
       
   636                     # easy, all lines are identical
       
   637                     selected = rqlst.children[0].selection
       
   638                     solution = rqlst.children[0].solutions[0]
       
   639                     description = _make_description(selected, args, solution)
       
   640                     descr = RepeatList(len(results), tuple(description))
       
   641                 else:
       
   642                     # hard, delegate the work :o)
       
   643                     descr = manual_build_descr(cnx, rqlst, args, results)
       
   644             elif rqlst.TYPE == 'insert':
       
   645                 # on insert plan, some entities may have been auto-casted,
       
   646                 # so compute description manually even if there is only
       
   647                 # one solution
       
   648                 basedescr = [None] * len(plan.selected)
       
   649                 todetermine = list(zip(range(len(plan.selected)), repeat(False)))
       
   650                 descr = _build_descr(cnx, results, basedescr, todetermine)
       
   651             # FIXME: get number of affected entities / relations on non
       
   652             # selection queries ?
       
   653         # return a result set object
       
   654         return ResultSet(results, rql, args, descr)
       
   655 
       
   656     # these are overridden by set_log_methods below
       
   657     # only defining here to prevent pylint from complaining
       
   658     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
       
   659 
       
   660 from logging import getLogger
       
   661 from cubicweb import set_log_methods
       
   662 LOGGER = getLogger('cubicweb.querier')
       
   663 set_log_methods(QuerierHelper, LOGGER)
       
   664 
       
   665 
       
   666 def manual_build_descr(cnx, rqlst, args, result):
       
   667     """build a description for a given result by analysing each row
       
   668 
       
   669     XXX could probably be done more efficiently during execution of query
       
   670     """
       
   671     # not so easy, looks for variable which changes from one solution
       
   672     # to another
       
   673     unstables = rqlst.get_variable_indices()
       
   674     basedescr = []
       
   675     todetermine = []
       
   676     for i in range(len(rqlst.children[0].selection)):
       
   677         ttype = _selection_idx_type(i, rqlst, args)
       
   678         if ttype is None or ttype == 'Any':
       
   679             ttype = None
       
   680             isfinal = True
       
   681         else:
       
   682             isfinal = ttype in BASE_TYPES
       
   683         if ttype is None or i in unstables:
       
   684             basedescr.append(None)
       
   685             todetermine.append( (i, isfinal) )
       
   686         else:
       
   687             basedescr.append(ttype)
       
   688     if not todetermine:
       
   689         return RepeatList(len(result), tuple(basedescr))
       
   690     return _build_descr(cnx, result, basedescr, todetermine)
       
   691 
       
   692 def _build_descr(cnx, result, basedescription, todetermine):
       
   693     description = []
       
   694     entity_metas = cnx.entity_metas
       
   695     todel = []
       
   696     for i, row in enumerate(result):
       
   697         row_descr = basedescription[:]
       
   698         for index, isfinal in todetermine:
       
   699             value = row[index]
       
   700             if value is None:
       
   701                 # None value inserted by an outer join, no type
       
   702                 row_descr[index] = None
       
   703                 continue
       
   704             if isfinal:
       
   705                 row_descr[index] = etype_from_pyobj(value)
       
   706             else:
       
   707                 try:
       
   708                     row_descr[index] = entity_metas(value)['type']
       
   709                 except UnknownEid:
       
   710                     cnx.error('wrong eid %s in repository, you should '
       
   711                              'db-check the database' % value)
       
   712                     todel.append(i)
       
   713                     break
       
   714         else:
       
   715             description.append(tuple(row_descr))
       
   716     for i in reversed(todel):
       
   717         del result[i]
       
   718     return description
       
   719 
       
   720 def _make_description(selected, args, solution):
       
   721     """return a description for a result set"""
       
   722     description = []
       
   723     for term in selected:
       
   724         description.append(term.get_type(solution, args))
       
   725     return description
       
   726 
       
   727 def _selection_idx_type(i, rqlst, args):
       
   728     """try to return type of term at index `i` of the rqlst's selection"""
       
   729     for select in rqlst.children:
       
   730         term = select.selection[i]
       
   731         for solution in select.solutions:
       
   732             try:
       
   733                 ttype = term.get_type(solution, args)
       
   734                 if ttype is not None:
       
   735                     return ttype
       
   736             except CoercionError:
       
   737                 return None