Merge pull request #29 from benbjohnson/cleanup

API Documentation
master
Ben Johnson 2014-02-13 10:59:47 -07:00
commit 34005ecd76
19 changed files with 245 additions and 109 deletions

View File

@ -1,11 +1,16 @@
package bolt
// Bucket represents a collection of key/value pairs inside the database.
// All keys inside the bucket are unique. The Bucket type is not typically used
// directly. Instead the bucket name is typically passed into the Get(), Put(),
// or Delete() functions.
type Bucket struct {
*bucket
name string
transaction *Transaction
}
// bucket represents the on-file representation of a bucket.
type bucket struct {
root pgid
}

View File

@ -13,8 +13,8 @@ type buckets struct {
// size returns the size of the page after serialization.
func (b *buckets) size() int {
var size int = pageHeaderSize
for key, _ := range b.items {
var size = pageHeaderSize
for key := range b.items {
size += int(unsafe.Sizeof(bucket{})) + len(key)
}
return size
@ -70,12 +70,12 @@ func (b *buckets) read(p *page) {
// write writes the items onto a page.
func (b *buckets) write(p *page) {
// Initialize page.
p.flags |= p_buckets
p.flags |= bucketsPageFlag
p.count = uint16(len(b.items))
// Sort keys.
var keys []string
for key, _ := range b.items {
for key := range b.items {
keys = append(keys, key)
}
sort.StringSlice(keys).Sort()

View File

@ -3,7 +3,12 @@ package bolt
const version = 1
const (
// MaxBucketNameSize is the maximum length of a bucket name, in bytes.
MaxBucketNameSize = 255
MaxKeySize = 32768
MaxDataSize = 4294967295
// MaxKeySize is the maximum length of a key, in bytes.
MaxKeySize = 32768
// MaxValueSize is the maximum length of a value, in bytes.
MaxValueSize = 4294967295
)

View File

@ -5,14 +5,17 @@ import (
"sort"
)
// Cursor represents an iterator that can traverse over all key/value pairs in a bucket in sorted order.
// Cursors can be obtained from a Transaction and are valid as long as the Transaction is open.
type Cursor struct {
transaction *Transaction
root pgid
stack []pageElementRef
}
// First moves the cursor to the first item in the bucket and returns its key and data.
func (c *Cursor) First() ([]byte, []byte) {
// First moves the cursor to the first item in the bucket and returns its key and value.
// If the bucket is empty then a nil key is returned.
func (c *Cursor) First() (key []byte, value []byte) {
if len(c.stack) > 0 {
c.stack = c.stack[:0]
}
@ -21,8 +24,9 @@ func (c *Cursor) First() ([]byte, []byte) {
return c.keyValue()
}
// Move the cursor to the next key/value.
func (c *Cursor) Next() ([]byte, []byte) {
// Next moves the cursor to the next item in the bucket and returns its key and value.
// If the cursor is at the end of the bucket then a nil key returned.
func (c *Cursor) Next() (key []byte, value []byte) {
// Attempt to move over one element until we're successful.
// Move up the stack as we hit the end of each page in our stack.
for i := len(c.stack) - 1; i >= 0; i-- {
@ -44,8 +48,9 @@ func (c *Cursor) Next() ([]byte, []byte) {
return c.keyValue()
}
// Get positions the cursor at a specific key and returns the its value.
func (c *Cursor) Get(key []byte) []byte {
// Get moves the cursor to a given key and returns its value.
// If the key does not exist then the cursor is left at the closest key and a nil key is returned.
func (c *Cursor) Get(key []byte) (value []byte) {
// Start from root page and traverse to correct page.
c.stack = c.stack[:0]
c.search(key, c.transaction.page(c.root))
@ -64,12 +69,12 @@ func (c *Cursor) Get(key []byte) []byte {
return c.element().value()
}
// first moves the cursor to the first leaf element under a page.
// first moves the cursor to the first leaf element under the last page in the stack.
func (c *Cursor) first() {
p := c.stack[len(c.stack)-1].page
for {
// Exit when we hit a leaf page.
if (p.flags & p_leaf) != 0 {
if (p.flags & leafPageFlag) != 0 {
break
}
@ -79,13 +84,14 @@ func (c *Cursor) first() {
}
}
// search recursively performs a binary search against a given page until it finds a given key.
func (c *Cursor) search(key []byte, p *page) {
_assert((p.flags&(p_branch|p_leaf)) != 0, "invalid page type: "+p.typ())
_assert((p.flags&(branchPageFlag|leafPageFlag)) != 0, "invalid page type: "+p.typ())
e := pageElementRef{page: p}
c.stack = append(c.stack, e)
// If we're on a leaf page then find the specific node.
if (p.flags & p_leaf) != 0 {
if (p.flags & leafPageFlag) != 0 {
c.nsearch(key, p)
return
}

47
db.go
View File

@ -8,16 +8,15 @@ import (
"unsafe"
)
const (
db_nosync = iota
db_nometasync
)
const minPageSize = 0x1000
// The smallest size that the mmap can be.
const minMmapSize = 1 << 22 // 4MB
// The largest step that can be taken when remapping the mmap.
const maxMmapStep = 1 << 30 // 1GB
// DB represents a collection of buckets persisted to a file on disk.
// All data access is performed through transactions which can be obtained through the DB.
// All the functions on DB will return a DatabaseNotOpenError if accessed before Open() is called.
type DB struct {
os _os
syscall _syscall
@ -66,7 +65,7 @@ func (db *DB) Open(path string, mode os.FileMode) error {
// Exit if the database is currently open.
if db.opened {
return DatabaseAlreadyOpenedError
return DatabaseOpenError
}
// Open data file and separate sync handler for metadata writes.
@ -90,7 +89,7 @@ func (db *DB) Open(path string, mode os.FileMode) error {
}
} else {
// Read the first meta page to determine the page size.
var buf [minPageSize]byte
var buf [0x1000]byte
if _, err := db.file.ReadAt(buf[:], 0); err == nil {
m := db.pageInBuffer(buf[:], 0).meta()
if err := m.validate(); err != nil {
@ -202,7 +201,7 @@ func (db *DB) init() error {
for i := 0; i < 2; i++ {
p := db.pageInBuffer(buf[:], pgid(i))
p.id = pgid(i)
p.flags = p_meta
p.flags = metaPageFlag
// Initialize the meta page.
m := p.meta()
@ -219,13 +218,13 @@ func (db *DB) init() error {
// Write an empty freelist at page 3.
p := db.pageInBuffer(buf[:], pgid(2))
p.id = pgid(2)
p.flags = p_freelist
p.flags = freelistPageFlag
p.count = 0
// Write an empty leaf page at page 4.
p = db.pageInBuffer(buf[:], pgid(3))
p.id = pgid(3)
p.flags = p_buckets
p.flags = bucketsPageFlag
p.count = 0
// Write the buffer to our data file.
@ -236,7 +235,8 @@ func (db *DB) init() error {
return nil
}
// Close releases all resources related to the database.
// Close releases all database resources.
// All transactions must be closed before closing the database.
func (db *DB) Close() {
db.metalock.Lock()
defer db.metalock.Unlock()
@ -250,12 +250,15 @@ func (db *DB) close() {
// TODO(benbjohnson): Undo everything in Open().
db.freelist = nil
db.path = ""
db.munmap()
}
// Transaction creates a read-only transaction.
// Multiple read-only transactions can be used concurrently.
//
// IMPORTANT: You must close the transaction after you are finished or else the database will not reclaim old pages.
func (db *DB) Transaction() (*Transaction, error) {
db.metalock.Lock()
defer db.metalock.Unlock()
@ -282,6 +285,7 @@ func (db *DB) Transaction() (*Transaction, error) {
// RWTransaction 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) RWTransaction() (*RWTransaction, error) {
db.metalock.Lock()
defer db.metalock.Unlock()
@ -332,6 +336,7 @@ func (db *DB) removeTransaction(t *Transaction) {
}
// Bucket retrieves a reference to a bucket.
// This is typically useful for checking the existence of a bucket.
func (db *DB) Bucket(name string) (*Bucket, error) {
t, err := db.Transaction()
if err != nil {
@ -351,7 +356,9 @@ func (db *DB) Buckets() ([]*Bucket, error) {
return t.Buckets(), nil
}
// CreateBucket creates a new bucket in the database.
// CreateBucket creates a new bucket with the given name.
// 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 {
t, err := db.RWTransaction()
if err != nil {
@ -367,6 +374,7 @@ func (db *DB) CreateBucket(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 {
t, err := db.RWTransaction()
if err != nil {
@ -382,16 +390,18 @@ func (db *DB) DeleteBucket(name string) error {
}
// Get retrieves the value for a key in a bucket.
// Returns an error if the key does not exist.
func (db *DB) Get(name string, key []byte) ([]byte, error) {
t, err := db.Transaction()
if err != nil {
return nil, err
}
defer t.Close()
return t.Get(name, key), nil
return t.Get(name, key)
}
// 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 {
t, err := db.RWTransaction()
if err != nil {
@ -405,6 +415,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 {
t, err := db.RWTransaction()
if err != nil {
@ -418,6 +429,8 @@ func (db *DB) Delete(name string, key []byte) error {
}
// Copy writes the entire database to a writer.
// A reader transaction is maintained during the copy so it is safe to continue
// using the database while a copy is in progress.
func (db *DB) Copy(w io.Writer) error {
if !db.opened {
return DatabaseNotOpenError
@ -445,6 +458,8 @@ func (db *DB) Copy(w io.Writer) error {
}
// CopyFile copies the entire database to file at the given path.
// A reader transaction is maintained during the copy so it is safe to continue
// using the database while a copy is in progress.
func (db *DB) CopyFile(path string) error {
f, err := os.Create(path)
if err != nil {
@ -503,7 +518,7 @@ func (db *DB) allocate(count int) (*page, error) {
// sync flushes the file descriptor to disk.
func (db *DB) sync(force bool) error {
if db.opened {
return DatabaseAlreadyOpenedError
return DatabaseNotOpenError
}
if err := syscall.Fsync(int(db.file.Fd())); err != nil {
return err

View File

@ -27,7 +27,7 @@ func TestDBReopen(t *testing.T) {
withDB(func(db *DB, path string) {
db.Open(path, 0666)
err := db.Open(path, 0666)
assert.Equal(t, err, DatabaseAlreadyOpenedError)
assert.Equal(t, err, DatabaseOpenError)
})
}

40
doc.go
View File

@ -1,3 +1,39 @@
package bolt
/*
Package bolt implements a low-level key/value store in pure Go. It supports
fully serializable transactions, ACID semantics, and lock-free MVCC with
multiple readers and a single writer. Bolt can be used for projects that
want a simple data store without the need to add large dependencies such as
Postgres or MySQL.
// TODO(benbjohnson)
Bolt is a single-level, zero-copy, B+tree data store. This means that Bolt is
optimized for fast read access and does not require recovery in the event of a
system crash. Transactions which have not finished committing will simply be
rolled back in the event of a crash.
The design of Bolt is based on Howard Chu's LMDB database project.
Basics
There are only a few types in Bolt: DB, Bucket, Transaction, RWTransaction, 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.
Transactions 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.
RWTransactions provide read-write access to the database. They can create and
delete buckets and they can insert and remove keys. Only one RWTransaction is
allowed at a time.
Caveats
The database uses a read-only, memory-mapped data file to ensure that
applications cannot corrupt the database, however, this means that keys and
values returned from Bolt cannot be changed. Writing to a read-only byte slice
will cause Go to panic. If you need to work with data returned from a Get() you
need to first copy it to a new byte slice.
Bolt currently works on Mac OS and Linux. Windows support is coming soon.
*/
package bolt

View File

@ -1,20 +1,52 @@
package bolt
var (
InvalidError = &Error{"Invalid database", nil}
VersionMismatchError = &Error{"version mismatch", nil}
DatabaseNotOpenError = &Error{"db is not open", nil}
DatabaseAlreadyOpenedError = &Error{"db already open", nil}
TransactionInProgressError = &Error{"writable transaction is already in progress", nil}
InvalidTransactionError = &Error{"txn is invalid", nil}
BucketAlreadyExistsError = &Error{"bucket already exists", nil}
// InvalidError is returned when a data file is not a Bolt-formatted database.
InvalidError = &Error{"Invalid database", nil}
// VersionMismatchError is returned when the data file was created with a
// different version of Bolt.
VersionMismatchError = &Error{"version mismatch", nil}
// DatabaseNotOpenError is returned when a DB instance is accessed before it
// is opened or after it is closed.
DatabaseNotOpenError = &Error{"database not open", nil}
// DatabaseOpenError is returned when opening a database that is
// already open.
DatabaseOpenError = &Error{"database already open", nil}
// BucketNotFoundError is returned when trying to access a bucket that has
// not been created yet.
BucketNotFoundError = &Error{"bucket not found", nil}
// BucketExistsError is returned when creating a bucket that already exists.
BucketExistsError = &Error{"bucket already exists", nil}
// BucketNameRequiredError is returned when creating a bucket with a blank name.
BucketNameRequiredError = &Error{"bucket name required", nil}
// BucketNameTooLargeError is returned when creating a bucket with a name
// that is longer than MaxBucketNameSize.
BucketNameTooLargeError = &Error{"bucket name too large", nil}
// KeyRequiredError is returned when inserting a zero-length key.
KeyRequiredError = &Error{"key required", nil}
// KeyTooLargeError is returned when inserting a key that is larger than MaxKeySize.
KeyTooLargeError = &Error{"key too large", nil}
// ValueTooLargeError is returned when inserting a value that is larger than MaxValueSize.
ValueTooLargeError = &Error{"value too large", nil}
)
// Error represents an error condition caused by Bolt.
type Error struct {
message string
cause error
}
// Error returns a string representation of the error.
func (e *Error) Error() string {
if e.cause != nil {
return e.message + ": " + e.cause.Error()

View File

@ -29,7 +29,7 @@ func (f *freelist) all() []pgid {
// If a contiguous block cannot be found then 0 is returned.
func (f *freelist) allocate(n int) pgid {
var count int
var previd pgid = 0
var previd pgid
for i, id := range f.ids {
// Reset count if this is not contiguous.
if previd == 0 || previd-id != 1 {
@ -82,7 +82,7 @@ func (f *freelist) read(p *page) {
// become free.
func (f *freelist) write(p *page) {
ids := f.all()
p.flags |= p_freelist
p.flags |= freelistPageFlag
p.count = uint16(len(ids))
copy(((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[:], ids)
}

View File

@ -52,7 +52,7 @@ func TestFreelistRead(t *testing.T) {
// Create a page.
var buf [4096]byte
page := (*page)(unsafe.Pointer(&buf[0]))
page.flags = p_freelist
page.flags = freelistPageFlag
page.count = 2
// Insert 2 page ids.

View File

@ -38,7 +38,7 @@ func (m *meta) copy(dest *meta) {
func (m *meta) write(p *page) {
// Page id is either going to be 0 or 1 which we can determine by the Txn ID.
p.id = pgid(m.txnid % 2)
p.flags |= p_meta
p.flags |= metaPageFlag
m.copy(p.meta())
}

12
node.go
View File

@ -28,9 +28,9 @@ func (n *node) minKeys() int {
// size returns the size of the node after serialization.
func (n *node) size() int {
var elementSize int = n.pageElementSize()
var elementSize = n.pageElementSize()
var size int = pageHeaderSize
var size = pageHeaderSize
for _, item := range n.inodes {
size += elementSize + len(item.key) + len(item.value)
}
@ -132,7 +132,7 @@ func (n *node) del(key []byte) {
// read initializes the node from a page.
func (n *node) read(p *page) {
n.pgid = p.id
n.isLeaf = ((p.flags & p_leaf) != 0)
n.isLeaf = ((p.flags & leafPageFlag) != 0)
n.inodes = make(inodes, int(p.count))
for i := 0; i < int(p.count); i++ {
@ -160,9 +160,9 @@ func (n *node) read(p *page) {
func (n *node) write(p *page) {
// Initialize page.
if n.isLeaf {
p.flags |= p_leaf
p.flags |= leafPageFlag
} else {
p.flags |= p_branch
p.flags |= branchPageFlag
}
p.count = uint16(len(n.inodes))
@ -344,7 +344,7 @@ func (n *node) dereference() {
copy(key, n.key)
n.key = key
for i, _ := range n.inodes {
for i := range n.inodes {
inode := &n.inodes[i]
key := make([]byte, len(inode.key))

View File

@ -28,7 +28,7 @@ func TestNodeReadLeafPage(t *testing.T) {
// Create a page.
var buf [4096]byte
page := (*page)(unsafe.Pointer(&buf[0]))
page.flags = p_leaf
page.flags = leafPageFlag
page.count = 2
// Insert 2 elements at the beginning. sizeof(leafPageElement) == 16

20
page.go
View File

@ -16,11 +16,11 @@ const branchPageElementSize = int(unsafe.Sizeof(branchPageElement{}))
const leafPageElementSize = int(unsafe.Sizeof(leafPageElement{}))
const (
p_branch = 0x01
p_leaf = 0x02
p_meta = 0x04
p_buckets = 0x08
p_freelist = 0x10
branchPageFlag = 0x01
leafPageFlag = 0x02
metaPageFlag = 0x04
bucketsPageFlag = 0x08
freelistPageFlag = 0x10
)
type pgid uint64
@ -41,15 +41,15 @@ type pageElementRef struct {
// typ returns a human readable page type string used for debugging.
func (p *page) typ() string {
if (p.flags & p_branch) != 0 {
if (p.flags & branchPageFlag) != 0 {
return "branch"
} else if (p.flags & p_leaf) != 0 {
} else if (p.flags & leafPageFlag) != 0 {
return "leaf"
} else if (p.flags & p_meta) != 0 {
} else if (p.flags & metaPageFlag) != 0 {
return "meta"
} else if (p.flags & p_buckets) != 0 {
} else if (p.flags & bucketsPageFlag) != 0 {
return "buckets"
} else if (p.flags & p_freelist) != 0 {
} else if (p.flags & freelistPageFlag) != 0 {
return "freelist"
}
return fmt.Sprintf("unknown<%02x>", p.flags)

View File

@ -7,11 +7,11 @@ import (
// Ensure that the page type can be returned in human readable format.
func TestPageTyp(t *testing.T) {
assert.Equal(t, (&page{flags: p_branch}).typ(), "branch")
assert.Equal(t, (&page{flags: p_leaf}).typ(), "leaf")
assert.Equal(t, (&page{flags: p_meta}).typ(), "meta")
assert.Equal(t, (&page{flags: p_buckets}).typ(), "buckets")
assert.Equal(t, (&page{flags: p_freelist}).typ(), "freelist")
assert.Equal(t, (&page{flags: branchPageFlag}).typ(), "branch")
assert.Equal(t, (&page{flags: leafPageFlag}).typ(), "leaf")
assert.Equal(t, (&page{flags: metaPageFlag}).typ(), "meta")
assert.Equal(t, (&page{flags: bucketsPageFlag}).typ(), "buckets")
assert.Equal(t, (&page{flags: freelistPageFlag}).typ(), "freelist")
assert.Equal(t, (&page{flags: 20000}).typ(), "unknown<4e20>")
}

View File

@ -6,7 +6,9 @@ import (
)
// RWTransaction represents a transaction that can read and write data.
// Only one read/write transaction can be active for a DB at a time.
// Only one read/write transaction can be active for a database at a time.
// RWTransaction is composed of a read-only Transaction so it can also use
// functions provided by Transaction.
type RWTransaction struct {
Transaction
nodes map[pgid]*node
@ -25,14 +27,15 @@ func (t *RWTransaction) init(db *DB) {
}
// 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 *RWTransaction) CreateBucket(name string) error {
// Check if bucket already exists.
if b := t.Bucket(name); b != nil {
return &Error{"bucket already exists", nil}
return BucketExistsError
} else if len(name) == 0 {
return &Error{"bucket name cannot be blank", nil}
return BucketNameRequiredError
} else if len(name) > MaxBucketNameSize {
return &Error{"bucket name too long", nil}
return BucketNameTooLargeError
}
// Create a blank root leaf page.
@ -40,7 +43,7 @@ func (t *RWTransaction) CreateBucket(name string) error {
if err != nil {
return err
}
p.flags = p_leaf
p.flags = leafPageFlag
// Add bucket to buckets page.
t.buckets.put(name, &bucket{root: p.id})
@ -48,28 +51,37 @@ func (t *RWTransaction) CreateBucket(name string) error {
return nil
}
// DropBucket deletes a bucket.
// DeleteBucket deletes a bucket.
// Returns an error if the bucket cannot be found.
func (t *RWTransaction) DeleteBucket(name string) error {
if b := t.Bucket(name); b == nil {
return BucketNotFoundError
}
// Remove from buckets page.
t.buckets.del(name)
// TODO(benbjohnson): Free all pages.
return nil
}
// Put sets the value for a key inside of the named bucket.
// If the key exist then its previous value will be overwritten.
// Returns an error if the bucket is not found, if the key is blank, if the key is too large, or if the value is too large.
func (t *RWTransaction) Put(name string, key []byte, value []byte) error {
b := t.Bucket(name)
if b == nil {
return &Error{"bucket not found", nil}
return BucketNotFoundError
}
// Validate the key and data size.
if len(key) == 0 {
return &Error{"key required", nil}
return KeyRequiredError
} else if len(key) > MaxKeySize {
return &Error{"key too large", nil}
} else if len(value) > MaxDataSize {
return &Error{"data too large", nil}
return KeyTooLargeError
} else if len(value) > MaxValueSize {
return ValueTooLargeError
}
// Move cursor to correct position.
@ -82,10 +94,13 @@ func (t *RWTransaction) Put(name string, key []byte, value []byte) error {
return nil
}
// Delete removes a key from the named bucket.
// If the key does not exist then nothing is done and a nil error is returned.
// Returns an error if the bucket cannot be found.
func (t *RWTransaction) Delete(name string, key []byte) error {
b := t.Bucket(name)
if b == nil {
return &Error{"bucket not found", nil}
return BucketNotFoundError
}
// Move cursor to correct position.
@ -98,7 +113,8 @@ func (t *RWTransaction) Delete(name string, key []byte) error {
return nil
}
// Commit writes all changes to disk.
// Commit writes all changes to disk and updates the meta page.
// Returns an error if a disk write error occurs.
func (t *RWTransaction) Commit() error {
defer t.close()
@ -131,6 +147,7 @@ func (t *RWTransaction) Commit() error {
return nil
}
// Rollback closes the transaction and ignores all previous updates.
func (t *RWTransaction) Rollback() {
t.close()
}

View File

@ -44,7 +44,7 @@ func TestRWTransactionRecreateBucket(t *testing.T) {
// Create the same bucket again.
err = db.CreateBucket("widgets")
assert.Equal(t, err, &Error{"bucket already exists", nil})
assert.Equal(t, err, BucketExistsError)
})
}
@ -52,7 +52,7 @@ func TestRWTransactionRecreateBucket(t *testing.T) {
func TestRWTransactionCreateBucketWithoutName(t *testing.T) {
withOpenDB(func(db *DB, path string) {
err := db.CreateBucket("")
assert.Equal(t, err, &Error{"bucket name cannot be blank", nil})
assert.Equal(t, err, BucketNameRequiredError)
})
}
@ -63,7 +63,7 @@ func TestRWTransactionCreateBucketWithLongName(t *testing.T) {
assert.NoError(t, err)
err = db.CreateBucket(strings.Repeat("X", 256))
assert.Equal(t, err, &Error{"bucket name too long", nil})
assert.Equal(t, err, BucketNameTooLargeError)
})
}
@ -152,7 +152,9 @@ func TestRWTransactionPutMultiple(t *testing.T) {
// Verify all items exist.
txn, _ := db.Transaction()
for _, item := range items {
if !assert.Equal(t, item.Value, txn.Get("widgets", item.Key)) {
value, err := txn.Get("widgets", item.Key)
assert.NoError(t, err)
if !assert.Equal(t, item.Value, value) {
db.CopyFile("/tmp/bolt.put.multiple.db")
t.FailNow()
}
@ -188,11 +190,15 @@ func TestRWTransactionDelete(t *testing.T) {
txn, _ := db.Transaction()
for j, exp := range items {
if j > i {
if !assert.Equal(t, exp.Value, txn.Get("widgets", exp.Key)) {
value, err := txn.Get("widgets", exp.Key)
assert.NoError(t, err)
if !assert.Equal(t, exp.Value, value) {
t.FailNow()
}
} else {
if !assert.Nil(t, txn.Get("widgets", exp.Key)) {
value, err := txn.Get("widgets", exp.Key)
assert.NoError(t, err)
if !assert.Nil(t, value) {
t.FailNow()
}
}

View File

@ -1,14 +1,12 @@
package bolt
const (
ps_modify = 1
ps_rootonly = 2
ps_first = 4
ps_last = 8
)
type txnid uint64
// Transaction 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 Transaction struct {
db *DB
meta *meta
@ -16,6 +14,9 @@ type Transaction struct {
pages map[pgid]*page
}
// txnid represents the internal transaction identifier.
type txnid uint64
// init initializes the transaction and associates it with a database.
func (t *Transaction) init(db *DB) {
t.db = db
@ -31,15 +32,18 @@ func (t *Transaction) id() txnid {
return t.meta.txnid
}
// Close closes the transaction and releases any pages it is using.
func (t *Transaction) Close() {
t.db.removeTransaction(t)
}
// DB returns a reference to the database that created the transaction.
func (t *Transaction) DB() *DB {
return t.db
}
// Bucket retrieves a bucket by name.
// Returns nil if the bucket does not exist.
func (t *Transaction) Bucket(name string) *Bucket {
b := t.buckets.get(name)
if b == nil {
@ -60,21 +64,25 @@ func (t *Transaction) Buckets() []*Bucket {
}
// Cursor creates a cursor associated with a given bucket.
func (t *Transaction) Cursor(name string) *Cursor {
// The cursor is only valid as long as the Transaction is open.
// Do not use a cursor after the transaction is closed.
func (t *Transaction) Cursor(name string) (*Cursor, error) {
b := t.Bucket(name)
if b == nil {
return nil
return nil, BucketNotFoundError
}
return b.cursor()
return b.cursor(), nil
}
// Get retrieves the value for a key in a named bucket.
func (t *Transaction) Get(name string, key []byte) []byte {
c := t.Cursor(name)
if c == nil {
return nil
// Returns a nil value if the key does not exist.
// Returns an error if the bucket does not exist.
func (t *Transaction) Get(name string, key []byte) (value []byte, err error) {
c, err := t.Cursor(name)
if err != nil {
return nil, err
}
return c.Get(key)
return c.Get(key), nil
}
// page returns a reference to the page with a given id.

View File

@ -43,7 +43,8 @@ func TestTransactionCursorEmptyBucket(t *testing.T) {
withOpenDB(func(db *DB, path string) {
db.CreateBucket("widgets")
txn, _ := db.Transaction()
c := txn.Cursor("widgets")
c, err := txn.Cursor("widgets")
assert.NoError(t, err)
k, v := c.First()
assert.Nil(t, k)
assert.Nil(t, v)
@ -56,7 +57,9 @@ func TestTransactionCursorMissingBucket(t *testing.T) {
withOpenDB(func(db *DB, path string) {
db.CreateBucket("widgets")
txn, _ := db.Transaction()
assert.Nil(t, txn.Cursor("woojits"))
c, err := txn.Cursor("woojits")
assert.Nil(t, c)
assert.Equal(t, err, BucketNotFoundError)
txn.Close()
})
}
@ -69,7 +72,8 @@ func TestTransactionCursorLeafRoot(t *testing.T) {
db.Put("widgets", []byte("foo"), []byte{0})
db.Put("widgets", []byte("bar"), []byte{1})
txn, _ := db.Transaction()
c := txn.Cursor("widgets")
c, err := txn.Cursor("widgets")
assert.NoError(t, err)
k, v := c.First()
assert.Equal(t, string(k), "bar")
@ -103,7 +107,8 @@ func TestTransactionCursorRestart(t *testing.T) {
db.Put("widgets", []byte("foo"), []byte{})
txn, _ := db.Transaction()
c := txn.Cursor("widgets")
c, err := txn.Cursor("widgets")
assert.NoError(t, err)
k, _ := c.First()
assert.Equal(t, string(k), "bar")
@ -139,7 +144,8 @@ func TestTransactionCursorIterate(t *testing.T) {
// Iterate over all items and check consistency.
var index = 0
txn, _ := db.Transaction()
c := txn.Cursor("widgets")
c, err := txn.Cursor("widgets")
assert.NoError(t, err)
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)