oldstable is 0.30 oldstable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 09 Dec 2011 12:12:43 +0100
brancholdstable
changeset 677 31130f18a5ef
parent 624 be394528fdc1 (current diff)
parent 666 8828046c2d3b (diff)
oldstable is 0.30
--- a/.hgtags	Wed May 11 09:02:33 2011 +0200
+++ b/.hgtags	Fri Dec 09 12:12:43 2011 +0100
@@ -43,7 +43,6 @@
 7dd29e42751ebebae4b50126dfb2071d9b2e8de1 rql-debian-version-0.23.0-1
 5cb31b7a463ea8fcc56da4e768648a2f818ec0ee rql-version-0.24.0
 4f8562728585d53053e914171180e623e73ac235 rql-debian-version-0.24.0-1
-4025f1f02d1da65d26eada37708409984942c432 oldstable
 3d59f6b1cbb90278f3b4374dce36b6e31c7e9884 rql-version-0.25.0
 360a6c3a48393f8d5353198d45fbcf25f9ef5369 rql-debian-version-0.25.0-1
 ae4cba1cf0240c615a8e78c94d81fa05e5ad8bc9 rql-version-0.26.0
@@ -62,3 +61,11 @@
 0a5a70c34c65fccaf64603613d5d295b332e85cb rql-debian-version-0.27.0-1
 ae02408da51e63aa2d1be6ac7170d77060bd0910 rql-version-0.28.0
 21e94bc12c1fcb7f97826fe6aae5dbe62cc4bd06 rql-debian-version-0.28.0-1
+c45e9d1c0db45b2f3f158f6b1be4dbe25d28c84d rql-version-0.29.0
+78e09096f881259f1f5d3080a78819997fe1eede rql-debian-version-0.29.0-1
+cf4fcca7c28946f384c0b15e59b77231d40148b5 rql-version-0.29.1
+0c9ac2a5635d3f6a224b799aecea836c4277ba21 rql-debian-version-0.29.1-1
+395b876af47bc01048f1d7aacdda069b9f36309c rql-version-0.30.0
+c3ae2279fe701809096d5f6bc50f8d8b4a9fd4fd rql-debian-version-0.30.0-1
+3c17b96750ad5045bd1fb43241da55c81c094bd4 rql-version-0.30.1
+13cd741f8e144265c15bb225fad863234fc8ce16 rql-debian-version-0.30.1-1
--- a/ChangeLog	Wed May 11 09:02:33 2011 +0200
+++ b/ChangeLog	Fri Dec 09 12:12:43 2011 +0100
@@ -2,7 +2,74 @@
 =================
 
 --
-* suport != operator for non equality
+    * #78681: don't crash on column aliases used in outer join
+
+
+2011-09-07  --  0.30.1
+
+    * #74727: allow entity types to end with a capitalized letter
+      provided they contain a lower-cased letter
+
+
+
+2011-08-05  --  0.30.0
+    * #72295: add some missing operators:
+
+        - % (modulo),
+        - ^ (power),
+        - & (bitwise AND),
+        - | (bitwise OR),
+        - # (bitwise XOR),
+        - << (bitwise left shift),
+        - >> (bitwise right shift)
+
+    * #72052: new optional 'optcomparisons' key in variable stinfo, containing
+      HAVING comparison nodes where it's used and optional (eg outer
+      join)
+
+    * #69185: fix syntax error with unary operators by introducing
+      `UnaryExpression` node
+
+    * drop old backward compat for ORDERBY/GROUPBY after where clause
+
+    * fix Comparison.as_string to considerer its optional attribute
+
+
+
+2011-07-27  --  0.29.1
+    * #70264: remove_group_var renamed into remove_group_term and fixed
+      implementation
+
+    * #70416: rql annotator add 'having' list into variable's stinfo, and
+      properly update variable graph
+
+    * #71131: as_string doesn't  propagate encoding/kwargs to subqueries
+
+    * #71132: column alias scope should be handled as variable scope, not bound
+      to subquery
+
+    * #71157: bad analyze when using functions
+
+    * #71415: needs to allow outer join on rhs of final relation and in HAVING express
+
+    * Select.replace must properly reset old node's parent attribute
+
+    * new undo_modification context manager on select nodes
+
+
+
+2011-06-09  --  0.29.0
+    * support != operator for non equality
+
+    * support for CAST function
+
+    * support for regexp-based pattern matching using a REGEXP operator
+
+    * may now GROUPBY functions / column number
+
+    * fix parsing of negative float
+
+
 
 2011-01-12  --  0.28.0
     * enhance rewrite_shared_optional so one can specify where the new identity
--- a/__pkginfo__.py	Wed May 11 09:02:33 2011 +0200
+++ b/__pkginfo__.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,5 +1,5 @@
 # pylint: disable-msg=W0622
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -20,7 +20,7 @@
 __docformat__ = "restructuredtext en"
 
 modname = "rql"
-numversion = (0, 28, 0)
+numversion = (0, 30, 1)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL'
@@ -81,7 +81,7 @@
 
 install_requires = [
     'logilab-common >= 0.47.0',
-    'logilab-database',
+    'logilab-database >= 1.6.0',
     'yapps == 2.1.1', # XXX to ensure we don't use the broken pypi version
     'constraint', # fallback if the gecode compiled module is missing
     ]
--- a/analyze.py	Wed May 11 09:02:33 2011 +0200
+++ b/analyze.py	Fri Dec 09 12:12:43 2011 +0100
@@ -493,45 +493,33 @@
             if not isinstance(lhs, nodes.VariableRef) or rhs.type is None:
                 return True
             self._extract_constraint(constraints, lhs.name, rhs, rschema.subjects)
-        else:
-            if not isinstance(lhs, nodes.VariableRef):
-                # XXX: check relation is valid
-                return True
+        elif not isinstance(lhs, nodes.VariableRef):
+            # XXX: check relation is valid
+            return True
+        elif isinstance(rhs, nodes.VariableRef):
             lhsvar = lhs.name
-            rhsvars = []
-            samevar = False
-            if not isinstance(rhs, nodes.MathExpression):
-                # rhs type is the result of the math expression, not of
-                # individual variables, so don't add constraints on rhs
-                # variables
-                for v in rhs.iget_nodes(nodes.VariableRef):
-                    if v.name == lhsvar:
-                        samevar = True
-                    else:
-                        rhsvars.append(v.name)
+            rhsvar = rhs.name
             lhsdomain = constraints.domains[lhsvar]
-            if rhsvars:
-                s2 = '=='.join(rhsvars)
-                # filter according to domain necessary for column aliases
-                rhsdomain = constraints.domains[rhsvars[0]]
-                res = []
-                for fromtype, totypes in rschema.associations():
-                    if not fromtype in lhsdomain:
-                        continue
-                    ptypes = [str(t) for t in totypes if t in rhsdomain]
-                    res.append( [ ( [lhsvar], [str(fromtype)]), (rhsvars, ptypes) ] )
-                constraints.or_and( res )
-            else:
-                ptypes = [str(subj) for subj in rschema.subjects()
-                          if subj in lhsdomain]
-                constraints.var_has_types( lhsvar, ptypes )
-            if samevar:
-                res = []
-                for fromtype, totypes in rschema.associations():
-                    if not (fromtype in totypes and fromtype in lhsdomain):
-                        continue
-                    res.append(str(fromtype))
+            # filter according to domain necessary for column aliases
+            rhsdomain = constraints.domains[rhsvar]
+            res = []
+            for fromtype, totypes in rschema.associations():
+                if not fromtype in lhsdomain:
+                    continue
+                ptypes = [str(t) for t in totypes if t in rhsdomain]
+                res.append( [ ([lhsvar], [str(fromtype)]),
+                              ([rhsvar], ptypes) ] )
+            constraints.or_and(res)
+            if rhsvar == lhsvar:
+                res = [str(fromtype) for fromtype, totypes in rschema.associations()
+                       if (fromtype in totypes and fromtype in lhsdomain)]
                 constraints.var_has_types( lhsvar, res )
+        else:
+            # XXX consider rhs.get_type?
+            lhsdomain = constraints.domains[lhs.name]
+            ptypes = [str(subj) for subj in rschema.subjects()
+                      if subj in lhsdomain]
+            constraints.var_has_types( lhs.name, ptypes )
         return True
 
     def visit_type_restriction(self, relation, constraints):
--- a/base.py	Wed May 11 09:02:33 2011 +0200
+++ b/base.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -22,6 +22,7 @@
 
 __docformat__ = "restructuredtext en"
 
+
 class BaseNode(object):
     __slots__ = ('parent',)
 
@@ -196,4 +197,3 @@
         stmt is the root node, which should be use to get new variables.
         """
         return self.__class__(*self.initargs(stmt))
-
--- a/debian/changelog	Wed May 11 09:02:33 2011 +0200
+++ b/debian/changelog	Fri Dec 09 12:12:43 2011 +0100
@@ -1,10 +1,29 @@
-rql (0.28.0-2) UNRELEASED; urgency=low
+rql (0.30.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 07 Sep 2011 18:54:33 +0200
+
+rql (0.30.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 05 Aug 2011 09:30:35 +0200
+
+rql (0.29.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 27 Jul 2011 15:05:34 +0200
+
+rql (0.29.0-1) unstable; urgency=low
 
   * debian/control:
     - remove Ludovic Aubry from Uploaders
   * lintian fixes
+  * new upstream release
 
- -- 
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 09 Jun 2011 16:57:56 +0200
 
 rql (0.28.0-1) unstable; urgency=low
 
--- a/debian/control	Wed May 11 09:02:33 2011 +0200
+++ b/debian/control	Fri Dec 09 12:12:43 2011 +0100
@@ -11,7 +11,7 @@
 Package: python-rql
 Architecture: any
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, ${misc:Depends}, ${shlibs:Depends}, python-logilab-common (>= 0.35.3-1), yapps2-runtime, python-logilab-database
+Depends: ${python:Depends}, ${misc:Depends}, ${shlibs:Depends}, python-logilab-common (>= 0.35.3-1), yapps2-runtime, python-logilab-database (>= 1.6.0)
 Conflicts: cubicweb-common (<= 3.8.3)
 Provides: ${python:Provides}
 Description: relationship query language (RQL) utilities
--- a/makefile	Wed May 11 09:02:33 2011 +0200
+++ b/makefile	Fri Dec 09 12:12:43 2011 +0100
@@ -1,8 +1,6 @@
 YAPPS=yapps
-#python thirdparty/yapps2.py
 
 parser.py: parser.g parser_main.py
 	${YAPPS} parser.g
-	#sed "s/from yappsrt import/from thirdparty.yappsrt import/" parser.py > tmp.py
-	sed -i "s/__main__/old__main__/" parser.py
-	cat parser_main.py >> parser.py
\ No newline at end of file
+	#sed -i "s/__main__/old__main__/" parser.py
+	#cat parser_main.py >> parser.py
\ No newline at end of file
--- a/nodes.py	Wed May 11 09:02:33 2011 +0200
+++ b/nodes.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -28,6 +28,8 @@
 from datetime import datetime, date, time, timedelta
 from time import localtime
 
+from logilab.database import DYNAMIC_RTYPE
+
 from rql import CoercionError
 from rql.base import BaseNode, Node, BinaryNode, LeafNode
 from rql.utils import (function_description, quote, uquote, build_visitor_stub,
@@ -74,6 +76,31 @@
             yield vref
 
 
+class OperatorExpressionMixin(object):
+
+    def initargs(self, stmt):
+        """return list of arguments to give to __init__ to clone this node"""
+        return (self.operator,)
+
+    def is_equivalent(self, other):
+        if not Node.is_equivalent(self, other):
+            return False
+        return self.operator == other.operator
+
+    def get_description(self, mainindex, tr):
+        """if there is a variable in the math expr used as rhs of a relation,
+        return the name of this relation, else return the type of the math
+        expression
+        """
+        try:
+            return tr(self.get_type())
+        except CoercionError:
+            for vref in self.iget_nodes(VariableRef):
+                vtype = vref.get_description(mainindex, tr)
+                if vtype != 'Any':
+                    return tr(vtype)
+
+
 class HSMixin(object):
     """mixin class for classes which may be the lhs or rhs of an expression"""
     __slots__ = ()
@@ -184,14 +211,16 @@
         return self.add_restriction(make_relation(lhsvar, rtype, (rhsvar,),
                                                   VariableRef))
 
-    def add_eid_restriction(self, var, eid):
+    def add_eid_restriction(self, var, eid, c_type='Int'):
         """builds a restriction node to express '<var> eid <eid>'"""
-        return self.add_constant_restriction(var, 'eid', eid, 'Int')
+        assert c_type in ('Int', 'Substitute'), "Error got c_type=%r in eid restriction" % c_type
+        return self.add_constant_restriction(var, 'eid', eid, c_type)
 
     def add_type_restriction(self, var, etype):
         """builds a restriction node to express : variable is etype"""
         return self.add_constant_restriction(var, 'is', etype, 'etype')
 
+
 # base RQL nodes ##############################################################
 
 class SubQuery(BaseNode):
@@ -221,7 +250,7 @@
 
     def as_string(self, encoding=None, kwargs=None):
         return '%s BEING (%s)' % (','.join(v.name for v in self.aliases),
-                                  self.query.as_string())
+                                  self.query.as_string(encoding, kwargs))
     def __repr__(self):
         return '%s BEING (%s)' % (','.join(repr(v) for v in self.aliases),
                                   repr(self.query))
@@ -440,12 +469,7 @@
             return False
         rhs = self.children[1]
         if isinstance(rhs, Comparison):
-            try:
-                rhs = rhs.children[0]
-            except:
-                print 'opppp', rhs
-                print rhs.root
-                raise
+            rhs = rhs.children[0]
         # else: relation used in SET OR DELETE selection
         return ((isinstance(rhs, Constant) and rhs.type == 'etype')
                 or (isinstance(rhs, Function) and rhs.name == 'IN'))
@@ -478,33 +502,42 @@
 
     def change_optional(self, value):
         root = self.root
-        if root.should_register_op and value != self.optional:
+        if root is not None and root.should_register_op and value != self.optional:
             from rql.undo import SetOptionalOperation
             root.undo_manager.add_operation(SetOptionalOperation(self, self.optional))
         self.optional= value
 
 
-OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE'))
+CMP_OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE', 'REGEXP'))
 
 class Comparison(HSMixin, Node):
     """handle comparisons:
 
      <, <=, =, >=, > LIKE and ILIKE operators have a unique children.
     """
-    __slots__ = ('operator',)
+    __slots__ = ('operator', 'optional')
 
-    def __init__(self, operator, value=None):
+    def __init__(self, operator, value=None, optional=None):
         Node.__init__(self)
         if operator == '~=':
             operator = 'ILIKE'
-        assert operator in OPERATORS, operator
+        assert operator in CMP_OPERATORS, operator
         self.operator = operator.encode()
+        self.optional = optional
         if value is not None:
             self.append(value)
 
     def initargs(self, stmt):
         """return list of arguments to give to __init__ to clone this node"""
-        return (self.operator,)
+        return (self.operator, None, self.optional)
+
+    def set_optional(self, left, right):
+        if left and right:
+            self.optional = 'both'
+        elif left:
+            self.optional = 'left'
+        elif right:
+            self.optional = 'right'
 
     def is_equivalent(self, other):
         if not Node.is_equivalent(self, other):
@@ -516,9 +549,14 @@
         if len(self.children) == 0:
             return self.operator
         if len(self.children) == 2:
-            return '%s %s %s' % (self.children[0].as_string(encoding, kwargs),
-                                 self.operator.encode(),
-                                 self.children[1].as_string(encoding, kwargs))
+            lhsopt = rhsopt = ''
+            if self.optional in ('left', 'both'):
+                lhsopt = '?'
+            if self.optional in ('right', 'both'):
+                rhsopt = '?'
+            return '%s%s %s %s%s' % (self.children[0].as_string(encoding, kwargs),
+                                     lhsopt, self.operator.encode(),
+                                     self.children[1].as_string(encoding, kwargs), rhsopt)
         if self.operator == '=':
             return self.children[0].as_string(encoding, kwargs)
         return '%s %s' % (self.operator.encode(),
@@ -528,23 +566,14 @@
         return '%s %s' % (self.operator, ', '.join(repr(c) for c in self.children))
 
 
-class MathExpression(HSMixin, BinaryNode):
-    """Operators plus, minus, multiply, divide."""
+class MathExpression(OperatorExpressionMixin, HSMixin, BinaryNode):
+    """Mathematical Operators"""
     __slots__ = ('operator',)
 
     def __init__(self, operator, lhs=None, rhs=None):
         BinaryNode.__init__(self, lhs, rhs)
         self.operator = operator.encode()
 
-    def initargs(self, stmt):
-        """return list of arguments to give to __init__ to clone this node"""
-        return (self.operator,)
-
-    def is_equivalent(self, other):
-        if not Node.is_equivalent(self, other):
-            return False
-        return self.operator == other.operator
-
     def as_string(self, encoding=None, kwargs=None):
         """return the tree as an encoded rql string"""
         return '(%s %s %s)' % (self.children[0].as_string(encoding, kwargs),
@@ -579,18 +608,31 @@
                 return 'Float'
             raise CoercionError(key)
 
-    def get_description(self, mainindex, tr):
-        """if there is a variable in the math expr used as rhs of a relation,
-        return the name of this relation, else return the type of the math
-        expression
+
+class UnaryExpression(OperatorExpressionMixin, Node):
+    """Unary Operators"""
+    __slots__ = ('operator',)
+
+    def __init__(self, operator, child=None):
+        Node.__init__(self)
+        self.operator = operator.encode()
+        if child is not None:
+            self.append(child)
+
+    def as_string(self, encoding=None, kwargs=None):
+        """return the tree as an encoded rql string"""
+        return '%s%s' % (self.operator.encode(),
+                         self.children[0].as_string(encoding, kwargs))
+
+    def __repr__(self):
+        return '%s%r' % (self.operator, self.children[0])
+
+    def get_type(self, solution=None, kwargs=None):
+        """return the type of object returned by this expression if known
+
+        solution is an optional variable/etype mapping
         """
