-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Summary
When creating nodes with indexed datetime predicates within a transaction, those predicates are not visible to subsequent queries using index-based filters within the same uncommitted transaction. The query returns empty results even though the data was successfully mutated in the same transaction context.
Environment
- Dgraph Version: v24.1.3
- Client Library:
github.com/dgraph-io/dgo/v240 - Language: Go
- Running 1 Alpha node, 1 Zero node, 1 Ratel node
Schema
type <Element> {
xid
}
type <st.Element> {
dw.from
dw.parented_by
dw.state_of
dw.to
}
<dw.from>: datetime @index(hour) .
<dw.parented_by>: uid @reverse .
<dw.state_of>: uid @reverse .
<dw.to>: datetime @index(hour) .
<xid>: string @index(exact) .
Reproduction Steps
- Start a new transaction using
dg.NewTxn() - Mutate (create) a node of type
st.Elementwith predicatesdw.fromanddw.toset to valid datetime values - Within the same transaction (before commit), execute a query that filters on the indexed
dw.fromanddw.topredicates - Observe that the query returns empty results
Example Code
Please note that we observed this issue very sporadicly and have not been able to pin point exactly why it works some of the time and not other times.
package main
import (
"context"
"fmt"
"time"
"github.com/dgraph-io/dgo/v240"
"github.com/dgraph-io/dgo/v240/protos/api"
"google.golang.org/grpc"
)
func main() {
conn, _ := grpc.Dial("localhost:9080", grpc.WithInsecure())
dgraphClient := dgo.NewDgraphClient(api.NewDgraphClient(conn))
ctx := context.Background()
txn := dgraphClient.NewTxn()
defer txn.Discard(ctx)
now := time.Now()
futureTime := now.Add(24 * time.Hour)
// Create a new st.Element node with indexed datetime predicates
createMutation := &api.Mutation{
SetJson: []byte(fmt.Sprintf(`{
"xid": "test-element-123",
"dgraph.type": "st.Element",
"dw.from": "%s",
"dw.to": "%s"
}`, now.Format(time.RFC3339), futureTime.Format(time.RFC3339))),
}
_, err := txn.Mutate(ctx, createMutation)
if err != nil {
panic(err)
}
// Query for the newly created node using indexed predicates
// This should find the node we just created, but returns empty
queryTime := now.Add(time.Minute).Format(time.RFC3339)
query := fmt.Sprintf(`
{
Root(func: eq(xid, "test-element-123")) {
g as ~dw.state_of
@filter((type(st.Element)
AND lt(dw.from, "%s")
AND ge(dw.to, "%s")))
}
Details(func: uid(g)) {
uid
xid
dw.from
dw.to
}
}`, queryTime, queryTime)
resp, err := txn.Query(ctx, query)
if err != nil {
panic(err)
}
// EXPECTED: Details should contain our newly created node
// ACTUAL: Details is empty
fmt.Println(string(resp.Json))
// If we commit first, then query in a new transaction, it works:
// txn.Commit(ctx)
// newTxn := dgraphClient.NewTxn()
// resp, _ = newTxn.Query(ctx, query)
// fmt.Println(string(resp.Json)) // Now returns the node
}Observed Query That Fails
This is one of our exact queries if it helps with investigations however please take note of the workarounds we already tried.
query q($time: string, $xid: string) {
Root(func: eq(xid, $xid)) {
g as ~dw.state_of
@filter((type(st.Element)
AND lt(dw.from, $time)
AND ge(dw.to, $time)))
}
Structure(func: eq(xid, $xid))
@recurse {
Tag : uid
Values : dw.state_of
@filter(type(Element))
f as Nodes : ~dw.parented_by
@filter((type(st.Element)
AND lt(dw.from, $time)
AND ge(dw.to, $time)))
}
Details(func: uid(f, g)) {
uid
dgraph.type
expand (_all_) {
xid
uid
}
}
}
Dgraph Logs
The following warnings appear in the Dgraph logs when the query is executed within the uncommitted transaction:
2025-07-23 23:04:35 I0724 04:04:35.496869 1 query.go:1783] Warning: reached default case in fillVars for var: f
2025-07-23 23:04:35 I0724 04:04:35.496919 1 query.go:1783] Warning: reached default case in fillVars for var: g
This indicates that variables f and g are not being populated, meaning the filters on the indexed predicates are not matching the newly created node.
Expected Behavior
Within a transaction, reads should be able to see uncommitted writes (read-your-own-writes consistency). The query filtering on dw.from and dw.to should return the node that was created earlier in the same transaction.
Actual Behavior
The query returns empty results for variables f and g, even though the node with matching predicate values was created in the same transaction.
Workarounds Tested (All Failed)
- Changed filter operators: Tried
leinstead oflt,gtinstead ofge- no change - Added time delay: Added
time.Sleep()between mutation and query - no change - Different time precision: Tried with different time formats/precision - no change
Workarounds That Work (But Are Not Acceptable)
- Commit transaction first: If we commit the mutation transaction and then query in a new transaction, the data is visible (defeats purpose of atomic transactions)
- Remove indexes: If we remove
@index(hour)fromdw.fromanddw.topredicates, the filtering works within the same transaction (but we need the indexes for production queries)
Hypothesis
This appears to be a bug in Dgraph's transaction isolation implementation. When indexes are present, it seems the index lookup does not consult the transaction's uncommitted write buffer, only the committed state. Non-indexed predicates (like xid with @index(exact)) and type filters (type(st.Element)) seem to work correctly.
Impact
This bug breaks atomic transactional workflows where:
- A node is created/updated with indexed predicates
- Subsequent operations in the same transaction need to query for that node using index-based filters
- The entire operation needs to be atomic (all-or-nothing)
Additional Context
- The
xidpredicate with@index(exact)works correctly within transactions - Type filtering (
type(st.Element)) works correctly within transactions - The issue specifically manifests with
datetime @index(hour)predicates when used in comparison filters (lt,le,gt,ge)