Implement new namespaced db package named walletdb.

This commit implements a new namespaced db package which  is intended to
be used be wallet and any sub-packages as its data storage mechanism.

- Key/value store
- Namespace support
- Allows multiple packages to have their own area in the database without
  worrying about conflicts
- Read-only and read-write transactions with both manual and managed modes
- Nested buckets
- Supports registration of backend databases
- Comprehensive test coverage
This commit is contained in:
Dave Collins 2014-11-09 18:31:38 -06:00
parent 9225863706
commit e8b4de9379
6 changed files with 929 additions and 0 deletions

372
walletdb/bdb/db.go Normal file
View file

@ -0,0 +1,372 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package bdb
import (
"io"
"os"
"github.com/conformal/bolt"
"github.com/conformal/btcwallet/walletdb"
)
// convertErr converts some bolt errors to the equivalent walletdb error.
func convertErr(err error) error {
switch err {
// Database open/create errors.
case bolt.ErrDatabaseNotOpen:
return walletdb.ErrDbNotOpen
case bolt.ErrInvalid:
return walletdb.ErrInvalid
// Transaction errors.
case bolt.ErrTxNotWritable:
return walletdb.ErrTxNotWritable
case bolt.ErrTxClosed:
return walletdb.ErrTxClosed
// Value/bucket errors.
case bolt.ErrBucketNotFound:
return walletdb.ErrBucketNotFound
case bolt.ErrBucketExists:
return walletdb.ErrBucketExists
case bolt.ErrBucketNameRequired:
return walletdb.ErrBucketNameRequired
case bolt.ErrKeyRequired:
return walletdb.ErrKeyRequired
case bolt.ErrKeyTooLarge:
return walletdb.ErrKeyTooLarge
case bolt.ErrValueTooLarge:
return walletdb.ErrValueTooLarge
case bolt.ErrIncompatibleValue:
return walletdb.ErrIncompatibleValue
}
// Return the original error if none of the above applies.
return err
}
// bucket is an internal type used to represent a collection of key/value pairs
// and implements the walletdb.Bucket interface.
type bucket bolt.Bucket
// Enforce bucket implements the walletdb.Bucket interface.
var _ walletdb.Bucket = (*bucket)(nil)
// Bucket retrieves a nested bucket with the given key. Returns nil if
// the bucket does not exist.
//
// This function is part of the walletdb.Bucket interface implementation.
func (b *bucket) Bucket(key []byte) walletdb.Bucket {
// This nil check is intentional so the return value can be checked
// against nil directly.
boltBucket := (*bolt.Bucket)(b).Bucket(key)
if boltBucket == nil {
return nil
}
return (*bucket)(boltBucket)
}
// CreateBucket creates and returns a new nested bucket with the given key.
// Returns ErrBucketExists if the bucket already exists, ErrBucketNameRequired
// if the key is empty, or ErrIncompatibleValue if the key value is otherwise
// invalid.
//
// This function is part of the walletdb.Bucket interface implementation.
func (b *bucket) CreateBucket(key []byte) (walletdb.Bucket, error) {
boltBucket, err := (*bolt.Bucket)(b).CreateBucket(key)
if err != nil {
return nil, convertErr(err)
}
return (*bucket)(boltBucket), nil
}
// CreateBucketIfNotExists creates and returns a new nested bucket with the
// given key if it does not already exist. Returns ErrBucketNameRequired if the
// key is empty or ErrIncompatibleValue if the key value is otherwise invalid.
//
// This function is part of the walletdb.Bucket interface implementation.
func (b *bucket) CreateBucketIfNotExists(key []byte) (walletdb.Bucket, error) {
boltBucket, err := (*bolt.Bucket)(b).CreateBucketIfNotExists(key)
if err != nil {
return nil, convertErr(err)
}
return (*bucket)(boltBucket), nil
}
// DeleteBucket removes a nested bucket with the given key. Returns
// ErrTxNotWritable if attempted against a read-only transaction and
// ErrBucketNotFound if the specified bucket does not exist.
//
// This function is part of the walletdb.Bucket interface implementation.
func (b *bucket) DeleteBucket(key []byte) error {
return convertErr((*bolt.Bucket)(b).DeleteBucket(key))
}
// ForEach invokes the passed function with every key/value pair in the bucket.
// This includes nested buckets, in which case the value is nil, but it does not
// include the key/value pairs within those nested buckets.
//
// NOTE: The values returned by this function are only valid during a
// transaction. Attempting to access them after a transaction has ended will
// likely result in an access violation.
//
// This function is part of the walletdb.Bucket interface implementation.
func (b *bucket) ForEach(fn func(k, v []byte) error) error {
return convertErr((*bolt.Bucket)(b).ForEach(fn))
}
// Writable returns whether or not the bucket is writable.
//
// This function is part of the walletdb.Bucket interface implementation.
func (b *bucket) Writable() bool {
return (*bolt.Bucket)(b).Writable()
}
// Put saves the specified key/value pair to the bucket. Keys that do not
// already exist are added and keys that already exist are overwritten. Returns
// ErrTxNotWritable if attempted against a read-only transaction.
//
// This function is part of the walletdb.Bucket interface implementation.
func (b *bucket) Put(key, value []byte) error {
return convertErr((*bolt.Bucket)(b).Put(key, value))
}
// Get returns the value for the given key. Returns nil if the key does
// not exist in this bucket (or nested buckets).
//
// NOTE: The value returned by this function is only valid during a
// transaction. Attempting to access it after a transaction has ended
// will likely result in an access violation.
//
// This function is part of the walletdb.Bucket interface implementation.
func (b *bucket) Get(key []byte) []byte {
return (*bolt.Bucket)(b).Get(key)
}
// Delete removes the specified key from the bucket. Deleting a key that does
// not exist does not return an error. Returns ErrTxNotWritable if attempted
// against a read-only transaction.
//
// This function is part of the walletdb.Bucket interface implementation.
func (b *bucket) Delete(key []byte) error {
return convertErr((*bolt.Bucket)(b).Delete(key))
}
// transaction represents a database transaction. It can either by read-only or
// read-write and implements the walletdb.Bucket interface. The transaction
// provides a root bucket against which all read and writes occur.
type transaction struct {
boltTx *bolt.Tx
rootBucket *bolt.Bucket
}
// Enforce transaction implements the walletdb.Tx interface.
var _ walletdb.Tx = (*transaction)(nil)
// RootBucket returns the top-most bucket for the namespace the transaction was
// created from.
//
// This function is part of the walletdb.Tx interface implementation.
func (tx *transaction) RootBucket() walletdb.Bucket {
return (*bucket)(tx.rootBucket)
}
// Commit commits all changes that have been made through the root bucket and
// all of its sub-buckets to persistent storage.
//
// This function is part of the walletdb.Tx interface implementation.
func (tx *transaction) Commit() error {
return convertErr(tx.boltTx.Commit())
}
// Rollback undoes all changes that have been made to the root bucket and all of
// its sub-buckets.
//
// This function is part of the walletdb.Tx interface implementation.
func (tx *transaction) Rollback() error {
return convertErr(tx.boltTx.Rollback())
}
// namespace represents a database namespace that is inteded to support the
// concept of a single entity that controls the opening, creating, and closing
// of a database while providing other entities their own namespace to work in.
// It implements the walletdb.Namespace interface.
type namespace struct {
db *bolt.DB
key []byte
}
// Enforce namespace implements the walletdb.Namespace interface.
var _ walletdb.Namespace = (*namespace)(nil)
// Begin starts a transaction which is either read-only or read-write depending
// on the specified flag. Multiple read-only transactions can be started
// simultaneously while only a single read-write transaction can be started at a
// time. The call will block when starting a read-write transaction when one is
// already open.
//
// NOTE: The transaction must be closed by calling Rollback or Commit on it when
// it is no longer needed. Failure to do so will result in unclaimed memory.
//
// This function is part of the walletdb.Namespace interface implementation.
func (ns *namespace) Begin(writable bool) (walletdb.Tx, error) {
boltTx, err := ns.db.Begin(writable)
if err != nil {
return nil, convertErr(err)
}
bucket := boltTx.Bucket(ns.key)
if bucket == nil {
return nil, walletdb.ErrBucketNotFound
}
return &transaction{boltTx: boltTx, rootBucket: bucket}, nil
}
// View invokes the passed function in the context of a managed read-only
// transaction. Any errors returned from the user-supplied function are
// returned from this function.
//
// Calling Rollback on the transaction passed to the user-supplied function will
// result in a panic.
//
// This function is part of the walletdb.Namespace interface implementation.
func (ns *namespace) View(fn func(walletdb.Tx) error) error {
return convertErr(ns.db.View(func(boltTx *bolt.Tx) error {
bucket := boltTx.Bucket(ns.key)
if bucket == nil {
return walletdb.ErrBucketNotFound
}
return fn(&transaction{boltTx: boltTx, rootBucket: bucket})
}))
}
// Update invokes the passed function in the context of a managed read-write
// transaction. Any errors returned from the user-supplied function will cause
// the transaction to be rolled back and are returned from this function.
// Otherwise, the transaction is commited when the user-supplied function
// returns a nil error.
//
// Calling Rollback on the transaction passed to the user-supplied function will
// result in a panic.
//
// This function is part of the walletdb.Namespace interface implementation.
func (ns *namespace) Update(fn func(walletdb.Tx) error) error {
return convertErr(ns.db.Update(func(boltTx *bolt.Tx) error {
bucket := boltTx.Bucket(ns.key)
if bucket == nil {
return walletdb.ErrBucketNotFound
}
return fn(&transaction{boltTx: boltTx, rootBucket: bucket})
}))
}
// db represents a collection of namespaces which are persisted and implements
// the walletdb.Db interface. All database access is performed through
// transactions which are obtained through the specific Namespace.
type db bolt.DB
// Enforce db implements the walletdb.Db interface.
var _ walletdb.DB = (*db)(nil)
// Namespace returns a Namespace interface for the provided key. See the
// Namespace interface documentation for more details. Attempting to access a
// Namespace on a database that is not open yet or has been closed will result
// in ErrDbNotOpen. Namespaces are created in the database on first access.
//
// This function is part of the walletdb.Db interface implementation.
func (db *db) Namespace(key []byte) (walletdb.Namespace, error) {
// Check if the namespace needs to be created using a read-only
// transaction. This is done because read-only transactions are faster
// and don't block like write transactions.
var doCreate bool
err := (*bolt.DB)(db).View(func(tx *bolt.Tx) error {
boltBucket := tx.Bucket(key)
if boltBucket == nil {
doCreate = true
}
return nil
})
if err != nil {
return nil, convertErr(err)
}
// Create the namespace if needed by using an writable update
// transaction.
if doCreate {
err := (*bolt.DB)(db).Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucket(key)
return err
})
if err != nil {
return nil, convertErr(err)
}
}
return &namespace{db: (*bolt.DB)(db), key: key}, nil
}
// DeleteNamespace deletes the namespace for the passed key. ErrBucketNotFound
// will be returned if the namespace does not exist.
//
// This function is part of the walletdb.Db interface implementation.
func (db *db) DeleteNamespace(key []byte) error {
return convertErr((*bolt.DB)(db).Update(func(tx *bolt.Tx) error {
return tx.DeleteBucket(key)
}))
}
// Copy writes a copy of the database to the provided writer. This call will
// start a read-only transaction to perform all operations.
//
// This function is part of the walletdb.Db interface implementation.
func (db *db) Copy(w io.Writer) error {
return convertErr((*bolt.DB)(db).View(func(tx *bolt.Tx) error {
return tx.Copy(w)
}))
}
// Close cleanly shuts down the database and syncs all data.
//
// This function is part of the walletdb.Db interface implementation.
func (db *db) Close() error {
return convertErr((*bolt.DB)(db).Close())
}
// filesExists reports whether the named file or directory exists.
func fileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// openDB opens the database at the provided path. walletdb.ErrDbDoesNotExist
// is returned if the database doesn't exist and the create flag is not set.
func openDB(dbPath string, create bool) (walletdb.DB, error) {
if !create && !fileExists(dbPath) {
return nil, walletdb.ErrDbDoesNotExist
}
boltDB, err := bolt.Open(dbPath, 0600, nil)
return (*db)(boltDB), convertErr(err)
}