-        try:
-            return tr(self.get_type())
-        except CoercionError:
-            for vref in self.iget_nodes(VariableRef):
-                vtype = vref.get_description(mainindex, tr)
-                if vtype != 'Any':
-                    return tr(vtype)
+        return self.children[0].get_type(solution, kwargs)
 
 
 class Function(HSMixin, Node):
@@ -625,7 +667,8 @@
 
         solution is an optional variable/etype mapping
         """
-        rtype = self.descr().rtype
+        func_descr = self.descr()
+        rtype = func_descr.rql_return_type(self)
         if rtype is None:
             # XXX support one variable ref child
             try:
@@ -826,7 +869,7 @@
 ###############################################################################
 
 class Referenceable(object):
-    __slots__ = ('name', 'stinfo')
+    __slots__ = ('name', 'stinfo', 'stmt')
 
     def __init__(self, name):
         self.name = name.strip().encode()
@@ -835,6 +878,12 @@
             # link to VariableReference objects in the syntax tree
             'references': set(),
             }
+        # reference to the selection
+        self.stmt = None
+
+    @property
+    def schema(self):
+        return self.stmt.root.schema
 
     def init_copy(self, old):
         # should copy variable's possibletypes on copy
@@ -863,6 +912,7 @@
 
     def prepare_annotation(self):
         self.stinfo.update({
+            'scope': None,
             # relations where this variable is used on the lhs/rhs
             'relations': set(),
             'rhsrelations': set(),
@@ -883,6 +933,18 @@
         for key in ('optrelations', 'blocsimplification', 'ftirels'):
             self.stinfo.pop(key, None)
 
+    def _set_scope(self, key, scopenode):
+        if scopenode is self.stmt or self.stinfo[key] is None:
+            self.stinfo[key] = scopenode
+        elif not (self.stinfo[key] is self.stmt or scopenode is self.stinfo[key]):
+            self.stinfo[key] = common_parent(self.stinfo[key], scopenode).scope
+
+    def set_scope(self, scopenode):
+        self._set_scope('scope', scopenode)
+    def get_scope(self):
+        return self.stinfo['scope']
+    scope = property(get_scope, set_scope)
+
     def add_optional_relation(self, relation):
         try:
             self.stinfo['optrelations'].add(relation)
@@ -986,10 +1048,6 @@
     def __repr__(self):
         return 'alias %s(%#X)' % (self.name, id(self))
 
-    @property
-    def schema(self):
-        return self.query.root.schema
-
     def get_type(self, solution=None, kwargs=None):
         """return entity type of this object, 'Any' if not found"""
         vtype = super(ColumnAlias, self).get_type(solution, kwargs)
@@ -1014,12 +1072,6 @@
                 return ', '.join(sorted(vtype for vtype in vtypes))
         return vtype
 
-    def set_scope(self, scopenode):
-        pass
-    def get_scope(self):
-        return self.query
-    scope = property(get_scope, set_scope)
-
 
 class Variable(Referenceable):
     """
@@ -1028,37 +1080,11 @@
 
     collects information about a variable use in a syntax tree
     """
-    __slots__ = ('stmt',
-                 '_q_invariant', '_q_sql', '_q_sqltable') # XXX ginco specific
-
-    def __init__(self, name):
-        super(Variable, self).__init__(name)
-        # reference to the selection
-        self.stmt = None
+    __slots__ = ('_q_invariant', '_q_sql', '_q_sqltable') # XXX ginco specific
 
     def __repr__(self):
         return '%s(%#X)' % (self.name, id(self))
 
-    @property
-    def schema(self):
-        return self.stmt.root.schema
-
-    def prepare_annotation(self):
-        super(Variable, self).prepare_annotation()
-        self.stinfo['scope'] = None
-
-    def _set_scope(self, key, scopenode):
-        if scopenode is self.stmt or self.stinfo[key] is None:
-            self.stinfo[key] = scopenode
-        elif not (self.stinfo[key] is self.stmt or scopenode is self.stinfo[key]):
-            self.stinfo[key] = common_parent(self.stinfo[key], scopenode).scope
-
-    def set_scope(self, scopenode):
-        self._set_scope('scope', scopenode)
-    def get_scope(self):
-        return self.stinfo['scope']
-    scope = property(get_scope, set_scope)
-
     def valuable_references(self):
         """return the number of "valuable" references :
         references is in selection or in a non type (is) relations
@@ -1068,5 +1094,5 @@
 
 
 build_visitor_stub((SubQuery, And, Or, Not, Exists, Relation,
-                    Comparison, MathExpression, Function, Constant,
-                    VariableRef, SortTerm, ColumnAlias, Variable))
+                    Comparison, MathExpression, UnaryExpression, Function,
+                    Constant, VariableRef, SortTerm, ColumnAlias, Variable))
--- a/parser.g	Wed May 11 09:02:33 2011 +0200
+++ b/parser.g	Fri Dec 09 12:12:43 2011 +0100
@@ -1,7 +1,7 @@
 """yapps input grammar for RQL.
 
 :organization: Logilab
-:copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 
 
@@ -71,7 +71,7 @@
     token DISTINCT:    r'(?i)DISTINCT'
     token WITH:        r'(?i)WITH'
     token WHERE:       r'(?i)WHERE'
-    token BEING:          r'(?i)BEING'
+    token BEING:       r'(?i)BEING'
     token OR:          r'(?i)OR'
     token AND:         r'(?i)AND'
     token NOT:         r'(?i)NOT'
@@ -88,18 +88,20 @@
     token FALSE:       r'(?i)FALSE'
     token NULL:        r'(?i)NULL'
     token EXISTS:      r'(?i)EXISTS'
-    token CMP_OP:      r'(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE'
-    token ADD_OP:      r'\+|-'
-    token MUL_OP:      r'\*|/'
+    token CMP_OP:      r'(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP'
+    token ADD_OP:      r'\+|-|\||#'
+    token MUL_OP:      r'\*|/|%|&'
+    token POW_OP:      r'\^|>>|<<'
+    token UNARY_OP:    r'-|~'
     token FUNCTION:    r'[A-Za-z_]+\s*(?=\()'
     token R_TYPE:      r'[a-z_][a-z0-9_]*'
-    token E_TYPE:      r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*'
+    token E_TYPE:      r'[A-Z][A-Za-z0-9]*[a-z]+[A-Z0-9]*'
     token VARIABLE:    r'[A-Z][A-Z0-9_]*'
     token COLALIAS:    r'[A-Z][A-Z0-9_]*\.\d+'
     token QMARK:       r'\?'
 
     token STRING:      r"'([^\'\\]|\\.)*'|\"([^\\\"\\]|\\.)*\""
-    token FLOAT:       r'\d+\.\d*'
+    token FLOAT:       r'-?\d+\.\d*'
     token INT:         r'-?\d+'
     token SUBSTITUTE:  r'%\([A-Za-z_0-9]+\)s'
 
@@ -155,10 +157,7 @@
                    limit_offset<<S>>
                    where<<S>>
                    having<<S>>
-                   with_<<S>>
-                   dgroupby<<S>>
-                   dorderby<<S>>
-                   dlimit_offset<<S>>    {{ S.set_statement_type(E_TYPE); return S }}
+                   with_<<S>>             {{ S.set_statement_type(E_TYPE); return S }}
 
 rule selection<<S>>: expr_add<<S>>        {{ S.append_selected(expr_add) }}
                      (  ',' expr_add<<S>> {{ S.append_selected(expr_add) }}
@@ -168,12 +167,10 @@
 
 #// other clauses (groupby, orderby, with, having) ##############################
 
-#// to remove in rql 1.0
-rule dorderby<<S>>: orderby<<S>> {{ if orderby: warn('ORDERBY is now before WHERE clause') }}
-rule dgroupby<<S>>: groupby<<S>> {{ if groupby: warn('GROUPBY is now before WHERE clause') }}
-rule dlimit_offset<<S>>: limit_offset<<S>> {{ if limit_offset: warn('LIMIT/OFFSET are now before WHERE clause') }}
-
-rule groupby<<S>>: GROUPBY variables<<S>> {{ S.set_groupby(variables); return True }}
+rule groupby<<S>>: GROUPBY              {{ nodes = [] }}
+                   expr_add<<S>>        {{ nodes.append(expr_add) }}
+                   ( ',' expr_add<<S>>  {{ nodes.append(expr_add) }}
+                   )*                   {{ S.set_groupby(nodes); return True }}
                  |
 
 rule having<<S>>: HAVING logical_expr<<S>> {{ S.set_having([logical_expr]) }}
@@ -230,7 +227,7 @@
                    )*                 {{ return node }}
 
 rule rels_and<<S>>: rels_not<<S>>        {{ node = rels_not }}
-                    (  AND rels_not<<S>> {{ node = And(node, rels_not) }}
+                    ( AND rels_not<<S>>  {{ node = And(node, rels_not) }}
                     )*                   {{ return node }}
 
 rule rels_not<<S>>: NOT rel<<S>> {{ return Not(rel) }}
@@ -262,7 +259,7 @@
                     )*                  {{ return node }}
 
 rule exprs_and<<S>>: exprs_not<<S>>        {{ node = exprs_not }}
-                     (  AND exprs_not<<S>> {{ node = And(node, exprs_not) }}
+                     ( AND exprs_not<<S>>  {{ node = And(node, exprs_not) }}
                      )*                    {{ return node }}
 
 rule exprs_not<<S>>: NOT balanced_expr<<S>> {{ return Not(balanced_expr) }}
@@ -277,11 +274,12 @@
 #//
 #//   Any T2 WHERE T1 relation T2 HAVING (1+2) < COUNT(T1);
 rule balanced_expr<<S>>: r"\(" logical_expr<<S>> r"\)" {{ return logical_expr }}
-                       | expr_add<<S>> expr_op<<S>>    {{ expr_op.insert(0, expr_add); return expr_op }}
+                       | expr_add<<S>> opt_left<<S>>
+                         expr_op<<S>> opt_right<<S>> {{ expr_op.insert(0, expr_add); expr_op.set_optional(opt_left, opt_right); return expr_op }}
 
 # // cant use expr<<S>> without introducing some ambiguities
 rule expr_op<<S>>: CMP_OP expr_add<<S>> {{ return Comparison(CMP_OP.upper(), expr_add) }}
-                 | in_expr<<S>>      {{ return Comparison('=', in_expr) }}
+                 | in_expr<<S>>         {{ return Comparison('=', in_expr) }}
 
 
 #// common statements ###########################################################
@@ -311,10 +309,17 @@
 rule expr_add<<S>>: expr_mul<<S>>          {{ node = expr_mul }}
                     ( ADD_OP expr_mul<<S>> {{ node = MathExpression( ADD_OP, node, expr_mul ) }}
                     )*                     {{ return node }}
+                  | UNARY_OP expr_mul<<S>> {{ node = UnaryExpression( UNARY_OP, expr_mul ) }}
+                    ( ADD_OP expr_mul<<S>> {{ node = MathExpression( ADD_OP, node, expr_mul ) }}
+                    )*                     {{ return node }}
 
 
-rule expr_mul<<S>>: expr_base<<S>>          {{ node = expr_base }}
-                    ( MUL_OP expr_base<<S>> {{ node = MathExpression( MUL_OP, node, expr_base) }}
+rule expr_mul<<S>>: expr_pow<<S>>          {{ node = expr_pow }}
+                    ( MUL_OP expr_pow<<S>> {{ node = MathExpression( MUL_OP, node, expr_pow) }}
+                    )*                     {{ return node }}
+
+rule expr_pow<<S>>: expr_base<<S>>          {{ node = expr_base }}
+                    ( POW_OP expr_base<<S>> {{ node = MathExpression( MUL_OP, node, expr_base) }}
                     )*                      {{ return node }}
 
 
--- a/parser.py	Wed May 11 09:02:33 2011 +0200
+++ b/parser.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,22 +1,8 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of rql.
-#
-# rql is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# rql is distributed in the hope that it will be useful, but WITHOUT ANY
-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
-# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with rql. If not, see <http://www.gnu.org/licenses/>.
 """yapps input grammar for RQL.
 
+:organization: Logilab
+:copyright: 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 
 
 Select statement grammar
@@ -109,17 +95,19 @@
         ('FALSE', re.compile('(?i)FALSE')),
         ('NULL', re.compile('(?i)NULL')),
         ('EXISTS', re.compile('(?i)EXISTS')),
-        ('CMP_OP', re.compile('(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE')),
-        ('ADD_OP', re.compile('\\+|-')),
-        ('MUL_OP', re.compile('\\*|/')),
+        ('CMP_OP', re.compile('(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP')),
+        ('ADD_OP', re.compile('\\+|-|\\||#')),
+        ('MUL_OP', re.compile('\\*|/|%|&')),
+        ('POW_OP', re.compile('\\^|>>|<<')),
+        ('UNARY_OP', re.compile('-|~')),
         ('FUNCTION', re.compile('[A-Za-z_]+\\s*(?=\\()')),
         ('R_TYPE', re.compile('[a-z_][a-z0-9_]*')),
-        ('E_TYPE', re.compile('[A-Z][A-Za-z0-9]*[a-z]+[0-9]*')),
+        ('E_TYPE', re.compile('[A-Z][A-Za-z0-9]*[a-z]+[A-Z0-9]*')),
         ('VARIABLE', re.compile('[A-Z][A-Z0-9_]*')),
         ('COLALIAS', re.compile('[A-Z][A-Z0-9_]*\\.\\d+')),
         ('QMARK', re.compile('\\?')),
         ('STRING', re.compile('\'([^\\\'\\\\]|\\\\.)*\'|\\"([^\\\\\\"\\\\]|\\\\.)*\\"')),
-        ('FLOAT', re.compile('\\d+\\.\\d*')),
+        ('FLOAT', re.compile('-?\\d+\\.\\d*')),
         ('INT', re.compile('-?\\d+')),
         ('SUBSTITUTE', re.compile('%\\([A-Za-z_0-9]+\\)s')),
     ]
@@ -226,9 +214,6 @@
         where = self.where(S, _context)
         having = self.having(S, _context)
         with_ = self.with_(S, _context)
-        dgroupby = self.dgroupby(S, _context)
-        dorderby = self.dorderby(S, _context)
-        dlimit_offset = self.dlimit_offset(S, _context)
         S.set_statement_type(E_TYPE); return S
 
     def selection(self, S, _parent=None):
@@ -240,80 +225,63 @@
             expr_add = self.expr_add(S, _context)
             S.append_selected(expr_add)
 
