Skip to content

Commit

Permalink
Cursor iteration.
Browse files Browse the repository at this point in the history
  • Loading branch information
benbjohnson committed Feb 11, 2014
1 parent a68bd30 commit b8122bf
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 3 deletions.
53 changes: 50 additions & 3 deletions cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,35 @@ type Cursor struct {

// First moves the cursor to the first item in the bucket and returns its key and data.
func (c *Cursor) First() ([]byte, []byte) {
// TODO: Traverse to the first key.
return nil, nil
if len(c.stack) > 0 {
c.stack = c.stack[:0]
}
c.stack = append(c.stack, pageElementRef{page: c.transaction.page(c.root), index: 0})
c.first()
return c.keyValue()
}

// Move the cursor to the next key/value.
func (c *Cursor) Next() ([]byte, []byte) {
return nil, nil
// Attempt to move over one element until we're successful.
// Move up the stack as we hit the end of each page in our stack.
for i := len(c.stack) - 1; i >= 0; i-- {
elem := &c.stack[i]
if elem.index < elem.page.count-1 {
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 first element of the first leaf under this branch.
c.first()
return c.keyValue()
}

// Get positions the cursor at a specific key and returns the its value.
Expand All @@ -42,6 +64,21 @@ func (c *Cursor) Get(key []byte) []byte {
return c.element().value()
}

// first moves the cursor to the first leaf element under a page.
func (c *Cursor) first() {
p := c.stack[len(c.stack)-1].page
for {
// Exit when we hit a leaf page.
if (p.flags & p_leaf) != 0 {
break
}

// Keep adding pages pointing to the first element to the stack.
p = c.transaction.page(p.branchPageElement(c.stack[len(c.stack)-1].index).pgid)
c.stack = append(c.stack, pageElementRef{page: p, index: 0})
}
}

func (c *Cursor) search(key []byte, p *page) {
_assert((p.flags&(p_branch|p_leaf)) != 0, "invalid page type: "+p.typ())
e := pageElementRef{page: p}
Expand Down Expand Up @@ -99,6 +136,16 @@ func (c *Cursor) element() *leafPageElement {
return ref.page.leafPageElement(ref.index)
}

// keyValue returns the key and value of the current leaf element.
func (c *Cursor) keyValue() ([]byte, []byte) {
ref := &c.stack[len(c.stack)-1]
if ref.index >= ref.page.count {
return nil, nil
}
e := ref.page.leafPageElement(ref.index)
return e.key(), e.value()
}

// node returns the node that the cursor is currently positioned on.
func (c *Cursor) node(t *RWTransaction) *node {
if len(c.stack) == 0 {
Expand Down
4 changes: 4 additions & 0 deletions node.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,10 @@ func (n *node) write(p *page) {
// Initialize page.
if n.isLeaf {
p.flags |= p_leaf
// warn("∑", p.id, "leaf")
} else {
p.flags |= p_branch
// warn("∑", p.id, "branch")
}
p.count = uint16(len(n.inodes))

Expand All @@ -175,11 +177,13 @@ func (n *node) write(p *page) {
elem.pos = uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem)))
elem.ksize = uint32(len(item.key))
elem.vsize = uint32(len(item.value))
// warn(" »", string(item.key), "->", string(item.value))
} else {
elem := p.branchPageElement(uint16(i))
elem.pos = uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem)))
elem.ksize = uint32(len(item.key))
elem.pgid = item.pgid
// warn(" »", string(item.key))
}

// Write data for the element to the end of the page.
Expand Down
5 changes: 5 additions & 0 deletions quick_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package bolt

import (
"bytes"
"flag"
"math/rand"
"reflect"
Expand Down Expand Up @@ -39,6 +40,10 @@ func qconfig() *quick.Config {

type testdata []testdataitem

func (t testdata) Len() int { return len(t) }
func (t testdata) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t testdata) Less(i, j int) bool { return bytes.Compare(t[i].Key, t[j].Key) == -1 }

func (t testdata) Generate(rand *rand.Rand, size int) reflect.Value {
n := rand.Intn(qmaxitems-1) + 1
items := make(testdata, n)
Expand Down
97 changes: 97 additions & 0 deletions transaction_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package bolt

import (
"fmt"
"os"
"sort"
"testing"
"testing/quick"

"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -33,3 +37,96 @@ func TestTransactionGetMissing(t *testing.T) {
assert.Nil(t, value)
})
}

// Ensure that a Transaction cursor can iterate over an empty bucket without error.
func TestTransactionCursorEmptyBucket(t *testing.T) {
withOpenDB(func(db *DB, path string) {
db.CreateBucket("widgets")
txn, _ := db.Transaction()
c := txn.Cursor("widgets")
k, v := c.First()
assert.Nil(t, k)
assert.Nil(t, v)
txn.Close()
})
}

// Ensure that a Transaction returns a nil when a bucket doesn't exist.
func TestTransactionCursorMissingBucket(t *testing.T) {
withOpenDB(func(db *DB, path string) {
db.CreateBucket("widgets")
txn, _ := db.Transaction()
assert.Nil(t, txn.Cursor("woojits"))
txn.Close()
})
}

// Ensure that a Transaction cursor can iterate over a single root with a couple elements.
func TestTransactionCursorLeafRoot(t *testing.T) {
withOpenDB(func(db *DB, path string) {
db.CreateBucket("widgets")
db.Put("widgets", []byte("baz"), []byte{})
db.Put("widgets", []byte("foo"), []byte{0})
db.Put("widgets", []byte("bar"), []byte{1})
txn, _ := db.Transaction()
c := txn.Cursor("widgets")

k, v := c.First()
assert.Equal(t, string(k), "bar")
assert.Equal(t, v, []byte{1})

k, v = c.Next()
assert.Equal(t, string(k), "baz")
assert.Equal(t, v, []byte{})

k, v = c.Next()
assert.Equal(t, string(k), "foo")
assert.Equal(t, v, []byte{0})

k, v = c.Next()
assert.Nil(t, k)
assert.Nil(t, v)

k, v = c.Next()
assert.Nil(t, k)
assert.Nil(t, v)

txn.Close()
})
}

// Ensure that a transaction can iterate over all elements in a bucket.
func TestTransactionCursorIterate(t *testing.T) {
f := func(items testdata) bool {
withOpenDB(func(db *DB, path string) {
// Bulk insert all values.
db.CreateBucket("widgets")
rwtxn, _ := db.RWTransaction()
for _, item := range items {
assert.NoError(t, rwtxn.Put("widgets", item.Key, item.Value))
}
assert.NoError(t, rwtxn.Commit())

// Sort test data.
sort.Sort(items)

// Iterate over all items and check consistency.
var index = 0
txn, _ := db.Transaction()
c := txn.Cursor("widgets")
for k, v := c.First(); k != nil && index < len(items); k, v = c.Next() {
assert.Equal(t, k, items[index].Key)
assert.Equal(t, v, items[index].Value)
index++
}
assert.Equal(t, len(items), index)
txn.Close()
})
fmt.Fprint(os.Stderr, ".")
return true
}
if err := quick.Check(f, qconfig()); err != nil {
t.Error(err)
}
fmt.Fprint(os.Stderr, "\n")
}

0 comments on commit b8122bf

Please sign in to comment.