stable is 0.31 stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 09 Dec 2011 12:12:56 +0100
branchstable
changeset 678 a031d85966f1
parent 666 8828046c2d3b (current diff)
parent 676 601279ab9600 (diff)
child 679 de1bbfec40c3
stable is 0.31
--- a/.hgtags	Tue Oct 11 14:14:44 2011 +0200
+++ b/.hgtags	Fri Dec 09 12:12:56 2011 +0100
@@ -69,3 +69,5 @@
 c3ae2279fe701809096d5f6bc50f8d8b4a9fd4fd rql-debian-version-0.30.0-1
 3c17b96750ad5045bd1fb43241da55c81c094bd4 rql-version-0.30.1
 13cd741f8e144265c15bb225fad863234fc8ce16 rql-debian-version-0.30.1-1
+bb70a998ced6a839fd1e28686e4ff5254feefde3 rql-version-0.31.0
+f4f27e4c588e40ba9c6c9c5d92cb22f74f1207a6 rql-debian-version-0.31.0-1
--- a/ChangeLog	Tue Oct 11 14:14:44 2011 +0200
+++ b/ChangeLog	Fri Dec 09 12:12:56 2011 +0100
@@ -3,7 +3,11 @@
 
 --
     * #78681: don't crash on column aliases used in outer join
-
+    * #81394: HAVING support in write queries (INSERT,SET,DELETE)
+    * #80799: fix wrong type analysis with 'NOT identity'
+    * when possible, use entity type as translation context of relation
+      (break cw < 3.13.10 compat)
+    * #81817: fix add_type_restriction for cases where some types restriction is already in there
 
 2011-09-07  --  0.30.1
 
--- a/__pkginfo__.py	Tue Oct 11 14:14:44 2011 +0200
+++ b/__pkginfo__.py	Fri Dec 09 12:12:56 2011 +0100
@@ -20,7 +20,7 @@
 __docformat__ = "restructuredtext en"
 
 modname = "rql"
-numversion = (0, 30, 1)
+numversion = (0, 31, 0)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL'
--- a/analyze.py	Tue Oct 11 14:14:44 2011 +0200
+++ b/analyze.py	Fri Dec 09 12:12:56 2011 +0100
@@ -469,9 +469,11 @@
         """extract constraints for an relation according to it's  type"""
         if relation.is_types_restriction():
             self.visit_type_restriction(relation, constraints)
-            return True
+            return None
         rtype = relation.r_type
         lhs, rhs = relation.get_parts()
+        if rtype == 'identity' and relation.neged(strict=True):
+            return None
         if rtype in self.uid_func_mapping:
             if isinstance(relation.parent, nodes.Not) or relation.operator() != '=':
                 # non final entity types
@@ -480,22 +482,22 @@
                 etypes = self._uid_node_types(rhs)
             if etypes:
                 constraints.var_has_types( lhs.name, etypes )
-                return True
+                return None
         if isinstance(rhs, nodes.Comparison):
             rhs = rhs.children[0]
         rschema = self.schema.rschema(rtype)
         if isinstance(lhs, nodes.Constant): # lhs is a constant node (simplified tree)
             if not isinstance(rhs, nodes.VariableRef):
-                return True
+                return None
             self._extract_constraint(constraints, rhs.name, lhs, rschema.objects)
         elif isinstance(rhs, nodes.Constant) and not rschema.final:
             # rhs.type is None <-> NULL
             if not isinstance(lhs, nodes.VariableRef) or rhs.type is None:
-                return True
+                return None
             self._extract_constraint(constraints, lhs.name, rhs, rschema.subjects)
         elif not isinstance(lhs, nodes.VariableRef):
             # XXX: check relation is valid
-            return True
+            return None
         elif isinstance(rhs, nodes.VariableRef):
             lhsvar = lhs.name
             rhsvar = rhs.name
@@ -520,7 +522,7 @@
             ptypes = [str(subj) for subj in rschema.subjects()
                       if subj in lhsdomain]
             constraints.var_has_types( lhs.name, ptypes )
-        return True
+        return None
 
     def visit_type_restriction(self, relation, constraints):
         lhs, rhs = relation.get_parts()
--- a/debian/changelog	Tue Oct 11 14:14:44 2011 +0200
+++ b/debian/changelog	Fri Dec 09 12:12:56 2011 +0100
@@ -1,3 +1,9 @@
+rql (0.31.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 09 Nov 2011 18:18:01 +0100
+
 rql (0.30.1-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Tue Oct 11 14:14:44 2011 +0200
+++ b/debian/control	Fri Dec 09 12:12:56 2011 +0100
@@ -12,7 +12,7 @@
 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 (>= 1.6.0)
-Conflicts: cubicweb-common (<= 3.8.3)
+Conflicts: cubicweb-common (<= 3.13.9)
 Provides: ${python:Provides}
 Description: relationship query language (RQL) utilities
  A library providing the base utilities to handle RQL queries,
--- a/nodes.py	Tue Oct 11 14:14:44 2011 +0200
+++ b/nodes.py	Fri Dec 09 12:12:56 2011 +0100
@@ -30,7 +30,7 @@
 
 from logilab.database import DYNAMIC_RTYPE
 
-from rql import CoercionError
+from rql import CoercionError, RQLException
 from rql.base import BaseNode, Node, BinaryNode, LeafNode
 from rql.utils import (function_description, quote, uquote, build_visitor_stub,
                        common_parent)
@@ -218,6 +218,39 @@
 
     def add_type_restriction(self, var, etype):
         """builds a restriction node to express : variable is etype"""
+        typerel = var.stinfo.get('typerel', None)
+        if typerel:
+            istarget = typerel.children[1].children[0]
+            if typerel.r_type == 'is':
+                if isinstance(istarget, Constant):
+                    etypes = (istarget.value,)
+                else: # Function (IN)
+                    etypes = [et.value for et in istarget.children]
+                if etype not in etypes:
+                    raise RQLException('%r not in %r' % (etype, etypes))
+                if len(etypes) > 1:
+                    for child in istarget.children:
+                        if child.value != etype:
+                            istarget.remove(child)
+            else:
+                # let's botte en touche IN cases (who would do that anyway ?)
+                if isinstance(istarget, Function):
+                    msg = 'adding type restriction over is_instance_of IN is not supported'
+                    raise NotImplementedError(msg)
+                schema = self.root.schema
+                if schema is None:
+                    msg = 'restriction with is_instance_of cannot be done without a schema'
+                    raise RQLException(msg)
+                # let's check the restriction is compatible
+                eschema = schema[etype]
+                ancestors = set(eschema.ancestors())
+                ancestors.add(etype) # let's be unstrict
+                if istarget.value in ancestors:
+                    istarget.value = etype
+                else:
+                    raise RQLException('type restriction %s-%s cannot be made on %s' %
+                                       (var, etype, self))
+            return typerel
         return self.add_constant_restriction(var, 'is', etype, 'etype')
 
 
@@ -956,7 +989,9 @@
         if solution:
             return solution[self.name]
         if self.stinfo['typerel']:
-            return str(self.stinfo['typerel'].children[1].children[0].value)
+            rhs = self.stinfo['typerel'].children[1].children[0]
+            if isinstance(rhs, Constant):
+                return str(rhs.value)
         schema = self.schema
         if schema is not None:
             for rel in self.stinfo['rhsrelations']:
@@ -997,12 +1032,17 @@
             if mainindex is not None:
                 # relation to the main variable, stop searching
                 lhsvar = getattr(lhs, 'variable', None)
+                context = None
                 if lhsvar is not None and mainindex in lhsvar.stinfo['selected']:
-                    return tr(rtype)
+                    if len(lhsvar.stinfo['possibletypes']) == 1:
+                        context = iter(lhsvar.stinfo['possibletypes']).next()
+                    return tr(rtype, context=context)
                 if rhsvar is not None and mainindex in rhsvar.stinfo['selected']:
+                    if len(rhsvar.stinfo['possibletypes']) == 1:
+                        context = iter(rhsvar.stinfo['possibletypes']).next()
                     if schema is not None and rschema.symmetric:
-                        return tr(rtype)
-                    return tr(rtype + '_object')
+                        return tr(rtype, context=context)
+                    return tr(rtype + '_object', context=context)
             if rhsvar is self:
                 rtype += '_object'
         if frtype is not None:
--- a/parser.g	Tue Oct 11 14:14:44 2011 +0200
+++ b/parser.g	Fri Dec 09 12:12:56 2011 +0100
@@ -119,9 +119,9 @@
 
 #// Deletion  ###################################################################
 
-rule _delete<<R>>: decl_rels<<R>> where<<R>> {{ return R }}
+rule _delete<<R>>: decl_rels<<R>> where<<R>> having<<R>> {{ return R }}
 
-                 | decl_vars<<R>> where<<R>> {{ return R }}
+                 | decl_vars<<R>> where<<R>> having<<R>> {{ return R }}
 
 
 #// Insertion  ##################################################################
@@ -129,14 +129,14 @@
 rule _insert<<R>>: decl_vars<<R>> insert_rels<<R>> {{ return R }}
 
 
-rule insert_rels<<R>>: ":" decl_rels<<R>> where<<R>> {{ return R }}
+rule insert_rels<<R>>: ":" decl_rels<<R>> where<<R>> having<<R>> {{ return R }}
 
                      |
 
 
 #// Update  #####################################################################
 
-rule update<<R>>: decl_rels<<R>> where<<R>> {{ return R }}
+rule update<<R>>: decl_rels<<R>> where<<R>> having<<R>> {{ return R }}
 
 
 #// Selection  ##################################################################
--- a/parser.py	Tue Oct 11 14:14:44 2011 +0200
+++ b/parser.py	Fri Dec 09 12:12:56 2011 +0100
@@ -145,10 +145,12 @@
         if _token == 'VARIABLE':
             decl_rels = self.decl_rels(R, _context)
             where = self.where(R, _context)
+            having = self.having(R, _context)
             return R
         else: # == 'E_TYPE'
             decl_vars = self.decl_vars(R, _context)
             where = self.where(R, _context)
+            having = self.having(R, _context)
             return R
 
     def _insert(self, R, _parent=None):
@@ -164,6 +166,7 @@
             self._scan('":"', context=_context)
             decl_rels = self.decl_rels(R, _context)
             where = self.where(R, _context)
+            having = self.having(R, _context)
             return R
         else: # == "';'"
             pass
@@ -172,6 +175,7 @@
         _context = self.Context(_parent, self._scanner, 'update', [R])
         decl_rels = self.decl_rels(R, _context)
         where = self.where(R, _context)
+        having = self.having(R, _context)
         return R
 
     def union(self, R, _parent=None):
@@ -243,12 +247,12 @@
 
     def having(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'having', [S])
-        _token = self._peek('HAVING', 'WITH', '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])
-        else: # in ['WITH', 'r"\\)"', "';'"]
+        else: # in ['WITH', "';'", 'r"\\)"']
             pass
 
     def orderby(self, S, _parent=None):
@@ -339,19 +343,19 @@
 
     def where(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'where', [S])
-        _token = self._peek('WHERE', 'HAVING', "';'", 'WITH', '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)
-        else: # in ['HAVING', "';'", 'WITH', 'r"\\)"']
+        else: # in ['HAVING', 'WITH', "';'", 'r"\\)"']
             pass
 
     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', 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)
@@ -361,7 +365,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', 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)
@@ -371,7 +375,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', 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)
@@ -434,11 +438,11 @@
 
     def opt_right(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'opt_right', [S])
-        _token = self._peek('QMARK', 'AND', 'OR', "','", 'r"\\)"', 'WITH', 'HAVING', "';'", 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: # in ['AND', 'OR', "','", 'r"\\)"', 'WITH', 'HAVING', "';'"]
+        else: # in ['AND', 'OR', "','", 'r"\\)"', 'WITH', "';'", 'HAVING']
             pass
 
     def logical_expr(self, S, _parent=None):
@@ -525,7 +529,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', "';'", 'POW_OP', 'BEING', 'WITH', 'MUL_OP', 'r"\\)"', 'ADD_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'AND', 'OR', context=_context) == "','":
+        while self._peek("','", 'R_TYPE', 'QMARK', 'WHERE', '":"', 'CMP_OP', 'HAVING', "'IN'", "';'", '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)
@@ -535,7 +539,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', '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)
@@ -566,7 +570,7 @@
         if _token != 'UNARY_OP':
             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':
+            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 )
@@ -575,7 +579,7 @@
             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':
+            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 )
@@ -585,7 +589,7 @@
         _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':
+        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)
@@ -595,7 +599,7 @@
         _context = self.Context(_parent, self._scanner, 'expr_pow', [S])
         expr_base = self.expr_base(S, _context)
         node = expr_base
-        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':
+        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)
@@ -629,7 +633,7 @@
         F = Function(FUNCTION)
         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) == "','":
