Nodes aren't ordered correctly if the query+mutate are in the same txn

Moved from GitHub dgraph/5937

Posted by jostillmanns:

What version of Dgraph are you using?

v20.03.1

Have you tried reproducing the issue with the latest release?

no

Steps to reproduce the issue (command/config used to run Dgraph).

run this test:

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/require"
	"google.golang.org/grpc"
)

func Test_it_orders_correctly_after_update(t *testing.T) {
	conn, err := grpc.Dial(dgraphAddr(), 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})

	schema := `
type node {
  node.order: int
  node.children: [uid]
}

node.order: int .
node.children: [uid] .
`
	err = dg.Alter(context.Background(), &api.Operation{Schema: schema})
	require.NoError(t, err)

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

		Order    int    `json:"node.order"`
		Children []Node `json:"node.children"`
	}

	in := Node{
		ID:   "_:probe",
		Type: "node",
		Children: []Node{
			{
				Type:  "node",
				Order: 1,
			},
			{
				Type:  "node",
				Order: 2,
			},
			{
				Type:  "node",
				Order: 3,
			},
		},
	}

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

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

	q := `
query {
  nodes (func: uid(%s)) {
    uid
    node.children (orderasc: node.order) {
      uid
      node.order
    }
  }
}
`
	resp, err := dg.NewTxn().Query(context.Background(), fmt.Sprintf(q, mutateResp.Uids["probe"]))
	require.NoError(t, err)

	var res struct {
		Nodes []Node `json:"nodes"`
	}
	err = json.Unmarshal(resp.GetJson(), &res)
	require.NoError(t, err)

	txn := dg.NewTxn()

	delete, err := json.Marshal(map[string]interface{}{"uid": res.Nodes[0].ID, "node.childen": nil})
	require.NoError(t, err)

	_, err = txn.Mutate(context.Background(), &api.Mutation{DeleteJson: delete})
	require.NoError(t, err)

	update, err := json.Marshal(Node{
		ID: res.Nodes[0].ID,
		Children: []Node{
			{ID: res.Nodes[0].Children[2].ID, Order: 1},
			{ID: res.Nodes[0].Children[1].ID, Order: 2},
			{ID: res.Nodes[0].Children[0].ID, Order: 3},
		},
	})
	require.NoError(t, err)

	_, err = txn.Mutate(context.Background(), &api.Mutation{SetJson: update})

	resp, err = txn.Query(context.Background(), fmt.Sprintf(q, res.Nodes[0].ID))
	require.NoError(t, err)

	err = txn.Commit(context.Background())
	require.NoError(t, err)

	var postUpdate struct {
		Nodes []Node `json:"nodes"`
	}
	err = json.Unmarshal(resp.GetJson(), &postUpdate)
	require.NoError(t, err)

	require.Equal(t, 1, postUpdate.Nodes[0].Children[0].Order) // <- THIS FAILS
}

Expected behaviour and actual result.

the nodes referenced by node.children should be sorted correctly post .Mutate(), however the query yields nodes in incorrect order. I suspect this is because of the query being in the same txn as the .Mutate() statements, as the query shows a correct order outside of the txn.

3 Likes

any chance someone could take a look at this?

Hi @Joschka, I am looking into this and will get back soon.

Hi @Joschka,

I agree that this looks like a bug. When I read the same query again in a new transaction, the order is as expected and the test passes (just as you have already noted). I am reviewing this with the team and will update this post.

Thanks for your patience.

Regards,
Anand.

2 Likes

Hi @Joschka, just to confirm: we have opened a JIRA ticket for this and will be looking into this. Will keep you posted. Cheers!

1 Like

I can observe the same behavior for any function. The problem seems to be that no values are indexed during a txn. Here is another test which shows this:

package main

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

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

func Test_it_filters_within_txn(t *testing.T) {
	conn, err := grpc.Dial("localhost:9080", grpc.WithInsecure())
	require.NoError(t, err)

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

	schema := `
type node {
  node.createdAt: dateTime
}

node.createdAt: dateTime @index(hour) .
`
	err = dg.Alter(context.Background(), &api.Operation{Schema: schema})
	require.NoError(t, err)

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

		CreatedAt time.Time `json:"node.createdAt"`
	}

	now := time.Now()
	later := now.Add(time.Hour)

	js, err := json.Marshal(Node{
		ID:        "_:probe",
		Type:      "node",
		CreatedAt: now,
	})
	require.NoError(t, err)

	txn := dg.NewTxn()
	defer txn.Discard(context.Background())

	_, err = txn.Mutate(context.Background(), &api.Mutation{SetJson: js})
	require.NoError(t, err)

	q := `
query {
  nodes (func: type(node)) {
    uid
    node.createdAt
  }
}
`

	resp, err := txn.Query(context.Background(), q)
	require.NoError(t, err)

	var res struct {
		Nodes []Node `json:"nodes"`
	}
	err = json.Unmarshal(resp.GetJson(), &res)
	require.NoError(t, err)
	require.Len(t, res.Nodes, 1)
	require.Equal(t, now.Round(time.Second), res.Nodes[0].CreatedAt.Round(time.Second))
	require.True(t, res.Nodes[0].CreatedAt.Before(later))

	q = `
query {
  nodes (func: type(node)) @filter(le(node.createdAt, "%s")) {
    uid
    node.createdAt
  }
}
`
	resp, err = txn.Query(context.Background(), fmt.Sprintf(q, later.Format(time.RFC3339)))
	require.NoError(t, err)

	var filtered struct {
		Nodes []Node `json:"nodes"`
	}

	err = json.Unmarshal(resp.GetJson(), &filtered)
	require.NoError(t, err)
	require.Len(t, filtered.Nodes, 1)
	require.Equal(t, now.Round(time.Second), filtered.Nodes[0].CreatedAt.Round(time.Second))
}

Has there been any progress on this issue? Being able to use functions during a txn is even more critical for me than the other issue. Is there anything I can do to resolve this issue?

Hi @Joschka, thanks for checking on this issue. We haven’t yet been able to work on this due to other priorities. I am following up to see when we can get the fix scheduled.

1 Like

Hi @Joschka, I wanted to confirm that we have added this bug as a to-do task to our next sprint. Our team will update this thread as we make progress.

1 Like

Hi @Joschka,

Thanks for reporting this issue.
I am working on a fix for this and would get back to you shortly with an update on this.

4 Likes

Hi @Joschka,

This is the expected behavior. Indices don’t work for uncommitted transactions. This has been addressed before here.

Please mark this resolved if this addresses your issue.

Uhm, yes, this is exactly my issue, but simply not supporting indices for uncomitted txns doesn’t resolve it. You can transform this issue into a feature request if you want.

HI @Joschka
Here are the links for upsert block as well as conditional upsert.
Please let us know how it goes.
Cheers!
Thanks and regards.
Anand.