Merge with 3.25 branch
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 12 Sep 2017 09:49:30 +0200
changeset 12203 c615f945b38a
parent 12192 cf5d11ac79fb (diff)
parent 12200 457274b4e017 (current diff)
child 12206 cfd25da225c2
Merge with 3.25 branch
cubicweb/__pkginfo__.py
cubicweb/dataimport/massive_store.py
--- a/cubicweb/__pkginfo__.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/__pkginfo__.py	Tue Sep 12 09:49:30 2017 +0200
@@ -27,8 +27,8 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 25, 2)
-version = '.'.join(str(num) for num in numversion)
+numversion = (3, 26, 0)
+version = '.'.join(str(num) for num in numversion) + '.dev0'
 
 description = "a repository of entities / relations for knowledge management"
 author = "Logilab"
--- a/cubicweb/dataimport/importer.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/dataimport/importer.py	Tue Sep 12 09:49:30 2017 +0200
@@ -244,7 +244,7 @@
             if not rschema.final:
                 # .prepare() should drop other cases from the entity dict
                 assert rschema.inlined
-                if not entity_dict[rtype] in extid2eid:
+                if entity_dict[rtype] not in extid2eid:
                     return False
         # entity is ready, replace all relation's extid by eids
         for rtype in entity_dict:
@@ -253,6 +253,21 @@
                 entity_dict[rtype] = extid2eid[entity_dict[rtype]]
         return True
 
+    def why_not_ready(self, extid2eid):
+        """Return some text explaining why this ext entity is not ready.
+        """
+        assert self._schema, 'prepare() method should be called first on %s' % self
+        # as .prepare has been called, we know that .values only contains subject relation *type* as
+        # key (no more (rtype, role) tuple)
+        schema = self._schema
+        entity_dict = self.values
+        for rtype in entity_dict:
+            rschema = schema.rschema(rtype)
+            if not rschema.final:
+                if entity_dict[rtype] not in extid2eid:
+                    return u'inlined relation %s is not present (%s)' % (rtype, entity_dict[rtype])
+        raise AssertionError('this external entity seems actually ready for insertion')
+
 
 class ExtEntitiesImporter(object):
     """This class is responsible for importing externals entities, that is instances of
@@ -413,7 +428,8 @@
                     "missing data?"]
             for ext_entities in queue.values():
                 for ext_entity in ext_entities:
-                    msgs.append(str(ext_entity))
+                    msg = '{}: {}'.format(ext_entity, ext_entity.why_not_ready(self.extid2eid))
+                    msgs.append(msg)
             map(error, msgs)
             if self.raise_on_error:
                 raise Exception('\n'.join(msgs))
--- a/cubicweb/dataimport/massive_store.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/dataimport/massive_store.py	Tue Sep 12 09:49:30 2017 +0200
@@ -186,6 +186,19 @@
 
     def finish(self):
         """Remove temporary tables and columns."""
+        try:
+            self._finish()
+            self._cnx.commit()
+        except Exception:
+            self._cnx.rollback()
+            raise
+        finally:
+            # delete the meta data table
+            self.sql('DROP TABLE IF EXISTS cwmassive_initialized')
+            self.commit()
+
+    def _finish(self):
+        """Remove temporary tables and columns."""
         assert not self.slave_mode, 'finish method should only be called by the master store'
         self.logger.info("Start cleaning")
         # Get all the initialized etypes/rtypes
@@ -227,9 +240,6 @@
                     self._tmp_data_cleanup(tmp_tablename, rtype, uuid)
         # restore all deleted indexes and constraints
         self._dbh.restore_indexes_and_constraints()
-        # delete the meta data table
-        self.sql('DROP TABLE IF EXISTS cwmassive_initialized')
-        self.commit()
 
     def _insert_etype_metadata(self, etype, tmp_tablename):
         """Massive insertion of meta data for `etype`, with new entities in `tmp_tablename`.
--- a/cubicweb/dataimport/test/data-massimport/schema.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/dataimport/test/data-massimport/schema.py	Tue Sep 12 09:49:30 2017 +0200
@@ -48,7 +48,7 @@
     Entity type for timezone of geonames.
     See timeZones.txt
     """
-    code = String(maxsize=1024, indexed=True)
+    code = String(maxsize=1024, indexed=True, required=True)
     gmt = Float()
     dst = Float()
     raw_offset = Float()
--- a/cubicweb/dataimport/test/test_massive_store.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/dataimport/test/test_massive_store.py	Tue Sep 12 09:49:30 2017 +0200
@@ -16,6 +16,8 @@
 # with this program. If not, see <http://www.gnu.org/licenses/>.
 """Massive store test case"""
 
+import psycopg2
+
 from cubicweb.devtools import testlib, PostgresApptestConfiguration
 from cubicweb.devtools import startpgcluster, stoppgcluster
 from cubicweb.dataimport import ucsvreader, stores
@@ -113,7 +115,7 @@
     def test_massimport_etype_metadata(self):
         with self.admin_access.repo_cnx() as cnx:
             store = MassiveObjectStore(cnx)
-            timezone_eid = store.prepare_insert_entity('TimeZone')
+            timezone_eid = store.prepare_insert_entity('TimeZone', code=u'12')
             store.prepare_insert_entity('Location', timezone=timezone_eid)
             store.flush()
             store.commit()
@@ -264,6 +266,16 @@
             store.prepare_insert_entity('Location', name=u'toto')
             store.finish()
 
+    def test_delete_metatable_on_integrity_error(self):
+        with self.admin_access.repo_cnx() as cnx:
+            store = MassiveObjectStore(cnx)
+            store.prepare_insert_entity('TimeZone')
+            store.flush()
+            store.commit()
+            with self.assertRaises(psycopg2.IntegrityError):
+                store.finish()
+            self.assertNotIn('cwmassive_initialized', set(self.get_db_descr(cnx)))
+
 
 if __name__ == '__main__':
     import unittest
--- a/cubicweb/dataimport/test/test_sqlgenstore.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/dataimport/test/test_sqlgenstore.py	Tue Sep 12 09:49:30 2017 +0200
@@ -16,8 +16,6 @@
 # with this program. If not, see <http://www.gnu.org/licenses/>.
 """SQL object store test case"""
 
-import itertools
-
 from cubicweb.dataimport import ucsvreader
 from cubicweb.devtools import testlib, PostgresApptestConfiguration
 from cubicweb.devtools import startpgcluster, stoppgcluster
@@ -48,7 +46,7 @@
         for code, gmt, dst, raw_offset in ucsvreader(open(self.datapath('timeZones.txt'), 'rb'),
                                                      delimiter='\t'):
             cnx.create_entity('TimeZone', code=code, gmt=float(gmt),
-                                    dst=float(dst), raw_offset=float(raw_offset))
+                              dst=float(dst), raw_offset=float(raw_offset))
         timezone_code = dict(cnx.execute('Any C, X WHERE X is TimeZone, X code C'))
         cnx.commit()
         # Push data
@@ -70,12 +68,12 @@
                       'alternatenames': infos[3],
                       'latitude': latitude, 'longitude': longitude,
                       'feature_class': feature_class,
-                      'alternate_country_code':infos[9],
+                      'alternate_country_code': infos[9],
                       'admin_code_3': infos[12],
                       'admin_code_4': infos[13],
                       'population': population, 'elevation': elevation,
                       'gtopo30': gtopo, 'timezone': timezone_code.get(infos[17]),
-                      'cwuri':  u'http://sws.geonames.org/%s/' % int(infos[0]),
+                      'cwuri': u'http://sws.geonames.org/%s/' % int(infos[0]),
                       'geonameid': int(infos[0]),
                       }
             store.prepare_insert_entity('Location', **entity)
@@ -98,7 +96,7 @@
     def test_sqlgenstore_etype_metadata(self):
         with self.admin_access.repo_cnx() as cnx:
             store = SQLGenObjectStore(cnx)
-            timezone_eid = store.prepare_insert_entity('TimeZone')
+            timezone_eid = store.prepare_insert_entity('TimeZone', code=u'12')
             store.prepare_insert_entity('Location', timezone=timezone_eid)
             store.flush()
             store.commit()
@@ -120,5 +118,5 @@
 
 
 if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
+    import unittest
+    unittest.main()
--- a/cubicweb/req.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/req.py	Tue Sep 12 09:49:30 2017 +0200
@@ -213,26 +213,30 @@
         >>> users = find('CWGroup', name=u"users").one()
         >>> groups = find('CWGroup').entities()
         """
-        parts = ['Any X WHERE X is %s' % etype]
+        parts = ['Any X WHERE X is {0}'.format(etype)]
         varmaker = rqlvar_maker(defined='X')
         eschema = self.vreg.schema.eschema(etype)
         for attr, value in kwargs.items():
             if isinstance(value, list) or isinstance(value, tuple):
-                raise NotImplementedError("List of values are not supported")
+                raise NotImplementedError(
+                    '{0}: list of values are not supported'.format(attr))
             if hasattr(value, 'eid'):
                 kwargs[attr] = value.eid
             if attr.startswith('reverse_'):
                 attr = attr[8:]
-                assert attr in eschema.objrels, \
-                    '%s not in %s object relations' % (attr, eschema)
-                parts.append(
-                    '%(varname)s %(attr)s X, '
-                    '%(varname)s eid %%(reverse_%(attr)s)s'
-                    % {'attr': attr, 'varname': next(varmaker)})
+                if attr not in eschema.objrels:
+                    raise KeyError('{0} not in {1} object relations'.format(attr, eschema))
+                parts.append('{var} {attr} X, {var} eid %(reverse_{attr})s'.format(
+                    var=next(varmaker), attr=attr))
             else:
-                assert attr in eschema.subjrels, \
-                    '%s not in %s subject relations' % (attr, eschema)
-                parts.append('X %(attr)s %%(%(attr)s)s' % {'attr': attr})
+                rel = eschema.subjrels.get(attr)
+                if rel is None:
+                    raise KeyError('{0} not in {1} subject relations'.format(attr, eschema))
+                if rel.final:
+                    parts.append('X {attr} %({attr})s'.format(attr=attr))
+                else:
+                    parts.append('X {attr} {var}, {var} eid %({attr})s'.format(
+                        attr=attr, var=next(varmaker)))
 
         rql = ', '.join(parts)
 
--- a/cubicweb/rqlrewrite.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/rqlrewrite.py	Tue Sep 12 09:49:30 2017 +0200
@@ -206,6 +206,19 @@
     return stinfo['relations'] - stinfo['rhsrelations']
 
 
+def need_exists(node):
+    """Return true if the given node should be wrapped in an `Exists` node.
+
+    This is true when node isn't already an `Exists` or `Not` node, nor a
+    `And`/`Or` of `Exists` or `Not` nodes.
+    """
+    if isinstance(node, (n.Exists, n.Not)):
+        return False
+    if isinstance(node, (n.Or, n.And)):
+        return need_exists(node.children[0]) or need_exists(node.children[1])
+    return True
+
+
 class Unsupported(Exception):
     """raised when an rql expression can't be inserted in some rql query
     because it create an unresolvable query (eg no solutions found)
@@ -474,7 +487,7 @@
             self.existingvars = existing
 
     def _inserted_root(self, new):
-        if not isinstance(new, (n.Exists, n.Not)):
+        if need_exists(new):
             new = n.Exists(new)
         return new
 
@@ -649,6 +662,10 @@
         # the snippet has introduced some ambiguities, we have to resolve them
         # "manually"
         variantes = self.build_variantes(newsolutions)
+        # if all ambiguities have been generated by variables within a "NOT
+        # EXISTS()#" or with type explicitly specified, we've nothing to change
+        if not variantes:
+            return newsolutions
         # insert "is" where necessary
         varexistsmap = {}
         self.removing_ambiguity = True
@@ -680,21 +697,32 @@
         variantes = set()
         for sol in newsolutions:
             variante = []
-            for key, newvar in self.rewritten.items():
-                variante.append((key, sol[newvar]))
-            variantes.add(tuple(variante))
-        # rebuild variantes as dict
-        variantes = [dict(v) for v in variantes]
-        # remove variable which have always the same type
-        for key in self.rewritten:
-            it = iter(variantes)
-            etype = next(it)[key]
-            for variante in it:
-                if variante[key] != etype:
-                    break
-            else:
-                for variante in variantes:
-                    del variante[key]
+            for key, var_name in self.rewritten.items():
+                var = self.select.defined_vars[var_name]
+                # skip variable which are only in a NOT EXISTS
+                if len(var.stinfo['relations']) == 1 and isinstance(var.scope.parent, n.Not):
+                    continue
+                # skip variable whose type is already explicitly specified
+                if var.stinfo['typerel']:
+                    continue
+                variante.append((key, sol[var_name]))
+            if variante:
+                variantes.add(tuple(variante))
+
+        if variantes:
+            # rebuild variantes as dict
+            variantes = [dict(v) for v in variantes]
+            # remove variable which have always the same type
+            for key in self.rewritten:
+                it = iter(variantes)
+                etype = next(it)[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):
--- a/cubicweb/server/querier.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/server/querier.py	Tue Sep 12 09:49:30 2017 +0200
@@ -293,7 +293,7 @@
                         if rqlexpr.check(cnx, eid):
                             break
                     else:
-                        raise Unauthorized('No read acces on %r with eid %i.' % (var, eid))
+                        raise Unauthorized('No read access on %r with eid %i.' % (var, eid))
                 # mark variables protected by an rql expression
                 restricted_vars.update(localcheck)
                 # turn local check into a dict key
--- a/cubicweb/test/unittest_req.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/test/unittest_req.py	Tue Sep 12 09:49:30 2017 +0200
@@ -132,7 +132,10 @@
                 firstname=u'adrien',
                 in_group=req.find('CWGroup', name=u'users').one())
 
-            u = req.find('CWUser', login=u'cdevienne').one()
+            rset = req.find('CWUser', login=u'cdevienne')
+            self.assertEqual(rset.printable_rql(),
+                             'Any X WHERE X is CWUser, X login "cdevienne"')
+            u = rset.one()
             self.assertEqual(u.firstname, u"Christophe")
 
             users = list(req.find('CWUser').entities())
@@ -143,17 +146,26 @@
             self.assertEqual(len(groups), 1)
             self.assertEqual(groups[0].name, u'users')
 
-            users = req.find('CWUser', in_group=groups[0]).entities()
-            users = list(users)
+            rset = req.find('CWUser', in_group=groups[0])
+            self.assertEqual(rset.printable_rql(),
+                             'Any X WHERE X is CWUser, X in_group A, '
+                             'A eid {0}'.format(groups[0].eid))
+            users = list(rset.entities())
             self.assertEqual(len(users), 2)
 
-            with self.assertRaises(AssertionError):
+            with self.assertRaisesRegexp(
+                KeyError, "^'chapeau not in CWUser subject relations'$"
+            ):
                 req.find('CWUser', chapeau=u"melon")
 
-            with self.assertRaises(AssertionError):
+            with self.assertRaisesRegexp(
+                KeyError, "^'buddy not in CWUser object relations'$"
+            ):
                 req.find('CWUser', reverse_buddy=users[0])
 
-            with self.assertRaises(NotImplementedError):
+            with self.assertRaisesRegexp(
+                NotImplementedError, '^in_group: list of values are not supported$'
+            ):
                 req.find('CWUser', in_group=[1, 2])
 
 
--- a/cubicweb/test/unittest_rqlrewrite.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/test/unittest_rqlrewrite.py	Tue Sep 12 09:49:30 2017 +0200
@@ -18,15 +18,17 @@
 
 from six import string_types
 
-from logilab.common.testlib import unittest_main, TestCase
 from logilab.common.testlib import mock_object
+from logilab.common.decorators import monkeypatch
 from yams import BadSchemaDefinition
 from yams.buildobjs import RelationDefinition
 from rql import parse, nodes, RQLHelper
 
 from cubicweb import Unauthorized, rqlrewrite, devtools
+from cubicweb.rqlrewrite import RQLRewriter
 from cubicweb.schema import RRQLExpression, ERQLExpression
 from cubicweb.devtools import repotest
+from cubicweb.devtools.testlib import CubicWebTC, TestCase
 
 
 def setUpModule(*args):
@@ -40,46 +42,65 @@
                                                      'has_text': 'fti'})
     repotest.do_monkey_patch()
 
+
 def tearDownModule(*args):
     repotest.undo_monkey_patch()
     global rqlhelper, schema
     del rqlhelper, schema
 
+
 def eid_func_map(eid):
     return {1: 'CWUser',
             2: 'Card',
             3: 'Affaire'}[eid]
 
+
 def _prepare_rewriter(rewriter_cls, kwargs):
     class FakeVReg:
         schema = schema
+
         @staticmethod
         def solutions(sqlcursor, rqlst, 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)
+
     return rewriter_cls(mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
 
+
 def rewrite(rqlst, snippets_map, kwargs, existingvars=None):
     rewriter = _prepare_rewriter(rqlrewrite.RQLRewriter, kwargs)
+    # turn {(V1, V2): constraints} into [(varmap, constraints)]
     snippets = []
+    snippet_varmap = {}
     for v, exprs in sorted(snippets_map.items()):
-        rqlexprs = [isinstance(snippet, string_types)
-                    and mock_object(snippet_rqlst=parse(u'Any X WHERE '+snippet).children[0],
-                                    expression=u'Any X WHERE '+snippet)
-                    or snippet
-                    for snippet in exprs]
-        snippets.append((dict([v]), rqlexprs))
+        rqlexprs = []
+        varmap = dict([v])
+        for snippet in exprs:
+            # when the same snippet is impacting several variables, group them
+            # unless there is some conflicts on the snippet's variable name (we
+            # only want that for constraint on relations using both S and O)
+            if snippet in snippet_varmap and not (
+                    set(varmap.values()) & set(snippet_varmap[snippet].values())):
+                snippet_varmap[snippet].update(varmap)
+                continue
+            snippet_varmap[snippet] = varmap
+            if isinstance(snippet, string_types):
+                snippet = mock_object(snippet_rqlst=parse(u'Any X WHERE ' + snippet).children[0],
+                                      expression=u'Any X WHERE ' + snippet)
+            rqlexprs.append(snippet)
+        if rqlexprs:
+            snippets.append((varmap, rqlexprs))
+
     rqlhelper.compute_solutions(rqlst.children[0], {'eid': eid_func_map}, kwargs=kwargs)
     rewriter.rewrite(rqlst.children[0], snippets, kwargs, existingvars)
     check_vrefs(rqlst.children[0])
     return rewriter.rewritten
 
+
 def check_vrefs(node):
     vrefmaps = {}
     selects = []
@@ -88,14 +109,15 @@
         try:
             vrefmaps[stmt].setdefault(vref.name, set()).add(vref)
         except KeyError:
-            vrefmaps[stmt] = {vref.name: set( (vref,) )}
+            vrefmaps[stmt] = {vref.name: set((vref,))}
             selects.append(stmt)
     assert node in selects, (node, selects)
     for stmt in selects:
         for var in stmt.defined_vars.values():
             assert var.stinfo['references']
             vrefmap = vrefmaps[stmt]
-            assert not (var.stinfo['references'] ^ vrefmap[var.name]), (node.as_string(), var, var.stinfo['references'], vrefmap[var.name])
+            assert not (var.stinfo['references'] ^ vrefmap[var.name]), (
+                node.as_string(), var, var.stinfo['references'], vrefmap[var.name])
 
 
 class RQLRewriteTC(TestCase):
@@ -109,72 +131,81 @@
 
     def test_base_var(self):
         constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
+                      'P name "read", P require_group G')
         rqlst = parse(u'Card C')
         rewrite(rqlst, {('C', 'X'): (constraint,)}, {})
-        self.assertEqual(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)')
+        self.assertEqual(
+            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}
+        kwargs = {'u': 2}
         rqlst = parse(u'Any S WHERE S documented_by C, C eid %(u)s')
         rewrite(rqlst, {('C', 'X'): (card_constraint,), ('S', 'X'): affaire_constraints},
                 kwargs)
         self.assertMultiLineEqual(
             rqlst.as_string(),
             u'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')
+            '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.assertIn('D', kwargs)
 
     def test_or(self):
-        constraint = '(X identity U) OR (X in_state ST, CL identity U, CL in_state ST, ST name "subscribed")'
+        constraint = (
+            '(X identity U) OR '
+            '(X in_state ST, CL identity U, CL in_state ST, ST name "subscribed")'
+        )
         rqlst = parse(u'Any S WHERE S owned_by C, C eid %(u)s, S is in (CWUser, CWGroup)')
-        rewrite(rqlst, {('C', 'X'): (constraint,)}, {'u':1})
-        self.assertEqual(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)')
+        rewrite(rqlst, {('C', 'X'): (constraint,)}, {'u': 1})
+        self.assertEqual(
+            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):
         constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
-        rqlst = parse(u'Any 2') # this is the simplified rql st for Any X WHERE X eid 12
+                      'P name "read", P require_group G')
+        rqlst = parse(u'Any 2')  # this is the simplified rql st for Any X WHERE X eid 12
         rewrite(rqlst, {('2', 'X'): (constraint,)}, {})
-        self.assertEqual(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)')
+        self.assertEqual(
+            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_1(self):
         constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
+                      'P name "read", P require_group G')
         rqlst = parse(u'Any A,C WHERE A documented_by C?')
         rewrite(rqlst, {('C', 'X'): (constraint,)}, {})
-        self.assertEqual(rqlst.as_string(),
-                         u'Any A,C WHERE A documented_by C?, A is Affaire '
-                          'WITH C BEING '
-                          '(Any C WHERE EXISTS(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)')
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any A,C WHERE A documented_by C?, A is Affaire '
+            'WITH C BEING '
+            '(Any C WHERE EXISTS(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)')
 
     def test_optional_var_2(self):
         constraint = ('X in_state S, U in_group G, P require_state S,'
-                           'P name "read", P require_group G')
+                      'P name "read", P require_group G')
         rqlst = parse(u'Any A,C,T WHERE A documented_by C?, C title T')
         rewrite(rqlst, {('C', 'X'): (constraint,)}, {})
-        self.assertEqual(rqlst.as_string(),
-                         u'Any A,C,T WHERE A documented_by C?, A is Affaire '
-                          'WITH C,T BEING '
-                          '(Any C,T WHERE C title T, EXISTS(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)')
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any A,C,T WHERE A documented_by C?, A is Affaire '
+            'WITH C,T BEING '
+            '(Any C,T WHERE C title T, EXISTS(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)')
 
     def test_optional_var_3(self):
         constraint1 = ('X in_state S, U in_group G, P require_state S,'
@@ -182,12 +213,14 @@
         constraint2 = 'X in_state S, S name "public"'
         rqlst = parse(u'Any A,C,T WHERE A documented_by C?, C title T')
         rewrite(rqlst, {('C', 'X'): (constraint1, constraint2)}, {})
-        self.assertEqual(rqlst.as_string(),
-                         u'Any A,C,T WHERE A documented_by C?, A is Affaire '
-                          'WITH C,T BEING (Any C,T WHERE C title T, '
-                          '(EXISTS(C in_state B, D in_group F, G require_state B, G name "read", G require_group F)) '
-                          'OR (EXISTS(C in_state E, E name "public")), '
-                          'D eid %(A)s, C is Card)')
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any A,C,T WHERE A documented_by C?, A is Affaire '
+            'WITH C,T BEING (Any C,T WHERE C title T, '
+            '(EXISTS(C in_state B, D in_group F, G require_state B, '
+            'G name "read", G require_group F)) '
+            'OR (EXISTS(C in_state E, E name "public")), '
+            'D eid %(A)s, C is Card)')
 
     def test_optional_var_4(self):
         constraint1 = 'A created_by U, X documented_by A'
@@ -197,29 +230,38 @@
         rewrite(rqlst, {('LA', 'X'): (constraint1, constraint2),
                         ('X', 'X'): (constraint3,),
                         ('Y', 'X'): (constraint3,)}, {})
-        self.assertEqual(rqlst.as_string(),
-                             u'Any X,LA,Y WHERE LA? documented_by X, LA concerne Y, B eid %(C)s, '
-                             'EXISTS(X created_by B), EXISTS(Y created_by B), '
-                             'X is Card, Y is IN(Division, Note, Societe) '
-                             'WITH LA BEING (Any LA WHERE (EXISTS(A created_by B, LA documented_by A)) OR (EXISTS(E created_by B, LA concerne E)), '
-                             'B eid %(D)s, LA is Affaire)')
-
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any X,LA,Y WHERE LA? documented_by X, LA concerne Y, B eid %(C)s, '
+            'EXISTS(X created_by B), EXISTS(Y created_by B), '
+            'X is Card, Y is IN(Division, Note, Societe) '
+            'WITH LA BEING (Any LA WHERE (EXISTS(A created_by B, LA documented_by A)) '
+            'OR (EXISTS(E created_by B, LA concerne E)), '
+            'B eid %(D)s, LA is Affaire)')
 
     def test_ambiguous_optional_same_exprs(self):
         """See #3013535"""
         # see test of the same name in RewriteFullTC: original problem is
         # unreproducible here because it actually lies in
         # RQLRewriter.insert_local_checks
-        rqlst = parse(u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, A eid %(a)s, X creation_date CD')
-        rewrite(rqlst, {('X', 'X'): ('X created_by U',),}, {'a': 3})
-        self.assertEqual(rqlst.as_string(),
-                         u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, A eid %(a)s WITH X,CD BEING (Any X,CD WHERE X creation_date CD, EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))')
+        rqlst = parse(u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, '
+                      'A eid %(a)s, X creation_date CD')
+        rewrite(rqlst, {('X', 'X'): ('X created_by U',)}, {'a': 3})
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any A,AR,X,CD WHERE A concerne X?, A ref AR, A eid %(a)s '
+            'WITH X,CD BEING (Any X,CD WHERE X creation_date CD, '
+            'EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))')
 
     def test_ambiguous_optional_same_exprs_constant(self):
-        rqlst = parse(u'Any A,AR,X WHERE A concerne X?, A ref AR, A eid %(a)s, X creation_date TODAY')
-        rewrite(rqlst, {('X', 'X'): ('X created_by U',),}, {'a': 3})
-        self.assertEqual(rqlst.as_string(),
-                         u'Any A,AR,X WHERE A concerne X?, A ref AR, A eid %(a)s WITH X BEING (Any X WHERE X creation_date TODAY, EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))')
+        rqlst = parse(u'Any A,AR,X WHERE A concerne X?, A ref AR, '
+                      'A eid %(a)s, X creation_date TODAY')
+        rewrite(rqlst, {('X', 'X'): ('X created_by U',)}, {'a': 3})
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any A,AR,X WHERE A concerne X?, A ref AR, A eid %(a)s '
+            'WITH X BEING (Any X WHERE X creation_date TODAY, '
+            'EXISTS(X created_by B), B eid %(A)s, X is IN(Division, Note, Societe))')
 
     def test_optional_var_inlined(self):
         c1 = ('X require_permission P')
@@ -229,12 +271,13 @@
                         ('A', 'X'): (c2,),
                         }, {})
         # XXX suboptimal
-        self.assertEqual(rqlst.as_string(),
-                         "Any C,A,R WITH A,C,R BEING "
-                         "(Any A,C,R WHERE A? inlined_card C, A ref R, "
-                         "(A is NULL) OR (EXISTS(A inlined_card B, B require_permission D, "
-                         "B is Card, D is CWPermission)), "
-                         "A is Affaire, C is Card, EXISTS(C require_permission E, E is CWPermission))")
+        self.assertEqual(
+            rqlst.as_string(),
+            "Any C,A,R WITH A,C,R BEING "
+            "(Any A,C,R WHERE A? inlined_card C, A ref R, "
+            "(A is NULL) OR (EXISTS(A inlined_card B, B require_permission D, "
+            "B is Card, D is CWPermission)), "
+            "A is Affaire, C is Card, EXISTS(C require_permission E, E is CWPermission))")
 
     # def test_optional_var_inlined_has_perm(self):
     #     c1 = ('X require_permission P')
@@ -249,7 +292,8 @@
     def test_optional_var_inlined_imbricated_error(self):
         c1 = ('X require_permission P')
         c2 = ('X inlined_card O, O require_permission P')
-        rqlst = parse(u'Any C,A,R,A2,R2 WHERE A? inlined_card C, A ref R,A2? inlined_card C, A2 ref R2')
+        rqlst = parse(u'Any C,A,R,A2,R2 WHERE A? inlined_card C, A ref R,'
+                      'A2? inlined_card C, A2 ref R2')
         self.assertRaises(BadSchemaDefinition,
                           rewrite, rqlst, {('C', 'X'): (c1,),
                                            ('A', 'X'): (c2,),
@@ -258,7 +302,6 @@
 
     def test_optional_var_inlined_linked(self):
         c1 = ('X require_permission P')
-        c2 = ('X inlined_card O, O require_permission P')
         rqlst = parse(u'Any A,W WHERE A inlined_card C?, C inlined_note N, '
                       'N inlined_affaire W')
         rewrite(rqlst, {('C', 'X'): (c1,)}, {})
@@ -295,6 +338,7 @@
         self.assertEqual(rqlst.as_string(),
                          'Any C WHERE C in_state STATE?, C is Card, '
                          'EXISTS(STATE name "hop"), STATE is State')
+
     def test_relation_optimization_2_rhs(self):
         snippet = ('TW? subworkflow_exit X, TW name "hop"')
         rqlst = parse(u'SubWorkflowExitPoint EXIT WHERE C? subworkflow_exit EXIT')
@@ -362,10 +406,11 @@
     def test_unsupported_constraint_2(self):
         trinfo_constraint = ('X wf_info_for Y, Y require_permission P, P name "read"')
         rqlst = parse(u'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"')}, {})
+        rewrite(rqlst, {('T', 'X'): (trinfo_constraint,
+                                     'X wf_info_for Y, Y in_group G, G name "managers"')}, {})
         self.assertEqual(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')
+                         u'EXISTS(U in_group B, B name "managers", B is CWGroup), T is TrInfo')
 
     def test_unsupported_constraint_3(self):
         self.skipTest('raise unauthorized for now')
@@ -379,17 +424,20 @@
         constraint = ('X concerne Y')
         rqlst = parse(u'Affaire X')
         rewrite(rqlst, {('X', 'X'): (constraint,)}, {})
-        self.assertEqual(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))")
+        self.assertEqual(
+            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(u'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.assertEqual(rqlst.as_string(),
-                         u"Any X,C WHERE X? documented_by C, C is Card WITH X BEING (Any X WHERE EXISTS(X concerne A), X is Affaire)")
-
+        self.assertEqual(
+            rqlst.as_string(),
+            u"Any X,C WHERE X? documented_by C, C is Card "
+            "WITH X BEING (Any X WHERE EXISTS(X concerne A), X is Affaire)")
 
     def test_rrqlexpr_nonexistant_subject_1(self):
         constraint = RRQLExpression('S owned_by U')
@@ -418,26 +466,33 @@
                          'Any C WHERE C is Card, B eid %(D)s, EXISTS(A owned_by B, A is Card)')
         rqlst = parse(u'Card C')
         rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SOU')
-        self.assertEqual(rqlst.as_string(),
-                         'Any C WHERE C is Card, A eid %(B)s, EXISTS(C owned_by A, D owned_by A, D is Card)')
+        self.assertEqual(
+            rqlst.as_string(),
+            'Any C WHERE C is Card, A eid %(B)s, EXISTS(C owned_by A, D owned_by A, D is Card)')
 
     def test_rrqlexpr_nonexistant_subject_3(self):
         constraint = RRQLExpression('U in_group G, G name "users"')
         rqlst = parse(u'Card C')
         rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SU')
-        self.assertEqual(rqlst.as_string(),
-                         u'Any C WHERE C is Card, A eid %(B)s, EXISTS(A in_group D, D name "users", D is CWGroup)')
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any C WHERE C is Card, A eid %(B)s, '
+            'EXISTS(A in_group D, D name "users", D is CWGroup)')
 
     def test_rrqlexpr_nonexistant_subject_4(self):
         constraint = RRQLExpression('U in_group G, G name "users", S owned_by U')
         rqlst = parse(u'Card C')
         rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'SU')
-        self.assertEqual(rqlst.as_string(),
-                         u'Any C WHERE C is Card, A eid %(B)s, EXISTS(A in_group D, D name "users", C owned_by A, D is CWGroup)')
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any C WHERE C is Card, A eid %(B)s, '
+            'EXISTS(A in_group D, D name "users", C owned_by A, D is CWGroup)')
         rqlst = parse(u'Card C')
         rewrite(rqlst, {('C', 'S'): (constraint,)}, {}, 'OU')
-        self.assertEqual(rqlst.as_string(),
-                         u'Any C WHERE C is Card, A eid %(B)s, EXISTS(A in_group D, D name "users", D is CWGroup)')
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any C WHERE C is Card, A eid %(B)s, '
+            'EXISTS(A in_group D, D name "users", D is CWGroup)')
 
     def test_rrqlexpr_nonexistant_subject_5(self):
         constraint = RRQLExpression('S owned_by Z, O owned_by Z, O is Card')
@@ -450,22 +505,28 @@
         constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X')
         rqlst = parse(u'Affaire A WHERE NOT EXISTS(A documented_by C)')
         rewrite(rqlst, {('C', 'X'): (constraint,)}, {}, 'X')
-        self.assertEqual(rqlst.as_string(),
-                         u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire')
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any A WHERE NOT EXISTS(A documented_by C, '
+            'EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire')
 
     def test_rqlexpr_not_relation_1_2(self):
         constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X')
         rqlst = parse(u'Affaire A WHERE NOT EXISTS(A documented_by C)')
         rewrite(rqlst, {('A', 'X'): (constraint,)}, {}, 'X')
-        self.assertEqual(rqlst.as_string(),
-                         u'Any A WHERE NOT EXISTS(A documented_by C, C is Card), A is Affaire, EXISTS(A owned_by B, B login "hop", B is CWUser)')
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any A WHERE NOT EXISTS(A documented_by C, C is Card), A is Affaire, '
+            'EXISTS(A owned_by B, B login "hop", B is CWUser)')
 
     def test_rqlexpr_not_relation_2(self):
         constraint = ERQLExpression('X owned_by Z, Z login "hop"', 'X')
         rqlst = rqlhelper.parse(u'Affaire A WHERE NOT A documented_by C', annotate=False)
         rewrite(rqlst, {('C', 'X'): (constraint,)}, {}, 'X')
