diff --git a/bucket.go b/bucket.go index 2ce2350..77d31c9 100644 --- a/bucket.go +++ b/bucket.go @@ -63,6 +63,16 @@ func newBucket(tx *Tx) Bucket { return b } +// Tx returns the tx of the bucket. +func (b *Bucket) Tx() *Tx { + return b.tx +} + +// Root returns the root of the bucket. +func (b *Bucket) Root() pgid { + return b.root +} + // Writable returns whether the bucket is writable. func (b *Bucket) Writable() bool { return b.tx.writable diff --git a/c/cursor.go b/c/cursor.go index 84eae03..0aebd47 100644 --- a/c/cursor.go +++ b/c/cursor.go @@ -135,26 +135,29 @@ int bolt_cursor_next(bolt_cursor *c, bolt_val *key, bolt_val *value) { */ import "C" -import "github.com/boltdb/bolt" +import ( + "unsafe" + + "github.com/boltdb/bolt" +) type bolt_cursor *C.bolt_cursor func NewCursor(b *bolt.Bucket) bolt_cursor { - data := (*C.void)(&b.tx.db.data[0]) - pgsz := (C.size_t)(b.tx.db.pageSize) + data, pgsz := b.Tx().DB().RawData() cursor := new(C.bolt_cursor) - C.bolt_cursor_init(cursor, data, pgsz, (C.pgid)(b.root)) + C.bolt_cursor_init(cursor, unsafe.Pointer(&data[0]), (C.size_t)(pgsz), (C.pgid)(b.Root())) return cursor } -func (c bolt_cursor) first() (key, value []byte) { +func first(c bolt_cursor) (key, value []byte) { var k, v C.bolt_val C.bolt_cursor_first(c, &k, &v) - return C.GoBytes(k.data, k.size), C.GoBytes(v.data, v.size) + return C.GoBytes(k.data, C.int(k.size)), C.GoBytes(v.data, C.int(v.size)) } -func (c bolt_cursor) next() (key, value []byte) { +func next(c bolt_cursor) (key, value []byte) { var k, v C.bolt_val C.bolt_cursor_next(c, &k, &v) - return C.GoBytes(k.data, k.size), C.GoBytes(v.data, v.size) + return C.GoBytes(k.data, C.int(k.size)), C.GoBytes(v.data, C.int(v.size)) } diff --git a/c/cursor_test.go b/c/cursor_test.go index ff6d547..3208757 100644 --- a/c/cursor_test.go +++ b/c/cursor_test.go @@ -1,12 +1,20 @@ package c import ( - "github.com/boltdb/bolt" - "github.com/stretchr/testify/assert" + "fmt" + "io/ioutil" + "os" + "sort" "testing" "testing/quick" + + "github.com/boltdb/bolt" + "github.com/stretchr/testify/assert" ) +// Test when cursor hits the end +// Implement seek; binary search within the page (branch page and element page) + // Ensure that a cursor can iterate over all elements in a bucket. func TestIterate(t *testing.T) { if testing.Short() { @@ -14,7 +22,7 @@ func TestIterate(t *testing.T) { } f := func(items testdata) bool { - withOpenDB(func(db *DB, path string) { + withOpenDB(func(db *bolt.DB, path string) { // Bulk insert all values. tx, _ := db.Begin(true) tx.CreateBucket("widgets") @@ -31,7 +39,7 @@ func TestIterate(t *testing.T) { var index = 0 tx, _ = db.Begin(false) c := NewCursor(tx.Bucket("widgets")) - for key, value := c.first(); key != nil && index < len(items); key, value = c.next() { + for key, value := first(c); key != nil && index < len(items); key, value = next(c) { assert.Equal(t, key, items[index].Key) assert.Equal(t, value, items[index].Value) index++ @@ -47,3 +55,58 @@ func TestIterate(t *testing.T) { } fmt.Fprint(os.Stderr, "\n") } + +// withTempPath executes a function with a database reference. +func withTempPath(fn func(string)) { + f, _ := ioutil.TempFile("", "bolt-") + path := f.Name() + f.Close() + os.Remove(path) + defer os.RemoveAll(path) + + fn(path) +} + +// withOpenDB executes a function with an already opened database. +func withOpenDB(fn func(*bolt.DB, string)) { + withTempPath(func(path string) { + db, err := bolt.Open(path, 0666) + if err != nil { + panic("cannot open db: " + err.Error()) + } + defer db.Close() + fn(db, path) + + // Log statistics. + // if *statsFlag { + // logStats(db) + // } + + // Check database consistency after every test. + mustCheck(db) + }) +} + +// mustCheck runs a consistency check on the database and panics if any errors are found. +func mustCheck(db *bolt.DB) { + if err := db.Check(); err != nil { + // Copy db off first. + db.CopyFile("/tmp/check.db", 0600) + + if errors, ok := err.(bolt.ErrorList); ok { + for _, err := range errors { + warn(err) + } + } + warn(err) + panic("check failure: see /tmp/check.db") + } +} + +func warn(v ...interface{}) { + fmt.Fprintln(os.Stderr, v...) +} + +func warnf(msg string, v ...interface{}) { + fmt.Fprintf(os.Stderr, msg+"\n", v...) +} diff --git a/c/quick_test.go b/c/quick_test.go new file mode 100644 index 0000000..7d87249 --- /dev/null +++ b/c/quick_test.go @@ -0,0 +1,77 @@ +package c + +import ( + "bytes" + "flag" + "math/rand" + "reflect" + "testing/quick" + "time" +) + +// testing/quick defaults to 5 iterations and a random seed. +// You can override these settings from the command line: +// +// -quick.count The number of iterations to perform. +// -quick.seed The seed to use for randomizing. +// -quick.maxitems The maximum number of items to insert into a DB. +// -quick.maxksize The maximum size of a key. +// -quick.maxvsize The maximum size of a value. +// + +var qcount, qseed, qmaxitems, qmaxksize, qmaxvsize int + +func init() { + flag.IntVar(&qcount, "quick.count", 5, "") + flag.IntVar(&qseed, "quick.seed", int(time.Now().UnixNano())%100000, "") + flag.IntVar(&qmaxitems, "quick.maxitems", 1000, "") + flag.IntVar(&qmaxksize, "quick.maxksize", 1024, "") + flag.IntVar(&qmaxvsize, "quick.maxvsize", 1024, "") + flag.Parse() + warn("seed:", qseed) + warnf("quick settings: count=%v, items=%v, ksize=%v, vsize=%v", qcount, qmaxitems, qmaxksize, qmaxvsize) +} + +func qconfig() *quick.Config { + return &quick.Config{ + MaxCount: qcount, + Rand: rand.New(rand.NewSource(int64(qseed))), + } +} + +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) + for i := 0; i < n; i++ { + item := &items[i] + item.Key = randByteSlice(rand, 1, qmaxksize) + item.Value = randByteSlice(rand, 0, qmaxvsize) + } + return reflect.ValueOf(items) +} + +type revtestdata []testdataitem + +func (t revtestdata) Len() int { return len(t) } +func (t revtestdata) Swap(i, j int) { t[i], t[j] = t[j], t[i] } +func (t revtestdata) Less(i, j int) bool { return bytes.Compare(t[i].Key, t[j].Key) == 1 } + +type testdataitem struct { + Key []byte + Value []byte +} + +func randByteSlice(rand *rand.Rand, minSize, maxSize int) []byte { + n := rand.Intn(maxSize-minSize) + minSize + b := make([]byte, n) + for i := 0; i < n; i++ { + b[i] = byte(rand.Intn(255)) + } + return b +} diff --git a/db.go b/db.go index c9611f9..54f726e 100644 --- a/db.go +++ b/db.go @@ -578,6 +578,12 @@ func (db *DB) checkBucket(b *Bucket, reachable map[pgid]*page, errors *ErrorList }) } +// This is for internal access to the raw data bytes from the C cursor, use +// carefully, or not at all. +func (db *DB) RawData() ([]byte, int) { + return db.data, db.pageSize +} + // page retrieves a page reference from the mmap based on the current page size. func (db *DB) page(id pgid) *page { pos := id * pgid(db.pageSize)