[CWEP003] Implementation of FROM clause in grammar, closes #242754

This patch:

  • implements the notion of Rql 'FROM' function in subqueries:

    Any X WHERE X FROM FOO("....")

  • update checking and parsing accordingly;

Specs may be found here: http://www.cubicweb.org/card/cwep-0003

authorVincent Michel <vincent.michel@logilab.fr>
changesetd30a8a33ac04
branchdefault
phasedraft
hiddenno
parent revision#e3a69da9c6d0 Added tag rql-version-0.33.0, rql-debian-version-0.33.0-1, rql-centos-version-0.33.0-1 for changeset 659a6b26eedc
child revision<not specified>
files modified by this revision
__init__.py
analyze.py
nodes.py
parser.g
parser.py
stcheck.py
stmts.py
test/unittest_analyze.py
test/unittest_nodes.py
test/unittest_stcheck.py
utils.py
# HG changeset patch
# User Vincent Michel <vincent.michel@logilab.fr>
# Date 1405420601 0
# Tue Jul 15 10:36:41 2014 +0000
# Node ID d30a8a33ac047dc95cd30d22bea64b647ee712f3
# Parent e3a69da9c6d035d6a58b5ae34178e20d0071386c
[CWEP003] Implementation of FROM clause in grammar, closes #242754

This patch:

* implements the notion of Rql 'FROM' function in subqueries:

Any X WHERE X FROM FOO("....")

* update checking and parsing accordingly;

Specs may be found here: http://www.cubicweb.org/card/cwep-0003

