Repository: edgedb/edgedb

Issue:
Cardinality of polymorphic shape field isn't optional
**Describe the bug**
Polymorphic EdgeQL query where a string field may not be present is inferred as `string`

**Reproduction**
Schema
```
type Message {}
type SlackMultiUserMessage extending Message {
  slackDisplayName: str;
}
```
edgeql query file
```
select Message {
  *,
  [is SlackMultiUserMessage].slackDisplayName
}
```
Generated TS
```typescript
export type TestReturns = {
  "slackDisplayName": string;
  "id": string;
}[];
```

**Expected behavior**
`slackDisplayName` would be optional.

**Versions (please complete the following information):**

<!--
For 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 EdgeDB CLI version: Run `edgedb --version` from anywhere
For `edgedb-js` version: Run `npm list edgedb --depth=0` from your project directory
For Node/Deno version: Run `node --version` or `deno --version`
-->

- OS: macos 13.5
- EdgeDB version: 3.3
- EdgeDB CLI version : 3.5.0
- `edgedb-js` version :  "edgedb": "^1.3.6", "@edgedb/generate": "^0.3.3",
- Node/Deno version: 18.16.0



Discussion:
Thanks for reporting @jackfischer 

It's unclear if this is a bug in the queries generator, or a subtle bug in the cardinality inference in EdgeDB itself. I'll see if I can get to the bottom of that.
Totally. Not aware of a great way to examine this on the CLI side so filed here for now.

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
@@ -1337,6 +1337,8 @@ def _normalize_view_ptr_expr(
     is_polymorphic = shape_el_desc.is_polymorphic
     target_typexpr = shape_el_desc.target_typexpr
 
+    is_independent_polymorphic = False
+
     compexpr: Optional[qlast.Expr] = shape_el.compexpr
     if compexpr is None and is_mutation:
         raise errors.QueryError(
@@ -1354,10 +1356,29 @@ def _normalize_view_ptr_expr(
             ctx=ctx,
             source_context=shape_el.context,
         )
+        real_ptrcls = None
         if is_polymorphic:
+            # For polymorphic pointers, we need to see if the *real*
+            # base class has the pointer, because if so we need to use
+            # that when doing cardinality inference (since it may need
+            # to raise an error, if it is required). If it isn't
+            # present on the real type, take note of that so that we
+            # suppress the inherited cardinality.
+            try:
+                real_ptrcls = setgen.resolve_ptr(
+                    view_scls,
+                    ptrname,
+                    track_ref=shape_el_desc.ptr_ql,
+                    ctx=ctx,
+                    source_context=shape_el.context,
+                )
+            except errors.InvalidReferenceError:
+                is_independent_polymorphic = True
             ptrcls = schemactx.derive_ptr(ptrcls, view_scls, ctx=ctx)
+        real_ptrcls = real_ptrcls or ptrcls
 
-        base_ptrcls = ptrcls.get_bases(ctx.env.schema).first(ctx.env.schema)
+        base_ptrcls = real_ptrcls.get_bases(
+            ctx.env.schema).first(ctx.env.schema)
         base_ptr_is_computable = base_ptrcls in ctx.env.source_map
         ptr_name = sn.QualName(
             module='__',
@@ -1439,7 +1460,7 @@ def _normalize_view_ptr_expr(
 
         ptr_required = base_required
         ptr_cardinality = base_cardinality
-        if shape_el.where:
+        if shape_el.where or is_polymorphic:
             # If the shape has a filter on it, we need to force a reinference
             # of the cardinality, to produce an error if needed.
             ptr_cardinality = None
@@ -1883,7 +1904,11 @@ def _normalize_view_ptr_expr(
 
         base_cardinality = None
         base_required = None
-        if base_ptrcls is not None and not base_ptrcls_is_alias:
+        if (
+            base_ptrcls is not None
+            and not base_ptrcls_is_alias
+            and not is_independent_polymorphic
+        ):
             base_cardinality = _get_base_ptr_cardinality(base_ptrcls, ctx=ctx)
             base_required = base_ptrcls.get_required(ctx.env.schema)
 


Test patch:
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
@@ -1205,3 +1205,67 @@ def test_edgeql_ir_card_inference_141(self):
 % OK %
         AT_MOST_ONE
         """
+
+    def test_edgeql_ir_card_inference_142(self):
+        """
+        select Named { [is Card].element }
+% OK %
+        element: AT_MOST_ONE
+        """
+
+    def test_edgeql_ir_card_inference_143(self):
+        """
+        select Named { element := [is Card].element }
+% OK %
+        element: AT_MOST_ONE
+        """
+
+    def test_edgeql_ir_card_inference_144(self):
+        """
+        select (
+          select assert_exists(Named) { [is Card].element } limit 1).element
+% OK %
+        AT_MOST_ONE
+        """
+
+    def test_edgeql_ir_card_inference_145(self):
+        """
+        select Named { [is Named].name }
+% OK %
+        name: ONE
+        """
+
+    def test_edgeql_ir_card_inference_146(self):
+        """
+        select User { [is Named].name }
+% OK %
+        name: ONE
+        """
+
+    @tb.must_fail(errors.QueryError,
+                  "possibly an empty set returned")
+    def test_edgeql_ir_card_inference_147(self):
+        """
+        select Named { [is User].name }
+        """
+
+    @tb.must_fail(errors.QueryError,
+                  "possibly an empty set returned")
+    def test_edgeql_ir_card_inference_148(self):
+        """
+        select Named { name := [is User].name }
+        """
+
+    @tb.must_fail(errors.QueryError,
+                  "possibly an empty set returned")
+    def test_edgeql_ir_card_inference_149(self):
+        """
+        select Named { [is schema::Object].name }
+        """
+
+    @tb.must_fail(errors.QueryError,
+                  "possibly an empty set returned")
+    def test_edgeql_ir_card_inference_150(self):
+        """
+        select User { [is schema::Object].name }
+        """