Repository: geldata/gel
Pull request: Fix insert default cycle
Closes #5348

Problem statement:
Recursive defaults with DML will cause ISE: recursion depth limit exceeded
EdgeDB 3.0-dev.7456+local (repl 3.0.0-dev)


Create a migration on the following schema, an infinite recursion will follow
```
type Test3 {
    d1 : str ;
    d7 : Test3 {
        default :=  (insert Test3 {
            d1 := "d1"
        })
    }
}
```

Hints from issue discussion:

Commit message: Fix insert default cycle

Patch to review:
diff --git a/edb/edgeql/compiler/context.py b/edb/edgeql/compiler/context.py
index 9d1c0a980af..4fb777d142b 100644
--- a/edb/edgeql/compiler/context.py
+++ b/edb/edgeql/compiler/context.py
@@ -543,6 +543,10 @@ class ContextLevel(compiler.ContextLevel):
         if not None, then it is of the form `in a FILTER clause`  """
 
     active_rewrites: FrozenSet[s_objtypes.ObjectType]
+    """For detecting cycles in rewrite rules"""
+
+    active_defaults: FrozenSet[s_objtypes.ObjectType]
+    """For detecting cycles in default rules"""
 
     def __init__(
         self,
@@ -598,6 +602,7 @@ def __init__(
             self.active_computeds = ordered.OrderedSet()
             self.recompiling_schema_alias = False
             self.active_rewrites = frozenset()
+            self.active_defaults = frozenset()
 
             self.disallow_dml = None
 
@@ -639,6 +644,7 @@ def __init__(
             self.active_computeds = prevlevel.active_computeds
             self.recompiling_schema_alias = prevlevel.recompiling_schema_alias
             self.active_rewrites = prevlevel.active_rewrites
+            self.active_defaults = prevlevel.active_defaults
 
             self.disallow_dml = prevlevel.disallow_dml
 
diff --git a/edb/edgeql/compiler/viewgen.py b/edb/edgeql/compiler/viewgen.py
index 92c6c3ce2f0..25a4cb9d8dd 100644
--- a/edb/edgeql/compiler/viewgen.py
+++ b/edb/edgeql/compiler/viewgen.py
@@ -709,6 +709,12 @@ def _gen_pointers_from_defaults(
     path_id = ir_set.path_id
     result: List[EarlyShapePtr] = []
 
+    if stype in ctx.active_defaults:
+        vn = stype.get_verbosename(ctx.env.schema)
+        raise errors.QueryError(
+            f"default rule on {vn} is part of a default rule cycle",
+        )
+
     scls_pointers = stype.get_pointers(ctx.env.schema)
     for pn, ptrcls in scls_pointers.items(ctx.env.schema):
         if pn in specified_ptrs or ptrcls.is_pure_computable(ctx.env.schema):
@@ -741,6 +747,7 @@ def _gen_pointers_from_defaults(
         )
 
         with ctx.new() as scopectx:
+            scopectx.active_defaults |= {stype}
 
             pointer, ptr_set = _normalize_view_ptr_expr(
                 ir_set,
diff --git a/tests/test_edgeql_ddl.py b/tests/test_edgeql_ddl.py
index af0b27c1771..517a3e6d4bd 100644
--- a/tests/test_edgeql_ddl.py
+++ b/tests/test_edgeql_ddl.py
@@ -1952,6 +1952,19 @@ async def test_edgeql_ddl_default_11(self):
                 };
             """)
 
+    async def test_edgeql_ddl_default_12(self):
+        with self.assertRaisesRegex(
+            edgedb.QueryError,
+            'is part of a default rule cycle'
+        ):
+            await self.con.execute(r"""
+                CREATE TYPE Test3 {
+                    CREATE LINK d7 : Test3 {
+                        SET default := (INSERT Test3 {})
+                    }
+                }
+            """)
+
     async def test_edgeql_ddl_default_circular(self):
         await self.con.execute(r"""
             CREATE TYPE TestDefaultCircular {


Code review comments:
diff_hunk: @@ -1952,6 +1952,19 @@ async def test_edgeql_ddl_default_11(self):
                 };
             """)
 
+    async def test_edgeql_ddl_default_12(self):
+        with self.assertRaisesRegex(
+            edgedb.QueryError,
+            'is part of a default rule cycle'
path: tests/test_edgeql_ddl.py
text: @user1:
drop "rule", I guess?