diff --git a/__init__.py b/__init__.py
@@ -134,10 +134,13 @@
1                  self._simplify(select)
2 
3      def _simplify(self, select):
4          # recurse on subqueries first
5          for subquery in select.with_:
6 +            # Do not simplify FROM function
7 +            if subquery.fromfunc:
8 +                continue
9              for subselect in subquery.query.children:
10                  self._simplify(subselect)
11          rewritten = False
12          for var in select.defined_vars.values():
13              stinfo = var.stinfo
diff --git a/analyze.py b/analyze.py
@@ -443,16 +443,21 @@
14      def visit_select(self, node):
15          if not (node.defined_vars or node.aliases):
16              node.set_possible_types([{}])
17              return
18          for subquery in node.with_: # resolve subqueries first
19 -            self.visit_union(subquery.query)
20 +            if subquery.query:
21 +                # Only visit subqueries with query
22 +                self.visit_union(subquery.query)
23          constraints = self._init_stmt(node)
24          for ca in node.aliases.itervalues():
25 -            etypes = set(stmt.selection[ca.colnum].get_type(sol, self.kwargs)
26 -                         for stmt in ca.query.children for sol in stmt.solutions)
27 -            constraints.add_var( ca.name, etypes )
28 +            if ca.query:
29 +                etypes = set(stmt.selection[ca.colnum].get_type(sol, self.kwargs)
30 +                             for stmt in ca.query.children for sol in stmt.solutions)
31 +                constraints.add_var( ca.name, etypes )
32 +            else:
33 +                constraints.add_var( ca.name, nodes.CONSTANT_TYPES)
34          constraints.end_domain_definition()
35          if self.uid_func:
36              # check rewritten uid const
37              for consts in node.stinfo['rewritten'].values():
38                  if not consts:
diff --git a/nodes.py b/nodes.py
@@ -242,39 +242,82 @@
39 
40  # base RQL nodes ##############################################################
41 
42  class SubQuery(BaseNode):
43      """WITH clause"""
44 -    __slots__ = ('aliases', 'query')
45 -    def __init__(self, aliases=None, query=None):
46 +    __slots__ = ('aliases', 'query', 'fromfunc', 'join_variables')
47 +
48 +    def __init__(self, aliases=None, query=None, fromfunc=None, join_variables=()):
49 +        self.query = None
50 +        self.fromfunc = None
51 +        self.join_variables = join_variables
52 +        if query and join_variables:
53 +            raise RuntimeError('Join variables are only supported with FROM subqueries')
54          if aliases is not None:
55              self.set_aliases(aliases)
56          if query is not None:
57              self.set_query(query)
58 +        if fromfunc is not None:
59 +            self.set_fromfunc(fromfunc)
60 +
61 +    def set_join_variables(self, join_variables):
62 +        if join_variables:
63 +            if self.query:
64 +                raise RuntimeError('Join variables are only supported with FROM subqueries')
65 +            # Do not override already existing join_variables (e.g. from copy())
66 +            # if there are no new join_variables
67 +            self.join_variables = join_variables
68 
69      def set_aliases(self, aliases):
70          self.aliases = aliases
71          for node in aliases:
72              node.parent = self
73 
74 +    def set_global_query(self, node):
75 +        if type(node) == Function:
76 +            self.set_fromfunc(node)
77 +        else: # Normal case
78 +            self.set_query(node)
79 +        node.parent = self
80 +
81      def set_query(self, node):
82 +        assert self.fromfunc is None
83          self.query = node
84          node.parent = self
85 
86 +    def set_fromfunc(self, node):
87 +        assert self.query is None
88 +        self.fromfunc = node
89 +        node.parent = self
90 +
91      def copy(self, stmt):
92 -        return SubQuery([v.copy(stmt) for v in self.aliases], self.query.copy())
93 +        return SubQuery([v.copy(stmt) for v in self.aliases],
94 +                        query=self.query.copy() if self.query else None,
95 +                        fromfunc=self.fromfunc.copy(stmt) if self.fromfunc else None,
96 +                        join_variables=self.join_variables)
97 
98      @property
99      def children(self):
100 -        return self.aliases + [self.query]
101 +        if self.query:
102 +            return self.aliases + [self.query]
103 +        else:
104 +            return self.aliases
105 
106      def as_string(self, encoding=None, kwargs=None):
107 -        return '%s BEING (%s)' % (','.join(v.name for v in self.aliases),
108 -                                  self.query.as_string(encoding, kwargs))
109 +        if self.query:
110 +            return '%s BEING (%s)' % (','.join(v.name for v in self.aliases),
111 +                                      self.query.as_string(encoding, kwargs))
112 +        if self.fromfunc:
113 +            return '%s FROM %s' % (','.join(v.name for v in self.aliases),
114 +                                   self.fromfunc.as_string(encoding, kwargs))
115      def __repr__(self):
116 -        return '%s BEING (%s)' % (','.join(repr(v) for v in self.aliases),
117 -                                  repr(self.query))
118 +        if self.query:
119 +            return '%s BEING (%s)' % (','.join(repr(v) for v in self.aliases),
120 +                                      repr(self.query))
121 +        if self.fromfunc:
122 +            return '%s FROM (%s)' % (','.join(repr(v) for v in self.aliases),
123 +                                     repr(self.fromfunc))
124 
125  class And(BinaryNode):
126      """a logical AND node (binary)"""
127      __slots__ = ()
128 
@@ -970,11 +1013,11 @@
129          except KeyError:
130              self.stinfo['optrelations'] = set((relation,))
131 
132      def get_type(self, solution=None, kwargs=None):
133          """return entity type of this object, 'Any' if not found"""
134 -        if solution:
135 +        if solution and self.name in solution:
136              return solution[self.name]
137          if self.stinfo['typerel']:
138              rhs = self.stinfo['typerel'].children[1].children[0]
139              if isinstance(rhs, Constant):
140                  return str(rhs.value)
@@ -1082,11 +1125,12 @@
141          return 'alias %s' % self.name
142 
143      def get_type(self, solution=None, kwargs=None):
144          """return entity type of this object, 'Any' if not found"""
145          vtype = super(ColumnAlias, self).get_type(solution, kwargs)
146 -        if vtype == 'Any':
147 +        if vtype == 'Any' and self.query:
148 +            # self.query could be None with FROM clause
149              for select in self.query.children:
150                  vtype = select.selection[self.colnum].get_type(solution, kwargs)
151                  if vtype != 'Any':
152                      return vtype
153          return vtype
diff --git a/parser.g b/parser.g
@@ -70,10 +70,12 @@
154      token UNION:       r'(?i)UNION'
155      token DISTINCT:    r'(?i)DISTINCT'
156      token WITH:        r'(?i)WITH'
157      token WHERE:       r'(?i)WHERE'
158      token BEING:       r'(?i)BEING'
159 +    token FROM:        r'(?i)FROM'
160 +    token JOINON:      r'(?i)JOIN ON'
161      token OR:          r'(?i)OR'
162      token AND:         r'(?i)AND'
163      token NOT:         r'(?i)NOT'
164      token GROUPBY:     r'(?i)GROUPBY'
165      token HAVING:      r'(?i)HAVING'
@@ -186,13 +188,18 @@
166                   subquery<<S>>       {{ nodes.append(subquery) }}
167                   ( ',' subquery<<S>> {{ nodes.append(subquery) }}
168                   )*                  {{ S.set_with(nodes) }}
169                 |
170 
171 -rule subquery<<S>>: variables<<S>>                     {{ node = SubQuery() ; node.set_aliases(variables) }}
172 -                    BEING r"\(" union<<Union()>> r"\)" {{ node.set_query(union); return node }}
173 +rule subquery<<S>>: variables<<S>>                     {{ node = SubQuery() ; join_vars=[]; node.set_aliases(variables) }}
174 +                    subquery_expr<<S>>                 {{ node.set_global_query(subquery_expr)}}
175 +                    (JOINON r"\(" variables<<S>> r"\)" {{ join_vars = variables}}
176 +                    )?
177 +                                                       {{node.set_join_variables(join_vars); return node}}
178 
179 +rule subquery_expr<<S>>:  BEING r"\(" union<<Union()>> r"\)"     {{ return union }}
180 +                          | FROM func <<S>>                      {{ return func }}
181 
182  rule sort_term<<S>>: expr_add<<S>> sort_meth {{ return SortTerm(expr_add, sort_meth) }}
183 
184 
185  rule sort_meth: SORT_DESC {{ return 0 }}
diff --git a/parser.py b/parser.py
@@ -77,10 +77,12 @@
186          ('UNION', re.compile('(?i)UNION')),
187          ('DISTINCT', re.compile('(?i)DISTINCT')),
188          ('WITH', re.compile('(?i)WITH')),
189          ('WHERE', re.compile('(?i)WHERE')),
190          ('BEING', re.compile('(?i)BEING')),
191 +        ('FROM', re.compile('(?i)FROM')),
192 +        ('JOINON', re.compile('(?i)JOIN ON')),
193          ('OR', re.compile('(?i)OR')),
194          ('AND', re.compile('(?i)AND')),
195          ('NOT', re.compile('(?i)NOT')),
196          ('GROUPBY', re.compile('(?i)GROUPBY')),
197          ('HAVING', re.compile('(?i)HAVING')),
@@ -288,16 +290,34 @@
198              pass
199 
200      def subquery(self, S, _parent=None):
201          _context = self.Context(_parent, self._scanner, 'subquery', [S])
202          variables = self.variables(S, _context)
203 -        node = SubQuery() ; node.set_aliases(variables)
204 -        BEING = self._scan('BEING', context=_context)
205 -        self._scan('r"\\("', context=_context)
206 -        union = self.union(Union(), _context)
207 -        self._scan('r"\\)"', context=_context)
208 -        node.set_query(union); return node
209 +        node = SubQuery() ; join_vars=[]; node.set_aliases(variables)
210 +        subquery_expr = self.subquery_expr(S, _context)
211 +        node.set_global_query(subquery_expr)
212 +        if self._peek('JOINON', "','", 'r"\\)"', "';'", context=_context) == 'JOINON':
213 +            JOINON = self._scan('JOINON', context=_context)
214 +            self._scan('r"\\("', context=_context)
215 +            variables = self.variables(S, _context)
216 +            self._scan('r"\\)"', context=_context)
217 +            join_vars = variables
218 +        node.set_join_variables(join_vars); return node
219 +
220 +    def subquery_expr(self, S, _parent=None):
221 +        _context = self.Context(_parent, self._scanner, 'subquery_expr', [S])
222 +        _token = self._peek('BEING', 'FROM', context=_context)
223 +        if _token == 'BEING':
224 +            BEING = self._scan('BEING', context=_context)
225 +            self._scan('r"\\("', context=_context)
226 +            union = self.union(Union(), _context)
227 +            self._scan('r"\\)"', context=_context)
228 +            return union
229 +        else: # == 'FROM'
230 +            FROM = self._scan('FROM', context=_context)
231 +            func = self.func(S, _context)
232 +            return func
233 
234      def sort_term(self, S, _parent=None):
235          _context = self.Context(_parent, self._scanner, 'sort_term', [S])
236          expr_add = self.expr_add(S, _context)
237          sort_meth = self.sort_meth(_context)
@@ -517,21 +537,21 @@
238      def variables(self, S, _parent=None):
239          _context = self.Context(_parent, self._scanner, 'variables', [S])
240          vars = []
241          var = self.var(S, _context)
242          vars.append(var)
243 -        while self._peek("','", 'BEING', context=_context) == "','":
244 +        while self._peek("','", 'r"\\)"', 'BEING', 'FROM', context=_context) == "','":
245              self._scan("','", context=_context)
246              var = self.var(S, _context)
247              vars.append(var)
248          return vars
249 
250      def decl_vars(self, R, _parent=None):
251          _context = self.Context(_parent, self._scanner, 'decl_vars', [R])
252          E_TYPE = self._scan('E_TYPE', context=_context)
253          var = self.var(R, _context)
254 -        while self._peek("','", 'R_TYPE', 'QMARK', 'WHERE', '":"', 'CMP_OP', 'HAVING', "'IN'", "';'", 'POW_OP', 'BEING', 'WITH', 'MUL_OP', 'r"\\)"', 'ADD_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'AND', 'OR', context=_context) == "','":
255 +        while self._peek("','", 'R_TYPE', 'QMARK', 'WHERE', '":"', 'CMP_OP', 'HAVING', "'IN'", 'r"\\)"', "';'", 'POW_OP', 'WITH', 'BEING', 'FROM', 'MUL_OP', 'ADD_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'AND', 'OR', context=_context) == "','":
256              R.add_main_variable(E_TYPE, var)
257              self._scan("','", context=_context)
258              E_TYPE = self._scan('E_TYPE', context=_context)
259              var = self.var(R, _context)
260          R.add_main_variable(E_TYPE, var)
diff --git a/stcheck.py b/stcheck.py
@@ -231,10 +231,12 @@
261          pass
262 
263      def leave_subquery(self, node, state):
264          # copy graph information we're interested in
265          pgraph = node.parent.vargraph
266 +        if not node.query:
267 +            return
268          for select in node.query.children:
269              # map subquery variable names to outer query variable names
270              trmap = {}
271              for i, vref in enumerate(node.aliases):
272                  try:
@@ -514,12 +516,14 @@
273      def visit_select(self, node):
274          for var in node.aliases.itervalues():
275              var.prepare_annotation()
276          if node.with_ is not None:
277              for subquery in node.with_:
278 -                self.visit_union(subquery.query)
279 -                subquery.query.schema = node.root.schema
280 +                if subquery.query:
281 +                    # Do not visit subquery with fromfunc
282 +                    self.visit_union(subquery.query)
283 +                    subquery.query.schema = node.root.schema
284          node.has_aggregat = False
285          self._visit_stmt(node)
286          if node.having:
287              # if there is a having clause, bloc simplification of variables used in GROUPBY
288              for term in node.groupby:
diff --git a/stmts.py b/stmts.py
@@ -525,10 +525,13 @@
289              ca.stinfo[key] = capt = set()
290              for solution in solutions:
291                  capt.add(solution[ca.name])
292              if kwargs is _MARKER:
293                  continue
294 +            if not ca.query:
295 +                # FROM clause, do not propagate
296 +                continue
297              # propagage to subqueries in case we're introducing additional
298              # type constraints
299              for stmt in ca.query.children[:]:
300                  term = stmt.selection[ca.colnum]
301                  sols = [sol for sol in stmt.solutions
@@ -580,22 +583,23 @@
302      def set_groupby(self, terms):
303          self.groupby = terms
304          for node in terms:
305              node.parent = self
306 
307 -    def set_with(self, terms, check=True):
308 +    def set_with(self, terms, check=True, join_on_vars=()):
309          self.with_ = []
310          for node in terms:
311 -            self.add_subquery(node, check)
312 +            self.add_subquery(node, check, join_on_vars)
313 
314 -    def add_subquery(self, node, check=True):
315 -        assert node.query
316 +    def add_subquery(self, node, check=True, join_on_vars=()):
317 +        assert node.query or node.fromfunc
318 +        node.set_join_variables(join_on_vars)
319          if not isinstance(self.with_, list):
320              self.with_ = []
321          node.parent = self
322          self.with_.append(node)
323 -        if check and len(node.aliases) != len(node.query.children[0].selection):
324 +        if check and node.query and len(node.aliases) != len(node.query.children[0].selection):
325              raise BadRQLQuery('Should have the same number of aliases than '
326                                'selected terms in sub-query')
327          for i, alias in enumerate(node.aliases):
328              alias = alias.name
329              if check and alias in self.aliases:
diff --git a/test/unittest_analyze.py b/test/unittest_analyze.py
@@ -545,10 +545,30 @@
330          node = self.helper.parse('Any U WHERE NOT U owned_by U')
331          self.helper.compute_solutions(node, debug=DEBUG)
332          sols = sorted(node.children[0].solutions)
333          self.assertEqual(sols, [{'U': 'Person'}])
334 
335 +    def test_from_solution(self):
336 +        node = self.helper.parse('Any X WITH X FROM RQL("http://cubicweb.org", '
337 +                                 '"Any Z WHERE Z is Plop")')
338 +        # check constant type of the is relation inserted
339 +        self.helper.compute_solutions(node, debug=DEBUG)
340 +        self.helper.simplify(node)
341 +        sols = node.children[0].solutions
342 +        self.assertEqual(len(sols), 9)
343 +
344 +    def test_from_solution_inner(self):
345 +        node = self.helper.parse('Any Z WHERE Z is Company, Z name N '
346 +                                 'WITH N FROM RQL("http://cubicweb.org", '
347 +                                 '"Any Z WHERE Z is Plop")')
348 +        # check constant type of the is relation inserted
349 +        self.helper.compute_solutions(node, debug=DEBUG)
350 +        self.helper.simplify(node)
351 +        sols = node.children[0].solutions
352 +        self.assertEqual(len(sols), 1)
353 +
354 +
355 
356      def test_selection_with_cast(self):
357          node = self.helper.parse('Any X WHERE X name CAST(String, E), Y eid E, X owned_by Y')
358          self.helper.compute_solutions(node, debug=DEBUG)
359          sols = sorted(node.children[0].solutions)
diff --git a/test/unittest_nodes.py b/test/unittest_nodes.py
@@ -611,10 +611,32 @@
360          self.assertEqual(N.get_description(0, lambda x,**k:x), 'firstname, name')
361          self.assertEqual(X.selected_index(), 0)
362          self.assertEqual(N.selected_index(), None)
363          self.assertEqual(X.main_relation(), None)
364 
365 +    def test_subquery_join_variables(self):
366 +        tree = sparse('Any X WITH X FROM RQL("http://cubicweb.org", '
367 +                      '"Any Z WHERE Z is Plop")')
368 +        subq = tree.children[0].with_[0]
369 +        self.assertFalse(subq.join_variables)
370 +        tree = sparse('Any X WITH X FROM RQL("http://cubicweb.org", '
371 +                      '"Any Z WHERE Z is Plop") JOIN ON (N)')
372 +        subq = tree.children[0].with_[0]
373 +        self.assertTrue(subq.join_variables)
374 +        var = nodes.VariableRef(nodes.Variable('N'))
375 +        self.assertIn(var, subq.join_variables)
376 +
377 +    def test_subqueries_join_variables(self):
378 +        tree = sparse('Any X,Y WITH X FROM RQL("http://cubicweb.org", "Any Z WHERE Z is Plop") JOIN ON (Z), '
379 +                      'Y FROM RQL("http://cubicweb.org", "Any Z WHERE Z is Plop")')
380 +        subq = tree.children[0].with_[0]
381 +        self.assertTrue(subq.join_variables)
382 +        var = nodes.VariableRef(nodes.Variable('Z'))
383 +        self.assertIn(var, subq.join_variables)
384 +        subq = tree.children[0].with_[1]
385 +        self.assertFalse(subq.join_variables)
386 +
387      # non regression tests ####################################################
388 
389      def test_get_description_and_get_type(self):
390          tree = sparse("Any N,COUNT(X),NOW-D GROUPBY N WHERE X name N, X creation_date D;")
391          tree.schema = schema
diff --git a/test/unittest_stcheck.py b/test/unittest_stcheck.py
@@ -82,10 +82,16 @@
392      'DISTINCT Any P ORDERBY XN WHERE P work_for X, X name XN',
393      'Any X WHERE X eid > 0, X eid < 42',
394      'Any X WHERE X eid 1, X eid < 42',
395      'Any X WHERE X number CAST(Int, Y), X name Y',
396      'SET X number CAST(Int, Y) WHERE X name Y',
397 +    'Any X WITH X FROM RQL("http://cubicweb.org", "Any Z WHERE Z is Plop")',
398 +    'Any X,Y WITH X,Y FROM SPARQL("http://dbpedia.org/sparql", "SELECT ?u, ?y WHERE {?u dc:subject ?y}")',
399 +    'Any X WITH X FROM RQL("http://cubicweb.org", "Any Z WHERE Z is Plop", N, V)',
400 +    'Any X WITH X FROM RQL("http://cubicweb.org", "Any Z WHERE Z is Plop") JOIN ON (V)',
401 +    'Any X WITH X FROM RQL("http://cubicweb.org", "Any Z WHERE Z is Plop") JOIN ON (V, N)',
402 +    'Any X WITH X FROM RQL("http://cubicweb.org", "Any Z WHERE Z is Plop") JOIN ON (V, N), Z FROM RQL("http://cubicweb.org", "Any Z WHERE Z is Plop")',
403      )
404 
405  class CheckClassTest(TestCase):
406      """check wrong queries are correctly detected"""
407 
@@ -321,7 +327,32 @@
408      def test_no_attr_var_if_uid_rel(self):
409          with self.assertRaises(BadRQLQuery) as cm:
410              self.parse('Any X, Y WHERE X work_for Z, Y work_for Z, X eid > Y')
411          self.assertEqual(str(cm.exception), 'variable Y should not be used as rhs of attribute relation X eid > Y')
412 
413 +    def test_subquery_annotation_from(self):
414 +        rqlst = self.parse('Any X WITH X FROM RQL("http://cubicweb.org", '
415 +                                                 '"Any Z WHERE Z is Plop")').children[0]
416 +        self.assertEqual(rqlst.with_[0].aliases[0].name, 'X')
417 +        self.assertEqual(rqlst.with_[0].query, None)
418 +        self.assertEqual(rqlst.with_[0].fromfunc.name, 'RQL')
419 +
420 +    def test_subquery_annotation_from_variables(self):
421 +        rqlst = self.parse('Any X WITH X FROM RQL("http://cubicweb.org", '
422 +                                                 '"Any Z WHERE Z is Plop", N)').children[0]
423 +        self.assertEqual(rqlst.with_[0].aliases[0].name, 'X')
424 +        self.assertEqual(rqlst.with_[0].query, None)
425 +        variables = rqlst.with_[0].fromfunc.children
426 +        self.assertEqual(variables[0].value, 'http://cubicweb.org')
427 +        self.assertEqual(variables[1].value, 'Any Z WHERE Z is Plop')
428 +        self.assertEqual(variables[2].name, 'N')
429 +
430 +    def test_subquery_annotation_copy(self):
431 +        rqlst = self.parse('Any X WITH X FROM RQL("http://cubicweb.org", '
432 +                                                 '"Any Z WHERE Z is Plop")').children[0]
433 +        subquery = rqlst.with_[0]
434 +        subcopy = subquery.copy(rqlst)
435 +        self.assertEqual(subquery.as_string(), subcopy.as_string())
436 +
437 +
438  if __name__ == '__main__':
439      unittest_main()
diff --git a/utils.py b/utils.py
@@ -69,10 +69,12 @@
440 
441  from logilab.common.decorators import monkeypatch
442  from logilab.database import SQL_FUNCTIONS_REGISTRY, FunctionDescr, CAST
443 
444  RQL_FUNCTIONS_REGISTRY = SQL_FUNCTIONS_REGISTRY.copy()
445 +RQL_FROM_FUNCTIONS_REGISTRY = {}
446 +
447 
448  @monkeypatch(FunctionDescr)
449  def st_description(self, funcnode, mainindex, tr):
450      return '%s(%s)' % (
451          tr(self.name),
@@ -134,10 +136,25 @@
452 
453  def function_description(funcname):
454      """Return the description (:class:`FunctionDescr`) for a RQL function."""
455      return RQL_FUNCTIONS_REGISTRY.get_function(funcname)
456 
457 +def register_from_function(funcdef):
458 +    if hasattr(funcdef, 'name') is not None:
459 +        name = funcdef.name
460 +    else:
461 +        name = funcdef.__class__.__name__
462 +    RQL_FROM_FUNCTIONS_REGISTRY[name] = funcdef
463 +
464 +def function_from_description(funcname):
465 +    """Return the description (:class:`FunctionDescr`) for a RQL function."""
466 +    try:
467 +        return RQL_FROM_FUNCTIONS_REGISTRY[funcname]
468 +    except KeyError:
469 +        raise RuntimeError('Unknow function %s in RQL FROM functions' % funcname)
470 +
471 +
472  def quote(value):
473      """Quote a string value."""
474      res = ['"']
475      for char in value:
476          if char == '"':