37
walletdb/bdb/doc.go Normal file
View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
Package bdb implements an instance of walletdb that uses boltdb for the backing
datastore.
Usage
This package is only a driver to the walletdb package and provides the database
type of "bdb". The only parameter the Open and Create functions take is the
database path as a string:
db, err := walletdb.Open("bdb", "path/to/database.db")
if err != nil {
// Handle error
}
db, err := walletdb.Create("bdb", "path/to/database.db")
if err != nil {
// Handle error
}
*/
package bdb

78
walletdb/bdb/driver.go Normal file
View file

@ -0,0 +1,78 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package bdb
import (
"fmt"
"github.com/conformal/btcwallet/walletdb"
)
const (
dbType = "bdb"
)
// parseArgs parses the arguments from the walletdb Open/Create methods.
func parseArgs(funcName string, args ...interface{}) (string, error) {
if len(args) != 1 {
return "", fmt.Errorf("invalid arguments to %s.%s -- "+
"expected database path", dbType, funcName)
}
dbPath, ok := args[0].(string)
if !ok {
return "", fmt.Errorf("first argument to %s.%s is invalid -- "+
"expected database path string", dbType, funcName)
}
return dbPath, nil
}
// openDBDriver is the callback provided during driver registration that opens
// an existing database for use.
func openDBDriver(args ...interface{}) (walletdb.DB, error) {
dbPath, err := parseArgs("Open", args...)
if err != nil {
return nil, err
}
return openDB(dbPath, false)
}
// createDBDriver is the callback provided during driver registration that
// creates, initializes, and opens a database for use.
func createDBDriver(args ...interface{}) (walletdb.DB, error) {
dbPath, err := parseArgs("Create", args...)
if err != nil {
return nil, err
}
return openDB(dbPath, true)
}
func init() {
// Register the driver.
driver := walletdb.Driver{
DbType: dbType,
Create: createDBDriver,
Open: openDBDriver,
}
if err := walletdb.RegisterDriver(driver); err != nil {
panic(fmt.Sprintf("Failed to regiser database driver '%s': %v",
dbType, err))
}
}

