oldstable is 0.30

authorSylvain Th?nault <sylvain.thenault@logilab.fr>
changeset31130f18a5ef
brancholdstable
phasepublic
hiddenno
parent revision#8828046c2d3b closes #78681: stcheck don't crash on column aliases used in outer join, #be394528fdc1 backport stable into oldstable, not messing with the oldstable tag...
child revision<not specified>
files modified by this revision
.hgtags
ChangeLog
__pkginfo__.py
analyze.py
base.py
debian/changelog
debian/control
makefile
nodes.py
parser.g
parser.py
stcheck.py
stmts.py
test/unittest_analyze.py
test/unittest_editextensions.py
test/unittest_nodes.py
test/unittest_parser.py
test/unittest_stcheck.py
undo.py
utils.py
# HG changeset patch
# User Sylvain Thénault <sylvain.thenault@logilab.fr>
# Date 1323429163 -3600
# Fri Dec 09 12:12:43 2011 +0100
# Branch oldstable
# Node ID 31130f18a5ef1017a4d82e664a22122210c4ae71
# Parent be394528fdc1283835ef545952d6c8f21f287a90
# Parent 8828046c2d3b47371f6659430d4492b2bcaf1f68
oldstable is 0.30

diff --git a/.hgtags b/.hgtags
@@ -41,11 +41,10 @@
1  bf6e2008180dab292fe49c70a47883852fb8d46b rql-debian-version-0.22.2-1
2  42004883d4cd6b42f7898a93df8537d5b87a662d rql-version-0.23.0
3  7dd29e42751ebebae4b50126dfb2071d9b2e8de1 rql-debian-version-0.23.0-1
4  5cb31b7a463ea8fcc56da4e768648a2f818ec0ee rql-version-0.24.0
5  4f8562728585d53053e914171180e623e73ac235 rql-debian-version-0.24.0-1
6 -4025f1f02d1da65d26eada37708409984942c432 oldstable
7  3d59f6b1cbb90278f3b4374dce36b6e31c7e9884 rql-version-0.25.0
8  360a6c3a48393f8d5353198d45fbcf25f9ef5369 rql-debian-version-0.25.0-1
9  ae4cba1cf0240c615a8e78c94d81fa05e5ad8bc9 rql-version-0.26.0
10  677736b455f5fb7a31882e37165dbd4879c4bf11 rql-debian-version-0.26.0-1
11  42ae413193a8403a749fb1a206a86cec09f5efdb rql-version-0.26.1
@@ -60,5 +59,13 @@
12  23bd1f36ec77f30cd525327d408ef6836f88eb24 rql-debian-version-0.26.6-1
13  3c59bf663ec78dad82016b43f58348d5e35058ad rql-version-0.27.0
14  0a5a70c34c65fccaf64603613d5d295b332e85cb rql-debian-version-0.27.0-1
15  ae02408da51e63aa2d1be6ac7170d77060bd0910 rql-version-0.28.0
16  21e94bc12c1fcb7f97826fe6aae5dbe62cc4bd06 rql-debian-version-0.28.0-1
17 +c45e9d1c0db45b2f3f158f6b1be4dbe25d28c84d rql-version-0.29.0
18 +78e09096f881259f1f5d3080a78819997fe1eede rql-debian-version-0.29.0-1
19 +cf4fcca7c28946f384c0b15e59b77231d40148b5 rql-version-0.29.1
20 +0c9ac2a5635d3f6a224b799aecea836c4277ba21 rql-debian-version-0.29.1-1
21 +395b876af47bc01048f1d7aacdda069b9f36309c rql-version-0.30.0
22 +c3ae2279fe701809096d5f6bc50f8d8b4a9fd4fd rql-debian-version-0.30.0-1
23 +3c17b96750ad5045bd1fb43241da55c81c094bd4 rql-version-0.30.1
24 +13cd741f8e144265c15bb225fad863234fc8ce16 rql-debian-version-0.30.1-1
diff --git a/ChangeLog b/ChangeLog
@@ -1,10 +1,77 @@
25  ChangeLog for RQL
26  =================
27 
28  --
29 -* suport != operator for non equality
30 +    * #78681: don't crash on column aliases used in outer join
31 +
32 +
33 +2011-09-07  --  0.30.1
34 +
35 +    * #74727: allow entity types to end with a capitalized letter
36 +      provided they contain a lower-cased letter
37 +
38 +
39 +
40 +2011-08-05  --  0.30.0
41 +    * #72295: add some missing operators:
42 +
43 +        - % (modulo),
44 +        - ^ (power),
45 +        - & (bitwise AND),
46 +        - | (bitwise OR),
47 +        - # (bitwise XOR),
48 +        - << (bitwise left shift),
49 +        - >> (bitwise right shift)
50 +
51 +    * #72052: new optional 'optcomparisons' key in variable stinfo, containing
52 +      HAVING comparison nodes where it's used and optional (eg outer
53 +      join)
54 +
55 +    * #69185: fix syntax error with unary operators by introducing
56 +      `UnaryExpression` node
57 +
58 +    * drop old backward compat for ORDERBY/GROUPBY after where clause
59 +
60 +    * fix Comparison.as_string to considerer its optional attribute
61 +
62 +
63 +
64 +2011-07-27  --  0.29.1
65 +    * #70264: remove_group_var renamed into remove_group_term and fixed
66 +      implementation
67 +
68 +    * #70416: rql annotator add 'having' list into variable's stinfo, and
69 +      properly update variable graph
70 +
71 +    * #71131: as_string doesn't  propagate encoding/kwargs to subqueries
72 +
73 +    * #71132: column alias scope should be handled as variable scope, not bound
74 +      to subquery
75 +
76 +    * #71157: bad analyze when using functions
77 +
78 +    * #71415: needs to allow outer join on rhs of final relation and in HAVING express
79 +
80 +    * Select.replace must properly reset old node's parent attribute
81 +
82 +    * new undo_modification context manager on select nodes
83 +
84 +
85 +
86 +2011-06-09  --  0.29.0
87 +    * support != operator for non equality
88 +
89 +    * support for CAST function
90 +
91 +    * support for regexp-based pattern matching using a REGEXP operator
92 +
93 +    * may now GROUPBY functions / column number
94 +
95 +    * fix parsing of negative float
96 +
97 +
98 
99  2011-01-12  --  0.28.0
100      * enhance rewrite_shared_optional so one can specify where the new identity
101        relation should be added (used by cw multi-sources planner)
102 
diff --git a/__pkginfo__.py b/__pkginfo__.py
@@ -1,7 +1,7 @@
103  # pylint: disable-msg=W0622
104 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
105 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
106  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
107  #
108  # This file is part of rql.
109  #
110  # rql is free software: you can redistribute it and/or modify it under the
@@ -18,11 +18,11 @@
111  # with rql. If not, see <http://www.gnu.org/licenses/>.
112  """RQL packaging information."""
113  __docformat__ = "restructuredtext en"
114 
115  modname = "rql"
116 -numversion = (0, 28, 0)
117 +numversion = (0, 30, 1)
118  version = '.'.join(str(num) for num in numversion)
119 
120  license = 'LGPL'
121 
122  author = "Logilab"
@@ -79,11 +79,11 @@
123                                )
124                      ]
125 
126  install_requires = [
127      'logilab-common >= 0.47.0',
128 -    'logilab-database',
129 +    'logilab-database >= 1.6.0',
130      'yapps == 2.1.1', # XXX to ensure we don't use the broken pypi version
131      'constraint', # fallback if the gecode compiled module is missing
132      ]
133 
134  # links to download yapps2 package that is not (yet) registered in pypi
diff --git a/analyze.py b/analyze.py
@@ -491,49 +491,37 @@
135          elif isinstance(rhs, nodes.Constant) and not rschema.final:
136              # rhs.type is None <-> NULL
137              if not isinstance(lhs, nodes.VariableRef) or rhs.type is None:
138                  return True
139              self._extract_constraint(constraints, lhs.name, rhs, rschema.subjects)
140 -        else:
141 -            if not isinstance(lhs, nodes.VariableRef):
142 -                # XXX: check relation is valid
143 -                return True
144 +        elif not isinstance(lhs, nodes.VariableRef):
145 +            # XXX: check relation is valid
146 +            return True
147 +        elif isinstance(rhs, nodes.VariableRef):
148              lhsvar = lhs.name
149 -            rhsvars = []
150 -            samevar = False
151 -            if not isinstance(rhs, nodes.MathExpression):
152 -                # rhs type is the result of the math expression, not of
153 -                # individual variables, so don't add constraints on rhs
154 -                # variables
155 -                for v in rhs.iget_nodes(nodes.VariableRef):
156 -                    if v.name == lhsvar:
157 -                        samevar = True
158 -                    else:
159 -                        rhsvars.append(v.name)
160 +            rhsvar = rhs.name
161              lhsdomain = constraints.domains[lhsvar]
162 -            if rhsvars:
163 -                s2 = '=='.join(rhsvars)
164 -                # filter according to domain necessary for column aliases
165 -                rhsdomain = constraints.domains[rhsvars[0]]
166 -                res = []
167 -                for fromtype, totypes in rschema.associations():
168 -                    if not fromtype in lhsdomain:
169 -                        continue
170 -                    ptypes = [str(t) for t in totypes if t in rhsdomain]
171 -                    res.append( [ ( [lhsvar], [str(fromtype)]), (rhsvars, ptypes) ] )
172 -                constraints.or_and( res )
173 -            else:
174 -                ptypes = [str(subj) for subj in rschema.subjects()
175 -                          if subj in lhsdomain]
176 -                constraints.var_has_types( lhsvar, ptypes )
177 -            if samevar:
178 -                res = []
179 -                for fromtype, totypes in rschema.associations():
180 -                    if not (fromtype in totypes and fromtype in lhsdomain):
181 -                        continue
182 -                    res.append(str(fromtype))
183 +            # filter according to domain necessary for column aliases
184 +            rhsdomain = constraints.domains[rhsvar]
185 +            res = []
186 +            for fromtype, totypes in rschema.associations():
187 +                if not fromtype in lhsdomain:
188 +                    continue
189 +                ptypes = [str(t) for t in totypes if t in rhsdomain]
190 +                res.append( [ ([lhsvar], [str(fromtype)]),
191 +                              ([rhsvar], ptypes) ] )
192 +            constraints.or_and(res)
193 +            if rhsvar == lhsvar:
194 +                res = [str(fromtype) for fromtype, totypes in rschema.associations()
195 +                       if (fromtype in totypes and fromtype in lhsdomain)]
196                  constraints.var_has_types( lhsvar, res )
197 +        else:
198 +            # XXX consider rhs.get_type?
199 +            lhsdomain = constraints.domains[lhs.name]
200 +            ptypes = [str(subj) for subj in rschema.subjects()
201 +                      if subj in lhsdomain]
202 +            constraints.var_has_types( lhs.name, ptypes )
203          return True
204 
205      def visit_type_restriction(self, relation, constraints):
206          lhs, rhs = relation.get_parts()
207          etypes = set(c.value for c in rhs.iget_nodes(nodes.Constant)
diff --git a/base.py b/base.py
@@ -1,6 +1,6 @@
208 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
209 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
210  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
211  #
212  # This file is part of rql.
213  #
214  # rql is free software: you can redistribute it and/or modify it under the
@@ -20,10 +20,11 @@
215  Note: this module uses __slots__ to limit memory usage.
216  """
217 
218  __docformat__ = "restructuredtext en"
219 
220 +
221  class BaseNode(object):
222      __slots__ = ('parent',)
223 
224      def __str__(self):
225          return self.as_string(encoding='utf-8')
@@ -194,6 +195,5 @@
226          """Create and return a copy of this node and its descendant.
227 
228          stmt is the root node, which should be use to get new variables.
229          """
230          return self.__class__(*self.initargs(stmt))
231 -
diff --git a/debian/changelog b/debian/changelog
@@ -1,12 +1,31 @@
232 -rql (0.28.0-2) UNRELEASED; urgency=low
233 +rql (0.30.1-1) unstable; urgency=low
234 +
235 +  * new upstream release
236 +
237 + -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 07 Sep 2011 18:54:33 +0200
238 +
239 +rql (0.30.0-1) unstable; urgency=low
240 +
241 +  * new upstream release
242 +
243 + -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 05 Aug 2011 09:30:35 +0200
244 +
245 +rql (0.29.1-1) unstable; urgency=low
246 +
247 +  * new upstream release
248 +
249 + -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 27 Jul 2011 15:05:34 +0200
250 +
251 +rql (0.29.0-1) unstable; urgency=low
252 
253    * debian/control:
254      - remove Ludovic Aubry from Uploaders
255    * lintian fixes
256 +  * new upstream release
257 
258 - -- 
259 + -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 09 Jun 2011 16:57:56 +0200
260 
261  rql (0.28.0-1) unstable; urgency=low
262 
263    * new upstream release
264 
diff --git a/debian/control b/debian/control
@@ -9,11 +9,11 @@
265  Homepage: http://www.logilab.org/project/rql
266 
267  Package: python-rql
268  Architecture: any
269  XB-Python-Version: ${python:Versions}
270 -Depends: ${python:Depends}, ${misc:Depends}, ${shlibs:Depends}, python-logilab-common (>= 0.35.3-1), yapps2-runtime, python-logilab-database
271 +Depends: ${python:Depends}, ${misc:Depends}, ${shlibs:Depends}, python-logilab-common (>= 0.35.3-1), yapps2-runtime, python-logilab-database (>= 1.6.0)
272  Conflicts: cubicweb-common (<= 3.8.3)
273  Provides: ${python:Provides}
274  Description: relationship query language (RQL) utilities
275   A library providing the base utilities to handle RQL queries,
276   such as a parser, a type inferencer.
diff --git a/makefile b/makefile
@@ -1,8 +1,6 @@
277  YAPPS=yapps
278 -#python thirdparty/yapps2.py
279 
280  parser.py: parser.g parser_main.py
281  	${YAPPS} parser.g
282 -	#sed "s/from yappsrt import/from thirdparty.yappsrt import/" parser.py > tmp.py
283 -	sed -i "s/__main__/old__main__/" parser.py
284 -	cat parser_main.py >> parser.py
285 \ No newline at end of file
286 +	#sed -i "s/__main__/old__main__/" parser.py
287 +	#cat parser_main.py >> parser.py
288 \ No newline at end of file
diff --git a/nodes.py b/nodes.py
@@ -1,6 +1,6 @@
289 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
290 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
291  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
292  #
293  # This file is part of rql.
294  #
295  # rql is free software: you can redistribute it and/or modify it under the
@@ -26,10 +26,12 @@
296  from itertools import chain
297  from decimal import Decimal
298  from datetime import datetime, date, time, timedelta
299  from time import localtime
300 
301 +from logilab.database import DYNAMIC_RTYPE
302 +
303  from rql import CoercionError
304  from rql.base import BaseNode, Node, BinaryNode, LeafNode
305  from rql.utils import (function_description, quote, uquote, build_visitor_stub,
306                         common_parent)
307 
@@ -72,10 +74,35 @@
308      for vref in node.iget_nodes(VariableRef):
309          if isinstance(vref.variable, Variable):
310              yield vref
311 
312 
313 +class OperatorExpressionMixin(object):
314 +
315 +    def initargs(self, stmt):
316 +        """return list of arguments to give to __init__ to clone this node"""
317 +        return (self.operator,)
318 +
319 +    def is_equivalent(self, other):
320 +        if not Node.is_equivalent(self, other):
321 +            return False
322 +        return self.operator == other.operator
323 +
324 +    def get_description(self, mainindex, tr):
325 +        """if there is a variable in the math expr used as rhs of a relation,
326 +        return the name of this relation, else return the type of the math
327 +        expression
328 +        """
329 +        try:
330 +            return tr(self.get_type())
331 +        except CoercionError:
332 +            for vref in self.iget_nodes(VariableRef):
333 +                vtype = vref.get_description(mainindex, tr)
334 +                if vtype != 'Any':
335 +                    return tr(vtype)
336 +
337 +
338  class HSMixin(object):
339      """mixin class for classes which may be the lhs or rhs of an expression"""
340      __slots__ = ()
341 
342      def relation(self):
@@ -182,18 +209,20 @@
343      def add_relation(self, lhsvar, rtype, rhsvar):
344          """builds a restriction node to express '<var> eid <eid>'"""
345          return self.add_restriction(make_relation(lhsvar, rtype, (rhsvar,),
346                                                    VariableRef))
347 
348 -    def add_eid_restriction(self, var, eid):
349 +    def add_eid_restriction(self, var, eid, c_type='Int'):
350          """builds a restriction node to express '<var> eid <eid>'"""
351 -        return self.add_constant_restriction(var, 'eid', eid, 'Int')
352 +        assert c_type in ('Int', 'Substitute'), "Error got c_type=%r in eid restriction" % c_type
353 +        return self.add_constant_restriction(var, 'eid', eid, c_type)
354 
355      def add_type_restriction(self, var, etype):
356          """builds a restriction node to express : variable is etype"""
357          return self.add_constant_restriction(var, 'is', etype, 'etype')
358 
359 +
360  # base RQL nodes ##############################################################
361 
362  class SubQuery(BaseNode):
363      """WITH clause"""
364      __slots__ = ('aliases', 'query')
@@ -219,11 +248,11 @@
365      def children(self):
366          return self.aliases + [self.query]
367 
368      def as_string(self, encoding=None, kwargs=None):
369          return '%s BEING (%s)' % (','.join(v.name for v in self.aliases),
370 -                                  self.query.as_string())
371 +                                  self.query.as_string(encoding, kwargs))
372      def __repr__(self):
373          return '%s BEING (%s)' % (','.join(repr(v) for v in self.aliases),
374                                    repr(self.query))
375 
376  class And(BinaryNode):
@@ -438,16 +467,11 @@
377      def is_types_restriction(self):
378          if self.r_type not in ('is', 'is_instance_of'):
379              return False
380          rhs = self.children[1]
381          if isinstance(rhs, Comparison):
382 -            try:
383 -                rhs = rhs.children[0]
384 -            except:
385 -                print 'opppp', rhs
386 -                print rhs.root
387 -                raise
388 +            rhs = rhs.children[0]
389          # else: relation used in SET OR DELETE selection
390          return ((isinstance(rhs, Constant) and rhs.type == 'etype')
391                  or (isinstance(rhs, Function) and rhs.name == 'IN'))
392 
393      def operator(self):
@@ -476,37 +500,46 @@
394          rhs = self.children[1].children[0]
395          return lhs, rhs
396 
397      def change_optional(self, value):
398          root = self.root
399 -        if root.should_register_op and value != self.optional:
400 +        if root is not None and root.should_register_op and value != self.optional:
401              from rql.undo import SetOptionalOperation
402              root.undo_manager.add_operation(SetOptionalOperation(self, self.optional))
403          self.optional= value
404 
405 
406 -OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE'))
407 +CMP_OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE', 'REGEXP'))
408 
409  class Comparison(HSMixin, Node):
410      """handle comparisons:
411 
412       <, <=, =, >=, > LIKE and ILIKE operators have a unique children.
413      """
414 -    __slots__ = ('operator',)
415 +    __slots__ = ('operator', 'optional')
416 
417 -    def __init__(self, operator, value=None):
418 +    def __init__(self, operator, value=None, optional=None):
419          Node.__init__(self)
420          if operator == '~=':
421              operator = 'ILIKE'
422 -        assert operator in OPERATORS, operator
423 +        assert operator in CMP_OPERATORS, operator
424          self.operator = operator.encode()
425 +        self.optional = optional
426          if value is not None:
427              self.append(value)
428 
429      def initargs(self, stmt):
430          """return list of arguments to give to __init__ to clone this node"""
431 -        return (self.operator,)
432 +        return (self.operator, None, self.optional)
433 +
434 +    def set_optional(self, left, right):
435 +        if left and right:
436 +            self.optional = 'both'
437 +        elif left:
438 +            self.optional = 'left'
439 +        elif right:
440 +            self.optional = 'right'
441 
442      def is_equivalent(self, other):
443          if not Node.is_equivalent(self, other):
444              return False
445          return self.operator == other.operator
@@ -514,39 +547,35 @@
446      def as_string(self, encoding=None, kwargs=None):
447          """return the tree as an encoded rql string"""
448          if len(self.children) == 0:
449              return self.operator
450          if len(self.children) == 2:
451 -            return '%s %s %s' % (self.children[0].as_string(encoding, kwargs),
452 -                                 self.operator.encode(),
453 -                                 self.children[1].as_string(encoding, kwargs))
454 +            lhsopt = rhsopt = ''
455 +            if self.optional in ('left', 'both'):
456 +                lhsopt = '?'
457 +            if self.optional in ('right', 'both'):
458 +                rhsopt = '?'
459 +            return '%s%s %s %s%s' % (self.children[0].as_string(encoding, kwargs),
460 +                                     lhsopt, self.operator.encode(),
461 +                                     self.children[1].as_string(encoding, kwargs), rhsopt)
462          if self.operator == '=':
463              return self.children[0].as_string(encoding, kwargs)
464          return '%s %s' % (self.operator.encode(),
465                            self.children[0].as_string(encoding, kwargs))
466 
467      def __repr__(self):
468          return '%s %s' % (self.operator, ', '.join(repr(c) for c in self.children))
469 
470 
471 -class MathExpression(HSMixin, BinaryNode):
472 -    """Operators plus, minus, multiply, divide."""
473 +class MathExpression(OperatorExpressionMixin, HSMixin, BinaryNode):
474 +    """Mathematical Operators"""
475      __slots__ = ('operator',)
476 
477      def __init__(self, operator, lhs=None, rhs=None):
478          BinaryNode.__init__(self, lhs, rhs)
479          self.operator = operator.encode()
480 
481 -    def initargs(self, stmt):
482 -        """return list of arguments to give to __init__ to clone this node"""
483 -        return (self.operator,)
484 -
485 -    def is_equivalent(self, other):
486 -        if not Node.is_equivalent(self, other):
487 -            return False
488 -        return self.operator == other.operator
489 -
490      def as_string(self, encoding=None, kwargs=None):
491          """return the tree as an encoded rql string"""
492          return '(%s %s %s)' % (self.children[0].as_string(encoding, kwargs),
493                                 self.operator.encode(),
494                                 self.children[1].as_string(encoding, kwargs))
@@ -577,22 +606,35 @@
495                  return rhstype
496              if sorted((lhstype, rhstype)) == ['Float', 'Int']:
497                  return 'Float'
498              raise CoercionError(key)
499 
500 -    def get_description(self, mainindex, tr):
501 -        """if there is a variable in the math expr used as rhs of a relation,
502 -        return the name of this relation, else return the type of the math
503 -        expression
504 +
505 +class UnaryExpression(OperatorExpressionMixin, Node):
506 +    """Unary Operators"""
507 +    __slots__ = ('operator',)
508 +
509 +    def __init__(self, operator, child=None):
510 +        Node.__init__(self)
511 +        self.operator = operator.encode()
512 +        if child is not None:
513 +            self.append(child)
514 +
515 +    def as_string(self, encoding=None, kwargs=None):
516 +        """return the tree as an encoded rql string"""
517 +        return '%s%s' % (self.operator.encode(),
518 +                         self.children[0].as_string(encoding, kwargs))
519 +
520 +    def __repr__(self):
521 +        return '%s%r' % (self.operator, self.children[0])
522 +
523 +    def get_type(self, solution=None, kwargs=None):
524 +        """return the type of object returned by this expression if known
525 +
526 +        solution is an optional variable/etype mapping
527          """
528 -        try:
529 -            return tr(self.get_type())
530 -        except CoercionError:
531 -            for vref in self.iget_nodes(VariableRef):
532 -                vtype = vref.get_description(mainindex, tr)
533 -                if vtype != 'Any':
534 -                    return tr(vtype)
535 +        return self.children[0].get_type(solution, kwargs)
536 
537 
538  class Function(HSMixin, Node):
539      """Class used to deal with aggregat functions (sum, min, max, count, avg)
540      and latter upper(), lower() and other RQL transformations functions
@@ -623,11 +665,12 @@
541      def get_type(self, solution=None, kwargs=None):
542          """return the type of object returned by this function if known
543 
544          solution is an optional variable/etype mapping
545          """
546 -        rtype = self.descr().rtype
547 +        func_descr = self.descr()
548 +        rtype = func_descr.rql_return_type(self)
549          if rtype is None:
550              # XXX support one variable ref child
551              try:
552                  rtype = solution and solution.get(self.children[0].name)
553              except AttributeError:
@@ -824,19 +867,25 @@
554 
555 
556  ###############################################################################
557 
558  class Referenceable(object):
559 -    __slots__ = ('name', 'stinfo')
560 +    __slots__ = ('name', 'stinfo', 'stmt')
561 
562      def __init__(self, name):
563          self.name = name.strip().encode()
564          # used to collect some global information about the syntax tree
565          self.stinfo = {
566              # link to VariableReference objects in the syntax tree
567              'references': set(),
568              }
569 +        # reference to the selection
570 +        self.stmt = None
571 +
572 +    @property
573 +    def schema(self):
574 +        return self.stmt.root.schema
575 
576      def init_copy(self, old):
577          # should copy variable's possibletypes on copy
578          if not self.stinfo.get('possibletypes'):
579              self.stinfo['possibletypes'] = old.stinfo.get('possibletypes')
@@ -861,10 +910,11 @@
580          """return all references on this variable"""
581          return tuple(self.stinfo['references'])
582 
583      def prepare_annotation(self):
584          self.stinfo.update({
585 +            'scope': None,
586              # relations where this variable is used on the lhs/rhs
587              'relations': set(),
588              'rhsrelations': set(),
589              # selection indexes if any
590              'selected': set(),
@@ -881,10 +931,22 @@
591              })
592          # remove optional st infos
593          for key in ('optrelations', 'blocsimplification', 'ftirels'):
594              self.stinfo.pop(key, None)
595 
596 +    def _set_scope(self, key, scopenode):
597 +        if scopenode is self.stmt or self.stinfo[key] is None:
598 +            self.stinfo[key] = scopenode
599 +        elif not (self.stinfo[key] is self.stmt or scopenode is self.stinfo[key]):
600 +            self.stinfo[key] = common_parent(self.stinfo[key], scopenode).scope
601 +
602 +    def set_scope(self, scopenode):
603 +        self._set_scope('scope', scopenode)
604 +    def get_scope(self):
605 +        return self.stinfo['scope']
606 +    scope = property(get_scope, set_scope)
607 +
608      def add_optional_relation(self, relation):
609          try:
610              self.stinfo['optrelations'].add(relation)
611          except KeyError:
612              self.stinfo['optrelations'] = set((relation,))
@@ -984,14 +1046,10 @@
613          self.query = query
614 
615      def __repr__(self):
616          return 'alias %s(%#X)' % (self.name, id(self))
617 
618 -    @property
619 -    def schema(self):
620 -        return self.query.root.schema
621 -
622      def get_type(self, solution=None, kwargs=None):
623          """return entity type of this object, 'Any' if not found"""
624          vtype = super(ColumnAlias, self).get_type(solution, kwargs)
625          if vtype == 'Any':
626              for select in self.query.children:
@@ -1012,61 +1070,29 @@
627                      vtypes.add(vtype)
628              if vtypes:
629                  return ', '.join(sorted(vtype for vtype in vtypes))
630          return vtype
631 
632 -    def set_scope(self, scopenode):
633 -        pass
634 -    def get_scope(self):
635 -        return self.query
636 -    scope = property(get_scope, set_scope)
637 -
638 
639  class Variable(Referenceable):
640      """
641      a variable definition, should not be directly added to the syntax tree (use
642      VariableRef instead)
643 
644      collects information about a variable use in a syntax tree
645      """
646 -    __slots__ = ('stmt',
647 -                 '_q_invariant', '_q_sql', '_q_sqltable') # XXX ginco specific
648 -
649 -    def __init__(self, name):
650 -        super(Variable, self).__init__(name)
651 -        # reference to the selection
652 -        self.stmt = None
653 +    __slots__ = ('_q_invariant', '_q_sql', '_q_sqltable') # XXX ginco specific
654 
655      def __repr__(self):
656          return '%s(%#X)' % (self.name, id(self))
657 
658 -    @property
659 -    def schema(self):
660 -        return self.stmt.root.schema
661 -
662 -    def prepare_annotation(self):
663 -        super(Variable, self).prepare_annotation()
664 -        self.stinfo['scope'] = None
665 -
666 -    def _set_scope(self, key, scopenode):
667 -        if scopenode is self.stmt or self.stinfo[key] is None:
668 -            self.stinfo[key] = scopenode
669 -        elif not (self.stinfo[key] is self.stmt or scopenode is self.stinfo[key]):
670 -            self.stinfo[key] = common_parent(self.stinfo[key], scopenode).scope
671 -
672 -    def set_scope(self, scopenode):
673 -        self._set_scope('scope', scopenode)
674 -    def get_scope(self):
675 -        return self.stinfo['scope']
676 -    scope = property(get_scope, set_scope)
677 -
678      def valuable_references(self):
679          """return the number of "valuable" references :
680          references is in selection or in a non type (is) relations
681          """
682          stinfo = self.stinfo
683          return len(stinfo['selected']) + len(stinfo['relations'])
684 
685 
686  build_visitor_stub((SubQuery, And, Or, Not, Exists, Relation,
687 -                    Comparison, MathExpression, Function, Constant,
688 -                    VariableRef, SortTerm, ColumnAlias, Variable))
689 +                    Comparison, MathExpression, UnaryExpression, Function,
690 +                    Constant, VariableRef, SortTerm, ColumnAlias, Variable))
diff --git a/parser.g b/parser.g
@@ -1,9 +1,9 @@
691  """yapps input grammar for RQL.
692 
693  :organization: Logilab
694 -:copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
695 +:copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
696  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
697 
698 
699  Select statement grammar
700  ------------------------
@@ -69,11 +69,11 @@
701      token INSERT:      r'(?i)INSERT'
702      token UNION:       r'(?i)UNION'
703      token DISTINCT:    r'(?i)DISTINCT'
704      token WITH:        r'(?i)WITH'
705      token WHERE:       r'(?i)WHERE'
706 -    token BEING:          r'(?i)BEING'
707 +    token BEING:       r'(?i)BEING'
708      token OR:          r'(?i)OR'
709      token AND:         r'(?i)AND'
710      token NOT:         r'(?i)NOT'
711      token GROUPBY:     r'(?i)GROUPBY'
712      token HAVING:      r'(?i)HAVING'
@@ -86,22 +86,24 @@
713      token DATETIME:    r'(?i)NOW'
714      token TRUE:        r'(?i)TRUE'
715      token FALSE:       r'(?i)FALSE'
716      token NULL:        r'(?i)NULL'
717      token EXISTS:      r'(?i)EXISTS'
718 -    token CMP_OP:      r'(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE'
719 -    token ADD_OP:      r'\+|-'
720 -    token MUL_OP:      r'\*|/'
721 +    token CMP_OP:      r'(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP'
722 +    token ADD_OP:      r'\+|-|\||#'
723 +    token MUL_OP:      r'\*|/|%|&'
724 +    token POW_OP:      r'\^|>>|<<'
725 +    token UNARY_OP:    r'-|~'
726      token FUNCTION:    r'[A-Za-z_]+\s*(?=\()'
727      token R_TYPE:      r'[a-z_][a-z0-9_]*'
728 -    token E_TYPE:      r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*'
729 +    token E_TYPE:      r'[A-Z][A-Za-z0-9]*[a-z]+[A-Z0-9]*'
730      token VARIABLE:    r'[A-Z][A-Z0-9_]*'
731      token COLALIAS:    r'[A-Z][A-Z0-9_]*\.\d+'
732      token QMARK:       r'\?'
733 
734      token STRING:      r"'([^\'\\]|\\.)*'|\"([^\\\"\\]|\\.)*\""
735 -    token FLOAT:       r'\d+\.\d*'
736 +    token FLOAT:       r'-?\d+\.\d*'
737      token INT:         r'-?\d+'
738      token SUBSTITUTE:  r'%\([A-Za-z_0-9]+\)s'
739 
740 
741  #// Grammar entry ###############################################################
@@ -153,29 +155,24 @@
742                     groupby<<S>>
743                     orderby<<S>>
744                     limit_offset<<S>>
745                     where<<S>>
746                     having<<S>>
747 -                   with_<<S>>
748 -                   dgroupby<<S>>
749 -                   dorderby<<S>>
750 -                   dlimit_offset<<S>>    {{ S.set_statement_type(E_TYPE); return S }}
751 +                   with_<<S>>             {{ S.set_statement_type(E_TYPE); return S }}
752 
753  rule selection<<S>>: expr_add<<S>>        {{ S.append_selected(expr_add) }}
754                       (  ',' expr_add<<S>> {{ S.append_selected(expr_add) }}
755                       )*
756 
757 
758 
759  #// other clauses (groupby, orderby, with, having) ##############################
760 
761 -#// to remove in rql 1.0
762 -rule dorderby<<S>>: orderby<<S>> {{ if orderby: warn('ORDERBY is now before WHERE clause') }}
763 -rule dgroupby<<S>>: groupby<<S>> {{ if groupby: warn('GROUPBY is now before WHERE clause') }}
764 -rule dlimit_offset<<S>>: limit_offset<<S>> {{ if limit_offset: warn('LIMIT/OFFSET are now before WHERE clause') }}
765 -
766 -rule groupby<<S>>: GROUPBY variables<<S>> {{ S.set_groupby(variables); return True }}
767 +rule groupby<<S>>: GROUPBY              {{ nodes = [] }}
768 +                   expr_add<<S>>        {{ nodes.append(expr_add) }}
769 +                   ( ',' expr_add<<S>>  {{ nodes.append(expr_add) }}
770 +                   )*                   {{ S.set_groupby(nodes); return True }}
771                   |
772 
773  rule having<<S>>: HAVING logical_expr<<S>> {{ S.set_having([logical_expr]) }}
774                  |
775 
@@ -228,11 +225,11 @@
776  rule rels_or<<S>>: rels_and<<S>>      {{ node = rels_and }}
777                     ( OR rels_and<<S>> {{ node = Or(node, rels_and) }}
778                     )*                 {{ return node }}
779 
780  rule rels_and<<S>>: rels_not<<S>>        {{ node = rels_not }}
781 -                    (  AND rels_not<<S>> {{ node = And(node, rels_not) }}
782 +                    ( AND rels_not<<S>>  {{ node = And(node, rels_not) }}
783                      )*                   {{ return node }}
784 
785  rule rels_not<<S>>: NOT rel<<S>> {{ return Not(rel) }}
786                    | rel<<S>>     {{ return rel }}
787 
@@ -260,11 +257,11 @@
788  rule exprs_or<<S>>: exprs_and<<S>>      {{ node = exprs_and }}
789                      ( OR exprs_and<<S>> {{ node = Or(node, exprs_and) }}
790                      )*                  {{ return node }}
791 
792  rule exprs_and<<S>>: exprs_not<<S>>        {{ node = exprs_not }}
793 -                     (  AND exprs_not<<S>> {{ node = And(node, exprs_not) }}
794 +                     ( AND exprs_not<<S>>  {{ node = And(node, exprs_not) }}
795                       )*                    {{ return node }}
796 
797  rule exprs_not<<S>>: NOT balanced_expr<<S>> {{ return Not(balanced_expr) }}
798                     | balanced_expr<<S>>     {{ return balanced_expr }}
799 
@@ -275,15 +272,16 @@
800  #//
801  #// but not
802  #//
803  #//   Any T2 WHERE T1 relation T2 HAVING (1+2) < COUNT(T1);
804  rule balanced_expr<<S>>: r"\(" logical_expr<<S>> r"\)" {{ return logical_expr }}
805 -                       | expr_add<<S>> expr_op<<S>>    {{ expr_op.insert(0, expr_add); return expr_op }}
806 +                       | expr_add<<S>> opt_left<<S>>
807 +                         expr_op<<S>> opt_right<<S>> {{ expr_op.insert(0, expr_add); expr_op.set_optional(opt_left, opt_right); return expr_op }}
808 
809  # // cant use expr<<S>> without introducing some ambiguities
810  rule expr_op<<S>>: CMP_OP expr_add<<S>> {{ return Comparison(CMP_OP.upper(), expr_add) }}
811 -                 | in_expr<<S>>      {{ return Comparison('=', in_expr) }}
812 +                 | in_expr<<S>>         {{ return Comparison('=', in_expr) }}
813 
814 
815  #// common statements ###########################################################
816 
817  rule variables<<S>>:                   {{ vars = [] }}
@@ -309,14 +307,21 @@
818 
819 
820  rule expr_add<<S>>: expr_mul<<S>>          {{ node = expr_mul }}
821                      ( ADD_OP expr_mul<<S>> {{ node = MathExpression( ADD_OP, node, expr_mul ) }}
822                      )*                     {{ return node }}
823 +                  | UNARY_OP expr_mul<<S>> {{ node = UnaryExpression( UNARY_OP, expr_mul ) }}
824 +                    ( ADD_OP expr_mul<<S>> {{ node = MathExpression( ADD_OP, node, expr_mul ) }}
825 +                    )*                     {{ return node }}
826 
827 
828 -rule expr_mul<<S>>: expr_base<<S>>          {{ node = expr_base }}
829 -                    ( MUL_OP expr_base<<S>> {{ node = MathExpression( MUL_OP, node, expr_base) }}
830 +rule expr_mul<<S>>: expr_pow<<S>>          {{ node = expr_pow }}
831 +                    ( MUL_OP expr_pow<<S>> {{ node = MathExpression( MUL_OP, node, expr_pow) }}
832 +                    )*                     {{ return node }}
833 +
834 +rule expr_pow<<S>>: expr_base<<S>>          {{ node = expr_base }}
835 +                    ( POW_OP expr_base<<S>> {{ node = MathExpression( MUL_OP, node, expr_base) }}
836                      )*                      {{ return node }}
837 
838 
839  rule expr_base<<S>>: const                     {{ return const }}
840                     | var<<S>>                  {{ return var }}
diff --git a/parser.py b/parser.py
@@ -1,24 +1,10 @@
841 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
842 -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
843 -#
844 -# This file is part of rql.
845 -#
846 -# rql is free software: you can redistribute it and/or modify it under the
847 -# terms of the GNU Lesser General Public License as published by the Free
848 -# Software Foundation, either version 2.1 of the License, or (at your option)
849 -# any later version.
850 -#
851 -# rql is distributed in the hope that it will be useful, but WITHOUT ANY
852 -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
853 -# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
854 -# details.
855 -#
856 -# You should have received a copy of the GNU Lesser General Public License along
857 -# with rql. If not, see <http://www.gnu.org/licenses/>.
858  """yapps input grammar for RQL.
859 
860 +:organization: Logilab
861 +:copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
862 +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
863 
864 
865  Select statement grammar
866  ------------------------
867 
@@ -107,21 +93,23 @@
868          ('DATETIME', re.compile('(?i)NOW')),
869          ('TRUE', re.compile('(?i)TRUE')),
870          ('FALSE', re.compile('(?i)FALSE')),
871          ('NULL', re.compile('(?i)NULL')),
872          ('EXISTS', re.compile('(?i)EXISTS')),
873 -        ('CMP_OP', re.compile('(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE')),
874 -        ('ADD_OP', re.compile('\\+|-')),
875 -        ('MUL_OP', re.compile('\\*|/')),
876 +        ('CMP_OP', re.compile('(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP')),
877 +        ('ADD_OP', re.compile('\\+|-|\\||#')),
878 +        ('MUL_OP', re.compile('\\*|/|%|&')),
879 +        ('POW_OP', re.compile('\\^|>>|<<')),
880 +        ('UNARY_OP', re.compile('-|~')),
881          ('FUNCTION', re.compile('[A-Za-z_]+\\s*(?=\\()')),
882          ('R_TYPE', re.compile('[a-z_][a-z0-9_]*')),
883 -        ('E_TYPE', re.compile('[A-Z][A-Za-z0-9]*[a-z]+[0-9]*')),
884 +        ('E_TYPE', re.compile('[A-Z][A-Za-z0-9]*[a-z]+[A-Z0-9]*')),
885          ('VARIABLE', re.compile('[A-Z][A-Z0-9_]*')),
886          ('COLALIAS', re.compile('[A-Z][A-Z0-9_]*\\.\\d+')),
887          ('QMARK', re.compile('\\?')),
888          ('STRING', re.compile('\'([^\\\'\\\\]|\\\\.)*\'|\\"([^\\\\\\"\\\\]|\\\\.)*\\"')),
889 -        ('FLOAT', re.compile('\\d+\\.\\d*')),
890 +        ('FLOAT', re.compile('-?\\d+\\.\\d*')),
891          ('INT', re.compile('-?\\d+')),
892          ('SUBSTITUTE', re.compile('%\\([A-Za-z_0-9]+\\)s')),
893      ]
894      def __init__(self, str,*args,**kw):
895          runtime.Scanner.__init__(self,None,{'\\s+':None,'/\\*(?:[^*]|\\*(?!/))*\\*/':None,},str,*args,**kw)
@@ -224,13 +212,10 @@
896          orderby = self.orderby(S, _context)
897          limit_offset = self.limit_offset(S, _context)
898          where = self.where(S, _context)
899          having = self.having(S, _context)
900          with_ = self.with_(S, _context)
901 -        dgroupby = self.dgroupby(S, _context)
902 -        dorderby = self.dorderby(S, _context)
903 -        dlimit_offset = self.dlimit_offset(S, _context)
904          S.set_statement_type(E_TYPE); return S
905 
906      def selection(self, S, _parent=None):
907          _context = self.Context(_parent, self._scanner, 'selection', [S])
908          expr_add = self.expr_add(S, _context)
@@ -238,84 +223,67 @@
909          while self._peek("','", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
910              self._scan("','", context=_context)
911              expr_add = self.expr_add(S, _context)
912              S.append_selected(expr_add)
913 
914 -    def dorderby(self, S, _parent=None):
915 -        _context = self.Context(_parent, self._scanner, 'dorderby', [S])
916 -        orderby = self.orderby(S, _context)
917 -        if orderby: warn('ORDERBY is now before WHERE clause')
918 -
919 -    def dgroupby(self, S, _parent=None):
920 -        _context = self.Context(_parent, self._scanner, 'dgroupby', [S])
921 -        groupby = self.groupby(S, _context)
922 -        if groupby: warn('GROUPBY is now before WHERE clause')
923 -
924 -    def dlimit_offset(self, S, _parent=None):
925 -        _context = self.Context(_parent, self._scanner, 'dlimit_offset', [S])
926 -        limit_offset = self.limit_offset(S, _context)
927 -        if limit_offset: warn('LIMIT/OFFSET are now before WHERE clause')
928 -
929      def groupby(self, S, _parent=None):
930          _context = self.Context(_parent, self._scanner, 'groupby', [S])
931          _token = self._peek('GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
932          if _token == 'GROUPBY':
933              GROUPBY = self._scan('GROUPBY', context=_context)
934 -            variables = self.variables(S, _context)
935 -            S.set_groupby(variables); return True
936 -        elif 1:
937 +            nodes = []
938 +            expr_add = self.expr_add(S, _context)
939 +            nodes.append(expr_add)
940 +            while self._peek("','", 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
941 +                self._scan("','", context=_context)
942 +                expr_add = self.expr_add(S, _context)
943 +                nodes.append(expr_add)
944 +            S.set_groupby(nodes); return True
945 +        else:
946              pass
947 -        else:
948 -            raise runtime.SyntaxError(_token[0], 'Could not match groupby')
949 
950      def having(self, S, _parent=None):
951          _context = self.Context(_parent, self._scanner, 'having', [S])
952 -        _token = self._peek('HAVING', 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', "';'", 'r"\\)"', context=_context)
953 +        _token = self._peek('HAVING', 'WITH', 'r"\\)"', "';'", context=_context)
954          if _token == 'HAVING':
955              HAVING = self._scan('HAVING', context=_context)
956              logical_expr = self.logical_expr(S, _context)
957              S.set_having([logical_expr])
958 -        elif 1:
959 +        else: # in ['WITH', 'r"\\)"', "';'"]
960              pass
961 -        else:
962 -            raise runtime.SyntaxError(_token[0], 'Could not match having')
963 
964      def orderby(self, S, _parent=None):
965          _context = self.Context(_parent, self._scanner, 'orderby', [S])
966 -        _token = self._peek('ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'GROUPBY', 'r"\\)"', context=_context)
967 +        _token = self._peek('ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
968          if _token == 'ORDERBY':
969              ORDERBY = self._scan('ORDERBY', context=_context)
970              nodes = []
971              sort_term = self.sort_term(S, _context)
972              nodes.append(sort_term)
973 -            while self._peek("','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'GROUPBY', 'ORDERBY', 'r"\\)"', context=_context) == "','":
974 +            while self._peek("','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
975                  self._scan("','", context=_context)
976                  sort_term = self.sort_term(S, _context)
977                  nodes.append(sort_term)
978              S.set_orderby(nodes); return True
979 -        elif 1:
980 +        else:
981              pass
982 -        else:
983 -            raise runtime.SyntaxError(_token[0], 'Could not match orderby')
984 
985      def with_(self, S, _parent=None):
986          _context = self.Context(_parent, self._scanner, 'with_', [S])
987 -        _token = self._peek('WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', "';'", 'r"\\)"', context=_context)
988 +        _token = self._peek('WITH', 'r"\\)"', "';'", context=_context)
989          if _token == 'WITH':
990              WITH = self._scan('WITH', context=_context)
991              nodes = []
992              subquery = self.subquery(S, _context)
993              nodes.append(subquery)
994 -            while self._peek("','", 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
995 +            while self._peek("','", 'r"\\)"', "';'", context=_context) == "','":
996                  self._scan("','", context=_context)
997                  subquery = self.subquery(S, _context)
998                  nodes.append(subquery)
999              S.set_with(nodes)
1000 -        elif 1:
1001 +        else: # in ['r"\\)"', "';'"]
1002              pass
1003 -        else:
1004 -            raise runtime.SyntaxError(_token[0], 'Could not match with_')
1005 
1006      def subquery(self, S, _parent=None):
1007          _context = self.Context(_parent, self._scanner, 'subquery', [S])
1008          variables = self.variables(S, _context)
1009          node = SubQuery() ; node.set_aliases(variables)
@@ -331,11 +299,11 @@
1010          sort_meth = self.sort_meth(_context)
1011          return SortTerm(expr_add, sort_meth)
1012 
1013      def sort_meth(self, _parent=None):
1014          _context = self.Context(_parent, self._scanner, 'sort_meth', [])
1015 -        _token = self._peek('SORT_DESC', 'SORT_ASC', "','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'GROUPBY', 'ORDERBY', 'r"\\)"', context=_context)
1016 +        _token = self._peek('SORT_DESC', 'SORT_ASC', "','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
1017          if _token == 'SORT_DESC':
1018              SORT_DESC = self._scan('SORT_DESC', context=_context)
1019              return 0
1020          elif _token == 'SORT_ASC':
1021              SORT_ASC = self._scan('SORT_ASC', context=_context)
@@ -349,65 +317,63 @@
1022          offset = self.offset(R, _context)
1023          return limit or offset
1024 
1025      def limit(self, R, _parent=None):
1026          _context = self.Context(_parent, self._scanner, 'limit', [R])
1027 -        _token = self._peek('LIMIT', 'OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'GROUPBY', 'ORDERBY', 'r"\\)"', context=_context)
1028 +        _token = self._peek('LIMIT', 'OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
1029          if _token == 'LIMIT':
1030              LIMIT = self._scan('LIMIT', context=_context)
1031              INT = self._scan('INT', context=_context)
1032              R.set_limit(int(INT)); return True
1033 -        else:
1034 +        else: # in ['OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"']
1035              pass
1036 
1037      def offset(self, R, _parent=None):
1038          _context = self.Context(_parent, self._scanner, 'offset', [R])
1039 -        _token = self._peek('OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'GROUPBY', 'ORDERBY', 'r"\\)"', 'LIMIT', context=_context)
1040 +        _token = self._peek('OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
1041          if _token == 'OFFSET':
1042              OFFSET = self._scan('OFFSET', context=_context)
1043              INT = self._scan('INT', context=_context)
1044              R.set_offset(int(INT)); return True
1045 -        else:
1046 +        else: # in ['WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"']
1047              pass
1048 
1049      def where(self, S, _parent=None):
1050          _context = self.Context(_parent, self._scanner, 'where', [S])
1051 -        _token = self._peek('WHERE', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'r"\\)"', context=_context)
1052 +        _token = self._peek('WHERE', 'HAVING', "';'", 'WITH', 'r"\\)"', context=_context)
1053          if _token == 'WHERE':
1054              WHERE = self._scan('WHERE', context=_context)
1055              restriction = self.restriction(S, _context)
1056              S.set_where(restriction)
1057 -        elif 1:
1058 +        else: # in ['HAVING', "';'", 'WITH', 'r"\\)"']
1059              pass
1060 -        else:
1061 -            raise runtime.SyntaxError(_token[0], 'Could not match where')
1062 
1063      def restriction(self, S, _parent=None):
1064          _context = self.Context(_parent, self._scanner, 'restriction', [S])
1065          rels_or = self.rels_or(S, _context)
1066          node = rels_or
1067 -        while self._peek("','", 'r"\\)"', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', context=_context) == "','":
1068 +        while self._peek("','", 'r"\\)"', 'HAVING', "';'", 'WITH', context=_context) == "','":
1069              self._scan("','", context=_context)
1070              rels_or = self.rels_or(S, _context)
1071              node = And(node, rels_or)
1072          return node
1073 
1074      def rels_or(self, S, _parent=None):
1075          _context = self.Context(_parent, self._scanner, 'rels_or', [S])
1076          rels_and = self.rels_and(S, _context)
1077          node = rels_and
1078 -        while self._peek('OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', context=_context) == 'OR':
1079 +        while self._peek('OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', context=_context) == 'OR':
1080              OR = self._scan('OR', context=_context)
1081              rels_and = self.rels_and(S, _context)
1082              node = Or(node, rels_and)
1083          return node
1084 
1085      def rels_and(self, S, _parent=None):
1086          _context = self.Context(_parent, self._scanner, 'rels_and', [S])
1087          rels_not = self.rels_not(S, _context)
1088          node = rels_not
1089 -        while self._peek('AND', 'OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', context=_context) == 'AND':
1090 +        while self._peek('AND', 'OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', context=_context) == 'AND':
1091              AND = self._scan('AND', context=_context)
1092              rels_not = self.rels_not(S, _context)
1093              node = And(node, rels_not)
1094          return node
1095 
@@ -457,79 +423,81 @@
1096          R_TYPE = self._scan('R_TYPE', context=_context)
1097          return Relation(R_TYPE)
1098 
1099      def opt_left(self, S, _parent=None):
1100          _context = self.Context(_parent, self._scanner, 'opt_left', [S])
1101 -        _token = self._peek('QMARK', 'R_TYPE', context=_context)
1102 +        _token = self._peek('QMARK', 'R_TYPE', 'CMP_OP', "'IN'", context=_context)
1103          if _token == 'QMARK':
1104              QMARK = self._scan('QMARK', context=_context)
1105              return 'left'
1106 -        else: # == 'R_TYPE'
1107 +        else: # in ['R_TYPE', 'CMP_OP', "'IN'"]
1108              pass
1109 
1110      def opt_right(self, S, _parent=None):
1111          _context = self.Context(_parent, self._scanner, 'opt_right', [S])
1112 -        _token = self._peek('QMARK', 'AND', 'OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', context=_context)
1113 +        _token = self._peek('QMARK', 'AND', 'OR', "','", 'r"\\)"', 'WITH', 'HAVING', "';'", context=_context)
1114          if _token == 'QMARK':
1115              QMARK = self._scan('QMARK', context=_context)
1116              return 'right'
1117 -        else:
1118 +        else: # in ['AND', 'OR', "','", 'r"\\)"', 'WITH', 'HAVING', "';'"]
1119              pass
1120 
1121      def logical_expr(self, S, _parent=None):
1122          _context = self.Context(_parent, self._scanner, 'logical_expr', [S])
1123          exprs_or = self.exprs_or(S, _context)
1124          node = exprs_or
1125 -        while self._peek("','", 'r"\\)"', 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', "';'", context=_context) == "','":
1126 +        while self._peek("','", 'r"\\)"', 'WITH', "';'", context=_context) == "','":
1127              self._scan("','", context=_context)
1128              exprs_or = self.exprs_or(S, _context)
1129              node = And(node, exprs_or)
1130          return node
1131 
1132      def exprs_or(self, S, _parent=None):
1133          _context = self.Context(_parent, self._scanner, 'exprs_or', [S])
1134          exprs_and = self.exprs_and(S, _context)
1135          node = exprs_and
1136 -        while self._peek('OR', "','", 'r"\\)"', 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', "';'", context=_context) == 'OR':
1137 +        while self._peek('OR', "','", 'r"\\)"', 'WITH', "';'", context=_context) == 'OR':
1138              OR = self._scan('OR', context=_context)
1139              exprs_and = self.exprs_and(S, _context)
1140              node = Or(node, exprs_and)
1141          return node
1142 
1143      def exprs_and(self, S, _parent=None):
1144          _context = self.Context(_parent, self._scanner, 'exprs_and', [S])
1145          exprs_not = self.exprs_not(S, _context)
1146          node = exprs_not
1147 -        while self._peek('AND', 'OR', "','", 'r"\\)"', 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', "';'", context=_context) == 'AND':
1148 +        while self._peek('AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", context=_context) == 'AND':
1149              AND = self._scan('AND', context=_context)
1150              exprs_not = self.exprs_not(S, _context)
1151              node = And(node, exprs_not)
1152          return node
1153 
1154      def exprs_not(self, S, _parent=None):
1155          _context = self.Context(_parent, self._scanner, 'exprs_not', [S])
1156 -        _token = self._peek('NOT', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
1157 +        _token = self._peek('NOT', 'r"\\("', 'UNARY_OP', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
1158          if _token == 'NOT':
1159              NOT = self._scan('NOT', context=_context)
1160              balanced_expr = self.balanced_expr(S, _context)
1161              return Not(balanced_expr)
1162          else:
1163              balanced_expr = self.balanced_expr(S, _context)
1164              return balanced_expr
1165 
1166      def balanced_expr(self, S, _parent=None):
1167          _context = self.Context(_parent, self._scanner, 'balanced_expr', [S])
1168 -        _token = self._peek('r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
1169 +        _token = self._peek('r"\\("', 'UNARY_OP', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
1170          if _token == 'r"\\("':
1171              self._scan('r"\\("', context=_context)
1172              logical_expr = self.logical_expr(S, _context)
1173              self._scan('r"\\)"', context=_context)
1174              return logical_expr
1175          elif 1:
1176              expr_add = self.expr_add(S, _context)
1177 +            opt_left = self.opt_left(S, _context)
1178              expr_op = self.expr_op(S, _context)
1179 -            expr_op.insert(0, expr_add); return expr_op
1180 +            opt_right = self.opt_right(S, _context)
1181 +            expr_op.insert(0, expr_add); expr_op.set_optional(opt_left, opt_right); return expr_op
1182          else:
1183              raise runtime.SyntaxError(_token[0], 'Could not match balanced_expr')
1184 
1185      def expr_op(self, S, _parent=None):
1186          _context = self.Context(_parent, self._scanner, 'expr_op', [S])
@@ -545,31 +513,31 @@
1187      def variables(self, S, _parent=None):
1188          _context = self.Context(_parent, self._scanner, 'variables', [S])
1189          vars = []
1190          var = self.var(S, _context)
1191          vars.append(var)
1192 -        while self._peek("','", 'BEING', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'GROUPBY', 'r"\\)"', context=_context) == "','":
1193 +        while self._peek("','", 'BEING', context=_context) == "','":
1194              self._scan("','", context=_context)
1195              var = self.var(S, _context)
1196              vars.append(var)
1197          return vars
1198 
1199      def decl_vars(self, R, _parent=None):
1200          _context = self.Context(_parent, self._scanner, 'decl_vars', [R])
1201          E_TYPE = self._scan('E_TYPE', context=_context)
1202          var = self.var(R, _context)
1203 -        while self._peek("','", 'R_TYPE', 'QMARK', 'WHERE', '":"', 'HAVING', "';'", 'MUL_OP', 'BEING', 'WITH', 'GROUPBY', 'ORDERBY', 'ADD_OP', 'LIMIT', 'OFFSET', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', "'IN'", 'AND', 'OR', context=_context) == "','":
1204 +        while self._peek("','", 'R_TYPE', 'QMARK', 'WHERE', '":"', 'CMP_OP', "'IN'", 'HAVING', "';'", 'POW_OP', 'BEING', 'WITH', 'MUL_OP', 'r"\\)"', 'ADD_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'AND', 'OR', context=_context) == "','":
1205              R.add_main_variable(E_TYPE, var)
1206              self._scan("','", context=_context)
1207              E_TYPE = self._scan('E_TYPE', context=_context)
1208              var = self.var(R, _context)
1209          R.add_main_variable(E_TYPE, var)
1210 
1211      def decl_rels(self, R, _parent=None):
1212          _context = self.Context(_parent, self._scanner, 'decl_rels', [R])
1213          simple_rel = self.simple_rel(R, _context)
1214 -        while self._peek("','", 'WHERE', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'r"\\)"', context=_context) == "','":
1215 +        while self._peek("','", 'WHERE', 'HAVING', "';'", 'WITH', 'r"\\)"', context=_context) == "','":
1216              R.add_main_relation(simple_rel)
1217              self._scan("','", context=_context)
1218              simple_rel = self.simple_rel(R, _context)
1219          R.add_main_relation(simple_rel)
1220 
@@ -581,35 +549,56 @@
1221          expr_add = self.expr_add(R, _context)
1222          e.append(Comparison('=', expr_add)) ; return e
1223 
1224      def expr(self, S, _parent=None):
1225          _context = self.Context(_parent, self._scanner, 'expr', [S])
1226 -        _token = self._peek('CMP_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
1227 +        _token = self._peek('CMP_OP', 'UNARY_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
1228          if _token == 'CMP_OP':
1229              CMP_OP = self._scan('CMP_OP', context=_context)
1230              expr_add = self.expr_add(S, _context)
1231              return Comparison(CMP_OP.upper(), expr_add)
1232          else:
1233              expr_add = self.expr_add(S, _context)
1234              return Comparison('=', expr_add)
1235 
1236      def expr_add(self, S, _parent=None):
1237          _context = self.Context(_parent, self._scanner, 'expr_add', [S])
1238 -        expr_mul = self.expr_mul(S, _context)
1239 -        node = expr_mul
1240 -        while self._peek('ADD_OP', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', "'IN'", 'GROUPBY', 'QMARK', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'ADD_OP':
1241 -            ADD_OP = self._scan('ADD_OP', context=_context)
1242 +        _token = self._peek('UNARY_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
1243 +        if _token != 'UNARY_OP':
1244              expr_mul = self.expr_mul(S, _context)
1245 -            node = MathExpression( ADD_OP, node, expr_mul )
1246 -        return node
1247 +            node = expr_mul
1248 +            while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'ADD_OP':
1249 +                ADD_OP = self._scan('ADD_OP', context=_context)
1250 +                expr_mul = self.expr_mul(S, _context)
1251 +                node = MathExpression( ADD_OP, node, expr_mul )
1252 +            return node
1253 +        else: # == 'UNARY_OP'
1254 +            UNARY_OP = self._scan('UNARY_OP', context=_context)
1255 +            expr_mul = self.expr_mul(S, _context)
1256 +            node = UnaryExpression( UNARY_OP, expr_mul )
1257 +            while self._peek('ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'ADD_OP':
1258 +                ADD_OP = self._scan('ADD_OP', context=_context)
1259 +                expr_mul = self.expr_mul(S, _context)
1260 +                node = MathExpression( ADD_OP, node, expr_mul )
1261 +            return node
1262 
1263      def expr_mul(self, S, _parent=None):
1264          _context = self.Context(_parent, self._scanner, 'expr_mul', [S])
1265 +        expr_pow = self.expr_pow(S, _context)
1266 +        node = expr_pow
1267 +        while self._peek('MUL_OP', 'ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'MUL_OP':
1268 +            MUL_OP = self._scan('MUL_OP', context=_context)
1269 +            expr_pow = self.expr_pow(S, _context)
1270 +            node = MathExpression( MUL_OP, node, expr_pow)
1271 +        return node
1272 +
1273 +    def expr_pow(self, S, _parent=None):
1274 +        _context = self.Context(_parent, self._scanner, 'expr_pow', [S])
1275          expr_base = self.expr_base(S, _context)
1276          node = expr_base
1277 -        while self._peek('MUL_OP', 'ADD_OP', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', "'IN'", 'GROUPBY', 'QMARK', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'MUL_OP':
1278 -            MUL_OP = self._scan('MUL_OP', context=_context)
1279 +        while self._peek('POW_OP', 'MUL_OP', 'ADD_OP', 'QMARK', 'r"\\)"', "','", 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == 'POW_OP':
1280 +            POW_OP = self._scan('POW_OP', context=_context)
1281              expr_base = self.expr_base(S, _context)
1282              node = MathExpression( MUL_OP, node, expr_base)
1283          return node
1284 
1285      def expr_base(self, S, _parent=None):
@@ -636,13 +625,13 @@
1286      def func(self, S, _parent=None):
1287          _context = self.Context(_parent, self._scanner, 'func', [S])
1288          FUNCTION = self._scan('FUNCTION', context=_context)
1289          self._scan('r"\\("', context=_context)
1290          F = Function(FUNCTION)
1291 -        if self._peek('r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
1292 +        if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
1293              expr_add = self.expr_add(S, _context)
1294 -            while self._peek("','", 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', "'IN'", 'GROUPBY', 'QMARK', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == "','":
1295 +            while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == "','":
1296                  F.append(expr_add)
1297                  self._scan("','", context=_context)
1298                  expr_add = self.expr_add(S, _context)
1299              F.append(expr_add)
1300          self._scan('r"\\)"', context=_context)
@@ -651,13 +640,13 @@
1301      def in_expr(self, S, _parent=None):
1302          _context = self.Context(_parent, self._scanner, 'in_expr', [S])
1303          self._scan("'IN'", context=_context)
1304          self._scan('r"\\("', context=_context)
1305          F = Function('IN')
1306 -        if self._peek('r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
1307 +        if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
1308              expr_add = self.expr_add(S, _context)
1309 -            while self._peek("','", 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', "'IN'", 'GROUPBY', 'QMARK', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == "','":
1310 +            while self._peek("','", 'QMARK', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', 'R_TYPE', "'IN'", 'GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == "','":
1311                  F.append(expr_add)
1312                  self._scan("','", context=_context)
1313                  expr_add = self.expr_add(S, _context)
1314              F.append(expr_add)
1315          self._scan('r"\\)"', context=_context)
@@ -707,37 +696,15 @@
1316 
1317  def parse(rule, text):
1318      P = Hercule(HerculeScanner(text))
1319      return runtime.wrap_error_reporter(P, rule)
1320 
1321 -if __name__ == 'old__main__':
1322 +if __name__ == '__main__':
1323      from sys import argv, stdin
1324      if len(argv) >= 2:
1325          if len(argv) >= 3:
1326              f = open(argv[2],'r')
1327          else:
1328              f = stdin
1329          print parse(argv[1], f.read())
1330      else: print >>sys.stderr, 'Args:  <rule> [<filename>]'
1331  # End -- grammar generated by Yapps
1332 -"""Main parser command.
1333 -
1334 -"""
1335 -__docformat__ = "restructuredtext en"
1336 -
1337 -if __name__ == '__main__':
1338 -    from sys import argv
1339 -
1340 -    parser = Hercule(HerculeScanner(argv[1]))
1341 -    e_types = {}
1342 -    # parse the RQL string
1343 -    try:
1344 -        tree = parser.goal(e_types)
1345 -        print '-'*80
1346 -        print tree
1347 -        print '-'*80
1348 -        print repr(tree)
1349 -        print e_types
1350 -    except SyntaxError, ex:
1351 -        # try to get error message from yapps
1352 -        from yapps.runtime import print_error
1353 -        print_error(ex, parser._scanner)
diff --git a/stcheck.py b/stcheck.py
@@ -1,6 +1,6 @@
1354 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1355 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1356  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1357  #
1358  # This file is part of rql.
1359  #
1360  # rql is free software: you can redistribute it and/or modify it under the
@@ -25,11 +25,11 @@
1361  from logilab.database import UnknownFunction
1362 
1363  from rql._exceptions import BadRQLQuery
1364  from rql.utils import function_description
1365  from rql.nodes import (Relation, VariableRef, Constant, Not, Exists, Function,
1366 -                       And, Variable, variable_refs, make_relation)
1367 +                       And, Variable, Comparison, variable_refs, make_relation)
1368  from rql.stmts import Union
1369 
1370 
1371  def _var_graphid(subvarname, trmap, select):
1372      try:
@@ -370,13 +370,13 @@
1373              try:
1374                  rschema = self.schema.rschema(rtype)
1375              except KeyError:
1376                  state.error('unknown relation `%s`' % rtype)
1377              else:
1378 -                if relation.optional and rschema.final:
1379 -                    state.error("shouldn't use optional on final relation `%s`"
1380 -                                % relation.r_type)
1381 +                if rschema.final and relation.optional not in (None, 'right'):
1382 +                     state.error("optional may only be set on the rhs on final relation `%s`"
1383 +                                 % relation.r_type)
1384                  if self.special_relations.get(rtype) == 'uid':
1385                      if state.var_info.get(lhsvar, 0) & VAR_HAS_UID_REL:
1386                          state.error('can only one uid restriction per variable '
1387                                      '(use IN for %s if desired)' % lhsvar.name)
1388                      else:
@@ -406,10 +406,14 @@
1389 
1390      def visit_mathexpression(self, mathexpr, state):
1391          pass #assert len(mathexpr.children) == 2, len(mathexpr.children)
1392      def leave_mathexpression(self, node, state):
1393          pass
1394 +    def visit_unaryexpression(self, unaryexpr, state):
1395 +        pass #assert len(unaryexpr.children) == 2, len(unaryexpr.children)
1396 +    def leave_unaryexpression(self, node, state):
1397 +        pass
1398 
1399      def visit_function(self, function, state):
1400          try:
1401              funcdescr = function_description(function.name)
1402          except UnknownFunction:
@@ -453,15 +457,20 @@
1403          pass
1404 
1405      def visit_constant(self, constant, state):
1406          #assert len(constant.children)==0
1407          if constant.type == 'etype':
1408 -            if constant.relation().r_type not in ('is', 'is_instance_of'):
1409 -                msg ='using an entity type in only allowed with "is" relation'
1410 -                state.error(msg)
1411 -            if not constant.value in self.schema:
1412 +            if constant.value not in self.schema:
1413                  state.error('unknown entity type %s' % constant.value)
1414 +            rel = constant.relation()
1415 +            if rel is not None:
1416 +                if rel.r_type not in ('is', 'is_instance_of'):
1417 +                    msg ='using an entity type in only allowed with "is" relation'
1418 +                    state.error(msg)
1419 +            elif not (isinstance(constant.parent, Function) and
1420 +                      constant.parent.name == 'CAST'):
1421 +                state.error('Entity types can only be used inside a CAST()')
1422 
1423      def leave_constant(self, node, state):
1424          pass
1425 
1426 
@@ -514,10 +523,39 @@
1427          if node.having:
1428              # if there is a having clause, bloc simplification of variables used in GROUPBY
1429              for term in node.groupby:
1430                  for vref in term.get_nodes(VariableRef):
1431                      bloc_simplification(vref.variable, term)
1432 +            try:
1433 +                vargraph = node.vargraph
1434 +            except AttributeError:
1435 +                vargraph = None
1436 +            # XXX node.having is a list of size 1
1437 +            assert len(node.having) == 1
1438 +            for term in node.having[0].get_nodes(Comparison):
1439 +                lhsvariables = set(vref.variable for vref in term.children[0].get_nodes(VariableRef))
1440 +                rhsvariables = set(vref.variable for vref in term.children[1].get_nodes(VariableRef))
1441 +                for var in lhsvariables | rhsvariables:
1442 +                    var.stinfo.setdefault('having', []).append(term)
1443 +                if vargraph is not None:
1444 +                    for v1 in lhsvariables:
1445 +                        v1 = v1.name
1446 +                        for v2 in rhsvariables:
1447 +                            v2 = v2.name
1448 +                            if v1 != v2:
1449 +                                vargraph.setdefault(v1, []).append(v2)
1450 +                                vargraph.setdefault(v2, []).append(v1)
1451 +                if term.optional in ('left', 'both'):
1452 +                    for var in lhsvariables:
1453 +                        if var.stinfo['attrvar'] is not None:
1454 +                            optcomps = var.stinfo['attrvar'].stinfo.setdefault('optcomparisons', set())
1455 +                            optcomps.add(term)
1456 +                if term.optional in ('right', 'both'):
1457 +                    for var in rhsvariables:
1458 +                        if var.stinfo['attrvar'] is not None:
1459 +                            optcomps = var.stinfo['attrvar'].stinfo.setdefault('optcomparisons', set())
1460 +                            optcomps.add(term)
1461 
1462      def rewrite_shared_optional(self, exists, var, identity_rel_scope=None):
1463          """if variable is shared across multiple scopes, need some tree
1464          rewriting
1465          """
@@ -658,10 +696,13 @@
1466              var.stinfo['rhsrelations'].add(relation)
1467              if vref is rhs.children[0] and rschema.final:
1468                  update_attrvars(var, relation, lhs)
1469 
1470  def update_attrvars(var, relation, lhs):
1471 +    if var.stinfo['relations'] - var.stinfo['rhsrelations']:
1472 +        raise BadRQLQuery('variable %s should not be used as rhs of attribute relation %s'
1473 +                          % (var.name, relation))
1474      # stinfo['attrvars'] is set of couple (lhs variable name, relation name)
1475      # where the `var` attribute variable is used
1476      lhsvar = getattr(lhs, 'variable', None)
1477      try:
1478          var.stinfo['attrvars'].add( (lhsvar, relation.r_type) )
diff --git a/stmts.py b/stmts.py
@@ -1,6 +1,6 @@
1479 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1480 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1481  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1482  #
1483  # This file is part of rql.
1484  #
1485  # rql is free software: you can redistribute it and/or modify it under the
@@ -25,10 +25,11 @@
1486 
1487  from copy import deepcopy
1488  from warnings import warn
1489 
1490  from logilab.common.decorators import cached
1491 +from logilab.common.deprecation import deprecated
1492 
1493  from rql import BadRQLQuery, CoercionError, nodes
1494  from rql.base import BaseNode, Node
1495  from rql.utils import rqlvar_maker, build_visitor_stub
1496 
@@ -45,10 +46,17 @@
1497      for vref in varrefs:
1498          if not refs.has_key(id(vref)):
1499              raise AssertionError('vref %r is not referenced (%r)' % (vref, vref.stmt))
1500      return True
1501 
1502 +class undo_modification(object):
1503 +    def __init__(self, select):
1504 +        self.select = select
1505 +    def __enter__(self):
1506 +        self.select.save_state()
1507 +    def __exit__(self):
1508 +        self.select.recover()
1509 
1510  class ScopeNode(BaseNode):
1511      solutions = ()   # list of possibles solutions for used variables
1512      _varmaker = None # variable names generator, built when necessary
1513      where = None     # where clause node
@@ -352,10 +360,13 @@
1514 
1515      @property
1516      def should_register_op(self):
1517          return self.memorizing and not self.undoing
1518 
1519 +    def undo_modification(self):
1520 +        return undo_modification(self)
1521 +
1522      def save_state(self):
1523          """save the current tree"""
1524          self.undo_manager.push_state()
1525          self.memorizing += 1
1526 
@@ -471,11 +482,11 @@
1527              s.append('OFFSET %s' % self.offset)
1528          if self.where is not None:
1529              s.append('WHERE ' + as_string(self.where))
1530          if self.having:
1531              s.append('HAVING ' + ','.join(as_string(term)
1532 -                                           for term in self.having))
1533 +                                          for term in self.having))
1534          if self.with_:
1535              s.append('WITH ' + ','.join(as_string(term)
1536                                          for term in self.with_))
1537          if self.distinct:
1538              return 'DISTINCT Any ' + ' '.join(s)
@@ -605,10 +616,11 @@
1539          """
1540          if name in self.aliases:
1541              return self.aliases[name]
1542          if colnum is not None: # take care, may be 0
1543              self.aliases[name] = calias = nodes.ColumnAlias(name, colnum)
1544 +            calias.stmt = self
1545              # alias may already have been used as a regular variable, replace it
1546              if name in self.defined_vars:
1547                  var = self.defined_vars.pop(name)
1548                  calias.stinfo['references'] = var.stinfo['references']
1549                  for vref in var.references():
@@ -695,22 +707,21 @@
1550              self.having[self.having.index(oldnode)] = newnode
1551          else:
1552              raise Exception('duh XXX %s' % oldnode)
1553          # XXX no undo/reference support 'by design' (eg breaks things if you add
1554          # it...)
1555 -        # XXX resetting oldnode parent cause pb with cw.test_views (w/ facets)
1556 -        #oldnode.parent = None
1557 +        oldnode.parent = None
1558          newnode.parent = self
1559          return oldnode, self, None
1560 
1561      def remove(self, node):
1562          if node is self.where:
1563              self.where = None
1564          elif node in self.orderby:
1565              self.remove_sort_term(node)
1566          elif node in self.groupby:
1567 -            self.remove_group_var(node)
1568 +            self.remove_group_term(node)
1569          elif node in self.having:
1570              self.having.remove(node)
1571          # XXX selection
1572          else:
1573              raise Exception('duh XXX')
@@ -728,11 +739,11 @@
1574                  self.remove_node(rel)
1575              # XXX may have other nodes between vref and the sort term
1576              elif isinstance(vref.parent, nodes.SortTerm):
1577                  self.remove_sort_term(vref.parent)
1578              elif vref in self.groupby:
1579 -                self.remove_group_var(vref)
1580 +                self.remove_group_term(vref)
1581              else: # selected variable
1582                  self.remove_selected(vref)
1583          # effective undefine operation
1584          if self.should_register_op:
1585              from rql.undo import UndefineVarOperation
@@ -794,21 +805,23 @@
1586          vref.parent = self
1587          if self.should_register_op:
1588              from rql.undo import AddGroupOperation
1589              self.undo_manager.add_operation(AddGroupOperation(vref))
1590 
1591 -    def remove_group_var(self, vref):
1592 +    def remove_group_term(self, term):
1593          """remove the group variable and the group node if necessary"""
1594          if self.should_register_op:
1595              from rql.undo import RemoveGroupOperation
1596 -            self.undo_manager.add_operation(RemoveGroupOperation(vref))
1597 -        vref.unregister_reference()
1598 -        self.groupby.remove(vref)
1599 +            self.undo_manager.add_operation(RemoveGroupOperation(term))
1600 +        for vref in term.iget_nodes(nodes.VariableRef):
1601 +            vref.unregister_reference()
1602 +        self.groupby.remove(term)
1603 +    remove_group_var = deprecated('[rql 0.29] use remove_group_term instead')(remove_group_term)
1604 
1605      def remove_groups(self):
1606          for vref in self.groupby[:]:
1607 -            self.remove_group_var(vref)
1608 +            self.remove_group_term(vref)
1609 
1610      def add_sort_var(self, var, asc=True):
1611          """add var in 'orderby' constraints
1612          asc is a boolean indicating the sort order (ascendent or descendent)
1613          """
diff --git a/test/unittest_analyze.py b/test/unittest_analyze.py
@@ -529,11 +529,11 @@
1614          sols = sorted(node.solutions)
1615          self.assertEqual(sols, [{'P': 'Person', 'S': 'Company', 'N': 'Int'},
1616                                  {'P': 'Student', 'S': 'Company', 'N': 'Int'}])
1617 
1618 
1619 -    def test_nongrer_not_u_ownedby_u(self):
1620 +    def test_nonregr_not_u_ownedby_u(self):
1621          node = self.helper.parse('Any U WHERE NOT U owned_by U')
1622          self.helper.compute_solutions(node, debug=DEBUG)
1623          sols = sorted(node.children[0].solutions)
1624          self.assertEqual(sols, [{'U': 'Person'}])
1625 
diff --git a/test/unittest_editextensions.py b/test/unittest_editextensions.py
@@ -1,6 +1,6 @@
1626 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1627 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1628  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1629  #
1630  # This file is part of rql.
1631  #
1632  # rql is free software: you can redistribute it and/or modify it under the
@@ -17,14 +17,15 @@
1633  # with rql. If not, see <http://www.gnu.org/licenses/>.
1634 
1635  from logilab.common.testlib import TestCase, unittest_main
1636 
1637  from rql import parse
1638 +from rql.nodes import Exists
1639  from rql.editextensions import *
1640 
1641  class RQLUndoTestCase(TestCase):
1642 -    
1643 +
1644      def test_selected(self):
1645          rqlst = parse('Person X')
1646          orig = rqlst.as_string()
1647          rqlst.save_state()
1648          select = rqlst.children[0]
@@ -38,11 +39,11 @@
1649          rqlst.recover()
1650          # check equivalence after recovering
1651          self.assertEqual(rqlst.as_string(), orig)
1652          # check references after recovering
1653          rqlst.check_references()
1654 -    
1655 +
1656      def test_selected3(self):
1657          rqlst = parse('Any lower(N) WHERE X is Person, X name N')
1658          orig = rqlst.as_string()
1659          rqlst.save_state()
1660          select = rqlst.children[0]
@@ -56,11 +57,11 @@
1661          rqlst.recover()
1662          # check equivalence after recovering
1663          self.assertEqual(rqlst.as_string(), orig)
1664          # check references after recovering
1665          rqlst.check_references()
1666 -        
1667 +
1668      def test_undefine_1(self):
1669          rqlst = parse('Person X, Y WHERE X travaille_pour Y')
1670          orig = rqlst.as_string()
1671          rqlst.save_state()
1672          rqlst.children[0].undefine_variable(rqlst.children[0].defined_vars['Y'])
@@ -71,11 +72,11 @@
1673          rqlst.recover()
1674          # check equivalence
1675          self.assertEqual(rqlst.as_string(), orig)
1676          # check references after recovering
1677          rqlst.check_references()
1678 -        
1679 +
1680      def test_undefine_2(self):
1681          rqlst = parse('Person X')
1682          orig = rqlst.as_string()
1683          rqlst.save_state()
1684          rqlst.children[0].undefine_variable(rqlst.children[0].defined_vars['X'])
@@ -88,9 +89,26 @@
1685          rqlst.recover()
1686          # check equivalence
1687          self.assertEqual(rqlst.as_string(), orig)
1688          # check references after recovering
1689          rqlst.check_references()
1690 -        
1691 -        
1692 +
1693 +
1694 +    def test_remove_exists(self):
1695 +        rqlst = parse('Any U,COUNT(P) GROUPBY U WHERE U is CWUser, P? patch_reviewer U, EXISTS(P in_state S AND S name "pouet")').children[0]
1696 +        orig = rqlst.as_string()
1697 +        rqlst.save_state()
1698 +        n = [r for r in rqlst.get_nodes(Exists)][0].query
1699 +        rqlst.remove_node(n)
1700 +        # check operations
1701 +        self.assertEqual(rqlst.as_string(), 'Any U,COUNT(P) GROUPBY U WHERE U is CWUser, P? patch_reviewer U')
1702 +        # check references before recovering
1703 +        rqlst.check_references()
1704 +        rqlst.recover()
1705 +        # check equivalence
1706 +        self.assertEqual(rqlst.as_string(), orig)
1707 +        # check references after recovering
1708 +        rqlst.check_references()
1709 +
1710 +
1711  if __name__ == '__main__':
1712      unittest_main()
diff --git a/test/unittest_nodes.py b/test/unittest_nodes.py
@@ -1,7 +1,7 @@
1713  # -*- coding: iso-8859-1 -*-
1714 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1715 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1716  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1717  #
1718  # This file is part of rql.
1719  #
1720  # rql is free software: you can redistribute it and/or modify it under the
@@ -230,15 +230,15 @@
1721          self.assertEqual(tree.as_string(), 'Any X GROUPBY X')
1722          tree.recover()
1723          tree.check_references()
1724          self.assertEqual(tree.as_string(), 'Any X')
1725 
1726 -    def test_select_remove_group_var(self):
1727 +    def test_select_remove_group_term(self):
1728          tree = self._parse('Any X GROUPBY X')
1729          tree.save_state()
1730          select = tree.children[0]
1731 -        select.remove_group_var(select.groupby[0])
1732 +        select.remove_group_term(select.groupby[0])
1733          tree.check_references()
1734          self.assertEqual(tree.as_string(), 'Any X')
1735          tree.recover()
1736          tree.check_references()
1737          self.assertEqual(tree.as_string(), 'Any X GROUPBY X')
@@ -562,10 +562,16 @@
1738 
1739      def test_get_description_mainvar_symrel(self):
1740          tree = sparse('Any X,R,D,Y WHERE X work_for R, R creation_date D, Y connait X')
1741          self.assertEqual(tree.get_description(0), [['Person, Student', 'work_for', 'creation_date', 'connait']])
1742 
1743 +    def test_get_description_cast(self):
1744 +        tree = sparse('Any CAST(String, Y) WHERE X creation_date Y')
1745 +        select = tree.children[0]
1746 +        self.assertEqual(select.selection[0].get_type(), 'String')
1747 +        self.assertEqual(tree.get_description(0), [['String']])
1748 +
1749 
1750  class GetNodesFunctionTest(TestCase):
1751      def test_known_values_1(self):
1752          tree = parse('Any X where X name "turlututu"').children[0]
1753          constants = tree.get_nodes(nodes.Constant)
diff --git a/test/unittest_parser.py b/test/unittest_parser.py
@@ -1,7 +1,7 @@
1754  # -*- coding: iso-8859-1 -*-
1755 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1756 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1757  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1758  #
1759  # This file is part of rql.
1760  #
1761  # rql is free software: you can redistribute it and/or modify it under the
@@ -80,10 +80,11 @@
1762      "INSERT Personne X: X nom 'bidule';",
1763      "INSERT Personne X, Personne Y: X nom 'bidule', Y nom 'chouette', X ami Y;",
1764      "INSERT Person X: X nom 'bidule', X ami Y WHERE Y nom 'chouette';",
1765      "SET X nom 'toto', X prenom 'original' WHERE X is Person, X nom 'bidule';",
1766      "SET X know Y WHERE X ami Y;",
1767 +    "SET X value -Y WHERE X value Y;",
1768      "DELETE Person X WHERE X nom 'toto';",
1769      "DELETE X ami Y WHERE X is Person, X nom 'toto';",
1770 
1771      # some additional cases
1772      'INSERT Person X : X name "bidule", Y workfor X WHERE Y name "logilab";',
@@ -151,10 +152,19 @@
1773      ' HAVING 1+2 < COUNT(T1);',
1774 
1775      'Any X,Y,A ORDERBY Y '
1776      'WHERE A done_for Y, X split_into Y, A diem D '
1777      'HAVING MIN(D) < "2010-07-01", MAX(D) >= "2010-07-01";',
1778 +
1779 +    'Any YEAR(XD),COUNT(X) GROUPBY YEAR(XD) ORDERBY YEAR(XD) WHERE X date XD;',
1780 +    'Any YEAR(XD),COUNT(X) GROUPBY 1 ORDERBY 1 WHERE X date XD;',
1781 +
1782 +    'Any -1.0;',
1783 +
1784 +    'Any U,G WHERE U login UL, G name GL, G is CWGroup HAVING UPPER(UL)=UPPER(GL)?;',
1785 +
1786 +    'Any U,G WHERE U login UL, G name GL, G is CWGroup HAVING UPPER(UL)?=UPPER(GL);',
1787      )
1788 
1789  class ParserHercule(TestCase):
1790      _syntaxerr = SyntaxError
1791 
@@ -308,11 +318,11 @@
1792      def test_spec(self):
1793          """test all RQL string found in the specification and test they are well parsed"""
1794          for rql in SPEC_QUERIES:
1795  #            print "Orig:", rql
1796  #            print "Resu:", rqltree
1797 -            yield self.assert_, self.parse(rql, True)
1798 +            yield self.parse, rql, True
1799 
1800      def test_raise_badsyntax_error(self):
1801          for rql in BAD_SYNTAX_QUERIES:
1802              yield self.assertRaises, self._syntaxerr, self.parse, rql
1803 
diff --git a/test/unittest_stcheck.py b/test/unittest_stcheck.py
@@ -1,6 +1,6 @@
1804 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1805 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1806  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1807  #
1808  # This file is part of rql.
1809  #
1810  # rql is free software: you can redistribute it and/or modify it under the
@@ -13,10 +13,12 @@
1811  # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
1812  # details.
1813  #
1814  # You should have received a copy of the GNU Lesser General Public License along
1815  # with rql. If not, see <http://www.gnu.org/licenses/>.
1816 +from __future__ import with_statement
1817 +
1818  from logilab.common.testlib import TestCase, unittest_main
1819 
1820  from rql import RQLHelper, BadRQLQuery, stmts, nodes
1821 
1822  from unittest_analyze import DummySchema
@@ -310,8 +312,15 @@
1823      def test_subquery_annotation_2(self):
1824          rqlst = self.parse('Any X,ET WITH X,ET BEING (Any X, ET WHERE C is ET, EXISTS(X work_for C))').children[0]
1825          C = rqlst.with_[0].query.children[0].defined_vars['C']
1826          self.failUnless(C.scope is rqlst.with_[0].query.children[0], C.scope)
1827          self.assertEqual(len(C.stinfo['relations']), 2)
1828 +        X = rqlst.get_variable('X')
1829 +        self.failUnless(X.scope is rqlst, X.scope)
1830 +
1831 +    def test_no_attr_var_if_uid_rel(self):
1832 +        with self.assertRaises(BadRQLQuery) as cm:
1833 +            self.parse('Any X, Y WHERE X work_for Z, Y work_for Z, X eid > Y')
1834 +        self.assertEqual(str(cm.exception), 'variable Y should not be used as rhs of attribute relation X eid > Y')
1835 
1836  if __name__ == '__main__':
1837      unittest_main()
diff --git a/undo.py b/undo.py
@@ -1,6 +1,6 @@
1838 -# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1839 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
1840  # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
1841  #
1842  # This file is part of rql.
1843  #
1844  # rql is free software: you can redistribute it and/or modify it under the
@@ -17,11 +17,11 @@
1845  # with rql. If not, see <http://www.gnu.org/licenses/>.
1846  """Manages undos on RQL syntax trees."""
1847 
1848  __docformat__ = "restructuredtext en"
1849 
1850 -from rql.nodes import VariableRef, Variable, BinaryNode
1851 +from rql.nodes import Exists, VariableRef, Variable, BinaryNode
1852  from rql.stmts import Select
1853 
1854  class SelectionManager(object):
1855      """Manage the operation stacks."""
1856 
@@ -140,23 +140,25 @@
1857      """Defines how to undo remove_node()."""
1858 
1859      def __init__(self, node, parent, stmt, index):
1860          NodeOperation.__init__(self, node, stmt)
1861          self.node_parent = parent
1862 -        #if isinstance(parent, Select):
1863 -        #    assert self.node is parent.where
1864 +        if index is None:
1865 +            assert isinstance(parent, (Exists, Select)), (node, parent)
1866          self.index = index
1867          # XXX FIXME : find a better way to do that
1868          self.binary_remove = isinstance(node, BinaryNode)
1869 
1870      def undo(self, selection):
1871          """undo the operation on the selection"""
1872          parent = self.node_parent
1873          if self.index is None:
1874 -            assert isinstance(parent, Select)
1875 -            sibling = parent.where = self.node
1876 -            parent.where = self.node
1877 +            if isinstance(parent, Select):
1878 +                parent.where = self.node
1879 +            else: # Exists
1880 +                parent.query = self.node
1881 +            sibling = self.node
1882          if self.binary_remove:
1883              # if 'parent' was a BinaryNode, then first reinsert the removed node
1884              # at the same pos in the original 'parent' Binary Node, and then
1885              # reinsert this BinaryNode in its parent's children list
1886              # WARNING : the removed node sibling's parent is no longer the
@@ -192,11 +194,11 @@
1887  class AddGroupOperation(NodeOperation):
1888      """Defines how to undo 'add group'."""
1889 
1890      def undo(self, selection):
1891          """undo the operation on the selection"""
1892 -        self.stmt.remove_group_var(self.node)
1893 +        self.stmt.remove_group_term(self.node)
1894 
1895  class RemoveGroupOperation(NodeOperation):
1896      """Defines how to undo 'remove group'."""
1897 
1898      def __init__(self, node):
diff --git a/utils.py b/utils.py
@@ -66,11 +66,11 @@
1899                  'GROUPBY', 'HAVING', 'ORDERBY', 'ASC', 'DESC',
1900                  'LIMIT', 'OFFSET'))
1901 
1902 
1903  from logilab.common.decorators import monkeypatch
1904 -from logilab.database import SQL_FUNCTIONS_REGISTRY, FunctionDescr
1905 +from logilab.database import SQL_FUNCTIONS_REGISTRY, FunctionDescr, CAST
1906 
1907  RQL_FUNCTIONS_REGISTRY = SQL_FUNCTIONS_REGISTRY.copy()
1908 
1909  @monkeypatch(FunctionDescr)
1910  def st_description(self, funcnode, mainindex, tr):
@@ -83,10 +83,23 @@
1911  def st_check_backend(self, backend, funcnode):
1912      if not self.supports(backend):
1913          raise BadRQLQuery("backend %s doesn't support function %s" % (backend, self.name))
1914 
1915 
1916 +@monkeypatch(FunctionDescr)
1917 +def rql_return_type(self, funcnode):
1918 +    return self.rtype
1919 +
1920 +@monkeypatch(CAST)
1921 +def st_description(self, funcnode, mainindex, tr):
1922 +    return self.rql_return_type(funcnode)
1923 +
1924 +@monkeypatch(CAST)
1925 +def rql_return_type(self, funcnode):
1926 +    return funcnode.children[0].value
1927 +
1928 +
1929  def iter_funcnode_variables(funcnode):
1930      for term in funcnode.children:
1931          try:
1932              yield term.variable.stinfo['attrvar'] or term
1933          except AttributeError, ex: