aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Johnson <benbjohnson@yahoo.com>2014-03-08 20:25:37 -0700
committerBen Johnson <benbjohnson@yahoo.com>2014-03-08 20:40:48 -0700
commitc551e45a4722f58dc4c19f9d1b80b0b8db3ad039 (patch)
tree93ac43914f9cd20c8eea13f18105e55026a7ea8d
parentRename Transaction to Tx. (diff)
downloaddedo-c551e45a4722f58dc4c19f9d1b80b0b8db3ad039.tar.gz
dedo-c551e45a4722f58dc4c19f9d1b80b0b8db3ad039.tar.xz
Consolidate Tx and RWTx.
-rw-r--r--bucket.go7
-rw-r--r--bucket_test.go68
-rw-r--r--cursor.go2
-rw-r--r--db.go46
-rw-r--r--db_test.go46
-rw-r--r--doc.go11
-rw-r--r--error.go6
-rw-r--r--example_test.go28
-rw-r--r--functional_test.go14
-rw-r--r--meta.go2
-rw-r--r--node.go2
-rw-r--r--rwtransaction_test.go181
-rw-r--r--transaction.go134
-rw-r--r--transaction_test.go265
-rw-r--r--tx.go (renamed from rwtransaction.go)187
-rw-r--r--tx_test.go437
16 files changed, 718 insertions, 718 deletions
diff --git a/bucket.go b/bucket.go
index e406a6e..e1ad2a3 100644
--- a/bucket.go
+++ b/bucket.go
@@ -9,7 +9,6 @@ type Bucket struct {
*bucket
name string
tx *Tx
- rwtx *RWTx
}
// 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.
func (b *Bucket) Writable() bool {
- return (b.rwtx != nil)
+ return b.tx.writable
}
// Cursor creates a cursor associated with the bucket.
@@ -74,7 +73,7 @@ func (b *Bucket) Put(key []byte, value []byte) error {
c.Seek(key)
// Insert the key/value.
- c.node(b.rwtx).put(key, key, value, 0)
+ c.node(b.tx).put(key, key, value, 0)
return nil
}
@@ -92,7 +91,7 @@ func (b *Bucket) Delete(key []byte) error {
c.Seek(key)
// Delete the node if we have a matching key.
- c.node(b.rwtx).del(key)
+ c.node(b.tx).del(key)
return nil
}
diff --git a/bucket_test.go b/bucket_test.go
index a599557..9ccc488 100644
--- a/bucket_test.go
+++ b/bucket_test.go
@@ -27,8 +27,8 @@ func TestBucketGetNonExistent(t *testing.T) {
func TestBucketGetFromNode(t *testing.T) {
withOpenDB(func(db *DB, path string) {
db.CreateBucket("widgets")
- db.Do(func(txn *RWTx) error {
- b := txn.Bucket("widgets")
+ db.Do(func(tx *Tx) error {
+ b := tx.Bucket("widgets")
b.Put([]byte("foo"), []byte("bar"))
value := b.Get([]byte("foo"))
assert.Equal(t, value, []byte("bar"))
@@ -54,8 +54,8 @@ func TestBucketPut(t *testing.T) {
func TestBucketPutReadOnly(t *testing.T) {
withOpenDB(func(db *DB, path string) {
db.CreateBucket("widgets")
- db.With(func(txn *Tx) error {
- b := txn.Bucket("widgets")
+ db.With(func(tx *Tx) error {
+ b := tx.Bucket("widgets")
err := b.Put([]byte("foo"), []byte("bar"))
assert.Equal(t, err, ErrBucketNotWritable)
return nil
@@ -81,8 +81,8 @@ func TestBucketDelete(t *testing.T) {
func TestBucketDeleteReadOnly(t *testing.T) {
withOpenDB(func(db *DB, path string) {
db.CreateBucket("widgets")
- db.With(func(txn *Tx) error {
- b := txn.Bucket("widgets")
+ db.With(func(tx *Tx) error {
+ b := tx.Bucket("widgets")
err := b.Delete([]byte("foo"))
assert.Equal(t, err, ErrBucketNotWritable)
return nil
@@ -120,8 +120,8 @@ func TestBucketNextSequence(t *testing.T) {
func TestBucketNextSequenceReadOnly(t *testing.T) {
withOpenDB(func(db *DB, path string) {
db.CreateBucket("widgets")
- db.With(func(txn *Tx) error {
- b := txn.Bucket("widgets")
+ db.With(func(tx *Tx) error {
+ b := tx.Bucket("widgets")
i, err := b.NextSequence()
assert.Equal(t, i, 0)
assert.Equal(t, err, ErrBucketNotWritable)
@@ -134,8 +134,8 @@ func TestBucketNextSequenceReadOnly(t *testing.T) {
func TestBucketNextSequenceOverflow(t *testing.T) {
withOpenDB(func(db *DB, path string) {
db.CreateBucket("widgets")
- db.Do(func(txn *RWTx) error {
- b := txn.Bucket("widgets")
+ db.Do(func(tx *Tx) error {
+ b := tx.Bucket("widgets")
b.bucket.sequence = uint64(maxInt)
seq, err := b.NextSequence()
assert.Equal(t, err, ErrSequenceOverflow)
@@ -218,31 +218,31 @@ func TestBucketPutKeyTooLarge(t *testing.T) {
// Ensure a bucket can calculate stats.
func TestBucketStat(t *testing.T) {
withOpenDB(func(db *DB, path string) {
- db.Do(func(txn *RWTx) error {
+ db.Do(func(tx *Tx) error {
// Add bucket with lots of keys.
- txn.CreateBucket("widgets")
- b := txn.Bucket("widgets")
+ tx.CreateBucket("widgets")
+ b := tx.Bucket("widgets")
for i := 0; i < 100000; i++ {
b.Put([]byte(strconv.Itoa(i)), []byte(strconv.Itoa(i)))
}
// Add bucket with fewer keys but one big value.
- txn.CreateBucket("woojits")
- b = txn.Bucket("woojits")
+ tx.CreateBucket("woojits")
+ b = tx.Bucket("woojits")
for i := 0; i < 500; i++ {
b.Put([]byte(strconv.Itoa(i)), []byte(strconv.Itoa(i)))
}
b.Put([]byte("really-big-value"), []byte(strings.Repeat("*", 10000)))
// Add a bucket that fits on a single root leaf.
- txn.CreateBucket("whozawhats")
- b = txn.Bucket("whozawhats")
+ tx.CreateBucket("whozawhats")
+ b = tx.Bucket("whozawhats")
b.Put([]byte("foo"), []byte("bar"))
return nil
})
- db.With(func(txn *Tx) error {
- b := txn.Bucket("widgets")
+ db.With(func(tx *Tx) error {
+ b := tx.Bucket("widgets")
stat := b.Stat()
assert.Equal(t, stat.BranchPageCount, 15)
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.MaxDepth, 3)
- b = txn.Bucket("woojits")
+ b = tx.Bucket("woojits")
stat = b.Stat()
assert.Equal(t, stat.BranchPageCount, 1)
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.MaxDepth, 2)
- b = txn.Bucket("whozawhats")
+ b = tx.Bucket("whozawhats")
stat = b.Stat()
assert.Equal(t, stat.BranchPageCount, 0)
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) {
index := 0
f := func(items testdata) bool {
@@ -317,16 +317,16 @@ func TestBucketPutMultiple(t *testing.T) {
withOpenDB(func(db *DB, path string) {
// Bulk insert all values.
db.CreateBucket("widgets")
- rwtxn, _ := db.RWTx()
- b := rwtxn.Bucket("widgets")
+ tx, _ := db.RWTx()
+ b := tx.Bucket("widgets")
for _, item := range items {
assert.NoError(t, b.Put(item.Key, item.Value))
}
- assert.NoError(t, rwtxn.Commit())
+ assert.NoError(t, tx.Commit())
// Verify all items exist.
- txn, _ := db.Tx()
- b = txn.Bucket("widgets")
+ tx, _ = db.Tx()
+ b = tx.Bucket("widgets")
for _, item := range items {
value := b.Get(item.Key)
if !assert.Equal(t, item.Value, value) {
@@ -334,7 +334,7 @@ func TestBucketPutMultiple(t *testing.T) {
t.FailNow()
}
}
- txn.Close()
+ tx.Rollback()
})
fmt.Fprint(os.Stderr, ".")
return true
@@ -351,20 +351,20 @@ func TestBucketDeleteQuick(t *testing.T) {
withOpenDB(func(db *DB, path string) {
// Bulk insert all values.
db.CreateBucket("widgets")
- rwtxn, _ := db.RWTx()
- b := rwtxn.Bucket("widgets")
+ tx, _ := db.RWTx()
+ b := tx.Bucket("widgets")
for _, item := range items {
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.
for i, item := range items {
assert.NoError(t, db.Delete("widgets", item.Key))
// Anything before our deletion index should be nil.
- txn, _ := db.Tx()
- b := txn.Bucket("widgets")
+ tx, _ := db.Tx()
+ b := tx.Bucket("widgets")
for j, exp := range items {
if j > i {
value := b.Get(exp.Key)
@@ -378,7 +378,7 @@ func TestBucketDeleteQuick(t *testing.T) {
}
}
}
- txn.Close()
+ tx.Rollback()
}
})
fmt.Fprint(os.Stderr, ".")
diff --git a/cursor.go b/cursor.go
index 2907b84..244c915 100644
--- a/cursor.go
+++ b/cursor.go
@@ -251,7 +251,7 @@ func (c *Cursor) keyValue() ([]byte, []byte) {
}
// 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")
// If the top of the stack is a leaf node then just return it.
diff --git a/db.go b/db.go
index bf7be45..6493e1b 100644
--- a/db.go
+++ b/db.go
@@ -29,7 +29,7 @@ type DB struct {
meta1 *meta
pageSize int
opened bool
- rwtx *RWTx
+ rwtx *Tx
txs []*Tx
freelist *freelist
@@ -290,11 +290,11 @@ func (db *DB) Tx() (*Tx, error) {
// RWTx creates a read/write transaction.
// Only one read/write transaction is allowed at a time.
// 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()
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()
// 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.
- t := &RWTx{}
+ t := &Tx{writable: true}
t.init(db)
db.rwtx = t
@@ -331,20 +331,20 @@ func (db *DB) removeTx(t *Tx) {
db.mmaplock.RUnlock()
// Remove the transaction.
- for i, txn := range db.txs {
- if txn == t {
+ for i, tx := range db.txs {
+ if tx == t {
db.txs = append(db.txs[:i], db.txs[i+1:]...)
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 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
// 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()
if err != nil {
return err
@@ -366,7 +366,7 @@ func (db *DB) With(fn func(*Tx) error) error {
if err != nil {
return err
}
- defer t.Close()
+ defer t.Rollback()
// If an error is returned from the function then pass it through.
return fn(t)
@@ -391,7 +391,7 @@ func (db *DB) Bucket(name string) (*Bucket, error) {
if err != nil {
return nil, err
}
- defer t.Close()
+ defer t.Rollback()
return t.Bucket(name), nil
}
@@ -401,7 +401,7 @@ func (db *DB) Buckets() ([]*Bucket, error) {
if err != nil {
return nil, err
}
- defer t.Close()
+ defer t.Rollback()
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
// is blank, or the bucket name is too long.
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)
})
}
@@ -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.
// 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 {
- return db.Do(func(t *RWTx) error {
+ return db.Do(func(t *Tx) error {
return t.CreateBucketIfNotExists(name)
})
}
@@ -425,7 +425,7 @@ func (db *DB) CreateBucketIfNotExists(name string) error {
// DeleteBucket removes a bucket from the database.
// Returns an error if the bucket does not exist.
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)
})
}
@@ -434,7 +434,7 @@ func (db *DB) DeleteBucket(name string) error {
// This function can return an error if the bucket does not exist.
func (db *DB) NextSequence(name string) (int, error) {
var seq int
- err := db.Do(func(t *RWTx) error {
+ err := db.Do(func(t *Tx) error {
b := t.Bucket(name)
if b == nil {
return ErrBucketNotFound
@@ -457,17 +457,15 @@ func (db *DB) Get(name string, key []byte) ([]byte, error) {
if err != nil {
return nil, err
}
- defer t.Close()
+ defer t.Rollback()
// Open bucket and retrieve value for key.
b := t.Bucket(name)
if b == nil {
return nil, ErrBucketNotFound
}
- value, err := b.Get(key), nil
- if err != nil {
- return nil, err
- } else if value == nil {
+ value := b.Get(key)
+ if value == 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.
// 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 {
- return db.Do(func(t *RWTx) error {
+ return db.Do(func(t *Tx) error {
b := t.Bucket(name)
if b == nil {
return ErrBucketNotFound
@@ -494,7 +492,7 @@ func (db *DB) Put(name string, key []byte, value []byte) error {
// Delete removes a key from a bucket.
// Returns an error if the bucket cannot be found.
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)
if b == nil {
return ErrBucketNotFound
@@ -512,7 +510,7 @@ func (db *DB) Copy(w io.Writer) error {
if err != nil {
return err
}
- defer t.Close()
+ defer t.Rollback()
// Open reader on the database.
f, err := os.Open(db.path)
@@ -557,7 +555,7 @@ func (db *DB) Stat() (*Stat, error) {
db.mmaplock.RUnlock()
db.metalock.Unlock()
- err := db.Do(func(t *RWTx) error {
+ err := db.Do(func(t *Tx) error {
s.PageCount = int(t.meta.pgid)
s.FreePageCount = len(db.freelist.all())
s.PageSize = db.pageSize
diff --git a/db_test.go b/db_test.go
index 029d248..e35502c 100644
--- a/db_test.go
+++ b/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.
func TestDBTxErrDatabaseNotOpen(t *testing.T) {
withDB(func(db *DB, path string) {
- txn, err := db.Tx()
- assert.Nil(t, txn)
+ tx, err := db.Tx()
+ assert.Nil(t, tx)
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.
func TestDBTxBlock(t *testing.T) {
withOpenDB(func(db *DB, path string) {
- err := db.Do(func(txn *RWTx) error {
- txn.CreateBucket("widgets")
- b := txn.Bucket("widgets")
+ err := db.Do(func(tx *Tx) error {
+ tx.CreateBucket("widgets")
+ b := tx.Bucket("widgets")
b.Put([]byte("foo"), []byte("bar"))
b.Put([]byte("baz"), []byte("bat"))
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
func TestDBTxBlockWhileClosed(t *testing.T) {
withDB(func(db *DB, path string) {
- err := db.Do(func(txn *RWTx) error {
- txn.CreateBucket("widgets")
+ err := db.Do(func(tx *Tx) error {
+ tx.CreateBucket("widgets")
return nil
})
assert.Equal(t, err, ErrDatabaseNotOpen)
@@ -276,9 +296,9 @@ func TestDBCopyFile(t *testing.T) {
// Ensure the database can return stats about itself.
func TestDBStat(t *testing.T) {
withOpenDB(func(db *DB, path string) {
- db.Do(func(txn *RWTx) error {
- txn.CreateBucket("widgets")
- b := txn.Bucket("widgets")
+ db.Do(func(tx *Tx) error {
+ tx.CreateBucket("widgets")
+ b := tx.Bucket("widgets")
for i := 0; i < 10000; i++ {
b.Put([]byte(strconv.Itoa(i)), []byte(strconv.Itoa(i)))
}
@@ -293,7 +313,7 @@ func TestDBStat(t *testing.T) {
t0, _ := db.Tx()
t1, _ := db.Tx()
t2, _ := db.Tx()
- t2.Close()
+ t2.Rollback()
// Obtain stats.
stat, err := db.Stat()
@@ -305,8 +325,8 @@ func TestDBStat(t *testing.T) {
assert.Equal(t, stat.TxCount, 2)
// Close readers.
- t0.Close()
- t1.Close()
+ t0.Rollback()
+ t1.Rollback()
})
}
diff --git a/doc.go b/doc.go
index ec576a2..caf66e9 100644
--- a/doc.go
+++ b/doc.go
@@ -14,14 +14,15 @@ The design of Bolt is based on Howard Chu's LMDB database project.
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 unique keys that are associated with values.
-Txs provide read-only access to data inside the database. They can retrieve
-key/value pairs and can use Cursors to iterate over the entire dataset. RWTxs
-provide read-write access to the database. They can create and delete buckets
-and they can insert and remove keys. Only one RWTx is allowed at a time.
+Transactions provide either read-only or read-write access to the database.
+Read-only transactions can retrieve key/value pairs and can use Cursors to
+iterate over the dataset sequentially. Read-write transactions can create and
+delete buckets and can insert and remove keys. Only one read-write transaction
+is allowed at a time.
Caveats
diff --git a/error.go b/error.go
index 7e879a1..7f867b2 100644
--- a/error.go
+++ b/error.go
@@ -4,7 +4,7 @@ var (
// ErrInvalid is returned when a data file is not a Bolt-formatted database.
ErrInvalid = &Error{"Invalid database", nil}
- // ErrVersionMismatch is returned when the data file was created with a
+ // ErrVersionMismatch is returned when the data file was created with a
// different version of Bolt.
ErrVersionMismatch = &Error{"version mismatch", nil}
@@ -16,6 +16,10 @@ var (
// already open.
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
// not been created yet.
ErrBucketNotFound = &Error{"bucket not found", nil}
diff --git a/example_test.go b/example_test.go
index 82db552..0185bb1 100644
--- a/example_test.go
+++ b/example_test.go
@@ -67,7 +67,7 @@ func ExampleDB_Do() {
defer db.Close()
// 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 {
return err
}
@@ -134,30 +134,30 @@ func ExampleDB_ForEach() {
// A liger is awesome.
}
-func ExampleRWTx() {
+func ExampleTx() {
// Open the database.
var db DB
- db.Open("/tmp/bolt/rwtx.db", 0666)
+ db.Open("/tmp/bolt/tx.db", 0666)
defer db.Close()
// Create a bucket.
db.CreateBucket("widgets")
// Create several keys in a transaction.
- rwtxn, _ := db.RWTx()
- b := rwtxn.Bucket("widgets")
+ tx, _ := db.RWTx()
+ b := tx.Bucket("widgets")
b.Put([]byte("john"), []byte("blue"))
b.Put([]byte("abby"), []byte("red"))
b.Put([]byte("zephyr"), []byte("purple"))
- rwtxn.Commit()
+ tx.Commit()
// Iterate over the values in sorted key order.
- txn, _ := db.Tx()
- c := txn.Bucket("widgets").Cursor()
+ tx, _ = db.Tx()
+ c := tx.Bucket("widgets").Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
fmt.Printf("%s likes %s\n", string(k), string(v))
}
- txn.Close()
+ tx.Rollback()
// Output:
// abby likes red
@@ -165,10 +165,10 @@ func ExampleRWTx() {
// zephyr likes purple
}
-func ExampleRWTx_rollback() {
+func ExampleTx_rollback() {
// Open the database.
var db DB
- db.Open("/tmp/bolt/rwtx_rollback.db", 0666)
+ db.Open("/tmp/bolt/tx_rollback.db", 0666)
defer db.Close()
// Create a bucket.
@@ -178,10 +178,10 @@ func ExampleRWTx_rollback() {
db.Put("widgets", []byte("foo"), []byte("bar"))
// Update the key but rollback the transaction so it never saves.
- rwtxn, _ := db.RWTx()
- b := rwtxn.Bucket("widgets")
+ tx, _ := db.RWTx()
+ b := tx.Bucket("widgets")
b.Put([]byte("foo"), []byte("baz"))
- rwtxn.Rollback()
+ tx.Rollback()
// Ensure that our original value is still set.
value, _ := db.Get("widgets", []byte("foo"))
diff --git a/functional_test.go b/functional_test.go
index 20af8fc..a62e6a0 100644
--- a/functional_test.go
+++ b/functional_test.go
@@ -45,7 +45,7 @@ func TestParallelTxs(t *testing.T) {
go func() {
mutex.RLock()
local := current
- txn, err := db.Tx()
+ tx, err := db.Tx()
mutex.RUnlock()
if err == ErrDatabaseNotOpen {
wg.Done()
@@ -56,15 +56,15 @@ func TestParallelTxs(t *testing.T) {
// Verify all data is in for local data list.
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) {
- txn.Close()
+ tx.Rollback()
wg.Done()
t.FailNow()
}
}
- txn.Close()
+ tx.Rollback()
wg.Done()
<-readers
}()
@@ -83,13 +83,13 @@ func TestParallelTxs(t *testing.T) {
pending = pending[currentBatchSize:]
// Start write transaction.
- txn, err := db.RWTx()
+ tx, err := db.RWTx()
if !assert.NoError(t, err) {
t.FailNow()
}
// Insert whole batch.
- b := txn.Bucket("widgets")
+ b := tx.Bucket("widgets")
for _, item := range batchItems {
err := b.Put(item.Key, item.Value)
if !assert.NoError(t, err) {
@@ -99,7 +99,7 @@ func TestParallelTxs(t *testing.T) {
// Commit and update the current list.
mutex.Lock()
- err = txn.Commit()
+ err = tx.Commit()
current = append(current, batchItems...)
mutex.Unlock()
if !assert.NoError(t, err) {
diff --git a/meta.go b/meta.go
index cee2d29..0be4e94 100644
--- a/meta.go
+++ b/meta.go
@@ -36,7 +36,7 @@ func (m *meta) copy(dest *meta) {
// write writes the meta onto a 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.flags |= metaPageFlag
diff --git a/node.go b/node.go
index f0929c7..51be690 100644
--- a/node.go
+++ b/node.go
@@ -8,7 +8,7 @@ import (
// node represents an in-memory, deserialized page.
type node struct {
- tx *RWTx
+ tx *Tx
isLeaf bool
unbalanced bool
key []byte
diff --git a/rwtransaction_test.go b/rwtransaction_test.go
deleted file mode 100644
index c94f534..0000000
--- a/rwtransaction_test.go
+++ /dev/null
@@ -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
- })
- })
-}
diff --git a/transaction.go b/transaction.go
deleted file mode 100644
index 91ab730..0000000
--- a/transaction.go
+++ /dev/null
@@ -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)
- }
- }
-}
diff --git a/transaction_test.go b/transaction_test.go
deleted file mode 100644
index 59f4dd5..0000000
--- a/transaction_test.go
+++ /dev/null
@@ -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
- })
- })
-}
diff --git a/rwtransaction.go b/tx.go
index 0c670d9..53f0456 100644
--- a/rwtransaction.go
+++ b/tx.go
@@ -5,31 +5,99 @@ import (
"unsafe"
)
-// RWTx represents a transaction that can read and write data.
-// Only one read/write transaction can be active for a database at a time.
-// RWTx is composed of a read-only transaction so it can also use
-// functions provided by Tx.
-type RWTx struct {
- Tx
- pending []*node
+// txid represents the internal transaction identifier.
+type txid uint64
+
+// Tx represents a read-only or read/write transaction on the database.
+// Read-only transactions can be used for retrieving values for keys and creating cursors.
+// 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
}
// init initializes the transaction.
-func (t *RWTx) init(db *DB) {
- t.Tx.init(db)
- t.Tx.rwtx = t
- t.pages = make(map[pgid]*page)
- t.nodes = make(map[pgid]*node)
+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))
+
+ if t.writable {
+ t.pages = make(map[pgid]*page)
+ t.nodes = make(map[pgid]*node)
+
+ // Increment the transaction id.
+ 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
+}
- // Increment the transaction id.
- t.meta.txid += txid(1)
+// 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.
// 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 {
- // Check if bucket already exists.
- if b := t.Bucket(name); b != nil {
+func (t *Tx) CreateBucket(name string) error {
+ if !t.writable {
+ return ErrTxNotWritable
+ } else if b := t.Bucket(name); b != nil {
return ErrBucketExists
} else if len(name) == 0 {
return ErrBucketNameRequired
@@ -52,7 +120,7 @@ func (t *RWTx) CreateBucket(name string) error {
// 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.
-func (t *RWTx) CreateBucketIfNotExists(name string) error {
+func (t *Tx) CreateBucketIfNotExists(name string) error {
err := t.CreateBucket(name)
if err != nil && err != ErrBucketExists {
return err
@@ -62,7 +130,11 @@ func (t *RWTx) CreateBucketIfNotExists(name string) error {
// DeleteBucket deletes a bucket.
// 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)
if b == nil {
return ErrBucketNotFound
@@ -81,11 +153,13 @@ func (t *RWTx) DeleteBucket(name string) error {
// Commit writes all changes to disk and updates the meta page.
// Returns an error if a disk write error occurs.
-func (t *RWTx) Commit() error {
+func (t *Tx) Commit() error {
if t.db == nil {
return nil
+ } else if !t.writable {
+ t.Rollback()
+ return nil
}
-
defer t.close()
// 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.
-func (t *RWTx) Rollback() {
+func (t *Tx) Rollback() {
t.close()
}
-func (t *RWTx) close() {
+func (t *Tx) close() {
if t.db != nil {
- t.db.rwlock.Unlock()
+ if t.writable {
+ t.db.rwlock.Unlock()
+ } else {
+ t.db.removeTx(t)
+ }
t.db = nil
}
}
// 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)
if err != nil {
return nil, err
@@ -143,14 +221,14 @@ func (t *RWTx) allocate(count int) (*page, error) {
}
// rebalance attempts to balance all nodes.
-func (t *RWTx) rebalance() {
+func (t *Tx) rebalance() {
for _, n := range t.nodes {
n.rebalance()
}
}
// 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.
// We will update this at the end once all nodes are created.
type root struct {
@@ -233,7 +311,7 @@ func (t *RWTx) spill() error {
}
// write writes any dirty pages to disk.
-func (t *RWTx) write() error {
+func (t *Tx) write() error {
// Sort pages by id.
pages := make(pages, 0, len(t.pages))
for _, p := range t.pages {
@@ -258,7 +336,7 @@ func (t *RWTx) write() error {
}
// 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.
buf := make([]byte, t.db.pageSize)
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.
-func (t *RWTx) node(pgid pgid, parent *node) *node {
- // Retrieve node if it has already been fetched.
- if n := t.Tx.node(pgid); n != nil {
+func (t *Tx) node(pgid pgid, parent *node) *node {
+ // Retrieve node if it's already been created.
+ if t.nodes == nil {
+ return nil
+ } else if n := t.nodes[pgid]; n != nil {
return n
}
@@ -289,7 +369,7 @@ func (t *RWTx) node(pgid pgid, parent *node) *node {
}
// dereference removes all references to the old mmap.
-func (t *RWTx) dereference() {
+func (t *Tx) dereference() {
for _, n := range t.nodes {
n.dereference()
}
@@ -298,3 +378,44 @@ func (t *RWTx) 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)
+ }
+ }
+}
diff --git a/tx_test.go b/tx_test.go
new file mode 100644
index 0000000..1274476
--- /dev/null
+++ b/tx_test.go
@@ -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
+ })
+ })
+}