Repository: edgedb/edgedb

Issue:
RecursionError when compiling unions over many queries
Query to repro:

```
WITH MODULE test
FOR X IN {
     (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), 
     (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), 
     (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), 
     (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), 
}
UNION 1;
```


Discussion:
Just to be clear, this bug can be reproduced on an even simpler query:
```
SELECT {
     (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), 
     (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), 
     (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), 
     (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), 
};
```
**Targets:**

* Alpha 2: make sure codegen errors are serialized properly.
* Alpha 3: fix the bug, potentially by rewriting codegen to be non-recursive.
Both of these work now, but adding an extra row makes it fail
A few things:
 * It looks like the error does get serialized properly now
 * It's pretty fundamental to what we do that we are going to be recursing over user-provided nested data, so this sort of thing is always a risk
 * Python's default recursion limit is 1000, which is really not very big. It's probably worth turning it up.
 * If we want to make sure to prevent this sort of thing, one option is to have a check on the original AST that the nesting depth of constructs doesn't exceed some level, and then try not to blow up the nesting level beyond some factor during our compilation passes
 * This example in particular blows up the nesting level because we turn the set literal into a linearly deep chain of UNION operators. We could make it logarithmically deep by making it a tree of UNIONs instead of a list. (Though note that this is still incompatible with the above bullet point's suggest strategy which only permits a constant factor blowup.)

So I think my proposed plan for RecursionErrors is:
1) Turn up the recursion limit
2) Fix anything that seems to gratuitously blow up the nesting depth but otherwise don't worry too much at this stage

(That means fixing this one to not do a linear blowup of set literals)

Resolving patch:
diff --git a/edb/edgeql/compiler/expr.py b/edb/edgeql/compiler/expr.py
--- a/edb/edgeql/compiler/expr.py
+++ b/edb/edgeql/compiler/expr.py
@@ -144,17 +144,16 @@ def compile_Set(
             # a set literal is just sugar for a UNION
             op = 'UNION'
 
+            # Turn it into a tree of UNIONs so we only blow up the nesting
+            # depth logarithmically.
+            # TODO: Introduce an N-ary operation that handles the whole thing?
+            mid = len(elements) // 2
+            ls, rs = elements[:mid], elements[mid:]
             bigunion = qlast.BinOp(
-                left=elements[0],
-                right=elements[1],
+                left=qlast.Set(elements=ls) if len(ls) > 1 else ls[0],
+                right=qlast.Set(elements=rs) if len(rs) > 1 else rs[0],
                 op=op
             )
-            for el in elements[2:]:
-                bigunion = qlast.BinOp(
-                    left=bigunion,
-                    right=el,
-                    op=op
-                )
             return dispatch.compile(bigunion, ctx=ctx)
     else:
         return setgen.new_empty_set(


Test patch:
diff --git a/tests/test_edgeql_select.py b/tests/test_edgeql_select.py
--- a/tests/test_edgeql_select.py
+++ b/tests/test_edgeql_select.py
@@ -6005,3 +6005,21 @@ async def test_edgeql_select_duplicate_definition_03(self):
                     todo
                 }
             """)
+
+    async def test_edgeql_select_big_set_literal(self):
+        res = await self.con.query("""
+            SELECT {
+                 (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,),
+                 (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,),
+                 (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,),
+                 (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,),
+                 (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,),
+                 (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,),
+                 (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,),
+                 (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,),
+                 (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,),
+                 (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,), (1,),
+            };
+        """)
+
+        assert len(res) == 100