Unexpected bahaviour when mutating 1-to-1 relations

Hi there,

we currently upgraded to Dgraph 1.1. With this upgrade, we also changed our schema to use 1-to-1 relations where applicable. When I now perform a mutation on an object with a 1-to-1 relation set, I expected that Dgraph simply overwrites the old uid just as it does with literal values. Nevertheless, I got this error here:

rpc error: code = Unknown desc = 
cannot add value with uid 9eb45 to predicate 'surveyProcess.userScopeTree' because one 
of the existing values does not match this uid, either delete the existing values
first or modify the schema to 'surveyProcess.userScopeTree: [uid]'

This behaviour increases the complexity of the code I have to write since I first have to delete the existing edge in case there is already one and the write the new one. Can someone explain me, why Dgraph doesn’t handle that for me?

Thanks Sebastian

To ensure that the relationship will be 1:1 you need to set the schema before any mutations/operations. Cuz when you mutate, you eventually will create new value types. It means Dgraph will assume types if you don’t define them. In this case, if you don’t define the pred as “pred: uid .”. It will assume that it is 1:N and set as “pred: [uid]”.

I predefined the schema before the mutation. The schema looks like this:

type surveyProcess {
  surveyProcess.userScopeTree: uid
}

surveyProcess.userScopeTree: uid @reverse .

Otherwise, I think I wouldn’t have got this error.

I also thought the general idea behind introducing [uid] was to uniform handling of all predicates. Shouldn’t this mean I can simply replace a node reference using a uid predicate

<predicate>: uid .

as I do with, for instance a string and a string predicate <predicate>: string .?

I didn’t get what you mean. A predicate can’t have scalar type and UID type at the same time.

Dgraph has always been [uid] implicitly, what was introduced was uid means 1: 1.

Would you mind to create a reproducible sample (with all steps) so I can verify and give some actions in case of positive bug? otherwise, I can understand what you doing and guide you to do the right way.

This test reproduces the error I mentioned and the behaviour @Joschka described in his post:

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"testing"

	"github.com/dgraph-io/dgo/v2"
	"github.com/dgraph-io/dgo/v2/protos/api"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"google.golang.org/grpc"
)

func Test_it_replaces_uid_predicates(t *testing.T) {
	conn, err := grpc.Dial("localhost:9080", grpc.WithInsecure())
	if err != nil {
		t.Fatalf("conntect: %v", err)
	}

	dg := dgo.NewDgraphClient(api.NewDgraphClient(conn))
	defer dg.Alter(context.Background(), &api.Operation{DropAll: true})

	err = dg.Alter(context.Background(), &api.Operation{
		Schema: `
type Node {
  name: string
  child: uid
}

name: string .
child: uid .
`,
	})
	require.NoError(t, err)

	type Node struct {
		UID  string `json:"uid"`
		Type string `json:"dgraph.type"`

		Name  string `json:"name"`
		Child *Node  `json:"child"`
	}

	in := Node{
		UID:  "_:parent",
		Type: "Node",
		Name: "parent",
		Child: &Node{
			UID:  "_:child",
			Type: "Node",
			Name: "child",
		},
	}

	js, err := json.Marshal(in)
	require.NoError(t, err)

	res, err := dg.NewTxn().Mutate(context.Background(), &api.Mutation{CommitNow: true, SetJson: js})
	require.NoError(t, err)

	parentUID := res.GetUids()["parent"]

	update := Node{
		UID:  res.GetUids()["parent"],
		Type: "Node",
		Child: &Node{
			UID:  "_:child",
			Type: "Node",
			Name: "child replacement",
		},
	}

	js, err = json.Marshal(update)
	require.NoError(t, err)

	res, err = dg.NewTxn().Mutate(context.Background(), &api.Mutation{CommitNow: true, SetJson: js})
	assert.NoError(t, err)

	q := `
query {
  n(func: uid(%s)) {
    uid
    name
    child {
      uid
      name
    }
  }
}
`

	queryRes, err := dg.NewReadOnlyTxn().Query(context.Background(), fmt.Sprintf(q, parentUID))
	require.NoError(t, err)

	var actual map[string][]Node
	err = json.Unmarshal(queryRes.GetJson(), &actual)
	require.NoError(t, err)

	require.Len(t, actual["n"], 1)
	assert.Equal(t, "parent", actual["n"][0].Name)
	assert.Equal(t, "child replacement", actual["n"][0].Child.Name)
}

Hi Sebastian,

Okay, I think I get it*

I did my own test based in yours related to http://discuss.dgraph.io/t/unexpected-bahaviour-when-mutating-1-to-1-relations/5180 · GitHub
the logs are the same

The point is, you have already a child over there right? so you wanna “update” it. To do so, you need to delete the actual child over there and then add a new child. You can’t overwrite it. Dgraph can’t assume that. You should do a proper update procedure.

So, what I would recommend? use Upsert Transaction. Check this tests dgo/upsert_test.go at master · dgraph-io/dgo · GitHub

The Upsert TXN is the fasted way to do what you want. In the Upsert you should use a logic like.

1 - Find the Parent and the Child.
2 - With Parent and Child’s UIDs you use the Child one to delete the relation and delete the Child itself.
e.g:

bulkDelete := `
[
  {
    "uid": "uid(parent)", 
    "child": null # This deletes the relation between parent and child
  },
  {
    "uid": "uid(child)" # This deletes the child itself
  }
]`

3 - You can in the same bulk upsert transaction do one more action. That should be the “update” you wanna do. So you need to understand the multiple block usage in upsert transaction. (I’m not aware of how to do it in Dgo, but looking at the test shared you should be able to find something.)
e.g:

	update := `
[
  {
    "uid": "uid(parent)",
    "child": {
        "uid": "_:NewChild",
        "name": "child replacement"
    }
  }
]`

That’s it. This log you received

rpc error: code = Unknown desc = cannot add value with uid a3948 
to predicate child because one of the existing values does not match 
this uid, either delete the existing values first or modify the schema 
to 'child: [uid]'

only means that Dgraph stopped you from doing a possible mistake.

BTW, instead of deleting the Child, you can update only the Child itself if it is the whole point. No need of getting the parent node and try to change the child. If you wanna update the name of the child you should get only the Child UID. No need to touch the parent.

You can’t overwrite it. Dgraph can’t assume that.

@MichelDiz could you elaborate on this? For me, simply replacing a uid would be perfectly expected behaviour, as with any other type, such as string this isn’t an issue.

Scalar types works differently other than UID type. If you want this behavior, please fill up an issue.

The problem here is that in Scalar types, Dgraph overwrites, but leaves no artifacts/duplicates/things behind. For example, if you overwrite a relationship. Dgraph would only deal with the relationship, leaving the entity removed there intact unnecessarily.

Imagine you doing it thousands of times, you will have hundreds of thousands of nodes dangling around. With no use.

So, dealing with relations and entities in Dgraph is a thing that the user must deal with. It can be tricky to create a determined behave and not break something. Thus making the user complaining about it.

However, please fill up an issue for the case.

In addition to what Michel said, the reason for this warning is that 1-1 uid relationships are a new feature (which uses the syntax of what was previously a 1 to many relationship) and we didn’t want users to overwrite their data by mistake. For now you’ll have to update this triples by deleting and adding.

We might remove the warning in a future release once most users are aware of the distinction between predicates of type uid and [uid].

I created an issue Allow node reference replacements on 1-to-1 relationships · Issue #4136 · dgraph-io/dgraph · GitHub

2 Likes