Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Features + datastore scoping #188

Merged
merged 1 commit into from
Jun 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 0 additions & 56 deletions basic_ds.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,62 +89,6 @@ func (d *MapDatastore) Close() error {
return nil
}

// NullDatastore stores nothing, but conforms to the API.
// Useful to test with.
type NullDatastore struct {
}

var _ Datastore = (*NullDatastore)(nil)
var _ Batching = (*NullDatastore)(nil)

// NewNullDatastore constructs a null datastore
func NewNullDatastore() *NullDatastore {
return &NullDatastore{}
}

// Put implements Datastore.Put
func (d *NullDatastore) Put(ctx context.Context, key Key, value []byte) (err error) {
return nil
}

// Sync implements Datastore.Sync
func (d *NullDatastore) Sync(ctx context.Context, prefix Key) error {
return nil
}

// Get implements Datastore.Get
func (d *NullDatastore) Get(ctx context.Context, key Key) (value []byte, err error) {
return nil, ErrNotFound
}

// Has implements Datastore.Has
func (d *NullDatastore) Has(ctx context.Context, key Key) (exists bool, err error) {
return false, nil
}

// Has implements Datastore.GetSize
func (d *NullDatastore) GetSize(ctx context.Context, key Key) (size int, err error) {
return -1, ErrNotFound
}

// Delete implements Datastore.Delete
func (d *NullDatastore) Delete(ctx context.Context, key Key) (err error) {
return nil
}

// Query implements Datastore.Query
func (d *NullDatastore) Query(ctx context.Context, q dsq.Query) (dsq.Results, error) {
return dsq.ResultsWithEntries(q, nil), nil
}

func (d *NullDatastore) Batch(ctx context.Context) (Batch, error) {
return NewBasicBatch(d), nil
}

func (d *NullDatastore) Close() error {
return nil
}

// LogDatastore logs all accesses through the datastore.
type LogDatastore struct {
Name string
Expand Down
12 changes: 3 additions & 9 deletions basic_ds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,18 @@ import (
"log"
"testing"

dstore "github.com/ipfs/go-datastore"
"github.com/ipfs/go-datastore"
dstest "github.com/ipfs/go-datastore/test"
)

func TestMapDatastore(t *testing.T) {
ds := dstore.NewMapDatastore()
ds := datastore.NewMapDatastore()
dstest.SubtestAll(t, ds)
}

func TestNullDatastore(t *testing.T) {
ds := dstore.NewNullDatastore()
// The only test that passes. Nothing should be found.
dstest.SubtestNotFounds(t, ds)
}

func TestLogDatastore(t *testing.T) {
defer log.SetOutput(log.Writer())
log.SetOutput(ioutil.Discard)
ds := dstore.NewLogDatastore(dstore.NewMapDatastore(), "")
ds := datastore.NewLogDatastore(datastore.NewMapDatastore(), "")
dstest.SubtestAll(t, ds)
}
27 changes: 6 additions & 21 deletions datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"io"
"time"

query "github.com/ipfs/go-datastore/query"
)
Expand Down Expand Up @@ -103,8 +102,7 @@ type Read interface {
// capabilities of a `Batch`, but the reverse is NOT true.
type Batching interface {
Datastore

Batch(ctx context.Context) (Batch, error)
BatchingFeature
}

// ErrBatchUnsupported is returned if the by Batch if the Datastore doesn't
Expand All @@ -115,34 +113,29 @@ var ErrBatchUnsupported = errors.New("this datastore does not support batching")
// which may need checking on-disk data integrity.
type CheckedDatastore interface {
Datastore

Check(ctx context.Context) error
CheckedFeature
}

// ScrubbedDatastore is an interface that should be implemented by datastores
// which want to provide a mechanism to check data integrity and/or
// error correction.
type ScrubbedDatastore interface {
Datastore

Scrub(ctx context.Context) error
ScrubbedFeature
}

// GCDatastore is an interface that should be implemented by datastores which
// don't free disk space by just removing data from them.
type GCDatastore interface {
Datastore

CollectGarbage(ctx context.Context) error
GCFeature
}

// PersistentDatastore is an interface that should be implemented by datastores
// which can report disk usage.
type PersistentDatastore interface {
Datastore

// DiskUsage returns the space used by a datastore, in bytes.
DiskUsage(ctx context.Context) (uint64, error)
PersistentFeature
}

// DiskUsage checks if a Datastore is a
Expand All @@ -163,13 +156,6 @@ type TTLDatastore interface {
TTL
}

// TTL encapulates the methods that deal with entries with time-to-live.
type TTL interface {
PutWithTTL(ctx context.Context, key Key, value []byte, ttl time.Duration) error
SetTTL(ctx context.Context, key Key, ttl time.Duration) error
GetExpiration(ctx context.Context, key Key) (time.Time, error)
}

// Txn extends the Datastore type. Txns allow users to batch queries and
// mutations to the Datastore into atomic groups, or transactions. Actions
// performed on a transaction will not take hold until a successful call to
Expand All @@ -194,8 +180,7 @@ type Txn interface {
// support transactions.
type TxnDatastore interface {
Datastore

NewTransaction(ctx context.Context, readOnly bool) (Txn, error)
TxnFeature
}

// Errors
Expand Down
132 changes: 132 additions & 0 deletions features.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package datastore

import (
"context"
"reflect"
"time"
)

const (
FeatureNameBatching = "Batching"
FeatureNameChecked = "Checked"
FeatureNameGC = "GC"
FeatureNamePersistent = "Persistent"
FeatureNameScrubbed = "Scrubbed"
FeatureNameTTL = "TTL"
FeatureNameTransaction = "Transaction"
)

type BatchingFeature interface {
Batch(ctx context.Context) (Batch, error)
}

type CheckedFeature interface {
Check(ctx context.Context) error
}

type ScrubbedFeature interface {
Scrub(ctx context.Context) error
}

type GCFeature interface {
CollectGarbage(ctx context.Context) error
}

type PersistentFeature interface {
// DiskUsage returns the space used by a datastore, in bytes.
DiskUsage(ctx context.Context) (uint64, error)
}

// TTL encapulates the methods that deal with entries with time-to-live.
type TTL interface {
PutWithTTL(ctx context.Context, key Key, value []byte, ttl time.Duration) error
SetTTL(ctx context.Context, key Key, ttl time.Duration) error
GetExpiration(ctx context.Context, key Key) (time.Time, error)
}

type TxnFeature interface {
NewTransaction(ctx context.Context, readOnly bool) (Txn, error)
}

// Feature contains metadata about a datastore Feature.
type Feature struct {
Name string
// Interface is the nil interface of the feature.
Interface interface{}
// DatastoreInterface is the nil interface of the feature's corresponding datastore interface.
DatastoreInterface interface{}
}

var featuresByName map[string]Feature

func init() {
featuresByName = map[string]Feature{}
for _, f := range Features() {
featuresByName[f.Name] = f
}
}

// Features returns a list of all known datastore features.
// This serves both to provide an authoritative list of features,
// and to define a canonical ordering of features.
func Features() []Feature {
// for backwards compatibility, only append to this list
return []Feature{
{
Name: FeatureNameBatching,
Interface: (*BatchingFeature)(nil),
DatastoreInterface: (*Batching)(nil),
},
{
Name: FeatureNameChecked,
Interface: (*CheckedFeature)(nil),
DatastoreInterface: (*CheckedDatastore)(nil),
},
{
Name: FeatureNameGC,
Interface: (*GCFeature)(nil),
DatastoreInterface: (*GCDatastore)(nil),
},
{
Name: FeatureNamePersistent,
Interface: (*PersistentFeature)(nil),
DatastoreInterface: (*PersistentDatastore)(nil),
},
{
Name: FeatureNameScrubbed,
Interface: (*ScrubbedFeature)(nil),
DatastoreInterface: (*ScrubbedDatastore)(nil),
},
{
Name: FeatureNameTTL,
Interface: (*TTL)(nil),
DatastoreInterface: (*TTLDatastore)(nil),
},
{
Name: FeatureNameTransaction,
Interface: (*TxnFeature)(nil),
DatastoreInterface: (*TxnDatastore)(nil),
},
}
}

// FeatureByName returns the feature with the given name, if known.
func FeatureByName(name string) (Feature, bool) {
feat, known := featuresByName[name]
return feat, known
}

// FeaturesForDatastore returns the features supported by the given datastore.
func FeaturesForDatastore(dstore Datastore) (features []Feature) {
if dstore == nil {
return nil
}
dstoreType := reflect.TypeOf(dstore)
for _, f := range Features() {
fType := reflect.TypeOf(f.Interface).Elem()
if dstoreType.Implements(fType) {
features = append(features, f)
}
}
return
}
77 changes: 77 additions & 0 deletions features_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package datastore

import (
"fmt"
"reflect"
"testing"
)

func TestFeatureByName(t *testing.T) {
feat, ok := FeatureByName(FeatureNameBatching)
if !ok {
t.Fatalf("expected a batching feature")
}
if feat.Name != FeatureNameBatching ||
feat.Interface != (*BatchingFeature)(nil) ||
feat.DatastoreInterface != (*Batching)(nil) {
t.Fatalf("expected a batching feature, got %v", feat)
}

feat, ok = FeatureByName("UnknownFeature")
if ok {
t.Fatalf("expected UnknownFeature not to be found")
}
}

func featuresByNames(names []string) (fs []Feature) {
for _, n := range names {
f, ok := FeatureByName(n)
if !ok {
panic(fmt.Sprintf("unknown feature %s", n))
}
fs = append(fs, f)
}
return
}

func TestFeaturesForDatastore(t *testing.T) {
cases := []struct {
name string
d Datastore
expectedFeatures []string
}{
{
name: "MapDatastore",
d: &MapDatastore{},
expectedFeatures: []string{"Batching"},
},
{
name: "NullDatastore",
d: &NullDatastore{},
expectedFeatures: []string{"Batching", "Checked", "GC", "Persistent", "Scrubbed", "Transaction"},
},
{
name: "LogDatastore",
d: &LogDatastore{},
expectedFeatures: []string{"Batching", "Checked", "GC", "Persistent", "Scrubbed"},
},
{
name: "nil datastore",
d: nil,
expectedFeatures: nil,
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
feats := FeaturesForDatastore(c.d)
if len(feats) != len(c.expectedFeatures) {
t.Fatalf("expected %d features, got %v", len(c.expectedFeatures), feats)
}
expectedFeats := featuresByNames(c.expectedFeatures)
if !reflect.DeepEqual(expectedFeats, feats) {
t.Fatalf("expected features %v, got %v", c.expectedFeatures, feats)
}
})
}
}
Loading