Repository: edgedb/edgedb

Issue:
GraphQL cannot create links between objects
- EdgeDB Version: 1.0
- OS Version: Ubuntu Desktop

Steps to Reproduce:

1. Set Schema
```esdl
module default {
  type Employee {
    required property username -> str;
    link manager -> Employee;
    multi link friends -> Employee {
        property confirmed -> bool;
    }
  }
}
using extension graphql;
```
2. Run GraphQL Mutations to add Employees
```GraphQL
mutation {
  insert_Employee(data: [
    {
    	username: "Foo",
    },{
      username: "Bar",
    }, {
      username: "Baz",
    }
  ]){
    username
    friends {
      username
    }
    manager {
      username
    }
  }
}
```
4. Run GraphQL Mutations to link Employees
```GraphQL
mutation {
  update_Employee(
    filter: { username: { eq: "Bar" } }
    first: 1
    data: {
      manager: {
        set: {
          filter: { username: { eq: "Foo" } }
          first: 1
        }
      }
      friends: {
        add: {
          filter: {
            username: { eq: "Foo" }
          }
        }
      }
    }
  ){
    username
    friends {
      username
    }
    manager {
      username
    }
  }
}
```
See Error:
> "AttributeError: 'SelectQuery' object has no attribute 'elements'"
5. Modify mutation to force array on add type (shouldn't be needed)
```GraphQL
mutation {
  update_Employee(
    filter: { username: { eq: "Bar" } }
    first: 1
    data: {
      manager: {
        set: {
          filter: { username: { eq: "Foo" } }
          first: 1
        }
      }
      friends: {
        add: [{
          filter: {
            username: { eq: "Foo" }
          }
        }]
      }
    }
  ){
    username
    friends {
      username
    }
    manager {
      username
    }
  }
}
```
6. Response is employees are not linked
```JSON
{
  "data": {
    "update_Employee": [
      {
        "username": "Bar",
        "friends": [],
        "manager": null
      }
    ]
  }
}
```

---

**Expected steps 4 or 5 to produce:**
```JSON
{
  "data": {
    "update_Employee": [
      {
        "username": "Bar",
        "friends": [
          {
            "username": "Foo"
          }
        ],
        "manager": {
          "username": "Foo"
        }
      }
    ]
  }
}
```


Discussion:
Even in this simplest form using the UUIDs that are shown by a query, I still can't get the objects to link. And this shows that they do exist by those UUIDs

```GraphQL
mutation {
  bar: update_Employee(
    filter: { id: { eq: "568bfe2a-8c70-11ec-af24-272eea2a3df3" } }
    data: {
      manager: {
        set: {
          filter: { id: { eq: "568bf11e-8c70-11ec-af24-7fb758eb2abb" } }
        }
      }
    }
  ){
    id
    username
    manager {
      id
      username
    }
  }
  foo: update_Employee(
    filter: { id: { eq: "568bf11e-8c70-11ec-af24-7fb758eb2abb" } }
    data: {
      manager: {
        set: {
          filter: { id: { eq: "568bfe2a-8c70-11ec-af24-272eea2a3df3" } }
        }
      }
    }) {
    id
    username
    manager {
      id
      username
    }
  }
}
```

```JSON
{
  "data": {
    "bar": [
      {
        "id": "568bfe2a-8c70-11ec-af24-272eea2a3df3",
        "username": "Bar",
        "manager": null
      }
    ],
    "foo": [
      {
        "id": "568bf11e-8c70-11ec-af24-7fb758eb2abb",
        "username": "Foo",
        "manager": null
      }
    ]
  }
}
```
Debug Info using `EDGEDB_DEBUG_GRAPHQL_COMPILE=1 edgedb instance start <name> --foreground`

```log
INFO 75323 2022-02-12T22:02:02.929 edb.server: Connection established to backend database: edgedb
================================================================================
Input graphql
================================================================================
mutation {
  update_Employee(
    filter: { username: { eq: "Bar" } }
    data: {
      manager: {
        set: {
          filter: { username: { eq: "Foo" } }
        }
      }
    }
  ){
    id
    username
    manager {
      id
      username
    }
  }
}
variables: None
================================================================================
GraphQL optimized query
================================================================================
mutation($_edb_arg__0:String!$_edb_arg__1:String!){update_Employee(filter:{username:{eq:$_edb_arg__0}}data:{manager:{set:{filter:{username:{eq:$_edb_arg__1}}}}}){id username manager{id username}}}
key_vars: []
variables: {'_edb_arg__0': 'Bar', '_edb_arg__1': 'Foo'}
== operationName: None =============
select
    {
        multi update_Employee := (with
            x0 := 
                (update
                    default::Employee
                filter
                    (default::Employee.username = <str>$_edb_arg__0)
                set {
                    manager := (select
                        default::Employee
                    filter
                        (default::Employee.username = <str>$_edb_arg__1)
                    )
                })
        select
            x0 {
                id,
                username,
                manager: {
                    id,
                    username
                }
            }
        )
    }

```
Yep, it's a bug.  The nested `Employee` reference should be `DETACHED`.

Resolving patch:
diff --git a/edb/graphql/translator.py b/edb/graphql/translator.py
--- a/edb/graphql/translator.py
+++ b/edb/graphql/translator.py
@@ -1036,7 +1036,8 @@ def _visit_update_op(self, node, eqlpath, ftype):
 
             # NOTE: there will be more operations in the future
             if fname == 'set':
-                return self._get_input_expr_for_pointer_mutaiton(field)
+                return qlast.DetachedExpr(
+                    expr=self._get_input_expr_for_pointer_mutaiton(field))
             elif fname == 'clear':
                 cond = field.value
                 if isinstance(cond, gql_ast.VariableNode):
@@ -1109,7 +1110,7 @@ def _visit_update_op(self, node, eqlpath, ftype):
                 newset.elements.append(eqlpath)
                 return qlast.UnaryOp(
                     op='DISTINCT',
-                    operand=newset,
+                    operand=qlast.DetachedExpr(expr=newset),
                 )
             elif fname == 'remove':
                 alias = f'x{self._context.counter}'
@@ -1119,7 +1120,9 @@ def _visit_update_op(self, node, eqlpath, ftype):
                     where=qlast.BinOp(
                         left=qlast.Path(steps=[qlast.ObjectRef(name=alias)]),
                         op='NOT IN',
-                        right=self._visit_insert_value(field.value)
+                        right=qlast.DetachedExpr(
+                            expr=self._visit_insert_value(field.value)
+                        )
                     )
                 )
 
@@ -1163,6 +1166,12 @@ def _get_input_expr_for_pointer_mutaiton(self, field):
 
         # get the type of the value being inserted
         _, target = self._get_parent_and_current_type()
+
+        if target.is_object_type:
+            # Object types need to be wrapped in a DETACHED in
+            # mutations to avoid referencing the root object.
+            compexpr = qlast.DetachedExpr(expr=compexpr)
+
         if (isinstance(field.value, gql_ast.ListValueNode) and
                 target.is_object_type):
             # Need to wrap the set into an "assert_distinct()" if the


Test patch:
diff --git a/tests/schemas/graphql_other.esdl b/tests/schemas/graphql_other.esdl
--- a/tests/schemas/graphql_other.esdl
+++ b/tests/schemas/graphql_other.esdl
@@ -27,4 +27,9 @@ type Foo {
     property `select` -> str;
     property after -> str;
     required property color -> ColorEnum;
+
+    # Testing linking to the same type
+    multi link foos -> Foo {
+        on target delete deferred restrict;
+    }
 }
diff --git a/tests/test_http_graphql_mutation.py b/tests/test_http_graphql_mutation.py
--- a/tests/test_http_graphql_mutation.py
+++ b/tests/test_http_graphql_mutation.py
@@ -1286,6 +1286,193 @@ def test_graphql_mutation_insert_nested_08(self):
             "Game": []
         })
 
+    def test_graphql_mutation_insert_nested_09(self):
+        # Issue #3470
+        # Test nested insert of the same type.
+        data = {
+            'after': 'BaseFoo09',
+            'color': 'GREEN',
+            'foos': [{
+                'after': 'NestedFoo090',
+                'color': 'RED',
+            }, {
+                'after': 'NestedFoo091',
+                'color': 'BLUE',
+            }],
+        }
+
+        validation_query = r"""
+            query {
+                other__Foo(filter: {after: {eq: "BaseFoo09"}}) {
+                    after
+                    color
+                    foos(order: {color: {dir: ASC}}) {
+                        after
+                        color
+                    }
+                }
+            }
+        """
+
+        self.assert_graphql_query_result(r"""
+            mutation insert_other__Foo {
+                insert_other__Foo(
+                    data: [{
+                        after: "BaseFoo09",
+                        color: GREEN,
+                        foos: [{
+                            data: {
+                                after: "NestedFoo090",
+                                color: RED,
+                            }
+                        }, {
+                            data: {
+                                after: "NestedFoo091",
+                                color: BLUE,
+                            }
+                        }]
+                    }]
+                ) {
+                    after
+                    color
+                    foos(order: {color: {dir: ASC}}) {
+                        after
+                        color
+                    }
+                }
+            }
+        """, {
+            "insert_other__Foo": [data]
+        })
+
+        self.assert_graphql_query_result(validation_query, {
+            "other__Foo": [data]
+        })
+
+        self.assert_graphql_query_result(r"""
+            mutation delete_other__Foo {
+                delete_other__Foo(
+                    filter: {after: {like: "%Foo09%"}},
+                    order: {color: {dir: ASC}}
+                ) {
+                    after
+                    color
+                }
+            }
+        """, {
+            "delete_other__Foo": [{
+                'after': 'NestedFoo090',
+                'color': 'RED',
+            }, {
+                'after': 'BaseFoo09',
+                'color': 'GREEN',
+            }, {
+                'after': 'NestedFoo091',
+                'color': 'BLUE',
+            }]
+        })
+
+    def test_graphql_mutation_insert_nested_10(self):
+        # Issue #3470
+        # Test nested insert of the same type.
+        data = {
+            'after': 'BaseFoo10',
+            'color': 'GREEN',
+            'foos': [{
+                'after': 'NestedFoo100',
+                'color': 'RED',
+            }, {
+                'after': 'NestedFoo101',
+                'color': 'BLUE',
+            }],
+        }
+
+        validation_query = r"""
+            query {
+                other__Foo(filter: {after: {eq: "BaseFoo10"}}) {
+                    after
+                    color
+                    foos(order: {color: {dir: ASC}}) {
+                        after
+                        color
+                    }
+                }
+            }
+        """
+
+        self.assert_graphql_query_result(r"""
+            mutation insert_other__Foo {
+                insert_other__Foo(
+                    data: [{
+                        after: "NestedFoo100",
+                        color: RED,
+                    }]
+                ) {
+                    after
+                    color
+                }
+            }
+        """, {
+            "insert_other__Foo": [data['foos'][0]]
+        })
+
+        self.assert_graphql_query_result(r"""
+            mutation insert_other__Foo {
+                insert_other__Foo(
+                    data: [{
+                        after: "BaseFoo10",
+                        color: GREEN,
+                        foos: [{
+                            filter: {
+                                after: {eq: "NestedFoo100"},
+                            }
+                        }, {
+                            data: {
+                                after: "NestedFoo101",
+                                color: BLUE,
+                            }
+                        }]
+                    }]
+                ) {
+                    after
+                    color
+                    foos(order: {color: {dir: ASC}}) {
+                        after
+                        color
+                    }
+                }
+            }
+        """, {
+            "insert_other__Foo": [data]
+        })
+
+        self.assert_graphql_query_result(validation_query, {
+            "other__Foo": [data]
+        })
+
+        self.assert_graphql_query_result(r"""
+            mutation delete_other__Foo {
+                delete_other__Foo(
+                    filter: {after: {like: "%Foo10%"}},
+                    order: {color: {dir: ASC}}
+                ) {
+                    after
+                    color
+                }
+            }
+        """, {
+            "delete_other__Foo": [{
+                'after': 'NestedFoo100',
+                'color': 'RED',
+            }, {
+                'after': 'BaseFoo10',
+                'color': 'GREEN',
+            }, {
+                'after': 'NestedFoo101',
+                'color': 'BLUE',
+            }]
+        })
+
     def test_graphql_mutation_insert_bad_01(self):
         with self.assertRaisesRegex(
                 edgedb.QueryError,
@@ -2824,6 +3011,220 @@ def test_graphql_mutation_update_multiple_01(self):
             }]
         })
 
+    def test_graphql_mutation_update_multiple_02(self):
+        # Issue #3470
+        # Test nested update of the same type.
+        data = {
+            'after': 'BaseFoo02',
+            'color': 'GREEN',
+            'foos': [{
+                'after': 'NestedFoo020',
+                'color': 'RED',
+            }, {
+                'after': 'NestedFoo021',
+                'color': 'BLUE',
+            }],
+        }
+
+        validation_query = r"""
+            query {
+                other__Foo(filter: {after: {eq: "BaseFoo02"}}) {
+                    after
+                    color
+                    foos(order: {color: {dir: ASC}}) {
+                        after
+                        color
+                    }
+                }
+            }
+        """
+
+        self.graphql_query(r"""
+            mutation insert_other__Foo {
+                insert_other__Foo(
+                    data: [{
+                        after: "NestedFoo020",
+                        color: RED
+                    }, {
+                        after: "BaseFoo02",
+                        color: GREEN
+                    }, {
+                        after: "NestedFoo021",
+                        color: BLUE
+                    }]
+                ) {
+                    color
+                }
+            }
+        """)
+
+        self.assert_graphql_query_result(r"""
+            mutation update_other__Foo {
+                update_other__Foo(
+                    filter: {after: {eq: "BaseFoo02"}},
+                    data: {
+                        foos: {
+                            set: [{
+                                filter: {
+                                    after: {like: "NestedFoo02%"},
+                                }
+                            }]
+                        }
+                    }
+                ) {
+                    after
+                    color
+                    foos(order: {color: {dir: ASC}}) {
+                        after
+                        color
+                    }
+                }
+            }
+        """, {
+            "update_other__Foo": [data]
+        })
+
+        self.assert_graphql_query_result(validation_query, {
+            "other__Foo": [data]
+        })
+
+        self.assert_graphql_query_result(r"""
+            mutation delete_other__Foo {
+                delete_other__Foo(
+                    filter: {after: {like: "%Foo02%"}},
+                    order: {color: {dir: ASC}}
+                ) {
+                    after
+                    color
+                }
+            }
+        """, {
+            "delete_other__Foo": [{
+                'after': 'NestedFoo020',
+                'color': 'RED',
+            }, {
+                'after': 'BaseFoo02',
+                'color': 'GREEN',
+            }, {
+                'after': 'NestedFoo021',
+                'color': 'BLUE',
+            }]
+        })
+
+    def test_graphql_mutation_update_multiple_03(self):
+        # Issue #3470
+        # Test nested update of the same type.
+        self.graphql_query(r"""
+            mutation insert_other__Foo {
+                insert_other__Foo(
+                    data: [{
+                        after: "NestedFoo030",
+                        color: RED
+                    }, {
+                        after: "BaseFoo03",
+                        color: GREEN
+                    }, {
+                        after: "NestedFoo031",
+                        color: BLUE
+                    }]
+                ) {
+                    color
+                }
+            }
+        """)
+
+        self.assert_graphql_query_result(r"""
+            mutation update_other__Foo {
+                update_other__Foo(
+                    filter: {after: {eq: "BaseFoo03"}},
+                    data: {
+                        foos: {
+                            add: [{
+                                filter: {
+                                    after: {like: "NestedFoo03%"},
+                                }
+                            }]
+                        }
+                    }
+                ) {
+                    after
+                    color
+                    foos(order: {color: {dir: ASC}}) {
+                        after
+                        color
+                    }
+                }
+            }
+        """, {
+            "update_other__Foo": [{
+                'after': 'BaseFoo03',
+                'color': 'GREEN',
+                'foos': [{
+                    'after': 'NestedFoo030',
+                    'color': 'RED',
+                }, {
+                    'after': 'NestedFoo031',
+                    'color': 'BLUE',
+                }],
+            }]
+        })
+
+        self.assert_graphql_query_result(r"""
+            mutation update_other__Foo {
+                update_other__Foo(
+                    filter: {after: {eq: "BaseFoo03"}},
+                    data: {
+                        foos: {
+                            remove: [{
+                                filter: {
+                                    after: {eq: "NestedFoo031"},
+                                }
+                            }]
+                        }
+                    }
+                ) {
+                    after
+                    color
+                    foos(order: {color: {dir: ASC}}) {
+                        after
+                        color
+                    }
+                }
+            }
+        """, {
+            "update_other__Foo": [{
+                'after': 'BaseFoo03',
+                'color': 'GREEN',
+                'foos': [{
+                    'after': 'NestedFoo030',
+                    'color': 'RED',
+                }],
+            }]
+        })
+
+        self.assert_graphql_query_result(r"""
+            mutation delete_other__Foo {
+                delete_other__Foo(
+                    filter: {after: {like: "%Foo03%"}},
+                    order: {color: {dir: ASC}}
+                ) {
+                    after
+                    color
+                }
+            }
+        """, {
+            "delete_other__Foo": [{
+                'after': 'NestedFoo030',
+                'color': 'RED',
+            }, {
+                'after': 'BaseFoo03',
+                'color': 'GREEN',
+            }, {
+                'after': 'NestedFoo031',
+                'color': 'BLUE',
+            }]
+        })
+
     def test_graphql_mutation_delete_alias_01(self):
         self.assert_graphql_query_result(r"""
             mutation insert_Setting {