Repository: edgedb/edgedb

Issue:
Single globals no longer correctly infer optional return type
Regression in 4.6 and 5.x caused by https://github.com/edgedb/edgedb/pull/6613

```
_localdev:main> create global y := (select Object filter false limit 1);
OK: CREATE GLOBAL
_localdev:main> select { required y := global y };
[{"y": null}]
```

Originally via https://discord.com/channels/841451783728529451/1217949944800088105


Discussion:

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
@@ -558,7 +558,12 @@ def compile_GlobalExpr(
         if glob.get_cardinality(ctx.env.schema).is_single():
             key = (setgen.get_set_type(target, ctx=ctx), False)
             if not ctx.env.type_rewrites.get(key):
-                ctx.env.type_rewrites[key] = target
+                with ctx.detached() as dctx:
+                    # The official rewrite needs to be in a detached
+                    # scope to avoid collisions; this won't really
+                    # recompile the whole thing, it will hit a cache
+                    # of the view.
+                    ctx.env.type_rewrites[key] = dispatch.compile(qry, ctx=dctx)
             rewrite_target = ctx.env.type_rewrites[key]
 
             # We need to have the set with expr=None, so that the rewrite
@@ -567,7 +572,11 @@ def compile_GlobalExpr(
             # instead of assuming it is MANY.
             assert isinstance(rewrite_target, irast.Set)
             target = setgen.new_set_from_set(
-                target, expr=irast.TypeRoot(typeref=target.typeref), ctx=ctx
+                target,
+                expr=irast.TypeRoot(
+                    typeref=target.typeref, is_cached_global=True
+                ),
+                ctx=ctx,
             )
             wrap = irast.SelectStmt(
                 result=target,
diff --git a/edb/ir/ast.py b/edb/ir/ast.py
--- a/edb/ir/ast.py
+++ b/edb/ir/ast.py
@@ -511,6 +511,10 @@ class TypeRoot(Expr):
     # This will be replicated in the enclosing set.
     typeref: TypeRef
 
+    # Whether this is a reference to a global that is cached in a
+    # materialized CTE in the query.
+    is_cached_global: bool = False
+
     # Whether to force this to not select subtypes
     skip_subtypes: bool = False
 
diff --git a/edb/pgsql/compiler/relctx.py b/edb/pgsql/compiler/relctx.py
--- a/edb/pgsql/compiler/relctx.py
+++ b/edb/pgsql/compiler/relctx.py
@@ -503,15 +503,20 @@ def new_primitive_rvar(
 ) -> pgast.PathRangeVar:
     if isinstance(ir_set.expr, irast.TypeRoot):
         skip_subtypes = ir_set.expr.skip_subtypes
+        is_global = ir_set.expr.is_cached_global
     else:
         skip_subtypes = False
+        is_global = False
 
     typeref = ir_set.typeref
     dml_source = irutils.get_dml_sources(ir_set)
     set_rvar = range_for_typeref(
         typeref, path_id, lateral=lateral, dml_source=dml_source,
         include_descendants=not skip_subtypes,
-        ignore_rewrites=ir_set.ignore_rewrites, ctx=ctx)
+        ignore_rewrites=ir_set.ignore_rewrites,
+        is_global=is_global,
+        ctx=ctx,
+    )
     pathctx.put_rvar_path_bond(set_rvar, path_id)
 
     # FIXME: This feels like it should all not be here.
@@ -1413,20 +1418,15 @@ def range_for_material_objtype(
     include_overlays: bool=True,
     include_descendants: bool=True,
     ignore_rewrites: bool=False,
+    is_global: bool=False,
     dml_source: Sequence[irast.MutatingLikeStmt]=(),
     ctx: context.CompilerContextLevel,
 ) -> pgast.PathRangeVar:
 
     env = ctx.env
 
-    # If this is a view type, but it still appears in rewrites, that is
-    # because it is a global that we are caching.
-    if (
-        typeref.material_type is not None
-        and (typeref.id, include_descendants) not in ctx.env.type_rewrites
-    ):
-        typeref = typeref.material_type
-    is_global = typeref.material_type is not None
+    if not is_global:
+        typeref = typeref.real_material_type
 
     if not is_global and not path_id.is_objtype_path():
         raise ValueError('cannot create root rvar for non-object path')
@@ -1562,7 +1562,7 @@ def range_for_material_objtype(
         )
 
     else:
-
+        assert not typeref.is_view, "attempting to generate range from view"
         table_schema_name, table_name = common.get_objtype_backend_name(
             typeref.id,
             typeref.name_hint.module,
@@ -1661,6 +1661,7 @@ def range_for_typeref(
     for_mutation: bool=False,
     include_descendants: bool=True,
     ignore_rewrites: bool=False,
+    is_global: bool=False,
     dml_source: Sequence[irast.MutatingLikeStmt]=(),
     ctx: context.CompilerContextLevel,
 ) -> pgast.PathRangeVar:
@@ -1743,6 +1744,7 @@ def range_for_typeref(
             ignore_rewrites=ignore_rewrites,
             include_overlays=not for_mutation,
             for_mutation=for_mutation,
+            is_global=is_global,
             dml_source=dml_source,
             ctx=ctx,
         )


Test patch:
diff --git a/tests/schemas/cards_ir_inference.esdl b/tests/schemas/cards_ir_inference.esdl
--- a/tests/schemas/cards_ir_inference.esdl
+++ b/tests/schemas/cards_ir_inference.esdl
@@ -157,3 +157,5 @@ abstract type Named2 {
     delegated constraint exclusive on (.name);
 }
 type Named2Sub extending Named2;
+
+global Alice := (select User filter .name = 'Alice');
diff --git a/tests/test_edgeql_ddl.py b/tests/test_edgeql_ddl.py
--- a/tests/test_edgeql_ddl.py
+++ b/tests/test_edgeql_ddl.py
@@ -7113,6 +7113,38 @@ async def test_edgeql_ddl_global_10(self):
             [{'org': {'id': obj.id}}],
         )
 
+    async def test_edgeql_ddl_global_11(self):
+        # Just like 9, but with a shape on current_org
+        await self.con.execute('''
+            create type Org;
+
+            create global current_org_id -> uuid;
+            create global current_org := (
+              select Org { * } filter .id = global current_org_id
+            );
+
+            create type Widget {
+              create required link org -> Org {
+                set default := global current_org;
+              }
+            };
+        ''')
+
+        obj = await self.con.query_single('insert Org')
+        await self.con.execute(
+            '''
+            set global current_org_id := <uuid>$0
+            ''',
+            obj.id,
+        )
+        await self.con.execute('insert Widget')
+        await self.assert_query_result(
+            '''
+                select Widget { org }
+            ''',
+            [{'org': {'id': obj.id}}],
+        )
+
     async def test_edgeql_ddl_global_default(self):
         await self.con.execute('''
             create global foo -> str;
diff --git a/tests/test_edgeql_ir_card_inference.py b/tests/test_edgeql_ir_card_inference.py
--- a/tests/test_edgeql_ir_card_inference.py
+++ b/tests/test_edgeql_ir_card_inference.py
@@ -1305,6 +1305,13 @@ def test_edgeql_ir_card_inference_155(self):
         # But should apply to this subtype
         """
         select Named2Sub filter .name = ''
+% OK %
+        AT_MOST_ONE
+        """
+
+    def test_edgeql_ir_card_inference_156(self):
+        """
+        select global Alice
 % OK %
         AT_MOST_ONE
         """