115
walletdb/doc.go Normal file
View file

@ -0,0 +1,115 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
Package walletdb provides a namespaced database interface for btcwallet.
Overview
A wallet essentially consists of a multitude of stored data such as private
and public keys, key derivation bits, pay-to-script-hash scripts, and various
metadata. One of the issues with many wallets is they are tightly integrated.
Designing a wallet with loosely coupled components that provide specific
functionality is ideal, however it presents a challenge in regards to data
storage since each component needs to store its own data without knowing the
internals of other components or breaking atomicity.
This package solves this issue by providing a pluggable driver, namespaced
database interface that is intended to be used by the main wallet daemon. This
allows the potential for any backend database type with a suitable driver. Each
component, which will typically be a package, can then implement various
functionality such as address management, voting pools, and colored coin
metadata in their own namespace without having to worry about conflicts with
other packages even though they are sharing the same database that is managed by
the wallet.
A quick overview of the features walletdb provides are as follows:
- Key/value store
- Namespace support
- Allows multiple packages to have their own area in the database without
worrying about conflicts
- Read-only and read-write transactions with both manual and managed modes
- Nested buckets
- Supports registration of backend databases
- Comprehensive test coverage
Database
The main entry point is the DB interface. It exposes functionality for
creating, retrieving, and removing namespaces. It is obtained via the Create
and Open functions which take a database type string that identifies the
specific database driver (backend) to use as well as arguments specific to the
specified driver.
Namespaces
The Namespace interface is an abstraction that provides facilities for obtaining
transactions (the Tx interface) that are the basis of all database reads and
writes. Unlike some database interfaces that support reading and writing
without transactions, this interface requires transactions even when only
reading or writing a single key.
The Begin function provides an unmanaged transaction while the View and Update
functions provide a managed transaction. These are described in more detail
below.
Transactions
The Tx interface provides facilities for rolling back or commiting changes that
took place while the transaction was active. It also provides the root bucket
under which all keys, values, and nested buckets are stored. A transaction
can either be read-only or read-write and managed or unmanaged.
Managed versus Unmanaged Transactions
A managed transaction is one where the caller provides a function to execute
within the context of the transaction and the commit or rollback is handled
automatically depending on whether or not the provided function returns an
error. Attempting to manually call Rollback or Commit on the managed
transaction will result in a panic.
An unmanaged transaction, on the other hand, requires the caller to manually
call Commit or Rollback when they are finished with it. Leaving transactions
open for long periods of time can have several adverse effects, so it is
recommended that managed transactions are used instead.
Buckets
The Bucket interface provides the ability to manipulate key/value pairs and
nested buckets as well as iterate through them.
The Get, Put, and Delete functions work with key/value pairs, while the Bucket,
CreateBucket, CreateBucketIfNotExists, and DeleteBucket functions work with
buckets. The ForEach function allows the caller to provide a function to be
called with each key/value pair and nested bucket in the current bucket.
Root Bucket
As discussed above, all of the functions which are used to manipulate key/value
pairs and nested buckets exist on the Bucket interface. The root bucket is the
upper-most bucket in a namespace under which data is stored and is created at
the same time as the namespace. Use the RootBucket function on the Tx interface
to retrieve it.
Nested Buckets
The CreateBucket and CreateBucketIfNotExists functions on the Bucket interface
provide the ability to create an arbitrary number of nested buckets. It is
a good idea to avoid a lot of buckets with little data in them as it could lead
to poor page utilization depending on the specific driver in use.
*/
package walletdb

92
walletdb/error.go Normal file
View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package walletdb
import (
"errors"
)
// Errors that can occur during driver registration.
var (
// ErrDbTypeRegistered is returned when two different database drivers
// attempt to register with the name database type.
ErrDbTypeRegistered = errors.New("database type already registered")
)
// Errors that the various database functions may return.
var (
// ErrDbUnknownType is returned when there is no driver registered for
// the specified database type.
ErrDbUnknownType = errors.New("unknown database type")
// ErrDbDoesNotExist is returned when open is called for a database that
// does not exist.
ErrDbDoesNotExist = errors.New("database does not exist")
// ErrDbExists is returned when create is called for a database that
// already exists.
ErrDbExists = errors.New("database already exists")
// ErrDbNotOpen is returned when a database instance is accessed before
// it is opened or after it is closed.
ErrDbNotOpen = errors.New("database not open")
// ErrDbAlreadyOpen is returned when open is called on a database that
// is already open.
ErrDbAlreadyOpen = errors.New("database already open")
// ErrInvalid is returned if the specified database is not valid.
ErrInvalid = errors.New("invalid database")
)
// Errors that can occur when beginning or committing a transaction.
var (
// ErrTxClosed is returned when attempting to commit or rollback a
// transaction that has already had one of those operations performed.
ErrTxClosed = errors.New("tx closed")
// ErrTxNotWritable is returned when an operation that requires write
// access to the database is attempted against a read-only transaction.
ErrTxNotWritable = errors.New("tx not writable")
)
// Errors that can occur when putting or deleting a value or bucket.
var (
// ErrBucketNotFound is returned when trying to access a bucket that has
// not been created yet.
ErrBucketNotFound = errors.New("bucket not found")
// ErrBucketExists is returned when creating a bucket that already exists.
ErrBucketExists = errors.New("bucket already exists")
// ErrBucketNameRequired is returned when creating a bucket with a blank name.
ErrBucketNameRequired = errors.New("bucket name required")
// ErrKeyRequired is returned when inserting a zero-length key.
ErrKeyRequired = errors.New("key required")
// ErrKeyTooLarge is returned when inserting a key that is larger than MaxKeySize.
ErrKeyTooLarge = errors.New("key too large")
// ErrValueTooLarge is returned when inserting a value that is larger than MaxValueSize.
ErrValueTooLarge = errors.New("value too large")
// ErrIncompatibleValue is returned when trying create or delete a
// bucket on an existing non-bucket key or when trying to create or
// delete a non-bucket key on an existing bucket key.
ErrIncompatibleValue = errors.New("incompatible value")
)

235
walletdb/interface.go Normal file
View file

@ -0,0 +1,235 @@
/*
* Copyright (c) 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
// This interface was inspired heavily by the excellent boltdb project at
// https://github.com/boltdb/bolt by Ben B. Johnson.
package walletdb
import "io"
// Bucket represents a collection of key/value pairs.
type Bucket interface {
// Bucket retrieves a nested bucket with the given key. Returns nil if
// the bucket does not exist.
Bucket(key []byte) Bucket
// CreateBucket creates and returns a new nested bucket with the given
// key. Returns ErrBucketExists if the bucket already exists,
// ErrBucketNameRequired if the key is empty, or ErrIncompatibleValue
// if the key value is otherwise invalid for the particular database
// implementation. Other errors are possible depending on the
// implementation.
CreateBucket(key []byte) (Bucket, error)
// CreateBucketIfNotExists creates and returns a new nested bucket with
// the given key if it does not already exist. Returns
// ErrBucketNameRequired if the key is empty or ErrIncompatibleValue
// if the key value is otherwise invalid for the particular database
// backend. Other errors are possible depending on the implementation.
CreateBucketIfNotExists(key []byte) (Bucket, error)
// DeleteBucket removes a nested bucket with the given key. Returns
// ErrTxNotWritable if attempted against a read-only transaction and
// ErrBucketNotFound if the specified bucket does not exist.
DeleteBucket(key []byte) error
// ForEach invokes the passed function with every key/value pair in
// the bucket. This includes nested buckets, in which case the value
// is nil, but it does not include the key/value pairs within those
// nested buckets.
//
// NOTE: The values returned by this function are only valid during a
// transaction. Attempting to access them after a transaction has ended
// results in undefined behavior. This constraint prevents additional
// data copies and allows support for memory-mapped database
// implementations.
ForEach(func(k, v []byte) error) error
// Writable returns whether or not the bucket is writable.
Writable() bool
// Put saves the specified key/value pair to the bucket. Keys that do
// not already exist are added and keys that already exist are
// overwritten. Returns ErrTxNotWritable if attempted against a
// read-only transaction.
Put(key, value []byte) error
// Get returns the value for the given key. Returns nil if the key does
// not exist in this bucket (or nested buckets).
//
// NOTE: The value returned by this function is only valid during a
// transaction. Attempting to access it after a transaction has ended
// results in undefined behavior. This constraint prevents additional
// data copies and allows support for memory-mapped database
// implementations.
Get(key []byte) []byte
// Delete removes the specified key from the bucket. Deleting a key
// that does not exist does not return an error. Returns
// ErrTxNotWritable if attempted against a read-only transaction.
Delete(key []byte) error
}
// Tx represents a database transaction. It can either by read-only or
// read-write. The transaction provides a root bucket against which all read
// and writes occur.
//
// As would be expected with a transaction, no changes will be saved to the
// database until it has been committed. The transaction will only provide a
// view of the database at the time it was created. Transactions should not be
// long running operations.
type Tx interface {
// RootBucket returns the top-most bucket for the namespace the
// transaction was created from.
RootBucket() Bucket
// Commit commits all changes that have been made through the root
// bucket and all of its sub-buckets to persistent storage.
Commit() error
// Rollback undoes all changes that have been made to the root bucket
// and all of its sub-buckets.
Rollback() error
}
// Namespace represents a database namespace that is inteded to support the
// concept of a single entity that controls the opening, creating, and closing
// of a database while providing other entities their own namespace to work in.
type Namespace interface {
// Begin starts a transaction which is either read-only or read-write
// depending on the specified flag. Multiple read-only transactions
// can be started simultaneously while only a single read-write
// transaction can be started at a time. The call will block when
// starting a read-write transaction when one is already open.
//
// NOTE: The transaction must be closed by calling Rollback or Commit on
// it when it is no longer needed. Failure to do so can result in
// unclaimed memory depending on the specific database implementation.
Begin(writable bool) (Tx, error)
// View invokes the passed function in the context of a managed
// read-only transaction. Any errors returned from the user-supplied
// function are returned from this function.
//
// Calling Rollback on the transaction passed to the user-supplied
// function will result in a panic.
View(fn func(Tx) error) error
// Update invokes the passed function in the context of a managed
// read-write transaction. Any errors returned from the user-supplied
// function will cause the transaction to be rolled back and are
// returned from this function. Otherwise, the transaction is commited
// when the user-supplied function returns a nil error.
//
// Calling Rollback on the transaction passed to the user-supplied
// function will result in a panic.
Update(fn func(Tx) error) error
}
// DB represents a collection of namespaces which are persisted. All database
// access is performed through transactions which are obtained through the
// specific Namespace.
type DB interface {
// Namespace returns a Namespace interface for the provided key. See
// the Namespace interface documentation for more details. Attempting
// to access a Namespace on a database that is not open yet or has been
// closed will result in ErrDbNotOpen. Namespaces are created in the
// database on first access.
Namespace(key []byte) (Namespace, error)
// DeleteNamespace deletes the namespace for the passed key.
// ErrBucketNotFound will be returned if the namespace does not exist.
DeleteNamespace(key []byte) error
// Copy writes a copy of the database to the provided writer. This
// call will start a read-only transaction to perform all operations.
Copy(w io.Writer) error
// Close cleanly shuts down the database and syncs all data.
Close() error
}
// Driver defines a structure for backend drivers to use when they registered
// themselves as a backend which implements the Db interface.
type Driver struct {
// DbType is the identifier used to uniquely identify a specific
// database driver. There can be only one driver with the same name.
DbType string
// Create is the function that will be invoked with all user-specified
// arguments to create the database. This function must return
// ErrDbExists if the database already exists.
Create func(args ...interface{}) (DB, error)
// Open is the function that will be invoked with all user-specified
// arguments to open the database. This function must return
// ErrDbDoesNotExist if the database has not already been created.
Open func(args ...interface{}) (DB, error)
}
// driverList holds all of the registered database backends.
var drivers = make(map[string]*Driver)
// RegisterDriver adds a backend database driver to available interfaces.
// ErrDbTypeRegistered will be retruned if the database type for the driver has
// already been registered.
func RegisterDriver(driver Driver) error {
if _, exists := drivers[driver.DbType]; exists {
return ErrDbTypeRegistered
}
drivers[driver.DbType] = &driver
return nil
}
// SupportedDrivers returns a slice of strings that represent the database
// drivers that have been registered and are therefore supported.
func SupportedDrivers() []string {
supportedDBs := make([]string, 0, len(drivers))
for _, drv := range drivers {
supportedDBs = append(supportedDBs, drv.DbType)
}
return supportedDBs
}
// Create intializes and opens a database for the specified type. The arguments
// are specific to the database type driver. See the documentation for the
// database driver for further details.
//
// ErrDbUnknownType will be returned if the the database type is not registered.
func Create(dbType string, args ...interface{}) (DB, error) {
drv, exists := drivers[dbType]
if !exists {
return nil, ErrDbUnknownType
}
return drv.Create(args...)
}
// Open opens an existing database for the specified type. The arguments are
// specific to the database type driver. See the documentation for the database
// driver for further details.
//
// ErrDbUnknownType will be returned if the the database type is not registered.
func Open(dbType string, args ...interface{}) (DB, error) {
drv, exists := drivers[dbType]
if !exists {
return nil, ErrDbUnknownType
}
return drv.Open(args...)
}