backport stable into oldstable, not messing with the oldstable tag... oldstable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 11 May 2011 09:02:33 +0200
brancholdstable
changeset 624 be394528fdc1
parent 538 aa1d46c9f3ca (current diff)
parent 615 7d368dd97930 (diff)
child 677 31130f18a5ef
backport stable into oldstable, not messing with the oldstable tag...
--- a/.hgtags	Wed Apr 28 11:46:49 2010 +0200
+++ b/.hgtags	Wed May 11 09:02:33 2011 +0200
@@ -46,3 +46,19 @@
 4025f1f02d1da65d26eada37708409984942c432 oldstable
 3d59f6b1cbb90278f3b4374dce36b6e31c7e9884 rql-version-0.25.0
 360a6c3a48393f8d5353198d45fbcf25f9ef5369 rql-debian-version-0.25.0-1
+ae4cba1cf0240c615a8e78c94d81fa05e5ad8bc9 rql-version-0.26.0
+677736b455f5fb7a31882e37165dbd4879c4bf11 rql-debian-version-0.26.0-1
+42ae413193a8403a749fb1a206a86cec09f5efdb rql-version-0.26.1
+3142115086127f3e9995081fff3fef3d420838cf rql-debian-version-0.26.1-1
+7d5bef1742bc302309668982af10409bcc96eadf rql-version-0.26.2
+cb66c5a9918dd8958dd3cdf48f8bdd0c2786b76a rql-debian-version-0.26.2-1
+7fb422fc2032ecc5a93528ed382e083b212b1cbf rql-version-0.26.3
+aca033de456a6b526045f9be0dbdb770e67912ab rql-debian-version-0.26.3-1
+bcf24f8a29c07146220816565a132ba148cdf82a rql-version-0.26.4
+88b739e85c615fc41a964f39e853fe77aaf3f207 rql-debian-version-0.26.4-1
+7a1df18b3a3ed41aa49d4baf10246a8e2e65a7d6 rql-version-0.26.6
+23bd1f36ec77f30cd525327d408ef6836f88eb24 rql-debian-version-0.26.6-1
+3c59bf663ec78dad82016b43f58348d5e35058ad rql-version-0.27.0
+0a5a70c34c65fccaf64603613d5d295b332e85cb rql-debian-version-0.27.0-1
+ae02408da51e63aa2d1be6ac7170d77060bd0910 rql-version-0.28.0
+21e94bc12c1fcb7f97826fe6aae5dbe62cc4bd06 rql-debian-version-0.28.0-1
--- a/ChangeLog	Wed Apr 28 11:46:49 2010 +0200
+++ b/ChangeLog	Wed May 11 09:02:33 2011 +0200
@@ -1,6 +1,93 @@
 ChangeLog for RQL
 =================
 
+--
+* suport != operator for non equality
+
+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)
+
+
+
+2010-10-13  --  0.27.0
+    * select.undefine_variable properly cleanup solutions (and restore them on
+      undo)
+
+    * fix potential crash in Referenceable.get_description
+
+    * introduce make_constant_restriction function, useful to build a
+      restriction without adding it yet to the tree
+
+
+
+2010-09-10  --  0.26.6
+    * enhance bad rql query detection with ordered distinct (can't use distinct
+      if an attribute is selected and we sort on another attribute)
+
+    * fix subquery_selection_index responsability mess-up: it wasn't doing what
+      it should have done (see cw.rset related_entity implementation)
+
+    * consider subquery aliases in Select.clean_solutions
+
+    * add constraint package to setuptools dependencies so we've fallback
+      opportunity if gecode is not installed
+
+    * fix setuptools dependency on yapps by forcing install of our custom
+      package, so it don't try to install pypi's one which doesn't work well
+      with both pip and easy_install
+
+
+
+2010-08-02  --  0.26.5
+    * fix solutions computation crash with some query using sub-queries (closes #37423)
+
+
+
+2010-07-28  --  0.26.4
+    * fix re-annotation pb: some stinfo keys were not properly reinitialized
+      which may cause pb later (at sql generation time for instance)
+
+
+
+2010-06-21  --  0.26.3
+    * support for node from having in Select.remove
+
+    * enhanced Select.replace method
+
+    * rql st checker now checks function avaibility according to backend (if specified)
+
+
+
+2010-06-11  --  0.26.2
+    * totally remove 'IS' operator
+
+    * replace get_variable_variables by get_variable_indicies
+
+    * fix rule order so 'HAVING (X op Y)' is now parseable while 'HAVING (1+2) op Y' isn't anymore parseable
+
+    * fix simplification bug with ored uid relations
+
+
+
+2010-06-04  --  0.26.1
+    * normalize NOT() to NOT EXISTS() when it makes sense
+
+    * fix grammar bug in HAVING clause: should all arbitrary expression and fix to deal with IN() hack
+
+
+
+2010-04-20  --  0.26.0
+    * setuptools support
+
+    * variable and column alias stinfo optimization
+
+    * analyzer return key used in args to unambiguify solutions
+
+    * rewrite_shared_optional refactoring
+
+
+
 2010-03-16  --  0.25.0
     * depends on logilab-database
 
--- a/__init__.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/__init__.py	Wed May 11 09:02:33 2011 +0200
@@ -38,7 +38,7 @@
       - comparison of two queries
     """
     def __init__(self, schema, uid_func_mapping=None, special_relations=None,
-                 resolver_class=None):
+                 resolver_class=None, backend=None):
         # chech schema
         #for e_type in REQUIRED_TYPES:
         #    if not schema.has_entity(e_type):
@@ -49,7 +49,7 @@
         if uid_func_mapping:
             for key in uid_func_mapping:
                 special_relations[key] = 'uid'
-        self._checker = RQLSTChecker(schema)
+        self._checker = RQLSTChecker(schema, special_relations, backend)
         self._annotator = RQLSTAnnotator(schema, special_relations)
         self._analyser_lock = threading.Lock()
         if resolver_class is None:
@@ -76,6 +76,12 @@
         self._annotator.schema = schema
         self._analyser.set_schema(schema)
 
+    def get_backend(self):
+        return self._checker.backend
+    def set_backend(self, backend):
+        self._checker.backend = backend
+    backend = property(get_backend, set_backend)
+
     def parse(self, rqlstring, annotate=True):
         """Return a syntax tree created from a RQL string."""
         rqlst = parse(rqlstring, False)
@@ -97,8 +103,8 @@
         """
         self._analyser_lock.acquire()
         try:
-            self._analyser.visit(rqlst, uid_func_mapping, kwargs,
-                                 debug)
+            return self._analyser.visit(rqlst, uid_func_mapping, kwargs,
+                                        debug)
         finally:
             self._analyser_lock.release()
 
@@ -135,17 +141,13 @@
         rewritten = False
         for var in select.defined_vars.values():
             stinfo = var.stinfo
-            if stinfo['constnode'] and not stinfo['blocsimplification']:
-                #assert len(stinfo['uidrels']) == 1, var
-                uidrel = stinfo['uidrels'].pop()
+            if stinfo['constnode'] and not stinfo.get('blocsimplification'):
+                uidrel = stinfo['uidrel']
                 var = uidrel.children[0].variable
                 vconsts = []
                 rhs = uidrel.children[1].children[0]
-                #from rql.nodes import Constant
-                #assert isinstance(rhs, nodes.Constant), rhs
                 for vref in var.references():
                     rel = vref.relation()
-                    #assert vref.parent
                     if rel is None:
                         term = vref
                         while not term.parent is select:
@@ -168,19 +170,15 @@
                             select.groupby[select.groupby.index(vref)] = rhs
                             rhs.parent = select
                     elif rel is uidrel:
-                        # drop this relation
-                        rel.parent.remove(rel)
+                        uidrel.parent.remove(uidrel)
                     elif rel.is_types_restriction():
-                        stinfo['typerels'].remove(rel)
-                        rel.parent.remove(rel)
-                    elif rel in stinfo['uidrels']:
-                        # XXX check equivalence not necessary else we wouldn't be here right?
-                        stinfo['uidrels'].remove(rel)
+                        stinfo['typerel'] = None
                         rel.parent.remove(rel)
                     else:
                         rhs = copy_uid_node(select, rhs, vconsts)
                         vref.parent.replace(vref, rhs)
                 del select.defined_vars[var.name]
+                stinfo['uidrel'] = None
                 rewritten = True
                 if vconsts:
                     select.stinfo['rewritten'][var.name] = vconsts
--- a/__pkginfo__.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/__pkginfo__.py	Wed May 11 09:02:33 2011 +0200
@@ -16,13 +16,11 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with rql. If not, see <http://www.gnu.org/licenses/>.
-"""RQL packaging information.
-
-"""
+"""RQL packaging information."""
 __docformat__ = "restructuredtext en"
 
 modname = "rql"
-numversion = (0, 25, 0)
+numversion = (0, 28, 0)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL'
@@ -30,15 +28,13 @@
 author = "Logilab"
 author_email = "contact@logilab.fr"
 
-short_desc = "relationship query language (RQL) utilities"
+description = "relationship query language (RQL) utilities"
 long_desc = """A library providing the base utilities to handle RQL queries,
 such as a parser, a type inferencer.
 """
 web = "http://www.logilab.org/project/rql"
 ftp = "ftp://ftp.logilab.org/pub/rql"
 
-pyversions = ['2.4']
-
 
 import os, subprocess, sys
 from distutils.core import Extension
@@ -73,8 +69,8 @@
 else:
     ext_modules = [ Extension('rql_solve',
                               ['gecode_solver.cpp'],
-                              libraries=['GecodeInt-3-3-1-r-x86', 
-                                         'GecodeKernel-3-3-1-r-x86', 
+                              libraries=['GecodeInt-3-3-1-r-x86',
+                                         'GecodeKernel-3-3-1-r-x86',
                                          'GecodeSearch-3-3-1-r-x86',
                                          'GecodeSupport-3-3-1-r-x86',
                                          ],
@@ -82,3 +78,15 @@
                               #extra_link_args=['-static-libgcc'],
                               )
                     ]
+
+install_requires = [
+    'logilab-common >= 0.47.0',
+    'logilab-database',
+    'yapps == 2.1.1', # XXX to ensure we don't use the broken pypi version
+    'constraint', # fallback if the gecode compiled module is missing
+    ]
+
+# links to download yapps2 package that is not (yet) registered in pypi
+dependency_links = [
+    "http://ftp.logilab.org/pub/yapps/yapps2-2.1.1.zip#egg=yapps-2.1.1",
+    ]
--- a/_exceptions.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/_exceptions.py	Wed May 11 09:02:33 2011 +0200
@@ -15,9 +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/>.
-"""Exceptions used in the RQL package.
+"""Exceptions used in the RQL package."""
 
-"""
 __docformat__ = "restructuredtext en"
 
 class RQLException(Exception):
--- a/analyze.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/analyze.py	Wed May 11 09:02:33 2011 +0200
@@ -354,9 +354,10 @@
             assert cst.type
             if cst.type == 'Substitute':
                 eid = self.kwargs[cst.value]
+                self.deambiguifiers.add(cst.value)
             else:
                 eid = cst.value
-            cst.uidtype = self.uid_func(eid)
+            cst.uidtype = self.uid_func(cst.eval(self.kwargs))
             types.add(cst.uidtype)
         return types
 
@@ -376,8 +377,8 @@
                     alltypes.add(targettypes)
         else:
             alltypes = get_target_types()
-
-        constraints.var_has_types( var, [ str(t) for t in alltypes] )
+        domain = constraints.domains[var]
+        constraints.var_has_types( var, [str(t) for t in alltypes if t in domain] )
 
     def visit(self, node, uid_func_mapping=None, kwargs=None, debug=False):
         # FIXME: not thread safe
@@ -387,10 +388,12 @@
             self.uid_func_mapping = uid_func_mapping
             self.uid_func = uid_func_mapping.values()[0]
         self.kwargs = kwargs
+        self.deambiguifiers = set()
         self._visit(node)
         if uid_func_mapping is not None:
             self.uid_func_mapping = None
             self.uid_func = None
+        return self.deambiguifiers
 
     def visit_union(self, node):
         for select in node.children:
@@ -506,18 +509,26 @@
                         samevar = True
                     else:
                         rhsvars.append(v.name)
+            lhsdomain = constraints.domains[lhsvar]
             if rhsvars:
                 s2 = '=='.join(rhsvars)
+                # filter according to domain necessary for column aliases
+                rhsdomain = constraints.domains[rhsvars[0]]
                 res = []
                 for fromtype, totypes in rschema.associations():
-                    res.append( [ ( [lhsvar], [str(fromtype)]), (rhsvars, [ str(t) for t in totypes]) ] )
+                    if not fromtype in lhsdomain:
+                        continue
+                    ptypes = [str(t) for t in totypes if t in rhsdomain]
+                    res.append( [ ( [lhsvar], [str(fromtype)]), (rhsvars, ptypes) ] )
                 constraints.or_and( res )
             else:
-                constraints.var_has_types( lhsvar, [ str(subj) for subj in rschema.subjects()] )
+                ptypes = [str(subj) for subj in rschema.subjects()
+                          if subj in lhsdomain]
+                constraints.var_has_types( lhsvar, ptypes )
             if samevar:
                 res = []
                 for fromtype, totypes in rschema.associations():
-                    if not fromtype in totypes:
+                    if not (fromtype in totypes and fromtype in lhsdomain):
                         continue
                     res.append(str(fromtype))
                 constraints.var_has_types( lhsvar, res )
--- a/base.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/base.py	Wed May 11 09:02:33 2011 +0200
@@ -18,8 +18,8 @@
 """Base classes for RQL syntax tree nodes.
 
 Note: this module uses __slots__ to limit memory usage.
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 class BaseNode(object):
@@ -57,13 +57,6 @@
         """
         return self.parent.scope
 
-    @property
-    def sqlscope(self):
-        """Return the SQL scope node to which this node belong (eg Select,
-        Exists or Not node)
-        """
-        return self.parent.sqlscope
-
     def get_nodes(self, klass):
         """Return the list of nodes of a given class in the subtree.
 
@@ -147,9 +140,14 @@
         child.parent = self
 
     def remove(self, child):
-        """remove a child node"""
-        self.children.remove(child)
+        """Remove a child node. Return the removed node, its old parent and
+        index in the children list.
+        """
+        index = self.children.index(child)
+        del self.children[index]
+        parent = child.parent
         child.parent = None
+        return child, parent, index
 
     def insert(self, index, child):
         """insert a child node"""
@@ -162,7 +160,7 @@
         self.children.pop(i)
         self.children.insert(i, new_child)
         new_child.parent = self
-
+        return old_child, self, i
 
 class BinaryNode(Node):
     __slots__ = ()
@@ -176,8 +174,8 @@
 
     def remove(self, child):
         """Remove the child and replace this node with the other child."""
-        self.children.remove(child)
-        self.parent.replace(self, self.children[0])
+        index = self.children.index(child)
+        return self.parent.replace(self, self.children[not index])
 
     def get_parts(self):
         """Return the left hand side and the right hand side of this node."""
--- a/debian.hardy/control	Wed Apr 28 11:46:49 2010 +0200
+++ b/debian.hardy/control	Wed May 11 09:02:33 2011 +0200
@@ -12,7 +12,8 @@
 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
+Depends: ${python:Depends}, ${misc:Depends}, ${shlibs:Depends}, python-logilab-common (>= 0.35.3-1), yapps2-runtime, python-logilab-database
+Conflicts: cubicweb-common (< 3.8.0)
 Provides: ${python:Provides}
 Description: relationship query language (RQL) utilities
  A library providing the base utilities to handle RQL queries,
--- a/debian.lenny/control	Wed Apr 28 11:46:49 2010 +0200
+++ b/debian.lenny/control	Wed May 11 09:02:33 2011 +0200
@@ -2,17 +2,17 @@
 Section: python
 Priority: optional
 Maintainer: Logilab Packaging Team <contact@logilab.fr>
-Uploaders: Sylvain Thenault <sylvain.thenault@logilab.fr>, Ludovic Aubry <ludovic.aubry@logilab.fr>, Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 5.0.37.1), python-all-dev (>=2.4), python-all (>=2.4), libgecode12-dev, python-sphinx, g++
-Build-Depends-Indep: python-support
+Uploaders: Sylvain Thenault <sylvain.thenault@logilab.fr>, Nicolas Chauvat <nicolas.chauvat@logilab.fr>
+Build-Depends: debhelper (>= 5.0.37.1), python-support, python-all-dev (>=2.4), python-all (>=2.4), libgecode12-dev, python-sphinx, g++ (>= 4)
 XS-Python-Version: >= 2.4
-Standards-Version: 3.8.0
+Standards-Version: 3.9.1
 Homepage: http://www.logilab.org/project/rql
 
 Package: python-rql
 Architecture: any
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, ${misc:Depends}, ${shlibs:Depends}, python-logilab-common (>= 0.35.3-1), python-constraint (>= 0.4.0-1), yapps2-runtime
+Depends: ${python:Depends}, ${misc:Depends}, ${shlibs:Depends}, python-logilab-common (>= 0.35.3-1), yapps2-runtime, python-logilab-database
+Conflicts: cubicweb-common (<= 3.8.3)
 Provides: ${python:Provides}
 Description: relationship query language (RQL) utilities
  A library providing the base utilities to handle RQL queries,
--- a/debian/changelog	Wed Apr 28 11:46:49 2010 +0200
+++ b/debian/changelog	Wed May 11 09:02:33 2011 +0200
@@ -1,3 +1,65 @@
+rql (0.28.0-2) UNRELEASED; urgency=low
+
+  * debian/control:
+    - remove Ludovic Aubry from Uploaders
+  * lintian fixes
+
+ -- 
+
+rql (0.28.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 12 Jan 2011 09:21:26 +0100
+
+rql (0.27.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 13 Oct 2010 07:55:35 +0200
+
+rql (0.26.6-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 10 Sep 2010 11:09:22 +0200
+
+rql (0.26.5-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 02 Aug 2010 14:22:00 +0200
+
+rql (0.26.4-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Wed, 28 Jul 2010 10:29:47 +0200
+
+rql (0.26.3-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 21 Jun 2010 09:34:41 +0200
+
+rql (0.26.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 11 Jun 2010 10:04:46 +0200
+
+rql (0.26.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 07 Jun 2010 10:12:50 +0200
+
+rql (0.26.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Tue, 20 Apr 2010 11:10:27 +0200
+
 rql (0.25.0-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Wed Apr 28 11:46:49 2010 +0200
+++ b/debian/control	Wed May 11 09:02:33 2011 +0200
@@ -2,17 +2,17 @@
 Section: python
 Priority: optional
 Maintainer: Logilab Packaging Team <contact@logilab.fr>
-Uploaders: Sylvain Thenault <sylvain.thenault@logilab.fr>, Ludovic Aubry <ludovic.aubry@logilab.fr>, Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 5.0.37.1), python-all-dev (>=2.4), python-all (>=2.4), libgecode-dev, python-sphinx, g++
-Build-Depends-Indep: python-support
-XS-Python-Version: >= 2.4
-Standards-Version: 3.8.0
+Uploaders: Sylvain Thenault <sylvain.thenault@logilab.fr>, Nicolas Chauvat <nicolas.chauvat@logilab.fr>
+Build-Depends: debhelper (>= 5.0.37.1), python-support, python-all-dev (>=2.5), python-all (>=2.5), libgecode-dev, python-sphinx, g++ (>= 4)
+XS-Python-Version: >= 2.5
+Standards-Version: 3.9.1
 Homepage: http://www.logilab.org/project/rql
 
 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
+Conflicts: cubicweb-common (<= 3.8.3)
 Provides: ${python:Provides}
 Description: relationship query language (RQL) utilities
  A library providing the base utilities to handle RQL queries,
--- a/debian/rules	Wed Apr 28 11:46:49 2010 +0200
+++ b/debian/rules	Wed May 11 09:02:33 2011 +0200
@@ -12,13 +12,13 @@
 #export DH_VERBOSE=1
 
 build: build-stamp
-build-stamp: 
+build-stamp:
 	dh_testdir
 	(for PYTHON in `pyversions -r`; do \
-	    $${PYTHON} setup.py build ; done )
+	    NO_SETUPTOOLS=1 $${PYTHON} setup.py build ; done )
 	${MAKE} -C doc html || true
 	touch build-stamp
-clean: 
+clean:
 	dh_testdir
 	dh_testroot
 	rm -f build-stamp configure-stamp
@@ -33,7 +33,7 @@
 	dh_clean -k
 	dh_installdirs
 	(for PYTHON in `pyversions -r`; do \
-		$${PYTHON} setup.py install --no-compile --prefix=debian/python-rql/usr/ ; \
+		NO_SETUPTOOLS=1 $${PYTHON} setup.py install --no-compile --prefix=debian/python-rql/usr/ ; \
 	done)
 	# remove test directory (installed in in the doc directory)
 	rm -rf debian/python-rql/usr/lib/python*/site-packages/rql/test
@@ -45,21 +45,23 @@
 
 # Build architecture-dependent files here.
 binary-arch: build install
-	dh_testdir 
-	dh_testroot 
+	dh_testdir
+	dh_testroot
 	dh_install -a
-	dh_pysupport -a 
+	dh_pysupport -a
 	gzip -9 -c ChangeLog > changelog.gz
 	dh_installchangelogs -a
 	dh_installexamples -a
 	dh_installdocs -a README TODO changelog.gz
 	dh_installman -a
 	dh_link -a
-	dh_compress -a -X.py -X.ini -X.xml -Xtest
+	# .js, .txt and .json are coming from sphinx build
+	dh_compress -a -X.py -X.ini -X.xml -Xtest/ -X.js -X.txt -X.json
 	dh_fixperms -a
+	dh_strip
 	dh_shlibdeps -a
 	dh_installdeb -a
-	dh_gencontrol -a 
+	dh_gencontrol -a
 	dh_md5sums -a
 	dh_builddeb -a
 
--- a/editextensions.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/editextensions.py	Wed May 11 09:02:33 2011 +0200
@@ -15,9 +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/>.
-"""RQL functions for manipulating syntax trees.
+"""RQL functions for manipulating syntax trees."""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from rql.nodes import Constant, Variable, VariableRef, Relation, make_relation
--- a/nodes.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/nodes.py	Wed May 11 09:02:33 2011 +0200
@@ -19,8 +19,8 @@
 
 This module defines all the nodes we can find in a RQL Syntax tree, except
 root nodes, defined in the `stmts` module.
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from itertools import chain
@@ -105,6 +105,19 @@
     relation.append(cmpop)
     return relation
 
+def make_constant_restriction(var, rtype, value, ctype, operator='='):
+    if ctype is None:
+        ctype = etype_from_pyobj(value)
+    if isinstance(value, (set, frozenset, tuple, list, dict)):
+        if len(value) > 1:
+            rel = make_relation(var, rtype, ('IN',), Function, operator)
+            infunc = rel.children[1].children[0]
+            for atype in sorted(value):
+                infunc.append(Constant(atype, ctype))
+            return rel
+        value = iter(value).next()
+    return make_relation(var, rtype, (value, ctype), Constant, operator)
+
 
 class EditableMixIn(object):
     """mixin class to add edition functionalities to some nodes, eg root nodes
@@ -129,14 +142,17 @@
         handling
         """
         # unregister variable references in the removed subtree
+        parent = node.parent
+        stmt = parent.stmt
         for varref in node.iget_nodes(VariableRef):
             varref.unregister_reference()
             if undefine and not varref.variable.stinfo['references']:
-                node.stmt.undefine_variable(varref.variable)
+                stmt.undefine_variable(varref.variable)
+        # remove return actually removed node and its parent
+        node, parent, index = parent.remove(node)
         if self.should_register_op:
             from rql.undo import RemoveNodeOperation
-            self.undo_manager.add_operation(RemoveNodeOperation(node))
-        node.parent.remove(node)
+            self.undo_manager.add_operation(RemoveNodeOperation(node, parent, stmt, index))
 
     def add_restriction(self, relation):
         """add a restriction relation"""
@@ -160,18 +176,8 @@
 
         variable rtype = value
         """
-        if ctype is None:
-            ctype = etype_from_pyobj(value)
-        if isinstance(value, (set, frozenset, tuple, list, dict)):
-            if len(value) > 1:
-                rel = make_relation(var, rtype, ('IN',), Function, operator=operator)
-                infunc = rel.children[1].children[0]
-                for atype in sorted(value):
-                    infunc.append(Constant(atype, ctype))
-                return self.add_restriction(rel)
-            value = iter(value).next()
-        return self.add_restriction(make_relation(var, rtype, (value, ctype),
-                                                  Constant, operator))
+        restr = make_constant_restriction(var, rtype, value, ctype, operator)
+        return self.add_restriction(restr)
 
     def add_relation(self, lhsvar, rtype, rhsvar):
         """builds a restriction node to express '<var> eid <eid>'"""
@@ -259,6 +265,10 @@
 class Not(Node):
     """a logical NOT node (unary)"""
     __slots__ = ()
+    def __init__(self, expr=None):
+        Node.__init__(self)
+        if expr is not None:
+            self.append(expr)
 
     def as_string(self, encoding=None, kwargs=None):
         if isinstance(self.children[0], (Exists, Relation)):
@@ -268,10 +278,6 @@
     def __repr__(self, encoding=None, kwargs=None):
         return 'NOT (%s)' % repr(self.children[0])
 
-    @property
-    def sqlscope(self):
-        return self
-
     def ored(self, traverse_scope=False, _fromnode=None):
         # XXX consider traverse_scope ?
         return self.parent.ored(traverse_scope, _fromnode or self)
@@ -279,6 +285,9 @@
     def neged(self, traverse_scope=False, _fromnode=None, strict=False):
         return self
 
+    def remove(self, child):
+        return self.parent.remove(self)
+
 # def parent_scope_property(attr):
 #     def _get_parent_attr(self, attr=attr):
 #         return getattr(self.parent.scope, attr)
@@ -334,11 +343,14 @@
         assert oldnode is self.query
         self.query = newnode
         newnode.parent = self
+        return oldnode, self, None
+
+    def remove(self, child):
+        return self.parent.remove(self)
 
     @property
     def scope(self):
         return self
-    sqlscope = scope
 
     def ored(self, traverse_scope=False, _fromnode=None):
         if not traverse_scope:
@@ -428,7 +440,12 @@
             return False
         rhs = self.children[1]
         if isinstance(rhs, Comparison):
-            rhs = rhs.children[0]
+            try:
+                rhs = rhs.children[0]
+            except:
+                print 'opppp', rhs
+                print rhs.root
+                raise
         # else: relation used in SET OR DELETE selection
         return ((isinstance(rhs, Constant) and rhs.type == 'etype')
                 or (isinstance(rhs, Function) and rhs.name == 'IN'))
@@ -467,6 +484,8 @@
         self.optional= value
 
 
+OPERATORS = frozenset(('=', '!=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE'))
+
 class Comparison(HSMixin, Node):
     """handle comparisons:
 
@@ -478,10 +497,7 @@
         Node.__init__(self)
         if operator == '~=':
             operator = 'ILIKE'
-        elif operator == '=' and isinstance(value, Constant) and \
-                 value.type is None:
-            operator = 'IS'
-        assert operator in ('=', '<', '<=', '>=', '>', 'ILIKE', 'LIKE', 'IS'), operator
+        assert operator in OPERATORS, operator
         self.operator = operator.encode()
         if value is not None:
             self.append(value)
@@ -503,7 +519,7 @@
             return '%s %s %s' % (self.children[0].as_string(encoding, kwargs),
                                  self.operator.encode(),
                                  self.children[1].as_string(encoding, kwargs))
-        if self.operator in ('=', 'IS'):
+        if self.operator == '=':
             return self.children[0].as_string(encoding, kwargs)
         return '%s %s' % (self.operator.encode(),
                           self.children[0].as_string(encoding, kwargs))
@@ -850,33 +866,35 @@
             # relations where this variable is used on the lhs/rhs
             'relations': set(),
             'rhsrelations': set(),
-            'optrelations': set(),
-            # empty if this variable may be simplified (eg not used in optional
-            # relations and no final relations where this variable is used on
-            # the lhs)
-            'blocsimplification': set(),
-            # type relations (e.g. "is") where this variable is used on the lhs
-            'typerels': set(),
-            # uid relations (e.g. "eid") where this variable is used on the lhs
-            'uidrels': set(),
             # selection indexes if any
             'selected': set(),
-            # if this variable is an attribute variable (ie final entity),
-            # link to the (prefered) attribute owner variable
+            # type restriction (e.g. "is" / "is_instance_of") where this
+            # variable is used on the lhs
+            'typerel': None,
+            # uid relations (e.g. "eid") where this variable is used on the lhs
+            'uidrel': None,
+            # if this variable is an attribute variable (ie final entity), link
+            # to the (prefered) attribute owner variable
             'attrvar': None,
-            # set of couple (lhs variable name, relation name) where this
-            # attribute variable is used
-            'attrvars': set(),
             # constant node linked to an uid variable if any
             'constnode': None,
             })
