Skip to content

Commit 2d48e1d

Browse files
committedMay 2, 2024·
Ensure a cursor can continue to iterate elements in reverse direction by call Next when it has already reached the beginning
Signed-off-by: Benjamin Wang <[email protected]>
1 parent 886753c commit 2d48e1d

File tree

3 files changed

+162
-5
lines changed

3 files changed

+162
-5
lines changed
 

‎README.md

+19-4
Original file line numberDiff line numberDiff line change
@@ -421,10 +421,19 @@ Prev() Move to the previous key.
421421
```
422422

423423
Each of those functions has a return signature of `(key []byte, value []byte)`.
424-
When you have iterated to the end of the cursor then `Next()` will return a
425-
`nil` key. You must seek to a position using `First()`, `Last()`, or `Seek()`
426-
before calling `Next()` or `Prev()`. If you do not seek to a position then
427-
these functions will return a `nil` key.
424+
You must seek to a position using `First()`, `Last()`, or `Seek()` before calling
425+
`Next()` or `Prev()`. If you do not seek to a position then these functions will
426+
return a `nil` key.
427+
428+
When you have iterated to the end of the cursor, then `Next()` will return a
429+
`nil` key and the cursor still points to the last element if present. When you
430+
have iterated to the beginning of the cursor, then `Prev()` will return a `nil`
431+
key and the cursor still points to the first element if present.
432+
433+
If you remove key/value pairs during iteration, the cursor may automatically
434+
move to the next position if present in current node each time removing a key.
435+
When you call `c.Next()` after removing a key, it may skip one key/value pair.
436+
Refer to [pull/611](https://github.com/etcd-io/bbolt/pull/611) to get more detailed info.
428437

429438
During iteration, if the key is non-`nil` but the value is `nil`, that means
430439
the key refers to a bucket rather than a value. Use `Bucket.Bucket()` to
@@ -850,6 +859,12 @@ Here are a few things to note when evaluating and using Bolt:
850859
to grow. However, it's important to note that deleting large chunks of data
851860
will not allow you to reclaim that space on disk.
852861

862+
* Removing key/values pairs in a bucket during iteration on the bucket using
863+
cursor may not work properly. Each time when removing a key/value pair, the
864+
cursor may automatically move to the next position if present. When users
865+
call `c.Next()` after removing a key, it may skip one key/value pair.
866+
Refer to https://github.com/etcd-io/bbolt/pull/611 for more detailed info.
867+
853868
For more information on page allocation, [see this comment][page-allocation].
854869

855870
[page-allocation]: https://github.com/boltdb/bolt/issues/308#issuecomment-74811638

‎cursor.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func (c *Cursor) Last() (key []byte, value []byte) {
7171

7272
// If this is an empty page (calling Delete may result in empty pages)
7373
// we call prev to find the last page that is not empty
74-
for len(c.stack) > 0 && c.stack[len(c.stack)-1].count() == 0 {
74+
for len(c.stack) > 1 && c.stack[len(c.stack)-1].count() == 0 {
7575
c.prev()
7676
}
7777

@@ -254,6 +254,15 @@ func (c *Cursor) prev() (key []byte, value []byte, flags uint32) {
254254
elem.index--
255255
break
256256
}
257+
// If we've hit the beginning, we should stop moving the cursor,
258+
// and stay at the first element, so that users can continue to
259+
// iterate over the elements in reverse direction by calling `Next`.
260+
// We should return nil in such case.
261+
// Refer to https://github.com/etcd-io/bbolt/issues/733
262+
if len(c.stack) == 1 {
263+
c.first()
264+
return nil, nil, 0
265+
}
257266
c.stack = c.stack[:i]
258267
}
259268

‎cursor_test.go

+133
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,143 @@ import (
1111
"testing"
1212
"testing/quick"
1313

14+
"github.com/stretchr/testify/require"
15+
1416
bolt "go.etcd.io/bbolt"
1517
"go.etcd.io/bbolt/internal/btesting"
1618
)
1719

20+
// TestCursor_RepeatOperations verifies that a cursor can continue to
21+
// iterate over all elements in reverse direction when it has already
22+
// reached to the end or beginning.
23+
// Refer to https://github.com/etcd-io/bbolt/issues/733
24+
func TestCursor_RepeatOperations(t *testing.T) {
25+
testCases := []struct {
26+
name string
27+
testFunc func(t2 *testing.T, bucket *bolt.Bucket)
28+
}{
29+
{
30+
name: "Repeat NextPrevNext",
31+
testFunc: testRepeatCursorOperations_NextPrevNext,
32+
},
33+
{
34+
name: "Repeat PrevNextPrev",
35+
testFunc: testRepeatCursorOperations_PrevNextPrev,
36+
},
37+
}
38+
39+
for _, tc := range testCases {
40+
t.Run(tc.name, func(t *testing.T) {
41+
db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: 4096})
42+
43+
bucketName := []byte("data")
44+
45+
_ = db.Update(func(tx *bolt.Tx) error {
46+
b, _ := tx.CreateBucketIfNotExists(bucketName)
47+
testCursorRepeatOperations_PrepareData(t, b)
48+
return nil
49+
})
50+
51+
_ = db.View(func(tx *bolt.Tx) error {
52+
b := tx.Bucket(bucketName)
53+
tc.testFunc(t, b)
54+
return nil
55+
})
56+
})
57+
}
58+
}
59+
60+
func testCursorRepeatOperations_PrepareData(t *testing.T, b *bolt.Bucket) {
61+
// ensure we have at least one branch page.
62+
for i := 0; i < 1000; i++ {
63+
k := []byte(fmt.Sprintf("%05d", i))
64+
err := b.Put(k, k)
65+
require.NoError(t, err)
66+
}
67+
}
68+
69+
func testRepeatCursorOperations_NextPrevNext(t *testing.T, b *bolt.Bucket) {
70+
c := b.Cursor()
71+
c.First()
72+
startKey := []byte(fmt.Sprintf("%05d", 2))
73+
returnedKey, _ := c.Seek(startKey)
74+
require.Equal(t, startKey, returnedKey)
75+
76+
// Step 1: verify next
77+
for i := 3; i < 1000; i++ {
78+
expectedKey := []byte(fmt.Sprintf("%05d", i))
79+
actualKey, _ := c.Next()
80+
require.Equal(t, expectedKey, actualKey)
81+
}
82+
83+
// Once we've reached the end, it should always return nil no matter how many times we call `Next`.
84+
for i := 0; i < 10; i++ {
85+
k, _ := c.Next()
86+
require.Equal(t, []byte(nil), k)
87+
}
88+
89+
// Step 2: verify prev
90+
for i := 998; i >= 0; i-- {
91+
expectedKey := []byte(fmt.Sprintf("%05d", i))
92+
actualKey, _ := c.Prev()
93+
require.Equal(t, expectedKey, actualKey)
94+
}
95+
96+
// Once we've reached the beginning, it should always return nil no matter how many times we call `Prev`.
97+
for i := 0; i < 10; i++ {
98+
k, _ := c.Prev()
99+
require.Equal(t, []byte(nil), k)
100+
}
101+
102+
// Step 3: verify next again
103+
for i := 1; i < 1000; i++ {
104+
expectedKey := []byte(fmt.Sprintf("%05d", i))
105+
actualKey, _ := c.Next()
106+
require.Equal(t, expectedKey, actualKey)
107+
}
108+
}
109+
110+
func testRepeatCursorOperations_PrevNextPrev(t *testing.T, b *bolt.Bucket) {
111+
c := b.Cursor()
112+
113+
startKey := []byte(fmt.Sprintf("%05d", 998))
114+
returnedKey, _ := c.Seek(startKey)
115+
require.Equal(t, startKey, returnedKey)
116+
117+
// Step 1: verify prev
118+
for i := 997; i >= 0; i-- {
119+
expectedKey := []byte(fmt.Sprintf("%05d", i))
120+
actualKey, _ := c.Prev()
121+
require.Equal(t, expectedKey, actualKey)
122+
}
123+
124+
// Once we've reached the beginning, it should always return nil no matter how many times we call `Prev`.
125+
for i := 0; i < 10; i++ {
126+
k, _ := c.Prev()
127+
require.Equal(t, []byte(nil), k)
128+
}
129+
130+
// Step 2: verify next
131+
for i := 1; i < 1000; i++ {
132+
expectedKey := []byte(fmt.Sprintf("%05d", i))
133+
actualKey, _ := c.Next()
134+
require.Equal(t, expectedKey, actualKey)
135+
}
136+
137+
// Once we've reached the end, it should always return nil no matter how many times we call `Next`.
138+
for i := 0; i < 10; i++ {
139+
k, _ := c.Next()
140+
require.Equal(t, []byte(nil), k)
141+
}
142+
143+
// Step 3: verify prev again
144+
for i := 998; i >= 0; i-- {
145+
expectedKey := []byte(fmt.Sprintf("%05d", i))
146+
actualKey, _ := c.Prev()
147+
require.Equal(t, expectedKey, actualKey)
148+
}
149+
}
150+
18151
// Ensure that a cursor can return a reference to the bucket that created it.
19152
func TestCursor_Bucket(t *testing.T) {
20153
db := btesting.MustCreateDB(t)

0 commit comments

Comments
 (0)