I found myself wanting the database abstraction layer by @imsodin today because I want to test badger instead of leveldb. (For reasons, mostly that the leveldb implementation is not seeing too much action on issues and such.) But that PR is a bit outdated and the interface it declares isn’t optimal for integrating something like badger. I’d like to propose a tweaked interface and fixup that PR…
Reader
is what we can do with a read only transaction, but also with a read-write transaction so it becomes its own interface.
type Reader interface {
Get(key []byte) ([]byte, error)
Has(key []byte) (bool, error)
NewIterator(prefix []byte) Iterator
}
The ReadTransaction
is that plus a Release
that must be called. All operations (even Get
) need to happen in a transaction. Leveldb doesn’t enforce this, but Badger does. In our current code we generally take a read transaction (snapshot) anyway.
type ReadTransaction interface {
Reader
Release()
}
The WriteTransaction
adds write stuff and Commit
. It’s fine to Release
(== discard) after Commit
ing so that one can always be deferred, but if commit is successful then release is a no-op.
The transaction here might be somewhat best effort. This stuff is shaky as is in leveldb, and badger has its own wrinkles – a transaction might grow “too large” and needs to be committed and reopened. I think we should handle as much of that as possible in the implementation so our “user” code doesn’t need to know this.
type WriteTransaction interface {
Reader
Put(key, val []byte) error
Delete(key []byte) error
Release()
Commit() error
}
The Iterator
interface is simplified. We take a prefix instead of a range, because that’s the sort of scans we do. This can become a range iterator in the backend, or it can be just a seek plus an end check implemented in our wrapper type.
type Iterator interface {
Next() bool
Key() []byte
Value() []byte
Error() error
Release()
}
Finally the top level database type and functions to inspect returned errors.
type Backend interface {
NewReadTransaction() ReadTransaction
NewWriteTransaction() (WriteTransaction, error)
Close() error
IsNotFound(err error) bool
}
No implementation specific types in this interface.
No batches (rwTran
can be a batch when appropriate).
Typical iterator usage:
ro := db.NewROTransaction()
defer ro.Release()
it := ro.NewIterator(nil)
defer it.Release()
for it.Next() {
// it.Key() and it.Value() are valid
}
if it.Error() != nil {
// iteration failed due to database error
}
It’s debatable whether to return errors for new transactions and iterators. The underlying implementations differ on this. On balance, I think it’s reasonable for NewWriteTransaction()
to return an error (it probably does writes as part of this call). The read transaction can return the error in its Get()
method, which should anyway be checked. The iterator can return false on Next()
and return the error in Error()
and it will be handled as part of that required error checking anyway.