diff options
author | Ben Johnson <benbjohnson@yahoo.com> | 2014-02-12 14:57:27 -0700 |
---|---|---|
committer | Ben Johnson <benbjohnson@yahoo.com> | 2014-02-13 10:58:27 -0700 |
commit | 8ad59edd02a8ea6001f15cd6d92944ae83c88f6d (patch) | |
tree | 3597cf9a764ba9e99779477a70f7bb71848d1a4c | |
parent | Mmap remap. (diff) | |
download | dedo-8ad59edd02a8ea6001f15cd6d92944ae83c88f6d.tar.gz dedo-8ad59edd02a8ea6001f15cd6d92944ae83c88f6d.tar.xz |
API Documentation.
-rw-r--r-- | bucket.go | 5 | ||||
-rw-r--r-- | buckets.go | 8 | ||||
-rw-r--r-- | const.go | 9 | ||||
-rw-r--r-- | cursor.go | 26 | ||||
-rw-r--r-- | db.go | 47 | ||||
-rw-r--r-- | db_test.go | 2 | ||||
-rw-r--r-- | doc.go | 40 | ||||
-rw-r--r-- | error.go | 46 | ||||
-rw-r--r-- | freelist.go | 4 | ||||
-rw-r--r-- | freelist_test.go | 2 | ||||
-rw-r--r-- | meta.go | 2 | ||||
-rw-r--r-- | node.go | 12 | ||||
-rw-r--r-- | node_test.go | 2 | ||||
-rw-r--r-- | page.go | 20 | ||||
-rw-r--r-- | page_test.go | 10 | ||||
-rw-r--r-- | rwtransaction.go | 43 | ||||
-rw-r--r-- | rwtransaction_test.go | 18 | ||||
-rw-r--r-- | transaction.go | 42 | ||||
-rw-r--r-- | transaction_test.go | 16 |
19 files changed, 245 insertions, 109 deletions
@@ -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 } @@ -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() @@ -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 ) @@ -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 } @@ -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 @@ -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) }) } @@ -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 @@ -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. @@ -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()) } @@ -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 @@ -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) |