Correct implementation of an interface

I have experienced a very strange behaviour - or at least I do not understand how Dgraph want’s this to be implemented - when it comes to interfaces. I guess it is partly related to this thread.

According to the GraphQL specs

An Interface is an abstract type that includes a certain set of fields that a type must include to implement the interface.

and

This means that any type that implements the interface needs to have the exact same fields as the interface, with these arguments and return types.

So far, so good. Let’s assume the following schema

# Schema ---

# The interface with some searchable fields
interface Implement {
  id: ID!
  some: String! @id
  more: String! @id
  fields: String @search
}

# Type One, which implements all the fields according to the specs
type One implements Implement {
  id: ID!
  one: Int
  
  # Implemented
  some: String! @id
  more: String! @id
  fields: String @search
}

# Type Two, which also implements the interface but I did not set the fields specifically 
type Two implements Implement {
  id: ID!
	two: Int
	
  # Implemented
  # some: String @id
  # more: String @id
  # fields: String @search
}

DQL Mutation to populate the database

I’m running a DQL mutation to populate the database. Since the interface itself should be queryable (and has an ID), I need to set both types One and Implement or Two and Implement when setting both types One and Two. Since the additional fields are implemented via the interface Implement, the predicate must be set to Implement.some, Implement.more, etc.

{
    "set": [
        {
            "uid": "_:nOneUid",
            "dgraph.type": ["One", "Implement"],
            "One.one": 1,

            "Implement.some": "unique_1",
            "Implement.more": "unique_2",
            "Implement.fields": "text for one"
        },
        {
            "uid": "_:nTwoUid",
            "dgraph.type": ["Two", "Implement"],
            "Two.two": 2,

            "Implement.some": "unique_3",
            "Implement.more": "unique_4",
            "Implement.fields": "text for two"
        }
    ]
}

Query results

Now I’ve run the queries for each type and the interface. It pretty much does exactly what I would expect. Both One and Two inherit both types One/Two and Implement. The value for the interface field some, is only accessible via Interface.some (this is clear since I did not define any other predicate in my DQL mutation).

# Query ---

{
    qOne(func: type(One)) {
        uid
        dgraph.type
        Implement.some
        One.some
    }

    qTwo(func: type(Two)) {
        uid
        dgraph.type
        Implement.some
        Two.some
    }

    qImplement(func: type(Implement)) {
        uid
        dgraph.type
        Implement.some
    }
}

The query result gives

{
  data: {
    qOne: [
      0: {
        uid: "0x1a4863389c"
        dgraph.type: [
          0: "One"
          1: "Implement"
        ]
        Implement.some: "unique_1"
      }
    ]
    qTwo: [
      0: {
        uid: "0x1a4863389d"
        dgraph.type: [
          0: "Two"
          1: "Implement"
         ]
         Implement.some: "unique_3"
       }
     ]
     qImplement: [
       0: {
         uid: "0x1a4863389c"
         dgraph.type: [
           0: "One"
           1: "Implement"
         ]
         Implement.some: "unique_1"
       }
      1: {
        uid: "0x1a4863389d"
        dgraph.type: [
          0: "Two"
          1: "Implement"
        ]
        Implement.some: "unique_3"
      }
   ]
}

The Dgraph Cloud Data Studio

This is where I first realised there is something different with the 2 types I have just set up.

Type One can be sorted by

  • one
  • some
  • more
  • fields

but type Two only has a sorting option for

  • two

Furthermore, if I set the sort parameter to eg. some for type One, the DQL query fails and I get the

Failed to retrieve the data

error.

The Network tab reveals

{
    "errors": [
        {
            "message": ": Cannot sort by unknown attribute One.more",
            "extensions": {
                "code": "ErrorInvalidRequest"
            }
        }
    ],
    "data": null
}

that Dgraph is looking for the predicate One.some, which obviously does not exist! This means that when actively implementing the interface fields into the type, somehow makes Dgraph “expect” that both predicates, “One.some” and “Interface.some” must exist.

Both implementations in the schema are GraphQL compliant if I’m not mistaken! So which is the right implementation? Would I have to add the missing predicates for type One? So sth. like this

{
    "set": [
        {
            "uid": "_:nOneUid",
            "dgraph.type": ["One", "Implement"],
            "One.one": 1,

            "Implement.some": "unique_1",
            "Implement.more": "unique_2",
            "Implement.fields": "text for one",

            "One.some": "unique_1",
            "One.more": "unique_2",
            "One.fields": "text for one"
        }
    ]
}

I’m really confused! :see_no_evil: Any help appreciated.

1 Like

Update

I have done some further investigation on this topic and it seems that Dgraph’s implementation of interfaces is NOT GraphQL compliant!

Tests

Since my naming in the last post was pretty confusing, I’ve tried to clean things up a bit and changed the schema to a hopefully more readable form

# Schema ---

interface Animal {
  id: ID!
  name: String! @id
}

type Dog implements Animal {
  id: ID!
  breed: String
  name: String! @id
}

type Cat implements Animal {
  id: ID!
  colour: String
}

type Ant implements Animal {
  hasVenom: Boolean
}

I have covered all three possible cases here:

  1. Dog → Repetition of all fields of the interface
  2. Cat → Repetition of only the ID field of the inter face
  3. AntNo repetition of any of the interface fields

Number 1 is the GraphQL conform implementation and 2/3 are “internally extended” by Dgraph to match the compliance. All implementations should be returning the same results when querying for the type. Unfortunately, this is not the case!

Mutations

I have also run two sets of mutations, a DQL and a GraphQL mutation, for each of the types. The DQL mutations represent my understanding of how Dgraph represents the interface implementation in the database. The GraphQL queries are just a test how Dgraph does actually save the data.

DQL mutation
# DQL Mutation

{
    "set": [
        {
            "uid": "_:nDogUid",
            "dgraph.type": ["Dog", "Animal"],
            "Dog.breed": "Bulldog",
            "Animal.name": "Bully"
        },
        {
            "uid": "_:nCatUid",
            "dgraph.type": ["Cat", "Animal"],
            "Cat.colour": "black",
            "Animal.name": "Kitty"
        },
        {
            "uid": "_:nAntUid",
            "dgraph.type": ["Animal"],
            "Ant.hasVenom": true,
            "Animal.name": "Badboy"
        }
    ]
}
GraphQL mutations
# GraphQL Mutations ---

mutation AddDog {
  addDog(
    input: {
      breed: "Labrador",
			name: "Bernie"
    }
  ) {
    numUids
  }
}

mutation AddCat {
  addCat(
    input: {
      colour: "white",
			name: "Snowflake"
    }
  ) {
    numUids
  }
}

mutation AddAnt {
  addAnt(
    input: {
      hasVenom: false,
			name: "Goodboy"
    }
  ) {
    numUids
  }
}

Query Results

This is where it gets interesting. Again I have different sorting options for Dog vs Cat, with the failing DQL query for Dog.name. The Ant Badboy which was created via DQL does not show up in theData Studio at all, the via GraphQL created Goodboy does.

If I run a DQL query on all the types for all possible predicates

# DQL Query ---
{
    qAnimal(func: type(Animal)) {
        dgraph.type
        Animal.name
        
    }

    qDog(func: type(Dog)) {
        dgraph.type
        Animal.name
        Dog.name
        Dog.breed 
    }

    qCat(func: type(Cat)) {
        dgraph.type
        Animal.name
        Cat.name
        Cat.colour 
    }

    qAnt(func: type(Ant)) {
        dgraph.type
        Animal.name
        Ant.name
        Ant.hasVenom 
    }
}

I get this result

# Result ---

{
  "data": {
    "qAnimal": [
      {
        "dgraph.type": [
          "Cat",
          "Animal"
        ],
        "Animal.name": "Kitty"
      },
      {
        "dgraph.type": [
          "Animal"
        ],
        "Animal.name": "Badboy"
      },
      {
        "dgraph.type": [
          "Animal",
          "Dog"
        ],
        "Animal.name": "Bully"
      },
      {
        "dgraph.type": [
          "Animal",
          "Dog"
        ],
        "Animal.name": "Bernie"
      },
      {
        "dgraph.type": [
          "Cat",
          "Animal"
        ],
        "Animal.name": "Snowflake"
      },
      {
        "dgraph.type": [
          "Animal",
          "Ant"
        ],
        "Animal.name": "Goodboy"
      }
    ],
    "qDog": [
      {
        "dgraph.type": [
          "Animal",
          "Dog"
        ],
        "Animal.name": "Bully",
        "Dog.breed": "Bulldog"
      },
      {
        "dgraph.type": [
          "Animal",
          "Dog"
        ],
        "Animal.name": "Bernie",
        "Dog.breed": "Labrador"
      }
    ],
    "qCat": [
      {
        "dgraph.type": [
          "Cat",
          "Animal"
        ],
        "Animal.name": "Kitty",
        "Cat.colour": "black"
      },
      {
        "dgraph.type": [
          "Cat",
          "Animal"
        ],
        "Animal.name": "Snowflake",
        "Cat.colour": "white"
      }
    ],
    "qAnt": [
      {
        "dgraph.type": [
          "Animal",
          "Ant"
        ],
        "Animal.name": "Goodboy",
        "Ant.hasVenom": false
      }
    ]
  },
  "extensions": {
    "server_latency": {
      "parsing_ns": 105492,
      "processing_ns": 3208408,
      "encoding_ns": 85736,
      "assign_timestamp_ns": 453735,
      "total_ns": 3947158
    },
    "txn": {
      "start_ts": 249258010,
      "hash": "63e299bd98e4e5e57eb732ceb9ed3e716fcda7bc939ad10affe7a41d22371ffc"
    },
    "metrics": {
      "num_uids": {
        "Animal.name": 11,
        "Ant.hasVenom": 1,
        "Ant.name": 1,
        "Cat.colour": 2,
        "Cat.name": 2,
        "Dog.breed": 2,
        "Dog.name": 2,
        "_total": 32,
        "dgraph.type": 11
      }
    }
  }
}

Conclusion

The implementation in Dog leads to false results since there will be two different predicates

  • Dog.name and
  • Animal.name

Although, it might be the most correct implementation since Dgraph needs a representation for every predicate and Dog.name and Animal.name is valid. Unfortunately, Dgraph’s own GraphQL mutation does not create the predicate Dog.name and in custom DQL mutations we would have to type out every field which has been implemented via the interface for each type (Animal, Dog).

The implementation in Cat is probably the one to go for. Dgraph probably does exactly this in a GraphQL mutation.

The implementation in Ant is probably similar to Cat but a bit more confusing, Dgraph always assigns a UID for each type so basically you also end up with id two times implemented.

Correct. It is simplified on the SDL side of things but spec compliance with the served schema.

In Dgraph an implementation of an interface automatically includes all fields of the interface.

This simplifies typing and directives.

I do not agree that this simplifies anything! In my opinion it makes things much more complicated or at least more confusing. It is nowhere stated that we are “not allowed” to type out the fields which are implemented via the interface - especially when this is the regular GraphQL syntax.

So if I understand correctly my problem is a follow up of the original thread.

So field repetition is allowed but it causes different implementations to a case where we omit the fields. And this should simply NOT be the case! However the interpretation of Dgraph’s “valid GraphQL" schema is, the representation of it should not change with the way we write out the schema.

So either Dgraph should always automatically generate the predicates for the missing type

  • Dog.name
  • Cat.name and
  • Ant.name

or there is only one predicate, Animal.name for all the types which implement the interface - even when repeating the fields.

Follow-up

I know it is a small community and people here are trying their best to answer questions and help unexperienced users like me out. But I’m wondering if this forum is mainly hosted by people who do not use or do not a connection to the Cloud product. I guess this is still the main focus of Dgraph since it seems to be the way Dgraph earns money.

I have submitted various “problems” which - at least in my opinion - seemed to be bugs. So far only one of them has been implemented or taken care of. I really like the product and I can really see the potential but for me as a paying customer it seems a bit out of scope to personally identify problems and spend days to figure out why something is not working.

So posts like this (and unfortunately also support tickets) get “ghosted” very quickly and thus unanswered which often leaves me with some “workaround solution” I had to come up with. I am not saying that Dgraph should be able to 100% fulfil my business logic but at least some things which are at least worth a discussion should be handled a bit more professionally.

So, regarding my question:

  • the implementation can break the Data Studio so (even if not a crucial feature) it would be awesome if there is at least a guideline of how we should implement interfaces.
  • it is a difference if we repeat the fields or not and therefore I think this should be fixed in one way or the other

Maybe I’m wrong and I don’t want to offend anyone here! It’s not so many posts and it would be awesome if one of the DEVs or so would focus more on getting (at least bug reports) solved.

I am also more than willing to contribute as much as I can with my limited knowledge! :stuck_out_tongue:

@MichelDiz @akon @matthewmcneely @mrjn @koppula @ashwin95r @pawan @rarvikar