Repository: edgedb/edgedb

Issue:
Weird staeval infinite recursion
<!-- Please search existing issues to avoid creating duplicates. -->

<!--
For the EdgeDB Version: run `edgedb query 'select sys::get_version_as_str()'` from your project directory (or run `select sys::get_version_as_str();` in the EdgeDB interactive shell).
For the EdgeDB CLI Version: Run `edgedb --version` from anywhere
-->

- EdgeDB Version: 3.0-rc.1+23c3462 (also tested on nightly 4.0-dev.7515+6f86d18)
- EdgeDB CLI Version: 3.1.0-dev.998+d16f478
- OS Version: macOS 13.3.1 (22E261)

Steps to Reproduce:

This query used to work on certain versions of nightly (specifically 4.0-dev.7512+bca7e08). Testing on 3.0-rc.1 and the latest nightly however causes a maximum recursive depth exceeded in comparison.

```
with
  cursor := <optional tuple<
    createdAt: datetime,
    id: uuid,
  >>$cursor,
  `limit` := <optional int64>$limit ?? 10,
  notifications := (
    select Notification
    filter all({
      exists .account,
      .account.id = global currentAccountId,
      any({
        .createdAt < cursor.createdAt,
        .createdAt = cursor.createdAt and .id > cursor.id,
      }) if exists cursor else true,
    })
    order by .createdAt desc
    then .id asc
    limit `limit`
  ),
  newStakeNotifications := (
    select NewStakeNotification {
      stakerId,
      staker: {
        avatar: { storageId, etagId, type, url := generate_etag_url(.storageId, .etagId, true) },
        cover: { storageId, etagId, type, url := generate_etag_url(.storageId, .etagId, true) },
        *,
      },
      amount,
    }
  ),
  marketplaceListingFilledNotifications := (
    select MarketplaceListingFilledNotification {
      lister: {
        avatar: { storageId, etagId, type, url := generate_etag_url(.storageId, .etagId, true) },
        cover: { storageId, etagId, type, url := generate_etag_url(.storageId, .etagId, true) },
        *,
      },
      buyer: {
        avatar: { storageId, etagId, type, url := generate_etag_url(.storageId, .etagId, true) },
        cover: { storageId, etagId, type, url := generate_etag_url(.storageId, .etagId, true) },
        *,
      },
      amount,
      primarySale,
    }
  )
select notifications {
  *,
  data := assert_exists((
    select
    (
      <json>(select newStakeNotifications filter .id = notifications.id) union
      <json>(select marketplaceListingFilledNotifications filter .id = notifications.id)
    )
    limit 1
  ))
};
```

Stack trace:

```
edgedb error: InternalServerError: maximum recursion depth exceeded in comparison
  Hint: This is most likely a bug in EdgeDB. Please consider opening an issue ticket at https://github.com/edgedb/edgedb/issues/new?template=bug_report.md
  Server traceback:
      Traceback (most recent call last):
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/server/compiler_pool/worker_proc.py", line 55, in worker
          res = meth(*args)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/server/compiler_pool/worker.py", line 161, in compile
          units, cstate = COMPILER.compile(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 751, in compile
          unit_group = compile(ctx=ctx, source=source)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 1936, in compile
          return _try_compile(ctx=ctx, source=source)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 2004, in _try_compile
          comp, capabilities = _compile_dispatch_ql(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 1910, in _compile_dispatch_ql
          query = _compile_ql_query(ctx, ql, script_info=script_info)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 1471, in _compile_ql_query
          ir = qlcompiler.compile_ast_to_ir(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/__init__.py", line 179, in wrapper
          return func(*args, **kwargs)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/__init__.py", line 237, in compile_ast_to_ir
          ir_set = dispatch_mod.compile(tree, ctx=ctx)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 127, in compile_SelectQuery
          stmt.result = compile_result_clause(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 1333, in compile_result_clause
          ir_result = compile_query_subject(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 1470, in compile_query_subject
          view_scls, expr = viewgen.process_view(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/viewgen.py", line 151, in process_view
          view_scls, ir = _process_view(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/viewgen.py", line 361, in _process_view
          pointer, ptr_set = _normalize_view_ptr_expr(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/viewgen.py", line 1520, in _normalize_view_ptr_expr
          irexpr, sub_view_rptr = _compile_qlexpr(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/viewgen.py", line 1285, in _compile_qlexpr
          irexpr = dispatch.compile(qlexpr, ctx=shape_expr_ctx)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 127, in compile_SelectQuery
          stmt.result = compile_result_clause(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 1329, in compile_result_clause
          expr = dispatch.compile(result_expr, ctx=ectx)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 127, in compile_SelectQuery
          stmt.result = compile_result_clause(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 1329, in compile_result_clause
          expr = dispatch.compile(result_expr, ctx=ectx)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/func.py", line 102, in compile_FunctionCall
          args, kwargs = compile_func_call_args(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/func.py", line 694, in compile_func_call_args
          arg_ir = polyres.compile_arg(arg, typemods[ai], ctx=ctx)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/polyres.py", line 583, in compile_arg
          arg_ir = dispatch.compile(arg_ql, ctx=argctx)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 127, in compile_SelectQuery
          stmt.result = compile_result_clause(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 1329, in compile_result_clause
          expr = dispatch.compile(result_expr, ctx=ectx)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 127, in compile_SelectQuery
          stmt.result = compile_result_clause(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 1329, in compile_result_clause
          expr = dispatch.compile(result_expr, ctx=ectx)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 127, in compile_SelectQuery
          stmt.result = compile_result_clause(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/stmt.py", line 1329, in compile_result_clause
          expr = dispatch.compile(result_expr, ctx=ectx)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/expr.py", line 136, in compile_BinOp
          folded = try_fold_binop(op_node.expr, ctx=ctx)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/edgeql/compiler/expr.py", line 282, in try_fold_binop
          const = ireval.evaluate(opcall, schema=ctx.env.schema)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 155, in evaluate_OperatorCall
          return _evaluate_union(opcall, schema)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 203, in _evaluate_union
          val = evaluate(arg.expr, schema=schema)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 126, in evaluate_Set
          return evaluate(ir_set.expr, schema=schema)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 93, in evaluate_SelectStmt
          return evaluate(ir_stmt.result, schema)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 126, in evaluate_Set
          return evaluate(ir_set.expr, schema=schema)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/functools.py", line 889, in wrapper
          return dispatch(args[0].__class__)(*args, **kw)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 108, in evaluate_TypeCast
          schema_type_to_python_type(from_type, schema)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 316, in schema_type_to_python_type
          return object_type_to_python_type(stype, schema)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 377, in object_type_to_python_type
          pytype = object_type_to_python_type(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 377, in object_type_to_python_type
          pytype = object_type_to_python_type(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 377, in object_type_to_python_type
          pytype = object_type_to_python_type(
        [Previous line repeated 929 more times]
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 365, in object_type_to_python_type
          for pn, p in objtype.get_pointers(schema).items(schema):
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/schema/objects.py", line 2604, in items
          for obj in self.objects(schema):
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/schema/objects.py", line 2375, in objects
          return tuple(
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/schema/objects.py", line 2376, in <genexpr>
          schema.get_by_id(iid) for iid in self._ids  # type: ignore
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/schema/schema.py", line 283, in get_by_id
          return self._get_by_id(obj_id, default, type=type)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/schema/schema.py", line 1924, in _get_by_id
          obj = self._top_schema.get_by_id(obj_id, type=type, default=None)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/schema/schema.py", line 283, in get_by_id
          return self._get_by_id(obj_id, default, type=type)
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/schema/schema.py", line 1316, in _get_by_id
          obj = so.Object.schema_restore((sclass_name, obj_id))
      RecursionError: maximum recursion depth exceeded in comparison
```

Schema:

```
  global publicBucketUrl -> str;
  global privateBucketUrl -> str;

  function format_type_name(schema: str, t: str) -> str
    using (str_lower(
      re_replace(
        r'([a-z0-9])([A-Z])',
        r'\1_\2',
        re_replace(r'^' ++ schema ++ '::([a-zA-Z]+)$', r'\1', t),
        flags := 'g',
      ),
    ));

  function generate_etag_url(storageId: Storage, etagId: str, userHasAccess: bool) -> str
    using (assert_exists(
      (global publicBucketUrl ++ "/" ++ etagId)
      if userHasAccess else
      (global privateBucketUrl ++ "/protected/" ++ etagId ++ "/preview")
    ));

  abstract type Timestamped {
    required property createdAt -> datetime {
      default := datetime_truncate(datetime_of_statement(), "milliseconds");
    };
    required property updatedAt -> datetime {
      default := datetime_truncate(datetime_of_statement(), "milliseconds");
      rewrite update using (datetime_truncate(datetime_of_statement(), "milliseconds"));
    };
  }

  type Account extending Timestamped {
    required property publicKey -> str {
      constraint exclusive;
    };
    link avatar -> Etag {
      on target delete allow;
    };
    link cover -> Etag {
      on target delete allow;
    };

    trigger delete_notifications after delete for each do (
      delete Notification filter .accountPublicKey = __old__.publicKey
    );
  }

  scalar type Storage extending enum<r2>;

  type Etag extending Timestamped {
    required property storageId -> Storage;
    required property etagId -> str;
    constraint exclusive on ((.storageId, .etagId));

    required property type -> str;
  }

  scalar type NotificationType extending enum<
    new_stake,
    marketplace_listing_filled
  >;

  abstract type Notification extending Timestamped {
    required property accountPublicKey -> str;
    link account := (select Account filter .publicKey = Notification.accountPublicKey limit 1);
    property type := <NotificationType>str_replace(format_type_name("default", .__type__.name), "_notification", "");
    required property read -> bool {
      default := false;
    };
  }

  type NewStakeNotification extending Notification {
    required property stakerId -> str;
    link staker := (select Account filter .publicKey = NewStakeNotification.stakerId limit 1);
    required property amount -> json;
  }

  type MarketplaceListingFilledNotification extending Notification {
    link lister -> Account {
      on target delete delete source;
    };
    link buyer -> Account {
      on target delete delete source;
    };
    required property amount -> bigint;
    required property primarySale := not exists(.lister);
  }
```


Discussion:
This problem is more severe than is described by #4876: practical queries are large enough to overflow the stack. For a temporary fix, I suggest adding an option to set maximum recursion depth and in a way that the stack overflow is still unlikely. This option may be an environment variable or a global configuration.

Also, it seems that python is *not* fixing the stack overflow issue. https://peps.python.org/pep-0651/ 
```
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/ir/staeval.py", line 377, in object_type_to_python_type
          pytype = object_type_to_python_type(
        [Previous line repeated 929 more times]
```

This actually seems like a bug in staeval to me
Would there be any way to set the maximum recursion depth limit right now? I am noticing in particular that on the same latest version of nightly, a Docker-deployed EdgeDB instance is not running into the maximum recursion depth limit, whereas my local instance deployed via. CLI is running into it when generating this large query.
I don't believe there is a way to set the maximum recursion depth limit in EdgeDB currently, but here is a hack that you can try:

1. From the stack trace locate which files are being run, i.e. 
```
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/server/compiler_pool/worker.py", line 161, in compile
```
or 
```
        File "/Users/dew/Library/Application Support/edgedb/portable/3.0-rc.1/lib/python3.10/site-packages/edb/server/compiler/compiler.py", line 751, in compile
```
2. Edit those files to manually set the recursion depth limit. For example, you could put the following lines on top of those files.
```
import sys
sys.setrecursionlimit(1500)
```

This temporarily increases the Python's stack limit and should temporarily resolve the issue. 
I wasn't able to reproduce it from the example but I managed to figure out a reproducer from staring at the stack trace:
```
edgedb> create type Z { create link z -> Z; };
OK: CREATE TYPE
edgedb> select <json>Z union <json>Z;
```

Resolving patch:
diff --git a/edb/ir/staeval.py b/edb/ir/staeval.py
--- a/edb/ir/staeval.py
+++ b/edb/ir/staeval.py
@@ -108,8 +108,14 @@ def evaluate_TypeCast(
         schema, ir_cast.from_type)
     schema, to_type = irtyputils.ir_typeref_to_type(
         schema, ir_cast.from_type)
-    schema_type_to_python_type(from_type, schema)
-    schema_type_to_python_type(to_type, schema)
+
+    if (
+        not isinstance(from_type, s_scalars.ScalarType)
+        or not isinstance(to_type, s_scalars.ScalarType)
+    ):
+        raise UnsupportedExpressionError('object cast not supported')
+    scalar_type_to_python_type(from_type, schema)
+    scalar_type_to_python_type(to_type, schema)
     evaluate(ir_cast.expr, schema)
     return ir_cast
 
@@ -375,6 +381,10 @@ def scalar_type_to_python_type(
 T_spec = TypeVar('T_spec', bound=statypes.CompositeTypeSpec)
 
 
+class _Missing:
+    pass
+
+
 def object_type_to_spec(
     objtype: s_objtypes.ObjectType,
     schema: s_schema.Schema,
@@ -387,6 +397,9 @@ def object_type_to_spec(
 ) -> T_spec:
     if _memo is None:
         _memo = {}
+    # Prevent infinite recursion
+    _memo[objtype] = _Missing
+
     default: Any
     fields = {}
 
@@ -401,6 +414,8 @@ def object_type_to_spec(
 
         if isinstance(ptype, s_objtypes.ObjectType):
             pytype = _memo.get(ptype)
+            if pytype is _Missing:
+                raise UnsupportedExpressionError()
             if pytype is None:
                 pytype = object_type_to_spec(
                     ptype, schema, spec_class=spec_class,


Test patch:
diff --git a/tests/test_edgeql_casts.py b/tests/test_edgeql_casts.py
--- a/tests/test_edgeql_casts.py
+++ b/tests/test_edgeql_casts.py
@@ -2439,6 +2439,17 @@ async def test_edgeql_casts_json_14(self):
             [[]],
         )
 
+    async def test_edgeql_casts_json_15(self):
+        # At one point, a cast from an object inside a binary
+        # operation triggered an infinite loop in staeval if the
+        # object had a self link.
+        await self.con.execute('''
+            create type Z { create link z -> Z; };
+        ''')
+        await self.con.query('''
+            select <json>Z union <json>Z;
+        ''')
+
     async def test_edgeql_casts_assignment_01(self):
         async with self._run_and_rollback():
             await self.con.execute(r"""