Repository: edgedb/edgedb

Issue:
Allow overloading `__type__` with a computed in shapes
Context:

> https://github.com/edgedb/edgedb-js/issues/326 — do we have to disallow computeds called __type__ in shapes even when they are returning a Type? This works for all other links but not for __type__ and that breaks our querybuilder which is cumbersome to get fixed.
I think part of the reasoning here is purely historical - at one time we used to use __type__ in our REPL. Also we ban __names__ in general but we can relax things here because this name __type__ is already defined.

And

<img width="442" alt="CleanShot 2022-10-19 at 13 26 13@2x" src="https://user-images.githubusercontent.com/239003/196796764-530c7f74-fb4f-4997-ae0f-7baba791368d.png">

---

We want this to be in the next 2.x release.

cc @jaclarke @msullivan 


Discussion:

Resolving patch:
diff --git a/edb/edgeql/compiler/viewgen.py b/edb/edgeql/compiler/viewgen.py
--- a/edb/edgeql/compiler/viewgen.py
+++ b/edb/edgeql/compiler/viewgen.py
@@ -1096,7 +1096,7 @@ def _normalize_view_ptr_expr(
             context=compexpr and compexpr.context,
         )
 
-    # Prohibit invalid operations on id and __type__
+    # Prohibit invalid operations on id
     id_access = (
         ptrcls.is_id_pointer(ctx.env.schema)
         and (
@@ -1106,8 +1106,7 @@ def _normalize_view_ptr_expr(
     )
     if (
         (compexpr is not None or is_polymorphic)
-        and (id_access or ptrcls.is_protected_pointer(ctx.env.schema))
-        and not from_default
+        and id_access and not from_default
     ):
         ptrcls_sn = ptrcls.get_shortname(ctx.env.schema)
         if is_polymorphic:
@@ -1115,7 +1114,7 @@ def _normalize_view_ptr_expr(
                    f'shape element')
         else:
             msg = f'cannot assign to {ptrcls_sn.name}'
-        if id_access and not ctx.env.options.allow_user_specified_id:
+        if not ctx.env.options.allow_user_specified_id:
             hint = (
                 'consider enabling the "allow_user_specified_id" '
                 'configuration parameter to allow setting custom object ids'
@@ -1291,9 +1290,16 @@ def _inline_type_computable(
         )
         with ctx.new() as scopectx:
             scopectx.anchors = scopectx.anchors.copy()
-            scopectx.anchors[qlast.Source().name] = ir_set
+            # Use the actual base type as the root of the injection, so that
+            # if a user has overridden `__type__` in a computable,
+            # we see through that.
+            base_stype = stype.get_nearest_non_derived_parent(ctx.env.schema)
+            base_ir_set = setgen.ensure_set(
+                ir_set, type_override=base_stype, ctx=ctx)
+
+            scopectx.anchors[qlast.Source().name] = base_ir_set
             ptr, ptr_set = _normalize_view_ptr_expr(
-                ir_set, ql, stype, path_id=ir_set.path_id, ctx=scopectx)
+                base_ir_set, ql, stype, path_id=ir_set.path_id, ctx=scopectx)
 
     view_shape = ctx.env.view_shapes[stype]
     view_shape_ptrs = {p for p, _ in view_shape}


Test patch:
diff --git a/tests/test_edgeql_expressions.py b/tests/test_edgeql_expressions.py
--- a/tests/test_edgeql_expressions.py
+++ b/tests/test_edgeql_expressions.py
@@ -6625,16 +6625,6 @@ async def test_edgeql_expr_range_38(self):
             [edgedb.Range(0, 10)],
         )
 
-    async def test_edgeql_expr_cannot_assign_dunder_type_01(self):
-        with self.assertRaisesRegex(
-                edgedb.QueryError, r'cannot assign to __type__'):
-            await self.con.query(r"""
-                SELECT Text {
-                    __type__ := (SELECT schema::ObjectType
-                                 FILTER .name = 'default::Named')
-                };
-            """)
-
     async def test_edgeql_expr_cannot_assign_id_01(self):
         with self.assertRaisesRegex(
                 edgedb.QueryError, r'cannot assign to id'):
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
@@ -771,6 +771,21 @@ async def test_edgeql_select_computable_34(self):
                 FILTER Issue.number = '1';
             """)
 
+    async def test_edgeql_select_computable_35(self):
+        # allow computed __type__ field
+        await self.assert_query_result(
+            """
+            SELECT Issue {
+                number,
+                __type__ := (select Issue.__type__ { name }),
+            }
+            FILTER .number = '3'
+            """,
+            [
+                {'number': '3', '__type__': {'name': 'default::Issue'}},
+            ],
+        )
+
     async def test_edgeql_select_match_01(self):
         await self.assert_query_result(
             r"""
@@ -1409,22 +1424,6 @@ async def test_edgeql_select_polymorphic_04(self):
                 };
             ''')
 
-    async def test_edgeql_select_polymorphic_05(self):
-        # Since using a polymorphic shape element means that sometimes
-        # that element may be empty, it is prohibited to access
-        # protected link such as `__type__` on it as that would be
-        # equivalent to re-writing it.
-        with self.assertRaisesRegex(
-                edgedb.QueryError,
-                r'cannot access __type__ on a polymorphic shape element'):
-            await self.con.query(r'''
-                SELECT User {
-                    [IS Named].__type__: {
-                        name
-                    },
-                };
-            ''')
-
     async def test_edgeql_select_polymorphic_06(self):
         await self.assert_query_result(
             r'''
@@ -7637,3 +7636,11 @@ async def test_edgeql_shape_computed_alias_01(self):
             ''',
             [{"is_abstract": True}]
         )
+
+    async def test_edgeql_select_tname_overriden_type_01(self):
+        # Test that overriding type doesn't break __tname__
+        res = await self.con._fetchall("""
+            SELECT User { __type__ := introspect Issue }
+        """, __typenames__=True)
+        for row in res:
+            self.assertEqual(row.__tname__, "default::User")