[rql] closes #2054468: support for HAVING in SET/DELETE queries
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 25 Oct 2011 14:03:52 +0200
changeset 8018 f01c80513274
parent 8016 1a1b23c37013
child 8019 eb83c52ffa0c
[rql] closes #2054468: support for HAVING in SET/DELETE queries
server/ssplanner.py
server/test/unittest_querier.py
--- a/server/ssplanner.py	Tue Oct 25 13:51:27 2011 +0200
+++ b/server/ssplanner.py	Tue Oct 25 14:03:52 2011 +0200
@@ -103,28 +103,26 @@
     When select has nothing selected, search in origrqlst for restriction that
     should be considered.
     """
+    if origrqlst.where is not None and not select.selection:
+        # no selection, append one randomly by searching for a relation which is
+        # neither a type restriction (is) nor an eid specification (not neged
+        # eid with constant node)
+        for rel in origrqlst.where.iget_nodes(Relation):
+            if rel.neged(strict=True) or not (
+                rel.is_types_restriction() or
+                (rel.r_type == 'eid'
+                 and isinstance(rel.get_variable_parts()[1], Constant))):
+                select.append_selected(rel.children[0].copy(select))
+                break
+        else:
+            return
     if select.selection:
         if origrqlst.where is not None:
             select.set_where(origrqlst.where.copy(select))
+        if getattr(origrqlst, 'having', None):
+            select.set_having([sq.copy(select) for sq in origrqlst.having])
         return select
-    if origrqlst.where is None:
-        return
-    for rel in origrqlst.where.iget_nodes(Relation):
-        # search for a relation which is neither a type restriction (is) nor an
-        # eid specification (not neged eid with constant node
-        if rel.neged(strict=True) or not (
-            rel.is_types_restriction() or
-            (rel.r_type == 'eid'
-             and isinstance(rel.get_variable_parts()[1], Constant))):
-            break
-    else:
-        return
-    select.set_where(origrqlst.where.copy(select))
-    if not select.selection:
-        # no selection, append one randomly
-        select.append_selected(rel.children[0].copy(select))
-    return select
-
+    return None
 
 class SSPlanner(object):
     """SingleSourcePlanner: build execution plan for rql queries
@@ -204,38 +202,40 @@
         steps = []
         for etype, var in rqlst.main_variables:
             step = DeleteEntitiesStep(plan)
-            step.children += self._sel_variable_step(plan, rqlst.solutions,
-                                                     rqlst.where, etype, var)
+            step.children += self._sel_variable_step(plan, rqlst, etype, var)
             steps.append(step)
         for relation in rqlst.main_relations:
             step = DeleteRelationsStep(plan, relation.r_type)
-            step.children += self._sel_relation_steps(plan, rqlst.solutions,
-                                                      rqlst.where, relation)
+            step.children += self._sel_relation_steps(plan, rqlst, relation)
             steps.append(step)
         return steps
 
-    def _sel_variable_step(self, plan, solutions, restriction, etype, varref):
+    def _sel_variable_step(self, plan, rqlst, etype, varref):
         """handle the selection of variables for a delete query"""
         select = Select()
         varref = varref.copy(select)
         select.defined_vars = {varref.name: varref.variable}
         select.append_selected(varref)
-        if restriction is not None:
-            select.set_where(restriction.copy(select))
+        if rqlst.where is not None:
+            select.set_where(rqlst.where.copy(select))
+        if getattr(rqlst, 'having', None):
+            select.set_having([x.copy(select) for x in rqlst.having])
         if etype != 'Any':
             select.add_type_restriction(varref.variable, etype)
-        return self._select_plan(plan, select, solutions)
+        return self._select_plan(plan, select, rqlst.solutions)
 
-    def _sel_relation_steps(self, plan, solutions, restriction, relation):
+    def _sel_relation_steps(self, plan, rqlst, relation):
         """handle the selection of relations for a delete query"""
         select = Select()
         lhs, rhs = relation.get_variable_parts()
         select.append_selected(lhs.copy(select))
         select.append_selected(rhs.copy(select))
         select.set_where(relation.copy(select))
-        if restriction is not None:
-            select.add_restriction(restriction.copy(select))
-        return self._select_plan(plan, select, solutions)
+        if rqlst.where is not None:
+            select.set_where(rqlst.where.copy(select))
+        if getattr(rqlst, 'having', None):
+            select.set_having([x.copy(select) for x in rqlst.having])
+        return self._select_plan(plan, select, rqlst.solutions)
 
     def build_set_plan(self, plan, rqlst):
         """get an execution plan from an SET RQL query"""
--- a/server/test/unittest_querier.py	Tue Oct 25 13:51:27 2011 +0200
+++ b/server/test/unittest_querier.py	Tue Oct 25 14:03:52 2011 +0200
@@ -1227,6 +1227,26 @@
         self.assertRaises(QueryError, self.execute, "SET X nom 'toto', X has_text 'tutu' WHERE X is Personne")
         self.assertRaises(QueryError, self.execute, "SET X login 'tutu', X eid %s" % cnx.user(self.session).eid)
 
+    # HAVING on write queries test #############################################
+
+    def test_update_having(self):
+        peid1 = self.execute("INSERT Personne Y: Y nom 'hop', Y tel 1")[0][0]
+        peid2 = self.execute("INSERT Personne Y: Y nom 'hop', Y tel 2")[0][0]
+        rset = self.execute("SET X tel 3 WHERE X tel TEL HAVING TEL&1=1")
+        self.assertEqual(tuplify(rset.rows), [(peid1, 3)])
+
+    def test_insert_having(self):
+        self.skip('unsupported yet')
+        self.execute("INSERT Personne Y: Y nom 'hop', Y tel 1")[0][0]
+        with self.debugged('DBG_SQL'):
+            self.assertFalse(self.execute("INSERT Personne Y: Y nom 'hop', Y tel 2 WHERE X tel XT HAVING XT&2=2"))
+            self.assertTrue(self.execute("INSERT Personne Y: Y nom 'hop', Y tel 2 WHERE X tel XT HAVING XT&1=1"))
+
+    def test_delete_having(self):
+        self.execute("INSERT Personne Y: Y nom 'hop', Y tel 1")[0][0]
+        self.assertFalse(self.execute("DELETE Personne Y WHERE X tel XT HAVING XT&2=2"))
+        self.assertTrue(self.execute("DELETE Personne Y WHERE X tel XT HAVING XT&1=1"))
+
     # upassword encryption tests #################################################
 
     def test_insert_upassword(self):