Repository: edgedb/edgedb

Issue:
[GraphQL] Inline fragments don't work when requesting nested properties
<!-- Please search existing issues to avoid creating duplicates. -->
- EdgeDB Version: 1-alpha 5
- OS Version: OSX 10.15.6

Steps to Reproduce:
Trying something like the following:
```
query {
    Superclass{
      ... on Subclass{
         property, #works fine
         aLink{
            nestedProperty #causes error
         }
      }
    }
}
```
I get the error `"InvalidReferenceError: object type 'default::Sublcass' has no link or property 'nestedProperty'"`

Instead it should be getting the property from the linked object.
<!-- If the issue is about a query error, please also provide your schema -->
Schema:

```
abstract type Action{
    required property txid -> str{
        constraint exclusive;
    }
    required property timestamp -> int32;
}

type User{
    required property publicKey -> str{
        constraint exclusive;
    }
    required property paymail -> str;
    property moneyButtonId -> int32{
        constraint exclusive;
    }

    property amountEarned := (
        sum(.<user[IS Content].voteTotal)
    );

    property metaname -> str;
    property bsvnewsname -> str;
    property displayName := .metaname??.bsvnewsname;
}

abstract type Content extending Action {
    required property voteAddress -> str;
    required link user -> User;
    multi link votes -> Vote;
    property voteTotal := sum(.votes.voteAmount);
    multi link comments -> Comment;
    
}

type Post extending Content { 
    required property title -> str;
    required property url -> str;
    property description -> str;

    property hotness := (
        .voteTotal/(datetime_get(datetime_current(), 'epochseconds')-.timestamp)
    );

    property numComments := 
        count(.<rootPost[IS Comment])
    ;
    
}

type Comment extending Content{
    required property body -> str;
    required link rootPost -> Post;
}

type Vote extending Action{
    required property voteAmount -> int32;
    required link user -> User;
   
}
```




Discussion:

Resolving patch:
diff --git a/edb/graphql/translator.py b/edb/graphql/translator.py
--- a/edb/graphql/translator.py
+++ b/edb/graphql/translator.py
@@ -94,7 +94,7 @@ def __init__(self, *, gqlcore: gt.GQLCoreSchema,
         self.fields = []
         self.path = []
         self.filter = None
-        self.include_base = [False]
+        self.include_base = []
         self.gqlcore = gqlcore
         self.query = query
         self.document_ast = document_ast
@@ -306,11 +306,14 @@ def visit_OperationDefinitionNode(self, node):
         self._context.vars = {
             name: Var(val=val, defn=None, critical=False)
             for name, val in self._context.variables.items()}
+        self._context.include_base.append(False)
+
         opname = None
         if node.name:
             opname = node.name.value
 
         if opname != self._context.operation_name:
+            self._context.include_base.pop()
             return None
 
         if (node.operation is None or
@@ -330,6 +333,8 @@ def visit_OperationDefinitionNode(self, node):
         defvars = {name: var.val for name, var in self._context.vars.items()
                    if var.defn is not None}
 
+        self._context.include_base.pop()
+
         return Operation(
             name=opname,
             stmt=stmt,
@@ -605,6 +610,7 @@ def visit_FieldNode(self, node):
                 cardinality=qltypes.SchemaCardinality.Many,
             )
 
+        self._context.include_base.append(False)
         # INSERT mutations have different arguments from queries
         if not is_shadowed and node.name.value.startswith('insert_'):
             # a single recursion target, so we can process
@@ -698,6 +704,7 @@ def visit_FieldNode(self, node):
             # shell for the shape paths.
             self._context.path.pop()
 
+        self._context.include_base.pop()
         return spec
 
     def visit_InlineFragmentNode(self, node):
@@ -705,6 +712,8 @@ def visit_InlineFragmentNode(self, node):
         result = self.visit(node.selection_set)
         if node.type_condition is not None:
             self._context.path.pop()
+            self._context.include_base.pop()
+
         return result
 
     def visit_FragmentSpreadNode(self, node):
@@ -716,6 +725,10 @@ def visit_FragmentSpreadNode(self, node):
 
         result = self.visit(selection_set)
         self._context.path.pop()
+
+        if frag.type_condition is not None:
+            self._context.include_base.pop()
+
         return result
 
     def _validate_fragment_type(self, frag, spread):
@@ -1303,10 +1316,23 @@ def get_path_prefix(self, end_trim=None):
             prefix = [qlast.ObjectRef(name=base_step.eql_alias)]
         else:
             prefix = [base_step.type.edb_base_name_ast]
-        prefix.extend(
-            qlast.Ptr(ptr=qlast.ObjectRef(name=step.name))
-            for step in path
-        )
+
+        for step in path:
+            if isinstance(step.name, gql_ast.NamedTypeNode):
+                # This is coming from a fragment, so we need to add a
+                # type intersection.
+                base = step.type
+                prefix.append(
+                    qlast.TypeIntersection(
+                        type=qlast.TypeName(
+                            maintype=base.edb_base_name_ast
+                        )
+                    )
+                )
+            else:
+                prefix.append(
+                    qlast.Ptr(ptr=qlast.ObjectRef(name=step.name))
+                )
 
         return prefix
 


Test patch:
diff --git a/tests/test_http_graphql_query.py b/tests/test_http_graphql_query.py
--- a/tests/test_http_graphql_query.py
+++ b/tests/test_http_graphql_query.py
@@ -1861,6 +1861,194 @@ def test_graphql_functional_fragment_type_12(self):
             ]
         })
 
+    def test_graphql_functional_fragment_type_13(self):
+        # ISSUE #1800
+        #
+        # After using a typed inline fragment the nested fields or
+        # fields following after the fragment are erroneously using
+        # the type intersection.
+        self.assert_graphql_query_result(r"""
+            query {
+                NamedObject(filter: {name: {eq: "Alice"}}) {
+                    ... on User {
+                        active
+                        profile {
+                            value
+                        }
+                    }
+                    name
+                }
+            }
+        """, {
+            'NamedObject': [{
+                'name': 'Alice',
+                'active': True,
+                'profile': {
+                    'value': 'special',
+                }
+            }],
+        })
+
+    def test_graphql_functional_fragment_type_14(self):
+        # ISSUE #1800
+        #
+        # After using a typed inline fragment the nested fields or
+        # fields following after the fragment are erroneously using
+        # the type intersection.
+        self.assert_graphql_query_result(r"""
+            fragment userFrag on User {
+                active
+                profile {
+                    value
+                }
+            }
+
+            query {
+                NamedObject(filter: {name: {eq: "Alice"}}) {
+                    ... userFrag
+                    name
+                }
+            }
+        """, {
+            'NamedObject': [{
+                'name': 'Alice',
+                'active': True,
+                'profile': {
+                    'value': 'special',
+                }
+            }],
+        })
+
+    def test_graphql_functional_fragment_type_15(self):
+        # ISSUE #1800
+        #
+        # After using a typed inline fragment the nested fields or
+        # fields following after the fragment are erroneously using
+        # the type intersection.
+        self.assert_graphql_query_result(r"""
+            query {
+                NamedObject(filter: {name: {eq: "Alice"}}) {
+                    ... on User {
+                        active
+                        profile(filter: {name: {eq: "Alice profile"}}) {
+                            value
+                        }
+                    }
+                    name
+                }
+            }
+        """, {
+            'NamedObject': [{
+                'name': 'Alice',
+                'active': True,
+                'profile': {
+                    'value': 'special',
+                }
+            }],
+        })
+
+        self.assert_graphql_query_result(r"""
+            query {
+                NamedObject(filter: {name: {eq: "Alice"}}) {
+                    ... on User {
+                        active
+                        profile(filter: {name: {eq: "no such profile"}}) {
+                            value
+                        }
+                    }
+                    name
+                }
+            }
+        """, {
+            'NamedObject': [{
+                'name': 'Alice',
+                'active': True,
+                'profile': None,
+            }],
+        })
+
+    def test_graphql_functional_fragment_type_16(self):
+        # ISSUE #1800
+        #
+        # After using a typed inline fragment the nested fields or
+        # fields following after the fragment are erroneously using
+        # the type intersection.
+        self.assert_graphql_query_result(r"""
+            fragment userFrag on User {
+                active
+                profile(filter: {name: {eq: "Alice profile"}}) {
+                    value
+                }
+            }
+
+            query {
+                NamedObject(filter: {name: {eq: "Alice"}}) {
+                    ... userFrag
+                    name
+                }
+            }
+        """, {
+            'NamedObject': [{
+                'name': 'Alice',
+                'active': True,
+                'profile': {
+                    'value': 'special',
+                }
+            }],
+        })
+
+        self.assert_graphql_query_result(r"""
+            fragment userFrag on User {
+                active
+                profile(filter: {name: {eq: "no such profile"}}) {
+                    value
+                }
+            }
+
+            query {
+                NamedObject(filter: {name: {eq: "Alice"}}) {
+                    ... userFrag
+                    name
+                }
+            }
+        """, {
+            'NamedObject': [{
+                'name': 'Alice',
+                'active': True,
+                'profile': None,
+            }],
+        })
+
+    def test_graphql_functional_fragment_type_17(self):
+        # ISSUE #1800
+        #
+        # After using a typed inline fragment the nested fields or
+        # fields following after the fragment are erroneously using
+        # the type intersection.
+        self.assert_graphql_query_result(r"""
+            query {
+                NamedObject(filter: {name: {eq: "Alice"}}) {
+                    ... on User {
+                        ... {
+                            active
+                            profile {
+                                value
+                            }
+                        }
+                    }
+                    name
+                }
+            }
+        """, {
+            'NamedObject': [{
+                'name': 'Alice',
+                'active': True,
+                'profile': {
+                    'value': 'special',
+                }
+            }],
+        })
+
     def test_graphql_functional_directives_01(self):
         self.assert_graphql_query_result(r"""
             query {