mirror of https://github.com/etcd-io/bbolt.git
Consolidate Tx and RWTx.
parent
57376f0905
commit
c551e45a47
|
@ -9,7 +9,6 @@ type Bucket struct {
|
||||||
*bucket
|
*bucket
|
||||||
name string
|
name string
|
||||||
tx *Tx
|
tx *Tx
|
||||||
rwtx *RWTx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// bucket represents the on-file representation of a bucket.
|
// bucket represents the on-file representation of a bucket.
|
||||||
|
@ -25,7 +24,7 @@ func (b *Bucket) Name() string {
|
||||||
|
|
||||||
// Writable returns whether the bucket is writable.
|
// Writable returns whether the bucket is writable.
|
||||||
func (b *Bucket) Writable() bool {
|
func (b *Bucket) Writable() bool {
|
||||||
return (b.rwtx != nil)
|
return b.tx.writable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor creates a cursor associated with the bucket.
|
// Cursor creates a cursor associated with the bucket.
|
||||||
|
@ -74,7 +73,7 @@ func (b *Bucket) Put(key []byte, value []byte) error {
|
||||||
c.Seek(key)
|
c.Seek(key)
|
||||||
|
|
||||||
// Insert the key/value.
|
// Insert the key/value.
|
||||||
c.node(b.rwtx).put(key, key, value, 0)
|
c.node(b.tx).put(key, key, value, 0)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -92,7 +91,7 @@ func (b *Bucket) Delete(key []byte) error {
|
||||||
c.Seek(key)
|
c.Seek(key)
|
||||||
|
|
||||||
// Delete the node if we have a matching key.
|
// Delete the node if we have a matching key.
|
||||||
c.node(b.rwtx).del(key)
|
c.node(b.tx).del(key)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ func TestBucketGetNonExistent(t *testing.T) {
|
||||||
func TestBucketGetFromNode(t *testing.T) {
|
func TestBucketGetFromNode(t *testing.T) {
|
||||||
withOpenDB(func(db *DB, path string) {
|
withOpenDB(func(db *DB, path string) {
|
||||||
db.CreateBucket("widgets")
|
db.CreateBucket("widgets")
|
||||||
db.Do(func(txn *RWTx) error {
|
db.Do(func(tx *Tx) error {
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
b.Put([]byte("foo"), []byte("bar"))
|
b.Put([]byte("foo"), []byte("bar"))
|
||||||
value := b.Get([]byte("foo"))
|
value := b.Get([]byte("foo"))
|
||||||
assert.Equal(t, value, []byte("bar"))
|
assert.Equal(t, value, []byte("bar"))
|
||||||
|
@ -54,8 +54,8 @@ func TestBucketPut(t *testing.T) {
|
||||||
func TestBucketPutReadOnly(t *testing.T) {
|
func TestBucketPutReadOnly(t *testing.T) {
|
||||||
withOpenDB(func(db *DB, path string) {
|
withOpenDB(func(db *DB, path string) {
|
||||||
db.CreateBucket("widgets")
|
db.CreateBucket("widgets")
|
||||||
db.With(func(txn *Tx) error {
|
db.With(func(tx *Tx) error {
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
err := b.Put([]byte("foo"), []byte("bar"))
|
err := b.Put([]byte("foo"), []byte("bar"))
|
||||||
assert.Equal(t, err, ErrBucketNotWritable)
|
assert.Equal(t, err, ErrBucketNotWritable)
|
||||||
return nil
|
return nil
|
||||||
|
@ -81,8 +81,8 @@ func TestBucketDelete(t *testing.T) {
|
||||||
func TestBucketDeleteReadOnly(t *testing.T) {
|
func TestBucketDeleteReadOnly(t *testing.T) {
|
||||||
withOpenDB(func(db *DB, path string) {
|
withOpenDB(func(db *DB, path string) {
|
||||||
db.CreateBucket("widgets")
|
db.CreateBucket("widgets")
|
||||||
db.With(func(txn *Tx) error {
|
db.With(func(tx *Tx) error {
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
err := b.Delete([]byte("foo"))
|
err := b.Delete([]byte("foo"))
|
||||||
assert.Equal(t, err, ErrBucketNotWritable)
|
assert.Equal(t, err, ErrBucketNotWritable)
|
||||||
return nil
|
return nil
|
||||||
|
@ -120,8 +120,8 @@ func TestBucketNextSequence(t *testing.T) {
|
||||||
func TestBucketNextSequenceReadOnly(t *testing.T) {
|
func TestBucketNextSequenceReadOnly(t *testing.T) {
|
||||||
withOpenDB(func(db *DB, path string) {
|
withOpenDB(func(db *DB, path string) {
|
||||||
db.CreateBucket("widgets")
|
db.CreateBucket("widgets")
|
||||||
db.With(func(txn *Tx) error {
|
db.With(func(tx *Tx) error {
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
i, err := b.NextSequence()
|
i, err := b.NextSequence()
|
||||||
assert.Equal(t, i, 0)
|
assert.Equal(t, i, 0)
|
||||||
assert.Equal(t, err, ErrBucketNotWritable)
|
assert.Equal(t, err, ErrBucketNotWritable)
|
||||||
|
@ -134,8 +134,8 @@ func TestBucketNextSequenceReadOnly(t *testing.T) {
|
||||||
func TestBucketNextSequenceOverflow(t *testing.T) {
|
func TestBucketNextSequenceOverflow(t *testing.T) {
|
||||||
withOpenDB(func(db *DB, path string) {
|
withOpenDB(func(db *DB, path string) {
|
||||||
db.CreateBucket("widgets")
|
db.CreateBucket("widgets")
|
||||||
db.Do(func(txn *RWTx) error {
|
db.Do(func(tx *Tx) error {
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
b.bucket.sequence = uint64(maxInt)
|
b.bucket.sequence = uint64(maxInt)
|
||||||
seq, err := b.NextSequence()
|
seq, err := b.NextSequence()
|
||||||
assert.Equal(t, err, ErrSequenceOverflow)
|
assert.Equal(t, err, ErrSequenceOverflow)
|
||||||
|
@ -218,31 +218,31 @@ func TestBucketPutKeyTooLarge(t *testing.T) {
|
||||||
// Ensure a bucket can calculate stats.
|
// Ensure a bucket can calculate stats.
|
||||||
func TestBucketStat(t *testing.T) {
|
func TestBucketStat(t *testing.T) {
|
||||||
withOpenDB(func(db *DB, path string) {
|
withOpenDB(func(db *DB, path string) {
|
||||||
db.Do(func(txn *RWTx) error {
|
db.Do(func(tx *Tx) error {
|
||||||
// Add bucket with lots of keys.
|
// Add bucket with lots of keys.
|
||||||
txn.CreateBucket("widgets")
|
tx.CreateBucket("widgets")
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
for i := 0; i < 100000; i++ {
|
for i := 0; i < 100000; i++ {
|
||||||
b.Put([]byte(strconv.Itoa(i)), []byte(strconv.Itoa(i)))
|
b.Put([]byte(strconv.Itoa(i)), []byte(strconv.Itoa(i)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add bucket with fewer keys but one big value.
|
// Add bucket with fewer keys but one big value.
|
||||||
txn.CreateBucket("woojits")
|
tx.CreateBucket("woojits")
|
||||||
b = txn.Bucket("woojits")
|
b = tx.Bucket("woojits")
|
||||||
for i := 0; i < 500; i++ {
|
for i := 0; i < 500; i++ {
|
||||||
b.Put([]byte(strconv.Itoa(i)), []byte(strconv.Itoa(i)))
|
b.Put([]byte(strconv.Itoa(i)), []byte(strconv.Itoa(i)))
|
||||||
}
|
}
|
||||||
b.Put([]byte("really-big-value"), []byte(strings.Repeat("*", 10000)))
|
b.Put([]byte("really-big-value"), []byte(strings.Repeat("*", 10000)))
|
||||||
|
|
||||||
// Add a bucket that fits on a single root leaf.
|
// Add a bucket that fits on a single root leaf.
|
||||||
txn.CreateBucket("whozawhats")
|
tx.CreateBucket("whozawhats")
|
||||||
b = txn.Bucket("whozawhats")
|
b = tx.Bucket("whozawhats")
|
||||||
b.Put([]byte("foo"), []byte("bar"))
|
b.Put([]byte("foo"), []byte("bar"))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
db.With(func(txn *Tx) error {
|
db.With(func(tx *Tx) error {
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
stat := b.Stat()
|
stat := b.Stat()
|
||||||
assert.Equal(t, stat.BranchPageCount, 15)
|
assert.Equal(t, stat.BranchPageCount, 15)
|
||||||
assert.Equal(t, stat.LeafPageCount, 1281)
|
assert.Equal(t, stat.LeafPageCount, 1281)
|
||||||
|
@ -250,7 +250,7 @@ func TestBucketStat(t *testing.T) {
|
||||||
assert.Equal(t, stat.KeyCount, 100000)
|
assert.Equal(t, stat.KeyCount, 100000)
|
||||||
assert.Equal(t, stat.MaxDepth, 3)
|
assert.Equal(t, stat.MaxDepth, 3)
|
||||||
|
|
||||||
b = txn.Bucket("woojits")
|
b = tx.Bucket("woojits")
|
||||||
stat = b.Stat()
|
stat = b.Stat()
|
||||||
assert.Equal(t, stat.BranchPageCount, 1)
|
assert.Equal(t, stat.BranchPageCount, 1)
|
||||||
assert.Equal(t, stat.LeafPageCount, 6)
|
assert.Equal(t, stat.LeafPageCount, 6)
|
||||||
|
@ -258,7 +258,7 @@ func TestBucketStat(t *testing.T) {
|
||||||
assert.Equal(t, stat.KeyCount, 501)
|
assert.Equal(t, stat.KeyCount, 501)
|
||||||
assert.Equal(t, stat.MaxDepth, 2)
|
assert.Equal(t, stat.MaxDepth, 2)
|
||||||
|
|
||||||
b = txn.Bucket("whozawhats")
|
b = tx.Bucket("whozawhats")
|
||||||
stat = b.Stat()
|
stat = b.Stat()
|
||||||
assert.Equal(t, stat.BranchPageCount, 0)
|
assert.Equal(t, stat.BranchPageCount, 0)
|
||||||
assert.Equal(t, stat.LeafPageCount, 1)
|
assert.Equal(t, stat.LeafPageCount, 1)
|
||||||
|
@ -271,7 +271,7 @@ func TestBucketStat(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that a bucket can write random keys and values across multiple txns.
|
// Ensure that a bucket can write random keys and values across multiple transactions.
|
||||||
func TestBucketPutSingle(t *testing.T) {
|
func TestBucketPutSingle(t *testing.T) {
|
||||||
index := 0
|
index := 0
|
||||||
f := func(items testdata) bool {
|
f := func(items testdata) bool {
|
||||||
|
@ -317,16 +317,16 @@ func TestBucketPutMultiple(t *testing.T) {
|
||||||
withOpenDB(func(db *DB, path string) {
|
withOpenDB(func(db *DB, path string) {
|
||||||
// Bulk insert all values.
|
// Bulk insert all values.
|
||||||
db.CreateBucket("widgets")
|
db.CreateBucket("widgets")
|
||||||
rwtxn, _ := db.RWTx()
|
tx, _ := db.RWTx()
|
||||||
b := rwtxn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
assert.NoError(t, b.Put(item.Key, item.Value))
|
assert.NoError(t, b.Put(item.Key, item.Value))
|
||||||
}
|
}
|
||||||
assert.NoError(t, rwtxn.Commit())
|
assert.NoError(t, tx.Commit())
|
||||||
|
|
||||||
// Verify all items exist.
|
// Verify all items exist.
|
||||||
txn, _ := db.Tx()
|
tx, _ = db.Tx()
|
||||||
b = txn.Bucket("widgets")
|
b = tx.Bucket("widgets")
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
value := b.Get(item.Key)
|
value := b.Get(item.Key)
|
||||||
if !assert.Equal(t, item.Value, value) {
|
if !assert.Equal(t, item.Value, value) {
|
||||||
|
@ -334,7 +334,7 @@ func TestBucketPutMultiple(t *testing.T) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
txn.Close()
|
tx.Rollback()
|
||||||
})
|
})
|
||||||
fmt.Fprint(os.Stderr, ".")
|
fmt.Fprint(os.Stderr, ".")
|
||||||
return true
|
return true
|
||||||
|
@ -351,20 +351,20 @@ func TestBucketDeleteQuick(t *testing.T) {
|
||||||
withOpenDB(func(db *DB, path string) {
|
withOpenDB(func(db *DB, path string) {
|
||||||
// Bulk insert all values.
|
// Bulk insert all values.
|
||||||
db.CreateBucket("widgets")
|
db.CreateBucket("widgets")
|
||||||
rwtxn, _ := db.RWTx()
|
tx, _ := db.RWTx()
|
||||||
b := rwtxn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
assert.NoError(t, b.Put(item.Key, item.Value))
|
assert.NoError(t, b.Put(item.Key, item.Value))
|
||||||
}
|
}
|
||||||
assert.NoError(t, rwtxn.Commit())
|
assert.NoError(t, tx.Commit())
|
||||||
|
|
||||||
// Remove items one at a time and check consistency.
|
// Remove items one at a time and check consistency.
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
assert.NoError(t, db.Delete("widgets", item.Key))
|
assert.NoError(t, db.Delete("widgets", item.Key))
|
||||||
|
|
||||||
// Anything before our deletion index should be nil.
|
// Anything before our deletion index should be nil.
|
||||||
txn, _ := db.Tx()
|
tx, _ := db.Tx()
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
for j, exp := range items {
|
for j, exp := range items {
|
||||||
if j > i {
|
if j > i {
|
||||||
value := b.Get(exp.Key)
|
value := b.Get(exp.Key)
|
||||||
|
@ -378,7 +378,7 @@ func TestBucketDeleteQuick(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
txn.Close()
|
tx.Rollback()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
fmt.Fprint(os.Stderr, ".")
|
fmt.Fprint(os.Stderr, ".")
|
||||||
|
|
|
@ -251,7 +251,7 @@ func (c *Cursor) keyValue() ([]byte, []byte) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// node returns the node that the cursor is currently positioned on.
|
// node returns the node that the cursor is currently positioned on.
|
||||||
func (c *Cursor) node(t *RWTx) *node {
|
func (c *Cursor) node(t *Tx) *node {
|
||||||
_assert(len(c.stack) > 0, "accessing a node with a zero-length cursor stack")
|
_assert(len(c.stack) > 0, "accessing a node with a zero-length cursor stack")
|
||||||
|
|
||||||
// If the top of the stack is a leaf node then just return it.
|
// If the top of the stack is a leaf node then just return it.
|
||||||
|
|
46
db.go
46
db.go
|
@ -29,7 +29,7 @@ type DB struct {
|
||||||
meta1 *meta
|
meta1 *meta
|
||||||
pageSize int
|
pageSize int
|
||||||
opened bool
|
opened bool
|
||||||
rwtx *RWTx
|
rwtx *Tx
|
||||||
txs []*Tx
|
txs []*Tx
|
||||||
freelist *freelist
|
freelist *freelist
|
||||||
|
|
||||||
|
@ -290,11 +290,11 @@ func (db *DB) Tx() (*Tx, error) {
|
||||||
// RWTx creates a read/write transaction.
|
// RWTx creates a read/write transaction.
|
||||||
// Only one read/write transaction is allowed at a time.
|
// Only one read/write transaction is allowed at a time.
|
||||||
// You must call Commit() or Rollback() on the transaction to close it.
|
// You must call Commit() or Rollback() on the transaction to close it.
|
||||||
func (db *DB) RWTx() (*RWTx, error) {
|
func (db *DB) RWTx() (*Tx, error) {
|
||||||
db.metalock.Lock()
|
db.metalock.Lock()
|
||||||
defer db.metalock.Unlock()
|
defer db.metalock.Unlock()
|
||||||
|
|
||||||
// Obtain writer lock. This is released by the RWTx when it closes.
|
// Obtain writer lock. This is released by the transaction when it closes.
|
||||||
db.rwlock.Lock()
|
db.rwlock.Lock()
|
||||||
|
|
||||||
// Exit if the database is not open yet.
|
// Exit if the database is not open yet.
|
||||||
|
@ -304,7 +304,7 @@ func (db *DB) RWTx() (*RWTx, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a transaction associated with the database.
|
// Create a transaction associated with the database.
|
||||||
t := &RWTx{}
|
t := &Tx{writable: true}
|
||||||
t.init(db)
|
t.init(db)
|
||||||
db.rwtx = t
|
db.rwtx = t
|
||||||
|
|
||||||
|
@ -331,20 +331,20 @@ func (db *DB) removeTx(t *Tx) {
|
||||||
db.mmaplock.RUnlock()
|
db.mmaplock.RUnlock()
|
||||||
|
|
||||||
// Remove the transaction.
|
// Remove the transaction.
|
||||||
for i, txn := range db.txs {
|
for i, tx := range db.txs {
|
||||||
if txn == t {
|
if tx == t {
|
||||||
db.txs = append(db.txs[:i], db.txs[i+1:]...)
|
db.txs = append(db.txs[:i], db.txs[i+1:]...)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do executes a function within the context of a RWTx.
|
// Do executes a function within the context of a read-write transaction.
|
||||||
// If no error is returned from the function then the transaction is committed.
|
// If no error is returned from the function then the transaction is committed.
|
||||||
// If an error is returned then the entire transaction is rolled back.
|
// If an error is returned then the entire transaction is rolled back.
|
||||||
// Any error that is returned from the function or returned from the commit is
|
// Any error that is returned from the function or returned from the commit is
|
||||||
// returned from the Do() method.
|
// returned from the Do() method.
|
||||||
func (db *DB) Do(fn func(*RWTx) error) error {
|
func (db *DB) Do(fn func(*Tx) error) error {
|
||||||
t, err := db.RWTx()
|
t, err := db.RWTx()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -366,7 +366,7 @@ func (db *DB) With(fn func(*Tx) error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer t.Close()
|
defer t.Rollback()
|
||||||
|
|
||||||
// If an error is returned from the function then pass it through.
|
// If an error is returned from the function then pass it through.
|
||||||
return fn(t)
|
return fn(t)
|
||||||
|
@ -391,7 +391,7 @@ func (db *DB) Bucket(name string) (*Bucket, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer t.Close()
|
defer t.Rollback()
|
||||||
return t.Bucket(name), nil
|
return t.Bucket(name), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -401,7 +401,7 @@ func (db *DB) Buckets() ([]*Bucket, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer t.Close()
|
defer t.Rollback()
|
||||||
return t.Buckets(), nil
|
return t.Buckets(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,7 +409,7 @@ func (db *DB) Buckets() ([]*Bucket, error) {
|
||||||
// This function can return an error if the bucket already exists, if the name
|
// This function can return an error if the bucket already exists, if the name
|
||||||
// is blank, or the bucket name is too long.
|
// is blank, or the bucket name is too long.
|
||||||
func (db *DB) CreateBucket(name string) error {
|
func (db *DB) CreateBucket(name string) error {
|
||||||
return db.Do(func(t *RWTx) error {
|
return db.Do(func(t *Tx) error {
|
||||||
return t.CreateBucket(name)
|
return t.CreateBucket(name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -417,7 +417,7 @@ func (db *DB) CreateBucket(name string) error {
|
||||||
// CreateBucketIfNotExists creates a new bucket with the given name if it doesn't already exist.
|
// CreateBucketIfNotExists creates a new bucket with the given name if it doesn't already exist.
|
||||||
// This function can return an error if the name is blank, or the bucket name is too long.
|
// This function can return an error if the name is blank, or the bucket name is too long.
|
||||||
func (db *DB) CreateBucketIfNotExists(name string) error {
|
func (db *DB) CreateBucketIfNotExists(name string) error {
|
||||||
return db.Do(func(t *RWTx) error {
|
return db.Do(func(t *Tx) error {
|
||||||
return t.CreateBucketIfNotExists(name)
|
return t.CreateBucketIfNotExists(name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -425,7 +425,7 @@ func (db *DB) CreateBucketIfNotExists(name string) error {
|
||||||
// DeleteBucket removes a bucket from the database.
|
// DeleteBucket removes a bucket from the database.
|
||||||
// Returns an error if the bucket does not exist.
|
// Returns an error if the bucket does not exist.
|
||||||
func (db *DB) DeleteBucket(name string) error {
|
func (db *DB) DeleteBucket(name string) error {
|
||||||
return db.Do(func(t *RWTx) error {
|
return db.Do(func(t *Tx) error {
|
||||||
return t.DeleteBucket(name)
|
return t.DeleteBucket(name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -434,7 +434,7 @@ func (db *DB) DeleteBucket(name string) error {
|
||||||
// This function can return an error if the bucket does not exist.
|
// This function can return an error if the bucket does not exist.
|
||||||
func (db *DB) NextSequence(name string) (int, error) {
|
func (db *DB) NextSequence(name string) (int, error) {
|
||||||
var seq int
|
var seq int
|
||||||
err := db.Do(func(t *RWTx) error {
|
err := db.Do(func(t *Tx) error {
|
||||||
b := t.Bucket(name)
|
b := t.Bucket(name)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
return ErrBucketNotFound
|
return ErrBucketNotFound
|
||||||
|
@ -457,17 +457,15 @@ func (db *DB) Get(name string, key []byte) ([]byte, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer t.Close()
|
defer t.Rollback()
|
||||||
|
|
||||||
// Open bucket and retrieve value for key.
|
// Open bucket and retrieve value for key.
|
||||||
b := t.Bucket(name)
|
b := t.Bucket(name)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
return nil, ErrBucketNotFound
|
return nil, ErrBucketNotFound
|
||||||
}
|
}
|
||||||
value, err := b.Get(key), nil
|
value := b.Get(key)
|
||||||
if err != nil {
|
if value == nil {
|
||||||
return nil, err
|
|
||||||
} else if value == nil {
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,7 +480,7 @@ func (db *DB) Get(name string, key []byte) ([]byte, error) {
|
||||||
// Put sets the value for a key in a bucket.
|
// Put sets the value for a key in a bucket.
|
||||||
// Returns an error if the bucket is not found, if key is blank, if the key is too large, or if the value is too large.
|
// Returns an error if the bucket is not found, if key is blank, if the key is too large, or if the value is too large.
|
||||||
func (db *DB) Put(name string, key []byte, value []byte) error {
|
func (db *DB) Put(name string, key []byte, value []byte) error {
|
||||||
return db.Do(func(t *RWTx) error {
|
return db.Do(func(t *Tx) error {
|
||||||
b := t.Bucket(name)
|
b := t.Bucket(name)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
return ErrBucketNotFound
|
return ErrBucketNotFound
|
||||||
|
@ -494,7 +492,7 @@ func (db *DB) Put(name string, key []byte, value []byte) error {
|
||||||
// Delete removes a key from a bucket.
|
// Delete removes a key from a bucket.
|
||||||
// Returns an error if the bucket cannot be found.
|
// Returns an error if the bucket cannot be found.
|
||||||
func (db *DB) Delete(name string, key []byte) error {
|
func (db *DB) Delete(name string, key []byte) error {
|
||||||
return db.Do(func(t *RWTx) error {
|
return db.Do(func(t *Tx) error {
|
||||||
b := t.Bucket(name)
|
b := t.Bucket(name)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
return ErrBucketNotFound
|
return ErrBucketNotFound
|
||||||
|
@ -512,7 +510,7 @@ func (db *DB) Copy(w io.Writer) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer t.Close()
|
defer t.Rollback()
|
||||||
|
|
||||||
// Open reader on the database.
|
// Open reader on the database.
|
||||||
f, err := os.Open(db.path)
|
f, err := os.Open(db.path)
|
||||||
|
@ -557,7 +555,7 @@ func (db *DB) Stat() (*Stat, error) {
|
||||||
db.mmaplock.RUnlock()
|
db.mmaplock.RUnlock()
|
||||||
db.metalock.Unlock()
|
db.metalock.Unlock()
|
||||||
|
|
||||||
err := db.Do(func(t *RWTx) error {
|
err := db.Do(func(t *Tx) error {
|
||||||
s.PageCount = int(t.meta.pgid)
|
s.PageCount = int(t.meta.pgid)
|
||||||
s.FreePageCount = len(db.freelist.all())
|
s.FreePageCount = len(db.freelist.all())
|
||||||
s.PageSize = db.pageSize
|
s.PageSize = db.pageSize
|
||||||
|
|
46
db_test.go
46
db_test.go
|
@ -158,8 +158,8 @@ func TestDBCorruptMeta0(t *testing.T) {
|
||||||
// Ensure that a database cannot open a transaction when it's not open.
|
// Ensure that a database cannot open a transaction when it's not open.
|
||||||
func TestDBTxErrDatabaseNotOpen(t *testing.T) {
|
func TestDBTxErrDatabaseNotOpen(t *testing.T) {
|
||||||
withDB(func(db *DB, path string) {
|
withDB(func(db *DB, path string) {
|
||||||
txn, err := db.Tx()
|
tx, err := db.Tx()
|
||||||
assert.Nil(t, txn)
|
assert.Nil(t, tx)
|
||||||
assert.Equal(t, err, ErrDatabaseNotOpen)
|
assert.Equal(t, err, ErrDatabaseNotOpen)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -172,12 +172,32 @@ func TestDBDeleteFromMissingBucket(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that a read-write transaction can be retrieved.
|
||||||
|
func TestDBRWTx(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
tx, err := db.RWTx()
|
||||||
|
assert.NotNil(t, tx)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tx.DB(), db)
|
||||||
|
assert.Equal(t, tx.Writable(), true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that opening a transaction while the DB is closed returns an error.
|
||||||
|
func TestDBRWTxOpenWithClosedDB(t *testing.T) {
|
||||||
|
withDB(func(db *DB, path string) {
|
||||||
|
tx, err := db.RWTx()
|
||||||
|
assert.Equal(t, err, ErrDatabaseNotOpen)
|
||||||
|
assert.Nil(t, tx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure a database can provide a transactional block.
|
// Ensure a database can provide a transactional block.
|
||||||
func TestDBTxBlock(t *testing.T) {
|
func TestDBTxBlock(t *testing.T) {
|
||||||
withOpenDB(func(db *DB, path string) {
|
withOpenDB(func(db *DB, path string) {
|
||||||
err := db.Do(func(txn *RWTx) error {
|
err := db.Do(func(tx *Tx) error {
|
||||||
txn.CreateBucket("widgets")
|
tx.CreateBucket("widgets")
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
b.Put([]byte("foo"), []byte("bar"))
|
b.Put([]byte("foo"), []byte("bar"))
|
||||||
b.Put([]byte("baz"), []byte("bat"))
|
b.Put([]byte("baz"), []byte("bat"))
|
||||||
b.Delete([]byte("foo"))
|
b.Delete([]byte("foo"))
|
||||||
|
@ -194,8 +214,8 @@ func TestDBTxBlock(t *testing.T) {
|
||||||
// Ensure a closed database returns an error while running a transaction block
|
// Ensure a closed database returns an error while running a transaction block
|
||||||
func TestDBTxBlockWhileClosed(t *testing.T) {
|
func TestDBTxBlockWhileClosed(t *testing.T) {
|
||||||
withDB(func(db *DB, path string) {
|
withDB(func(db *DB, path string) {
|
||||||
err := db.Do(func(txn *RWTx) error {
|
err := db.Do(func(tx *Tx) error {
|
||||||
txn.CreateBucket("widgets")
|
tx.CreateBucket("widgets")
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
assert.Equal(t, err, ErrDatabaseNotOpen)
|
assert.Equal(t, err, ErrDatabaseNotOpen)
|
||||||
|
@ -276,9 +296,9 @@ func TestDBCopyFile(t *testing.T) {
|
||||||
// Ensure the database can return stats about itself.
|
// Ensure the database can return stats about itself.
|
||||||
func TestDBStat(t *testing.T) {
|
func TestDBStat(t *testing.T) {
|
||||||
withOpenDB(func(db *DB, path string) {
|
withOpenDB(func(db *DB, path string) {
|
||||||
db.Do(func(txn *RWTx) error {
|
db.Do(func(tx *Tx) error {
|
||||||
txn.CreateBucket("widgets")
|
tx.CreateBucket("widgets")
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
for i := 0; i < 10000; i++ {
|
for i := 0; i < 10000; i++ {
|
||||||
b.Put([]byte(strconv.Itoa(i)), []byte(strconv.Itoa(i)))
|
b.Put([]byte(strconv.Itoa(i)), []byte(strconv.Itoa(i)))
|
||||||
}
|
}
|
||||||
|
@ -293,7 +313,7 @@ func TestDBStat(t *testing.T) {
|
||||||
t0, _ := db.Tx()
|
t0, _ := db.Tx()
|
||||||
t1, _ := db.Tx()
|
t1, _ := db.Tx()
|
||||||
t2, _ := db.Tx()
|
t2, _ := db.Tx()
|
||||||
t2.Close()
|
t2.Rollback()
|
||||||
|
|
||||||
// Obtain stats.
|
// Obtain stats.
|
||||||
stat, err := db.Stat()
|
stat, err := db.Stat()
|
||||||
|
@ -305,8 +325,8 @@ func TestDBStat(t *testing.T) {
|
||||||
assert.Equal(t, stat.TxCount, 2)
|
assert.Equal(t, stat.TxCount, 2)
|
||||||
|
|
||||||
// Close readers.
|
// Close readers.
|
||||||
t0.Close()
|
t0.Rollback()
|
||||||
t1.Close()
|
t1.Rollback()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
11
doc.go
11
doc.go
|
@ -14,14 +14,15 @@ The design of Bolt is based on Howard Chu's LMDB database project.
|
||||||
|
|
||||||
Basics
|
Basics
|
||||||
|
|
||||||
There are only a few types in Bolt: DB, Bucket, Tx, RWTx, and Cursor. The DB is
|
There are only a few types in Bolt: DB, Bucket, Tx, and Cursor. The DB is
|
||||||
a collection of buckets and is represented by a single file on disk. A bucket is
|
a collection of buckets and is represented by a single file on disk. A bucket is
|
||||||
a collection of unique keys that are associated with values.
|
a collection of unique keys that are associated with values.
|
||||||
|
|
||||||
Txs provide read-only access to data inside the database. They can retrieve
|
Transactions provide either read-only or read-write access to the database.
|
||||||
key/value pairs and can use Cursors to iterate over the entire dataset. RWTxs
|
Read-only transactions can retrieve key/value pairs and can use Cursors to
|
||||||
provide read-write access to the database. They can create and delete buckets
|
iterate over the dataset sequentially. Read-write transactions can create and
|
||||||
and they can insert and remove keys. Only one RWTx is allowed at a time.
|
delete buckets and can insert and remove keys. Only one read-write transaction
|
||||||
|
is allowed at a time.
|
||||||
|
|
||||||
|
|
||||||
Caveats
|
Caveats
|
||||||
|
|
4
error.go
4
error.go
|
@ -16,6 +16,10 @@ var (
|
||||||
// already open.
|
// already open.
|
||||||
ErrDatabaseOpen = &Error{"database already open", nil}
|
ErrDatabaseOpen = &Error{"database already open", nil}
|
||||||
|
|
||||||
|
// ErrTxNotWritable is returned when performing a write operation on a
|
||||||
|
// read-only transaction.
|
||||||
|
ErrTxNotWritable = &Error{"tx not writable", nil}
|
||||||
|
|
||||||
// ErrBucketNotFound is returned when trying to access a bucket that has
|
// ErrBucketNotFound is returned when trying to access a bucket that has
|
||||||
// not been created yet.
|
// not been created yet.
|
||||||
ErrBucketNotFound = &Error{"bucket not found", nil}
|
ErrBucketNotFound = &Error{"bucket not found", nil}
|
||||||
|
|
|
@ -67,7 +67,7 @@ func ExampleDB_Do() {
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// Execute several commands within a write transaction.
|
// Execute several commands within a write transaction.
|
||||||
err := db.Do(func(t *RWTx) error {
|
err := db.Do(func(t *Tx) error {
|
||||||
if err := t.CreateBucket("widgets"); err != nil {
|
if err := t.CreateBucket("widgets"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -134,30 +134,30 @@ func ExampleDB_ForEach() {
|
||||||
// A liger is awesome.
|
// A liger is awesome.
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleRWTx() {
|
func ExampleTx() {
|
||||||
// Open the database.
|
// Open the database.
|
||||||
var db DB
|
var db DB
|
||||||
db.Open("/tmp/bolt/rwtx.db", 0666)
|
db.Open("/tmp/bolt/tx.db", 0666)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// Create a bucket.
|
// Create a bucket.
|
||||||
db.CreateBucket("widgets")
|
db.CreateBucket("widgets")
|
||||||
|
|
||||||
// Create several keys in a transaction.
|
// Create several keys in a transaction.
|
||||||
rwtxn, _ := db.RWTx()
|
tx, _ := db.RWTx()
|
||||||
b := rwtxn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
b.Put([]byte("john"), []byte("blue"))
|
b.Put([]byte("john"), []byte("blue"))
|
||||||
b.Put([]byte("abby"), []byte("red"))
|
b.Put([]byte("abby"), []byte("red"))
|
||||||
b.Put([]byte("zephyr"), []byte("purple"))
|
b.Put([]byte("zephyr"), []byte("purple"))
|
||||||
rwtxn.Commit()
|
tx.Commit()
|
||||||
|
|
||||||
// Iterate over the values in sorted key order.
|
// Iterate over the values in sorted key order.
|
||||||
txn, _ := db.Tx()
|
tx, _ = db.Tx()
|
||||||
c := txn.Bucket("widgets").Cursor()
|
c := tx.Bucket("widgets").Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
fmt.Printf("%s likes %s\n", string(k), string(v))
|
fmt.Printf("%s likes %s\n", string(k), string(v))
|
||||||
}
|
}
|
||||||
txn.Close()
|
tx.Rollback()
|
||||||
|
|
||||||
// Output:
|
// Output:
|
||||||
// abby likes red
|
// abby likes red
|
||||||
|
@ -165,10 +165,10 @@ func ExampleRWTx() {
|
||||||
// zephyr likes purple
|
// zephyr likes purple
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExampleRWTx_rollback() {
|
func ExampleTx_rollback() {
|
||||||
// Open the database.
|
// Open the database.
|
||||||
var db DB
|
var db DB
|
||||||
db.Open("/tmp/bolt/rwtx_rollback.db", 0666)
|
db.Open("/tmp/bolt/tx_rollback.db", 0666)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
// Create a bucket.
|
// Create a bucket.
|
||||||
|
@ -178,10 +178,10 @@ func ExampleRWTx_rollback() {
|
||||||
db.Put("widgets", []byte("foo"), []byte("bar"))
|
db.Put("widgets", []byte("foo"), []byte("bar"))
|
||||||
|
|
||||||
// Update the key but rollback the transaction so it never saves.
|
// Update the key but rollback the transaction so it never saves.
|
||||||
rwtxn, _ := db.RWTx()
|
tx, _ := db.RWTx()
|
||||||
b := rwtxn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
b.Put([]byte("foo"), []byte("baz"))
|
b.Put([]byte("foo"), []byte("baz"))
|
||||||
rwtxn.Rollback()
|
tx.Rollback()
|
||||||
|
|
||||||
// Ensure that our original value is still set.
|
// Ensure that our original value is still set.
|
||||||
value, _ := db.Get("widgets", []byte("foo"))
|
value, _ := db.Get("widgets", []byte("foo"))
|
||||||
|
|
|
@ -45,7 +45,7 @@ func TestParallelTxs(t *testing.T) {
|
||||||
go func() {
|
go func() {
|
||||||
mutex.RLock()
|
mutex.RLock()
|
||||||
local := current
|
local := current
|
||||||
txn, err := db.Tx()
|
tx, err := db.Tx()
|
||||||
mutex.RUnlock()
|
mutex.RUnlock()
|
||||||
if err == ErrDatabaseNotOpen {
|
if err == ErrDatabaseNotOpen {
|
||||||
wg.Done()
|
wg.Done()
|
||||||
|
@ -56,15 +56,15 @@ func TestParallelTxs(t *testing.T) {
|
||||||
|
|
||||||
// Verify all data is in for local data list.
|
// Verify all data is in for local data list.
|
||||||
for _, item := range local {
|
for _, item := range local {
|
||||||
value := txn.Bucket("widgets").Get(item.Key)
|
value := tx.Bucket("widgets").Get(item.Key)
|
||||||
if !assert.NoError(t, err) || !assert.Equal(t, value, item.Value) {
|
if !assert.NoError(t, err) || !assert.Equal(t, value, item.Value) {
|
||||||
txn.Close()
|
tx.Rollback()
|
||||||
wg.Done()
|
wg.Done()
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
txn.Close()
|
tx.Rollback()
|
||||||
wg.Done()
|
wg.Done()
|
||||||
<-readers
|
<-readers
|
||||||
}()
|
}()
|
||||||
|
@ -83,13 +83,13 @@ func TestParallelTxs(t *testing.T) {
|
||||||
pending = pending[currentBatchSize:]
|
pending = pending[currentBatchSize:]
|
||||||
|
|
||||||
// Start write transaction.
|
// Start write transaction.
|
||||||
txn, err := db.RWTx()
|
tx, err := db.RWTx()
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
t.FailNow()
|
t.FailNow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert whole batch.
|
// Insert whole batch.
|
||||||
b := txn.Bucket("widgets")
|
b := tx.Bucket("widgets")
|
||||||
for _, item := range batchItems {
|
for _, item := range batchItems {
|
||||||
err := b.Put(item.Key, item.Value)
|
err := b.Put(item.Key, item.Value)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
|
@ -99,7 +99,7 @@ func TestParallelTxs(t *testing.T) {
|
||||||
|
|
||||||
// Commit and update the current list.
|
// Commit and update the current list.
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
err = txn.Commit()
|
err = tx.Commit()
|
||||||
current = append(current, batchItems...)
|
current = append(current, batchItems...)
|
||||||
mutex.Unlock()
|
mutex.Unlock()
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
|
|
2
meta.go
2
meta.go
|
@ -36,7 +36,7 @@ func (m *meta) copy(dest *meta) {
|
||||||
|
|
||||||
// write writes the meta onto a page.
|
// write writes the meta onto a page.
|
||||||
func (m *meta) write(p *page) {
|
func (m *meta) write(p *page) {
|
||||||
// Page id is either going to be 0 or 1 which we can determine by the Txn ID.
|
// Page id is either going to be 0 or 1 which we can determine by the transaction ID.
|
||||||
p.id = pgid(m.txid % 2)
|
p.id = pgid(m.txid % 2)
|
||||||
p.flags |= metaPageFlag
|
p.flags |= metaPageFlag
|
||||||
|
|
||||||
|
|
2
node.go
2
node.go
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
// node represents an in-memory, deserialized page.
|
// node represents an in-memory, deserialized page.
|
||||||
type node struct {
|
type node struct {
|
||||||
tx *RWTx
|
tx *Tx
|
||||||
isLeaf bool
|
isLeaf bool
|
||||||
unbalanced bool
|
unbalanced bool
|
||||||
key []byte
|
key []byte
|
||||||
|
|
|
@ -1,181 +0,0 @@
|
||||||
package bolt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure that a RWTx can be retrieved.
|
|
||||||
func TestRWTx(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
txn, err := db.RWTx()
|
|
||||||
assert.NotNil(t, txn)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, txn.DB(), db)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that opening a RWTx while the DB is closed returns an error.
|
|
||||||
func TestRWTxOpenWithClosedDB(t *testing.T) {
|
|
||||||
withDB(func(db *DB, path string) {
|
|
||||||
txn, err := db.RWTx()
|
|
||||||
assert.Equal(t, err, ErrDatabaseNotOpen)
|
|
||||||
assert.Nil(t, txn)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that retrieving all buckets returns writable buckets.
|
|
||||||
func TestRWTxBuckets(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
db.CreateBucket("woojits")
|
|
||||||
db.Do(func(txn *RWTx) error {
|
|
||||||
buckets := txn.Buckets()
|
|
||||||
assert.Equal(t, len(buckets), 2)
|
|
||||||
assert.Equal(t, buckets[0].Name(), "widgets")
|
|
||||||
assert.Equal(t, buckets[1].Name(), "woojits")
|
|
||||||
buckets[0].Put([]byte("foo"), []byte("0000"))
|
|
||||||
buckets[1].Put([]byte("bar"), []byte("0001"))
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
v, _ := db.Get("widgets", []byte("foo"))
|
|
||||||
assert.Equal(t, v, []byte("0000"))
|
|
||||||
v, _ = db.Get("woojits", []byte("bar"))
|
|
||||||
assert.Equal(t, v, []byte("0001"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a bucket can be created and retrieved.
|
|
||||||
func TestRWTxCreateBucket(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
// Create a bucket.
|
|
||||||
err := db.CreateBucket("widgets")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Read the bucket through a separate transaction.
|
|
||||||
b, err := db.Bucket("widgets")
|
|
||||||
assert.NotNil(t, b)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a bucket can be created if it doesn't already exist.
|
|
||||||
func TestRWTxCreateBucketIfNotExists(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
assert.NoError(t, db.CreateBucketIfNotExists("widgets"))
|
|
||||||
assert.NoError(t, db.CreateBucketIfNotExists("widgets"))
|
|
||||||
assert.Equal(t, db.CreateBucketIfNotExists(""), ErrBucketNameRequired)
|
|
||||||
|
|
||||||
// Read the bucket through a separate transaction.
|
|
||||||
b, err := db.Bucket("widgets")
|
|
||||||
assert.NotNil(t, b)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a bucket cannot be created twice.
|
|
||||||
func TestRWTxRecreateBucket(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
// Create a bucket.
|
|
||||||
err := db.CreateBucket("widgets")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Create the same bucket again.
|
|
||||||
err = db.CreateBucket("widgets")
|
|
||||||
assert.Equal(t, err, ErrBucketExists)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a bucket is created with a non-blank name.
|
|
||||||
func TestRWTxCreateBucketWithoutName(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
err := db.CreateBucket("")
|
|
||||||
assert.Equal(t, err, ErrBucketNameRequired)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a bucket name is not too long.
|
|
||||||
func TestRWTxCreateBucketWithLongName(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
err := db.CreateBucket(strings.Repeat("X", 255))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = db.CreateBucket(strings.Repeat("X", 256))
|
|
||||||
assert.Equal(t, err, ErrBucketNameTooLarge)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a bucket can be deleted.
|
|
||||||
func TestRWTxDeleteBucket(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
// Create a bucket and add a value.
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
db.Put("widgets", []byte("foo"), []byte("bar"))
|
|
||||||
|
|
||||||
b, _ := db.Bucket("widgets")
|
|
||||||
|
|
||||||
// Delete the bucket and make sure we can't get the value.
|
|
||||||
assert.NoError(t, db.DeleteBucket("widgets"))
|
|
||||||
value, err := db.Get("widgets", []byte("foo"))
|
|
||||||
assert.Equal(t, err, ErrBucketNotFound)
|
|
||||||
assert.Nil(t, value)
|
|
||||||
|
|
||||||
// Verify that the bucket's page is free.
|
|
||||||
assert.Equal(t, db.freelist.all(), []pgid{b.root})
|
|
||||||
|
|
||||||
// Create the bucket again and make sure there's not a phantom value.
|
|
||||||
assert.NoError(t, db.CreateBucket("widgets"))
|
|
||||||
value, err = db.Get("widgets", []byte("foo"))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Nil(t, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that an error is returned when deleting from a bucket that doesn't exist.
|
|
||||||
func TestRWTxDeleteBucketNotFound(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
err := db.DeleteBucket("widgets")
|
|
||||||
assert.Equal(t, err, ErrBucketNotFound)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark the performance of bulk put transactions in random order.
|
|
||||||
func BenchmarkRWTxPutRandom(b *testing.B) {
|
|
||||||
indexes := rand.Perm(b.N)
|
|
||||||
value := []byte(strings.Repeat("0", 64))
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
var txn *RWTx
|
|
||||||
var bucket *Bucket
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
if i%1000 == 0 {
|
|
||||||
if txn != nil {
|
|
||||||
txn.Commit()
|
|
||||||
}
|
|
||||||
txn, _ = db.RWTx()
|
|
||||||
bucket = txn.Bucket("widgets")
|
|
||||||
}
|
|
||||||
bucket.Put([]byte(strconv.Itoa(indexes[i])), value)
|
|
||||||
}
|
|
||||||
txn.Commit()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark the performance of bulk put transactions in sequential order.
|
|
||||||
func BenchmarkRWTxPutSequential(b *testing.B) {
|
|
||||||
value := []byte(strings.Repeat("0", 64))
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
db.Do(func(txn *RWTx) error {
|
|
||||||
bucket := txn.Bucket("widgets")
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
bucket.Put([]byte(strconv.Itoa(i)), value)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
134
transaction.go
134
transaction.go
|
@ -1,134 +0,0 @@
|
||||||
package bolt
|
|
||||||
|
|
||||||
// Tx represents a read-only transaction on the database.
|
|
||||||
// It can be used for retrieving values for keys as well as creating cursors for
|
|
||||||
// iterating over the data.
|
|
||||||
//
|
|
||||||
// IMPORTANT: You must close transactions when you are done with them. Pages
|
|
||||||
// can not be reclaimed by the writer until no more transactions are using them.
|
|
||||||
// A long running read transaction can cause the database to quickly grow.
|
|
||||||
type Tx struct {
|
|
||||||
db *DB
|
|
||||||
rwtx *RWTx
|
|
||||||
meta *meta
|
|
||||||
buckets *buckets
|
|
||||||
nodes map[pgid]*node
|
|
||||||
pages map[pgid]*page
|
|
||||||
}
|
|
||||||
|
|
||||||
// txid represents the internal transaction identifier.
|
|
||||||
type txid uint64
|
|
||||||
|
|
||||||
// init initializes the transaction and associates it with a database.
|
|
||||||
func (t *Tx) init(db *DB) {
|
|
||||||
t.db = db
|
|
||||||
t.pages = nil
|
|
||||||
|
|
||||||
// Copy the meta page since it can be changed by the writer.
|
|
||||||
t.meta = &meta{}
|
|
||||||
db.meta().copy(t.meta)
|
|
||||||
|
|
||||||
// Read in the buckets page.
|
|
||||||
t.buckets = &buckets{}
|
|
||||||
t.buckets.read(t.page(t.meta.buckets))
|
|
||||||
}
|
|
||||||
|
|
||||||
// id returns the transaction id.
|
|
||||||
func (t *Tx) id() txid {
|
|
||||||
return t.meta.txid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the transaction and releases any pages it is using.
|
|
||||||
func (t *Tx) Close() {
|
|
||||||
if t.db != nil {
|
|
||||||
if t.rwtx != nil {
|
|
||||||
t.rwtx.Rollback()
|
|
||||||
} else {
|
|
||||||
t.db.removeTx(t)
|
|
||||||
t.db = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB returns a reference to the database that created the transaction.
|
|
||||||
func (t *Tx) DB() *DB {
|
|
||||||
return t.db
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bucket retrieves a bucket by name.
|
|
||||||
// Returns nil if the bucket does not exist.
|
|
||||||
func (t *Tx) Bucket(name string) *Bucket {
|
|
||||||
b := t.buckets.get(name)
|
|
||||||
if b == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Bucket{
|
|
||||||
bucket: b,
|
|
||||||
name: name,
|
|
||||||
tx: t,
|
|
||||||
rwtx: t.rwtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buckets retrieves a list of all buckets.
|
|
||||||
func (t *Tx) Buckets() []*Bucket {
|
|
||||||
buckets := make([]*Bucket, 0, len(t.buckets.items))
|
|
||||||
for name, b := range t.buckets.items {
|
|
||||||
bucket := &Bucket{
|
|
||||||
bucket: b,
|
|
||||||
name: name,
|
|
||||||
tx: t,
|
|
||||||
rwtx: t.rwtx,
|
|
||||||
}
|
|
||||||
buckets = append(buckets, bucket)
|
|
||||||
}
|
|
||||||
return buckets
|
|
||||||
}
|
|
||||||
|
|
||||||
// page returns a reference to the page with a given id.
|
|
||||||
// If page has been written to then a temporary bufferred page is returned.
|
|
||||||
func (t *Tx) page(id pgid) *page {
|
|
||||||
// Check the dirty pages first.
|
|
||||||
if t.pages != nil {
|
|
||||||
if p, ok := t.pages[id]; ok {
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise return directly from the mmap.
|
|
||||||
return t.db.page(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// node returns a reference to the in-memory node for a given page, if it exists.
|
|
||||||
func (t *Tx) node(id pgid) *node {
|
|
||||||
if t.nodes == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return t.nodes[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
// pageNode returns the in-memory node, if it exists.
|
|
||||||
// Otherwise returns the underlying page.
|
|
||||||
func (t *Tx) pageNode(id pgid) (*page, *node) {
|
|
||||||
if n := t.node(id); n != nil {
|
|
||||||
return nil, n
|
|
||||||
}
|
|
||||||
return t.page(id), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// forEachPage iterates over every page within a given page and executes a function.
|
|
||||||
func (t *Tx) forEachPage(pgid pgid, depth int, fn func(*page, int)) {
|
|
||||||
p := t.page(pgid)
|
|
||||||
|
|
||||||
// Execute function.
|
|
||||||
fn(p, depth)
|
|
||||||
|
|
||||||
// Recursively loop over children.
|
|
||||||
if (p.flags & branchPageFlag) != 0 {
|
|
||||||
for i := 0; i < int(p.count); i++ {
|
|
||||||
elem := p.branchPageElement(uint16(i))
|
|
||||||
t.forEachPage(elem.pgid, depth+1, fn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,265 +0,0 @@
|
||||||
package bolt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"testing/quick"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure that the database can retrieve a list of buckets.
|
|
||||||
func TestTxBuckets(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
db.CreateBucket("foo")
|
|
||||||
db.CreateBucket("bar")
|
|
||||||
db.CreateBucket("baz")
|
|
||||||
buckets, err := db.Buckets()
|
|
||||||
if assert.NoError(t, err) && assert.Equal(t, len(buckets), 3) {
|
|
||||||
assert.Equal(t, buckets[0].Name(), "bar")
|
|
||||||
assert.Equal(t, buckets[1].Name(), "baz")
|
|
||||||
assert.Equal(t, buckets[2].Name(), "foo")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a Tx can retrieve a bucket.
|
|
||||||
func TestTxBucketMissing(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
b, err := db.Bucket("widgets")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
if assert.NotNil(t, b) {
|
|
||||||
assert.Equal(t, "widgets", b.Name())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a Tx retrieving a non-existent key returns nil.
|
|
||||||
func TestTxGetMissing(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
db.Put("widgets", []byte("foo"), []byte("bar"))
|
|
||||||
value, err := db.Get("widgets", []byte("no_such_key"))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Nil(t, value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a Tx cursor can iterate over an empty bucket without error.
|
|
||||||
func TestTxCursorEmptyBucket(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
txn, _ := db.Tx()
|
|
||||||
c := txn.Bucket("widgets").Cursor()
|
|
||||||
k, v := c.First()
|
|
||||||
assert.Nil(t, k)
|
|
||||||
assert.Nil(t, v)
|
|
||||||
txn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a Tx cursor can iterate over a single root with a couple elements.
|
|
||||||
func TestTxCursorLeafRoot(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.Tx()
|
|
||||||
c := txn.Bucket("widgets").Cursor()
|
|
||||||
|
|
||||||
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 Tx cursor can iterate in reverse over a single root with a couple elements.
|
|
||||||
func TestTxCursorLeafRootReverse(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.Tx()
|
|
||||||
c := txn.Bucket("widgets").Cursor()
|
|
||||||
|
|
||||||
k, v := c.Last()
|
|
||||||
assert.Equal(t, string(k), "foo")
|
|
||||||
assert.Equal(t, v, []byte{0})
|
|
||||||
|
|
||||||
k, v = c.Prev()
|
|
||||||
assert.Equal(t, string(k), "baz")
|
|
||||||
assert.Equal(t, v, []byte{})
|
|
||||||
|
|
||||||
k, v = c.Prev()
|
|
||||||
assert.Equal(t, string(k), "bar")
|
|
||||||
assert.Equal(t, v, []byte{1})
|
|
||||||
|
|
||||||
k, v = c.Prev()
|
|
||||||
assert.Nil(t, k)
|
|
||||||
assert.Nil(t, v)
|
|
||||||
|
|
||||||
k, v = c.Prev()
|
|
||||||
assert.Nil(t, k)
|
|
||||||
assert.Nil(t, v)
|
|
||||||
|
|
||||||
txn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a Tx cursor can restart from the beginning.
|
|
||||||
func TestTxCursorRestart(t *testing.T) {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
db.Put("widgets", []byte("bar"), []byte{})
|
|
||||||
db.Put("widgets", []byte("foo"), []byte{})
|
|
||||||
|
|
||||||
txn, _ := db.Tx()
|
|
||||||
c := txn.Bucket("widgets").Cursor()
|
|
||||||
|
|
||||||
k, _ := c.First()
|
|
||||||
assert.Equal(t, string(k), "bar")
|
|
||||||
|
|
||||||
k, _ = c.Next()
|
|
||||||
assert.Equal(t, string(k), "foo")
|
|
||||||
|
|
||||||
k, _ = c.First()
|
|
||||||
assert.Equal(t, string(k), "bar")
|
|
||||||
|
|
||||||
k, _ = c.Next()
|
|
||||||
assert.Equal(t, string(k), "foo")
|
|
||||||
|
|
||||||
txn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a Tx can iterate over all elements in a bucket.
|
|
||||||
func TestTxCursorIterate(t *testing.T) {
|
|
||||||
f := func(items testdata) bool {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
// Bulk insert all values.
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
rwtxn, _ := db.RWTx()
|
|
||||||
b := rwtxn.Bucket("widgets")
|
|
||||||
for _, item := range items {
|
|
||||||
assert.NoError(t, b.Put(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.Tx()
|
|
||||||
c := txn.Bucket("widgets").Cursor()
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a transaction can iterate over all elements in a bucket in reverse.
|
|
||||||
func TestTxCursorIterateReverse(t *testing.T) {
|
|
||||||
f := func(items testdata) bool {
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
// Bulk insert all values.
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
rwtxn, _ := db.RWTx()
|
|
||||||
b := rwtxn.Bucket("widgets")
|
|
||||||
for _, item := range items {
|
|
||||||
assert.NoError(t, b.Put(item.Key, item.Value))
|
|
||||||
}
|
|
||||||
assert.NoError(t, rwtxn.Commit())
|
|
||||||
|
|
||||||
// Sort test data.
|
|
||||||
sort.Sort(revtestdata(items))
|
|
||||||
|
|
||||||
// Iterate over all items and check consistency.
|
|
||||||
var index = 0
|
|
||||||
txn, _ := db.Tx()
|
|
||||||
c := txn.Bucket("widgets").Cursor()
|
|
||||||
for k, v := c.Last(); k != nil && index < len(items); k, v = c.Prev() {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Benchmark the performance iterating over a cursor.
|
|
||||||
func BenchmarkTxCursor(b *testing.B) {
|
|
||||||
indexes := rand.Perm(b.N)
|
|
||||||
value := []byte(strings.Repeat("0", 64))
|
|
||||||
|
|
||||||
withOpenDB(func(db *DB, path string) {
|
|
||||||
// Write data to bucket.
|
|
||||||
db.CreateBucket("widgets")
|
|
||||||
db.Do(func(txn *RWTx) error {
|
|
||||||
bucket := txn.Bucket("widgets")
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
bucket.Put([]byte(strconv.Itoa(indexes[i])), value)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
// Iterate over bucket using cursor.
|
|
||||||
db.With(func(txn *Tx) error {
|
|
||||||
count := 0
|
|
||||||
c := txn.Bucket("widgets").Cursor()
|
|
||||||
for k, _ := c.First(); k != nil; k, _ = c.Next() {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
if count != b.N {
|
|
||||||
b.Fatalf("wrong count: %d; expected: %d", count, b.N)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -5,31 +5,99 @@ import (
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RWTx represents a transaction that can read and write data.
|
// txid represents the internal transaction identifier.
|
||||||
// Only one read/write transaction can be active for a database at a time.
|
type txid uint64
|
||||||
// RWTx is composed of a read-only transaction so it can also use
|
|
||||||
// functions provided by Tx.
|
// Tx represents a read-only or read/write transaction on the database.
|
||||||
type RWTx struct {
|
// Read-only transactions can be used for retrieving values for keys and creating cursors.
|
||||||
Tx
|
// Read/write transactions can create and remove buckets and create and remove keys.
|
||||||
|
//
|
||||||
|
// IMPORTANT: You must commit or rollback transactions when you are done with
|
||||||
|
// them. Pages can not be reclaimed by the writer until no more transactions
|
||||||
|
// are using them. A long running read transaction can cause the database to
|
||||||
|
// quickly grow.
|
||||||
|
type Tx struct {
|
||||||
|
writable bool
|
||||||
|
db *DB
|
||||||
|
meta *meta
|
||||||
|
buckets *buckets
|
||||||
|
nodes map[pgid]*node
|
||||||
|
pages map[pgid]*page
|
||||||
pending []*node
|
pending []*node
|
||||||
}
|
}
|
||||||
|
|
||||||
// init initializes the transaction.
|
// init initializes the transaction.
|
||||||
func (t *RWTx) init(db *DB) {
|
func (t *Tx) init(db *DB) {
|
||||||
t.Tx.init(db)
|
t.db = db
|
||||||
t.Tx.rwtx = t
|
t.pages = nil
|
||||||
|
|
||||||
|
// Copy the meta page since it can be changed by the writer.
|
||||||
|
t.meta = &meta{}
|
||||||
|
db.meta().copy(t.meta)
|
||||||
|
|
||||||
|
// Read in the buckets page.
|
||||||
|
t.buckets = &buckets{}
|
||||||
|
t.buckets.read(t.page(t.meta.buckets))
|
||||||
|
|
||||||
|
if t.writable {
|
||||||
t.pages = make(map[pgid]*page)
|
t.pages = make(map[pgid]*page)
|
||||||
t.nodes = make(map[pgid]*node)
|
t.nodes = make(map[pgid]*node)
|
||||||
|
|
||||||
// Increment the transaction id.
|
// Increment the transaction id.
|
||||||
t.meta.txid += txid(1)
|
t.meta.txid += txid(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// id returns the transaction id.
|
||||||
|
func (t *Tx) id() txid {
|
||||||
|
return t.meta.txid
|
||||||
|
}
|
||||||
|
|
||||||
|
// DB returns a reference to the database that created the transaction.
|
||||||
|
func (t *Tx) DB() *DB {
|
||||||
|
return t.db
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writable returns whether the transaction can perform write operations.
|
||||||
|
func (t *Tx) Writable() bool {
|
||||||
|
return t.writable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bucket retrieves a bucket by name.
|
||||||
|
// Returns nil if the bucket does not exist.
|
||||||
|
func (t *Tx) Bucket(name string) *Bucket {
|
||||||
|
b := t.buckets.get(name)
|
||||||
|
if b == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Bucket{
|
||||||
|
bucket: b,
|
||||||
|
name: name,
|
||||||
|
tx: t,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buckets retrieves a list of all buckets.
|
||||||
|
func (t *Tx) Buckets() []*Bucket {
|
||||||
|
buckets := make([]*Bucket, 0, len(t.buckets.items))
|
||||||
|
for name, b := range t.buckets.items {
|
||||||
|
bucket := &Bucket{
|
||||||
|
bucket: b,
|
||||||
|
name: name,
|
||||||
|
tx: t,
|
||||||
|
}
|
||||||
|
buckets = append(buckets, bucket)
|
||||||
|
}
|
||||||
|
return buckets
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateBucket creates a new bucket.
|
// CreateBucket creates a new bucket.
|
||||||
// Returns an error if the bucket already exists, if the bucket name is blank, or if the bucket name is too long.
|
// Returns an error if the bucket already exists, if the bucket name is blank, or if the bucket name is too long.
|
||||||
func (t *RWTx) CreateBucket(name string) error {
|
func (t *Tx) CreateBucket(name string) error {
|
||||||
// Check if bucket already exists.
|
if !t.writable {
|
||||||
if b := t.Bucket(name); b != nil {
|
return ErrTxNotWritable
|
||||||
|
} else if b := t.Bucket(name); b != nil {
|
||||||
return ErrBucketExists
|
return ErrBucketExists
|
||||||
} else if len(name) == 0 {
|
} else if len(name) == 0 {
|
||||||
return ErrBucketNameRequired
|
return ErrBucketNameRequired
|
||||||
|
@ -52,7 +120,7 @@ func (t *RWTx) CreateBucket(name string) error {
|
||||||
|
|
||||||
// CreateBucketIfNotExists creates a new bucket if it doesn't already exist.
|
// CreateBucketIfNotExists creates a new bucket if it doesn't already exist.
|
||||||
// Returns an error if the bucket name is blank, or if the bucket name is too long.
|
// Returns an error if the bucket name is blank, or if the bucket name is too long.
|
||||||
func (t *RWTx) CreateBucketIfNotExists(name string) error {
|
func (t *Tx) CreateBucketIfNotExists(name string) error {
|
||||||
err := t.CreateBucket(name)
|
err := t.CreateBucket(name)
|
||||||
if err != nil && err != ErrBucketExists {
|
if err != nil && err != ErrBucketExists {
|
||||||
return err
|
return err
|
||||||
|
@ -62,7 +130,11 @@ func (t *RWTx) CreateBucketIfNotExists(name string) error {
|
||||||
|
|
||||||
// DeleteBucket deletes a bucket.
|
// DeleteBucket deletes a bucket.
|
||||||
// Returns an error if the bucket cannot be found.
|
// Returns an error if the bucket cannot be found.
|
||||||
func (t *RWTx) DeleteBucket(name string) error {
|
func (t *Tx) DeleteBucket(name string) error {
|
||||||
|
if !t.writable {
|
||||||
|
return ErrTxNotWritable
|
||||||
|
}
|
||||||
|
|
||||||
b := t.Bucket(name)
|
b := t.Bucket(name)
|
||||||
if b == nil {
|
if b == nil {
|
||||||
return ErrBucketNotFound
|
return ErrBucketNotFound
|
||||||
|
@ -81,11 +153,13 @@ func (t *RWTx) DeleteBucket(name string) error {
|
||||||
|
|
||||||
// Commit writes all changes to disk and updates the meta page.
|
// Commit writes all changes to disk and updates the meta page.
|
||||||
// Returns an error if a disk write error occurs.
|
// Returns an error if a disk write error occurs.
|
||||||
func (t *RWTx) Commit() error {
|
func (t *Tx) Commit() error {
|
||||||
if t.db == nil {
|
if t.db == nil {
|
||||||
return nil
|
return nil
|
||||||
|
} else if !t.writable {
|
||||||
|
t.Rollback()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
defer t.close()
|
defer t.close()
|
||||||
|
|
||||||
// TODO(benbjohnson): Use vectorized I/O to write out dirty pages.
|
// TODO(benbjohnson): Use vectorized I/O to write out dirty pages.
|
||||||
|
@ -118,19 +192,23 @@ func (t *RWTx) Commit() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rollback closes the transaction and ignores all previous updates.
|
// Rollback closes the transaction and ignores all previous updates.
|
||||||
func (t *RWTx) Rollback() {
|
func (t *Tx) Rollback() {
|
||||||
t.close()
|
t.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *RWTx) close() {
|
func (t *Tx) close() {
|
||||||
if t.db != nil {
|
if t.db != nil {
|
||||||
|
if t.writable {
|
||||||
t.db.rwlock.Unlock()
|
t.db.rwlock.Unlock()
|
||||||
|
} else {
|
||||||
|
t.db.removeTx(t)
|
||||||
|
}
|
||||||
t.db = nil
|
t.db = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// allocate returns a contiguous block of memory starting at a given page.
|
// allocate returns a contiguous block of memory starting at a given page.
|
||||||
func (t *RWTx) allocate(count int) (*page, error) {
|
func (t *Tx) allocate(count int) (*page, error) {
|
||||||
p, err := t.db.allocate(count)
|
p, err := t.db.allocate(count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -143,14 +221,14 @@ func (t *RWTx) allocate(count int) (*page, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// rebalance attempts to balance all nodes.
|
// rebalance attempts to balance all nodes.
|
||||||
func (t *RWTx) rebalance() {
|
func (t *Tx) rebalance() {
|
||||||
for _, n := range t.nodes {
|
for _, n := range t.nodes {
|
||||||
n.rebalance()
|
n.rebalance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// spill writes all the nodes to dirty pages.
|
// spill writes all the nodes to dirty pages.
|
||||||
func (t *RWTx) spill() error {
|
func (t *Tx) spill() error {
|
||||||
// Keep track of the current root nodes.
|
// Keep track of the current root nodes.
|
||||||
// We will update this at the end once all nodes are created.
|
// We will update this at the end once all nodes are created.
|
||||||
type root struct {
|
type root struct {
|
||||||
|
@ -233,7 +311,7 @@ func (t *RWTx) spill() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// write writes any dirty pages to disk.
|
// write writes any dirty pages to disk.
|
||||||
func (t *RWTx) write() error {
|
func (t *Tx) write() error {
|
||||||
// Sort pages by id.
|
// Sort pages by id.
|
||||||
pages := make(pages, 0, len(t.pages))
|
pages := make(pages, 0, len(t.pages))
|
||||||
for _, p := range t.pages {
|
for _, p := range t.pages {
|
||||||
|
@ -258,7 +336,7 @@ func (t *RWTx) write() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeMeta writes the meta to the disk.
|
// writeMeta writes the meta to the disk.
|
||||||
func (t *RWTx) writeMeta() error {
|
func (t *Tx) writeMeta() error {
|
||||||
// Create a temporary buffer for the meta page.
|
// Create a temporary buffer for the meta page.
|
||||||
buf := make([]byte, t.db.pageSize)
|
buf := make([]byte, t.db.pageSize)
|
||||||
p := t.db.pageInBuffer(buf, 0)
|
p := t.db.pageInBuffer(buf, 0)
|
||||||
|
@ -271,9 +349,11 @@ func (t *RWTx) writeMeta() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// node creates a node from a page and associates it with a given parent.
|
// node creates a node from a page and associates it with a given parent.
|
||||||
func (t *RWTx) node(pgid pgid, parent *node) *node {
|
func (t *Tx) node(pgid pgid, parent *node) *node {
|
||||||
// Retrieve node if it has already been fetched.
|
// Retrieve node if it's already been created.
|
||||||
if n := t.Tx.node(pgid); n != nil {
|
if t.nodes == nil {
|
||||||
|
return nil
|
||||||
|
} else if n := t.nodes[pgid]; n != nil {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,7 +369,7 @@ func (t *RWTx) node(pgid pgid, parent *node) *node {
|
||||||
}
|
}
|
||||||
|
|
||||||
// dereference removes all references to the old mmap.
|
// dereference removes all references to the old mmap.
|
||||||
func (t *RWTx) dereference() {
|
func (t *Tx) dereference() {
|
||||||
for _, n := range t.nodes {
|
for _, n := range t.nodes {
|
||||||
n.dereference()
|
n.dereference()
|
||||||
}
|
}
|
||||||
|
@ -298,3 +378,44 @@ func (t *RWTx) dereference() {
|
||||||
n.dereference()
|
n.dereference()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// page returns a reference to the page with a given id.
|
||||||
|
// If page has been written to then a temporary bufferred page is returned.
|
||||||
|
func (t *Tx) page(id pgid) *page {
|
||||||
|
// Check the dirty pages first.
|
||||||
|
if t.pages != nil {
|
||||||
|
if p, ok := t.pages[id]; ok {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise return directly from the mmap.
|
||||||
|
return t.db.page(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pageNode returns the in-memory node, if it exists.
|
||||||
|
// Otherwise returns the underlying page.
|
||||||
|
func (t *Tx) pageNode(id pgid) (*page, *node) {
|
||||||
|
if t.nodes != nil {
|
||||||
|
if n := t.nodes[id]; n != nil {
|
||||||
|
return nil, n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t.page(id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// forEachPage iterates over every page within a given page and executes a function.
|
||||||
|
func (t *Tx) forEachPage(pgid pgid, depth int, fn func(*page, int)) {
|
||||||
|
p := t.page(pgid)
|
||||||
|
|
||||||
|
// Execute function.
|
||||||
|
fn(p, depth)
|
||||||
|
|
||||||
|
// Recursively loop over children.
|
||||||
|
if (p.flags & branchPageFlag) != 0 {
|
||||||
|
for i := 0; i < int(p.count); i++ {
|
||||||
|
elem := p.branchPageElement(uint16(i))
|
||||||
|
t.forEachPage(elem.pgid, depth+1, fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,437 @@
|
||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"testing/quick"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure that the database can retrieve a list of buckets.
|
||||||
|
func TestTxBuckets(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
db.CreateBucket("foo")
|
||||||
|
db.CreateBucket("bar")
|
||||||
|
db.CreateBucket("baz")
|
||||||
|
buckets, err := db.Buckets()
|
||||||
|
if assert.NoError(t, err) && assert.Equal(t, len(buckets), 3) {
|
||||||
|
assert.Equal(t, buckets[0].Name(), "bar")
|
||||||
|
assert.Equal(t, buckets[1].Name(), "baz")
|
||||||
|
assert.Equal(t, buckets[2].Name(), "foo")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that creating a bucket with a read-only transaction returns an error.
|
||||||
|
func TestTxCreateBucketReadOnly(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
db.With(func(tx *Tx) error {
|
||||||
|
assert.Equal(t, tx.CreateBucket("foo"), ErrTxNotWritable)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a Tx can retrieve a bucket.
|
||||||
|
func TestTxBucketMissing(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
b, err := db.Bucket("widgets")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if assert.NotNil(t, b) {
|
||||||
|
assert.Equal(t, "widgets", b.Name())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a Tx retrieving a non-existent key returns nil.
|
||||||
|
func TestTxGetMissing(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
db.Put("widgets", []byte("foo"), []byte("bar"))
|
||||||
|
value, err := db.Get("widgets", []byte("no_such_key"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that retrieving all buckets returns writable buckets.
|
||||||
|
func TestTxWritableBuckets(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
db.CreateBucket("woojits")
|
||||||
|
db.Do(func(tx *Tx) error {
|
||||||
|
buckets := tx.Buckets()
|
||||||
|
assert.Equal(t, len(buckets), 2)
|
||||||
|
assert.Equal(t, buckets[0].Name(), "widgets")
|
||||||
|
assert.Equal(t, buckets[1].Name(), "woojits")
|
||||||
|
buckets[0].Put([]byte("foo"), []byte("0000"))
|
||||||
|
buckets[1].Put([]byte("bar"), []byte("0001"))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
v, _ := db.Get("widgets", []byte("foo"))
|
||||||
|
assert.Equal(t, v, []byte("0000"))
|
||||||
|
v, _ = db.Get("woojits", []byte("bar"))
|
||||||
|
assert.Equal(t, v, []byte("0001"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a bucket can be created and retrieved.
|
||||||
|
func TestTxCreateBucket(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
// Create a bucket.
|
||||||
|
err := db.CreateBucket("widgets")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Read the bucket through a separate transaction.
|
||||||
|
b, err := db.Bucket("widgets")
|
||||||
|
assert.NotNil(t, b)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a bucket can be created if it doesn't already exist.
|
||||||
|
func TestTxCreateBucketIfNotExists(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
assert.NoError(t, db.CreateBucketIfNotExists("widgets"))
|
||||||
|
assert.NoError(t, db.CreateBucketIfNotExists("widgets"))
|
||||||
|
assert.Equal(t, db.CreateBucketIfNotExists(""), ErrBucketNameRequired)
|
||||||
|
|
||||||
|
// Read the bucket through a separate transaction.
|
||||||
|
b, err := db.Bucket("widgets")
|
||||||
|
assert.NotNil(t, b)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a bucket cannot be created twice.
|
||||||
|
func TestTxRecreateBucket(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
// Create a bucket.
|
||||||
|
err := db.CreateBucket("widgets")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Create the same bucket again.
|
||||||
|
err = db.CreateBucket("widgets")
|
||||||
|
assert.Equal(t, err, ErrBucketExists)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a bucket is created with a non-blank name.
|
||||||
|
func TestTxCreateBucketWithoutName(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
err := db.CreateBucket("")
|
||||||
|
assert.Equal(t, err, ErrBucketNameRequired)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a bucket name is not too long.
|
||||||
|
func TestTxCreateBucketWithLongName(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
err := db.CreateBucket(strings.Repeat("X", 255))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = db.CreateBucket(strings.Repeat("X", 256))
|
||||||
|
assert.Equal(t, err, ErrBucketNameTooLarge)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a bucket can be deleted.
|
||||||
|
func TestTxDeleteBucket(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
// Create a bucket and add a value.
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
db.Put("widgets", []byte("foo"), []byte("bar"))
|
||||||
|
|
||||||
|
b, _ := db.Bucket("widgets")
|
||||||
|
|
||||||
|
// Delete the bucket and make sure we can't get the value.
|
||||||
|
assert.NoError(t, db.DeleteBucket("widgets"))
|
||||||
|
value, err := db.Get("widgets", []byte("foo"))
|
||||||
|
assert.Equal(t, err, ErrBucketNotFound)
|
||||||
|
assert.Nil(t, value)
|
||||||
|
|
||||||
|
// Verify that the bucket's page is free.
|
||||||
|
assert.Equal(t, db.freelist.all(), []pgid{b.root})
|
||||||
|
|
||||||
|
// Create the bucket again and make sure there's not a phantom value.
|
||||||
|
assert.NoError(t, db.CreateBucket("widgets"))
|
||||||
|
value, err = db.Get("widgets", []byte("foo"))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Nil(t, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that deleting a bucket with a read-only transaction returns an error.
|
||||||
|
func TestTxDeleteBucketReadOnly(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
db.With(func(tx *Tx) error {
|
||||||
|
assert.Equal(t, tx.DeleteBucket("foo"), ErrTxNotWritable)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that an error is returned when deleting from a bucket that doesn't exist.
|
||||||
|
func TestTxDeleteBucketNotFound(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
err := db.DeleteBucket("widgets")
|
||||||
|
assert.Equal(t, err, ErrBucketNotFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a Tx cursor can iterate over an empty bucket without error.
|
||||||
|
func TestTxCursorEmptyBucket(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
tx, _ := db.Tx()
|
||||||
|
c := tx.Bucket("widgets").Cursor()
|
||||||
|
k, v := c.First()
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Nil(t, v)
|
||||||
|
tx.Commit()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a Tx cursor can iterate over a single root with a couple elements.
|
||||||
|
func TestTxCursorLeafRoot(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})
|
||||||
|
tx, _ := db.Tx()
|
||||||
|
c := tx.Bucket("widgets").Cursor()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
tx.Rollback()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a Tx cursor can iterate in reverse over a single root with a couple elements.
|
||||||
|
func TestTxCursorLeafRootReverse(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})
|
||||||
|
tx, _ := db.Tx()
|
||||||
|
c := tx.Bucket("widgets").Cursor()
|
||||||
|
|
||||||
|
k, v := c.Last()
|
||||||
|
assert.Equal(t, string(k), "foo")
|
||||||
|
assert.Equal(t, v, []byte{0})
|
||||||
|
|
||||||
|
k, v = c.Prev()
|
||||||
|
assert.Equal(t, string(k), "baz")
|
||||||
|
assert.Equal(t, v, []byte{})
|
||||||
|
|
||||||
|
k, v = c.Prev()
|
||||||
|
assert.Equal(t, string(k), "bar")
|
||||||
|
assert.Equal(t, v, []byte{1})
|
||||||
|
|
||||||
|
k, v = c.Prev()
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Nil(t, v)
|
||||||
|
|
||||||
|
k, v = c.Prev()
|
||||||
|
assert.Nil(t, k)
|
||||||
|
assert.Nil(t, v)
|
||||||
|
|
||||||
|
tx.Rollback()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a Tx cursor can restart from the beginning.
|
||||||
|
func TestTxCursorRestart(t *testing.T) {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
db.Put("widgets", []byte("bar"), []byte{})
|
||||||
|
db.Put("widgets", []byte("foo"), []byte{})
|
||||||
|
|
||||||
|
tx, _ := db.Tx()
|
||||||
|
c := tx.Bucket("widgets").Cursor()
|
||||||
|
|
||||||
|
k, _ := c.First()
|
||||||
|
assert.Equal(t, string(k), "bar")
|
||||||
|
|
||||||
|
k, _ = c.Next()
|
||||||
|
assert.Equal(t, string(k), "foo")
|
||||||
|
|
||||||
|
k, _ = c.First()
|
||||||
|
assert.Equal(t, string(k), "bar")
|
||||||
|
|
||||||
|
k, _ = c.Next()
|
||||||
|
assert.Equal(t, string(k), "foo")
|
||||||
|
|
||||||
|
tx.Rollback()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a Tx can iterate over all elements in a bucket.
|
||||||
|
func TestTxCursorIterate(t *testing.T) {
|
||||||
|
f := func(items testdata) bool {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
// Bulk insert all values.
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
tx, _ := db.RWTx()
|
||||||
|
b := tx.Bucket("widgets")
|
||||||
|
for _, item := range items {
|
||||||
|
assert.NoError(t, b.Put(item.Key, item.Value))
|
||||||
|
}
|
||||||
|
assert.NoError(t, tx.Commit())
|
||||||
|
|
||||||
|
// Sort test data.
|
||||||
|
sort.Sort(items)
|
||||||
|
|
||||||
|
// Iterate over all items and check consistency.
|
||||||
|
var index = 0
|
||||||
|
tx, _ = db.Tx()
|
||||||
|
c := tx.Bucket("widgets").Cursor()
|
||||||
|
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)
|
||||||
|
tx.Rollback()
|
||||||
|
})
|
||||||
|
fmt.Fprint(os.Stderr, ".")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if err := quick.Check(f, qconfig()); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
fmt.Fprint(os.Stderr, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that a transaction can iterate over all elements in a bucket in reverse.
|
||||||
|
func TestTxCursorIterateReverse(t *testing.T) {
|
||||||
|
f := func(items testdata) bool {
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
// Bulk insert all values.
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
tx, _ := db.RWTx()
|
||||||
|
b := tx.Bucket("widgets")
|
||||||
|
for _, item := range items {
|
||||||
|
assert.NoError(t, b.Put(item.Key, item.Value))
|
||||||
|
}
|
||||||
|
assert.NoError(t, tx.Commit())
|
||||||
|
|
||||||
|
// Sort test data.
|
||||||
|
sort.Sort(revtestdata(items))
|
||||||
|
|
||||||
|
// Iterate over all items and check consistency.
|
||||||
|
var index = 0
|
||||||
|
tx, _ = db.Tx()
|
||||||
|
c := tx.Bucket("widgets").Cursor()
|
||||||
|
for k, v := c.Last(); k != nil && index < len(items); k, v = c.Prev() {
|
||||||
|
assert.Equal(t, k, items[index].Key)
|
||||||
|
assert.Equal(t, v, items[index].Value)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
assert.Equal(t, len(items), index)
|
||||||
|
tx.Rollback()
|
||||||
|
})
|
||||||
|
fmt.Fprint(os.Stderr, ".")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if err := quick.Check(f, qconfig()); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
fmt.Fprint(os.Stderr, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark the performance iterating over a cursor.
|
||||||
|
func BenchmarkTxCursor(b *testing.B) {
|
||||||
|
indexes := rand.Perm(b.N)
|
||||||
|
value := []byte(strings.Repeat("0", 64))
|
||||||
|
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
// Write data to bucket.
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
db.Do(func(tx *Tx) error {
|
||||||
|
bucket := tx.Bucket("widgets")
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bucket.Put([]byte(strconv.Itoa(indexes[i])), value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
// Iterate over bucket using cursor.
|
||||||
|
db.With(func(tx *Tx) error {
|
||||||
|
count := 0
|
||||||
|
c := tx.Bucket("widgets").Cursor()
|
||||||
|
for k, _ := c.First(); k != nil; k, _ = c.Next() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
if count != b.N {
|
||||||
|
b.Fatalf("wrong count: %d; expected: %d", count, b.N)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark the performance of bulk put transactions in random order.
|
||||||
|
func BenchmarkTxPutRandom(b *testing.B) {
|
||||||
|
indexes := rand.Perm(b.N)
|
||||||
|
value := []byte(strings.Repeat("0", 64))
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
var tx *Tx
|
||||||
|
var bucket *Bucket
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if i%1000 == 0 {
|
||||||
|
if tx != nil {
|
||||||
|
tx.Commit()
|
||||||
|
}
|
||||||
|
tx, _ = db.RWTx()
|
||||||
|
bucket = tx.Bucket("widgets")
|
||||||
|
}
|
||||||
|
bucket.Put([]byte(strconv.Itoa(indexes[i])), value)
|
||||||
|
}
|
||||||
|
tx.Commit()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark the performance of bulk put transactions in sequential order.
|
||||||
|
func BenchmarkTxPutSequential(b *testing.B) {
|
||||||
|
value := []byte(strings.Repeat("0", 64))
|
||||||
|
withOpenDB(func(db *DB, path string) {
|
||||||
|
db.CreateBucket("widgets")
|
||||||
|
db.Do(func(tx *Tx) error {
|
||||||
|
bucket := tx.Bucket("widgets")
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bucket.Put([]byte(strconv.Itoa(i)), value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue