Skip to content

Commit 1f4e3d1

Browse files
committed
fix: stop the document fetch from reading the next document's key
1 parent d97a72d commit 1f4e3d1

2 files changed

Lines changed: 71 additions & 4 deletions

File tree

internal/db/fetcher/document.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,10 @@ func newDocumentFetcher(
6969
prefix = prefix.WithDeletedFlag()
7070
}
7171

72-
iterOptions := datastore.IterOptions{
73-
Start: prefix,
74-
End: prefix.PrefixEnd(),
75-
}
72+
// A prefix iterator bounds the scan in the store. A start/end range leaves it unbounded
73+
// and checks the end in Go, which reads the first key past the document and puts another
74+
// document's key in this transaction's read set.
75+
iterOptions := datastore.IterOptions{Prefix: prefix}
7676

7777
keysOnly := len(fieldsByID) == 0
7878
if keysOnly {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2026 Democratized Data Foundation
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the file licenses/BSL.txt.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0, included in the file
9+
// licenses/APL.txt.
10+
11+
package db
12+
13+
import (
14+
"context"
15+
"testing"
16+
17+
"github.com/stretchr/testify/require"
18+
19+
"github.com/sourcenetwork/defradb/client"
20+
)
21+
22+
// Reading one document must not put another document's keys in the transaction's read set.
23+
// Documents sit consecutively in the store, so a scan of one that runs past its end lands on
24+
// the next document, and a concurrent write there then rejects a transaction that never
25+
// touched it.
26+
func TestDocumentFetcher_ReadingOneDocDoesNotConflictWithItsNeighbour(t *testing.T) {
27+
ctx := context.Background()
28+
29+
db, err := newBadgerDB(ctx)
30+
require.NoError(t, err)
31+
t.Cleanup(func() { db.Close() })
32+
33+
_, err = db.AddCollection(ctx, userSchema)
34+
require.NoError(t, err)
35+
col, err := db.GetCollectionByName(ctx, "User")
36+
require.NoError(t, err)
37+
38+
first, err := client.NewDocFromJSON(ctx, []byte(`{"name":"alice","age":1}`), col.Version())
39+
require.NoError(t, err)
40+
require.NoError(t, col.AddDocument(ctx, first))
41+
42+
second, err := client.NewDocFromJSON(ctx, []byte(`{"name":"bob","age":2}`), col.Version())
43+
require.NoError(t, err)
44+
require.NoError(t, col.AddDocument(ctx, second))
45+
46+
// Read the first document and write it back, all in one transaction.
47+
txn, err := db.NewTxn(false)
48+
require.NoError(t, err)
49+
defer txn.Discard()
50+
txnCtx := InitContext(ctx, txn)
51+
52+
read, err := col.GetDocument(txnCtx, first.ID())
53+
require.NoError(t, err)
54+
require.NoError(t, read.Set(ctx, "age", 10))
55+
require.NoError(t, col.UpdateDocument(txnCtx, read))
56+
57+
// Someone else rewrites the neighbour and commits, in between. Every field is written so
58+
// the test does not depend on which one holds the lowest field ID.
59+
other, err := col.GetDocument(ctx, second.ID())
60+
require.NoError(t, err)
61+
require.NoError(t, other.Set(ctx, "name", "robert"))
62+
require.NoError(t, other.Set(ctx, "age", 99))
63+
require.NoError(t, col.UpdateDocument(ctx, other))
64+
65+
require.NoError(t, txn.Commit(),
66+
"reading one document must not conflict with a write to the next one")
67+
}

0 commit comments

Comments
 (0)