+        # remove optional st infos
+        for key in ('optrelations', 'blocsimplification', 'ftirels'):
+            self.stinfo.pop(key, None)
+
+    def add_optional_relation(self, relation):
+        try:
+            self.stinfo['optrelations'].add(relation)
+        except KeyError:
+            self.stinfo['optrelations'] = set((relation,))
 
     def get_type(self, solution=None, kwargs=None):
         """return entity type of this object, 'Any' if not found"""
         if solution:
             return solution[self.name]
-        for rel in self.stinfo['typerels']:
-            return str(rel.children[1].children[0].value)
+        if self.stinfo['typerel']:
+            return str(self.stinfo['typerel'].children[1].children[0].value)
         schema = self.schema
         if schema is not None:
             for rel in self.stinfo['rhsrelations']:
@@ -913,13 +931,13 @@
             rtype = rel.r_type
             lhs, rhs = rel.get_variable_parts()
             # use getattr, may not be a variable ref (rewritten, constant...)
-            lhsvar = getattr(lhs, 'variable', None)
             rhsvar = getattr(rhs, 'variable', None)
             if mainindex is not None:
                 # relation to the main variable, stop searching
-                if mainindex in lhsvar.stinfo['selected']:
+                lhsvar = getattr(lhs, 'variable', None)
+                if lhsvar is not None and mainindex in lhsvar.stinfo['selected']:
                     return tr(rtype)
-                if mainindex in rhsvar.stinfo['selected']:
+                if rhsvar is not None and mainindex in rhsvar.stinfo['selected']:
                     if schema is not None and rschema.symmetric:
                         return tr(rtype)
                     return tr(rtype + '_object')
@@ -1001,8 +1019,6 @@
     def get_scope(self):
         return self.query
     scope = property(get_scope, set_scope)
-    sqlscope = scope
-    set_sqlscope = set_scope
 
 
 class Variable(Referenceable):
@@ -1030,7 +1046,6 @@
     def prepare_annotation(self):
         super(Variable, self).prepare_annotation()
         self.stinfo['scope'] = None
-        self.stinfo['sqlscope'] = None
 
     def _set_scope(self, key, scopenode):
         if scopenode is self.stmt or self.stinfo[key] is None:
@@ -1044,12 +1059,6 @@
         return self.stinfo['scope']
     scope = property(get_scope, set_scope)
 
-    def set_sqlscope(self, sqlscopenode):
-        self._set_scope('sqlscope', sqlscopenode)
-    def get_sqlscope(self):
-        return self.stinfo['sqlscope']
-    sqlscope = property(get_sqlscope, set_sqlscope)
-
     def valuable_references(self):
         """return the number of "valuable" references :
         references is in selection or in a non type (is) relations
--- a/parser.g	Wed Apr 28 11:46:49 2010 +0200
+++ b/parser.g	Wed May 11 09:02:33 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|IS'
+    token CMP_OP:      r'(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE'
     token ADD_OP:      r'\+|-'
     token MUL_OP:      r'\*|/'
     token FUNCTION:    r'[A-Za-z_]+\s*(?=\()'
@@ -176,10 +176,7 @@
 rule groupby<<S>>: GROUPBY variables<<S>> {{ S.set_groupby(variables); return True }}
                  |
 
-rule having<<S>>: HAVING               {{ nodes = [] }}
-                   expr_cmp<<S>>       {{ nodes.append(expr_cmp) }}
-                   ( ',' expr_cmp<<S>> {{ nodes.append(expr_cmp) }}
-                   )*                  {{ S.set_having(nodes) }}
+rule having<<S>>: HAVING logical_expr<<S>> {{ S.set_having([logical_expr]) }}
                 |
 
 rule orderby<<S>>: ORDERBY              {{ nodes = [] }}
@@ -198,11 +195,6 @@
                     BEING r"\(" union<<Union()>> r"\)" {{ node.set_query(union); return node }}
 
 
-rule expr_cmp<<S>>: expr_add<<S>>  {{ c1 = expr_add }}
-                    CMP_OP         {{ cmp = Comparison(CMP_OP.upper(), c1) }}
-                    expr_add<<S>>  {{ cmp.append(expr_add); return cmp }}
-
-
 rule sort_term<<S>>: expr_add<<S>> sort_meth {{ return SortTerm(expr_add, sort_meth) }}
 
 
@@ -241,7 +233,7 @@
                     (  AND rels_not<<S>> {{ node = And(node, rels_not) }}
                     )*                   {{ return node }}
 
-rule rels_not<<S>>: NOT rel<<S>> {{ node = Not(); node.append(rel); return node }}
+rule rels_not<<S>>: NOT rel<<S>> {{ return Not(rel) }}
                   | rel<<S>>     {{ return rel }}
 
 rule rel<<S>>: rel_base<<S>>                {{ return rel_base }}
@@ -259,6 +251,39 @@
 rule opt_right<<S>>: QMARK  {{ return 'right' }}
                    |
 
+#// restriction expressions ####################################################
+
+rule logical_expr<<S>>: exprs_or<<S>>       {{ node = exprs_or }}
+                        ( ',' exprs_or<<S>> {{ node = And(node, exprs_or) }}
+                        )*                  {{ return node }}
+
+rule exprs_or<<S>>: exprs_and<<S>>      {{ node = exprs_and }}
+                    ( OR exprs_and<<S>> {{ node = Or(node, exprs_and) }}
+                    )*                  {{ return node }}
+
+rule exprs_and<<S>>: exprs_not<<S>>        {{ 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) }}
+                   | balanced_expr<<S>>     {{ return balanced_expr }}
+
+#// XXX ambiguity, expr_add may also have '(' as first token. Hence
+#// put "(" logical_expr<<S>> ")" rule first. We can then parse:
+#//
+#//   Any T2 WHERE T1 relation T2 HAVING (1 < COUNT(T1));
+#//
+#// but not
+#//
+#//   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 }}
+
+# // 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) }}
+
+
 #// common statements ###########################################################
 
 rule variables<<S>>:                   {{ vars = [] }}
@@ -307,6 +332,13 @@
                    )?
                 r"\)"                 {{ return F }}
 
+rule in_expr<<S>>: 'IN' r"\("        {{ F = Function('IN') }}
+                   ( expr_add<<S>> (     {{ F.append(expr_add) }}
+                      ',' expr_add<<S>>
+                     )*                  {{ F.append(expr_add) }}
+                   )?
+                r"\)"                 {{ return F }}
+
 
 rule var<<S>>: VARIABLE {{ return VariableRef(S.get_variable(VARIABLE)) }}
 
--- a/parser.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/parser.py	Wed May 11 09:02:33 2011 +0200
@@ -77,6 +77,7 @@
 
 class HerculeScanner(runtime.Scanner):
     patterns = [
+        ("'IN'", re.compile('IN')),
         ("','", re.compile(',')),
         ('r"\\)"', re.compile('\\)')),
         ('r"\\("', re.compile('\\(')),
@@ -108,7 +109,7 @@
         ('FALSE', re.compile('(?i)FALSE')),
         ('NULL', re.compile('(?i)NULL')),
         ('EXISTS', re.compile('(?i)EXISTS')),
-        ('CMP_OP', re.compile('(?i)<=|<|>=|>|~=|=|LIKE|ILIKE|IS')),
+        ('CMP_OP', re.compile('(?i)<=|<|>=|>|!=|=|~=|LIKE|ILIKE')),
         ('ADD_OP', re.compile('\\+|-')),
         ('MUL_OP', re.compile('\\*|/')),
         ('FUNCTION', re.compile('[A-Za-z_]+\\s*(?=\\()')),
@@ -271,14 +272,8 @@
         _token = self._peek('HAVING', 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', "';'", 'r"\\)"', context=_context)
         if _token == 'HAVING':
             HAVING = self._scan('HAVING', context=_context)
-            nodes = []
-            expr_cmp = self.expr_cmp(S, _context)
-            nodes.append(expr_cmp)
-            while self._peek("','", 'WITH', 'GROUPBY', 'ORDERBY', 'LIMIT', 'OFFSET', 'WHERE', 'HAVING', "';'", 'r"\\)"', context=_context) == "','":
-                self._scan("','", context=_context)
-                expr_cmp = self.expr_cmp(S, _context)
-                nodes.append(expr_cmp)
-            S.set_having(nodes)
+            logical_expr = self.logical_expr(S, _context)
+            S.set_having([logical_expr])
         elif 1:
             pass
         else:
@@ -330,15 +325,6 @@
         self._scan('r"\\)"', context=_context)
         node.set_query(union); return node
 
-    def expr_cmp(self, S, _parent=None):
-        _context = self.Context(_parent, self._scanner, 'expr_cmp', [S])
-        expr_add = self.expr_add(S, _context)
-        c1 = expr_add
-        CMP_OP = self._scan('CMP_OP', context=_context)
-        cmp = Comparison(CMP_OP.upper(), c1)
-        expr_add = self.expr_add(S, _context)
-        cmp.append(expr_add); return cmp
-
     def sort_term(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'sort_term', [S])
         expr_add = self.expr_add(S, _context)
@@ -431,7 +417,7 @@
         if _token == 'NOT':
             NOT = self._scan('NOT', context=_context)
             rel = self.rel(S, _context)
-            node = Not(); node.append(rel); return node
+            return Not(rel)
         else: # in ['r"\\("', 'EXISTS', 'VARIABLE']
             rel = self.rel(S, _context)
             return rel
@@ -489,6 +475,73 @@
         else:
             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) == "','":
+            self._scan("','", context=_context)
+            exprs_or = self.exprs_or(S, _context)
+            node = And(node, exprs_or)
+        return node
+
+    def exprs_or(self, S, _parent=None):
+        _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':
+            OR = self._scan('OR', context=_context)
+            exprs_and = self.exprs_and(S, _context)
+            node = Or(node, exprs_and)
+        return node
+
+    def exprs_and(self, S, _parent=None):
+        _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':
+            AND = self._scan('AND', context=_context)
+            exprs_not = self.exprs_not(S, _context)
+            node = And(node, exprs_not)
+        return node
+
+    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)
+        if _token == 'NOT':
+            NOT = self._scan('NOT', context=_context)
+            balanced_expr = self.balanced_expr(S, _context)
+            return Not(balanced_expr)
+        else:
+            balanced_expr = self.balanced_expr(S, _context)
+            return balanced_expr
+
+    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)
+        if _token == 'r"\\("':
+            self._scan('r"\\("', context=_context)
+            logical_expr = self.logical_expr(S, _context)
+            self._scan('r"\\)"', context=_context)
+            return logical_expr
+        elif 1:
+            expr_add = self.expr_add(S, _context)
+            expr_op = self.expr_op(S, _context)
+            expr_op.insert(0, expr_add); return expr_op
+        else:
+            raise runtime.SyntaxError(_token[0], 'Could not match balanced_expr')
+
+    def expr_op(self, S, _parent=None):
+        _context = self.Context(_parent, self._scanner, 'expr_op', [S])
+        _token = self._peek('CMP_OP', "'IN'", context=_context)
+        if _token == 'CMP_OP':
+            CMP_OP = self._scan('CMP_OP', context=_context)
+            expr_add = self.expr_add(S, _context)
+            return Comparison(CMP_OP.upper(), expr_add)
+        else: # == "'IN'"
+            in_expr = self.in_expr(S, _context)
+            return Comparison('=', in_expr)
+
     def variables(self, S, _parent=None):
         _context = self.Context(_parent, self._scanner, 'variables', [S])
         vars = []
@@ -504,7 +557,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"\\)"', 'CMP_OP', 'SORT_DESC', 'SORT_ASC', 'AND', 'OR', context=_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) == "','":
             R.add_main_variable(E_TYPE, var)
             self._scan("','", context=_context)
             E_TYPE = self._scan('E_TYPE', context=_context)
@@ -543,7 +596,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"\\)"', "','", 'CMP_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'QMARK', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'ADD_OP':
+        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':
             ADD_OP = self._scan('ADD_OP', context=_context)
             expr_mul = self.expr_mul(S, _context)
             node = MathExpression( ADD_OP, node, expr_mul )
@@ -553,7 +606,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"\\)"', "','", 'CMP_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'QMARK', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == 'MUL_OP':
+        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':
             MUL_OP = self._scan('MUL_OP', context=_context)
             expr_base = self.expr_base(S, _context)
             node = MathExpression( MUL_OP, node, expr_base)
@@ -587,7 +640,22 @@
         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"\\)"', 'CMP_OP', 'SORT_DESC', 'SORT_ASC', 'GROUPBY', 'QMARK', 'ORDERBY', 'WHERE', 'LIMIT', 'OFFSET', 'HAVING', 'WITH', "';'", 'AND', 'OR', context=_context) == "','":
+            while self._peek("','", 'r"\\)"', 'SORT_DESC', 'SORT_ASC', 'CMP_OP', "'IN'", 'GROUPBY', 'QMARK', '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)
+            F.append(expr_add)
+        self._scan('r"\\)"', context=_context)
+        return F
+
+    def in_expr(self, S, _parent=None):
+        _context = self.Context(_parent, self._scanner, 'in_expr', [S])
+        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"\\)"':
+            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) == "','":
                 F.append(expr_add)
                 self._scan("','", context=_context)
                 expr_add = self.expr_add(S, _context)
--- a/setup.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/setup.py	Wed May 11 09:02:33 2011 +0200
@@ -18,47 +18,45 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with rql. If not, see <http://www.gnu.org/licenses/>.
 """Generic Setup script, takes package info from __pkginfo__.py file.
-
 """
 __docformat__ = "restructuredtext en"
 
 import os
 import sys
 import shutil
-from distutils.core import setup
 from os.path import isdir, exists, join, walk
 
+try:
+    if os.environ.get('NO_SETUPTOOLS'):
+        raise ImportError()
+    from setuptools import setup
+    from setuptools.command import install_lib, build_ext
+    USE_SETUPTOOLS = 1
+except ImportError:
+    from distutils.core import setup
+    from distutils.command import install_lib, build_ext
+    USE_SETUPTOOLS = 0
+
+
+sys.modules.pop('__pkginfo__', None)
 # import required features
-from __pkginfo__ import modname, version, license, short_desc, long_desc, \
+from __pkginfo__ import modname, version, license, description, long_desc, \
      web, author, author_email
 # import optional features
-try:
-    from __pkginfo__ import distname
-except ImportError:
-    distname = modname
-try:
-    from __pkginfo__ import scripts
-except ImportError:
-    scripts = []
-try:
-    from __pkginfo__ import data_files
-except ImportError:
-    data_files = None
-try:
-    from __pkginfo__ import subpackage_of
-except ImportError:
-    subpackage_of = None
-try:
-    from __pkginfo__ import include_dirs
-except ImportError:
-    include_dirs = []
-try:
-    from __pkginfo__ import ext_modules
-except ImportError:
-    ext_modules = None
+import __pkginfo__
+distname = getattr(__pkginfo__, 'distname', modname)
+scripts = getattr(__pkginfo__, 'scripts', [])
+data_files = getattr(__pkginfo__, 'data_files', None)
+subpackage_of = getattr(__pkginfo__, 'subpackage_of', None)
+include_dirs = getattr(__pkginfo__, 'include_dirs', [])
+ext_modules = getattr(__pkginfo__, 'ext_modules', None)
+install_requires = getattr(__pkginfo__, 'install_requires', None)
+dependency_links = getattr(__pkginfo__, 'dependency_links', [])
 
-BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog')
-IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc')
+STD_BLACKLIST = ('CVS', '.svn', '.hg', 'debian', 'dist', 'build')
+
+IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc', '~')
+
 
 
 def ensure_scripts(linux_scripts):
@@ -91,8 +89,9 @@
     return result
 
 def export(from_dir, to_dir,
-           blacklist=BASE_BLACKLIST,
-           ignore_ext=IGNORED_EXTENSIONS):
+           blacklist=STD_BLACKLIST,
+           ignore_ext=IGNORED_EXTENSIONS,
+           verbose=True):
     """make a mirror of from_dir in to_dir, omitting directories and files
     listed in the black list
     """
@@ -109,9 +108,10 @@
                 continue
             if filename[-1] == '~':
                 continue
-            src = '%s/%s' % (directory, filename)
+            src = join(directory, filename)
             dest = to_dir + src[len(from_dir):]
-            print >> sys.stderr, src, '->', dest
+            if verbose:
+                print >> sys.stderr, src, '->', dest
             if os.path.isdir(src):
                 if not exists(dest):
                     os.mkdir(dest)
@@ -129,43 +129,28 @@
     walk(from_dir, make_mirror, None)
 
 
-EMPTY_FILE = '"""generated file, don\'t modify or your data will be lost"""\n'
+EMPTY_FILE = '''"""generated file, don\'t modify or your data will be lost"""
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    pass
+'''
 
-def install(**kwargs):
-    """setup entry point"""
-    if subpackage_of:
-        package = subpackage_of + '.' + modname
-        kwargs['package_dir'] = {package : '.'}
-        packages = [package] + get_packages(os.getcwd(), package)
-    else:
-        kwargs['package_dir'] = {modname : '.'}
-        packages = [modname] + get_packages(os.getcwd(), modname)
-    kwargs['packages'] = packages
-    dist = setup(name = distname,
-                 version = version,
-                 license =license,
-                 description = short_desc,
-                 long_description = long_desc,
-                 author = author,
-                 author_email = author_email,
-                 url = web,
-                 scripts = ensure_scripts(scripts),
-                 data_files=data_files,
-                 ext_modules=ext_modules,
-                 **kwargs
-                 )
-
-    if dist.have_run.get('install_lib'):
-        _install = dist.get_command_obj('install_lib')
+class MyInstallLib(install_lib.install_lib):
+    """extend install_lib command to handle  package __init__.py and
+    include_dirs variable if necessary
+    """
+    def run(self):
+        """overridden from install_lib class"""
+        install_lib.install_lib.run(self)
+        # create Products.__init__.py if needed
         if subpackage_of:
-            # create Products.__init__.py if needed
-            product_init = join(_install.install_dir, subpackage_of,
-                                '__init__.py')
+            product_init = join(self.install_dir, subpackage_of, '__init__.py')
             if not exists(product_init):
+                self.announce('creating %s' % product_init)
                 stream = open(product_init, 'w')
                 stream.write(EMPTY_FILE)
                 stream.close()
-
         # manually install included directories if any
         if include_dirs:
             if subpackage_of:
@@ -173,9 +158,66 @@
             else:
                 base = modname
             for directory in include_dirs:
-                dest = join(_install.install_dir, base, directory)
-                export(directory, dest)
-    return dist
+                dest = join(self.install_dir, base, directory)
+                export(directory, dest, verbose=False)
+
+class MyBuildExt(build_ext.build_ext):
+    """Extend build_ext command to pass through compilation error.
+    In fact, if gecode extension fail, rql will use logilab.constraint
+    """
+    def run(self):
+        from distutils.errors import CompileError
+        try:
+            build_ext.build_ext.run(self)
+        except CompileError:
+            import traceback
+            traceback.print_exc()
+            sys.stderr.write('================================\n'
+                             'The compilation of the gecode C extension failed. '
+                             'rql will use logilab.constraint which is a pure '
+                             'python implementation. '
+                             'Please note that the C extension run faster. '
+                             'So, install a compiler then install rql again with'
+                             ' the "force" option for better performance.\n'
+                             '================================\n')
+            pass
+
+def install(**kwargs):
+    """setup entry point"""
+    if USE_SETUPTOOLS:
+        if '--force-manifest' in sys.argv:
+            sys.argv.remove('--force-manifest')
+    # install-layout option was introduced in 2.5.3-1~exp1
+    elif sys.version_info < (2, 5, 4) and '--install-layout=deb' in sys.argv:
+        sys.argv.remove('--install-layout=deb')
+    if subpackage_of:
+        package = subpackage_of + '.' + modname
+        kwargs['package_dir'] = {package : '.'}
+        packages = [package] + get_packages(os.getcwd(), package)
+        if USE_SETUPTOOLS:
+            kwargs['namespace_packages'] = [subpackage_of]
+    else:
+        kwargs['package_dir'] = {modname : '.'}
+        packages = [modname] + get_packages(os.getcwd(), modname)
+    if USE_SETUPTOOLS and install_requires:
+        kwargs['install_requires'] = install_requires
+        kwargs['dependency_links'] = dependency_links
+    kwargs['packages'] = packages
+    return setup(name = distname,
+                 version = version,
+                 license = license,
+                 description = description,
+                 long_description = long_desc,
+                 author = author,
+                 author_email = author_email,
+                 url = web,
+                 scripts = ensure_scripts(scripts),
+                 data_files = data_files,
+                 ext_modules = ext_modules,
+                 cmdclass = {'install_lib': MyInstallLib,
+                             'build_ext':MyBuildExt},
+                 **kwargs
+                 )
 
 if __name__ == '__main__' :
     install()
--- a/stcheck.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/stcheck.py	Wed May 11 09:02:33 2011 +0200
@@ -15,9 +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/>.
-"""RQL Syntax tree annotator.
+"""RQL Syntax tree annotator"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from itertools import chain
@@ -27,8 +26,8 @@
 
 from rql._exceptions import BadRQLQuery
 from rql.utils import function_description
-from rql.nodes import (VariableRef, Constant, Not, Exists, Function,
-                       Variable, variable_refs)
+from rql.nodes import (Relation, VariableRef, Constant, Not, Exists, Function,
+                       And, Variable, variable_refs, make_relation)
 from rql.stmts import Union
 
 
@@ -38,12 +37,37 @@
     except KeyError:
         return subvarname + str(id(select))
 
+def bloc_simplification(variable, term):
+    try:
+        variable.stinfo['blocsimplification'].add(term)
+    except KeyError:
+        variable.stinfo['blocsimplification'] = set((term,))
+
 
 class GoTo(Exception):
     """Exception used to control the visit of the tree."""
     def __init__(self, node):
         self.node = node
 
+VAR_SELECTED = 1
+VAR_HAS_TYPE_REL = 2
+VAR_HAS_UID_REL = 4
+VAR_HAS_REL = 8
+
+class STCheckState(object):
+    def __init__(self):
+        self.errors = []
+        self.under_not = []
+        self.var_info = {}
+
+    def error(self, msg):
+        self.errors.append(msg)
+
+    def add_var_info(self, var, vi):
+        try:
+            self.var_info[var] |= vi
+        except KeyError:
+            self.var_info[var] = vi
 
 class RQLSTChecker(object):
     """Check a RQL syntax tree for errors not detected on parsing.
@@ -56,37 +80,39 @@
     errors due to a bad rql input
     """
 
-    def __init__(self, schema):
+    def __init__(self, schema, special_relations=None, backend=None):
         self.schema = schema
+        self.special_relations = special_relations or {}
+        self.backend = backend
 
     def check(self, node):
-        errors = []
-        self._visit(node, errors)
-        if errors:
-            raise BadRQLQuery('%s\n** %s' % (node, '\n** '.join(errors)))
+        state = STCheckState()
+        self._visit(node, state)
+        if state.errors:
+            raise BadRQLQuery('%s\n** %s' % (node, '\n** '.join(state.errors)))
         #if node.TYPE == 'select' and \
         #       not node.defined_vars and not node.get_restriction():
         #    result = []
         #    for term in node.selected_terms():
         #        result.append(term.eval(kwargs))
 
-    def _visit(self, node, errors):
+    def _visit(self, node, state):
         try:
-            node.accept(self, errors)
+            node.accept(self, state)
         except GoTo, ex:
-            self._visit(ex.node, errors)
+            self._visit(ex.node, state)
         else:
             for c in node.children:
-                self._visit(c, errors)
-            node.leave(self, errors)
+                self._visit(c, state)
+            node.leave(self, state)
 
-    def _visit_selectedterm(self, node, errors):
+    def _visit_selectedterm(self, node, state):
         for i, term in enumerate(node.selection):
             # selected terms are not included by the default visit,
             # accept manually each of them
-            self._visit(term, errors)
+            self._visit(term, state)
 
-    def _check_selected(self, term, termtype, errors):
+    def _check_selected(self, term, termtype, state):
         """check that variables referenced in the given term are selected"""
         for vref in variable_refs(term):
             # no stinfo yet, use references
@@ -96,41 +122,49 @@
                     break
             else:
                 msg = 'variable %s used in %s is not referenced by any relation'
-                errors.append(msg % (vref.name, termtype))
+                state.error(msg % (vref.name, termtype))
 
     # statement nodes #########################################################
 
-    def visit_union(self, node, errors):
+    def visit_union(self, node, state):
         nbselected = len(node.children[0].selection)
         for select in node.children[1:]:
             if not len(select.selection) == nbselected:
