mirror of https://github.com/hak5/bolt.git
commit
34005ecd76
|
@ -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()
|
||||
|
|
9
const.go
9
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
|
||||
)
|
||||
|
|
26
cursor.go
26
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
|
||||
}
|
||||
|
|
47
db.go
47
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
|
||||
|
|
|
@ -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
40
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.
|
||||
|
||||
// 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
|
||||
|
|
46
error.go
46
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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
2
meta.go
2
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())
|
||||
}
|
||||
|
|
12
node.go
12
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))
|
||||
|
|
|
@ -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
20
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)
|
||||
|
|
10
page_test.go
10
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>")
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue