Backport fix for cubicweb ticket #17074119
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 21 Apr 2017 10:59:35 +0200
changeset 3288 e22d0c70415f
parent 3287 777ac1759715
child 3289 c6916f0132d2
Backport fix for cubicweb ticket #17074119 We need this to fix a key error on edition of an authority record, because of the constraint on 'agent_kind' relation which use several ORed EXISTS expressions. Add insert test reproducing the crash in relevant schema tests. Closes extranet #18336413
cubicweb_saem_ref/site_cubicweb.py
test/unittest_schema.py
--- a/cubicweb_saem_ref/site_cubicweb.py	Fri Apr 21 14:56:06 2017 +0200
+++ b/cubicweb_saem_ref/site_cubicweb.py	Fri Apr 21 10:59:35 2017 +0200
@@ -307,3 +307,99 @@
         stream.write(extresources)
         stream.write(u'</div>\n')
     return stream.getvalue()
+
+
+# Fixed rql reqwrite (https://www.cubicweb.org/ticket/17074119)
+
+from cubicweb.rqlrewrite import RQLRewriter, n, remove_solutions  # noqa
+
+
+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
+
+
+@monkeypatch(RQLRewriter)
+def _inserted_root(self, new):
+    if need_exists(new):
+        new = n.Exists(new)
+    return new
+
+
+@monkeypatch(RQLRewriter)
+def remove_ambiguities(self, snippets, newsolutions):
+    # the snippet has introduced some ambiguities, we have to resolve them
+    # "manually"
+    variantes = self.build_variantes(newsolutions)
+    if not variantes:
+        return newsolutions
+    # insert "is" where necessary
+    varexistsmap = {}
+    self.removing_ambiguity = True
+    for (erqlexpr, varmap, oldvarname), etype in variantes[0].items():
+        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.items():
+            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
+    self.compute_solutions()
+    # clean solutions according to initial solutions
+    return remove_solutions(self.solutions, self.select.solutions,
+                            self.select.defined_vars)
+
+
+@monkeypatch(RQLRewriter)
+def build_variantes(self, newsolutions):
+    variantes = set()
+    for sol in newsolutions:
+        variante = []
+        for key, var_name in self.rewritten.items():
+            var = self.select.defined_vars[var_name]
+            # skip variable which are only in a NOT EXISTS (mustn't be splitted)
+            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))
+
+    # 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)
+        try:
+            etype = next(it)[key]
+        except StopIteration:
+            continue
+        for variante in it:
+            if variante[key] != etype:
+                break
+        else:
+            for variante in variantes:
+                del variante[key]
+
+    return variantes
--- a/test/unittest_schema.py	Fri Apr 21 14:56:06 2017 +0200
+++ b/test/unittest_schema.py	Fri Apr 21 10:59:35 2017 +0200
@@ -121,6 +121,10 @@
             testutils.organization_unit(cnx, u'unit', authority_record=arecord)
             cnx.commit()
 
+            self.assertEqual(
+                [k.name for k in arecord.unrelated('agent_kind', 'AgentKind').entities()],
+                [])
+
             self.assertCantChangeRecordKind(arecord, u'person')
 
             org = testutils.authority_with_naa(cnx)
@@ -129,6 +133,9 @@
             self.assertCantChangeRecordKind(arecord, u'person')
 
             org.cw_set(authority_record=None)
+            self.assertEqual(
+                {k.name for k in arecord.unrelated('agent_kind', 'AgentKind').entities()},
+                {u'unknown-agent-kind', u'family', u'person'})
             arecord.cw_set(agent_kind=cnx.find('AgentKind', name=u'person').one())
             cnx.commit()