+            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)
@@ -644,7 +648,7 @@
         F = Function('IN')
         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) == "','":
+            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)
--- a/stmts.py	Tue Oct 11 14:14:44 2011 +0200
+++ b/stmts.py	Fri Dec 09 12:12:56 2011 +0100
@@ -60,6 +60,7 @@
     solutions = ()   # list of possibles solutions for used variables
     _varmaker = None # variable names generator, built when necessary
     where = None     # where clause node
+    having = ()      # XXX now a single node
 
     def __init__(self):
         # dictionnary of defined variables in the original RQL syntax tree
@@ -72,6 +73,11 @@
         self.where = node
         node.parent = self
 
+    def set_having(self, terms):
+        self.having = terms
+        for node in terms:
+            node.parent = self
+
     def copy(self, copy_solutions=True, solutions=None):
         new = self.__class__()
         if self.schema is not None:
@@ -249,7 +255,7 @@
           returning a string
         """
         if tr is None:
-            tr = lambda x: x
+            tr = lambda x,**k: x
         return [c.get_description(mainindex, tr) for c in self.children]
 
     # repr / as_string / copy #################################################
@@ -409,7 +415,6 @@
     # select clauses
     groupby = ()
     orderby = ()
-    having = () # XXX now a single node
     with_ = ()
     # set by the annotator
     has_aggregat = False
@@ -577,11 +582,6 @@
         for node in terms:
             node.parent = self
 
-    def set_having(self, terms):
-        self.having = terms
-        for node in terms:
-            node.parent = self
-
     def set_with(self, terms, check=True):
         self.with_ = []
         for node in terms:
@@ -888,6 +888,8 @@
         children += self.main_relations
         if self.where:
             children.append(self.where)
+        if self.having:
+            children += self.having
         return children
 
     @property
@@ -922,6 +924,8 @@
             result.append(', '.join([repr(rel) for rel in self.main_relations]))
         if self.where is not None:
             result.append(repr(self.where))
+        if self.having:
+            result.append('HAVING ' + ','.join(repr(term) for term in self.having))
         return ' '.join(result)
 
     def as_string(self, encoding=None, kwargs=None):
@@ -937,6 +941,9 @@
                                      for rel in self.main_relations]))
         if self.where is not None:
             result.append('WHERE ' + self.where.as_string(encoding, kwargs))
+        if self.having:
+            result.append('HAVING ' + ','.join(term.as_string(encoding, kwargs)
+                                          for term in self.having))
         return ' '.join(result)
 
     def copy(self):
@@ -948,6 +955,8 @@
             new.add_main_relation(child.copy(new))
         if self.where:
             new.set_where(self.where.copy(new))
+        if self.having:
+            new.set_having([sq.copy(new) for sq in self.having])
         return new
 
 
@@ -969,6 +978,8 @@
         children += self.main_relations
         if self.where:
             children.append(self.where)
+        if self.having:
+            children += self.having
         return children
 
     @property
@@ -1006,6 +1017,8 @@
             result.append(', '.join([repr(rel) for rel in self.main_relations]))
         if self.where is not None:
             result.append('WHERE ' + repr(self.where))
+        if self.having:
+            result.append('HAVING ' + ','.join(repr(term) for term in self.having))
         return ' '.join(result)
 
     def as_string(self, encoding=None, kwargs=None):
@@ -1019,6 +1032,9 @@
                                      for rel in self.main_relations]))
         if self.where is not None:
             result.append('WHERE ' + self.where.as_string(encoding, kwargs))
+        if self.having:
+            result.append('HAVING ' + ','.join(term.as_string(encoding, kwargs)
+                                               for term in self.having))
         return ' '.join(result)
 
     def copy(self):
@@ -1030,6 +1046,8 @@
             new.add_main_relation(child.copy(new))
         if self.where:
             new.set_where(self.where.copy(new))
+        if self.having:
+            new.set_having([sq.copy(new) for sq in self.having])
         return new
 
 
@@ -1048,6 +1066,8 @@
         children = self.main_relations[:]
         if self.where:
             children.append(self.where)
+        if self.having:
+            children += self.having
         return children
 
     @property
@@ -1066,6 +1086,8 @@
         result.append(', '.join(repr(rel) for rel in self.main_relations))
         if self.where is not None:
             result.append('WHERE ' + repr(self.where))
+        if self.having:
+            result.append('HAVING ' + ','.join(repr(term) for term in self.having))
         return ' '.join(result)
 
     def as_string(self, encoding=None, kwargs=None):
@@ -1075,6 +1097,9 @@
                                 for rel in self.main_relations))
         if self.where is not None:
             result.append('WHERE ' + self.where.as_string(encoding, kwargs))
+        if self.having:
+            result.append('HAVING ' + ','.join(term.as_string(encoding, kwargs)
+                                               for term in self.having))
         return ' '.join(result)
 
     def copy(self):
@@ -1083,6 +1108,8 @@
             new.add_main_relation(child.copy(new))
         if self.where:
             new.set_where(self.where.copy(new))
+        if self.having:
+            new.set_having([sq.copy(new) for sq in self.having])
         return new
 
 
--- a/test/unittest_analyze.py	Tue Oct 11 14:14:44 2011 +0200
+++ b/test/unittest_analyze.py	Fri Dec 09 12:12:56 2011 +0100
@@ -300,6 +300,7 @@
                                 {'X': 'Person', 'T': 'Eetype'},
                                 {'X': 'Student', 'T': 'Eetype'}])
 
+
     def test_not(self):
         node = self.helper.parse('Any X WHERE NOT X is Person')
         self.helper.compute_solutions(node, debug=DEBUG)
@@ -308,6 +309,15 @@
         expected.remove({'X': 'Person'})
         self.assertEqual(sols, expected)
 
+    def test_not_identity(self):
+        node = self.helper.parse('Any X WHERE X located A, P is Person, NOT X identity P')
+        self.helper.compute_solutions(node, debug=DEBUG)
+        sols = sorted(node.children[0].solutions)
+        self.assertEqual(sols, [{'X': 'Company', 'A': 'Address', 'P': 'Person'},
+                                {'X': 'Person', 'A': 'Address', 'P': 'Person'},
+                                {'X': 'Student', 'A': 'Address', 'P': 'Person'},
+                                ])
+
     def test_uid_func_mapping(self):
         h = self.helper
         def type_from_uid(name):
--- a/test/unittest_nodes.py	Tue Oct 11 14:14:44 2011 +0200
+++ b/test/unittest_nodes.py	Fri Dec 09 12:12:56 2011 +0100
@@ -21,7 +21,7 @@
 
 from logilab.common.testlib import TestCase, unittest_main
 
-from rql import nodes, stmts, parse, BadRQLQuery, RQLHelper
+from rql import nodes, stmts, parse, BadRQLQuery, RQLHelper, RQLException
 
 from unittest_analyze import DummySchema
 schema = DummySchema()
@@ -55,6 +55,43 @@
         self.assertEqual(nodes.etype_from_pyobj(u'hop'), 'String')
 
 
+class TypesRestrictionNodesTest(TestCase):
+
+    def setUp(self):
+        self.parse = helper.parse
+        self.simplify = helper.simplify
+
+    def test_add_is_type_restriction(self):
+        tree = self.parse('Any X WHERE X is Person')
+        select = tree.children[0]
+        x = select.get_selected_variables().next()
+        self.assertRaises(RQLException, select.add_type_restriction, x.variable, 'Babar')
+        select.add_type_restriction(x.variable, 'Person')
+        self.assertEqual(tree.as_string(), 'Any X WHERE X is Person')
+
+    def test_add_new_is_type_restriction_in(self):
+        tree = self.parse('Any X WHERE X is IN(Person, Company)')
+        select = tree.children[0]
+        x = select.get_selected_variables().next()
+        select.add_type_restriction(x.variable, 'Company')
+        # implementation is KISS (the IN remains)
+        self.assertEqual(tree.as_string(), 'Any X WHERE X is IN(Company)')
+
+    def test_add_is_in_type_restriction(self):
+        tree = self.parse('Any X WHERE X is IN(Person, Company)')
+        select = tree.children[0]
+        x = select.get_selected_variables().next()
+        self.assertRaises(RQLException, select.add_type_restriction, x.variable, 'Babar')
+        self.assertEqual(tree.as_string(), 'Any X WHERE X is IN(Person, Company)')
+
+    # XXX a full schema is needed, see test in cw/server/test/unittest_security
+    # def test_add_is_against_isintance_type_restriction(self):
+    #     tree = self.parse('Any X WHERE X is_instance_of Person')
+    #     select = tree.children[0]
+    #     x = select.get_selected_variables().next()
+    #     select.add_type_restriction(x.variable, 'Student')
+    #     self.parse(tree.as_string())
+
 class NodesTest(TestCase):
     def _parse(self, rql, normrql=None):
         tree = parse(rql + ';')
@@ -508,6 +545,12 @@
         tree = parse(u"Any X WHERE X creation_date TODAY")
         self.assertEqual(tree.as_string(), 'Any X WHERE X creation_date TODAY')
 
+
+    def test_get_type_is_in(self):
+        tree = sparse("Any X WHERE X is IN (Person, Company)")
+        select = tree.children[0]
+        self.assertEqual(select.defined_vars['X'].get_type(), 'Any')
+
     # sub-queries tests #######################################################
 
     def test_subq_colalias_compat(self):
@@ -527,8 +570,8 @@
         self.assertEqual(X.get_type(), 'Company')
         self.assertEqual(X.get_type({'X': 'Person'}), 'Person')
         #self.assertEqual(N.get_type(), 'String')
-        self.assertEqual(X.get_description(0, lambda x:x), 'Company, Person, Student')
-        self.assertEqual(N.get_description(0, lambda x:x), 'firstname, name')
+        self.assertEqual(X.get_description(0, lambda x,**k:x), 'Company, Person, Student')
+        self.assertEqual(N.get_description(0, lambda x,**k:x), 'firstname, name')
         self.assertEqual(X.selected_index(), 0)
         self.assertEqual(N.selected_index(), None)
         self.assertEqual(X.main_relation(), None)
@@ -604,5 +647,6 @@
         self.assertEqual(sorted(x.name for x in varrefs),
                           ['X', 'X', 'X', 'Y', 'Y'])
 
+
 if __name__ == '__main__':
     unittest_main()