server/querier.py
changeset 0 b97547f5f1fa
child 321 247947250382
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """Helper classes to execute RQL queries on a set of sources, performing
       
     2 security checking and data aggregation.
       
     3 
       
     4 :organization: Logilab
       
     5 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 from itertools import repeat
       
    11 
       
    12 from logilab.common.cache import Cache
       
    13 from logilab.common.compat import any
       
    14 from rql import RQLHelper, RQLSyntaxError
       
    15 from rql.stmts import Union, Select
       
    16 from rql.nodes import (Relation, VariableRef, Constant, Exists, Variable,
       
    17                        SubQuery)
       
    18 
       
    19 from cubicweb import Unauthorized, QueryError, UnknownEid, typed_eid
       
    20 from cubicweb import server
       
    21 from cubicweb.rset import ResultSet
       
    22 
       
    23 from cubicweb.server.utils import cleanup_solutions
       
    24 from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
       
    25 from cubicweb.server.ssplanner import add_types_restriction
       
    26 
       
    27 def empty_rset(session, rql, args, rqlst=None):
       
    28     """build an empty result set object"""
       
    29     return ResultSet([], rql, args, rqlst=rqlst)
       
    30 
       
    31 def update_varmap(varmap, selected, table):
       
    32     """return a sql schema to store RQL query result"""
       
    33     for i, term in enumerate(selected):
       
    34         key = term.as_string()
       
    35         value = '%s.C%s' % (table, i)
       
    36         if varmap.get(key, value) != value:
       
    37             raise Exception('variable name conflict on %s' % key)
       
    38         varmap[key] = value
       
    39 
       
    40 # permission utilities ########################################################
       
    41 
       
    42 def var_kwargs(restriction, args):
       
    43     varkwargs = {}
       
    44     for rel in restriction.iget_nodes(Relation):
       
    45         cmp = rel.children[1]
       
    46         if rel.r_type == 'eid' and cmp.operator == '=' and \
       
    47                 isinstance(cmp.children[0], Constant) and \
       
    48                 cmp.children[0].type == 'Substitute':
       
    49             varkwargs[rel.children[0].name] = typed_eid(cmp.children[0].eval(args))
       
    50     return varkwargs
       
    51 
       
    52 def check_no_password_selected(rqlst):
       
    53     """check that Password entities are not selected"""
       
    54     for solution in rqlst.solutions:
       
    55         if 'Password' in solution.itervalues():
       
    56             raise Unauthorized('Password selection is not allowed')
       
    57 
       
    58 def check_read_access(schema, user, rqlst, solution):
       
    59     """check that the given user has credentials to access data read the
       
    60     query
       
    61 
       
    62     return a dict defining necessary local checks (due to use of rql expression
       
    63     in the schema), keys are variable names and values associated rql expression
       
    64     for the associated variable with the given solution
       
    65     """
       
    66     if rqlst.where is not None:
       
    67         for rel in rqlst.where.iget_nodes(Relation):
       
    68             # XXX has_text may have specific perm ?
       
    69             if rel.r_type in ('is', 'is_instance_of', 'has_text', 'identity', 'eid'):
       
    70                 continue
       
    71             if not schema.rschema(rel.r_type).has_access(user, 'read'):
       
    72                 raise Unauthorized('read', rel.r_type)
       
    73     localchecks = {}
       
    74     # iterate on defined_vars and not on solutions to ignore column aliases
       
    75     for varname in rqlst.defined_vars:
       
    76         etype = solution[varname]
       
    77         eschema = schema.eschema(etype)
       
    78         if not eschema.has_access(user, 'read'):
       
    79             erqlexprs = eschema.get_rqlexprs('read')
       
    80             if not erqlexprs:
       
    81                 ex = Unauthorized('read', etype)
       
    82                 ex.var = varname
       
    83                 raise ex
       
    84             #assert len(erqlexprs) == 1
       
    85             localchecks[varname] = tuple(erqlexprs)
       
    86     return localchecks
       
    87                     
       
    88 def noinvariant_vars(restricted, select, nbtrees):
       
    89     # a variable can actually be invariant if it has not been restricted for
       
    90     # security reason or if security assertion hasn't modified the possible
       
    91     # solutions for the query
       
    92     if nbtrees != 1:
       
    93         for vname in restricted:
       
    94             try:
       
    95                 yield select.defined_vars[vname]
       
    96             except KeyError:
       
    97                 # this is an alias
       
    98                 continue
       
    99     else:
       
   100         for vname in restricted:
       
   101             try:
       
   102                 var = select.defined_vars[vname]
       
   103             except KeyError:
       
   104                 # this is an alias
       
   105                 continue
       
   106             if len(var.stinfo['possibletypes']) != 1:
       
   107                 yield var
       
   108 
       
   109 def _expand_selection(terms, selected, aliases, select, newselect):
       
   110     for term in terms:
       
   111         for vref in term.iget_nodes(VariableRef):
       
   112             if not vref.name in selected:
       
   113                 select.append_selected(vref)
       
   114                 colalias = newselect.get_variable(vref.name, len(aliases))
       
   115                 aliases.append(VariableRef(colalias))
       
   116                 selected.add(vref.name)
       
   117                 
       
   118 # Plans #######################################################################
       
   119 
       
   120 class ExecutionPlan(object):
       
   121     """the execution model of a rql query, composed of querier steps"""
       
   122     
       
   123     def __init__(self, querier, rqlst, args, session):
       
   124         # original rql syntax tree
       
   125         self.rqlst = rqlst
       
   126         self.args = args or {}
       
   127         # session executing the query
       
   128         self.session = session
       
   129         # quick reference to the system source
       
   130         self.syssource = session.pool.source('system')
       
   131         # execution steps
       
   132         self.steps = []
       
   133         # index of temporary tables created during execution
       
   134         self.temp_tables = {}
       
   135         # various resource accesors
       
   136         self.querier = querier
       
   137         self.schema = querier.schema
       
   138         self.rqlhelper = querier._rqlhelper
       
   139         self.sqlannotate = querier.sqlgen_annotate
       
   140         
       
   141     def annotate_rqlst(self):
       
   142         if not self.rqlst.annotated:
       
   143             self.rqlhelper.annotate(self.rqlst)
       
   144             
       
   145     def add_step(self, step):
       
   146         """add a step to the plan"""
       
   147         self.steps.append(step)
       
   148 
       
   149     def clean(self):
       
   150         """remove temporary tables"""
       
   151         self.syssource.clean_temp_data(self.session, self.temp_tables)
       
   152         
       
   153     def sqlexec(self, sql, args=None):
       
   154         return self.syssource.sqlexec(self.session, sql, args)
       
   155             
       
   156     def execute(self):
       
   157         """execute a plan and return resulting rows"""
       
   158         try:
       
   159             for step in self.steps:
       
   160                 result = step.execute()
       
   161             # the latest executed step contains the full query result
       
   162             return result
       
   163         finally:
       
   164             self.clean()
       
   165             
       
   166     def init_temp_table(self, table, selected, sol):
       
   167         """initialize sql schema and variable map for a temporary table which
       
   168         will be used to store result for the given rqlst
       
   169         """
       
   170         try:
       
   171             outputmap, sqlschema, _ = self.temp_tables[table]
       
   172             update_varmap(outputmap, selected, table)
       
   173         except KeyError:
       
   174             sqlschema, outputmap = self.syssource.temp_table_def(selected, sol,
       
   175                                                                  table)
       
   176             self.temp_tables[table] = [outputmap, sqlschema, False]
       
   177         return outputmap
       
   178         
       
   179     def create_temp_table(self, table):
       
   180         """create a temporary table to store result for the given rqlst"""
       
   181         if not self.temp_tables[table][-1]:
       
   182             sqlschema = self.temp_tables[table][1]
       
   183             self.syssource.create_temp_table(self.session, table, sqlschema)
       
   184             self.temp_tables[table][-1] = True
       
   185         
       
   186     def preprocess(self, union, security=True):
       
   187         """insert security when necessary then annotate rql st for sql generation
       
   188         
       
   189         return rqlst to actually execute
       
   190         """
       
   191         #if server.DEBUG:
       
   192         #    print '------- preprocessing', union.as_string('utf8')
       
   193         noinvariant = set()
       
   194         if security and not self.session.is_super_session:
       
   195             self._insert_security(union, noinvariant)
       
   196         self.rqlhelper.simplify(union)
       
   197         self.sqlannotate(union)
       
   198         set_qdata(union, noinvariant)
       
   199         if union.has_text_query:
       
   200             self.cache_key = None
       
   201 
       
   202     def _insert_security(self, union, noinvariant):
       
   203         rh = self.rqlhelper
       
   204         for select in union.children[:]:
       
   205             for subquery in select.with_:
       
   206                 self._insert_security(subquery.query, noinvariant)
       
   207             localchecks, restricted = self._check_permissions(select)
       
   208             if any(localchecks):
       
   209                 rewrite = self.session.rql_rewriter.rewrite
       
   210                 nbtrees = len(localchecks)
       
   211                 myunion = union
       
   212                 # transform in subquery when len(localchecks)>1 and groups
       
   213                 if nbtrees > 1 and (select.orderby or select.groupby or
       
   214                                     select.having or select.has_aggregat or
       
   215                                     select.limit or select.offset):
       
   216                     newselect = Select()
       
   217                     # only select variables in subqueries
       
   218                     origselection = select.selection
       
   219                     select.select_only_variables()
       
   220                     select.has_aggregat = False
       
   221                     # create subquery first so correct node are used on copy
       
   222                     # (eg ColumnAlias instead of Variable)
       
   223                     aliases = [VariableRef(newselect.get_variable(vref.name, i))
       
   224                                for i, vref in enumerate(select.selection)]
       
   225                     selected = set(vref.name for vref in aliases)
       
   226                     # now copy original selection and groups
       
   227                     for term in origselection:
       
   228                         newselect.append_selected(term.copy(newselect))
       
   229                     if select.orderby:
       
   230                         newselect.set_orderby([s.copy(newselect) for s in select.orderby])
       
   231                         _expand_selection(select.orderby, selected, aliases, select, newselect)
       
   232                         select.orderby = () # XXX dereference?
       
   233                     if select.groupby:
       
   234                         newselect.set_groupby([g.copy(newselect) for g in select.groupby])
       
   235                         _expand_selection(select.groupby, selected, aliases, select, newselect)
       
   236                         select.groupby = () # XXX dereference?
       
   237                     if select.having:
       
   238                         newselect.set_having([g.copy(newselect) for g in select.having])
       
   239                         _expand_selection(select.having, selected, aliases, select, newselect)
       
   240                         select.having = () # XXX dereference?
       
   241                     if select.limit:
       
   242                         newselect.limit = select.limit
       
   243                         select.limit = None
       
   244                     if select.offset:
       
   245                         newselect.offset = select.offset
       
   246                         select.offset = 0
       
   247                     myunion = Union()
       
   248                     newselect.set_with([SubQuery(aliases, myunion)], check=False)
       
   249                     solutions = [sol.copy() for sol in select.solutions]
       
   250                     cleanup_solutions(newselect, solutions)
       
   251                     newselect.set_possible_types(solutions)
       
   252                     # if some solutions doesn't need rewriting, insert original
       
   253                     # select as first union subquery
       
   254                     if () in localchecks:
       
   255                         myunion.append(select)
       
   256                     # we're done, replace original select by the new select with
       
   257                     # subqueries (more added in the loop below)
       
   258                     union.replace(select, newselect)
       
   259                 elif not () in localchecks:
       
   260                     union.remove(select)
       
   261                 for lcheckdef, lchecksolutions in localchecks.iteritems():
       
   262                     if not lcheckdef:
       
   263                         continue
       
   264                     myrqlst = select.copy(solutions=lchecksolutions)
       
   265                     myunion.append(myrqlst)
       
   266                     # in-place rewrite + annotation / simplification
       
   267                     rewrite(myrqlst, lcheckdef, lchecksolutions, self.args)
       
   268                     noinvariant.update(noinvariant_vars(restricted, myrqlst, nbtrees))
       
   269                 if () in localchecks:
       
   270                     select.set_possible_types(localchecks[()])
       
   271                     add_types_restriction(self.schema, select)
       
   272                     noinvariant.update(noinvariant_vars(restricted, select, nbtrees))
       
   273 
       
   274     def _check_permissions(self, rqlst):
       
   275         """return a dict defining "local checks", e.g. RQLExpression defined in
       
   276         the schema that should be inserted in the original query
       
   277 
       
   278         solutions where a variable has a type which the user can't definitly read
       
   279         are removed, else if the user may read it (eg if an rql expression is
       
   280         defined for the "read" permission of the related type), the local checks
       
   281         dict for the solution is updated
       
   282         
       
   283         return a dict with entries for each different local check necessary,
       
   284         with associated solutions as value. A local check is defined by a list
       
   285         of 2-uple, with variable name as first item and the necessary rql
       
   286         expression as second item for each variable which has to be checked.
       
   287         So solutions which don't require local checks will be associated to
       
   288         the empty tuple key.
       
   289 
       
   290         note: rqlst should not have been simplified at this point
       
   291         """
       
   292         assert not self.session.is_super_session
       
   293         user = self.session.user
       
   294         schema = self.schema
       
   295         msgs = []
       
   296         # dictionnary of variables restricted for security reason
       
   297         localchecks = {}
       
   298         if rqlst.where is not None:
       
   299             varkwargs = var_kwargs(rqlst.where, self.args)
       
   300             neweids = self.session.query_data('neweids', ())
       
   301         else:
       
   302             varkwargs = None
       
   303         restricted_vars = set()
       
   304         newsolutions = []
       
   305         for solution in rqlst.solutions:
       
   306             try:
       
   307                 localcheck = check_read_access(schema, user, rqlst, solution)
       
   308             except Unauthorized, ex:
       
   309                 msg = 'remove %s from solutions since %s has no %s access to %s'
       
   310                 msg %= (solution, user.login, ex.args[0], ex.args[1])
       
   311                 msgs.append(msg)
       
   312                 LOGGER.info(msg)
       
   313             else:
       
   314                 newsolutions.append(solution)
       
   315                 if varkwargs:
       
   316                     # try to benefit of rqlexpr.check cache for entities which
       
   317                     # are specified by eid in query'args
       
   318                     for varname, eid in varkwargs.iteritems():
       
   319                         try:
       
   320                             rqlexprs = localcheck.pop(varname)
       
   321                         except KeyError:
       
   322                             continue
       
   323                         if eid in neweids:
       
   324                             continue
       
   325                         for rqlexpr in rqlexprs:
       
   326                             if rqlexpr.check(self.session, eid):
       
   327                                 break
       
   328                         else:
       
   329                             raise Unauthorized()
       
   330                 restricted_vars.update(localcheck)
       
   331                 localchecks.setdefault(tuple(localcheck.iteritems()), []).append(solution)
       
   332         # raise Unautorized exception if the user can't access to any solution
       
   333         if not newsolutions:
       
   334             raise Unauthorized('\n'.join(msgs))
       
   335         rqlst.set_possible_types(newsolutions)
       
   336         return localchecks, restricted_vars
       
   337 
       
   338     def finalize(self, select, solutions, insertedvars):
       
   339         rqlst = Union()
       
   340         rqlst.append(select)
       
   341         for mainvarname, rschema, newvarname in insertedvars:
       
   342             nvartype = str(rschema.objects(solutions[0][mainvarname])[0])
       
   343             for sol in solutions:
       
   344                 sol[newvarname] = nvartype
       
   345         select.clean_solutions(solutions)
       
   346         self.rqlhelper.annotate(rqlst)
       
   347         self.preprocess(rqlst, security=False)
       
   348         return rqlst
       
   349        
       
   350 class InsertPlan(ExecutionPlan):
       
   351     """an execution model specific to the INSERT rql query
       
   352     """
       
   353     
       
   354     def __init__(self, querier, rqlst, args, session):
       
   355         ExecutionPlan.__init__(self, querier, rqlst, args, session)
       
   356         # save originaly selected variable, we may modify this
       
   357         # dictionary for substitution (query parameters)
       
   358         self.selected = rqlst.selection
       
   359         # list of new or updated entities definition (utils.Entity)
       
   360         self.e_defs = [[]]
       
   361         # list of new relation definition (3-uple (from_eid, r_type, to_eid)
       
   362         self.r_defs = []
       
   363         # indexes to track entity definitions bound to relation definitions
       
   364         self._r_subj_index = {}
       
   365         self._r_obj_index = {}
       
   366         self._expanded_r_defs = {}
       
   367 
       
   368     def relation_definitions(self, rqlst, to_build):
       
   369         """add constant values to entity def, mark variables to be selected
       
   370         """
       
   371         to_select = {}
       
   372         for relation in rqlst.main_relations:
       
   373             lhs, rhs = relation.get_variable_parts()
       
   374             rtype = relation.r_type
       
   375             if rtype in ('eid', 'has_text', 'is', 'is_instance_of', 'identity'):
       
   376                 raise QueryError("can't assign to %s" % rtype)
       
   377             try:
       
   378                 edef = to_build[str(lhs)]
       
   379             except KeyError:
       
   380                 # lhs var is not to build, should be selected and added as an
       
   381                 # object relation
       
   382                 edef = to_build[str(rhs)]
       
   383                 to_select.setdefault(edef, []).append((rtype, lhs, 1))
       
   384             else:
       
   385                 if isinstance(rhs, Constant) and not rhs.uid:
       
   386                     # add constant values to entity def
       
   387                     value = rhs.eval(self.args)
       
   388                     eschema = edef.e_schema
       
   389                     attrtype = eschema.subject_relation(rtype).objects(eschema)[0]
       
   390                     if attrtype == 'Password' and isinstance(value, unicode): 
       
   391                         value = value.encode('UTF8')
       
   392                     edef[rtype] = value
       
   393                 elif to_build.has_key(str(rhs)):
       
   394                     # create a relation between two newly created variables
       
   395                     self.add_relation_def((edef, rtype, to_build[rhs.name]))
       
   396                 else:
       
   397                     to_select.setdefault(edef, []).append( (rtype, rhs, 0) )
       
   398         return to_select
       
   399 
       
   400         
       
   401     def add_entity_def(self, edef):
       
   402         """add an entity definition to build"""
       
   403         edef.querier_pending_relations = {}
       
   404         self.e_defs[-1].append(edef)
       
   405         
       
   406     def add_relation_def(self, rdef):
       
   407         """add an relation definition to build"""
       
   408         self.r_defs.append(rdef)
       
   409         if not isinstance(rdef[0], int):
       
   410             self._r_subj_index.setdefault(rdef[0], []).append(rdef)
       
   411         if not isinstance(rdef[2], int):
       
   412             self._r_obj_index.setdefault(rdef[2], []).append(rdef)
       
   413         
       
   414     def substitute_entity_def(self, edef, edefs):
       
   415         """substitute an incomplete entity definition by a list of complete
       
   416         equivalents
       
   417         
       
   418         e.g. on queries such as ::
       
   419           INSERT Personne X, Societe Y: X nom N, Y nom 'toto', X travaille Y
       
   420           WHERE U login 'admin', U login N
       
   421 
       
   422         X will be inserted as many times as U exists, and so the X travaille Y
       
   423         relations as to be added as many time as X is inserted
       
   424         """
       
   425         if not edefs or not self.e_defs:
       
   426             # no result, no entity will be created
       
   427             self.e_defs = ()
       
   428             return
       
   429         # first remove the incomplete entity definition
       
   430         colidx = self.e_defs[0].index(edef)
       
   431         for i, row in enumerate(self.e_defs[:]):
       
   432             self.e_defs[i][colidx] = edefs[0]
       
   433             samplerow = self.e_defs[i]
       
   434             for edef in edefs[1:]:
       
   435                 row = samplerow[:]
       
   436                 row[colidx] = edef
       
   437                 self.e_defs.append(row)
       
   438         # now, see if this entity def is referenced as subject in some relation
       
   439         # definition
       
   440         if self._r_subj_index.has_key(edef):
       
   441             for rdef in self._r_subj_index[edef]:
       
   442                 expanded = self._expanded(rdef)
       
   443                 result = []
       
   444                 for exp_rdef in expanded:
       
   445                     for edef in edefs:
       
   446                         result.append( (edef, exp_rdef[1], exp_rdef[2]) )
       
   447                 self._expanded_r_defs[rdef] = result
       
   448         # and finally, see if this entity def is referenced as object in some
       
   449         # relation definition
       
   450         if self._r_obj_index.has_key(edef):
       
   451             for rdef in self._r_obj_index[edef]:
       
   452                 expanded = self._expanded(rdef)
       
   453                 result = []
       
   454                 for exp_rdef in expanded:
       
   455                     for edef in edefs:
       
   456                         result.append( (exp_rdef[0], exp_rdef[1], edef) )
       
   457                 self._expanded_r_defs[rdef] = result
       
   458         
       
   459     def _expanded(self, rdef):
       
   460         """return expanded value for the given relation definition"""
       
   461         try:
       
   462             return self._expanded_r_defs[rdef]
       
   463         except KeyError:
       
   464             self.r_defs.remove(rdef)
       
   465             return [rdef]
       
   466         
       
   467     def relation_defs(self):
       
   468         """return the list for relation definitions to insert"""
       
   469         for rdefs in self._expanded_r_defs.values():
       
   470             for rdef in rdefs:
       
   471                 yield rdef
       
   472         for rdef in self.r_defs:
       
   473             yield rdef
       
   474             
       
   475     def insert_entity_defs(self):
       
   476         """return eids of inserted entities in a suitable form for the resulting
       
   477         result set, e.g.:
       
   478         
       
   479         e.g. on queries such as ::
       
   480           INSERT Personne X, Societe Y: X nom N, Y nom 'toto', X travaille Y
       
   481           WHERE U login 'admin', U login N
       
   482 
       
   483         if there is two entities matching U, the result set will look like
       
   484         [(eidX1, eidY1), (eidX2, eidY2)]
       
   485         """
       
   486         session = self.session
       
   487         repo = session.repo
       
   488         results = []
       
   489         for row in self.e_defs:
       
   490             results.append([repo.glob_add_entity(session, edef)
       
   491                             for edef in row])
       
   492         return results
       
   493         
       
   494     def insert_relation_defs(self):
       
   495         session = self.session
       
   496         repo = session.repo
       
   497         for subj, rtype, obj in self.relation_defs():
       
   498             # if a string is given into args instead of an int, we get it here
       
   499             if isinstance(subj, basestring):
       
   500                 subj = typed_eid(subj)
       
   501             elif not isinstance(subj, (int, long)):
       
   502                 subj = subj.eid
       
   503             if isinstance(obj, basestring):
       
   504                 obj = typed_eid(obj)
       
   505             elif not isinstance(obj, (int, long)):
       
   506                 obj = obj.eid
       
   507             if repo.schema.rschema(rtype).inlined:
       
   508                 entity = session.eid_rset(subj).get_entity(0, 0)
       
   509                 entity[rtype] = obj
       
   510                 repo.glob_update_entity(session, entity)
       
   511             else:
       
   512                 repo.glob_add_relation(session, subj, rtype, obj)
       
   513 
       
   514 
       
   515 class QuerierHelper(object):
       
   516     """helper class to execute rql queries, putting all things together"""
       
   517     
       
   518     def __init__(self, repo, schema):
       
   519         # system info helper
       
   520         self._repo = repo
       
   521         # application schema
       
   522         self.set_schema(schema)
       
   523         
       
   524     def set_schema(self, schema):
       
   525         self.schema = schema
       
   526         # rql parsing / analysing helper
       
   527         self._rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid',
       
   528                                                                'has_text': 'fti'})        
       
   529         self._rql_cache = Cache(self._repo.config['rql-cache-size'])
       
   530         self.cache_hit, self.cache_miss = 0, 0
       
   531         # rql planner
       
   532         # note: don't use repo.sources, may not be built yet, and also "admin"
       
   533         #       isn't an actual source
       
   534         if len([uri for uri in self._repo.config.sources() if uri != 'admin']) < 2:
       
   535             from cubicweb.server.ssplanner import SSPlanner
       
   536             self._planner = SSPlanner(schema, self._rqlhelper)
       
   537         else:
       
   538             from cubicweb.server.msplanner import MSPlanner            
       
   539             self._planner = MSPlanner(schema, self._rqlhelper)
       
   540         # sql generation annotator
       
   541         self.sqlgen_annotate = SQLGenAnnotator(schema).annotate
       
   542         
       
   543     def parse(self, rql, annotate=False):
       
   544         """return a rql syntax tree for the given rql"""
       
   545         try:
       
   546             return self._rqlhelper.parse(unicode(rql), annotate=annotate)
       
   547         except UnicodeError:
       
   548             raise RQLSyntaxError(rql)
       
   549 
       
   550     def solutions(self, session, rqlst, args):
       
   551         assert session is not None
       
   552         def type_from_eid(eid, type_from_eid=self._repo.type_from_eid,
       
   553                           session=session):
       
   554             return type_from_eid(eid, session)
       
   555         self._rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
       
   556 
       
   557     def plan_factory(self, rqlst, args, session):
       
   558         """create an execution plan for an INSERT RQL query"""
       
   559         if rqlst.TYPE == 'insert':
       
   560             return InsertPlan(self, rqlst, args, session)
       
   561         return ExecutionPlan(self, rqlst, args, session)
       
   562         
       
   563     def execute(self, session, rql, args=None, eid_key=None, build_descr=True):
       
   564         """execute a rql query, return resulting rows and their description in
       
   565         a `ResultSet` object
       
   566 
       
   567         * `rql` should be an unicode string or a plain ascii string
       
   568         * `args` the optional parameters dictionary associated to the query
       
   569         * `build_descr` is a boolean flag indicating if the description should
       
   570           be built on select queries (if false, the description will be en empty
       
   571           list)
       
   572         * `eid_key` must be both a key in args and a substitution in the rql
       
   573           query. It should be used to enhance cacheability of rql queries.
       
   574           It may be a tuple for keys in args.
       
   575           eid_key must be providen in case where a eid substitution is providen
       
   576           and resolve some ambiguity in the possible solutions infered for each
       
   577           variable in the query.
       
   578 
       
   579         on INSERT queries, there will be on row with the eid of each inserted
       
   580         entity
       
   581         
       
   582         result for DELETE and SET queries is undefined yet
       
   583 
       
   584         to maximize the rql parsing/analyzing cache performance, you should
       
   585         always use substitute arguments in queries (eg avoid query such as
       
   586         'Any X WHERE X eid 123'!)
       
   587         """
       
   588         if server.DEBUG:
       
   589             print '*'*80
       
   590             print rql
       
   591         # parse the query and binds variables
       
   592         if eid_key is not None:
       
   593             if not isinstance(eid_key, (tuple, list)):
       
   594                 eid_key = (eid_key,)
       
   595             cachekey = [rql]
       
   596             for key in eid_key:
       
   597                 try:
       
   598                     etype = self._repo.type_from_eid(args[key], session)
       
   599                 except KeyError:
       
   600                     raise QueryError('bad cache key %s (no value)' % key)
       
   601                 except TypeError:
       
   602                     raise QueryError('bad cache key %s (value: %r)' % (key, args[key]))
       
   603                 except UnknownEid:
       
   604                     # we want queries such as "Any X WHERE X eid 9999"
       
   605                     # return an empty result instead of raising UnknownEid
       
   606                     return empty_rset(session, rql, args)
       
   607                 cachekey.append(etype)
       
   608             cachekey = tuple(cachekey)
       
   609         else:
       
   610             cachekey = rql
       
   611         try:
       
   612             rqlst = self._rql_cache[cachekey]
       
   613             self.cache_hit += 1
       
   614         except KeyError:
       
   615             self.cache_miss += 1
       
   616             rqlst = self.parse(rql)
       
   617             try:
       
   618                 self.solutions(session, rqlst, args)
       
   619             except UnknownEid:
       
   620                 # we want queries such as "Any X WHERE X eid 9999"
       
   621                 # return an empty result instead of raising UnknownEid
       
   622                 return empty_rset(session, rql, args, rqlst)
       
   623             self._rql_cache[cachekey] = rqlst
       
   624         orig_rqlst = rqlst
       
   625         if not rqlst.TYPE == 'select':
       
   626             if not session.is_super_session:
       
   627                 check_no_password_selected(rqlst)
       
   628             # write query, ensure session's mode is 'write' so connections
       
   629             # won't be released until commit/rollback
       
   630             session.mode = 'write'
       
   631             cachekey = None
       
   632         else:
       
   633             if not session.is_super_session:
       
   634                 for select in rqlst.children:
       
   635                     check_no_password_selected(select)
       
   636             # on select query, always copy the cached rqlst so we don't have to
       
   637             # bother modifying it. This is not necessary on write queries since
       
   638             # a new syntax tree is built from them.
       
   639             rqlst = rqlst.copy()
       
   640             self._rqlhelper.annotate(rqlst)
       
   641         # make an execution plan
       
   642         plan = self.plan_factory(rqlst, args, session)
       
   643         plan.cache_key = cachekey
       
   644         self._planner.build_plan(plan)
       
   645         # execute the plan
       
   646         try:
       
   647             results = plan.execute()
       
   648         except Unauthorized:
       
   649             # XXX this could be done in security's after_add_relation hooks
       
   650             # since it's actually realy only needed there (other relations
       
   651             # security is done *before* actual changes, and add/update entity
       
   652             # security is done after changes but in an operation, and exception
       
   653             # generated in operation's events  properly generate a rollback on
       
   654             # the session). Even though, this is done here for a better
       
   655             # consistency: getting an Unauthorized exception means the
       
   656             # transaction has been rollbacked
       
   657             session.rollback()
       
   658             raise
       
   659         # build a description for the results if necessary
       
   660         descr = ()
       
   661         if build_descr:
       
   662             if rqlst.TYPE == 'select':
       
   663                 # sample selection
       
   664                 descr = session.build_description(orig_rqlst, args, results)
       
   665             elif rqlst.TYPE == 'insert':
       
   666                 # on insert plan, some entities may have been auto-casted,
       
   667                 # so compute description manually even if there is only
       
   668                 # one solution
       
   669                 basedescr = [None] * len(plan.selected)
       
   670                 todetermine = zip(xrange(len(plan.selected)), repeat(False))
       
   671                 descr = session._build_descr(results, basedescr, todetermine)
       
   672             # FIXME: get number of affected entities / relations on non
       
   673             # selection queries ?
       
   674         # return a result set object
       
   675         return ResultSet(results, rql, args, descr, eid_key, orig_rqlst)
       
   676 
       
   677 from logging import getLogger
       
   678 from cubicweb import set_log_methods
       
   679 LOGGER = getLogger('cubicweb.querier')
       
   680 set_log_methods(QuerierHelper, LOGGER)