backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 27 Jul 2011 19:28:00 +0200
changeset 651 2a267149fee6
parent 645 bc901a7460d1 (current diff)
parent 650 52b945da43ea (diff)
child 660 64ade95d2b85
backport stable
--- a/.hgtags	Wed Jul 20 17:11:06 2011 +0200
+++ b/.hgtags	Wed Jul 27 19:28:00 2011 +0200
@@ -63,3 +63,5 @@
 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
--- a/ChangeLog	Wed Jul 20 17:11:06 2011 +0200
+++ b/ChangeLog	Wed Jul 27 19:28:00 2011 +0200
@@ -1,7 +1,7 @@
 ChangeLog for RQL
 =================
 
---
+2011-07-27  --  0.29.1
     * #70264: remove_group_var renamed into remove_group_term and fixed
       implementation
 
@@ -15,17 +15,27 @@
 
     * #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
       relation should be added (used by cw multi-sources planner)
--- a/__pkginfo__.py	Wed Jul 20 17:11:06 2011 +0200
+++ b/__pkginfo__.py	Wed Jul 27 19:28:00 2011 +0200
@@ -20,7 +20,7 @@
 __docformat__ = "restructuredtext en"
 
 modname = "rql"
-numversion = (0, 29, 0)
+numversion = (0, 29, 1)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL'
--- a/debian/changelog	Wed Jul 20 17:11:06 2011 +0200
+++ b/debian/changelog	Wed Jul 27 19:28:00 2011 +0200
@@ -1,3 +1,9 @@
+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:
--- a/nodes.py	Wed Jul 20 17:11:06 2011 +0200
+++ b/nodes.py	Wed Jul 27 19:28:00 2011 +0200
@@ -490,20 +490,29 @@
 
      <, <=, =, >=, > 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
         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):
--- a/parser.g	Wed Jul 20 17:11:06 2011 +0200
+++ b/parser.g	Wed Jul 27 19:28:00 2011 +0200
@@ -233,7 +233,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) }}
@@ -265,7 +265,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) }}
@@ -280,11 +280,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 ###########################################################
--- a/parser.py	Wed Jul 20 17:11:06 2011 +0200
+++ b/parser.py	Wed Jul 27 19:28:00 2011 +0200
@@ -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
 
 
@@ -451,16 +451,16 @@
 
     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', 'GROUPBY', 'HAVING', 'ORDERBY', "';'", 'LIMIT', 'OFFSET', 'WHERE', context=_context)
         if _token == 'QMARK':
             QMARK = self._scan('QMARK', context=_context)
             return 'right'
@@ -518,8 +518,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')
 
@@ -549,7 +551,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', "';'", 'MUL_OP', 'BEING', 'WITH', 'GROUPBY', 'ORDERBY', 'ADD_OP', 'LIMIT', 'OFFSET', 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'AND', 'OR', context=_context) == "','":
             R.add_main_variable(E_TYPE, var)
             self._scan("','", context=_context)
             E_TYPE = self._scan('E_TYPE', context=_context)
@@ -588,7 +590,7 @@
         _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':
+        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 )
@@ -598,7 +600,7 @@
         _context = self.Context(_parent, self._scanner, 'expr_mul', [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':
+        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_base = self.expr_base(S, _context)
             node = MathExpression( MUL_OP, node, expr_base)
@@ -632,7 +634,7 @@
         F = Function(FUNCTION)
         if self._peek('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)
@@ -647,7 +649,7 @@
         F = Function('IN')
         if self._peek('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)
@@ -701,7 +703,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:
--- a/stcheck.py	Wed Jul 20 17:11:06 2011 +0200
+++ b/stcheck.py	Wed Jul 27 19:28:00 2011 +0200
@@ -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 '
--- a/test/unittest_parser.py	Wed Jul 20 17:11:06 2011 +0200
+++ b/test/unittest_parser.py	Wed Jul 27 19:28:00 2011 +0200
@@ -158,6 +158,10 @@
     '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):
@@ -315,7 +319,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: