GraphQL query colides with Dgraph's predicate (bug with pre-existing data)

Darwin - v20.11.0-gea15b664a

Context

I was trying to reproduce this

With an existing Dgraph instance. Which should be fine, as GraphQL in Dgraph has a kind of “namespacing” right? So, I have found this bug with preexisting data. I have started to do what is in the steps and then I have responses like this:

-------------- Query ----------------
query {
  queryUser {
    username
  }
}
-------------- Response ----------------
{
  "errors": [
    {
      "message": "Non-nullable field 'username' (type String!) was not present in result from Dgraph.  GraphQL error propagation triggered.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "queryUser",
        0,
        "username"
      ]
    },
    {
      "message": "Non-nullable field 'username' (type String!) was not present in result from Dgraph.  GraphQL error propagation triggered.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "queryUser",
        1,
        "username"
      ]
    },
    {
      "message": "Non-nullable field 'username' (type String!) was not present in result from Dgraph.  GraphQL error propagation triggered.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "queryUser",
        2,
        "username"
      ]
    },
    {
      "message": "Non-nullable field 'username' (type String!) was not present in result from Dgraph.  GraphQL error propagation triggered.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "queryUser",
        3,
        "username"
      ]
    },
    {
      "message": "Non-nullable field 'username' (type String!) was not present in result from Dgraph.  GraphQL error propagation triggered.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "queryUser",
        4,
        "username"
      ]
    },
    {
      "message": "Non-nullable field 'username' (type String!) was not present in result from Dgraph.  GraphQL error propagation triggered.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "queryUser",
        5,
        "username"
      ]
    },
    {
      "message": "Non-nullable field 'username' (type String!) was not present in result from Dgraph.  GraphQL error propagation triggered.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "queryUser",
        6,
        "username"
      ]
    },
    {
      "message": "Non-nullable field 'username' (type String!) was not present in result from Dgraph.  GraphQL error propagation triggered.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "queryUser",
        7,
        "username"
      ]
    },
    {
      "message": "Non-nullable field 'username' (type String!) was not present in result from Dgraph.  GraphQL error propagation triggered.",
      "locations": [
        {
          "line": 3,
          "column": 5
        }
      ],
      "path": [
        "queryUser",
        8,
        "username"
      ]
    }
  ],
  "data": {
    "queryUser": [
      null,
      null,
      null,
      null,
      null,
      null,
      null,
      null,
      null,
      {
        "username": "alice@dgraph.io"
      }
    ]
  },
  "extensions": {
    "touched_uids": 20,
    "tracing": {
      "version": 1,
      "startTime": "2020-08-22T01:24:37.029328-03:00",
      "endTime": "2020-08-22T01:24:37.042452-03:00",
      "duration": 13122978,
      "execution": {
        "resolvers": [
          {
            "path": [
              "queryUser"
            ],
            "parentType": "Query",
            "fieldName": "queryUser",
            "returnType": "[User]",
            "startOffset": 114310,
            "duration": 12950998,
            "dgraph": [
              {
                "label": "query",
                "startOffset": 166590,
                "duration": 12844688
              }
            ]
          }
        ]
      }
    }
  }
}

In the beginning, I thought that I had done something wrong in the steps, but no, it was all fine on my side. I suspected that something else was wrong with the dataset. So I saw “Alice” along with several “Nulls”. So I noticed that Dgraph GraphQL was querying for existing data with other predicate name.

-------------- Query ----------------
query {
  queryUser {
     name
  }
}
-------------- Response ----------------
{
  "data": {
    "queryUser": [
      {
        "name": null
      },
      {
        "name": null
      },
      {
        "name": null
      },
      {
        "name": null
      },
      {
        "name": null
      },
      {
        "name": null
      },
      {
        "name": null
      },
      {
        "name": null
      },
      {
        "name": null
      },
      {
        "name": "Alice"
      }
    ]
  },
  "extensions": {
    "touched_uids": 20,
    "tracing": {
      "version": 1,
      "startTime": "2020-08-22T01:29:41.162099-03:00",
      "endTime": "2020-08-22T01:29:41.165061-03:00",
      "duration": 2961470,
      "execution": {
        "resolvers": [
          {
            "path": [
              "queryUser"
            ],
            "parentType": "Query",
            "fieldName": "queryUser",
            "returnType": "[User]",
            "startOffset": 90370,
            "duration": 2851000,
            "dgraph": [
              {
                "label": "query",
                "startOffset": 126450,
                "duration": 2766050
              }
            ]
          }
        ]
      }
    }
  }
}

The weird part is that Dgraph should query for <User.name> not <name> and <User.name> at the same time. This can lead to issues with existing datasets in users contexts.

Prints

It’s a little hard to see what’s going on here. So I’m going to make some assumptions.

Looks like you have some existing data with things of type User that have a name predicate. Then I’m guessing that you’ve added a GraphQL schema like

type User {
   username: String!
   name: String
} 

so what happens is that GraphQL maps those to the predicates User.username and User.name. Note though that you’ve said that username should never be null. So when you query for

query {
  queryUser {
    username
  }
}

You’ll find all the existing users by type … but they don’t have User.username predicate, so GraphQL tells you that it found some users that were in an error state. So those errors make sense.

If you want to change how the GraphQL schema maps down to an existing Dgraph schema, then you need these instructions and do something like

type User {
   ...
   name: String @dgraph(pred: "name")
} 

Let me know if my assumptions were wrong there.

Yes. I had used Dgraph’s Type Def previously and the dataset has <dgraph.type> "User", in the case used

#PS. This is a Dgraph Type. Not GraphQL.
type User {
   name
   (...and other preds)
}

Seems like the GraphQL overwrites it.

The schema is exactly this one graphql-sample-apps/todo-app-react/schema.graphql at master · dgraph-io/graphql-sample-apps · GitHub

The assumptions are close to right, but the solution isn’t good enough. But might work in other cases.

This solution solves part of the issue, but it “reverts” the query. Now I can see all previous users, except the one made specifically to the Todo example in the repo. In this case, “Alice” is null now.

The issue here is that seems like Dgraph can’t handle well, two models at the same time. The GraphQL way and Dgraph way. The mapping you have shown is really nice to someone who wanna use GraphQL without converting their predicate naming to <User.name> .... They can just map it. But I would prefer that the result of this context would ignore my previous data, maybe using “cascade”. Assuming that the query:

query {
  queryUser {
    ...
  }
}

does internally a DQL query like this

{
  q(func: type(User)) {
    User.name
  }
}

In fact this query above returns only the desired node. I can return all the other ones if I use “has” instead of ‘type()’. Which is odd, the GraphQL query should return only Alice as Dgraph does using DQL, not the 9 other users. If I add “name” to that last query, I can see all of them.

So, what the GraphQL does under the hood? a has func?

No, GraphQL executes a type filter as you mentioned. So

query {
  queryUser {
    name
  }
}

would execute

{
  q(func: type(User)) {
    User.name
  }
}

if @dgraph directive is not used.

The GraphQL API allows you to define non-null fields. If you have existing data for a type in Dgraph, then those fields might be null because ± doesn’t enforce any such non-null constraints. It is possible that these two might not intermix properly.

Do you want to be able to query the data that was already there earlier using the GraphQL API or do you want the API to only return the data that you wrote using GraphQL mutations?

1 Like

Not quite sure, cuz for some reason if I run this query on Dgraph. It will return only Alice - which is the right response. But on the GraphQL side, it will return all of them as Null and Alice no matter what. That’s what confuses me. Cuz all the other’s aren’t part of the GraphQL context, just Alice.

Again:

This returns Alice

{
  q(func: type(User)) {
    User.name
  }
}

This returns the users created via DQL/RDF

{
  q(func: type(User)) {
   name
  }
}

And this returns all of them, as GraphQL does.

{
  q(func: type(User)) {
   name
   User.name
  }
}

The last one should be the right result, by logic - as the GraphQL has a type of convention that does a kind of “namespacing” - For me DQL and GraphQL should live together and I choose what to expose via GraphQL. Also, the data on Dgraph was made for DQL, not GraphQL. Would be good to have a nice separation and use the instructions here https://dgraph.io/docs/graphql/dgraph/ if I desire to migrate my existing data to GraphQL. Also, I could use Upsert Block as this https://dgraph.io/docs/mutations/upsert-block/#example-of-val-function
e.g

upsert {
  query {
    v as var(func: has(age)) {
      a as age
    }
  }

  mutation {
    # we copy the values from the old predicate
    set {
      uid(v) <User.age> val(a) .
    }

    # and we delete the old predicate
    delete {
      uid(v) <age> * .
    }
  }
}

For me, it is a collision for two reasons. 1. The other users were made in DQL and their predicates aren’t in the GraphQL Schema. The only similarity between the data is the existence of the same <dgraph.type> nothing more. 2. The GraphQL should behave as the DQL query mentioned (q(func: type(User))) and not like:

{
  q(func: type(User)) {
   name
   User.name
   expan(_all_)
  }
}