-        self.assertEqual(rqlst.as_string(),
-                         u'Any A WHERE NOT EXISTS(A documented_by C, EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire')
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any A WHERE NOT EXISTS(A documented_by C, '
+            'EXISTS(C owned_by B, B login "hop", B is CWUser), C is Card), A is Affaire')
 
     def test_rqlexpr_multiexpr_outerjoin(self):
         c1 = ERQLExpression('X owned_by Z, Z login "hop"', 'X')
@@ -473,11 +534,12 @@
         c3 = ERQLExpression('X owned_by Z, Z login "momo"', 'X')
         rqlst = rqlhelper.parse(u'Any A WHERE A documented_by C?', annotate=False)
         rewrite(rqlst, {('C', 'X'): (c1, c2, c3)}, {}, 'X')
-        self.assertEqual(rqlst.as_string(),
-                         u'Any A WHERE A documented_by C?, A is Affaire '
-                         'WITH C BEING (Any C WHERE ((EXISTS(C owned_by B, B login "hop")) '
-                         'OR (EXISTS(C owned_by D, D login "momo"))) '
-                         'OR (EXISTS(C owned_by A, A login "hip")), C is Card)')
+        self.assertEqual(
+            rqlst.as_string(),
+            u'Any A WHERE A documented_by C?, A is Affaire '
+            'WITH C BEING (Any C WHERE ((EXISTS(C owned_by B, B login "hop")) '
+            'OR (EXISTS(C owned_by D, D login "momo"))) '
+            'OR (EXISTS(C owned_by A, A login "hip")), C is Card)')
 
     def test_multiple_erql_one_bad(self):
         #: reproduce bug #2236985
@@ -503,7 +565,36 @@
                          'Any O WHERE S use_email O, S is CWUser, O is EmailAddress, '
                          'EXISTS(NOT S in_group A, A name "guests", A is CWGroup)')
 
-from cubicweb.devtools.testlib import CubicWebTC
+    def test_ambiguous_constraint_not_exists(self):
+        state_constraint = (
+            'NOT EXISTS(A require_permission S) '
+            'OR EXISTS(B require_permission S, B is Card, O name "state1")'
+            'OR EXISTS(C require_permission S, C is Note, O name "state2")'
+        )
+        rqlst = parse(u'Any P WHERE NOT P require_state S')
+        rewrite(rqlst, {('P', 'S'): (state_constraint,), ('S', 'O'): (state_constraint,)}, {})
+        self.assertMultiLineEqual(
+            rqlst.as_string(),
+            u'Any P WHERE NOT P require_state S, '
+            '((NOT EXISTS(A require_permission P, A is IN(Card, Note)))'
+            ' OR (EXISTS(B require_permission P, B is Card, S name "state1")))'
+            ' OR (EXISTS(C require_permission P, C is Note, S name "state2")), '
+            'P is CWPermission, S is State')
+
+    def test_ambiguous_using_is_in_function(self):
+        state_constraint = (
+            'NOT EXISTS(A require_permission S) '
+            'OR EXISTS(B require_permission S, B is IN (Card, Note), O name "state1")'
+        )
+        rqlst = parse(u'Any P WHERE NOT P require_state S')
+        rewrite(rqlst, {('P', 'S'): (state_constraint,), ('S', 'O'): (state_constraint,)}, {})
+        self.assertMultiLineEqual(
+            rqlst.as_string(),
+            u'Any P WHERE NOT P require_state S, '
+            '(NOT EXISTS(A require_permission P, A is IN(Card, Note))) '
+            'OR (EXISTS(B require_permission P, B is IN(Card, Note), S name "state1")), '
+            'P is CWPermission, S is State')
+
 
 class RewriteFullTC(CubicWebTC):
     appid = 'data-rewrite'
@@ -512,7 +603,7 @@
         if args is None:
             args = {}
         querier = self.repo.querier
-        union = parse(rql) # self.vreg.parse(rql, annotate=True)
+        union = parse(rql)  # self.vreg.parse(rql, annotate=True)
         with self.admin_access.repo_cnx() as cnx:
             self.vreg.solutions(cnx, union, args)
             querier._annotate(union)
@@ -546,7 +637,6 @@
             union = self.process('Any A,AR,X,CD WHERE A concerne X?, A ref AR, X creation_date CD')
             self.assertEqual(union.as_string(), 'not generated today')
 
-
     def test_xxxx(self):
         edef1 = self.schema['Societe']
         edef2 = self.schema['Division']
@@ -564,12 +654,11 @@
 
     def test_question_mark_attribute_snippet(self):
         # see #3661918
-        from cubicweb.rqlrewrite import RQLRewriter
-        from logilab.common.decorators import monkeypatch
         repotest.undo_monkey_patch()
         orig_insert_snippets = RQLRewriter.insert_snippets
         # patch insert_snippets and not rewrite, insert_snippets is already
         # monkey patches (see above setupModule/repotest)
+
         @monkeypatch(RQLRewriter)
         def insert_snippets(self, snippets, varexistsmap=None):
             # crash occurs if snippets are processed in a specific order, force
@@ -632,21 +721,21 @@
                          'C role D, D name "illustrator")',
                          rqlst.as_string())
 
-
     def test_rewrite2(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'Any A,B WHERE A illustrator_of B, C require_permission R, S'
                                 'require_state O')
         rule_rewrite(rqlst, rules)
-        self.assertEqual('Any A,B WHERE C require_permission R, S require_state O, '
-                         'D is Contribution, D contributor A, D manifestation B, D role E, '
-                         'E name "illustrator"',
-                          rqlst.as_string())
+        self.assertEqual(
+            'Any A,B WHERE C require_permission R, S require_state O, '
+            'D is Contribution, D contributor A, D manifestation B, D role E, '
+            'E name "illustrator"',
+            rqlst.as_string())
 
     def test_rewrite3(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'Any A,B WHERE E require_permission T, A illustrator_of B')
         rule_rewrite(rqlst, rules)
         self.assertEqual('Any A,B WHERE E require_permission T, '
@@ -656,7 +745,7 @@
 
     def test_rewrite4(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'Any A,B WHERE C require_permission R, A illustrator_of B')
         rule_rewrite(rqlst, rules)
         self.assertEqual('Any A,B WHERE C require_permission R, '
@@ -666,7 +755,7 @@
 
     def test_rewrite5(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'Any A,B WHERE C require_permission R, A illustrator_of B, '
                                 'S require_state O')
         rule_rewrite(rqlst, rules)
@@ -678,7 +767,7 @@
     # Tests for the with clause
     def test_rewrite_with(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'Any A,B WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
         rule_rewrite(rqlst, rules)
         self.assertEqual('Any A,B WITH A,B BEING '
@@ -688,8 +777,9 @@
 
     def test_rewrite_with2(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
-        rqlst = rqlhelper.parse(u'Any A,B WHERE T require_permission C WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
+                 'C manifestation O, C role R, R name "illustrator"'}
+        rqlst = rqlhelper.parse(u'Any A,B WHERE T require_permission C '
+                                'WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
         rule_rewrite(rqlst, rules)
         self.assertEqual('Any A,B WHERE T require_permission C '
                          'WITH A,B BEING (Any X,Y WHERE A is Contribution, '
@@ -707,32 +797,34 @@
 
     def test_rewrite_with4(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'Any A,B WHERE A illustrator_of B '
-                               'WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
+                                'WITH A, B BEING(Any X, Y WHERE X illustrator_of Y)')
         rule_rewrite(rqlst, rules)
-        self.assertEqual('Any A,B WHERE C is Contribution, '
-                         'C contributor A, C manifestation B, C role D, '
-                         'D name "illustrator" WITH A,B BEING '
-                         '(Any X,Y WHERE A is Contribution, A contributor X, '
-                         'A manifestation Y, A role B, B name "illustrator")',
-                          rqlst.as_string())
+        self.assertEqual(
+            'Any A,B WHERE C is Contribution, '
+            'C contributor A, C manifestation B, C role D, '
+            'D name "illustrator" WITH A,B BEING '
+            '(Any X,Y WHERE A is Contribution, A contributor X, '
+            'A manifestation Y, A role B, B name "illustrator")',
+            rqlst.as_string())
 
     # Tests for the union
     def test_rewrite_union(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'(Any A,B WHERE A illustrator_of B) UNION'
                                 '(Any X,Y WHERE X is CWUser, Z manifestation Y)')
         rule_rewrite(rqlst, rules)
-        self.assertEqual('(Any A,B WHERE C is Contribution, '
-                         'C contributor A, C manifestation B, C role D, '
-                         'D name "illustrator") UNION (Any X,Y WHERE X is CWUser, Z manifestation Y)',
-                         rqlst.as_string())
+        self.assertEqual(
+            '(Any A,B WHERE C is Contribution, '
+            'C contributor A, C manifestation B, C role D, '
+            'D name "illustrator") UNION (Any X,Y WHERE X is CWUser, Z manifestation Y)',
+            rqlst.as_string())
 
     def test_rewrite_union2(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'(Any Y WHERE Y match W) UNION '
                                 '(Any A WHERE A illustrator_of B) UNION '
                                 '(Any Y WHERE Y is ArtWork)')
@@ -746,9 +838,9 @@
     # Tests for the exists clause
     def test_rewrite_exists(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'(Any A,B WHERE A illustrator_of B, '
-                     'EXISTS(B is ArtWork))')
+                                'EXISTS(B is ArtWork))')
         rule_rewrite(rqlst, rules)
         self.assertEqual('Any A,B WHERE EXISTS(B is ArtWork), '
                          'C is Contribution, C contributor A, C manifestation B, C role D, '
@@ -757,7 +849,7 @@
 
     def test_rewrite_exists2(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'(Any A,B WHERE B contributor A, EXISTS(A illustrator_of W))')
         rule_rewrite(rqlst, rules)
         self.assertEqual('Any A,B WHERE B contributor A, '
@@ -767,7 +859,7 @@
 
     def test_rewrite_exists3(self):
         rules = {'illustrator_of': 'C is Contribution, C contributor S, '
-                'C manifestation O, C role R, R name "illustrator"'}
+                 'C manifestation O, C role R, R name "illustrator"'}
         rqlst = rqlhelper.parse(u'(Any A,B WHERE A illustrator_of B, EXISTS(A illustrator_of W))')
         rule_rewrite(rqlst, rules)
         self.assertEqual('Any A,B WHERE EXISTS(C is Contribution, C contributor A, '
@@ -779,13 +871,14 @@
     # Test for GROUPBY
     def test_rewrite_groupby(self):
         rules = {'participated_in': 'S contributor O'}
-        rqlst = rqlhelper.parse(u'Any SUM(SA) GROUPBY S WHERE P participated_in S, P manifestation SA')
+        rqlst = rqlhelper.parse(u'Any SUM(SA) GROUPBY S '
+                                'WHERE P participated_in S, P manifestation SA')
         rule_rewrite(rqlst, rules)
         self.assertEqual('Any SUM(SA) GROUPBY S WHERE P manifestation SA, P contributor S',
                          rqlst.as_string())
 
 
-class RQLRelationRewriterTC(CubicWebTC):
+class RQLRelationRewriterCWTC(CubicWebTC):
 
     appid = 'data-rewrite'
 
@@ -794,8 +887,8 @@
             art = cnx.create_entity('ArtWork', name=u'Les travailleurs de la Mer')
             role = cnx.create_entity('Role', name=u'illustrator')
             vic = cnx.create_entity('Person', name=u'Victor Hugo')
-            contrib = cnx.create_entity('Contribution', code=96, contributor=vic,
-                                        manifestation=art, role=role)
+            cnx.create_entity('Contribution', code=96, contributor=vic,
+                              manifestation=art, role=role)
             rset = cnx.execute('Any X WHERE X illustrator_of S')
             self.assertEqual([u'Victor Hugo'],
                              [result.name for result in rset.entities()])
@@ -816,4 +909,5 @@
 
 
 if __name__ == '__main__':
-    unittest_main()
+    import unittest
+    unittest.main()
--- a/cubicweb/web/component.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/web/component.py	Tue Sep 12 09:49:30 2017 +0200
@@ -126,7 +126,7 @@
         return url
 
     def ajax_page_url(self, **params):
-        divid = params.setdefault('divid', 'pageContent')
+        divid = params.setdefault('divid', 'contentmain')
         params['rql'] = self.cw_rset.printable_rql()
         return js_href("$(%s).loadxhtml(AJAX_PREFIX_URL, %s, 'get', 'swap')" % (
             json_dumps('#'+divid), js.ajaxFuncArgs('view', params)))
--- a/cubicweb/web/test/unittest_views_basecontrollers.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/web/test/unittest_views_basecontrollers.py	Tue Sep 12 09:49:30 2017 +0200
@@ -1012,6 +1012,13 @@
             f = appobject(req)
             self.assertEqual(f(12, 13), '25')
 
+    def test_badrequest(self):
+        with self.assertRaises(RemoteCallFailed) as cm:
+            with self.remote_calling('foo'):
+                pass
+        self.assertEqual(cm.exception.status, 400)
+        self.assertEqual(cm.exception.reason, 'no foo method')
+
 
 class JSonControllerTC(AjaxControllerTC):
     # NOTE: this class performs the same tests as AjaxController but with
--- a/cubicweb/web/views/ajaxcontroller.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/web/views/ajaxcontroller.py	Tue Sep 12 09:49:30 2017 +0200
@@ -67,6 +67,7 @@
 from functools import partial
 
 from six import PY2, text_type
+from six.moves import http_client
 
 from logilab.common.date import strptime
 from logilab.common.registry import yes
@@ -116,7 +117,8 @@
         try:
             fname = self._cw.form['fname']
         except KeyError:
-            raise RemoteCallFailed('no method specified')
+            raise RemoteCallFailed('no method specified',
+                                   status=http_client.BAD_REQUEST)
         # 1/ check first for old-style (JSonController) ajax func for bw compat
         try:
             func = getattr(basecontrollers.JSonController, 'js_%s' % fname)
@@ -128,11 +130,13 @@
             try:
                 func = self._cw.vreg['ajax-func'].select(fname, self._cw)
             except ObjectNotFound:
-                raise RemoteCallFailed('no %s method' % fname)
+                raise RemoteCallFailed('no %s method' % fname,
+                                       status=http_client.BAD_REQUEST)
         else:
             warn('[3.15] remote function %s found on JSonController, '
                  'use AjaxFunction / @ajaxfunc instead' % fname,
                  DeprecationWarning, stacklevel=2)
+        debug_mode = self._cw.vreg.config.debugmode
         # no <arg> attribute means the callback takes no argument
         args = self._cw.form.get('arg', ())
         if not isinstance(args, (list, tuple)):
@@ -140,16 +144,20 @@
         try:
             args = [json.loads(arg) for arg in args]
         except ValueError as exc:
-            self.exception('error while decoding json arguments for '
-                           'js_%s: %s (err: %s)', fname, args, exc)
-            raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
+            if debug_mode:
+                self.exception('error while decoding json arguments for '
+                               'js_%s: %s (err: %s)', fname, args, exc)
+            raise RemoteCallFailed(exc_message(exc, self._cw.encoding),
+                                   status=http_client.BAD_REQUEST)
         try:
             result = func(*args)
         except (RemoteCallFailed, DirectResponse):
             raise
         except Exception as exc:
-            self.exception('an exception occurred while calling js_%s(%s): %s',
-                           fname, args, exc)
+            if debug_mode:
+                self.exception(
+                    'an exception occurred while calling js_%s(%s): %s',
+                    fname, args, exc)
             raise RemoteCallFailed(exc_message(exc, self._cw.encoding))
         if result is None:
             return ''
@@ -219,7 +227,8 @@
         try:
             return self._cw.execute(rql, args)
         except Exception as ex:
-            self.exception("error in _exec(rql=%s): %s", rql, ex)
+            if self._cw.vreg.config.debugmode:
+                self.exception("error in _exec(rql=%s): %s", rql, ex)
             return None
         return None
 
@@ -232,29 +241,25 @@
             stream = UStringIO()
             kwargs['w'] = stream.write
             assert not paginate
-        if divid == 'pageContent':
+        if divid == 'contentmain':
             # ensure divid isn't reused by the view (e.g. table view)
             del self._cw.form['divid']
-            # mimick main template behaviour
-            stream.write(u'<div id="pageContent">')
-            vtitle = self._cw.form.get('vtitle')
-            if vtitle:
-                stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle)
             paginate = True
+        if divid == 'contentmain':
+            stream.write(u'<div id="contentmain">')
         nav_html = UStringIO()
         if paginate and not view.handle_pagination:
             view.paginate(w=nav_html.write)
         stream.write(nav_html.getvalue())
-        if divid == 'pageContent':
-            stream.write(u'<div id="contentmain">')
         view.render(**kwargs)
+        stream.write(nav_html.getvalue())
+        if divid == 'contentmain':
+            stream.write(u'</div>')
         extresources = self._cw.html_headers.getvalue(skiphead=True)
         if extresources:
             stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget?
             stream.write(extresources)
             stream.write(u'</div>\n')
-        if divid == 'pageContent':
-            stream.write(u'</div>%s</div>' % nav_html.getvalue())
         return stream.getvalue()
 
 
--- a/cubicweb/web/views/basetemplates.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/web/views/basetemplates.py	Tue Sep 12 09:49:30 2017 +0200
@@ -158,14 +158,14 @@
             'etypenavigation', self._cw, rset=self.cw_rset)
         if etypefilter and etypefilter.cw_propval('visible'):
             etypefilter.render(w=w)
+        w(u'<div id="contentmain">\n')
         nav_html = UStringIO()
         if view and not view.handle_pagination:
             view.paginate(w=nav_html.write)
         w(nav_html.getvalue())
-        w(u'<div id="contentmain">\n')
         view.render(w=w)
+        w(nav_html.getvalue())
         w(u'</div>\n') # close id=contentmain
-        w(nav_html.getvalue())
         w(u'</div>\n') # closes id=pageContent
         self.template_footer(view)
 
--- a/cubicweb/web/views/facets.py	Tue Sep 12 09:48:02 2017 +0200
+++ b/cubicweb/web/views/facets.py	Tue Sep 12 09:49:30 2017 +0200
@@ -246,7 +246,7 @@
             rset, vid, divid, paginate = context
         else:
             rset = self.cw_rset
-            vid, divid = None, 'pageContent'
+            vid, divid = None, 'contentmain'
             paginate = view and view.paginable
         return rset, vid, divid, paginate
 
--- a/flake8-ok-files.txt	Tue Sep 12 09:48:02 2017 +0200
+++ b/flake8-ok-files.txt	Tue Sep 12 09:49:30 2017 +0200
@@ -9,6 +9,7 @@
 cubicweb/dataimport/test/test_csv.py
 cubicweb/dataimport/test/test_pgstore.py
 cubicweb/dataimport/test/test_massive_store.py
+cubicweb/dataimport/test/test_sqlgenstore.py
 cubicweb/dataimport/test/test_stores.py
 cubicweb/dataimport/test/unittest_importer.py
 cubicweb/devtools/test/data/cubes/i18ntestcube/__init__.py
@@ -42,6 +43,8 @@
 cubicweb/server/rqlannotation.py
 cubicweb/server/schema2sql.py
 cubicweb/server/session.py
+cubicweb/server/sources/__init__.py
+cubicweb/server/sources/native.py
 cubicweb/server/sqlutils.py
 cubicweb/server/utils.py
 cubicweb/server/test/datacomputed/migratedapp/schema.py
@@ -65,8 +68,6 @@
 cubicweb/sobjects/test/unittest_notification.py
 cubicweb/sobjects/test/unittest_register_user.py
 cubicweb/sobjects/textparsers.py
-cubicweb/sources/__init__.py
-cubicweb/sources/native.py
 cubicweb/test/data/libpython/cubicweb_comment/__init__.py
 cubicweb/test/data/libpython/cubicweb_comment/__pkginfo__.py
 cubicweb/test/data/libpython/cubicweb_email/entities.py
@@ -94,6 +95,7 @@
 cubicweb/test/unittest_mail.py
 cubicweb/test/unittest_repoapi.py
 cubicweb/test/unittest_req.py
+cubicweb/test/unittest_rqlrewrite.py
 cubicweb/test/unittest_rtags.py
 cubicweb/test/unittest_schema.py
 cubicweb/test/unittest_toolsutils.py