backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 05 Aug 2011 14:19:18 +0200
changeset 660 64ade95d2b85
parent 651 2a267149fee6 (current diff)
parent 659 5dc45f9b41e3 (diff)
child 661 10b45c8c870d
backport stable
--- a/.hgtags	Wed Jul 27 19:28:00 2011 +0200
+++ b/.hgtags	Fri Aug 05 14:19:18 2011 +0200
@@ -65,3 +65,5 @@
 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
--- a/ChangeLog	Wed Jul 27 19:28:00 2011 +0200
+++ b/ChangeLog	Fri Aug 05 14:19:18 2011 +0200
@@ -1,6 +1,36 @@
 ChangeLog for RQL
 =================
 
+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
--- a/__pkginfo__.py	Wed Jul 27 19:28:00 2011 +0200
+++ b/__pkginfo__.py	Fri Aug 05 14:19:18 2011 +0200
@@ -20,7 +20,7 @@
 __docformat__ = "restructuredtext en"
 
 modname = "rql"
-numversion = (0, 29, 1)
+numversion = (0, 30, 0)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL'
--- a/base.py	Wed Jul 27 19:28:00 2011 +0200
+++ b/base.py	Fri Aug 05 14:19:18 2011 +0200
@@ -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 Jul 27 19:28:00 2011 +0200
+++ b/debian/changelog	Fri Aug 05 14:19:18 2011 +0200
@@ -1,3 +1,9 @@
+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
@@ -10,7 +16,7 @@
     - 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/nodes.py	Wed Jul 27 19:28:00 2011 +0200
+++ b/nodes.py	Fri Aug 05 14:19:18 2011 +0200
@@ -76,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__ = ()
@@ -483,7 +508,7 @@
         self.optional= value
 
 
-OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE', 'REGEXP'))
+CMP_OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE', 'REGEXP'))
 
 class Comparison(HSMixin, Node):
     """handle comparisons:
@@ -496,7 +521,7 @@
         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:
@@ -524,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(),
@@ -536,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),
@@ -587,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):
@@ -1060,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 Jul 27 19:28:00 2011 +0200
+++ b/parser.g	Fri Aug 05 14:19:18 2011 +0200
@@ -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'
@@ -89,8 +89,10 @@
     token NULL:        r'(?i)NULL'
     token EXISTS:      r'(?i)EXISTS'
     token CMP_OP:      r'(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP'
-    token ADD_OP:      r'\+|-'
-    token MUL_OP:      r'\*|/'
+    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]*'
@@ -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,11 +167,6 @@
 
 #// 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              {{ nodes = [] }}
                    expr_add<<S>>        {{ nodes.append(expr_add) }}
                    ( ',' expr_add<<S>>  {{ nodes.append(expr_add) }}
@@ -315,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 Jul 27 19:28:00 2011 +0200
+++ b/parser.py	Fri Aug 05 14:19:18 2011 +0200
@@ -96,8 +96,10 @@
         ('NULL', re.compile('(?i)NULL')),
         ('EXISTS', re.compile('(?i)EXISTS')),
         ('CMP_OP', re.compile('(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP')),
-        ('ADD_OP', re.compile('\\+|-')),
-        ('MUL_OP', re.compile('\\*|/')),
+        ('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]*')),
@@ -212,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):
@@ -226,21 +225,6 @@
             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)
@@ -249,63 +233,55 @@
             nodes = []
             expr_add = self.expr_add(S, _context)
             nodes.append(expr_add)
-            while self._peek("','", 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'GROUPBY', 'r"\\)"', context=_context) == "','":
+            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
-        elif 1:
+        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])
@@ -325,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
@@ -343,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)
@@ -387,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)
@@ -397,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)
@@ -460,18 +434,18 @@
 
     def opt_right(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'opt_right', [S])
-        _token = self._peek('QMARK', 'AND', 'OR', "','", 'r"\\)"', 'WITH', 'GROUPBY', 'HAVING', '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)
@@ -481,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)
@@ -491,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)
@@ -499,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)
@@ -510,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)
@@ -551,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', '":"', 'CMP_OP', "'IN'", 'HAVING', "';'", 'MUL_OP', 'BEING', 'WITH', 'GROUPBY', 'ORDERBY', 'ADD_OP', 'LIMIT', 'OFFSET', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', '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)
@@ -561,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)
@@ -577,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)
@@ -588,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', '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)
+        _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', '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)
+        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
@@ -632,7 +627,7 @@
         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("','", '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)
@@ -647,7 +642,7 @@
         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("','", '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)
--- a/stcheck.py	Wed Jul 27 19:28:00 2011 +0200
+++ b/stcheck.py	Fri Aug 05 14:19:18 2011 +0200
@@ -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:
@@ -528,16 +532,26 @@
             # XXX node.having is a list of size 1
             assert len(node.having) == 1
             for term in node.having[0].get_nodes(Comparison):
-                for vref in term.iget_nodes(VariableRef):
-                    vref.variable.stinfo.setdefault('having', []).append(term)
+                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:
-                    lhsvariables = set(vref.name for vref in term.children[0].get_nodes(VariableRef))
-                    rhsvariables = set(vref.name for vref in term.children[1].get_nodes(VariableRef))
                     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:
+                        optcomps = var.stinfo['attrvar'].stinfo.setdefault('optcomparisons', set())
+                        optcomps.add(term)
+                if term.optional in ('right', 'both'):
+                    for var in rhsvariables:
+                        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
--- a/test/unittest_parser.py	Wed Jul 27 19:28:00 2011 +0200
+++ b/test/unittest_parser.py	Fri Aug 05 14:19:18 2011 +0200
@@ -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';",