From e8b4de93792ffa65667fc1ba0aceeab69f3ab30c Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Sun, 9 Nov 2014 18:31:38 -0600 Subject: [PATCH] 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 --- walletdb/bdb/db.go | 372 +++++++++++++++++++++++++++++++++++++++++ walletdb/bdb/doc.go | 37 ++++ walletdb/bdb/driver.go | 78 +++++++++ walletdb/doc.go | 115 +++++++++++++ walletdb/error.go | 92 ++++++++++ walletdb/interface.go | 235 ++++++++++++++++++++++++++ 6 files changed, 929 insertions(+) create mode 100644 walletdb/bdb/db.go create mode 100644 walletdb/bdb/doc.go create mode 100644 walletdb/bdb/driver.go create mode 100644 walletdb/doc.go create mode 100644 walletdb/error.go create mode 100644 walletdb/interface.go diff --git a/walletdb/bdb/db.go b/walletdb/bdb/db.go new file mode 100644 index 0000000..0cc64cd --- /dev/null +++ b/walletdb/bdb/db.go @@ -0,0 +1,372 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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) +} diff --git a/walletdb/bdb/doc.go b/walletdb/bdb/doc.go new file mode 100644 index 0000000..2fc4a59 --- /dev/null +++ b/walletdb/bdb/doc.go @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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 diff --git a/walletdb/bdb/driver.go b/walletdb/bdb/driver.go new file mode 100644 index 0000000..41ab6e8 --- /dev/null +++ b/walletdb/bdb/driver.go @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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)) + } +} diff --git a/walletdb/doc.go b/walletdb/doc.go new file mode 100644 index 0000000..f6712c5 --- /dev/null +++ b/walletdb/doc.go @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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 diff --git a/walletdb/error.go b/walletdb/error.go new file mode 100644 index 0000000..1e28a73 --- /dev/null +++ b/walletdb/error.go @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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") +) diff --git a/walletdb/interface.go b/walletdb/interface.go new file mode 100644 index 0000000..55b477b --- /dev/null +++ b/walletdb/interface.go @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2014 Conformal Systems LLC + * + * 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...) +}