default is stable stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 10 Jun 2011 08:04:00 +0200
branchstable
changeset 636 6030a30b10af
parent 631 6b28ea39cf54 (current diff)
parent 635 bc9c7885a709 (diff)
child 637 23ae480e4e43
default is stable
--- a/.hgtags	Thu May 26 01:50:01 2011 +0200
+++ b/.hgtags	Fri Jun 10 08:04:00 2011 +0200
@@ -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,5 @@
 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
--- a/ChangeLog	Thu May 26 01:50:01 2011 +0200
+++ b/ChangeLog	Fri Jun 10 08:04:00 2011 +0200
@@ -2,7 +2,11 @@
 =================
 
 --
-* suport != operator for non equality
+* 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	Thu May 26 01:50:01 2011 +0200
+++ b/__pkginfo__.py	Fri Jun 10 08:04:00 2011 +0200
@@ -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, 29, 0)
 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/debian/changelog	Thu May 26 01:50:01 2011 +0200
+++ b/debian/changelog	Fri Jun 10 08:04:00 2011 +0200
@@ -1,10 +1,11 @@
-rql (0.28.0-2) UNRELEASED; urgency=low
+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	Thu May 26 01:50:01 2011 +0200
+++ b/debian/control	Fri Jun 10 08:04:00 2011 +0200
@@ -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	Thu May 26 01:50:01 2011 +0200
+++ b/makefile	Fri Jun 10 08:04:00 2011 +0200
@@ -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	Thu May 26 01:50:01 2011 +0200
+++ b/nodes.py	Fri Jun 10 08:04:00 2011 +0200
@@ -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,
@@ -441,12 +443,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'))
@@ -485,7 +482,7 @@
         self.optional= value
 
 
-OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE'))
+OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE', 'REGEXP'))
 
 class Comparison(HSMixin, Node):
     """handle comparisons:
@@ -626,7 +623,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:
--- a/parser.g	Thu May 26 01:50:01 2011 +0200
+++ b/parser.g	Fri Jun 10 08:04:00 2011 +0200
@@ -88,7 +88,7 @@
     token FALSE:       r'(?i)FALSE'
     token NULL:        r'(?i)NULL'
     token EXISTS:      r'(?i)EXISTS'
-    token CMP_OP:      r'(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE'
+    token CMP_OP:      r'(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP'
     token ADD_OP:      r'\+|-'
     token MUL_OP:      r'\*|/'
     token FUNCTION:    r'[A-Za-z_]+\s*(?=\()'
@@ -99,7 +99,7 @@
     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'
 
@@ -173,7 +173,10 @@
 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]) }}
--- a/parser.py	Thu May 26 01:50:01 2011 +0200
+++ b/parser.py	Fri Jun 10 08:04:00 2011 +0200
@@ -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-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 
 
 Select statement grammar
@@ -109,7 +95,7 @@
         ('FALSE', re.compile('(?i)FALSE')),
         ('NULL', re.compile('(?i)NULL')),
         ('EXISTS', re.compile('(?i)EXISTS')),
-        ('CMP_OP', re.compile('(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE')),
+        ('CMP_OP', re.compile('(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE|REGEXP')),
         ('ADD_OP', re.compile('\\+|-')),
         ('MUL_OP', re.compile('\\*|/')),
         ('FUNCTION', re.compile('[A-Za-z_]+\\s*(?=\\()')),
@@ -119,7 +105,7 @@
         ('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')),
     ]
@@ -260,8 +246,14 @@
         _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
+            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) == "','":
+                self._scan("','", context=_context)
+                expr_add = self.expr_add(S, _context)
+                nodes.append(expr_add)
+            S.set_groupby(nodes); return True
         elif 1:
             pass
         else:
@@ -547,7 +539,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)
@@ -719,25 +711,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	Thu May 26 01:50:01 2011 +0200
+++ b/stcheck.py	Fri Jun 10 08:04:00 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.
@@ -455,11 +455,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
@@ -660,6 +665,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/test/unittest_editextensions.py	Thu May 26 01:50:01 2011 +0200
+++ b/test/unittest_editextensions.py	Fri Jun 10 08:04:00 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.
@@ -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	Thu May 26 01:50:01 2011 +0200
+++ b/test/unittest_nodes.py	Fri Jun 10 08:04:00 2011 +0200
@@ -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	Thu May 26 01:50:01 2011 +0200
+++ b/test/unittest_parser.py	Fri Jun 10 08:04:00 2011 +0200
@@ -153,6 +153,11 @@
     '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;',
     )
 
 class ParserHercule(TestCase):
--- a/test/unittest_stcheck.py	Thu May 26 01:50:01 2011 +0200
+++ b/test/unittest_stcheck.py	Fri Jun 10 08:04:00 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.
@@ -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
@@ -313,5 +315,10 @@
         self.failUnless(C.scope is rqlst.with_[0].query.children[0], C.scope)
         self.assertEqual(len(C.stinfo['relations']), 2)
 
+    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	Thu May 26 01:50:01 2011 +0200
+++ b/undo.py	Fri Jun 10 08:04:00 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.
@@ -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
--- a/utils.py	Thu May 26 01:50:01 2011 +0200
+++ b/utils.py	Fri Jun 10 08:04:00 2011 +0200
@@ -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: