allow optional on final relation (rhs only) and in having expression (hence Comparison node gain a 'optional' attribute). Closes #71415 stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 22 Jul 2011 10:23:02 +0200
branchstable
changeset 647 6afe7acaf314
parent 646 e1cc9656bb4c
child 648 cf4fcca7c289
allow optional on final relation (rhs only) and in having expression (hence Comparison node gain a 'optional' attribute). Closes #71415
ChangeLog
nodes.py
parser.g
parser.py
stcheck.py
test/unittest_parser.py
--- a/ChangeLog	Thu Jul 21 08:11:07 2011 +0200
+++ b/ChangeLog	Fri Jul 22 10:23:02 2011 +0200
@@ -15,6 +15,8 @@
 
     * #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
--- a/nodes.py	Thu Jul 21 08:11:07 2011 +0200
+++ b/nodes.py	Fri Jul 22 10:23:02 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	Thu Jul 21 08:11:07 2011 +0200
+++ b/parser.g	Fri Jul 22 10:23:02 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	Thu Jul 21 08:11:07 2011 +0200
+++ b/parser.py	Fri Jul 22 10:23:02 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	Thu Jul 21 08:11:07 2011 +0200
+++ b/stcheck.py	Fri Jul 22 10:23:02 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	Thu Jul 21 08:11:07 2011 +0200
+++ b/test/unittest_parser.py	Fri Jul 22 10:23:02 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: