refactor so that rql rewriter may be used outside the server. Enhance it to be usable for RRQLExpression as well 3.5
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 16 Sep 2009 14:24:31 +0200
branch3.5
changeset 3240 8604a15995d1
parent 3239 1ceac4cd4fb7
child 3241 1a6f7a0e7dbd
refactor so that rql rewriter may be used outside the server. Enhance it to be usable for RRQLExpression as well
cwvreg.py
devtools/fake.py
devtools/repotest.py
goa/gaesource.py
goa/goactl.py
rqlrewrite.py
schema.py
server/querier.py
server/repository.py
server/rqlrewrite.py
server/session.py
server/sources/native.py
server/test/unittest_msplanner.py
server/test/unittest_rqlrewrite.py
test/data/rewrite/__init__.py
test/data/rewrite/bootstrap_cubes
test/data/rewrite/schema.py
test/unittest_rqlrewrite.py
--- a/cwvreg.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/cwvreg.py	Wed Sep 16 14:24:31 2009 +0200
@@ -291,8 +291,7 @@
 
     def set_schema(self, schema):
         """set instance'schema and load application objects"""
-        self.schema = schema
-        clear_cache(self, 'rqlhelper')
+        self._set_schema(schema)
         # now we can load application's web objects
         searchpath = self.config.vregistry_path()
         self.reset(searchpath, force_reload=False)
@@ -303,6 +302,11 @@
             etype = str(etype)
             self.case_insensitive_etypes[etype.lower()] = etype
 
+    def _set_schema(self, schema):
+        """set instance'schema"""
+        self.schema = schema
+        clear_cache(self, 'rqlhelper')
+
     def update_schema(self, schema):
         """update .schema attribute on registered objects, necessary for some
         tests
@@ -386,16 +390,7 @@
         # objects on automatic reloading
         self._needs_iface.clear()
 
-    def parse(self, session, rql, args=None):
-        rqlst = self.rqlhelper.parse(rql)
-        def type_from_eid(eid, session=session):
-            return session.describe(eid)[0]
-        try:
-            self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
-        except UnknownEid:
-            for select in rqlst.children:
-                select.solutions = []
-        return rqlst
+    # rql parsing utilities ####################################################
 
     @property
     @cached
@@ -403,38 +398,19 @@
         return RQLHelper(self.schema,
                          special_relations={'eid': 'uid', 'has_text': 'fti'})
 
-
-    @deprecated('use vreg["etypes"].etype_class(etype)')
-    def etype_class(self, etype):
-        return self["etypes"].etype_class(etype)
-
-    @deprecated('use vreg["views"].main_template(*args, **kwargs)')
-    def main_template(self, req, oid='main-template', **context):
-        return self["views"].main_template(req, oid, **context)
-
-    @deprecated('use vreg[registry].possible_vobjects(*args, **kwargs)')
-    def possible_vobjects(self, registry, *args, **kwargs):
-        return self[registry].possible_vobjects(*args, **kwargs)
+    def solutions(self, req, rqlst, args):
+        def type_from_eid(eid, req=req):
+            return req.describe(eid)[0]
+        self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
 
-    @deprecated('use vreg["actions"].possible_actions(*args, **kwargs)')
-    def possible_actions(self, req, rset=None, **kwargs):
-        return self["actions"].possible_actions(req, rest=rset, **kwargs)
-
-    @deprecated("use vreg['boxes'].select_object(...)")
-    def select_box(self, oid, *args, **kwargs):
-        return self['boxes'].select_object(oid, *args, **kwargs)
-
-    @deprecated("use vreg['components'].select_object(...)")
-    def select_component(self, cid, *args, **kwargs):
-        return self['components'].select_object(cid, *args, **kwargs)
-
-    @deprecated("use vreg['actions'].select_object(...)")
-    def select_action(self, oid, *args, **kwargs):
-        return self['actions'].select_object(oid, *args, **kwargs)
-
-    @deprecated("use vreg['views'].select(...)")
-    def select_view(self, __vid, req, rset=None, **kwargs):
-        return self['views'].select(__vid, req, rset=rset, **kwargs)
+    def parse(self, req, rql, args=None):
+        rqlst = self.rqlhelper.parse(rql)
+        try:
+            self.solutions(req, rqlst, args)
+        except UnknownEid:
+            for select in rqlst.children:
+                select.solutions = []
+        return rqlst
 
     # properties handling #####################################################
 
@@ -507,6 +483,40 @@
                 self.warning('%s (you should probably delete that property '
                              'from the database)', ex)
 
+    # deprecated code ####################################################
+
+    @deprecated('[3.4] use vreg["etypes"].etype_class(etype)')
+    def etype_class(self, etype):
+        return self["etypes"].etype_class(etype)
+
+    @deprecated('[3.4] use vreg["views"].main_template(*args, **kwargs)')
+    def main_template(self, req, oid='main-template', **context):
+        return self["views"].main_template(req, oid, **context)
+
+    @deprecated('[3.4] use vreg[registry].possible_vobjects(*args, **kwargs)')
+    def possible_vobjects(self, registry, *args, **kwargs):
+        return self[registry].possible_vobjects(*args, **kwargs)
+
+    @deprecated('[3.4] use vreg["actions"].possible_actions(*args, **kwargs)')
+    def possible_actions(self, req, rset=None, **kwargs):
+        return self["actions"].possible_actions(req, rest=rset, **kwargs)
+
+    @deprecated('[3.4] use vreg["boxes"].select_object(...)')
+    def select_box(self, oid, *args, **kwargs):
+        return self['boxes'].select_object(oid, *args, **kwargs)
+
+    @deprecated('[3.4] use vreg["components"].select_object(...)')
+    def select_component(self, cid, *args, **kwargs):
+        return self['components'].select_object(cid, *args, **kwargs)
+
+    @deprecated('[3.4] use vreg["actions"].select_object(...)')
+    def select_action(self, oid, *args, **kwargs):
+        return self['actions'].select_object(oid, *args, **kwargs)
+
+    @deprecated('[3.4] use vreg["views"].select(...)')
+    def select_view(self, __vid, req, rset=None, **kwargs):
+        return self['views'].select(__vid, req, rset=rset, **kwargs)
+
 
 from datetime import datetime, date, time, timedelta
 
--- a/devtools/fake.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/devtools/fake.py	Wed Sep 16 14:24:31 2009 +0200
@@ -13,6 +13,7 @@
 from indexer import get_indexer
 
 from cubicweb import RequestSessionMixIn
+from cubicweb.cwvreg import CubicWebVRegistry
 from cubicweb.web.request import CubicWebRequestBase
 from cubicweb.devtools import BASE_URL, BaseApptestConfiguration
 
@@ -67,7 +68,7 @@
 
     def __init__(self, *args, **kwargs):
         if not (args or 'vreg' in kwargs):
-            kwargs['vreg'] = FakeVReg()
+            kwargs['vreg'] = CubicWebVRegistry(FakeConfig(), initlog=False)
         kwargs['https'] = False
         self._url = kwargs.pop('url', 'view?rql=Blop&vid=blop')
         super(FakeRequest, self).__init__(*args, **kwargs)
@@ -177,7 +178,7 @@
 class FakeSession(RequestSessionMixIn):
     def __init__(self, repo=None, user=None):
         self.repo = repo
-        self.vreg = getattr(self.repo, 'vreg', FakeVReg())
+        self.vreg = getattr(self.repo, 'vreg', CubicWebVRegistry(FakeConfig(), initlog=False))
         self.pool = FakePool()
         self.user = user or FakeUser()
         self.is_internal_session = False
@@ -210,8 +211,9 @@
         self.eids = {}
         self._count = 0
         self.schema = schema
-        self.vreg = vreg or FakeVReg()
         self.config = config or FakeConfig()
+        self.vreg = vreg or CubicWebVRegistry(self.config, initlog=False)
+        self.vreg.schema = schema
 
     def internal_session(self):
         return FakeSession(self)
--- a/devtools/repotest.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/devtools/repotest.py	Wed Sep 16 14:24:31 2009 +0200
@@ -108,9 +108,10 @@
     schema = None # set this in concret test
 
     def setUp(self):
+        self.repo = FakeRepo(self.schema)
         self.rqlhelper = RQLHelper(self.schema, special_relations={'eid': 'uid',
                                                                    'has_text': 'fti'})
-        self.qhelper = QuerierHelper(FakeRepo(self.schema), self.schema)
+        self.qhelper = QuerierHelper(self.repo, self.schema)
         ExecutionPlan._check_permissions = _dummy_check_permissions
         rqlannotation._select_principal = _select_principal
 
@@ -129,7 +130,7 @@
         #print '********* solutions', solutions
         self.rqlhelper.simplify(union)
         #print '********* simplified', union.as_string()
-        plan = self.qhelper.plan_factory(union, {}, FakeSession())
+        plan = self.qhelper.plan_factory(union, {}, FakeSession(self.repo))
         plan.preprocess(union)
         for select in union.children:
             select.solutions.sort()
@@ -167,7 +168,7 @@
         set_debug(debug)
 
     def _rqlhelper(self):
-        rqlhelper = self.o._rqlhelper
+        rqlhelper = self.repo.vreg.rqlhelper
         # reset uid_func so it don't try to get type from eids
         rqlhelper._analyser.uid_func = None
         rqlhelper._analyser.uid_func_mapping = {}
@@ -241,7 +242,7 @@
         rqlst = self.o.parse(rql, annotate=True)
         self.o.solutions(self.session, rqlst, kwargs)
         if rqlst.TYPE == 'select':
-            self.o._rqlhelper.annotate(rqlst)
+            self.repo.vreg.rqlhelper.annotate(rqlst)
             for select in rqlst.children:
                 select.solutions.sort()
         else:
@@ -251,7 +252,7 @@
 
 # monkey patch some methods to get predicatable results #######################
 
-from cubicweb.server.rqlrewrite import RQLRewriter
+from cubicweb.rqlrewrite import RQLRewriter
 _orig_insert_snippets = RQLRewriter.insert_snippets
 _orig_build_variantes = RQLRewriter.build_variantes
 
--- a/goa/gaesource.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/goa/gaesource.py	Wed Sep 16 14:24:31 2009 +0200
@@ -149,7 +149,7 @@
     # ISource interface #######################################################
 
     def compile_rql(self, rql):
-        rqlst = self.repo.querier._rqlhelper.parse(rql)
+        rqlst = self.repo.vreg.parse(rql)
         rqlst.restricted_vars = ()
         rqlst.children[0].solutions = self._sols
         return rqlst
--- a/goa/goactl.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/goa/goactl.py	Wed Sep 16 14:24:31 2009 +0200
@@ -54,6 +54,7 @@
     'cwconfig.py',
     'entity.py',
     'interfaces.py',
+    'rqlrewrite.py',
     'rset.py',
     'schema.py',
     'schemaviewer.py',
@@ -78,7 +79,6 @@
     'server/pool.py',
     'server/querier.py',
     'server/repository.py',
-    'server/rqlrewrite.py',
     'server/securityhooks.py',
     'server/session.py',
     'server/serverconfig.py',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rqlrewrite.py	Wed Sep 16 14:24:31 2009 +0200
@@ -0,0 +1,480 @@
+"""RQL rewriting utilities : insert rql expression snippets into rql syntax
+tree.
+
+This is used for instance for read security checking in the repository.
+
+:organization: Logilab
+:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from rql import nodes as n, stmts, TypeResolverException
+
+from logilab.common.compat import any
+
+from cubicweb import Unauthorized, server, typed_eid
+from cubicweb.server.ssplanner import add_types_restriction
+
+
+def remove_solutions(origsolutions, solutions, defined):
+    """when a rqlst has been generated from another by introducing security
+    assertions, this method returns solutions which are contained in orig
+    solutions
+    """
+    newsolutions = []
+    for origsol in origsolutions:
+        for newsol in solutions[:]:
+            for var, etype in origsol.items():
+                try:
+                    if newsol[var] != etype:
+                        try:
+                            defined[var].stinfo['possibletypes'].remove(newsol[var])
+                        except KeyError:
+                            pass
+                        break
+                except KeyError:
+                    # variable has been rewritten
+                    continue
+            else:
+                newsolutions.append(newsol)
+                solutions.remove(newsol)
+    return newsolutions
+
+
+class Unsupported(Exception): pass
+
+
+class RQLRewriter(object):
+    """insert some rql snippets into another rql syntax tree
+
+    this class *isn't thread safe*
+    """
+
+    def __init__(self, session):
+        self.session = session
+        vreg = session.vreg
+        self.schema = vreg.schema
+        self.annotate = vreg.rqlhelper.annotate
+        self._compute_solutions = vreg.solutions
+
+    def compute_solutions(self):
+        self.annotate(self.select)
+        try:
+            self._compute_solutions(self.session, self.select, self.kwargs)
+        except TypeResolverException:
+            raise Unsupported(str(self.select))
+        if len(self.select.solutions) < len(self.solutions):
+            raise Unsupported()
+
+    def rewrite(self, select, snippets, solutions, kwargs):
+        """
+        snippets: (varmap, list of rql expression)
+                  with varmap a *tuple* (select var, snippet var)
+        """
+        if server.DEBUG:
+            print '---- rewrite', select, snippets, solutions
+        self.select = self.insert_scope = select
+        self.solutions = solutions
+        self.kwargs = kwargs
+        self.u_varname = None
+        self.removing_ambiguity = False
+        self.exists_snippet = {}
+        self.pending_keys = []
+        # we have to annotate the rqlst before inserting snippets, even though
+        # we'll have to redo it latter
+        self.annotate(select)
+        self.insert_snippets(snippets)
+        if not self.exists_snippet and self.u_varname:
+            # U has been inserted than cancelled, cleanup
+            select.undefine_variable(select.defined_vars[self.u_varname])
+        # clean solutions according to initial solutions
+        newsolutions = remove_solutions(solutions, select.solutions,
+                                        select.defined_vars)
+        assert len(newsolutions) >= len(solutions), (
+            'rewritten rql %s has lost some solutions, there is probably '
+            'something wrong in your schema permission (for instance using a '
+            'RQLExpression which insert a relation which doesn\'t exists in '
+            'the schema)\nOrig solutions: %s\nnew solutions: %s' % (
+            select, solutions, newsolutions))
+        if len(newsolutions) > len(solutions):
+            newsolutions = self.remove_ambiguities(snippets, newsolutions)
+        select.solutions = newsolutions
+        add_types_restriction(self.schema, select)
+        if server.DEBUG:
+            print '---- rewriten', select
+
+    def insert_snippets(self, snippets, varexistsmap=None):
+        self.rewritten = {}
+        for varmap, rqlexprs in snippets:
+            if varexistsmap is not None and not varmap in varexistsmap:
+                continue
+            self.varmap = varmap
+            selectvar, snippetvar = varmap
+            assert snippetvar in 'SOX'
+            self.revvarmap = {snippetvar: selectvar}
+            self.varinfo = vi = {}
+            try:
+                vi['const'] = typed_eid(selectvar) # XXX gae
+                vi['rhs_rels'] = vi['lhs_rels'] = {}
+            except ValueError:
+                vi['stinfo'] = sti = self.select.defined_vars[selectvar].stinfo
+                if varexistsmap is None:
+                    vi['rhs_rels'] = dict( (r.r_type, r) for r in sti['rhsrelations'])
+                    vi['lhs_rels'] = dict( (r.r_type, r) for r in sti['relations']
+                                           if not r in sti['rhsrelations'])
+                else:
+                    vi['rhs_rels'] = vi['lhs_rels'] = {}
+            parent = None
+            inserted = False
+            for rqlexpr in rqlexprs:
+                self.current_expr = rqlexpr
+                if varexistsmap is None:
+                    try:
+                        new = self.insert_snippet(varmap, rqlexpr.snippet_rqlst, parent)
+                    except Unsupported:
+                        import traceback
+                        traceback.print_exc()
+                        continue
+                    inserted = True
+                    if new is not None:
+                        self.exists_snippet[rqlexpr] = new
+                    parent = parent or new
+                else:
+                    # called to reintroduce snippet due to ambiguity creation,
+                    # so skip snippets which are not introducing this ambiguity
+                    exists = varexistsmap[varmap]
+                    if self.exists_snippet[rqlexpr] is exists:
+                        self.insert_snippet(varmap, rqlexpr.snippet_rqlst, exists)
+            if varexistsmap is None and not inserted:
+                # no rql expression found matching rql solutions. User has no access right
+                raise Unauthorized(str((varmap, str(self.select), [expr.expression for expr in rqlexprs])))
+
+    def insert_snippet(self, varmap, snippetrqlst, parent=None):
+        new = snippetrqlst.where.accept(self)
+        if new is not None:
+            if self.varinfo.get('stinfo', {}).get('optrelations'):
+                assert parent is None
+                self.insert_scope = self.snippet_subquery(varmap, new)
+                self.insert_pending()
+                self.insert_scope = self.select
+                return
+            new = n.Exists(new)
+            if parent is None:
+                self.insert_scope.add_restriction(new)
+            else:
+                grandpa = parent.parent
+                or_ = n.Or(parent, new)
+                grandpa.replace(parent, or_)
+            if not self.removing_ambiguity:
+                try:
+                    self.compute_solutions()
+                except Unsupported:
+                    # some solutions have been lost, can't apply this rql expr
+                    if parent is None:
+                        self.select.remove_node(new, undefine=True)
+                    else:
+                        parent.parent.replace(or_, or_.children[0])
+                        self._cleanup_inserted(new)
+                    raise
+                else:
+                    self.insert_scope = new
+                    self.insert_pending()
+                    self.insert_scope = self.select
+            return new
+        self.insert_pending()
+
+    def insert_pending(self):
+        """pending_keys hold variable referenced by U has_<action>_permission X
+        relation.
+
+        Once the snippet introducing this has been inserted and solutions
+        recomputed, we have to insert snippet defined for <action> of entity
+        types taken by X
+        """
+        while self.pending_keys:
+            key, action = self.pending_keys.pop()
+            try:
+                varname = self.rewritten[key]
+            except KeyError:
+                try:
+                    varname = self.revvarmap[key[-1]]
+                except KeyError:
+                    # variable isn't used anywhere else, we can't insert security
+                    raise Unauthorized()
+            ptypes = self.select.defined_vars[varname].stinfo['possibletypes']
+            if len(ptypes) > 1:
+                # XXX dunno how to handle this
+                self.session.error(
+                    'cant check security of %s, ambigous type for %s in %s',
+                    self.select, varname, key[0]) # key[0] == the rql expression
+                raise Unauthorized()
+            etype = iter(ptypes).next()
+            eschema = self.schema.eschema(etype)
+            if not eschema.has_perm(self.session, action):
+                rqlexprs = eschema.get_rqlexprs(action)
+                if not rqlexprs:
+                    raise Unauthorised()
+                self.insert_snippets([((varname, 'X'), rqlexprs)])
+
+    def snippet_subquery(self, varmap, transformedsnippet):
+        """introduce the given snippet in a subquery"""
+        subselect = stmts.Select()
+        selectvar, snippetvar = varmap
+        subselect.append_selected(n.VariableRef(
+            subselect.get_variable(selectvar)))
+        aliases = [selectvar]
+        subselect.add_restriction(transformedsnippet.copy(subselect))
+        stinfo = self.varinfo['stinfo']
+        for rel in stinfo['relations']:
+            rschema = self.schema.rschema(rel.r_type)
+            if rschema.is_final() or (rschema.inlined and
+                                      not rel in stinfo['rhsrelations']):
+                self.select.remove_node(rel)
+                rel.children[0].name = selectvar
+                subselect.add_restriction(rel.copy(subselect))
+                for vref in rel.children[1].iget_nodes(n.VariableRef):
+                    subselect.append_selected(vref.copy(subselect))
+                    aliases.append(vref.name)
+        if self.u_varname:
+            # generate an identifier for the substitution
+            argname = subselect.allocate_varname()
+            while argname in self.kwargs:
+                argname = subselect.allocate_varname()
+            subselect.add_constant_restriction(subselect.get_variable(self.u_varname),
+                                               'eid', unicode(argname), 'Substitute')
+            self.kwargs[argname] = self.session.user.eid
+        add_types_restriction(self.schema, subselect, subselect,
+                              solutions=self.solutions)
+        myunion = stmts.Union()
+        myunion.append(subselect)
+        aliases = [n.VariableRef(self.select.get_variable(name, i))
+                   for i, name in enumerate(aliases)]
+        self.select.add_subquery(n.SubQuery(aliases, myunion), check=False)
+        self._cleanup_inserted(transformedsnippet)
+        try:
+            self.compute_solutions()
+        except Unsupported:
+            # some solutions have been lost, can't apply this rql expr
+            self.select.remove_subquery(new, undefine=True)
+            raise
+        return subselect
+
+    def remove_ambiguities(self, snippets, newsolutions):
+        # the snippet has introduced some ambiguities, we have to resolve them
+        # "manually"
+        variantes = self.build_variantes(newsolutions)
+        # insert "is" where necessary
+        varexistsmap = {}
+        self.removing_ambiguity = True
+        for (erqlexpr, varmap, oldvarname), etype in variantes[0].iteritems():
+            varname = self.rewritten[(erqlexpr, varmap, oldvarname)]
+            var = self.select.defined_vars[varname]
+            exists = var.references()[0].scope
+            exists.add_constant_restriction(var, 'is', etype, 'etype')
+            varexistsmap[varmap] = exists
+        # insert ORED exists where necessary
+        for variante in variantes[1:]:
+            self.insert_snippets(snippets, varexistsmap)
+            for key, etype in variante.iteritems():
+                varname = self.rewritten[key]
+                try:
+                    var = self.select.defined_vars[varname]
+                except KeyError:
+                    # not a newly inserted variable
+                    continue
+                exists = var.references()[0].scope
+                exists.add_constant_restriction(var, 'is', etype, 'etype')
+        # recompute solutions
+        #select.annotated = False # avoid assertion error
+        self.compute_solutions()
+        # clean solutions according to initial solutions
+        return remove_solutions(self.solutions, self.select.solutions,
+                                self.select.defined_vars)
+
+    def build_variantes(self, newsolutions):
+        variantes = set()
+        for sol in newsolutions:
+            variante = []
+            for key, newvar in self.rewritten.iteritems():
+                variante.append( (key, sol[newvar]) )
+            variantes.add(tuple(variante))
+        # rebuild variantes as dict
+        variantes = [dict(variante) for variante in variantes]
+        # remove variable which have always the same type
+        for key in self.rewritten:
+            it = iter(variantes)
+            etype = it.next()[key]
+            for variante in it:
+                if variante[key] != etype:
+                    break
+            else:
+                for variante in variantes:
+                    del variante[key]
+        return variantes
+
+    def _cleanup_inserted(self, node):
+        # cleanup inserted variable references
+        for vref in node.iget_nodes(n.VariableRef):
+            vref.unregister_reference()
+            if not vref.variable.stinfo['references']:
+                # no more references, undefine the variable
+                del self.select.defined_vars[vref.name]
+
+    def _may_be_shared(self, relation, target, searchedvarname):
+        """return True if the snippet relation can be skipped to use a relation
+        from the original query
+        """
+        # if cardinality is in '?1', we can ignore the relation and use variable
+        # from the original query
+        rschema = self.schema.rschema(relation.r_type)
+        if target == 'object':
+            cardindex = 0
+            ttypes_func = rschema.objects
+            rprop = rschema.rproperty
+        else: # target == 'subject':
+            cardindex = 1
+            ttypes_func = rschema.subjects
+            rprop = lambda x, y, z: rschema.rproperty(y, x, z)
+        for etype in self.varinfo['stinfo']['possibletypes']:
+            for ttype in ttypes_func(etype):
+                if rprop(etype, ttype, 'cardinality')[cardindex] in '+*':
+                    return False
+        return True
+
+    def _use_outer_term(self, snippet_varname, term):
+        key = (self.current_expr, self.varmap, snippet_varname)
+        if key in self.rewritten:
+            insertedvar = self.select.defined_vars.pop(self.rewritten[key])
+            for inserted_vref in insertedvar.references():
+                inserted_vref.parent.replace(inserted_vref, term.copy(self.select))
+        self.rewritten[key] = term.name
+
+    def _get_varname_or_term(self, vname):
+        if vname == 'U':
+            if self.u_varname is None:
+                select = self.select
+                self.u_varname = select.allocate_varname()
+                # generate an identifier for the substitution
+                argname = select.allocate_varname()
+                while argname in self.kwargs:
+                    argname = select.allocate_varname()
+                # insert "U eid %(u)s"
+                var = select.get_variable(self.u_varname)
+                select.add_constant_restriction(select.get_variable(self.u_varname),
+                                                'eid', unicode(argname), 'Substitute')
+                self.kwargs[argname] = self.session.user.eid
+            return self.u_varname
+        key = (self.current_expr, self.varmap, vname)
+        try:
+            return self.rewritten[key]
+        except KeyError:
+            self.rewritten[key] = newvname = self.select.allocate_varname()
+            return newvname
+
+    # visitor methods ##########################################################
+
+    def _visit_binary(self, node, cls):
+        newnode = cls()
+        for c in node.children:
+            new = c.accept(self)
+            if new is None:
+                continue
+            newnode.append(new)
+        if len(newnode.children) == 0:
+            return None
+        if len(newnode.children) == 1:
+            return newnode.children[0]
+        return newnode
+
+    def _visit_unary(self, node, cls):
+        newc = node.children[0].accept(self)
+        if newc is None:
+            return None
+        newnode = cls()
+        newnode.append(newc)
+        return newnode
+
+    def visit_and(self, node):
+        return self._visit_binary(node, n.And)
+
+    def visit_or(self, node):
+        return self._visit_binary(node, n.Or)
+
+    def visit_not(self, node):
+        return self._visit_unary(node, n.Not)
+
+    def visit_exists(self, node):
+        return self._visit_unary(node, n.Exists)
+
+    def visit_relation(self, node):
+        lhs, rhs = node.get_variable_parts()
+        if node.r_type in ('has_add_permission', 'has_update_permission',
+                           'has_delete_permission', 'has_read_permission'):
+            assert lhs.name == 'U'
+            action = node.r_type.split('_')[1]
+            key = (self.current_expr, self.varmap, rhs.name)
+            self.pending_keys.append( (key, action) )
+            return
+        if lhs.name in self.revvarmap:
+            # on lhs
+            # see if we can reuse this relation
+            rels = self.varinfo['lhs_rels']
+            if (node.r_type in rels and isinstance(rhs, n.VariableRef)
+                and rhs.name != 'U' and not rels[node.r_type].neged(strict=True)
+                and self._may_be_shared(node, 'object', lhs.name)):
+                # ok, can share variable
+                term = rels[node.r_type].children[1].children[0]
+                self._use_outer_term(rhs.name, term)
+                return
+        elif isinstance(rhs, n.VariableRef) and rhs.name in self.revvarmap and lhs.name != 'U':
+            # on rhs
+            # see if we can reuse this relation
+            rels = self.varinfo['rhs_rels']
+            if (node.r_type in rels and not rels[node.r_type].neged(strict=True)
+                and self._may_be_shared(node, 'subject', rhs.name)):
+                # ok, can share variable
+                term = rels[node.r_type].children[0]
+                self._use_outer_term(lhs.name, term)
+                return
+        rel = n.Relation(node.r_type, node.optional)
+        for c in node.children:
+            rel.append(c.accept(self))
+        return rel
+
+    def visit_comparison(self, node):
+        cmp_ = n.Comparison(node.operator)
+        for c in node.children:
+            cmp_.append(c.accept(self))
+        return cmp_
+
+    def visit_mathexpression(self, node):
+        cmp_ = n.MathExpression(node.operator)
+        for c in cmp.children:
+            cmp_.append(c.accept(self))
+        return cmp_
+
+    def visit_function(self, node):
+        """generate filter name for a function"""
+        function_ = n.Function(node.name)
+        for c in node.children:
+            function_.append(c.accept(self))
+        return function_
+
+    def visit_constant(self, node):
+        """generate filter name for a constant"""
+        return n.Constant(node.value, node.type)
+
+    def visit_variableref(self, node):
+        """get the sql name for a variable reference"""
+        if node.name in self.revvarmap:
+            if self.varinfo.get('const') is not None:
+                return n.Constant(self.varinfo['const'], 'Int') # XXX gae
+            return n.VariableRef(self.select.get_variable(
+                self.revvarmap[node.name]))
+        vname_or_term = self._get_varname_or_term(node.name)
+        if isinstance(vname_or_term, basestring):
+            return n.VariableRef(self.select.get_variable(vname_or_term))
+        # shared term
+        return vname_or_term.copy(self.select)
--- a/schema.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/schema.py	Wed Sep 16 14:24:31 2009 +0200
@@ -642,6 +642,8 @@
             if len(self.rqlst.defined_vars[mainvar].references()) <= 2:
                 _LOGGER.warn('You did not use the %s variable in your RQL '
                              'expression %s', mainvar, self)
+        # syntax tree used by read security (inserted in queries when necessary
+        self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
 
     def __str__(self):
         return self.full_rql
@@ -767,8 +769,6 @@
 class ERQLExpression(RQLExpression):
     def __init__(self, expression, mainvars=None, eid=None):
         RQLExpression.__init__(self, expression, mainvars or 'X', eid)
-        # syntax tree used by read security (inserted in queries when necessary
-        self.snippet_rqlst = parse(self.minimal_rql, print_errors=False).children[0]
 
     @property
     def full_rql(self):
--- a/server/querier.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/server/querier.py	Wed Sep 16 14:24:31 2009 +0200
@@ -138,8 +138,8 @@
         # various resource accesors
         self.querier = querier
         self.schema = querier.schema
-        self.rqlhelper = querier._rqlhelper
         self.sqlannotate = querier.sqlgen_annotate
+        self.rqlhelper = session.vreg.rqlhelper
 
     def annotate_rqlst(self):
         if not self.rqlst.annotated:
@@ -265,6 +265,8 @@
                     myrqlst = select.copy(solutions=lchecksolutions)
                     myunion.append(myrqlst)
                     # in-place rewrite + annotation / simplification
+                    lcheckdef = [((varmap, 'X'), rqlexprs)
+                                 for varmap, rqlexprs in lcheckdef]
                     rewrite(myrqlst, lcheckdef, lchecksolutions, self.args)
                     noinvariant.update(noinvariant_vars(restricted, myrqlst, nbtrees))
                 if () in localchecks:
@@ -524,37 +526,33 @@
 
     def set_schema(self, schema):
         self.schema = schema
+        repo = self._repo
         # rql parsing / analysing helper
-        self._rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid',
-                                                               'has_text': 'fti'})
-        self._rql_cache = Cache(self._repo.config['rql-cache-size'])
+        self.solutions = repo.vreg.solutions
+        self._rql_cache = Cache(repo.config['rql-cache-size'])
         self.cache_hit, self.cache_miss = 0, 0
         # rql planner
         # note: don't use repo.sources, may not be built yet, and also "admin"
         #       isn't an actual source
-        if len([uri for uri in self._repo.config.sources() if uri != 'admin']) < 2:
+        rqlhelper = repo.vreg.rqlhelper
+        self._parse = rqlhelper.parse
+        self._annotate = rqlhelper.annotate
+        if len([uri for uri in repo.config.sources() if uri != 'admin']) < 2:
             from cubicweb.server.ssplanner import SSPlanner
-            self._planner = SSPlanner(schema, self._rqlhelper)
+            self._planner = SSPlanner(schema, rqlhelper)
         else:
             from cubicweb.server.msplanner import MSPlanner
-            self._planner = MSPlanner(schema, self._rqlhelper)
+            self._planner = MSPlanner(schema, rqlhelper)
         # sql generation annotator
         self.sqlgen_annotate = SQLGenAnnotator(schema).annotate
 
     def parse(self, rql, annotate=False):
         """return a rql syntax tree for the given rql"""
         try:
-            return self._rqlhelper.parse(unicode(rql), annotate=annotate)
+            return self._parse(unicode(rql), annotate=annotate)
         except UnicodeError:
             raise RQLSyntaxError(rql)
 
-    def solutions(self, session, rqlst, args):
-        assert session is not None
-        def type_from_eid(eid, type_from_eid=self._repo.type_from_eid,
-                          session=session):
-            return type_from_eid(eid, session)
-        self._rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
-
     def plan_factory(self, rqlst, args, session):
         """create an execution plan for an INSERT RQL query"""
         if rqlst.TYPE == 'insert':
@@ -642,7 +640,7 @@
             # bother modifying it. This is not necessary on write queries since
             # a new syntax tree is built from them.
             rqlst = rqlst.copy()
-            self._rqlhelper.annotate(rqlst)
+            self._annotate(rqlst)
         # make an execution plan
         plan = self.plan_factory(rqlst, args, session)
         plan.cache_key = cachekey
--- a/server/repository.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/server/repository.py	Wed Sep 16 14:24:31 2009 +0200
@@ -149,6 +149,7 @@
         self._running_threads = []
         # initial schema, should be build or replaced latter
         self.schema = CubicWebSchema(config.appid)
+        self.vreg.schema = self.schema # until actual schema is loaded...
         # querier helper, need to be created after sources initialization
         self.querier = QuerierHelper(self, self.schema)
         # should we reindex in changes?
@@ -192,7 +193,6 @@
             config.bootstrap_cubes()
             self.set_bootstrap_schema(config.load_schema())
             # need to load the Any and CWUser entity types
-            self.vreg.schema = self.schema
             etdirectory = join(CW_SOFTWARE_ROOT, 'entities')
             self.vreg.init_registration([etdirectory])
             self.vreg.load_file(join(etdirectory, '__init__.py'),
@@ -246,15 +246,16 @@
         if rebuildinfered:
             schema.rebuild_infered_relations()
         self.info('set schema %s %#x', schema.name, id(schema))
-        self.debug(', '.join(sorted(str(e) for e in schema.entities())))
+        if resetvreg:
+            # full reload of all appobjects
+            self.vreg.reset()
+            self.vreg.set_schema(schema)
+        else:
+            self.vreg._set_schema(schema)
         self.querier.set_schema(schema)
         for source in self.sources:
             source.set_schema(schema)
         self.schema = schema
-        if resetvreg:
-            # full reload of all appobjects
-            self.vreg.reset()
-            self.vreg.set_schema(schema)
         self.reset_hooks()
 
     def reset_hooks(self):
--- a/server/rqlrewrite.py	Wed Sep 16 14:17:12 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,396 +0,0 @@
-"""RQL rewriting utilities, used for read security checking
-
-:organization: Logilab
-:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-
-from rql import nodes, stmts, TypeResolverException
-from cubicweb import Unauthorized, server, typed_eid
-from cubicweb.server.ssplanner import add_types_restriction
-
-def remove_solutions(origsolutions, solutions, defined):
-    """when a rqlst has been generated from another by introducing security
-    assertions, this method returns solutions which are contained in orig
-    solutions
-    """
-    newsolutions = []
-    for origsol in origsolutions:
-        for newsol in solutions[:]:
-            for var, etype in origsol.items():
-                try:
-                    if newsol[var] != etype:
-                        try:
-                            defined[var].stinfo['possibletypes'].remove(newsol[var])
-                        except KeyError:
-                            pass
-                        break
-                except KeyError:
-                    # variable has been rewritten
-                    continue
-            else:
-                newsolutions.append(newsol)
-                solutions.remove(newsol)
-    return newsolutions
-
-class Unsupported(Exception): pass
-
-class RQLRewriter(object):
-    """insert some rql snippets into another rql syntax tree"""
-    def __init__(self, querier, session):
-        self.session = session
-        self.annotate = querier._rqlhelper.annotate
-        self._compute_solutions = querier.solutions
-        self.schema = querier.schema
-
-    def compute_solutions(self):
-        self.annotate(self.select)
-        try:
-            self._compute_solutions(self.session, self.select, self.kwargs)
-        except TypeResolverException:
-            raise Unsupported()
-        if len(self.select.solutions) < len(self.solutions):
-            raise Unsupported()
-
-    def rewrite(self, select, snippets, solutions, kwargs):
-        if server.DEBUG:
-            print '---- rewrite', select, snippets, solutions
-        self.select = select
-        self.solutions = solutions
-        self.kwargs = kwargs
-        self.u_varname = None
-        self.removing_ambiguity = False
-        self.exists_snippet = {}
-        # we have to annotate the rqlst before inserting snippets, even though
-        # we'll have to redo it latter
-        self.annotate(select)
-        self.insert_snippets(snippets)
-        if not self.exists_snippet and self.u_varname:
-            # U has been inserted than cancelled, cleanup
-            select.undefine_variable(select.defined_vars[self.u_varname])
-        # clean solutions according to initial solutions
-        newsolutions = remove_solutions(solutions, select.solutions,
-                                        select.defined_vars)
-        assert len(newsolutions) >= len(solutions), \
-               'rewritten rql %s has lost some solutions, there is probably something '\
-               'wrong in your schema permission (for instance using a '\
-              'RQLExpression which insert a relation which doesn\'t exists in '\
-               'the schema)\nOrig solutions: %s\nnew solutions: %s' % (
-            select, solutions, newsolutions)
-        if len(newsolutions) > len(solutions):
-            # the snippet has introduced some ambiguities, we have to resolve them
-            # "manually"
-            variantes = self.build_variantes(newsolutions)
-            # insert "is" where necessary
-            varexistsmap = {}
-            self.removing_ambiguity = True
-            for (erqlexpr, mainvar, oldvarname), etype in variantes[0].iteritems():
-                varname = self.rewritten[(erqlexpr, mainvar, oldvarname)]
-                var = select.defined_vars[varname]
-                exists = var.references()[0].scope
-                exists.add_constant_restriction(var, 'is', etype, 'etype')
-                varexistsmap[mainvar] = exists
-            # insert ORED exists where necessary
-            for variante in variantes[1:]:
-                self.insert_snippets(snippets, varexistsmap)
-                for (erqlexpr, mainvar, oldvarname), etype in variante.iteritems():
-                    varname = self.rewritten[(erqlexpr, mainvar, oldvarname)]
-                    try:
-                        var = select.defined_vars[varname]
-                    except KeyError:
-                        # not a newly inserted variable
-                        continue
-                    exists = var.references()[0].scope
-                    exists.add_constant_restriction(var, 'is', etype, 'etype')
-            # recompute solutions
-            #select.annotated = False # avoid assertion error
-            self.compute_solutions()
-            # clean solutions according to initial solutions
-            newsolutions = remove_solutions(solutions, select.solutions,
-                                            select.defined_vars)
-        select.solutions = newsolutions
-        add_types_restriction(self.schema, select)
-        if server.DEBUG:
-            print '---- rewriten', select
-
-    def build_variantes(self, newsolutions):
-        variantes = set()
-        for sol in newsolutions:
-            variante = []
-            for (erqlexpr, mainvar, oldvar), newvar in self.rewritten.iteritems():
-                variante.append( ((erqlexpr, mainvar, oldvar), sol[newvar]) )
-            variantes.add(tuple(variante))
-        # rebuild variantes as dict
-        variantes = [dict(variante) for variante in variantes]
-        # remove variable which have always the same type
-        for erqlexpr, mainvar, oldvar in self.rewritten:
-            it = iter(variantes)
-            etype = it.next()[(erqlexpr, mainvar, oldvar)]
-            for variante in it:
-                if variante[(erqlexpr, mainvar, oldvar)] != etype:
-                    break
-            else:
-                for variante in variantes:
-                    del variante[(erqlexpr, mainvar, oldvar)]
-        return variantes
-
-    def insert_snippets(self, snippets, varexistsmap=None):
-        self.rewritten = {}
-        for varname, erqlexprs in snippets:
-            if varexistsmap is not None and not varname in varexistsmap:
-                continue
-            try:
-                self.const = typed_eid(varname)
-                self.varname = self.const
-                self.rhs_rels = self.lhs_rels = {}
-            except ValueError:
-                self.varname = varname
-                self.const = None
-                self.varstinfo = stinfo = self.select.defined_vars[varname].stinfo
-                if varexistsmap is None:
-                    self.rhs_rels = dict( (rel.r_type, rel) for rel in stinfo['rhsrelations'])
-                    self.lhs_rels = dict( (rel.r_type, rel) for rel in stinfo['relations']
-                                                  if not rel in stinfo['rhsrelations'])
-                else:
-                    self.rhs_rels = self.lhs_rels = {}
-            parent = None
-            inserted = False
-            for erqlexpr in erqlexprs:
-                self.current_expr = erqlexpr
-                if varexistsmap is None:
-                    try:
-                        new = self.insert_snippet(varname, erqlexpr.snippet_rqlst, parent)
-                    except Unsupported:
-                        continue
-                    inserted = True
-                    if new is not None:
-                        self.exists_snippet[erqlexpr] = new
-                    parent = parent or new
-                else:
-                    # called to reintroduce snippet due to ambiguity creation,
-                    # so skip snippets which are not introducing this ambiguity
-                    exists = varexistsmap[varname]
-                    if self.exists_snippet[erqlexpr] is exists:
-                        self.insert_snippet(varname, erqlexpr.snippet_rqlst, exists)
-            if varexistsmap is None and not inserted:
-                # no rql expression found matching rql solutions. User has no access right
-                raise Unauthorized()
-
-    def insert_snippet(self, varname, snippetrqlst, parent=None):
-        new = snippetrqlst.where.accept(self)
-        if new is not None:
-            try:
-                var = self.select.defined_vars[varname]
-            except KeyError:
-                # not a variable
-                pass
-            else:
-                if var.stinfo['optrelations']:
-                    # use a subquery
-                    subselect = stmts.Select()
-                    subselect.append_selected(nodes.VariableRef(subselect.get_variable(varname)))
-                    subselect.add_restriction(new.copy(subselect))
-                    aliases = [varname]
-                    for rel in var.stinfo['relations']:
-                        rschema = self.schema.rschema(rel.r_type)
-                        if rschema.is_final() or (rschema.inlined and not rel in var.stinfo['rhsrelations']):
-                            self.select.remove_node(rel)
-                            rel.children[0].name = varname
-                            subselect.add_restriction(rel.copy(subselect))
-                            for vref in rel.children[1].iget_nodes(nodes.VariableRef):
-                                subselect.append_selected(vref.copy(subselect))
-                                aliases.append(vref.name)
-                    if self.u_varname:
-                        # generate an identifier for the substitution
-                        argname = subselect.allocate_varname()
-                        while argname in self.kwargs:
-                            argname = subselect.allocate_varname()
-                        subselect.add_constant_restriction(subselect.get_variable(self.u_varname),
-                                                        'eid', unicode(argname), 'Substitute')
-                        self.kwargs[argname] = self.session.user.eid
-                    add_types_restriction(self.schema, subselect, subselect, solutions=self.solutions)
-                    assert parent is None
-                    myunion = stmts.Union()
-                    myunion.append(subselect)
-                    aliases = [nodes.VariableRef(self.select.get_variable(name, i))
-                               for i, name in enumerate(aliases)]
-                    self.select.add_subquery(nodes.SubQuery(aliases, myunion), check=False)
-                    self._cleanup_inserted(new)
-                    try:
-                        self.compute_solutions()
-                    except Unsupported:
-                        # some solutions have been lost, can't apply this rql expr
-                        self.select.remove_subquery(new, undefine=True)
-                        raise
-                    return
-            new = nodes.Exists(new)
-            if parent is None:
-                self.select.add_restriction(new)
-            else:
-                grandpa = parent.parent
-                or_ = nodes.Or(parent, new)
-                grandpa.replace(parent, or_)
-            if not self.removing_ambiguity:
-                try:
-                    self.compute_solutions()
-                except Unsupported:
-                    # some solutions have been lost, can't apply this rql expr
-                    if parent is None:
-                        self.select.remove_node(new, undefine=True)
-                    else:
-                        parent.parent.replace(or_, or_.children[0])
-                        self._cleanup_inserted(new)
-                    raise
-            return new
-
-    def _cleanup_inserted(self, node):
-        # cleanup inserted variable references
-        for vref in node.iget_nodes(nodes.VariableRef):
-            vref.unregister_reference()
-            if not vref.variable.stinfo['references']:
-                # no more references, undefine the variable
-                del self.select.defined_vars[vref.name]
-
-    def _visit_binary(self, node, cls):
-        newnode = cls()
-        for c in node.children:
-            new = c.accept(self)
-            if new is None:
-                continue
-            newnode.append(new)
-        if len(newnode.children) == 0:
-            return None
-        if len(newnode.children) == 1:
-            return newnode.children[0]
-        return newnode
-
-    def _visit_unary(self, node, cls):
-        newc = node.children[0].accept(self)
-        if newc is None:
-            return None
-        newnode = cls()
-        newnode.append(newc)
-        return newnode
-
-    def visit_and(self, et):
-        return self._visit_binary(et, nodes.And)
-
-    def visit_or(self, ou):
-        return self._visit_binary(ou, nodes.Or)
-
-    def visit_not(self, node):
-        return self._visit_unary(node, nodes.Not)
-
-    def visit_exists(self, node):
-        return self._visit_unary(node, nodes.Exists)
-
-    def visit_relation(self, relation):
-        lhs, rhs = relation.get_variable_parts()
-        if lhs.name == 'X':
-            # on lhs
-            # see if we can reuse this relation
-            if relation.r_type in self.lhs_rels and isinstance(rhs, nodes.VariableRef) and rhs.name != 'U':
-                if self._may_be_shared(relation, 'object'):
-                    # ok, can share variable
-                    term = self.lhs_rels[relation.r_type].children[1].children[0]
-                    self._use_outer_term(rhs.name, term)
-                    return
-        elif isinstance(rhs, nodes.VariableRef) and rhs.name == 'X' and lhs.name != 'U':
-            # on rhs
-            # see if we can reuse this relation
-            if relation.r_type in self.rhs_rels and self._may_be_shared(relation, 'subject'):
-                # ok, can share variable
-                term = self.rhs_rels[relation.r_type].children[0]
-                self._use_outer_term(lhs.name, term)
-                return
-        rel = nodes.Relation(relation.r_type, relation.optional)
-        for c in relation.children:
-            rel.append(c.accept(self))
-        return rel
-
-    def visit_comparison(self, cmp):
-        cmp_ = nodes.Comparison(cmp.operator)
-        for c in cmp.children:
-            cmp_.append(c.accept(self))
-        return cmp_
-
-    def visit_mathexpression(self, mexpr):
-        cmp_ = nodes.MathExpression(mexpr.operator)
-        for c in cmp.children:
-            cmp_.append(c.accept(self))
-        return cmp_
-
-    def visit_function(self, function):
-        """generate filter name for a function"""
-        function_ = nodes.Function(function.name)
-        for c in function.children:
-            function_.append(c.accept(self))
-        return function_
-
-    def visit_constant(self, constant):
-        """generate filter name for a constant"""
-        return nodes.Constant(constant.value, constant.type)
-
-    def visit_variableref(self, vref):
-        """get the sql name for a variable reference"""
-        if vref.name == 'X':
-            if self.const is not None:
-                return nodes.Constant(self.const, 'Int')
-            return nodes.VariableRef(self.select.get_variable(self.varname))
-        vname_or_term = self._get_varname_or_term(vref.name)
-        if isinstance(vname_or_term, basestring):
-            return nodes.VariableRef(self.select.get_variable(vname_or_term))
-        # shared term
-        return vname_or_term.copy(self.select)
-
-    def _may_be_shared(self, relation, target):
-        """return True if the snippet relation can be skipped to use a relation
-        from the original query
-        """
-        # if cardinality is in '?1', we can ignore the relation and use variable
-        # from the original query
-        rschema = self.schema.rschema(relation.r_type)
-        if target == 'object':
-            cardindex = 0
-            ttypes_func = rschema.objects
-            rprop = rschema.rproperty
-        else: # target == 'subject':
-            cardindex = 1
-            ttypes_func = rschema.subjects
-            rprop = lambda x, y, z: rschema.rproperty(y, x, z)
-        for etype in self.varstinfo['possibletypes']:
-            for ttype in ttypes_func(etype):
-                if rprop(etype, ttype, 'cardinality')[cardindex] in '+*':
-                    return False
-        return True
-
-    def _use_outer_term(self, snippet_varname, term):
-        key = (self.current_expr, self.varname, snippet_varname)
-        if key in self.rewritten:
-            insertedvar = self.select.defined_vars.pop(self.rewritten[key])
-            for inserted_vref in insertedvar.references():
-                inserted_vref.parent.replace(inserted_vref, term.copy(self.select))
-        self.rewritten[key] = term
-
-    def _get_varname_or_term(self, vname):
-        if vname == 'U':
-            if self.u_varname is None:
-                select = self.select
-                self.u_varname = select.allocate_varname()
-                # generate an identifier for the substitution
-                argname = select.allocate_varname()
-                while argname in self.kwargs:
-                    argname = select.allocate_varname()
-                # insert "U eid %(u)s"
-                var = select.get_variable(self.u_varname)
-                select.add_constant_restriction(select.get_variable(self.u_varname),
-                                                'eid', unicode(argname), 'Substitute')
-                self.kwargs[argname] = self.session.user.eid
-            return self.u_varname
-        key = (self.current_expr, self.varname, vname)
-        try:
-            return self.rewritten[key]
-        except KeyError:
-            self.rewritten[key] = newvname = self.select.allocate_varname()
-            return newvname
--- a/server/session.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/server/session.py	Wed Sep 16 14:24:31 2009 +0200
@@ -18,7 +18,7 @@
 from cubicweb import RequestSessionMixIn, Binary, UnknownEid
 from cubicweb.dbapi import ConnectionProperties
 from cubicweb.utils import make_uid
-from cubicweb.server.rqlrewrite import RQLRewriter
+from cubicweb.rqlrewrite import RQLRewriter
 
 ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
 
@@ -543,7 +543,7 @@
         try:
             return self._threaddata._rewriter
         except AttributeError:
-            self._threaddata._rewriter = RQLRewriter(self.repo.querier, self)
+            self._threaddata._rewriter = RQLRewriter(self)
             return self._threaddata._rewriter
 
     def build_description(self, rqlst, args, result):
--- a/server/sources/native.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/server/sources/native.py	Wed Sep 16 14:24:31 2009 +0200
@@ -231,7 +231,7 @@
     # ISource interface #######################################################
 
     def compile_rql(self, rql):
-        rqlst = self.repo.querier._rqlhelper.parse(rql)
+        rqlst = self.repo.vreg.rqlhelper.parse(rql)
         rqlst.restricted_vars = ()
         rqlst.children[0].solutions = self._sols
         self.repo.querier.sqlgen_annotate(rqlst)
--- a/server/test/unittest_msplanner.py	Wed Sep 16 14:17:12 2009 +0200
+++ b/server/test/unittest_msplanner.py	Wed Sep 16 14:24:31 2009 +0200
@@ -348,7 +348,7 @@
 
     def setUp(self):
         BaseMSPlannerTC.setUp(self)
-        self.planner = MSPlanner(self.o.schema, self.o._rqlhelper)
+        self.planner = MSPlanner(self.o.schema, self.repo.vreg.rqlhelper)
 
     _test = test_plan
 
@@ -1989,7 +1989,7 @@
         self.setup()
         self.add_source(FakeCardSource, 'cards')
         self.add_source(FakeCardSource, 'cards2')
-        self.planner = MSPlanner(self.o.schema, self.o._rqlhelper)
+        self.planner = MSPlanner(self.o.schema, self.repo.vreg.rqlhelper)
         assert repo.sources_by_uri['cards2'].support_relation('multisource_crossed_rel')
         assert 'multisource_crossed_rel' in repo.sources_by_uri['cards2'].cross_relations
         assert repo.sources_by_uri['cards'].support_relation('multisource_crossed_rel')
@@ -2142,7 +2142,7 @@
     def setUp(self):
         self.setup()
         self.add_source(FakeVCSSource, 'vcs')
-        self.planner = MSPlanner(self.o.schema, self.o._rqlhelper)
+        self.planner = MSPlanner(self.o.schema, self.repo.vreg.rqlhelper)
     _test = test_plan
 
     def test_multisource_inlined_rel_skipped(self):
--- a/server/test/unittest_rqlrewrite.py	Wed Sep 16 14:17:12 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,193 +0,0 @@
-"""
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-from logilab.common.testlib import unittest_main, TestCase
-from logilab.common.testlib import mock_object
-
-from rql import parse, nodes, RQLHelper
-
-from cubicweb import Unauthorized
-from cubicweb.server.rqlrewrite import RQLRewriter
-from cubicweb.devtools import repotest, TestServerConfiguration
-
-config = TestServerConfiguration('data')
-config.bootstrap_cubes()
-schema = config.load_schema()
-schema.add_relation_def(mock_object(subject='Card', name='in_state', object='State', cardinality='1*'))
-
-rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid',
-                                                 'has_text': 'fti'})
-
-def setup_module(*args):
-    repotest.do_monkey_patch()
-
-def teardown_module(*args):
-    repotest.undo_monkey_patch()
-
-def eid_func_map(eid):
-    return {1: 'CWUser',
-            2: 'Card'}[eid]
-
-def rewrite(rqlst, snippets_map, kwargs):
-    class FakeQuerier:
-        schema = schema
-        @staticmethod
-        def solutions(sqlcursor, mainrqlst, kwargs):
-            rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs)
-        class _rqlhelper:
-            @staticmethod
-            def annotate(rqlst):
-                rqlhelper.annotate(rqlst)
-            @staticmethod
-            def simplify(mainrqlst, needcopy=False):
-                rqlhelper.simplify(rqlst, needcopy)
-    rewriter = RQLRewriter(FakeQuerier, mock_object(user=(mock_object(eid=1))))
-    for v, snippets in snippets_map.items():
-        snippets_map[v] = [mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0],
-                                       expression='Any X WHERE '+snippet)
-                           for snippet in snippets]
-    rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs)
-    solutions = rqlst.children[0].solutions
-    rewriter.rewrite(rqlst.children[0], snippets_map.items(), solutions, kwargs)
-    test_vrefs(rqlst.children[0])
-    return rewriter.rewritten
-
-def test_vrefs(node):
-    vrefmap = {}
-    for vref in node.iget_nodes(nodes.VariableRef):
-        vrefmap.setdefault(vref.name, set()).add(vref)
-    for var in node.defined_vars.itervalues():
-        assert not (var.stinfo['references'] ^ vrefmap[var.name])
-        assert (var.stinfo['references'])
-
-class RQLRewriteTC(TestCase):
-    """a faire:
-
-    * optimisation: detecter les relations utilisees dans les rqlexpressions qui
-      sont presentes dans la requete de depart pour les reutiliser si possible
-
-    * "has_<ACTION>_permission" ?
-    """
-
-    def test_base_var(self):
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        rqlst = parse('Card C')
-        rewrite(rqlst, {'C': (card_constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any C WHERE C is Card, B eid %(D)s, "
-                             "EXISTS(C in_state A, B in_group E, F require_state A, "
-                             "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission)")
-
-    def test_multiple_var(self):
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        affaire_constraints = ('X ref LIKE "PUBLIC%"', 'U in_group G, G name "public"')
-        kwargs = {'u':2}
-        rqlst = parse('Any S WHERE S documented_by C, C eid %(u)s')
-        rewrite(rqlst, {'C': (card_constraint,), 'S': affaire_constraints},
-                kwargs)
-        self.assertTextEquals(rqlst.as_string(),
-                             "Any S WHERE S documented_by C, C eid %(u)s, B eid %(D)s, "
-                             "EXISTS(C in_state A, B in_group E, F require_state A, "
-                             "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission), "
-                             "(EXISTS(S ref LIKE 'PUBLIC%')) OR (EXISTS(B in_group G, G name 'public', G is CWGroup)), "
-                             "S is Affaire")
-        self.failUnless('D' in kwargs)
-
-    def test_or(self):
-        constraint = '(X identity U) OR (X in_state ST, CL identity U, CL in_state ST, ST name "subscribed")'
-        rqlst = parse('Any S WHERE S owned_by C, C eid %(u)s, S is in (CWUser, CWGroup)')
-        rewrite(rqlst, {'C': (constraint,)}, {'u':1})
-        self.failUnlessEqual(rqlst.as_string(),
-                             "Any S WHERE S owned_by C, C eid %(u)s, S is IN(CWUser, CWGroup), A eid %(B)s, "
-                             "EXISTS((C identity A) OR (C in_state D, E identity A, "
-                             "E in_state D, D name 'subscribed'), D is State, E is CWUser)")
-
-    def test_simplified_rqlst(self):
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        rqlst = parse('Any 2') # this is the simplified rql st for Any X WHERE X eid 12
-        rewrite(rqlst, {'2': (card_constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any 2 WHERE B eid %(C)s, "
-                             "EXISTS(2 in_state A, B in_group D, E require_state A, "
-                             "E name 'read', E require_group D, A is State, D is CWGroup, E is CWPermission)")
-
-    def test_optional_var(self):
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        rqlst = parse('Any A,C WHERE A documented_by C?')
-        rewrite(rqlst, {'C': (card_constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             "Any A,C WHERE A documented_by C?, A is Affaire "
-                             "WITH C BEING "
-                             "(Any C WHERE C in_state B, D in_group F, G require_state B, G name 'read', "
-                             "G require_group F, D eid %(A)s, C is Card)")
-        rqlst = parse('Any A,C,T WHERE A documented_by C?, C title T')
-        rewrite(rqlst, {'C': (card_constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             "Any A,C,T WHERE A documented_by C?, A is Affaire "
-                             "WITH C,T BEING "
-                             "(Any C,T WHERE C in_state B, D in_group F, G require_state B, G name 'read', "
-                             "G require_group F, C title T, D eid %(A)s, C is Card)")
-
-    def test_relation_optimization(self):
-        # since Card in_state State as monovalued cardinality, the in_state
-        # relation used in the rql expression can be ignored and S replaced by
-        # the variable from the incoming query
-        card_constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        rqlst = parse('Card C WHERE C in_state STATE')
-        rewrite(rqlst, {'C': (card_constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any C WHERE C in_state STATE, C is Card, A eid %(B)s, "
-                             "EXISTS(A in_group D, E require_state STATE, "
-                             "E name 'read', E require_group D, D is CWGroup, E is CWPermission), "
-                             "STATE is State")
-
-    def test_unsupported_constraint_1(self):
-        # CWUser doesn't have require_permission
-        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
-        rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U')
-        self.assertRaises(Unauthorized, rewrite, rqlst, {'T': (trinfo_constraint,)}, {})
-
-    def test_unsupported_constraint_2(self):
-        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
-        rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U')
-        rewrite(rqlst, {'T': (trinfo_constraint, 'X wf_info_for Y, Y in_group G, G name "managers"')}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any U,T WHERE U is CWUser, T wf_info_for U, "
-                             "EXISTS(U in_group B, B name 'managers', B is CWGroup), T is TrInfo")
-
-    def test_unsupported_constraint_3(self):
-        self.skip('raise unauthorized for now')
-        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
-        rqlst = parse('Any T WHERE T wf_info_for X')
-        rewrite(rqlst, {'T': (trinfo_constraint, 'X in_group G, G name "managers"')}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u'XXX dunno what should be generated')
-
-    def test_add_ambiguity_exists(self):
-        constraint = ('X concerne Y')
-        rqlst = parse('Affaire X')
-        rewrite(rqlst, {'X': (constraint,)}, {})
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any X WHERE X is Affaire, (((EXISTS(X concerne A, A is Division)) OR (EXISTS(X concerne D, D is SubDivision))) OR (EXISTS(X concerne C, C is Societe))) OR (EXISTS(X concerne B, B is Note))")
-
-    def test_add_ambiguity_outerjoin(self):
-        constraint = ('X concerne Y')
-        rqlst = parse('Any X,C WHERE X? documented_by C')
-        rewrite(rqlst, {'X': (constraint,)}, {})
-        # ambiguity are kept in the sub-query, no need to be resolved using OR
-        self.failUnlessEqual(rqlst.as_string(),
-                             u"Any X,C WHERE X? documented_by C, C is Card WITH X BEING (Any X WHERE X concerne A, X is Affaire)")
-
-
-
-if __name__ == '__main__':
-    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/rewrite/bootstrap_cubes	Wed Sep 16 14:24:31 2009 +0200
@@ -0,0 +1,1 @@
+card, person
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data/rewrite/schema.py	Wed Sep 16 14:24:31 2009 +0200
@@ -0,0 +1,42 @@
+from yams.buildobjs import EntityType, RelationDefinition, String, SubjectRelation
+from cubicweb.schema import ERQLExpression
+
+class Affaire(EntityType):
+    permissions = {
+        'read':   ('managers',
+                   ERQLExpression('X owned_by U'), ERQLExpression('X concerne S?, S owned_by U')),
+        'add':    ('managers', ERQLExpression('X concerne S, S owned_by U')),
+        'update': ('managers', 'owners', ERQLExpression('X in_state S, S name in ("pitetre", "en cours")')),
+        'delete': ('managers', 'owners', ERQLExpression('X concerne S, S owned_by U')),
+        }
+    ref = String(fulltextindexed=True, indexed=True,
+                 constraints=[SizeConstraint(16)])
+    documented_by = SubjectRelation('Card')
+    concerne = SubjectRelation(('Societe', 'Note'))
+
+
+class Societe(EntityType):
+    permissions = {
+        'read': ('managers', 'users', 'guests'),
+        'update': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
+        'delete': ('managers', 'owners', ERQLExpression('U login L, X nom L')),
+        'add': ('managers', 'users',)
+        }
+
+
+class Division(Societe):
+    __specializes_schema__ = True
+
+
+class Note(EntityType):
+    pass
+
+
+class require_permission(RelationDefinition):
+    subject = ('Card', 'Note', 'Person')
+    object = 'CWPermission'
+
+
+class require_state(RelationDefinition):
+    subject = 'CWPermission'
+    object = 'State'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_rqlrewrite.py	Wed Sep 16 14:24:31 2009 +0200
@@ -0,0 +1,193 @@
+"""
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+from logilab.common.testlib import unittest_main, TestCase
+from logilab.common.testlib import mock_object
+
+from rql import parse, nodes, RQLHelper
+
+from cubicweb import Unauthorized
+from cubicweb.rqlrewrite import RQLRewriter
+from cubicweb.devtools import repotest, TestServerConfiguration
+
+config = TestServerConfiguration('data/rewrite')
+config.bootstrap_cubes()
+schema = config.load_schema()
+schema.add_relation_def(mock_object(subject='Card', name='in_state', object='State', cardinality='1*'))
+
+rqlhelper = RQLHelper(schema, special_relations={'eid': 'uid',
+                                                 'has_text': 'fti'})
+
+def setup_module(*args):
+    repotest.do_monkey_patch()
+
+def teardown_module(*args):
+    repotest.undo_monkey_patch()
+
+def eid_func_map(eid):
+    return {1: 'CWUser',
+            2: 'Card'}[eid]
+
+def rewrite(rqlst, snippets_map, kwargs):
+    class FakeVReg:
+        schema = schema
+        @staticmethod
+        def solutions(sqlcursor, mainrqlst, kwargs):
+            rqlhelper.compute_solutions(rqlst, {'eid': eid_func_map}, kwargs=kwargs)
+        class rqlhelper:
+            @staticmethod
+            def annotate(rqlst):
+                rqlhelper.annotate(rqlst)
+            @staticmethod
+            def simplify(mainrqlst, needcopy=False):
+                rqlhelper.simplify(rqlst, needcopy)
+    rewriter = RQLRewriter(mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
+    for v, snippets in snippets_map.items():
+        snippets_map[v] = [mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0],
+                                       expression='Any X WHERE '+snippet)
+                           for snippet in snippets]
+    rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs)
+    solutions = rqlst.children[0].solutions
+    rewriter.rewrite(rqlst.children[0], snippets_map.items(), solutions, kwargs)
+    test_vrefs(rqlst.children[0])
+    return rewriter.rewritten
+
+def test_vrefs(node):
+    vrefmap = {}
+    for vref in node.iget_nodes(nodes.VariableRef):
+        vrefmap.setdefault(vref.name, set()).add(vref)
+    for var in node.defined_vars.itervalues():
+        assert not (var.stinfo['references'] ^ vrefmap[var.name])
+        assert (var.stinfo['references'])
+
+class RQLRewriteTC(TestCase):
+    """a faire:
+
+    * optimisation: detecter les relations utilisees dans les rqlexpressions qui
+      sont presentes dans la requete de depart pour les reutiliser si possible
+
+    * "has_<ACTION>_permission" ?
+    """
+
+    def test_base_var(self):
+        card_constraint = ('X in_state S, U in_group G, P require_state S,'
+                           'P name "read", P require_group G')
+        rqlst = parse('Card C')
+        rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any C WHERE C is Card, B eid %(D)s, "
+                             "EXISTS(C in_state A, B in_group E, F require_state A, "
+                             "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission)")
+
+    def test_multiple_var(self):
+        card_constraint = ('X in_state S, U in_group G, P require_state S,'
+                           'P name "read", P require_group G')
+        affaire_constraints = ('X ref LIKE "PUBLIC%"', 'U in_group G, G name "public"')
+        kwargs = {'u':2}
+        rqlst = parse('Any S WHERE S documented_by C, C eid %(u)s')
+        rewrite(rqlst, {('C', 'X'): (card_constraint,), ('S', 'X'): affaire_constraints},
+                kwargs)
+        self.assertTextEquals(rqlst.as_string(),
+                             "Any S WHERE S documented_by C, C eid %(u)s, B eid %(D)s, "
+                             "EXISTS(C in_state A, B in_group E, F require_state A, "
+                             "F name 'read', F require_group E, A is State, E is CWGroup, F is CWPermission), "
+                             "(EXISTS(S ref LIKE 'PUBLIC%')) OR (EXISTS(B in_group G, G name 'public', G is CWGroup)), "
+                             "S is Affaire")
+        self.failUnless('D' in kwargs)
+
+    def test_or(self):
+        constraint = '(X identity U) OR (X in_state ST, CL identity U, CL in_state ST, ST name "subscribed")'
+        rqlst = parse('Any S WHERE S owned_by C, C eid %(u)s, S is in (CWUser, CWGroup)')
+        rewrite(rqlst, {('C', 'X'): (constraint,)}, {'u':1})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any S WHERE S owned_by C, C eid %(u)s, S is IN(CWUser, CWGroup), A eid %(B)s, "
+                             "EXISTS((C identity A) OR (C in_state D, E identity A, "
+                             "E in_state D, D name 'subscribed'), D is State, E is CWUser)")
+
+    def test_simplified_rqlst(self):
+        card_constraint = ('X in_state S, U in_group G, P require_state S,'
+                           'P name "read", P require_group G')
+        rqlst = parse('Any 2') # this is the simplified rql st for Any X WHERE X eid 12
+        rewrite(rqlst, {('2', 'X'): (card_constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any 2 WHERE B eid %(C)s, "
+                             "EXISTS(2 in_state A, B in_group D, E require_state A, "
+                             "E name 'read', E require_group D, A is State, D is CWGroup, E is CWPermission)")
+
+    def test_optional_var(self):
+        card_constraint = ('X in_state S, U in_group G, P require_state S,'
+                           'P name "read", P require_group G')
+        rqlst = parse('Any A,C WHERE A documented_by C?')
+        rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any A,C WHERE A documented_by C?, A is Affaire "
+                             "WITH C BEING "
+                             "(Any C WHERE C in_state B, D in_group F, G require_state B, G name 'read', "
+                             "G require_group F, D eid %(A)s, C is Card)")
+        rqlst = parse('Any A,C,T WHERE A documented_by C?, C title T')
+        rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             "Any A,C,T WHERE A documented_by C?, A is Affaire "
+                             "WITH C,T BEING "
+                             "(Any C,T WHERE C in_state B, D in_group F, G require_state B, G name 'read', "
+                             "G require_group F, C title T, D eid %(A)s, C is Card)")
+
+    def test_relation_optimization(self):
+        # since Card in_state State as monovalued cardinality, the in_state
+        # relation used in the rql expression can be ignored and S replaced by
+        # the variable from the incoming query
+        card_constraint = ('X in_state S, U in_group G, P require_state S,'
+                           'P name "read", P require_group G')
+        rqlst = parse('Card C WHERE C in_state STATE')
+        rewrite(rqlst, {('C', 'X'): (card_constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any C WHERE C in_state STATE, C is Card, A eid %(B)s, "
+                             "EXISTS(A in_group D, E require_state STATE, "
+                             "E name 'read', E require_group D, D is CWGroup, E is CWPermission), "
+                             "STATE is State")
+
+    def test_unsupported_constraint_1(self):
+        # CWUser doesn't have require_permission
+        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
+        rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U')
+        self.assertRaises(Unauthorized, rewrite, rqlst, {('T', 'X'): (trinfo_constraint,)}, {})
+
+    def test_unsupported_constraint_2(self):
+        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
+        rqlst = parse('Any U,T WHERE U is CWUser, T wf_info_for U')
+        rewrite(rqlst, {('T', 'X'): (trinfo_constraint, 'X wf_info_for Y, Y in_group G, G name "managers"')}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any U,T WHERE U is CWUser, T wf_info_for U, "
+                             "EXISTS(U in_group B, B name 'managers', B is CWGroup), T is TrInfo")
+
+    def test_unsupported_constraint_3(self):
+        self.skip('raise unauthorized for now')
+        trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
+        rqlst = parse('Any T WHERE T wf_info_for X')
+        rewrite(rqlst, {('T', 'X'): (trinfo_constraint, 'X in_group G, G name "managers"')}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u'XXX dunno what should be generated')
+
+    def test_add_ambiguity_exists(self):
+        constraint = ('X concerne Y')
+        rqlst = parse('Affaire X')
+        rewrite(rqlst, {('X', 'X'): (constraint,)}, {})
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any X WHERE X is Affaire, ((EXISTS(X concerne A, A is Division)) OR (EXISTS(X concerne C, C is Societe))) OR (EXISTS(X concerne B, B is Note))")
+
+    def test_add_ambiguity_outerjoin(self):
+        constraint = ('X concerne Y')
+        rqlst = parse('Any X,C WHERE X? documented_by C')
+        rewrite(rqlst, {('X', 'X'): (constraint,)}, {})
+        # ambiguity are kept in the sub-query, no need to be resolved using OR
+        self.failUnlessEqual(rqlst.as_string(),
+                             u"Any X,C WHERE X? documented_by C, C is Card WITH X BEING (Any X WHERE X concerne A, X is Affaire)")
+
+
+
+if __name__ == '__main__':
+    unittest_main()