[rqlrewrite] Introduce scaffolding for annotable query arguments

This change introduces the ability to annotate query arguments so that the type identification of an argument can be cached.

authorLaurent Wouters <lwouters@cenotelie.fr>
changeseta6f0758c32df
branchdefault
phasedraft
hiddenno
parent revision#e08d8e171238 Merge 3.26
child revision#f22be300dee1 [rql] Introduces the new security model for RQL statements
files modified by this revision
cubicweb/cwvreg.py
cubicweb/rqlrewrite.py
cubicweb/server/querier.py
# HG changeset patch
# User Laurent Wouters <lwouters@cenotelie.fr>
# Date 1524238558 -7200
# Fri Apr 20 17:35:58 2018 +0200
# Node ID a6f0758c32df019a997d1b22ea10c390359c2e85
# Parent e08d8e1712380c6fb0e7657b870d2d409b33f2df
[rqlrewrite] Introduce scaffolding for annotable query arguments

This change introduces the ability to annotate query arguments so that the type
identification of an argument can be cached.

diff --git a/cubicweb/cwvreg.py b/cubicweb/cwvreg.py
@@ -37,10 +37,11 @@
1 
2  from cubicweb import _
3  from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER,
4                        onevent, Binary, UnknownProperty, UnknownEid)
5  from cubicweb.predicates import appobject_selectable, _reset_is_instance_cache
6 +from cubicweb.rqlrewrite import ArgumentValue
7 
8 
9  @onevent('before-registry-reload')
10  def cleanup_uicfg_compat():
11      """ backward compat: those modules are now refering to app objects in
@@ -569,11 +570,15 @@
12          return RQLHelper(self.schema,
13                           special_relations={'eid': 'uid', 'has_text': 'fti'})
14 
15      def solutions(self, req, rqlst, args):
16          def type_from_eid(eid, req=req):
17 -            return req.entity_type(eid)
18 +            if isinstance(eid, ArgumentValue):
19 +                if eid.type_hint is None:
20 +                    eid.type_hint = req.entity_type(eid.value)
21 +                return eid.type_hint
22 +            return req.entity_type(int(eid))
23          return self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
24 
25      def parse(self, req, rql, args=None):
26          rqlst = self.rqlhelper.parse(rql)
27          try:
diff --git a/cubicweb/rqlrewrite.py b/cubicweb/rqlrewrite.py
@@ -32,10 +32,52 @@
28  from logilab.common.graph import has_path
29 
30  from cubicweb import Unauthorized
31 
32 
33 +class AnnotatedArguments(dict):
34 +    """
35 +    Query arguments that can be annotated
36 +    """
37 +
38 +    def __init__(self, original):
39 +        super(AnnotatedArguments, self).__init__()
40 +        for k, v in original.items():
41 +            self[k] = ArgumentValue(v)
42 +
43 +    def to_simple(self):
44 +        """
45 +        Gets the simple version of this dictionary of arguments
46 +        :return: The naked arguments
47 +        """
48 +        result = {}
49 +        for k, v in self.items():
50 +            result[k] = v.value
51 +        return result
52 +
53 +
54 +class ArgumentValue:
55 +    """
56 +    Represents the value of an argument with some meta-data
57 +    This structure is used for rewriting purposes
58 +    """
59 +    def __init__(self, value, type_hint=None):
60 +        """
61 +        Initializes this value
62 +        :param value: The
63 +        """
64 +        self.value = value
65 +        self.type_hint = type_hint
66 +
67 +    def __int__(self):
68 +        """
69 +        Coerces the value of this argument to int
70 +        :return: The integer value
71 +        """
72 +        return int(self.value)
73 +
74 +
75  def cleanup_solutions(rqlst, solutions):
76      for sol in solutions:
77          for vname in list(sol):
78              if not (vname in rqlst.defined_vars or vname in rqlst.aliases):
79                  del sol[vname]
@@ -273,12 +315,15 @@
80                applied
81 
82          noinvariant: set of variable names that can't be considered has
83                invariant due to security reason (will be filed by this method)
84          """
85 +        # translate the arguments
86 +        kwargs = AnnotatedArguments(kwargs)
87          nbtrees = len(localchecks)
88          myunion = union = select.parent
89 +        result = []
90          # transform in subquery when len(localchecks)>1 and groups
91          if nbtrees > 1 and (select.orderby or select.groupby or
92                              select.having or select.has_aggregat or
93                              select.distinct or
94                              select.limit or select.offset):
@@ -343,26 +388,29 @@
95              if () in localchecks:
96                  myunion.append(select)
97              # we're done, replace original select by the new select with
98              # subqueries (more added in the loop below)
99              union.replace(select, newselect)
100 +            result.append(newselect)
101          elif not () in localchecks:
102              union.remove(select)
103          for lcheckdef, lchecksolutions in localchecks.items():
104              if not lcheckdef:
105                  continue
106              myrqlst = select.copy(solutions=lchecksolutions)
107              myunion.append(myrqlst)
108 +            result.append(myrqlst)
109              # in-place rewrite + annotation / simplification
110              lcheckdef = [({v: 'X'}, rqlexprs) for v, rqlexprs in lcheckdef]
111              self.rewrite(myrqlst, lcheckdef, kwargs)
112              _add_noinvariant(noinvariant, restricted, myrqlst, nbtrees)
113          if () in localchecks:
114              select.set_possible_types(localchecks[()])
115              add_types_restriction(self.schema, select)
116              _add_noinvariant(noinvariant, restricted, select, nbtrees)
117          self.annotate(union)
118 +        return result, kwargs.to_simple()
119 
120      def rewrite(self, select, snippets, kwargs, existingvars=None):
121          """
122          snippets: (varmap, list of rql expression)
123                    with varmap a *dict* {select var: snippet var}
@@ -639,11 +687,11 @@
124              argname = subselect.allocate_varname()
125              while argname in self.kwargs:
126                  argname = subselect.allocate_varname()
127              subselect.add_constant_restriction(subselect.get_variable(self.u_varname),
128                                                 'eid', text_type(argname), 'Substitute')
129 -            self.kwargs[argname] = self.session.user.eid
130 +            self.kwargs[argname] = ArgumentValue(self.session.user.eid, "CWUser")
131          add_types_restriction(self.schema, subselect, subselect,
132                                solutions=self.solutions)
133          myunion = stmts.Union()
134          myunion.append(subselect)
135          aliases = [n.VariableRef(self.select.get_variable(name, i))
@@ -794,11 +842,11 @@
136                      argname = stmt.allocate_varname()
137                  # insert "U eid %(u)s"
138                  stmt.add_constant_restriction(
139                      stmt.get_variable(self.u_varname),
140                      'eid', text_type(argname), 'Substitute')
141 -                self.kwargs[argname] = self.session.user.eid
142 +                self.kwargs[argname] = ArgumentValue(self.session.user.eid, "CWUser")
143              return self.u_varname
144          key = (self.current_expr, self.varmap, vname)
145          try:
146              return self.rewritten[key]
147          except KeyError:
diff --git a/cubicweb/server/querier.py b/cubicweb/server/querier.py
@@ -229,11 +229,11 @@
148          for select in union.children[:]:
149              for subquery in select.with_:
150                  self._insert_security(subquery.query)
151              localchecks, restricted = self._check_permissions(select)
152              if any(localchecks):
153 -                self.cnx.rql_rewriter.insert_local_checks(
154 +                rewritten, self.args = self.cnx.rql_rewriter.insert_local_checks(
155                      select, self.args, localchecks, restricted, noinvariant)
156          return noinvariant
157 
158      def _check_permissions(self, rqlst):
159          """Return a dict defining "local checks", i.e. RQLExpression defined in