-                errors.append('when using union, all subqueries should have '
+                state.error('when using union, all subqueries should have '
                               'the same number of selected terms')
-    def leave_union(self, node, errors):
+    def leave_union(self, node, state):
         pass
 
-    def visit_select(self, node, errors):
+    def visit_select(self, node, state):
         node.vargraph = {} # graph representing links between variable
         node.aggregated = set()
-        self._visit_selectedterm(node, errors)
+        self._visit_selectedterm(node, state)
 
-    def leave_select(self, node, errors):
+    def leave_select(self, node, state):
         selected = node.selection
         # check selected variable are used in restriction
         if node.where is not None or len(selected) > 1:
             for term in selected:
-                self._check_selected(term, 'selection', errors)
+                self._check_selected(term, 'selection', state)
+                for vref in term.iget_nodes(VariableRef):
+                    state.add_var_info(vref.variable, VAR_SELECTED)
+        for var in node.defined_vars.itervalues():
+            vinfo = state.var_info.get(var, 0)
+            if not (vinfo & VAR_HAS_REL) and (vinfo & VAR_HAS_TYPE_REL) \
+                   and not (vinfo & VAR_SELECTED):
+                raise BadRQLQuery('unbound variable %s (%s)' % (var.name, selected))
         if node.groupby:
             # check that selected variables are used in groups
             for var in node.selection:
                 if isinstance(var, VariableRef) and not var in node.groupby:
-                    errors.append('variable %s should be grouped' % var)
+                    state.error('variable %s should be grouped' % var)
             for group in node.groupby:
-                self._check_selected(group, 'group', errors)
+                self._check_selected(group, 'group', state)
         if node.distinct and node.orderby:
             # check that variables referenced in the given term are reachable from
-            # a selected variable with only ?1 cardinalityselected
-            selectidx = frozenset(vref.name for term in selected for vref in term.get_nodes(VariableRef))
+            # a selected variable with only ?1 cardinality selected
+            selectidx = frozenset(vref.name for term in selected
+                                  for vref in term.get_nodes(VariableRef))
             schema = self.schema
             for sortterm in node.orderby:
                 for vref in sortterm.term.get_nodes(VariableRef):
@@ -146,56 +180,57 @@
                         msg = ('can\'t sort on variable %s which is linked to a'
                                ' variable in the selection but may have different'
                                ' values for a resulting row')
-                        errors.append(msg % vref.name)
+                        state.error(msg % vref.name)
 
     def has_unique_value_path(self, select, fromvar, tovar):
         graph = select.vargraph
         path = has_path(graph, fromvar, tovar)
         if path is None:
             return False
-        for tovar in path:
+        for var in path:
             try:
-                rtype = graph[(fromvar, tovar)]
+                rtype = graph[(fromvar, var)]
                 cardidx = 0
             except KeyError:
-                rtype = graph[(tovar, fromvar)]
+                rtype = graph[(var, fromvar)]
                 cardidx = 1
             rschema = self.schema.rschema(rtype)
             for rdef in rschema.rdefs.itervalues():
                 # XXX aggregats handling needs much probably some enhancements...
-                if not (tovar in select.aggregated
-                        or rdef.cardinality[cardidx] in '?1'):
+                if not (var in select.aggregated
+                        or (rdef.cardinality[cardidx] in '?1' and
+                            (var == tovar or not rschema.final))):
                     return False
-            fromvar = tovar
+            fromvar = var
         return True
 
 
-    def visit_insert(self, insert, errors):
-        self._visit_selectedterm(insert, errors)
-    def leave_insert(self, node, errors):
+    def visit_insert(self, insert, state):
+        self._visit_selectedterm(insert, state)
+    def leave_insert(self, node, state):
         pass
 
-    def visit_delete(self, delete, errors):
-        self._visit_selectedterm(delete, errors)
-    def leave_delete(self, node, errors):
+    def visit_delete(self, delete, state):
+        self._visit_selectedterm(delete, state)
+    def leave_delete(self, node, state):
         pass
 
-    def visit_set(self, update, errors):
-        self._visit_selectedterm(update, errors)
-    def leave_set(self, node, errors):
+    def visit_set(self, update, state):
+        self._visit_selectedterm(update, state)
+    def leave_set(self, node, state):
         pass
 
     # tree nodes ##############################################################
 
-    def visit_exists(self, node, errors):
+    def visit_exists(self, node, state):
         pass
-    def leave_exists(self, node, errors):
+    def leave_exists(self, node, state):
         pass
 
-    def visit_subquery(self, node, errors):
+    def visit_subquery(self, node, state):
         pass
 
-    def leave_subquery(self, node, errors):
+    def leave_subquery(self, node, state):
         # copy graph information we're interested in
         pgraph = node.parent.vargraph
         for select in node.query.children:
@@ -205,7 +240,7 @@
                 try:
                     subvref = select.selection[i]
                 except IndexError:
-                    errors.append('subquery "%s" has only %s selected terms, needs %s'
+                    state.error('subquery "%s" has only %s selected terms, needs %s'
                                   % (select, len(select.selection), len(node.aliases)))
                     continue
                 if isinstance(subvref, VariableRef):
@@ -225,12 +260,12 @@
                     values = pgraph.setdefault(_var_graphid(key, trmap, select), [])
                     values += [_var_graphid(v, trmap, select) for v in val]
 
-    def visit_sortterm(self, sortterm, errors):
+    def visit_sortterm(self, sortterm, state):
         term = sortterm.term
         if isinstance(term, Constant):
             for select in sortterm.root.children:
                 if len(select.selection) < term.value:
-                    errors.append('order column out of bound %s' % term.value)
+                    state.error('order column out of bound %s' % term.value)
         else:
             stmt = term.stmt
             for tvref in variable_refs(term):
@@ -239,17 +274,17 @@
                         break
                 else:
                     msg = 'sort variable %s is not referenced any where else'
-                    errors.append(msg % tvref.name)
+                    state.error(msg % tvref.name)
 
-    def leave_sortterm(self, node, errors):
+    def leave_sortterm(self, node, state):
         pass
 
-    def visit_and(self, et, errors):
+    def visit_and(self, et, state):
         pass #assert len(et.children) == 2, len(et.children)
-    def leave_and(self, node, errors):
+    def leave_and(self, node, state):
         pass
 
-    def visit_or(self, ou, errors):
+    def visit_or(self, ou, state):
         #assert len(ou.children) == 2, len(ou.children)
         # simplify Ored expression of a symmetric relation
         r1, r2 = ou.children[0], ou.children[1]
@@ -270,80 +305,129 @@
                     raise GoTo(r1)
             except AttributeError:
                 pass
-    def leave_or(self, node, errors):
-        pass
-
-    def visit_not(self, not_, errors):
-        pass
-    def leave_not(self, not_, errors):
+    def leave_or(self, node, state):
         pass
 
-    def visit_relation(self, relation, errors):
-        if relation.optional and relation.neged():
-            errors.append("can use optional relation under NOT (%s)"
-                          % relation.as_string())
-        # special case "X identity Y"
-        if relation.r_type == 'identity':
-            lhs, rhs = relation.children
-            #assert not isinstance(relation.parent, Not)
-            #assert rhs.operator == '='
-        elif relation.r_type == 'is':
+    def visit_not(self, not_, state):
+        state.under_not.append(True)
+    def leave_not(self, not_, state):
+        state.under_not.pop()
+        # NOT normalization
+        child = not_.children[0]
+        if self._should_wrap_by_exists(child):
+            not_.replace(child, Exists(child))
+
+    def _should_wrap_by_exists(self, child):
+        if isinstance(child, Exists):
+            return False
+        if not isinstance(child, Relation):
+            return True
+        if child.r_type == 'identity':
+            return False
+        rschema = self.schema.rschema(child.r_type)
+        if rschema.final:
+            return False
+        # XXX no exists for `inlined` relation (allow IS NULL optimization)
+        # unless the lhs variable is only referenced from this neged relation,
+        # in which case it's *not* in the statement's scope, hence EXISTS should
+        # be added anyway
+        if rschema.inlined:
+            references = child.children[0].variable.references()
+            valuable = 0
+            for vref in references:
+                rel = vref.relation()
+                if rel is None or not rel.is_types_restriction():
+                    if valuable:
+                        return False
+                    valuable = 1
+            return True
+        return not child.is_types_restriction()
+
+    def visit_relation(self, relation, state):
+        if relation.optional and state.under_not:
+            state.error("can't use optional relation under NOT (%s)"
+                        % relation.as_string())
+        lhsvar = relation.children[0].variable
+        if relation.is_types_restriction():
+            if relation.optional:
+                state.error('can\'t use optional relation on "%s"'
+                            % relation.as_string())
+            if state.var_info.get(lhsvar, 0) & VAR_HAS_TYPE_REL:
+                state.error('can only one type restriction per variable (use '
+                            'IN for %s if desired)' % lhsvar.name)
+            else:
+                state.add_var_info(lhsvar, VAR_HAS_TYPE_REL)
             # special case "C is NULL"
-            if relation.children[1].operator == 'IS':
-                lhs, rhs = relation.children
-                #assert isinstance(lhs, VariableRef), lhs
-                #assert isinstance(rhs.children[0], Constant)
-                #assert rhs.operator == 'IS', rhs.operator
-                #assert rhs.children[0].type == None
+            # if relation.children[1].operator == 'IS':
+            #     lhs, rhs = relation.children
+            #     #assert isinstance(lhs, VariableRef), lhs
+            #     #assert isinstance(rhs.children[0], Constant)
+            #     #assert rhs.operator == 'IS', rhs.operator
+            #     #assert rhs.children[0].type == None
         else:
+            state.add_var_info(lhsvar, VAR_HAS_REL)
+            rtype = relation.r_type
             try:
-                rschema = self.schema.rschema(relation.r_type)
+                rschema = self.schema.rschema(rtype)
             except KeyError:
-                errors.append('unknown relation `%s`' % relation.r_type)
+                state.error('unknown relation `%s`' % rtype)
             else:
                 if relation.optional and rschema.final:
-                    errors.append("shouldn't use optional on final relation `%s`"
-                                  % relation.r_type)
+                    state.error("shouldn't use optional 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 '
+                                    '(use IN for %s if desired)' % lhsvar.name)
+                    else:
+                        state.add_var_info(lhsvar, VAR_HAS_UID_REL)
+
+            for vref in relation.children[1].get_nodes(VariableRef):
+                state.add_var_info(vref.variable, VAR_HAS_REL)
         try:
             vargraph = relation.stmt.vargraph
             rhsvarname = relation.children[1].children[0].variable.name
-            lhsvarname = relation.children[0].name
         except AttributeError:
             pass
         else:
-            vargraph.setdefault(lhsvarname, []).append(rhsvarname)
-            vargraph.setdefault(rhsvarname, []).append(lhsvarname)
-            vargraph[(lhsvarname, rhsvarname)] = relation.r_type
+            vargraph.setdefault(lhsvar.name, []).append(rhsvarname)
+            vargraph.setdefault(rhsvarname, []).append(lhsvar.name)
+            vargraph[(lhsvar.name, rhsvarname)] = relation.r_type
 
-    def leave_relation(self, relation, errors):
+    def leave_relation(self, relation, state):
         pass
         #assert isinstance(lhs, VariableRef), '%s: %s' % (lhs.__class__,
         #                                                       relation)
 
-    def visit_comparison(self, comparison, errors):
+    def visit_comparison(self, comparison, state):
         pass #assert len(comparison.children) in (1,2), len(comparison.children)
-    def leave_comparison(self, node, errors):
+    def leave_comparison(self, node, state):
         pass
 
-    def visit_mathexpression(self, mathexpr, errors):
+    def visit_mathexpression(self, mathexpr, state):
         pass #assert len(mathexpr.children) == 2, len(mathexpr.children)
-    def leave_mathexpression(self, node, errors):
+    def leave_mathexpression(self, node, state):
         pass
 
-    def visit_function(self, function, errors):
+    def visit_function(self, function, state):
         try:
             funcdescr = function_description(function.name)
         except UnknownFunction:
-            errors.append('unknown function "%s"' % function.name)
+            state.error('unknown function "%s"' % function.name)
         else:
             try:
                 funcdescr.check_nbargs(len(function.children))
             except BadRQLQuery, ex:
-                errors.append(str(ex))
+                state.error(str(ex))
+            if self.backend is not None:
+                try:
+                    funcdescr.st_check_backend(self.backend, function)
+                except BadRQLQuery, ex:
+                    state.error(str(ex))
             if funcdescr.aggregat:
                 if isinstance(function.children[0], Function) and \
                        function.children[0].descr().aggregat:
-                    errors.append('can\'t nest aggregat functions')
+                    state.error('can\'t nest aggregat functions')
             if funcdescr.name == 'IN':
                 #assert function.parent.operator == '='
                 if len(function.children) == 1:
@@ -351,10 +435,11 @@
                     function.parent.remove(function)
                 #else:
                 #    assert len(function.children) >= 1
-    def leave_function(self, node, errors):
+
+    def leave_function(self, node, state):
         pass
 
-    def visit_variableref(self, variableref, errors):
+    def visit_variableref(self, variableref, state):
         #assert len(variableref.children)==0
         #assert not variableref.parent is variableref
 ##         try:
@@ -364,23 +449,22 @@
 ##             raise Exception((variableref.root(), variableref.variable))
         pass
 
-    def leave_variableref(self, node, errors):
+    def leave_variableref(self, node, state):
         pass
 
-    def visit_constant(self, constant, errors):
+    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'
-                errors.append(msg)
+                state.error(msg)
             if not constant.value in self.schema:
-                errors.append('unknown entity type %s' % constant.value)
+                state.error('unknown entity type %s' % constant.value)
 
-    def leave_constant(self, node, errors):
+    def leave_constant(self, node, state):
         pass
 
 
-
 class RQLSTAnnotator(object):
     """Annotate RQL syntax tree to ease further code generation from it.
 
@@ -409,9 +493,8 @@
             for vref in term.get_nodes(VariableRef):
                 vref.variable.stinfo['selected'].add(i)
                 vref.variable.set_scope(node)
-                vref.variable.set_sqlscope(node)
         if node.where is not None:
-            node.where.accept(self, node, node)
+            node.where.accept(self, node)
 
     visit_insert = visit_delete = visit_set = _visit_stmt
 
@@ -432,154 +515,158 @@
             # if there is a having clause, bloc simplification of variables used in GROUPBY
             for term in node.groupby:
                 for vref in term.get_nodes(VariableRef):
-                    vref.variable.stinfo['blocsimplification'].add(term)
-        for var in node.defined_vars.itervalues():
-            if not var.stinfo['relations'] and var.stinfo['typerels'] and not var.stinfo['selected']:
-                raise BadRQLQuery('unbound variable %s (%s)' % (var.name, var.stmt.root))
-            if len(var.stinfo['uidrels']) > 1:
-                uidrels = iter(var.stinfo['uidrels'])
-                val = getattr(uidrels.next().get_variable_parts()[1], 'value', object())
-                for uidrel in uidrels:
-                    if getattr(uidrel.get_variable_parts()[1], 'value', None) != val:
-                        # XXX should check OR branch and check simplify in that case as well
-                        raise BadRQLQuery('conflicting eid values for %s' % var.name)
+                    bloc_simplification(vref.variable, term)
 
-    def rewrite_shared_optional(self, exists, var):
+    def rewrite_shared_optional(self, exists, var, identity_rel_scope=None):
         """if variable is shared across multiple scopes, need some tree
         rewriting
         """
-        if var.scope is var.stmt:
-            # allocate a new variable
-            newvar = var.stmt.make_variable()
-            newvar.prepare_annotation()
-            for vref in var.references():
-                if vref.scope is exists:
-                    rel = vref.relation()
-                    vref.unregister_reference()
-                    newvref = VariableRef(newvar)
-                    vref.parent.replace(vref, newvref)
-                    # update stinfo structure which may have already been
-                    # partially processed
-                    if rel in var.stinfo['rhsrelations']:
-                        lhs, rhs = rel.get_parts()
-                        if vref is rhs.children[0] and \
-                               self.schema.rschema(rel.r_type).final:
-                            update_attrvars(newvar, rel, lhs)
-                            lhsvar = getattr(lhs, 'variable', None)
-                            var.stinfo['attrvars'].remove( (lhsvar, rel.r_type) )
-                            if var.stinfo['attrvar'] is lhsvar:
-                                if var.stinfo['attrvars']:
-                                    var.stinfo['attrvar'] = iter(var.stinfo['attrvars']).next()
-                                else:
-                                    var.stinfo['attrvar'] = None
-                        var.stinfo['rhsrelations'].remove(rel)
-                        newvar.stinfo['rhsrelations'].add(rel)
-                    for stinfokey in ('blocsimplification','typerels', 'uidrels',
-                                      'relations', 'optrelations'):
-                        try:
-                            var.stinfo[stinfokey].remove(rel)
-                            newvar.stinfo[stinfokey].add(rel)
-                        except KeyError:
-                            continue
-            # shared references
-            newvar.stinfo['constnode'] = var.stinfo['constnode']
-            if newvar.stmt.solutions: # solutions already computed
-                newvar.stinfo['possibletypes'] = var.stinfo['possibletypes']
-                for sol in newvar.stmt.solutions:
-                    sol[newvar.name] = sol[var.name]
+        # allocate a new variable
+        newvar = var.stmt.make_variable()
+        newvar.prepare_annotation()
+        for vref in var.references():
+            if vref.scope is exists:
+                rel = vref.relation()
+                vref.unregister_reference()
+                newvref = VariableRef(newvar)
+                vref.parent.replace(vref, newvref)
+                stinfo = var.stinfo
+                # update stinfo structure which may have already been
+                # partially processed
+                if rel in stinfo['rhsrelations']:
+                    lhs, rhs = rel.get_parts()
+                    if vref is rhs.children[0] and \
+                           self.schema.rschema(rel.r_type).final:
+                        update_attrvars(newvar, rel, lhs)
+                        lhsvar = getattr(lhs, 'variable', None)
+                        stinfo['attrvars'].remove( (lhsvar, rel.r_type) )
+                        if stinfo['attrvar'] is lhsvar:
+                            if stinfo['attrvars']:
+                                stinfo['attrvar'] = iter(stinfo['attrvars']).next()
+                            else:
+                                stinfo['attrvar'] = None
+                    stinfo['rhsrelations'].remove(rel)
+                    newvar.stinfo['rhsrelations'].add(rel)
+                try:
+                    stinfo['relations'].remove(rel)
+                    newvar.stinfo['relations'].add(rel)
+                except KeyError:
+                    pass
+                try:
+                    stinfo['optrelations'].remove(rel)
+                    newvar.add_optional_relation(rel)
+                except KeyError:
+                    pass
+                try:
+                    stinfo['blocsimplification'].remove(rel)
+                    bloc_simplification(newvar, rel)
+                except KeyError:
+                    pass
+                if stinfo['uidrel'] is rel:
+                    newvar.stinfo['uidrel'] = rel
+                    stinfo['uidrel'] = None
+                if stinfo['typerel'] is rel:
+                    newvar.stinfo['typerel'] = rel
+                    stinfo['typerel'] = None
+        # shared references
+        newvar.stinfo['constnode'] = var.stinfo['constnode']
+        if newvar.stmt.solutions: # solutions already computed
+            newvar.stinfo['possibletypes'] = var.stinfo['possibletypes']
+            for sol in newvar.stmt.solutions:
+                sol[newvar.name] = sol[var.name]
+        if identity_rel_scope is None:
             rel = exists.add_relation(var, 'identity', newvar)
-            # we have to force visit of the introduced relation
-            self.visit_relation(rel, exists, exists)
-            return newvar
-        return None
+            identity_rel_scope = exists
+        else:
+            rel = make_relation(var, 'identity', (newvar,), VariableRef)
+            exists.parent.replace(exists, And(exists, Exists(rel)))
+        # we have to force visit of the introduced relation
+        self.visit_relation(rel, identity_rel_scope)
+        return newvar
 
     # tree nodes ##############################################################
 
-    def visit_exists(self, node, scope, sqlscope):
-        node.children[0].accept(self, node, node)
+    def visit_exists(self, node, scope):
+        node.children[0].accept(self, node)
 
-    def visit_not(self, node, scope, sqlscope):
-        node.children[0].accept(self, scope, node)
+    def visit_not(self, node, scope):
+        node.children[0].accept(self, scope)
 
-    def visit_and(self, node, scope, sqlscope):
-        node.children[0].accept(self, scope, sqlscope)
-        node.children[1].accept(self, scope, sqlscope)
+    def visit_and(self, node, scope):
+        node.children[0].accept(self, scope)
+        node.children[1].accept(self, scope)
     visit_or = visit_and
 
-    def visit_relation(self, relation, scope, sqlscope):
+    def visit_relation(self, relation, scope):
         #assert relation.parent, repr(relation)
         lhs, rhs = relation.get_parts()
         # may be a constant once rqlst has been simplified
         lhsvar = getattr(lhs, 'variable', None)
         if relation.is_types_restriction():
-            #assert rhs.operator == '='
-            #assert not relation.optional
             if lhsvar is not None:
-                lhsvar.stinfo['typerels'].add(relation)
+                lhsvar.stinfo['typerel'] = relation
             return
         if relation.optional is not None:
             exists = relation.scope
             if not isinstance(exists, Exists):
                 exists = None
             if lhsvar is not None:
-                if exists is not None:
-                    newvar = self.rewrite_shared_optional(exists, lhsvar)
-                    if newvar is not None:
-                        lhsvar = newvar
-                lhsvar.stinfo['blocsimplification'].add(relation)
+                if exists is not None and lhsvar.scope is lhsvar.stmt:
+                    lhsvar = self.rewrite_shared_optional(exists, lhsvar)
+                bloc_simplification(lhsvar, relation)
                 if relation.optional == 'both':
-                    lhsvar.stinfo['optrelations'].add(relation)
+                    lhsvar.add_optional_relation(relation)
                 elif relation.optional == 'left':
-                    lhsvar.stinfo['optrelations'].add(relation)
+                    lhsvar.add_optional_relation(relation)
             try:
                 rhsvar = rhs.children[0].variable
-                if exists is not None:
-                    newvar = self.rewrite_shared_optional(exists, rhsvar)
-                    if newvar is not None:
-                        rhsvar = newvar
-                rhsvar.stinfo['blocsimplification'].add(relation)
+                if exists is not None and rhsvar.scope is rhsvar.stmt:
+                    rhsvar = self.rewrite_shared_optional(exists, rhsvar)
+                bloc_simplification(rhsvar, relation)
                 if relation.optional == 'right':
-                    rhsvar.stinfo['optrelations'].add(relation)
+                    rhsvar.add_optional_relation(relation)
                 elif relation.optional == 'both':
-                    rhsvar.stinfo['optrelations'].add(relation)
+                    rhsvar.add_optional_relation(relation)
             except AttributeError:
                 # may have been rewritten as well
                 pass
         rtype = relation.r_type
-        try:
-            rschema = self.schema.rschema(rtype)
-        except KeyError:
-            raise BadRQLQuery('no relation %s' % rtype)
+        rschema = self.schema.rschema(rtype)
         if lhsvar is not None:
             lhsvar.set_scope(scope)
-            lhsvar.set_sqlscope(sqlscope)
             lhsvar.stinfo['relations'].add(relation)
             if rtype in self.special_relations:
                 key = '%srels' % self.special_relations[rtype]
                 if key == 'uidrels':
                     constnode = relation.get_variable_parts()[1]
                     if not (relation.operator() != '=' or
-                            isinstance(relation.parent, Not)):
+                            # XXX use state to detect relation under NOT/OR
+                            # + check variable's scope
+                            isinstance(relation.parent, Not) or
+                            relation.parent.ored()):
                         if isinstance(constnode, Constant):
                             lhsvar.stinfo['constnode'] = constnode
-                        lhsvar.stinfo.setdefault(key, set()).add(relation)
+                        lhsvar.stinfo['uidrel'] = relation
                 else:
                     lhsvar.stinfo.setdefault(key, set()).add(relation)
             elif rschema.final or rschema.inlined:
-                lhsvar.stinfo['blocsimplification'].add(relation)
+                bloc_simplification(lhsvar, relation)
         for vref in rhs.get_nodes(VariableRef):
             var = vref.variable
             var.set_scope(scope)
-            var.set_sqlscope(sqlscope)
             var.stinfo['relations'].add(relation)
             var.stinfo['rhsrelations'].add(relation)
             if vref is rhs.children[0] and rschema.final:
                 update_attrvars(var, relation, lhs)
 
-
 def update_attrvars(var, relation, lhs):
+    # stinfo['attrvars'] is set of couple (lhs variable name, relation name)
+    # where the `var` attribute variable is used
     lhsvar = getattr(lhs, 'variable', None)
-    var.stinfo['attrvars'].add( (lhsvar, relation.r_type) )
+    try:
+        var.stinfo['attrvars'].add( (lhsvar, relation.r_type) )
+    except KeyError:
+        var.stinfo['attrvars'] = set([(lhsvar, relation.r_type)])
     # give priority to variable which is not in an EXISTS as
     # "main" attribute variable
     if var.stinfo['attrvar'] is None or not isinstance(relation.scope, Exists):
--- a/stmts.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/stmts.py	Wed May 11 09:02:33 2011 +0200
@@ -19,8 +19,8 @@
 
 This module defines only first level nodes (i.e. statements). Child nodes are
 defined in the nodes module
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from copy import deepcopy
@@ -101,10 +101,7 @@
             self._varmaker = rqlvar_maker(defined=self.defined_vars,
                                           # XXX only on Select node
                                           aliases=getattr(self, 'aliases', None))
-        name =  self._varmaker.next()
-        while name in self.defined_vars:
-            name =  self._varmaker.next()
-        return name
+        return self._varmaker.next()
 
     def make_variable(self):
         """create a new variable with an unique name for this tree"""
@@ -146,6 +143,7 @@
             raise
         return True
 
+
 class Statement(object):
     """base class for statement nodes"""
 
@@ -168,7 +166,6 @@
     @property
     def scope(self):
         return self
-    sqlscope = scope
 
     def ored(self, traverse_scope=False, _fromnode=None):
         return None
@@ -272,14 +269,27 @@
 
     # union specific methods ##################################################
 
+    # XXX for bw compat, should now use get_variable_indices (cw > 3.8.4)
     def get_variable_variables(self):
-        """return the set of variable names which take different type according
-        to the solutions
+        change = set()
+        for idx in self.get_variable_indices():
+            for vref in self.children[0].selection[idx].iget_nodes(nodes.VariableRef):
+                change.add(vref.name)
+        return change
+
+    def get_variable_indices(self):
+        """return the set of selection indexes which take different types
+        according to the solutions
         """
         change = set()
         values = {}
         for select in self.children:
-            change.update(select.get_variable_variables(values))
+            for descr in select.get_selection_solutions():
+                for i, etype in enumerate(descr):
+                    values.setdefault(i, set()).add(etype)
+        for idx, etypes in values.iteritems():
+            if len(etypes) > 1:
+                change.add(idx)
         return change
 
     def _locate_subquery(self, col, etype, kwargs):
@@ -315,14 +325,16 @@
         return self._subq_cache[(col, etype)]
 
     def subquery_selection_index(self, subselect, col):
-        """given a select sub-query and a column index in this sub-query, return
-        the selection index for this column in the root query
+        """given a select sub-query and a column index in the root query, return
+        the selection index for this column in the sub-query
         """
-        while col is not None and subselect.parent.parent:
+        selectpath = []
+        while subselect.parent.parent is not None:
             subq = subselect.parent.parent
             subselect = subq.parent
-            termvar = subselect.aliases[subq.aliases[col].name]
-            col = termvar.selected_index()
+            selectpath.insert(0, subselect)
+        for select in selectpath:
+            col = select.selection[col].variable.colnum
         return col
 
     # recoverable modification methods ########################################
@@ -386,7 +398,7 @@
     # select clauses
     groupby = ()
     orderby = ()
-    having = ()
+    having = () # XXX now a single node
     with_ = ()
     # set by the annotator
     has_aggregat = False
@@ -612,7 +624,7 @@
             solutions = self.solutions
         # this may occurs with rql optimization, for instance on
         # 'Any X WHERE X eid 12' query
-        if not self.defined_vars:
+        if not (self.defined_vars or self.aliases):
             self.solutions = [{}]
         else:
             newsolutions = []
@@ -620,24 +632,26 @@
                 asol = {}
                 for var in self.defined_vars:
                     asol[var] = origsol[var]
+                for var in self.aliases:
+                    asol[var] = origsol[var]
                 if not asol in newsolutions:
                     newsolutions.append(asol)
             self.solutions = newsolutions
 
-    def get_variable_variables(self, _values=None):
+    def get_selection_solutions(self):
         """return the set of variable names which take different type according
         to the solutions
         """
-        change = set()
-        if _values is None:
-            _values = {}
+        descriptions = set()
         for solution in self.solutions:
-            for vname, etype in solution.iteritems():
-                if not vname in _values:
-                    _values[vname] = etype
-                elif _values[vname] != etype:
-                    change.add(vname)
-        return change
+            descr = []
+            for term in self.selection:
+                try:
+                    descr.append(term.get_type(solution=solution))
+                except CoercionError:
+                    pass
+            descriptions.add(tuple(descr))
+        return descriptions
 
     # quick accessors #########################################################
 
@@ -665,17 +679,28 @@
         term.parent = self
         self.selection.append(term)
 
+    # XXX proprify edition, we should specify if we want:
+    # * undo support
+    # * references handling
     def replace(self, oldnode, newnode):
-        assert oldnode is self.where
-        self.where = newnode
+        if oldnode is self.where:
+            self.where = newnode
+        elif oldnode in self.selection:
+            self.selection[self.selection.index(oldnode)] = newnode
+        elif oldnode in self.orderby:
+            self.orderby[self.orderby.index(oldnode)] = newnode
+        elif oldnode in self.groupby:
+            self.groupby[self.groupby.index(oldnode)] = newnode
+        elif oldnode in self.having:
+            self.having[self.having.index(oldnode)] = newnode
+        else:
+            raise Exception('duh XXX %s' % oldnode)
+        # XXX no undo/reference support 'by design' (eg breaks things if you add
+        # it...)
+        # XXX resetting oldnode parent cause pb with cw.test_views (w/ facets)
+        #oldnode.parent = None
         newnode.parent = self
-#         # XXX no vref handling ?
-#         try:
-#             Statement.replace(self, oldnode, newnode)
-#         except ValueError:
-#             i = self.selection.index(oldnode)
-#             self.selection.pop(i)
-#             self.selection.insert(i, newnode)
+        return oldnode, self, None
 
     def remove(self, node):
         if node is self.where:
@@ -684,9 +709,13 @@
             self.remove_sort_term(node)
         elif node in self.groupby:
             self.remove_group_var(node)
+        elif node in self.having:
+            self.having.remove(node)
+        # XXX selection
         else:
             raise Exception('duh XXX')
         node.parent = None
+        return node, self, None
 
     def undefine_variable(self, var):
         """undefine the given variable and remove all relations where it appears"""
@@ -707,7 +736,10 @@
         # effective undefine operation
         if self.should_register_op:
             from rql.undo import UndefineVarOperation
-            self.undo_manager.add_operation(UndefineVarOperation(var))
+            solutions = [d.copy() for d in self.solutions]
+            self.undo_manager.add_operation(UndefineVarOperation(var, self, solutions))
+        for sol in self.solutions:
+            sol.pop(var.name, None)
         del self.defined_vars[var.name]
 
     def _var_index(self, var):
--- a/test/unittest_analyze.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/test/unittest_analyze.py	Wed May 11 09:02:33 2011 +0200
@@ -47,7 +47,7 @@
         self.inlined = False
         if card is None:
             if self.final:
-                card = '?*'
+                card = '?1'
             else:
                 card = '**'
         self.card = card
@@ -301,7 +301,7 @@
                                 {'X': 'Student', 'T': 'Eetype'}])
 
     def test_not(self):
-        node = self.helper.parse('Any X WHERE not X is Person')
+        node = self.helper.parse('Any X WHERE NOT X is Person')
         self.helper.compute_solutions(node, debug=DEBUG)
         sols = sorted(node.children[0].solutions)
         expected = ALL_SOLS[:]
@@ -311,19 +311,19 @@
     def test_uid_func_mapping(self):
         h = self.helper
         def type_from_uid(name):
-            self.assertEquals(name, "Logilab")
+            self.assertEqual(name, "Logilab")
             return 'Company'
         uid_func_mapping = {'name': type_from_uid}
         # constant as rhs of the uid relation
         node = h.parse('Any X WHERE X name "Logilab"')
         h.compute_solutions(node, uid_func_mapping, debug=DEBUG)
         sols = sorted(node.children[0].solutions)
-        self.assertEquals(sols, [{'X': 'Company'}])
+        self.assertEqual(sols, [{'X': 'Company'}])
         # variable as rhs of the uid relation
         node = h.parse('Any N WHERE X name N')
         h.compute_solutions(node, uid_func_mapping, debug=DEBUG)
         sols = sorted(node.children[0].solutions)
-        self.assertEquals(sols, [{'X': 'Company', 'N': 'String'},
+        self.assertEqual(sols, [{'X': 'Company', 'N': 'String'},
                                  {'X': 'Person', 'N': 'String'},
                                  {'X': 'Student', 'N': 'String'}])
         # substitute as rhs of the uid relation
@@ -331,19 +331,19 @@
         h.compute_solutions(node, uid_func_mapping, {'company': 'Logilab'},
                         debug=DEBUG)
         sols = sorted(node.children[0].solutions)
-        self.assertEquals(sols, [{'X': 'Company'}])
+        self.assertEqual(sols, [{'X': 'Company'}])
 
     def test_non_regr_subjobj1(self):
         h = self.helper
         def type_from_uid(name):
-            self.assertEquals(name, "Societe")
+            self.assertEqual(name, "Societe")
             return 'Eetype'
         uid_func_mapping = {'name': type_from_uid}
         # constant as rhs of the uid relation
         node = h.parse('Any X WHERE X name "Societe", X is ISOBJ, ISSIBJ is X')
         h.compute_solutions(node, uid_func_mapping, debug=DEBUG)
         sols = sorted(node.children[0].solutions)
-        self.assertEquals(sols, [{'X': 'Eetype', 'ISOBJ': 'Eetype', 'ISSIBJ': 'Address'},
+        self.assertEqual(sols, [{'X': 'Eetype', 'ISOBJ': 'Eetype', 'ISSIBJ': 'Address'},
                                  {'X': 'Eetype', 'ISOBJ': 'Eetype', 'ISSIBJ': 'Company'},
                                  {'X': 'Eetype', 'ISOBJ': 'Eetype', 'ISSIBJ': 'Eetype'},
                                  {'X': 'Eetype', 'ISOBJ': 'Eetype', 'ISSIBJ': 'Person'},
@@ -352,46 +352,46 @@
     def test_non_regr_subjobj2(self):
         h = self.helper
         def type_from_uid(name):
-            self.assertEquals(name, "Societe")
+            self.assertEqual(name, "Societe")
             return 'Eetype'
         uid_func_mapping = {'name': type_from_uid}
         node = h.parse('Any X WHERE X name "Societe", X is ISOBJ, ISSUBJ is X, X is_instance_of ISIOOBJ, ISIOSUBJ is_instance_of X')
         h.compute_solutions(node, uid_func_mapping, debug=DEBUG)
         select = node.children[0]
         sols = sorted(select.solutions)
-        self.assertEquals(len(sols), 25)
+        self.assertEqual(len(sols), 25)
         def var_sols(var):
             s = set()
             for sol in sols:
                 s.add(sol.get(var))
             return s
-        self.assertEquals(var_sols('X'), set(('Eetype',)))
-        self.assertEquals(var_sols('X'), select.defined_vars['X'].stinfo['possibletypes'])
-        self.assertEquals(var_sols('ISSUBJ'), set(('Address', 'Company', 'Eetype', 'Person', 'Student')))
-        self.assertEquals(var_sols('ISSUBJ'), select.defined_vars['ISSUBJ'].stinfo['possibletypes'])
-        self.assertEquals(var_sols('ISOBJ'), set(('Eetype',)))
-        self.assertEquals(var_sols('ISOBJ'), select.defined_vars['ISOBJ'].stinfo['possibletypes'])
-        self.assertEquals(var_sols('ISIOSUBJ'), set(('Address', 'Company', 'Eetype', 'Person', 'Student')))
-        self.assertEquals(var_sols('ISIOSUBJ'), select.defined_vars['ISIOSUBJ'].stinfo['possibletypes'])
-        self.assertEquals(var_sols('ISIOOBJ'), set(('Eetype',)))
-        self.assertEquals(var_sols('ISIOOBJ'), select.defined_vars['ISIOOBJ'].stinfo['possibletypes'])
+        self.assertEqual(var_sols('X'), set(('Eetype',)))
+        self.assertEqual(var_sols('X'), select.defined_vars['X'].stinfo['possibletypes'])
+        self.assertEqual(var_sols('ISSUBJ'), set(('Address', 'Company', 'Eetype', 'Person', 'Student')))
+        self.assertEqual(var_sols('ISSUBJ'), select.defined_vars['ISSUBJ'].stinfo['possibletypes'])
+        self.assertEqual(var_sols('ISOBJ'), set(('Eetype',)))
+        self.assertEqual(var_sols('ISOBJ'), select.defined_vars['ISOBJ'].stinfo['possibletypes'])
+        self.assertEqual(var_sols('ISIOSUBJ'), set(('Address', 'Company', 'Eetype', 'Person', 'Student')))
+        self.assertEqual(var_sols('ISIOSUBJ'), select.defined_vars['ISIOSUBJ'].stinfo['possibletypes'])
+        self.assertEqual(var_sols('ISIOOBJ'), set(('Eetype',)))
+        self.assertEqual(var_sols('ISIOOBJ'), select.defined_vars['ISIOOBJ'].stinfo['possibletypes'])
 
     def test_unusableuid_func_mapping(self):
         h = self.helper
         def type_from_uid(name):
-            self.assertEquals(name, "Logilab")
+            self.assertEqual(name, "Logilab")
             return 'Company'
         uid_func_mapping = {'name': type_from_uid}
         node = h.parse('Any X WHERE NOT X name %(company)s')
         h.compute_solutions(node, uid_func_mapping, {'company': 'Logilab'},
                             debug=DEBUG)
         sols = sorted(node.children[0].solutions)
-        self.assertEquals(sols, ALL_SOLS)
+        self.assertEqual(sols, ALL_SOLS)
         node = h.parse('Any X WHERE X name > %(company)s')
         h.compute_solutions(node, uid_func_mapping, {'company': 'Logilab'},
                             debug=DEBUG)
         sols = sorted(node.children[0].solutions)
-        self.assertEquals(sols, ALL_SOLS)
+        self.assertEqual(sols, ALL_SOLS)
 
 
     def test_base_guess_3(self):
@@ -500,7 +500,7 @@
                          [{'X': 'Person', 'F': 'String'}])
         # auto-simplification
         self.assertEqual(len(node.children[0].with_[0].query.children), 1)
-        self.assertEquals(node.as_string(), 'Any L,Y,F WHERE Y located L, Y is Person WITH Y,F BEING (Any X,F WHERE X is Person, X firstname F)')
+        self.assertEqual(node.as_string(), 'Any L,Y,F WHERE Y located L, Y is Person WITH Y,F BEING (Any X,F WHERE X is Person, X firstname F)')
         self.assertEqual(node.children[0].with_[0].query.children[0].solutions,
                          [{'X': 'Person', 'F': 'String'}])
 
--- a/test/unittest_compare.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/test/unittest_compare.py	Wed May 11 09:02:33 2011 +0200
@@ -15,7 +15,7 @@
 #
 # 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 logilab.common.testlib import TestCase, unittest_main
+from logilab.common.testlib import TestCase, SkipTest, unittest_main
 
 from rql import RQLHelper
 from unittest_analyze import RelationSchema, EntitySchema, DummySchema as BaseSchema
@@ -36,13 +36,16 @@
 
 class RQLCompareClassTest(TestCase):
     """ Compare RQL strings """
+    @classmethod
+    def setUpClass(cls):
+        raise SkipTest('broken')
 
     def setUp(self):
         self.h = RQLHelper(DummySchema(), None)
 
     def _compareEquivalent(self,r1,r2):
         """fails if the RQL strings r1 and r2 are equivalent"""
-        self.skip('broken')
+        self.skipTest('broken')
         self.failUnless(self.h.compare(r1, r2),
                         'r1: %s\nr2: %s' % (r1, r2))
 
--- a/test/unittest_editextensions.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/test/unittest_editextensions.py	Wed May 11 09:02:33 2011 +0200
@@ -32,12 +32,12 @@
         select.remove_selected(select.selection[0])
         select.add_selected(var)
         # check operations
-        self.assertEquals(rqlst.as_string(), 'Any %s WHERE X is Person' % var.name)
+        self.assertEqual(rqlst.as_string(), 'Any %s WHERE X is Person' % var.name)
         # check references before recovering
         rqlst.check_references()
         rqlst.recover()
         # check equivalence after recovering
-        self.assertEquals(rqlst.as_string(), orig)
+        self.assertEqual(rqlst.as_string(), orig)
         # check references after recovering
         rqlst.check_references()
     
@@ -50,12 +50,12 @@
         select.remove_selected(select.selection[0])
         select.add_selected(var)
         # check operations
-        self.assertEquals(rqlst.as_string(), 'Any %s WHERE X is Person, X name N' % var.name)
+        self.assertEqual(rqlst.as_string(), 'Any %s WHERE X is Person, X name N' % var.name)
         # check references before recovering
         rqlst.check_references()
         rqlst.recover()
         # check equivalence after recovering
-        self.assertEquals(rqlst.as_string(), orig)
+        self.assertEqual(rqlst.as_string(), orig)
         # check references after recovering
         rqlst.check_references()
         
@@ -65,12 +65,12 @@
         rqlst.save_state()
         rqlst.children[0].undefine_variable(rqlst.children[0].defined_vars['Y'])
         # check operations
-        self.assertEquals(rqlst.as_string(), 'Any X WHERE X is Person')
+        self.assertEqual(rqlst.as_string(), 'Any X WHERE X is Person')
         # check references before recovering
         rqlst.check_references()
         rqlst.recover()
         # check equivalence
-        self.assertEquals(rqlst.as_string(), orig)
+        self.assertEqual(rqlst.as_string(), orig)
         # check references after recovering
         rqlst.check_references()
         
@@ -82,12 +82,12 @@
         var = rqlst.children[0].make_variable()
         rqlst.children[0].add_selected(var)
         # check operations
-        self.assertEquals(rqlst.as_string(), 'Any A')
+        self.assertEqual(rqlst.as_string(), 'Any A')
         # check references before recovering
         rqlst.check_references()
         rqlst.recover()
         # check equivalence
-        self.assertEquals(rqlst.as_string(), orig)
+        self.assertEqual(rqlst.as_string(), orig)
         # check references after recovering
         rqlst.check_references()
         
--- a/test/unittest_nodes.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/test/unittest_nodes.py	Wed May 11 09:02:33 2011 +0200
@@ -36,23 +36,23 @@
 
 class EtypeFromPyobjTC(TestCase):
     def test_bool(self):
-        self.assertEquals(nodes.etype_from_pyobj(True), 'Boolean')
-        self.assertEquals(nodes.etype_from_pyobj(False), 'Boolean')
+        self.assertEqual(nodes.etype_from_pyobj(True), 'Boolean')
+        self.assertEqual(nodes.etype_from_pyobj(False), 'Boolean')
 
     def test_int(self):
-        self.assertEquals(nodes.etype_from_pyobj(0), 'Int')
-        self.assertEquals(nodes.etype_from_pyobj(1L), 'Int')
+        self.assertEqual(nodes.etype_from_pyobj(0), 'Int')
+        self.assertEqual(nodes.etype_from_pyobj(1L), 'Int')
 
     def test_float(self):
-        self.assertEquals(nodes.etype_from_pyobj(0.), 'Float')
+        self.assertEqual(nodes.etype_from_pyobj(0.), 'Float')
 
     def test_datetime(self):
-        self.assertEquals(nodes.etype_from_pyobj(datetime.now()), 'Datetime')
-        self.assertEquals(nodes.etype_from_pyobj(date.today()), 'Date')
+        self.assertEqual(nodes.etype_from_pyobj(datetime.now()), 'Datetime')
+        self.assertEqual(nodes.etype_from_pyobj(date.today()), 'Date')
 
     def test_string(self):
-        self.assertEquals(nodes.etype_from_pyobj('hop'), 'String')
-        self.assertEquals(nodes.etype_from_pyobj(u'hop'), 'String')
+        self.assertEqual(nodes.etype_from_pyobj('hop'), 'String')
+        self.assertEqual(nodes.etype_from_pyobj(u'hop'), 'String')
 
 
 class NodesTest(TestCase):
@@ -61,11 +61,11 @@
         tree.check_references()
         if normrql is None:
             normrql = rql
-        self.assertEquals(tree.as_string(), normrql)
+        self.assertEqual(tree.as_string(), normrql)
         # just check repr() doesn't raise an exception
         repr(tree)
         copy = tree.copy()
-        self.assertEquals(copy.as_string(), normrql)
+        self.assertEqual(copy.as_string(), normrql)
         copy.check_references()
         return tree
 
@@ -77,9 +77,9 @@
         #del d1['parent']; del d1['children'] # parent and children are slots now
         #d2 = tree2.__dict__.copy()
         #del d2['parent']; del d2['children']
-        self.assertNotEquals(id(tree1), id(tree2))
+        self.assertNotEqual(id(tree1), id(tree2))
         self.assert_(tree1.is_equivalent(tree2))
-        #self.assertEquals(len(tree1.children), len(tree2.children))
+        #self.assertEqual(len(tree1.children), len(tree2.children))
         #for i in range(len(tree1.children)):
         #    self.check_equal_but_not_same(tree1.children[i], tree2.children[i])
 
@@ -93,22 +93,22 @@
         self.assertRaises(BadRQLQuery, tree.set_limit, '1')
         tree.save_state()
         tree.set_limit(10)
-        self.assertEquals(select.limit, 10)
-        self.assertEquals(tree.as_string(), 'Any X LIMIT 10 WHERE X is Person')
+        self.assertEqual(select.limit, 10)
+        self.assertEqual(tree.as_string(), 'Any X LIMIT 10 WHERE X is Person')
         tree.recover()
-        self.assertEquals(select.limit, None)
-        self.assertEquals(tree.as_string(), 'Any X WHERE X is Person')
+        self.assertEqual(select.limit, None)
+        self.assertEqual(tree.as_string(), 'Any X WHERE X is Person')
 
     def test_union_set_limit_2(self):
         # not undoable set_limit since a new root has to be introduced
         tree = self._parse("(Any X WHERE X is Person) UNION (Any X WHERE X is Company)")
         tree.save_state()
         tree.set_limit(10)
-        self.assertEquals(tree.as_string(), 'Any A LIMIT 10 WITH A BEING ((Any X WHERE X is Person) UNION (Any X WHERE X is Company))')
+        self.assertEqual(tree.as_string(), 'Any A LIMIT 10 WITH A BEING ((Any X WHERE X is Person) UNION (Any X WHERE X is Company))')
         select = tree.children[0]
-        self.assertEquals(select.limit, 10)
+        self.assertEqual(select.limit, 10)
         tree.recover()
-        self.assertEquals(tree.as_string(), '(Any X WHERE X is Person) UNION (Any X WHERE X is Company)')
+        self.assertEqual(tree.as_string(), '(Any X WHERE X is Person) UNION (Any X WHERE X is Company)')
 
     def test_union_set_offset_1(self):
         tree = self._parse("Any X WHERE X is Person")
@@ -117,10 +117,10 @@
         self.assertRaises(BadRQLQuery, tree.set_offset, '1')
         tree.save_state()
         tree.set_offset(10)
-        self.assertEquals(select.offset, 10)
+        self.assertEqual(select.offset, 10)
         tree.recover()
-        self.assertEquals(select.offset, 0)
-        self.assertEquals(tree.as_string(), 'Any X WHERE X is Person')
+        self.assertEqual(select.offset, 0)
+        self.assertEqual(tree.as_string(), 'Any X WHERE X is Person')
 
     def test_union_set_offset_2(self):
         # not undoable set_offset since a new root has to be introduced
@@ -128,10 +128,10 @@
         tree.save_state()
         tree.set_offset(10)
         select = tree.children[0]
-        self.assertEquals(tree.as_string(), 'Any A OFFSET 10 WITH A BEING ((Any X WHERE X is Person) UNION (Any X WHERE X is Company))')
-        self.assertEquals(select.offset, 10)
+        self.assertEqual(tree.as_string(), 'Any A OFFSET 10 WITH A BEING ((Any X WHERE X is Person) UNION (Any X WHERE X is Company))')
+        self.assertEqual(select.offset, 10)
         tree.recover()
-        self.assertEquals(tree.as_string(), '(Any X WHERE X is Person) UNION (Any X WHERE X is Company)')
+        self.assertEqual(tree.as_string(), '(Any X WHERE X is Person) UNION (Any X WHERE X is Company)')
 
     def test_union_undo_add_rel(self):
         tree = self._parse("Any A WITH A BEING ((Any X WHERE X is Person) UNION (Any X WHERE X is Company))")
@@ -140,34 +140,34 @@
         var = select.make_variable()
         mainvar = select.selection[0].variable
         select.add_relation(mainvar, 'name', var)
-        self.assertEquals(tree.as_string(), 'Any A WHERE A name B WITH A BEING ((Any X WHERE X is Person) UNION (Any X WHERE X is Company))')
+        self.assertEqual(tree.as_string(), 'Any A WHERE A name B WITH A BEING ((Any X WHERE X is Person) UNION (Any X WHERE X is Company))')
         tree.recover()
-        self.assertEquals(tree.as_string(), 'Any A WITH A BEING ((Any X WHERE X is Person) UNION (Any X WHERE X is Company))')
+        self.assertEqual(tree.as_string(), 'Any A WITH A BEING ((Any X WHERE X is Person) UNION (Any X WHERE X is Company))')
 
     def test_select_set_limit(self):
         tree = self._simpleparse("Any X WHERE X is Person")
-        self.assertEquals(tree.limit, None)
+        self.assertEqual(tree.limit, None)
         self.assertRaises(BadRQLQuery, tree.set_limit, 0)
         self.assertRaises(BadRQLQuery, tree.set_limit, -1)
         self.assertRaises(BadRQLQuery, tree.set_limit, '1')
         tree.save_state()
         tree.set_limit(10)
-        self.assertEquals(tree.limit, 10)
+        self.assertEqual(tree.limit, 10)
         tree.recover()
-        self.assertEquals(tree.limit, None)
+        self.assertEqual(tree.limit, None)
 
     def test_select_set_offset(self):
         tree = self._simpleparse("Any X WHERE X is Person")
         self.assertRaises(BadRQLQuery, tree.set_offset, -1)
         self.assertRaises(BadRQLQuery, tree.set_offset, '1')
-        self.assertEquals(tree.offset, 0)
+        self.assertEqual(tree.offset, 0)
         tree.save_state()
         tree.set_offset(0)
-        self.assertEquals(tree.offset, 0)
+        self.assertEqual(tree.offset, 0)
         tree.set_offset(10)
-        self.assertEquals(tree.offset, 10)
+        self.assertEqual(tree.offset, 10)
         tree.recover()
-        self.assertEquals(tree.offset, 0)
+        self.assertEqual(tree.offset, 0)
 
     def test_select_add_sort_var(self):
         tree = self._parse('Any X')
@@ -175,10 +175,10 @@
         select = tree.children[0]
         select.add_sort_var(select.get_variable('X'))
         tree.check_references()
-        self.assertEquals(tree.as_string(), 'Any X ORDERBY X')
+        self.assertEqual(tree.as_string(), 'Any X ORDERBY X')
         tree.recover()
         tree.check_references()
-        self.assertEquals(tree.as_string(), 'Any X')
+        self.assertEqual(tree.as_string(), 'Any X')
 
     def test_select_remove_sort_terms(self):
         tree = self._parse('Any X,Y ORDERBY X,Y')
@@ -186,25 +186,40 @@
         select = tree.children[0]
         select.remove_sort_terms()
         tree.check_references()
-        self.assertEquals(tree.as_string(), 'Any X,Y')
+        self.assertEqual(tree.as_string(), 'Any X,Y')
         tree.recover()
         tree.check_references()
-        self.assertEquals(tree.as_string(), 'Any X,Y ORDERBY X,Y')
+        self.assertEqual(tree.as_string(), 'Any X,Y ORDERBY X,Y')
+
+    def test_select_undefine_variable(self):
+        tree = sparse('Any X,Y ORDERBY X,Y WHERE X work_for Y')
+        tree.save_state()
+        select = tree.children[0]
+        select.undefine_variable(select.defined_vars['Y'])
+        self.assertEqual(select.solutions, [{'X': 'Person'},
+                                             {'X': 'Student'}])
+        tree.check_references()
+        self.assertEqual(tree.as_string(), 'Any X ORDERBY X')
+        tree.recover()
+        tree.check_references()
+        self.assertEqual(tree.as_string(), 'Any X,Y ORDERBY X,Y WHERE X work_for Y')
+        self.assertEqual(select.solutions, [{'X': 'Person', 'Y': 'Company'},
+                                             {'X': 'Student', 'Y': 'Company'}])
 
     def test_select_set_distinct(self):
         tree = self._parse('DISTINCT Any X')
         tree.save_state()
         select = tree.children[0]
-        self.assertEquals(select.distinct, True)
+        self.assertEqual(select.distinct, True)
         tree.save_state()
         select.set_distinct(True)
-        self.assertEquals(select.distinct, True)
+        self.assertEqual(select.distinct, True)
         tree.recover()
-        self.assertEquals(select.distinct, True)
+        self.assertEqual(select.distinct, True)
         select.set_distinct(False)
-        self.assertEquals(select.distinct, False)
+        self.assertEqual(select.distinct, False)
         tree.recover()
-        self.assertEquals(select.distinct, True)
+        self.assertEqual(select.distinct, True)
 
     def test_select_add_group_var(self):
         tree = self._parse('Any X')
@@ -212,10 +227,10 @@
         select = tree.children[0]
         select.add_group_var(select.get_variable('X'))
         tree.check_references()
-        self.assertEquals(tree.as_string(), 'Any X GROUPBY X')
+        self.assertEqual(tree.as_string(), 'Any X GROUPBY X')
         tree.recover()
         tree.check_references()
-        self.assertEquals(tree.as_string(), 'Any X')
+        self.assertEqual(tree.as_string(), 'Any X')
 
     def test_select_remove_group_var(self):
         tree = self._parse('Any X GROUPBY X')
@@ -223,10 +238,10 @@
         select = tree.children[0]
         select.remove_group_var(select.groupby[0])
         tree.check_references()
-        self.assertEquals(tree.as_string(), 'Any X')
+        self.assertEqual(tree.as_string(), 'Any X')
         tree.recover()
         tree.check_references()
-        self.assertEquals(tree.as_string(), 'Any X GROUPBY X')
+        self.assertEqual(tree.as_string(), 'Any X GROUPBY X')
 
     def test_select_remove_groups(self):
         tree = self._parse('Any X,Y GROUPBY X,Y')
@@ -234,10 +249,10 @@
         select = tree.children[0]
         select.remove_groups()
         tree.check_references()
-        self.assertEquals(tree.as_string(), 'Any X,Y')
+        self.assertEqual(tree.as_string(), 'Any X,Y')
         tree.recover()
         tree.check_references()
-        self.assertEquals(tree.as_string(), 'Any X,Y GROUPBY X,Y')
+        self.assertEqual(tree.as_string(), 'Any X,Y GROUPBY X,Y')
 
     def test_select_base_1(self):
         tree = self._parse("Any X WHERE X is Person")
@@ -352,15 +367,34 @@
     def test_selected_index(self):
         tree = self._simpleparse("Any X ORDERBY N DESC WHERE X is Person, X name N")
         annotator.annotate(tree)
-        self.assertEquals(tree.defined_vars['X'].selected_index(), 0)
-        self.assertEquals(tree.defined_vars['N'].selected_index(), None)
+        self.assertEqual(tree.defined_vars['X'].selected_index(), 0)
+        self.assertEqual(tree.defined_vars['N'].selected_index(), None)
+
+    def test_get_variable_indices_1(self):
+        dummy = self._parse("Any A,B,C")
+        dummy.children[0].solutions = [{'A': 'String', 'B': 'EUser', 'C': 'EGroup'},
+                                       {'A': 'String', 'B': 'Personne', 'C': 'EGroup'},
+                                       {'A': 'String', 'B': 'EUser', 'C': 'Societe'}]
+        self.assertEqual(dummy.get_variable_indices(), set([1, 2]))
 
-    def test_get_variable_variables(self):
-        dummy = self._simpleparse("Any X")
-        dummy.solutions = [{'A': 'String', 'B': 'EUser', 'C': 'EGroup'},
-                           {'A': 'String', 'B': 'Personne', 'C': 'EGroup'},
-                           {'A': 'String', 'B': 'EUser', 'C': 'Societe'}]
-        self.assertEquals(dummy.get_variable_variables(), set(['B', 'C']))
+    def test_get_variable_indices_2(self):
+        dummy = self._parse("Any A,B WHERE B relation C")
+        dummy.children[0].solutions = [{'A': 'String', 'B': 'EUser', 'C': 'EGroup'},
+                                       {'A': 'String', 'B': 'Personne', 'C': 'EGroup'},
+                                       {'A': 'String', 'B': 'EUser', 'C': 'Societe'}]
+        self.assertEqual(dummy.get_variable_indices(), set([1]))
+
+    def test_get_variable_indices_3(self):
+        dummy = self._parse("(Any X WHERE X is EGroup) UNION (Any C WHERE C is EUser)")
+        dummy.children[0].solutions = [{'X': 'EGroup'}]
+        dummy.children[1].solutions = [{'C': 'EUser'}]
+        self.assertEqual(dummy.get_variable_indices(), set([0]))
+
+    def test_get_variable_indices_4(self):
+        dummy = self._parse("(Any X,XN WHERE X is EGroup, X name XN) UNION (Any C,CL WHERE C is EUser, C login CL)")
+        dummy.children[0].solutions = [{'X': 'EGroup', 'XN': 'String'}]
+        dummy.children[1].solutions = [{'C': 'EUser', 'CL': 'String'}]
+        self.assertEqual(dummy.get_variable_indices(), set([0]))
 
     # insertion tests #########################################################
 
@@ -439,40 +473,40 @@
 
     def test_as_string(self):
         tree = parse("SET X know Y WHERE X friend Y;")
-        self.assertEquals(tree.as_string(), 'SET X know Y WHERE X friend Y')
+        self.assertEqual(tree.as_string(), 'SET X know Y WHERE X friend Y')
 
         tree = parse("Person X")
-        self.assertEquals(tree.as_string(),
+        self.assertEqual(tree.as_string(),
                           'Any X WHERE X is Person')
 
         tree = parse(u"Any X WHERE X has_text 'hh'")
-        self.assertEquals(tree.as_string('utf8'),
+        self.assertEqual(tree.as_string('utf8'),
                           u'Any X WHERE X has_text "hh"'.encode('utf8'))
         tree = parse(u"Any X WHERE X has_text %(text)s")
-        self.assertEquals(tree.as_string('utf8', {'text': u'hh'}),
+        self.assertEqual(tree.as_string('utf8', {'text': u'hh'}),
                           u'Any X WHERE X has_text "hh"'.encode('utf8'))
         tree = parse(u"Any X WHERE X has_text %(text)s")
-        self.assertEquals(tree.as_string('utf8', {'text': u'h"h'}),
+        self.assertEqual(tree.as_string('utf8', {'text': u'h"h'}),
                           u'Any X WHERE X has_text "h\\"h"'.encode('utf8'))
         tree = parse(u"Any X WHERE X has_text %(text)s")
-        self.assertEquals(tree.as_string('utf8', {'text': u'h"\'h'}),
+        self.assertEqual(tree.as_string('utf8', {'text': u'h"\'h'}),
                           u'Any X WHERE X has_text "h\\"\'h"'.encode('utf8'))
 
     def test_as_string_no_encoding(self):
         tree = parse(u"Any X WHERE X has_text 'hh'")
-        self.assertEquals(tree.as_string(),
+        self.assertEqual(tree.as_string(),
                           u'Any X WHERE X has_text "hh"')
         tree = parse(u"Any X WHERE X has_text %(text)s")
-        self.assertEquals(tree.as_string(kwargs={'text': u'hh'}),
+        self.assertEqual(tree.as_string(kwargs={'text': u'hh'}),
                           u'Any X WHERE X has_text "hh"')
 
     def test_as_string_now_today_null(self):
         tree = parse(u"Any X WHERE X name NULL")
-        self.assertEquals(tree.as_string(), 'Any X WHERE X name NULL')
+        self.assertEqual(tree.as_string(), 'Any X WHERE X name NULL')
         tree = parse(u"Any X WHERE X creation_date NOW")
-        self.assertEquals(tree.as_string(), 'Any X WHERE X creation_date NOW')
+        self.assertEqual(tree.as_string(), 'Any X WHERE X creation_date NOW')
         tree = parse(u"Any X WHERE X creation_date TODAY")
-        self.assertEquals(tree.as_string(), 'Any X WHERE X creation_date TODAY')
+        self.assertEqual(tree.as_string(), 'Any X WHERE X creation_date TODAY')
 
     # sub-queries tests #######################################################
 
@@ -485,19 +519,19 @@
         select.recover()
         X = select.get_variable('X')
         N = select.get_variable('N')
-        self.assertEquals(len(X.references()), 3)
-        self.assertEquals(len(N.references()), 2)
+        self.assertEqual(len(X.references()), 3)
+        self.assertEqual(len(N.references()), 2)
         tree.schema = schema
         #annotator.annotate(tree)
         # XXX how to choose
-        self.assertEquals(X.get_type(), 'Company')
-        self.assertEquals(X.get_type({'X': 'Person'}), 'Person')
-        #self.assertEquals(N.get_type(), 'String')
-        self.assertEquals(X.get_description(0, lambda x:x), 'Company, Person, Student')
-        self.assertEquals(N.get_description(0, lambda x:x), 'firstname, name')
-        self.assertEquals(X.selected_index(), 0)
-        self.assertEquals(N.selected_index(), None)
-        self.assertEquals(X.main_relation(), None)
+        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.selected_index(), 0)
+        self.assertEqual(N.selected_index(), None)
+        self.assertEqual(X.main_relation(), None)
 
     # non regression tests ####################################################
 
@@ -535,33 +569,33 @@
     def test_known_values_1(self):
         tree = parse('Any X where X name "turlututu"').children[0]
         constants = tree.get_nodes(nodes.Constant)
-        self.assertEquals(len(constants), 1)
-        self.assertEquals(isinstance(constants[0], nodes.Constant), 1)
-        self.assertEquals(constants[0].value, 'turlututu')
+        self.assertEqual(len(constants), 1)
+        self.assertEqual(isinstance(constants[0], nodes.Constant), 1)
+        self.assertEqual(constants[0].value, 'turlututu')
 
     def test_known_values_2(self):
         tree = parse('Any X where X name "turlututu", Y know X, Y name "chapo pointu"').children[0]
         varrefs = tree.get_nodes(nodes.VariableRef)
-        self.assertEquals(len(varrefs), 5)
+        self.assertEqual(len(varrefs), 5)
         for varref in varrefs:
             self.assertIsInstance(varref, nodes.VariableRef)
-        self.assertEquals(sorted(x.name for x in varrefs),
+        self.assertEqual(sorted(x.name for x in varrefs),
                           ['X', 'X', 'X', 'Y', 'Y'])
 
     def test_iknown_values_1(self):
         tree = parse('Any X where X name "turlututu"').children[0]
         constants = list(tree.iget_nodes(nodes.Constant))
-        self.assertEquals(len(constants), 1)
-        self.assertEquals(isinstance(constants[0], nodes.Constant), 1)
-        self.assertEquals(constants[0].value, 'turlututu')
+        self.assertEqual(len(constants), 1)
+        self.assertEqual(isinstance(constants[0], nodes.Constant), 1)
+        self.assertEqual(constants[0].value, 'turlututu')
 
     def test_iknown_values_2(self):
         tree = parse('Any X where X name "turlututu", Y know X, Y name "chapo pointu"').children[0]
         varrefs = list(tree.iget_nodes(nodes.VariableRef))
-        self.assertEquals(len(varrefs), 5)
+        self.assertEqual(len(varrefs), 5)
         for varref in varrefs:
             self.assertIsInstance(varref, nodes.VariableRef)
-        self.assertEquals(sorted(x.name for x in varrefs),
+        self.assertEqual(sorted(x.name for x in varrefs),
                           ['X', 'X', 'X', 'Y', 'Y'])
 
 if __name__ == '__main__':
--- a/test/unittest_parser.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/test/unittest_parser.py	Wed May 11 09:02:33 2011 +0200
@@ -63,6 +63,7 @@
     'Any X WHERE X eid 53;',
     'Any X WHERE X eid -53;',
     "Document X WHERE X occurence_of F, F class C, C name 'Bande dessine', X owned_by U, U login 'syt', X available true;",
+    u"Document X WHERE X occurence_of F, F class C, C name 'Bande dessine', X owned_by U, U login 'syt', X available true;",
     "Personne P WHERE P travaille_pour S, S nom 'Eurocopter', P interesse_par T, T nom 'formation';",
     "Note N WHERE N ecrit_le D, D day > (today -10), N ecrit_par P, P nom 'jphc' or P nom 'ocy';",
     "Personne P WHERE (P interesse_par T, T nom 'formation') or (P ville 'Paris');",
@@ -116,7 +117,42 @@
     ' WITH T1,T2 BEING ('
     '      (Any X,N WHERE X name N, X transition_of E, E name %(name)s)'
     '       UNION '
-    '      (Any X,N WHERE X name N, X state_of E, E name %(name)s))',
+    '      (Any X,N WHERE X name N, X state_of E, E name %(name)s));',
+
+
+    'Any T2'
+    ' GROUPBY T2'
+    ' WHERE T1 relation T2'
+    ' HAVING COUNT(T1) IN (1,2);',
+
+    'Any T2'
+    ' GROUPBY T2'
+    ' WHERE T1 relation T2'
+    ' HAVING COUNT(T1) IN (1,2) OR COUNT(T1) IN (3,4);',
+
+    'Any T2'
+    ' GROUPBY T2'
+    ' WHERE T1 relation T2'
+    ' HAVING 1 < COUNT(T1) OR COUNT(T1) IN (3,4);',
+
+    'Any T2'
+    ' GROUPBY T2'
+    ' WHERE T1 relation T2'
+    ' HAVING (COUNT(T1) IN (1,2)) OR (COUNT(T1) IN (3,4));',
+
+    'Any T2'
+    ' GROUPBY T2'
+    ' WHERE T1 relation T2'
+    ' HAVING (1 < COUNT(T1) OR COUNT(T1) IN (3,4));',
+
+    'Any T2'
+    ' GROUPBY T2'
+    ' WHERE T1 relation T2'
+    ' HAVING 1+2 < COUNT(T1);',
+
+    '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";',
     )
 
 class ParserHercule(TestCase):
@@ -137,6 +173,14 @@
                 print string, ex
             raise
 
+    def test_unicode_constant(self):
+        tree = self.parse(u"Any X WHERE X name 'ngstrm';")
+        base = tree.children[0].where
+        comparison = base.children[1]
+        self.failUnless(isinstance(comparison, nodes.Comparison))
+        rhs = comparison.children[0]
+        self.assertEqual(type(rhs.value), unicode)
+
     def test_precedence_1(self):
         tree = self.parse("Any X WHERE X firstname 'lulu' AND X name 'toto' OR X name 'tutu';")
         base = tree.children[0].where
@@ -228,22 +272,22 @@
         tree = self.parse("Any X WHERE X firstname %(firstname)s;")
         cste = tree.children[0].where.children[1].children[0]
         self.assert_(isinstance(cste, nodes.Constant))
-        self.assertEquals(cste.type, 'Substitute')
-        self.assertEquals(cste.value, 'firstname')
+        self.assertEqual(cste.type, 'Substitute')
+        self.assertEqual(cste.value, 'firstname')
 
     def test_optional_relation(self):
         tree = self.parse(r'Any X WHERE X related Y;')
         related = tree.children[0].where
-        self.assertEquals(related.optional, None)
+        self.assertEqual(related.optional, None)
         tree = self.parse(r'Any X WHERE X? related Y;')
         related = tree.children[0].where
-        self.assertEquals(related.optional, 'left')
+        self.assertEqual(related.optional, 'left')
         tree = self.parse(r'Any X WHERE X related Y?;')
         related = tree.children[0].where
-        self.assertEquals(related.optional, 'right')
+        self.assertEqual(related.optional, 'right')
         tree = self.parse(r'Any X WHERE X? related Y?;')
         related = tree.children[0].where
-        self.assertEquals(related.optional, 'both')
+        self.assertEqual(related.optional, 'both')
 
     def test_exists(self):
         tree = self.parse("Any X WHERE X firstname 'lulu',"
@@ -257,9 +301,9 @@
 
     def test_etype(self):
         tree = self.parse('EmailAddress X;')
-        self.assertEquals(tree.as_string(), 'Any X WHERE X is EmailAddress')
+        self.assertEqual(tree.as_string(), 'Any X WHERE X is EmailAddress')
         tree = self.parse('EUser X;')
-        self.assertEquals(tree.as_string(), 'Any X WHERE X is EUser')
+        self.assertEqual(tree.as_string(), 'Any X WHERE X is EUser')
 
     def test_spec(self):
         """test all RQL string found in the specification and test they are well parsed"""
--- a/test/unittest_rqlgen.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/test/unittest_rqlgen.py	Wed May 11 09:02:33 2011 +0200
@@ -41,21 +41,21 @@
         """tests select with entity type only
         """
         rql = self.rql_generator.select('Person')
-        self.assertEquals(rql, 'Person X')
+        self.assertEqual(rql, 'Person X')
         
 
     def test_select_group(self):
         """tests select with group
         """
         rql = self.rql_generator.select('Person', groups=('X',))
-        self.assertEquals(rql, 'Person X\nGROUPBY X')
+        self.assertEqual(rql, 'Person X\nGROUPBY X')
 
 
     def test_select_sort(self):
         """tests select with sort
         """
         rql = self.rql_generator.select('Person', sorts=('X ASC',))
-        self.assertEquals(rql, 'Person X\nSORTBY X ASC')
+        self.assertEqual(rql, 'Person X\nSORTBY X ASC')
 
 
     def test_select(self):
@@ -68,7 +68,7 @@
                                           ('X','surname','S') ),
                                         ('X',),
                                         ('F ASC', 'S DESC'))
-        self.assertEquals(rql, 'Person X\nWHERE X work_for S , S name "Logilab"'
+        self.assertEqual(rql, 'Person X\nWHERE X work_for S , S name "Logilab"'
                           ' , X firstname F , X surname S\nGROUPBY X'
                           '\nSORTBY F ASC, S DESC')
                                         
@@ -80,7 +80,7 @@
                                          ('S','name','"Logilab"'),
                                          ('X','firstname','F'),
                                          ('X','surname','S') ) )
-        self.assertEquals(rql, 'WHERE X work_for S , S name "Logilab" '
+        self.assertEqual(rql, 'WHERE X work_for S , S name "Logilab" '
                           ', X firstname F , X surname S')
 
 
@@ -88,14 +88,14 @@
         """tests the groupby() method behaviour
         """
         rql = self.rql_generator.groupby(('F', 'S'))
-        self.assertEquals(rql, 'GROUPBY F, S')
+        self.assertEqual(rql, 'GROUPBY F, S')
         
 
     def test_sortby(self):
         """tests the sortby() method behaviour
         """
         rql = self.rql_generator.sortby(('F ASC', 'S DESC'))
-        self.assertEquals(rql, 'SORTBY F ASC, S DESC')
+        self.assertEqual(rql, 'SORTBY F ASC, S DESC')
         
 
     def test_insert(self):
@@ -103,7 +103,7 @@
         """
         rql = self.rql_generator.insert('Person', (('firstname', "Clark"),
                                                    ('lastname', "Kent")))
-        self.assertEquals(rql, 'INSERT Person X: X firstname "Clark",'
+        self.assertEqual(rql, 'INSERT Person X: X firstname "Clark",'
                           ' X lastname "Kent"')
         
         
@@ -115,7 +115,7 @@
                                          ('lastname', "Kent")),
                                         (('job', "superhero"),
                                          ('nick', "superman")))
-        self.assertEquals(rql, 'SET X job "superhero", X nick "superman" '
+        self.assertEqual(rql, 'SET X job "superhero", X nick "superman" '
                           'WHERE X is "Person", X firstname "Clark", X '
                           'lastname "Kent"')
 
@@ -126,7 +126,7 @@
         rql = self.rql_generator.delete('Person',
                                         (('firstname', "Clark"),
                                          ('lastname', "Kent")))
-        self.assertEquals(rql, 'DELETE Person X where X firstname "Clark", '
+        self.assertEqual(rql, 'DELETE Person X where X firstname "Clark", '
                           'X lastname "Kent"')
         
 if __name__ == '__main__':
--- a/test/unittest_stcheck.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/test/unittest_stcheck.py	Wed May 11 09:02:33 2011 +0200
@@ -16,8 +16,10 @@
 # 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 logilab.common.testlib import TestCase, unittest_main
+
+from rql import RQLHelper, BadRQLQuery, stmts, nodes
+
 from unittest_analyze import DummySchema
-from rql import RQLHelper, BadRQLQuery, stmts, nodes
 
 BAD_QUERIES = (
     'Any X, Y GROUPBY X',
@@ -50,10 +52,17 @@
 
     'Any X WHERE X name "Toto", P is Person',
 
-    # BAD QUERY cant sort on y
+    "Any X WHERE X eid 0, X eid 1",
+
+    # DISTINCT+ORDERBY tests ###################################################
+    # cant sort on Y, B <- work_for X is multivalued
     'DISTINCT Any X ORDERBY Y WHERE B work_for X, B name Y',
-
-    "Any X WHERE X eid 0, X eid 1"
+    # cant sort on PN, there may be different PF values for the same PN value
+    # XXX untrue if PF or PN is marked as unique
+    'DISTINCT Any PF ORDERBY PN WHERE P firstname PF, P name PN',
+    # cant sort on XN, there may be different PF values for the same PF value
+    'DISTINCT Any PF ORDERBY X WHERE P work_for X, P firstname PF',
+    'DISTINCT Any PF ORDERBY XN WHERE P work_for X, P firstname PF, X name XN',
 
     )
 
@@ -64,12 +73,12 @@
 
     'DISTINCT Any X, MAX(Y) GROUPBY X WHERE X is Person, Y is Company',
 
+    # DISTINCT+ORDERBY tests ###################################################
     # sorting allowed since order variable reachable from a selected
     # variable with only ?1 cardinality
-    'DISTINCT Any B ORDERBY Y WHERE B work_for X, B name Y',
-    'DISTINCT Any B ORDERBY Y WHERE B work_for X, X name Y',
+    'DISTINCT Any P ORDERBY PN WHERE P work_for X, P name PN',
+    'DISTINCT Any P ORDERBY XN WHERE P work_for X, X name XN',
 
-#    'DISTINCT Any X ORDERBY SN WHERE X in_state S, S name SN',
 
 
     )
@@ -100,7 +109,7 @@
     def _test_rewrite(self, rql, expected):
         rqlst = self.parse(rql)
         self.simplify(rqlst)
-        self.assertEquals(rqlst.as_string(), expected)
+        self.assertEqual(rqlst.as_string(), expected)
 
     def test_rewrite(self):
         for rql, expected in (
@@ -168,6 +177,12 @@
             # A eid 12 can be removed since the type analyzer checked its existence
             ('Any X WHERE A eid 12, X connait Y',
              'Any X WHERE X connait Y'),
+
+            ('Any X WHERE EXISTS(X work_for Y, Y eid 12) OR X eid 12',
+             'Any X WHERE (EXISTS(X work_for 12)) OR (X eid 12)'),
+
+            ('Any X WHERE EXISTS(X work_for Y, Y eid IN (12)) OR X eid IN (12)',
+             'Any X WHERE (EXISTS(X work_for 12)) OR (X eid 12)'),
             ):
             yield self._test_rewrite, rql, expected
 
@@ -179,20 +194,20 @@
                             'VC work_for S, S name "draft" '
                             'WITH VF, VC, VCD BEING (Any VF, MAX(VC), VCD GROUPBY VF, VCD '
                             '                        WHERE VC connait VF, VC creation_date VCD)'))
-        self.assertEquals(rqlst.children[0].vargraph,
+        self.assertEqual(rqlst.children[0].vargraph,
                           {'VCD': ['VC'], 'VF': ['VC'], 'S': ['VC'], 'VC': ['S', 'VF', 'VCD'],
                            ('VC', 'S'): 'work_for',
                            ('VC', 'VF'): 'connait',
                            ('VC', 'VCD'): 'creation_date'})
-        self.assertEquals(rqlst.children[0].aggregated, set(('VC',)))
+        self.assertEqual(rqlst.children[0].aggregated, set(('VC',)))
 
 
 ##     def test_rewriten_as_string(self):
 ##         rqlst = self.parse('Any X WHERE X eid 12')
-##         self.assertEquals(rqlst.as_string(), 'Any X WHERE X eid 12')
+##         self.assertEqual(rqlst.as_string(), 'Any X WHERE X eid 12')
 ##         rqlst = rqlst.copy()
 ##         self.annotate(rqlst)
-##         self.assertEquals(rqlst.as_string(), 'Any X WHERE X eid 12')
+##         self.assertEqual(rqlst.as_string(), 'Any X WHERE X eid 12')
 
 class CopyTest(TestCase):
 
@@ -215,7 +230,7 @@
         root = self.parse('Any X,U WHERE C owned_by U, NOT X owned_by U, X eid 1, C eid 2')
         self.simplify(root)
         stmt = root.children[0]
-        self.assertEquals(stmt.defined_vars['U'].valuable_references(), 3)
+        self.assertEqual(stmt.defined_vars['U'].valuable_references(), 3)
         copy = stmts.Select()
         copy.append_selected(stmt.selection[0].copy(copy))
         copy.append_selected(stmt.selection[1].copy(copy))
@@ -224,8 +239,8 @@
         newroot.append(copy)
         self.annotate(newroot)
         self.simplify(newroot)
-        self.assertEquals(newroot.as_string(), 'Any 1,U WHERE 2 owned_by U, NOT 1 owned_by U')
-        self.assertEquals(copy.defined_vars['U'].valuable_references(), 3)
+        self.assertEqual(newroot.as_string(), 'Any 1,U WHERE 2 owned_by U, NOT EXISTS(1 owned_by U)')
+        self.assertEqual(copy.defined_vars['U'].valuable_references(), 3)
 
 
 class AnnotateTest(TestCase):
@@ -239,27 +254,64 @@
 #         self.annotate(rqlst)
 #         self.failUnless(rqlst.defined_vars['L'].stinfo['attrvar'])
 
-    def test_is_rel_no_scope(self):
-        """is relation used as type restriction should not affect variable's scope,
-        and should not be included in stinfo['relations']"""
+    def test_is_rel_no_scope_1(self):
+        """is relation used as type restriction should not affect variable's
+        scope, and should not be included in stinfo['relations']
+        """
         rqlst = self.parse('Any X WHERE C is Company, EXISTS(X work_for C)').children[0]
         C = rqlst.defined_vars['C']
         self.failIf(C.scope is rqlst, C.scope)
-        self.assertEquals(len(C.stinfo['relations']), 1)
+        self.assertEqual(len(C.stinfo['relations']), 1)
+
+    def test_is_rel_no_scope_2(self):
         rqlst = self.parse('Any X, ET WHERE C is ET, EXISTS(X work_for C)').children[0]
         C = rqlst.defined_vars['C']
         self.failUnless(C.scope is rqlst, C.scope)
-        self.assertEquals(len(C.stinfo['relations']), 2)
+        self.assertEqual(len(C.stinfo['relations']), 2)
+
+
+    def test_not_rel_normalization_1(self):
+        rqlst = self.parse('Any X WHERE C is Company, NOT X work_for C').children[0]
+        self.assertEqual(rqlst.as_string(), 'Any X WHERE C is Company, NOT EXISTS(X work_for C)')
+        C = rqlst.defined_vars['C']
+        self.failIf(C.scope is rqlst, C.scope)
+
+    def test_not_rel_normalization_2(self):
+        rqlst = self.parse('Any X, ET WHERE C is ET, NOT X work_for C').children[0]
+        self.assertEqual(rqlst.as_string(), 'Any X,ET WHERE C is ET, NOT EXISTS(X work_for C)')
+        C = rqlst.defined_vars['C']
+        self.failUnless(C.scope is rqlst, C.scope)
 
-    def test_subquery_annotation(self):
+    def test_not_rel_normalization_3(self):
+        rqlst = self.parse('Any X WHERE C is Company, X work_for C, NOT C name "World Company"').children[0]
+        self.assertEqual(rqlst.as_string(), "Any X WHERE C is Company, X work_for C, NOT C name 'World Company'")
+        C = rqlst.defined_vars['C']
+        self.failUnless(C.scope is rqlst, C.scope)
+
+    def test_not_rel_normalization_4(self):
+        rqlst = self.parse('Any X WHERE C is Company, NOT (X work_for C, C name "World Company")').children[0]
+        self.assertEqual(rqlst.as_string(), "Any X WHERE C is Company, NOT EXISTS(X work_for C, C name 'World Company')")
+        C = rqlst.defined_vars['C']
+        self.failIf(C.scope is rqlst, C.scope)
+
+    def test_not_rel_normalization_5(self):
+        rqlst = self.parse('Any X WHERE X work_for C, EXISTS(C identity D, NOT Y work_for D, D name "World Company")').children[0]
+        self.assertEqual(rqlst.as_string(), "Any X WHERE X work_for C, EXISTS(C identity D, NOT EXISTS(Y work_for D), D name 'World Company')")
+        D = rqlst.defined_vars['D']
+        self.failIf(D.scope is rqlst, D.scope)
+        self.failUnless(D.scope.parent.scope is rqlst, D.scope.parent.scope)
+
+    def test_subquery_annotation_1(self):
         rqlst = self.parse('Any X WITH X BEING (Any X WHERE C is Company, EXISTS(X work_for C))').children[0]
         C = rqlst.with_[0].query.children[0].defined_vars['C']
         self.failIf(C.scope is rqlst, C.scope)
-        self.assertEquals(len(C.stinfo['relations']), 1)
+        self.assertEqual(len(C.stinfo['relations']), 1)
+
+    def test_subquery_annotation_2(self):
         rqlst = self.parse('Any X,ET WITH X,ET BEING (Any X, ET WHERE C is ET, EXISTS(X work_for C))').children[0]
         C = rqlst.with_[0].query.children[0].defined_vars['C']
         self.failUnless(C.scope is rqlst.with_[0].query.children[0], C.scope)
-        self.assertEquals(len(C.stinfo['relations']), 2)
+        self.assertEqual(len(C.stinfo['relations']), 2)
 
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_utils.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/test/unittest_utils.py	Wed May 11 09:02:33 2011 +0200
@@ -55,19 +55,19 @@
 
     def test_rqlvar_maker(self):
         varlist = list(utils.rqlvar_maker(27))
-        self.assertEquals(varlist, list('ABCDEFGHIJKLMNOPQRSTUVWXYZ') + ['AA'])
+        self.assertEqual(varlist, list('ABCDEFGHIJKLMNOPQRSTUVWXYZ') + ['AA'])
         varlist = list(utils.rqlvar_maker(27*26+1))
-        self.assertEquals(varlist[-2], 'ZZ')
-        self.assertEquals(varlist[-1], 'AAA')
+        self.assertEqual(varlist[-2], 'ZZ')
+        self.assertEqual(varlist[-1], 'AAA')
 
     def test_rqlvar_maker_dontstop(self):
         varlist = utils.rqlvar_maker()
-        self.assertEquals(varlist.next(), 'A')
-        self.assertEquals(varlist.next(), 'B')
+        self.assertEqual(varlist.next(), 'A')
+        self.assertEqual(varlist.next(), 'B')
         for i in range(24):
             varlist.next()
-        self.assertEquals(varlist.next(), 'AA')
-        self.assertEquals(varlist.next(), 'AB')
+        self.assertEqual(varlist.next(), 'AA')
+        self.assertEqual(varlist.next(), 'AB')
 
         
 if __name__ == '__main__':
--- a/undo.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/undo.py	Wed May 11 09:02:33 2011 +0200
@@ -15,9 +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/>.
-"""Manages undos on RQL syntax trees.
+"""Manages undos on RQL syntax trees."""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from rql.nodes import VariableRef, Variable, BinaryNode
@@ -61,9 +60,11 @@
 
 class NodeOperation(object):
     """Abstract class for node manipulation operations."""
-    def __init__(self, node):
+    def __init__(self, node, stmt=None):
         self.node = node
-        self.stmt = node.stmt
+        if stmt is None:
+            stmt = node.stmt
+        self.stmt = stmt
 
     def __str__(self):
         """undo the operation on the selection"""
@@ -73,18 +74,21 @@
 
 class MakeVarOperation(NodeOperation):
     """Defines how to undo make_variable()."""
-
     def undo(self, selection):
         """undo the operation on the selection"""
         self.stmt.undefine_variable(self.node)
 
 class UndefineVarOperation(NodeOperation):
     """Defines how to undo undefine_variable()."""
+    def __init__(self, node, stmt, solutions):
+        NodeOperation.__init__(self, node, stmt)
+        self.solutions = solutions
 
     def undo(self, selection):
         """undo the operation on the selection"""
         var = self.node
         self.stmt.defined_vars[var.name] = var
+        self.stmt.solutions = self.solutions
 
 class SelectVarOperation(NodeOperation):
     """Defines how to undo add_selected()."""
@@ -135,45 +139,36 @@
 class RemoveNodeOperation(NodeOperation):
     """Defines how to undo remove_node()."""
 
-    def __init__(self, node):
-        NodeOperation.__init__(self, node)
-        self.node_parent = node.parent
-        if isinstance(self.node_parent, Select):
-            assert self.node is self.node_parent.where
-        else:
-            self.index = node.parent.children.index(node)
+    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
+        self.index = index
         # XXX FIXME : find a better way to do that
-        # needed when removing a BinaryNode's child
-        self.binary_remove = isinstance(self.node_parent, BinaryNode)
-        if self.binary_remove:
-            self.gd_parent = self.node_parent.parent
-            if isinstance(self.gd_parent, Select):
-                assert self.node_parent is self.gd_parent.where
-            else:
-                self.parent_index = self.gd_parent.children.index(self.node_parent)
+        self.binary_remove = isinstance(node, BinaryNode)
 
     def undo(self, selection):
         """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 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
             # reinsert this BinaryNode in its parent's children list
             # WARNING : the removed node sibling's parent is no longer the
             # 'node_parent'. We must Reparent it manually !
-            node_sibling = self.node_parent.children[0]
-            node_sibling.parent = self.node_parent
-            self.node_parent.insert(self.index, self.node)
-            if isinstance(self.gd_parent, Select):
-                self.gd_parent.where = self.node_parent
-            else:
-                self.gd_parent.children[self.parent_index] = self.node_parent
-                self.node_parent.parent = self.gd_parent
-        elif isinstance(self.node_parent, Select):
-            self.node_parent.where = self.node
-            self.node.parent = self.node_parent
-        else:
-            self.node_parent.insert(self.index, self.node)
+            if self.index is not None:
+                sibling = self.node_parent.children[self.index]
+                parent.children[self.index] = self.node
+            sibling.parent = self.node
+        elif self.index is not None:
+            parent.insert(self.index, self.node)
         # register reference from the removed node
+        self.node.parent = parent
         for varref in self.node.iget_nodes(VariableRef):
             varref.register_reference()
 
--- a/utils.py	Wed Apr 28 11:46:49 2010 +0200
+++ b/utils.py	Wed May 11 09:02:33 2011 +0200
@@ -15,11 +15,12 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with rql. If not, see <http://www.gnu.org/licenses/>.
-"""Miscellaneous utilities for RQL.
+"""Miscellaneous utilities for RQL."""
 
-"""
 __docformat__ = "restructuredtext en"
 
+from rql._exceptions import BadRQLQuery
+
 UPPERCASE = u'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
 def decompose_b26(index, table=UPPERCASE):
     """Return a letter (base-26) decomposition of index."""
@@ -78,6 +79,12 @@
         ', '.join(sorted(child.get_description(mainindex, tr)
                          for child in iter_funcnode_variables(funcnode))))
 
+@monkeypatch(FunctionDescr)
+def st_check_backend(self, backend, funcnode):
+    if not self.supports(backend):
+        raise BadRQLQuery("backend %s doesn't support function %s" % (backend, self.name))
+
+
 def iter_funcnode_variables(funcnode):
     for term in funcnode.children:
         try: