Skip to content

Commit

Permalink
fix(cursor): Last needs skip empty pages
Browse files Browse the repository at this point in the history
Signed-off-by: dchaofei <dchaofei@163.com>
  • Loading branch information
dchaofei committed Nov 14, 2022
1 parent c5901d2 commit 8a5b15f
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 20 deletions.
54 changes: 34 additions & 20 deletions cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ func (c *Cursor) Last() (key []byte, value []byte) {
ref.index = ref.count() - 1
c.stack = append(c.stack, ref)
c.last()

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

k, v, flags := c.keyValue()
if (flags & uint32(bucketLeafFlag)) != 0 {
return k, nil
Expand All @@ -84,26 +91,7 @@ func (c *Cursor) Next() (key []byte, value []byte) {
// The returned key and value are only valid for the life of the transaction.
func (c *Cursor) Prev() (key []byte, value []byte) {
_assert(c.bucket.tx.db != nil, "tx closed")

// Attempt to move back one element until we're successful.
// Move up the stack as we hit the beginning of each page in our stack.
for i := len(c.stack) - 1; i >= 0; i-- {
elem := &c.stack[i]
if elem.index > 0 {
elem.index--
break
}
c.stack = c.stack[:i]
}

// If we've hit the end then return nil.
if len(c.stack) == 0 {
return nil, nil
}

// Move down the stack to find the last element of the last leaf under this branch.
c.last()
k, v, flags := c.keyValue()
k, v, flags := c.prev()
if (flags & uint32(bucketLeafFlag)) != 0 {
return k, nil
}
Expand Down Expand Up @@ -243,6 +231,32 @@ func (c *Cursor) next() (key []byte, value []byte, flags uint32) {
}
}

// prev moves the cursor to the previous item in the bucket and returns its key and value.
// If the cursor is at the beginning of the bucket then a nil key and value are returned.
func (c *Cursor) prev() (key []byte, value []byte, flags uint32) {
_assert(c.bucket.tx.db != nil, "tx closed")

// Attempt to move back one element until we're successful.
// Move up the stack as we hit the beginning of each page in our stack.
for i := len(c.stack) - 1; i >= 0; i-- {
elem := &c.stack[i]
if elem.index > 0 {
elem.index--
break
}
c.stack = c.stack[:i]
}

// If we've hit the end then return nil.
if len(c.stack) == 0 {
return nil, nil, 0
}

// Move down the stack to find the last element of the last leaf under this branch.
c.last()
return c.keyValue()
}

// search recursively performs a binary search against a given page/node until it finds a given key.
func (c *Cursor) search(key []byte, pgid pgid) {
p, n := c.bucket.pageNode(pgid)
Expand Down
47 changes: 47 additions & 0 deletions cursor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,53 @@ func TestCursor_First_EmptyPages(t *testing.T) {
}
}

// Ensure that a cursor can skip over empty pages that have been deleted.
func TestCursor_Last_EmptyPages(t *testing.T) {
db := MustOpenDB()
defer db.MustClose()

// Create 1000 keys in the "widgets" bucket.
if err := db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucket([]byte("widgets"))
if err != nil {
t.Fatal(err)
}

for i := 0; i < 1000; i++ {
if err := b.Put(u64tob(uint64(i)), []byte{}); err != nil {
t.Fatal(err)
}
}

return nil
}); err != nil {
t.Fatal(err)
}

// Delete last 800 elements to ensure last page is empty
if err := db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("widgets"))
for i := 200; i < 1000; i++ {
if err := b.Delete(u64tob(uint64(i))); err != nil {
t.Fatal(err)
}
}

c := b.Cursor()
var n int
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
n++
}
if n != 200 {
t.Fatalf("unexpected key count: %d", n)
}

return nil
}); err != nil {
t.Fatal(err)
}
}

// Ensure that a Tx can iterate over all elements in a bucket.
func TestCursor_QuickCheck(t *testing.T) {
f := func(items testdata) bool {
Expand Down

0 comments on commit 8a5b15f

Please sign in to comment.