aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Johnson <benbjohnson@yahoo.com>2014-02-12 14:57:27 -0700
committerBen Johnson <benbjohnson@yahoo.com>2014-02-13 10:58:27 -0700
commit8ad59edd02a8ea6001f15cd6d92944ae83c88f6d (patch)
tree3597cf9a764ba9e99779477a70f7bb71848d1a4c
parentMmap remap. (diff)
downloaddedo-8ad59edd02a8ea6001f15cd6d92944ae83c88f6d.tar.gz
dedo-8ad59edd02a8ea6001f15cd6d92944ae83c88f6d.tar.xz
API Documentation.
-rw-r--r--bucket.go5
-rw-r--r--buckets.go8
-rw-r--r--const.go9
-rw-r--r--cursor.go26
-rw-r--r--db.go47
-rw-r--r--db_test.go2
-rw-r--r--doc.go40
-rw-r--r--error.go46
-rw-r--r--freelist.go4
-rw-r--r--freelist_test.go2
-rw-r--r--meta.go2
-rw-r--r--node.go12
-rw-r--r--node_test.go2
-rw-r--r--page.go20
-rw-r--r--page_test.go10
-rw-r--r--rwtransaction.go43
-rw-r--r--rwtransaction_test.go18
-rw-r--r--transaction.go42
-rw-r--r--transaction_test.go16
19 files changed, 245 insertions, 109 deletions
diff --git a/bucket.go b/bucket.go
index 28c570e..414d260 100644
--- a/bucket.go
+++ b/bucket.go
@@ -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
}
diff --git a/buckets.go b/buckets.go
index 482dea2..bb4b960 100644
--- a/buckets.go
+++ b/buckets.go
@@ -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()
diff --git a/const.go b/const.go
index fce1051..9ad7d25 100644
--- a/const.go
+++ b/const.go
@@ -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
)
diff --git a/cursor.go b/cursor.go
index b42c13c..a8a71b1 100644
--- a/cursor.go
+++ b/cursor.go
@@ -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
}
diff --git a/db.go b/db.go
index 8592d83..88c3065 100644
--- a/db.go
+++ b/db.go
@@ -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
diff --git a/db_test.go b/db_test.go
index db57825..51b76d5 100644
--- a/db_test.go
+++ b/db_test.go
@@ -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)
})
}
diff --git a/doc.go b/doc.go
index ea75951..d7f3ec1 100644
--- a/doc.go
+++ b/doc.go
@@ -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.
+
+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.
-// TODO(benbjohnson)
+
+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
diff --git a/error.go b/error.go
index b5302ad..519309f 100644
--- a/error.go
+++ b/error.go
@@ -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()
diff --git a/freelist.go b/freelist.go
index cd0bffa..3f5cf1c 100644
--- a/freelist.go
+++ b/freelist.go
@@ -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)
}
diff --git a/freelist_test.go b/freelist_test.go
index b760acb..d452a2e 100644
--- a/freelist_test.go
+++ b/freelist_test.go
@@ -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.
diff --git a/meta.go b/meta.go
index d4df9c4..ba62590 100644
--- a/meta.go
+++ b/meta.go
@@ -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())
}
diff --git a/node.go b/node.go
index 1460f52..ec49f11 100644
--- a/node.go
+++ b/node.go
@@ -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))
diff --git a/node_test.go b/node_test.go
index 6334fbe..8555223 100644
--- a/node_test.go
+++ b/node_test.go
@@ -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
diff --git a/page.go b/page.go
index 2b714ee..77a31e4 100644
--- a/page.go
+++ b/page.go
@@ -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)
diff --git a/page_test.go b/page_test.go
index 05b0509..976af0d 100644
--- a/page_test.go
+++ b/page_test.go
@@ -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>")
}
diff --git a/rwtransaction.go b/rwtransaction.go
index b96df51..92d05a8 100644
--- a/rwtransaction.go
+++ b/rwtransaction.go
@@ -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()
}
diff --git a/rwtransaction_test.go b/rwtransaction_test.go
index 88a246f..67855be 100644
--- a/rwtransaction_test.go
+++ b/rwtransaction_test.go
@@ -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()
}
}
diff --git a/transaction.go b/transaction.go
index 1b940d6..713a019 100644
--- a/transaction.go
+++ b/transaction.go
@@ -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.
diff --git a/transaction_test.go b/transaction_test.go
index b59d9b2..afe3b8e 100644
--- a/transaction_test.go
+++ b/transaction_test.go
@@ -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)