-    def dorderby(self, S, _parent=None):
-        _context = self.Context(_parent, self._scanner, 'dorderby', [S])
-        orderby = self.orderby(S, _context)
-        if orderby: warn('ORDERBY is now before WHERE clause')
-
-    def dgroupby(self, S, _parent=None):
-        _context = self.Context(_parent, self._scanner, 'dgroupby', [S])
-        groupby = self.groupby(S, _context)
-        if groupby: warn('GROUPBY is now before WHERE clause')
-
-    def dlimit_offset(self, S, _parent=None):
-        _context = self.Context(_parent, self._scanner, 'dlimit_offset', [S])
-        limit_offset = self.limit_offset(S, _context)
-        if limit_offset: warn('LIMIT/OFFSET are now before WHERE clause')
-
     def groupby(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'groupby', [S])
         _token = self._peek('GROUPBY', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
         if _token == 'GROUPBY':
             GROUPBY = self._scan('GROUPBY', context=_context)
-            variables = self.variables(S, _context)
-            S.set_groupby(variables); return True
-        elif 1:
+            nodes = []
+            expr_add = self.expr_add(S, _context)
+            nodes.append(expr_add)
+            while self._peek("','", 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
+                self._scan("','", context=_context)
+                expr_add = self.expr_add(S, _context)
+                nodes.append(expr_add)
+            S.set_groupby(nodes); return True
+        else:
             pass
-        else:
-            raise runtime.SyntaxError(_token[0], 'Could not match groupby')
 
     def having(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'having', [S])
-        _token = self._peek('HAVING', 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', "';'", 'r"\\)"', context=_context)
+        _token = self._peek('HAVING', 'WITH', 'r"\\)"', "';'", context=_context)
         if _token == 'HAVING':
             HAVING = self._scan('HAVING', context=_context)
             logical_expr = self.logical_expr(S, _context)
             S.set_having([logical_expr])
-        elif 1:
+        else: # in ['WITH', 'r"\\)"', "';'"]
             pass
-        else:
-            raise runtime.SyntaxError(_token[0], 'Could not match having')
 
     def orderby(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'orderby', [S])
-        _token = self._peek('ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'GROUPBY', 'r"\\)"', context=_context)
+        _token = self._peek('ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
         if _token == 'ORDERBY':
             ORDERBY = self._scan('ORDERBY', context=_context)
             nodes = []
             sort_term = self.sort_term(S, _context)
             nodes.append(sort_term)
-            while self._peek("','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'GROUPBY', 'ORDERBY', 'r"\\)"', context=_context) == "','":
+            while self._peek("','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
                 self._scan("','", context=_context)
                 sort_term = self.sort_term(S, _context)
                 nodes.append(sort_term)
             S.set_orderby(nodes); return True
-        elif 1:
+        else:
             pass
-        else:
-            raise runtime.SyntaxError(_token[0], 'Could not match orderby')
 
     def with_(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'with_', [S])
-        _token = self._peek('WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', "';'", 'r"\\)"', context=_context)
+        _token = self._peek('WITH', 'r"\\)"', "';'", context=_context)
         if _token == 'WITH':
             WITH = self._scan('WITH', context=_context)
             nodes = []
             subquery = self.subquery(S, _context)
             nodes.append(subquery)
-            while self._peek("','", 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context) == "','":
+            while self._peek("','", 'r"\\)"', "';'", context=_context) == "','":
                 self._scan("','", context=_context)
                 subquery = self.subquery(S, _context)
                 nodes.append(subquery)
             S.set_with(nodes)
-        elif 1:
+        else: # in ['r"\\)"', "';'"]
             pass
-        else:
-            raise runtime.SyntaxError(_token[0], 'Could not match with_')
 
     def subquery(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'subquery', [S])
@@ -333,7 +301,7 @@
 
     def sort_meth(self, _parent=None):
         _context = self.Context(_parent, self._scanner, 'sort_meth', [])
-        _token = self._peek('SORT_DESC', 'SORT_ASC', "','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'GROUPBY', 'ORDERBY', 'r"\\)"', context=_context)
+        _token = self._peek('SORT_DESC', 'SORT_ASC', "','", 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
         if _token == 'SORT_DESC':
             SORT_DESC = self._scan('SORT_DESC', context=_context)
             return 0
@@ -351,41 +319,39 @@
 
     def limit(self, R, _parent=None):
         _context = self.Context(_parent, self._scanner, 'limit', [R])
-        _token = self._peek('LIMIT', 'OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'GROUPBY', 'ORDERBY', 'r"\\)"', context=_context)
+        _token = self._peek('LIMIT', 'OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
         if _token == 'LIMIT':
             LIMIT = self._scan('LIMIT', context=_context)
             INT = self._scan('INT', context=_context)
             R.set_limit(int(INT)); return True
-        else:
+        else: # in ['OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"']
             pass
 
     def offset(self, R, _parent=None):
         _context = self.Context(_parent, self._scanner, 'offset', [R])
-        _token = self._peek('OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'GROUPBY', 'ORDERBY', 'r"\\)"', 'LIMIT', context=_context)
+        _token = self._peek('OFFSET', 'WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"', context=_context)
         if _token == 'OFFSET':
             OFFSET = self._scan('OFFSET', context=_context)
             INT = self._scan('INT', context=_context)
             R.set_offset(int(INT)); return True
-        else:
+        else: # in ['WHERE', 'HAVING', 'WITH', "';'", 'r"\\)"']
             pass
 
     def where(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'where', [S])
-        _token = self._peek('WHERE', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'r"\\)"', context=_context)
+        _token = self._peek('WHERE', 'HAVING', "';'", 'WITH', 'r"\\)"', context=_context)
         if _token == 'WHERE':
             WHERE = self._scan('WHERE', context=_context)
             restriction = self.restriction(S, _context)
             S.set_where(restriction)
-        elif 1:
+        else: # in ['HAVING', "';'", 'WITH', 'r"\\)"']
             pass
-        else:
-            raise runtime.SyntaxError(_token[0], 'Could not match where')
 
     def restriction(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'restriction', [S])
         rels_or = self.rels_or(S, _context)
         node = rels_or
-        while self._peek("','", 'r"\\)"', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', context=_context) == "','":
+        while self._peek("','", 'r"\\)"', 'HAVING', "';'", 'WITH', context=_context) == "','":
             self._scan("','", context=_context)
             rels_or = self.rels_or(S, _context)
             node = And(node, rels_or)
@@ -395,7 +361,7 @@
         _context = self.Context(_parent, self._scanner, 'rels_or', [S])
         rels_and = self.rels_and(S, _context)
         node = rels_and
-        while self._peek('OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', context=_context) == 'OR':
+        while self._peek('OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', context=_context) == 'OR':
             OR = self._scan('OR', context=_context)
             rels_and = self.rels_and(S, _context)
             node = Or(node, rels_and)
@@ -405,7 +371,7 @@
         _context = self.Context(_parent, self._scanner, 'rels_and', [S])
         rels_not = self.rels_not(S, _context)
         node = rels_not
-        while self._peek('AND', 'OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', context=_context) == 'AND':
+        while self._peek('AND', 'OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', context=_context) == 'AND':
             AND = self._scan('AND', context=_context)
             rels_not = self.rels_not(S, _context)
             node = And(node, rels_not)
@@ -459,27 +425,27 @@
 
     def opt_left(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'opt_left', [S])
-        _token = self._peek('QMARK', 'R_TYPE', context=_context)
+        _token = self._peek('QMARK', 'R_TYPE', 'CMP_OP', "'IN'", context=_context)
         if _token == 'QMARK':
             QMARK = self._scan('QMARK', context=_context)
             return 'left'
-        else: # == 'R_TYPE'
+        else: # in ['R_TYPE', 'CMP_OP', "'IN'"]
             pass
 
     def opt_right(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'opt_right', [S])
-        _token = self._peek('QMARK', 'AND', 'OR', "','", 'r"\\)"', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', context=_context)
+        _token = self._peek('QMARK', 'AND', 'OR', "','", 'r"\\)"', 'WITH', 'HAVING', "';'", context=_context)
         if _token == 'QMARK':
             QMARK = self._scan('QMARK', context=_context)
             return 'right'
-        else:
+        else: # in ['AND', 'OR', "','", 'r"\\)"', 'WITH', 'HAVING', "';'"]
             pass
 
     def logical_expr(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'logical_expr', [S])
         exprs_or = self.exprs_or(S, _context)
         node = exprs_or
-        while self._peek("','", 'r"\\)"', 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', "';'", context=_context) == "','":
+        while self._peek("','", 'r"\\)"', 'WITH', "';'", context=_context) == "','":
             self._scan("','", context=_context)
             exprs_or = self.exprs_or(S, _context)
             node = And(node, exprs_or)
@@ -489,7 +455,7 @@
         _context = self.Context(_parent, self._scanner, 'exprs_or', [S])
         exprs_and = self.exprs_and(S, _context)
         node = exprs_and
-        while self._peek('OR', "','", 'r"\\)"', 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', "';'", context=_context) == 'OR':
+        while self._peek('OR', "','", 'r"\\)"', 'WITH', "';'", context=_context) == 'OR':
             OR = self._scan('OR', context=_context)
             exprs_and = self.exprs_and(S, _context)
             node = Or(node, exprs_and)
@@ -499,7 +465,7 @@
         _context = self.Context(_parent, self._scanner, 'exprs_and', [S])
         exprs_not = self.exprs_not(S, _context)
         node = exprs_not
-        while self._peek('AND', 'OR', "','", 'r"\\)"', 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', "';'", context=_context) == 'AND':
+        while self._peek('AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", context=_context) == 'AND':
             AND = self._scan('AND', context=_context)
             exprs_not = self.exprs_not(S, _context)
             node = And(node, exprs_not)
@@ -507,7 +473,7 @@
 
     def exprs_not(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'exprs_not', [S])
-        _token = self._peek('NOT', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+        _token = self._peek('NOT', 'r"\\("', 'UNARY_OP', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
         if _token == 'NOT':
             NOT = self._scan('NOT', context=_context)
             balanced_expr = self.balanced_expr(S, _context)
@@ -518,7 +484,7 @@
 
     def balanced_expr(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'balanced_expr', [S])
-        _token = self._peek('r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+        _token = self._peek('r"\\("', 'UNARY_OP', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
         if _token == 'r"\\("':
             self._scan('r"\\("', context=_context)
             logical_expr = self.logical_expr(S, _context)
@@ -526,8 +492,10 @@
             return logical_expr
         elif 1:
             expr_add = self.expr_add(S, _context)
+            opt_left = self.opt_left(S, _context)
             expr_op = self.expr_op(S, _context)
-            expr_op.insert(0, expr_add); return expr_op
+            opt_right = self.opt_right(S, _context)
+            expr_op.insert(0, expr_add); expr_op.set_optional(opt_left, opt_right); return expr_op
         else:
             raise runtime.SyntaxError(_token[0], 'Could not match balanced_expr')
 
@@ -547,7 +515,7 @@
         vars = []
         var = self.var(S, _context)
         vars.append(var)
-        while self._peek("','", 'BEING', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'GROUPBY', 'r"\\)"', context=_context) == "','":
+        while self._peek("','", 'BEING', context=_context) == "','":
             self._scan("','", context=_context)
             var = self.var(S, _context)
             vars.append(var)
@@ -557,7 +525,7 @@
         _context = self.Context(_parent, self._scanner, 'decl_vars', [R])
         E_TYPE = self._scan('E_TYPE', context=_context)
         var = self.var(R, _context)
-        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) == "','":
+        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) == "','":
             R.add_main_variable(E_TYPE, var)
             self._scan("','", context=_context)
             E_TYPE = self._scan('E_TYPE', context=_context)
@@ -567,7 +535,7 @@
     def decl_rels(self, R, _parent=None):
         _context = self.Context(_parent, self._scanner, 'decl_rels', [R])
         simple_rel = self.simple_rel(R, _context)
-        while self._peek("','", 'WHERE', 'HAVING', "';'", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'r"\\)"', context=_context) == "','":
+        while self._peek("','", 'WHERE', 'HAVING', "';'", 'WITH', 'r"\\)"', context=_context) == "','":
             R.add_main_relation(simple_rel)
             self._scan("','", context=_context)
             simple_rel = self.simple_rel(R, _context)
@@ -583,7 +551,7 @@
 
     def expr(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'expr', [S])
-        _token = self._peek('CMP_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+        _token = self._peek('CMP_OP', 'UNARY_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
         if _token == 'CMP_OP':
             CMP_OP = self._scan('CMP_OP', context=_context)
             expr_add = self.expr_add(S, _context)
@@ -594,20 +562,41 @@
 
     def expr_add(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'expr_add', [S])
-        expr_mul = self.expr_mul(S, _context)
-        node = expr_mul
-        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':
-            ADD_OP = self._scan('ADD_OP', context=_context)
+        _token = self._peek('UNARY_OP', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context)
+        if _token != 'UNARY_OP':
             expr_mul = self.expr_mul(S, _context)
-            node = MathExpression( ADD_OP, node, expr_mul )
-        return node
+            node = expr_mul
+            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':
+                ADD_OP = self._scan('ADD_OP', context=_context)
+                expr_mul = self.expr_mul(S, _context)
+                node = MathExpression( ADD_OP, node, expr_mul )
+            return node
+        else: # == 'UNARY_OP'
+            UNARY_OP = self._scan('UNARY_OP', context=_context)
+            expr_mul = self.expr_mul(S, _context)
+            node = UnaryExpression( UNARY_OP, expr_mul )
+            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':
+                ADD_OP = self._scan('ADD_OP', context=_context)
+                expr_mul = self.expr_mul(S, _context)
+                node = MathExpression( ADD_OP, node, expr_mul )
+            return node
 
     def expr_mul(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'expr_mul', [S])
+        expr_pow = self.expr_pow(S, _context)
+        node = expr_pow
+        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':
+            MUL_OP = self._scan('MUL_OP', context=_context)
+            expr_pow = self.expr_pow(S, _context)
+            node = MathExpression( MUL_OP, node, expr_pow)
+        return node
+
+    def expr_pow(self, S, _parent=None):
+        _context = self.Context(_parent, self._scanner, 'expr_pow', [S])
         expr_base = self.expr_base(S, _context)
         node = expr_base
-        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':
-            MUL_OP = self._scan('MUL_OP', context=_context)
+        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':
+            POW_OP = self._scan('POW_OP', context=_context)
             expr_base = self.expr_base(S, _context)
             node = MathExpression( MUL_OP, node, expr_base)
         return node
@@ -638,9 +627,9 @@
         FUNCTION = self._scan('FUNCTION', context=_context)
         self._scan('r"\\("', context=_context)
         F = Function(FUNCTION)
-        if self._peek('r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
+        if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
             expr_add = self.expr_add(S, _context)
-            while self._peek("','", 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', "'IN'", 'GROUPBY', 'QMARK', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == "','":
+            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) == "','":
                 F.append(expr_add)
                 self._scan("','", context=_context)
                 expr_add = self.expr_add(S, _context)
@@ -653,9 +642,9 @@
         self._scan("'IN'", context=_context)
         self._scan('r"\\("', context=_context)
         F = Function('IN')
-        if self._peek('r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
+        if self._peek('UNARY_OP', 'r"\\)"', 'r"\\("', 'NULL', 'DATE', 'DATETIME', 'TRUE', 'FALSE', 'FLOAT', 'INT', 'STRING', 'SUBSTITUTE', 'VARIABLE', 'E_TYPE', 'FUNCTION', context=_context) != 'r"\\)"':
             expr_add = self.expr_add(S, _context)
-            while self._peek("','", 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', "'IN'", 'GROUPBY', 'QMARK', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', "';'", 'WITH', 'AND', 'OR', context=_context) == "','":
+            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) == "','":
                 F.append(expr_add)
                 self._scan("','", context=_context)
                 expr_add = self.expr_add(S, _context)
@@ -709,7 +698,7 @@
     P = Hercule(HerculeScanner(text))
     return runtime.wrap_error_reporter(P, rule)
 
-if __name__ == 'old__main__':
+if __name__ == '__main__':
     from sys import argv, stdin
     if len(argv) >= 2:
         if len(argv) >= 3:
@@ -719,25 +708,3 @@
         print parse(argv[1], f.read())
     else: print >>sys.stderr, 'Args:  <rule> [<filename>]'
 # End -- grammar generated by Yapps
-"""Main parser command.
-
-"""
-__docformat__ = "restructuredtext en"
-
-if __name__ == '__main__':
-    from sys import argv
-
-    parser = Hercule(HerculeScanner(argv[1]))
-    e_types = {}
-    # parse the RQL string
-    try:
-        tree = parser.goal(e_types)
-        print '-'*80
-        print tree
-        print '-'*80
-        print repr(tree)
-        print e_types
-    except SyntaxError, ex:
-        # try to get error message from yapps
-        from yapps.runtime import print_error
-        print_error(ex, parser._scanner)
--- a/stcheck.py	Wed May 11 09:02:33 2011 +0200
+++ b/stcheck.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -27,7 +27,7 @@
 from rql._exceptions import BadRQLQuery
 from rql.utils import function_description
 from rql.nodes import (Relation, VariableRef, Constant, Not, Exists, Function,
-                       And, Variable, variable_refs, make_relation)
+                       And, Variable, Comparison, variable_refs, make_relation)
 from rql.stmts import Union
 
 
@@ -372,9 +372,9 @@
             except KeyError:
                 state.error('unknown relation `%s`' % rtype)
             else:
-                if relation.optional and rschema.final:
-                    state.error("shouldn't use optional on final relation `%s`"
-                                % relation.r_type)
+                if rschema.final and relation.optional not in (None, 'right'):
+                     state.error("optional may only be set on the rhs on final relation `%s`"
+                                 % relation.r_type)
                 if self.special_relations.get(rtype) == 'uid':
                     if state.var_info.get(lhsvar, 0) & VAR_HAS_UID_REL:
                         state.error('can only one uid restriction per variable '
@@ -408,6 +408,10 @@
         pass #assert len(mathexpr.children) == 2, len(mathexpr.children)
     def leave_mathexpression(self, node, state):
         pass
+    def visit_unaryexpression(self, unaryexpr, state):
+        pass #assert len(unaryexpr.children) == 2, len(unaryexpr.children)
+    def leave_unaryexpression(self, node, state):
+        pass
 
     def visit_function(self, function, state):
         try:
@@ -455,11 +459,16 @@
     def visit_constant(self, constant, state):
         #assert len(constant.children)==0
         if constant.type == 'etype':
-            if constant.relation().r_type not in ('is', 'is_instance_of'):
-                msg ='using an entity type in only allowed with "is" relation'
-                state.error(msg)
-            if not constant.value in self.schema:
+            if constant.value not in self.schema:
                 state.error('unknown entity type %s' % constant.value)
+            rel = constant.relation()
+            if rel is not None:
+                if rel.r_type not in ('is', 'is_instance_of'):
+                    msg ='using an entity type in only allowed with "is" relation'
+                    state.error(msg)
+            elif not (isinstance(constant.parent, Function) and
+                      constant.parent.name == 'CAST'):
+                state.error('Entity types can only be used inside a CAST()')
 
     def leave_constant(self, node, state):
         pass
@@ -516,6 +525,35 @@
             for term in node.groupby:
                 for vref in term.get_nodes(VariableRef):
                     bloc_simplification(vref.variable, term)
+            try:
+                vargraph = node.vargraph
+            except AttributeError:
+                vargraph = None
+            # XXX node.having is a list of size 1
+            assert len(node.having) == 1
+            for term in node.having[0].get_nodes(Comparison):
+                lhsvariables = set(vref.variable for vref in term.children[0].get_nodes(VariableRef))
+                rhsvariables = set(vref.variable for vref in term.children[1].get_nodes(VariableRef))
+                for var in lhsvariables | rhsvariables:
+                    var.stinfo.setdefault('having', []).append(term)
+                if vargraph is not None:
+                    for v1 in lhsvariables:
+                        v1 = v1.name
+                        for v2 in rhsvariables:
+                            v2 = v2.name
+                            if v1 != v2:
+                                vargraph.setdefault(v1, []).append(v2)
+                                vargraph.setdefault(v2, []).append(v1)
+                if term.optional in ('left', 'both'):
+                    for var in lhsvariables:
+                        if var.stinfo['attrvar'] is not None:
+                            optcomps = var.stinfo['attrvar'].stinfo.setdefault('optcomparisons', set())
+                            optcomps.add(term)
+                if term.optional in ('right', 'both'):
+                    for var in rhsvariables:
+                        if var.stinfo['attrvar'] is not None:
+                            optcomps = var.stinfo['attrvar'].stinfo.setdefault('optcomparisons', set())
+                            optcomps.add(term)
 
     def rewrite_shared_optional(self, exists, var, identity_rel_scope=None):
         """if variable is shared across multiple scopes, need some tree
@@ -660,6 +698,9 @@
                 update_attrvars(var, relation, lhs)
 
 def update_attrvars(var, relation, lhs):
+    if var.stinfo['relations'] - var.stinfo['rhsrelations']:
+        raise BadRQLQuery('variable %s should not be used as rhs of attribute relation %s'
+                          % (var.name, relation))
     # stinfo['attrvars'] is set of couple (lhs variable name, relation name)
     # where the `var` attribute variable is used
     lhsvar = getattr(lhs, 'variable', None)
--- a/stmts.py	Wed May 11 09:02:33 2011 +0200
+++ b/stmts.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -27,6 +27,7 @@
 from warnings import warn
 
 from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
 
 from rql import BadRQLQuery, CoercionError, nodes
 from rql.base import BaseNode, Node
@@ -47,6 +48,13 @@
             raise AssertionError('vref %r is not referenced (%r)' % (vref, vref.stmt))
     return True
 
+class undo_modification(object):
+    def __init__(self, select):
+        self.select = select
+    def __enter__(self):
+        self.select.save_state()
+    def __exit__(self):
+        self.select.recover()
 
 class ScopeNode(BaseNode):
     solutions = ()   # list of possibles solutions for used variables
@@ -354,6 +362,9 @@
     def should_register_op(self):
         return self.memorizing and not self.undoing
 
+    def undo_modification(self):
+        return undo_modification(self)
+
     def save_state(self):
         """save the current tree"""
         self.undo_manager.push_state()
@@ -473,7 +484,7 @@
             s.append('WHERE ' + as_string(self.where))
         if self.having:
             s.append('HAVING ' + ','.join(as_string(term)
-                                           for term in self.having))
+                                          for term in self.having))
         if self.with_:
             s.append('WITH ' + ','.join(as_string(term)
                                         for term in self.with_))
@@ -607,6 +618,7 @@
             return self.aliases[name]
         if colnum is not None: # take care, may be 0
             self.aliases[name] = calias = nodes.ColumnAlias(name, colnum)
+            calias.stmt = self
             # alias may already have been used as a regular variable, replace it
             if name in self.defined_vars:
                 var = self.defined_vars.pop(name)
@@ -697,8 +709,7 @@
             raise Exception('duh XXX %s' % oldnode)
         # XXX no undo/reference support 'by design' (eg breaks things if you add
         # it...)
-        # XXX resetting oldnode parent cause pb with cw.test_views (w/ facets)
-        #oldnode.parent = None
+        oldnode.parent = None
         newnode.parent = self
         return oldnode, self, None
 
@@ -708,7 +719,7 @@
         elif node in self.orderby:
             self.remove_sort_term(node)
         elif node in self.groupby:
-            self.remove_group_var(node)
+            self.remove_group_term(node)
         elif node in self.having:
             self.having.remove(node)
         # XXX selection
@@ -730,7 +741,7 @@
             elif isinstance(vref.parent, nodes.SortTerm):
                 self.remove_sort_term(vref.parent)
             elif vref in self.groupby:
-                self.remove_group_var(vref)
+                self.remove_group_term(vref)
             else: # selected variable
                 self.remove_selected(vref)
         # effective undefine operation
@@ -796,17 +807,19 @@
             from rql.undo import AddGroupOperation
             self.undo_manager.add_operation(AddGroupOperation(vref))
 
-    def remove_group_var(self, vref):
+    def remove_group_term(self, term):
         """remove the group variable and the group node if necessary"""
         if self.should_register_op:
             from rql.undo import RemoveGroupOperation
-            self.undo_manager.add_operation(RemoveGroupOperation(vref))
-        vref.unregister_reference()
-        self.groupby.remove(vref)
+            self.undo_manager.add_operation(RemoveGroupOperation(term))
+        for vref in term.iget_nodes(nodes.VariableRef):
+            vref.unregister_reference()
+        self.groupby.remove(term)
+    remove_group_var = deprecated('[rql 0.29] use remove_group_term instead')(remove_group_term)
 
     def remove_groups(self):
         for vref in self.groupby[:]:
-            self.remove_group_var(vref)
+            self.remove_group_term(vref)
 
     def add_sort_var(self, var, asc=True):
         """add var in 'orderby' constraints
--- a/test/unittest_analyze.py	Wed May 11 09:02:33 2011 +0200
+++ b/test/unittest_analyze.py	Fri Dec 09 12:12:43 2011 +0100
@@ -531,7 +531,7 @@
                                 {'P': 'Student', 'S': 'Company', 'N': 'Int'}])
 
 
-    def test_nongrer_not_u_ownedby_u(self):
+    def test_nonregr_not_u_ownedby_u(self):
         node = self.helper.parse('Any U WHERE NOT U owned_by U')
         self.helper.compute_solutions(node, debug=DEBUG)
         sols = sorted(node.children[0].solutions)
--- a/test/unittest_editextensions.py	Wed May 11 09:02:33 2011 +0200
+++ b/test/unittest_editextensions.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -19,10 +19,11 @@
 from logilab.common.testlib import TestCase, unittest_main
 
 from rql import parse
+from rql.nodes import Exists
 from rql.editextensions import *
 
 class RQLUndoTestCase(TestCase):
-    
+
     def test_selected(self):
         rqlst = parse('Person X')
         orig = rqlst.as_string()
@@ -40,7 +41,7 @@
         self.assertEqual(rqlst.as_string(), orig)
         # check references after recovering
         rqlst.check_references()
-    
+
     def test_selected3(self):
         rqlst = parse('Any lower(N) WHERE X is Person, X name N')
         orig = rqlst.as_string()
@@ -58,7 +59,7 @@
         self.assertEqual(rqlst.as_string(), orig)
         # check references after recovering
         rqlst.check_references()
-        
+
     def test_undefine_1(self):
         rqlst = parse('Person X, Y WHERE X travaille_pour Y')
         orig = rqlst.as_string()
@@ -73,7 +74,7 @@
         self.assertEqual(rqlst.as_string(), orig)
         # check references after recovering
         rqlst.check_references()
-        
+
     def test_undefine_2(self):
         rqlst = parse('Person X')
         orig = rqlst.as_string()
@@ -90,7 +91,24 @@
         self.assertEqual(rqlst.as_string(), orig)
         # check references after recovering
         rqlst.check_references()
-        
-        
+
+
+    def test_remove_exists(self):
+        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]
+        orig = rqlst.as_string()
+        rqlst.save_state()
+        n = [r for r in rqlst.get_nodes(Exists)][0].query
+        rqlst.remove_node(n)
+        # check operations
+        self.assertEqual(rqlst.as_string(), 'Any U,COUNT(P) GROUPBY U WHERE U is CWUser, P? patch_reviewer U')
+        # check references before recovering
+        rqlst.check_references()
+        rqlst.recover()
+        # check equivalence
+        self.assertEqual(rqlst.as_string(), orig)
+        # check references after recovering
+        rqlst.check_references()
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_nodes.py	Wed May 11 09:02:33 2011 +0200
+++ b/test/unittest_nodes.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,5 +1,5 @@
 # -*- coding: iso-8859-1 -*-
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -232,11 +232,11 @@
         tree.check_references()
         self.assertEqual(tree.as_string(), 'Any X')
 
-    def test_select_remove_group_var(self):
+    def test_select_remove_group_term(self):
         tree = self._parse('Any X GROUPBY X')
         tree.save_state()
         select = tree.children[0]
-        select.remove_group_var(select.groupby[0])
+        select.remove_group_term(select.groupby[0])
         tree.check_references()
         self.assertEqual(tree.as_string(), 'Any X')
         tree.recover()
@@ -564,6 +564,12 @@
         tree = sparse('Any X,R,D,Y WHERE X work_for R, R creation_date D, Y connait X')
         self.assertEqual(tree.get_description(0), [['Person, Student', 'work_for', 'creation_date', 'connait']])
 
+    def test_get_description_cast(self):
+        tree = sparse('Any CAST(String, Y) WHERE X creation_date Y')
+        select = tree.children[0]
+        self.assertEqual(select.selection[0].get_type(), 'String')
+        self.assertEqual(tree.get_description(0), [['String']])
+
 
 class GetNodesFunctionTest(TestCase):
     def test_known_values_1(self):
--- a/test/unittest_parser.py	Wed May 11 09:02:33 2011 +0200
+++ b/test/unittest_parser.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,5 +1,5 @@
 # -*- coding: iso-8859-1 -*-
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -82,6 +82,7 @@
     "INSERT Person X: X nom 'bidule', X ami Y WHERE Y nom 'chouette';",
     "SET X nom 'toto', X prenom 'original' WHERE X is Person, X nom 'bidule';",
     "SET X know Y WHERE X ami Y;",
+    "SET X value -Y WHERE X value Y;",
     "DELETE Person X WHERE X nom 'toto';",
     "DELETE X ami Y WHERE X is Person, X nom 'toto';",
 
@@ -153,6 +154,15 @@
     'Any X,Y,A ORDERBY Y '
     'WHERE A done_for Y, X split_into Y, A diem D '
     'HAVING MIN(D) < "2010-07-01", MAX(D) >= "2010-07-01";',
+
+    'Any YEAR(XD),COUNT(X) GROUPBY YEAR(XD) ORDERBY YEAR(XD) WHERE X date XD;',
+    'Any YEAR(XD),COUNT(X) GROUPBY 1 ORDERBY 1 WHERE X date XD;',
+
+    'Any -1.0;',
+
+    'Any U,G WHERE U login UL, G name GL, G is CWGroup HAVING UPPER(UL)=UPPER(GL)?;',
+
+    'Any U,G WHERE U login UL, G name GL, G is CWGroup HAVING UPPER(UL)?=UPPER(GL);',
     )
 
 class ParserHercule(TestCase):
@@ -310,7 +320,7 @@
         for rql in SPEC_QUERIES:
 #            print "Orig:", rql
 #            print "Resu:", rqltree
-            yield self.assert_, self.parse(rql, True)
+            yield self.parse, rql, True
 
     def test_raise_badsyntax_error(self):
         for rql in BAD_SYNTAX_QUERIES:
--- a/test/unittest_stcheck.py	Wed May 11 09:02:33 2011 +0200
+++ b/test/unittest_stcheck.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -15,6 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with rql. If not, see <http://www.gnu.org/licenses/>.
+from __future__ import with_statement
+
 from logilab.common.testlib import TestCase, unittest_main
 
 from rql import RQLHelper, BadRQLQuery, stmts, nodes
@@ -312,6 +314,13 @@
         C = rqlst.with_[0].query.children[0].defined_vars['C']
         self.failUnless(C.scope is rqlst.with_[0].query.children[0], C.scope)
         self.assertEqual(len(C.stinfo['relations']), 2)
+        X = rqlst.get_variable('X')
+        self.failUnless(X.scope is rqlst, X.scope)
+
+    def test_no_attr_var_if_uid_rel(self):
+        with self.assertRaises(BadRQLQuery) as cm:
+            self.parse('Any X, Y WHERE X work_for Z, Y work_for Z, X eid > Y')
+        self.assertEqual(str(cm.exception), 'variable Y should not be used as rhs of attribute relation X eid > Y')
 
 if __name__ == '__main__':
     unittest_main()
--- a/undo.py	Wed May 11 09:02:33 2011 +0200
+++ b/undo.py	Fri Dec 09 12:12:43 2011 +0100
@@ -1,4 +1,4 @@
-# copyright 2004-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -19,7 +19,7 @@
 
 __docformat__ = "restructuredtext en"
 
-from rql.nodes import VariableRef, Variable, BinaryNode
+from rql.nodes import Exists, VariableRef, Variable, BinaryNode
 from rql.stmts import Select
 
 class SelectionManager(object):
@@ -142,8 +142,8 @@
     def __init__(self, node, parent, stmt, index):
         NodeOperation.__init__(self, node, stmt)
         self.node_parent = parent
-        #if isinstance(parent, Select):
-        #    assert self.node is parent.where
+        if index is None:
+            assert isinstance(parent, (Exists, Select)), (node, parent)
         self.index = index
         # XXX FIXME : find a better way to do that
         self.binary_remove = isinstance(node, BinaryNode)
@@ -152,9 +152,11 @@
         """undo the operation on the selection"""
         parent = self.node_parent
         if self.index is None:
-            assert isinstance(parent, Select)
-            sibling = parent.where = self.node
-            parent.where = self.node
+            if isinstance(parent, Select):
+                parent.where = self.node
+            else: # Exists
+                parent.query = self.node
+            sibling = self.node
         if self.binary_remove:
             # if 'parent' was a BinaryNode, then first reinsert the removed node
             # at the same pos in the original 'parent' Binary Node, and then
@@ -194,7 +196,7 @@
 
     def undo(self, selection):
         """undo the operation on the selection"""
-        self.stmt.remove_group_var(self.node)
+        self.stmt.remove_group_term(self.node)
 
 class RemoveGroupOperation(NodeOperation):
     """Defines how to undo 'remove group'."""
--- a/utils.py	Wed May 11 09:02:33 2011 +0200
+++ b/utils.py	Fri Dec 09 12:12:43 2011 +0100
@@ -68,7 +68,7 @@
 
 
 from logilab.common.decorators import monkeypatch
-from logilab.database import SQL_FUNCTIONS_REGISTRY, FunctionDescr
+from logilab.database import SQL_FUNCTIONS_REGISTRY, FunctionDescr, CAST
 
 RQL_FUNCTIONS_REGISTRY = SQL_FUNCTIONS_REGISTRY.copy()
 
@@ -85,6 +85,19 @@
         raise BadRQLQuery("backend %s doesn't support function %s" % (backend, self.name))
 
 
+@monkeypatch(FunctionDescr)
+def rql_return_type(self, funcnode):
+    return self.rtype
+
+@monkeypatch(CAST)
+def st_description(self, funcnode, mainindex, tr):
+    return self.rql_return_type(funcnode)
+
+@monkeypatch(CAST)
+def rql_return_type(self, funcnode):
+    return funcnode.children[0].value
+
+
 def iter_funcnode_variables(funcnode):
     for term in funcnode.children:
         try: