Bläddra i källkod

initial commit, untested

Aneurin Barker Snook 1 år sedan
incheckning
b7b920a637
20 ändrade filer med 1097 tillägg och 0 borttagningar
  1. 1 0
      .gitignore
  2. 7 0
      LICENSE.md
  3. 62 0
      README.md
  4. 22 0
      bytes.go
  5. 208 0
      collection_test.go
  6. 12 0
      errors.go
  7. 7 0
      go.mod
  8. 27 0
      go.sum
  9. 52 0
      interfaces.go
  10. 25 0
      json.go
  11. 43 0
      json_test.go
  12. 10 0
      key.go
  13. 101 0
      leveldb.go
  14. 104 0
      leveldb_iter.go
  15. 27 0
      leveldb_test.go
  16. 116 0
      memory.go
  17. 181 0
      memory_iter.go
  18. 14 0
      memory_test.go
  19. 76 0
      sort.go
  20. 2 0
      staticcheck.conf

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+.leveldb

+ 7 - 0
LICENSE.md

@@ -0,0 +1,7 @@
+Copyright © 2024 Aneurin Barker Snook
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 62 - 0
README.md

@@ -0,0 +1,62 @@
+# EZ DB
+
+This package provides simple interfaces for working with basic key-value storage in your Go application.
+
+**EZ DB is not a database unto itself.** If you want more control or features, just use the appropriate database software and connector for your needs.
+
+## Basic usage
+
+The primary interface in EZ DB is `Collection[T]` which reflects a single key-value store. This is analogous to tables in RDBMS, collections in NoSQL databases etc.
+
+Collections use a generic type `T` to specify the document type. You can use this to enforce a document schema. This example creates a collection which only accepts `Student` documents:
+
+```go
+package main
+
+import "github.com/annybs/ezdb"
+
+type Student struct {
+	Name string
+	Age int
+}
+
+var db = ezdb.Memory[Student](nil)
+
+func main() {
+	db.Open()
+	db.Put("annie", Student{Name: "Annie", Age: "32"})
+	db.Close()
+}
+```
+
+In other cases, such as media stores, you may prefer not to specify a document type. This example allows arbitrary bytes to be written:
+
+```go
+package main
+
+import "github.com/annybs/ezdb"
+
+var db = ezdb.Memory[[]byte](nil)
+
+func main() {
+	db.Open()
+	db.Put("data", []byte("arbitrary bytes"))
+	db.Close()
+}
+```
+
+## Marshaling data
+
+Some database backends require marshaling and unmarshaling data. The `DocumentMarshaler[T1, T2]` interface allows you to use whatever marshaler suits your needs or the requirements of your chosen database.
+
+The following marshalers are included in EZ DB:
+
+- `Bytes` allows you to write `[]byte` directly to a database that requires `[]byte`
+- `JSON[T]` marshals your data `T` to `[]byte` using [encoding/json](https://pkg.go.dev/encoding/json)
+
+## Supported databases
+
+The following databases are included in EZ DB:
+
+- `LevelDB[T]` is [fast key-value storage](https://github.com/google/leveldb) on disk
+- `Memory[T]` is essentially a wrapper for `map[string]T`. It can be provided another Collection to use as a persistence backend

+ 22 - 0
bytes.go

@@ -0,0 +1,22 @@
+package ezdb
+
+// BytesMarshaler is a DocumentMarshaler that simply passes along bytes.
+type BytesMarshaler struct{}
+
+func (m *BytesMarshaler) Factory() []byte {
+	return []byte{}
+}
+
+func (m *BytesMarshaler) Marshal(src []byte) ([]byte, error) {
+	return src, nil
+}
+
+func (m *BytesMarshaler) Unmarshal(src []byte, dest []byte) error {
+	dest = src
+	return nil
+}
+
+// Bytes creates a DocumentMarshaler that simply passes along bytes.
+func Bytes() *BytesMarshaler {
+	return &BytesMarshaler{}
+}

+ 208 - 0
collection_test.go

@@ -0,0 +1,208 @@
+package ezdb
+
+import (
+	"errors"
+	"testing"
+)
+
+// Basic struct for testing.
+type Student struct {
+	Name string `json:"name"`
+	Age  int    `json:"age"`
+}
+
+var invalidStudents = map[string]*Student{
+	"": {},
+}
+
+var nonexistentStudentKey = "nonexistent"
+
+// Basic marshaler for testing.
+var studentMarshaler = JSON(func() *Student {
+	return &Student{}
+})
+
+// Sample data.
+var students = map[string]*Student{
+	"annie": {Name: "Annie", Age: 32},
+	"ben":   {Name: "Ben", Age: 50},
+	"clive": {Name: "Clive", Age: 21},
+}
+
+// Sample data (marshaled).
+var studentsMarshaled = map[string][]byte{
+	"annie": []byte("{\"name\":\"Annie\",\"age\":32}"),
+	"ben":   []byte("{\"name\":\"Ben\",\"age\":50}"),
+	"clive": []byte("{\"name\":\"Clive\",\"age\":21}"),
+}
+
+type CollectionTest struct {
+	C Collection[*Student]
+	T *testing.T
+
+	F map[string]func() error
+}
+
+func (c *CollectionTest) open() error {
+	if err := c.C.Open(); err != nil {
+		c.T.Errorf("(open) failed to open collection: %v", err)
+		return err
+	}
+
+	return nil
+}
+
+func (c *CollectionTest) put() error {
+	// Test collection can store all students
+	for key, value := range students {
+		if err := c.C.Put(key, value); err != nil {
+			c.T.Errorf("(put) failed to put student '%s': %v", key, err)
+			return err
+		}
+		c.T.Logf("(put) put student '%s'", key)
+	}
+
+	// Test collection does not accept invalid keys
+	for key, value := range invalidStudents {
+		if err := c.C.Put(key, value); err == nil {
+			c.T.Errorf("(put) should not have put invalid student '%s'", key)
+			return err
+		}
+		c.T.Logf("(put) skipped invalid student '%s'", key)
+	}
+
+	return nil
+}
+
+func (c *CollectionTest) has() error {
+	// Test collection has all students
+	for key := range students {
+		has, err := c.C.Has(key)
+		if err != nil {
+			c.T.Errorf("(has) failed to test whether collection has student '%s': %v", key, err)
+			return err
+		} else if !has {
+			c.T.Errorf("(has) expected collection to have student '%s'", key)
+			return err
+		}
+		c.T.Logf("(has) found student '%s'", key)
+	}
+
+	// Test collection does claim to have a student that doesn't exist
+	has, err := c.C.Has(nonexistentStudentKey)
+	if err != nil {
+		c.T.Errorf("(has) failed to test whether collection has nonexistent student: %v", err)
+	} else if has {
+		c.T.Error("(has) expected collection not to have nonexistent student")
+	} else {
+		c.T.Logf("(has) collection does not have nonexistent student")
+	}
+
+	return nil
+}
+
+func (c *CollectionTest) get() error {
+	// Test collection can retrieve all students
+	for key, expected := range students {
+		actual, err := c.C.Get(key)
+		if err != nil {
+			c.T.Errorf("(get) failed to get student '%s': %v", key, err)
+			continue
+		} else if actual.Name != expected.Name {
+			c.T.Errorf("(get) student '%s' has wrong name (expected '%s', got '%s')", key, expected.Name, actual.Name)
+		} else if actual.Age != expected.Age {
+			c.T.Errorf("(get) student '%s' has wrong age (expected '%s', got '%s')", key, expected.Name, actual.Name)
+		} else {
+			c.T.Logf("(get) correctly got student '%s'", key)
+		}
+	}
+
+	// Test collection does not retrieve a nonexistent student
+	_, err := c.C.Get(nonexistentStudentKey)
+	if err == nil {
+		c.T.Error("(get) expected collection to return an error for nonexistent student")
+	} else {
+		c.T.Log("(get) collection did not get a nonexistent student")
+	}
+
+	return nil
+}
+
+func (c *CollectionTest) delete() error {
+	if err := c.C.Delete("annie"); err != nil {
+		c.T.Errorf("(delete) failed to delete student '%s': %v", "annie", err)
+		return err
+	}
+
+	// Confirm student has been deleted
+	has, err := c.C.Has("annie")
+	if err != nil {
+		c.T.Errorf("(delete) failed to test whether collection has deleted student 'annie': %v", err)
+		return err
+	} else if has {
+		c.T.Error("(delete) expected collection not to have deleted student 'annie'")
+		return err
+	} else {
+		c.T.Log("(delete) collection did not get the deleted student 'annie'")
+	}
+
+	// Reinsert deleted student
+	if err := c.C.Put("annie", students["annie"]); err != nil {
+		c.T.Errorf("(delete) failed to reinsert student 'annie': %v", err)
+		return err
+	} else {
+		c.T.Log("(delete) reinserted student 'annie'")
+	}
+
+	return nil
+}
+
+func (c *CollectionTest) iterCount() error {
+	iter := c.C.Iter()
+	defer iter.Release()
+
+	expected := len(students)
+	actual := iter.Count()
+	if expected != actual {
+		c.T.Errorf("(iterCount) incorrect count of students (expected %d, got %d)", expected, actual)
+		return errors.New("incorrect count")
+	} else {
+		c.T.Logf("(iterCount) correct count of students (expected %d, got %d)", expected, actual)
+	}
+
+	return nil
+}
+
+func (c *CollectionTest) close() error {
+	if c.F["close"] != nil {
+		if err := c.F["close"](); err != nil {
+			c.T.Errorf("(close) failed to close collection: %v", err)
+			return err
+		}
+	} else if err := c.C.Close(); err != nil {
+		c.T.Errorf("(close) failed to close collection: %v", err)
+		return err
+	}
+
+	c.T.Log("(close) closed database")
+
+	return nil
+}
+
+func (c *CollectionTest) Run() {
+	tests := []func() error{
+		c.open,
+		c.put,
+		c.has,
+		c.get,
+		c.delete,
+		c.iterCount,
+		c.close,
+	}
+
+	for _, test := range tests {
+		if err := test(); err != nil {
+			return
+		}
+	}
+}

+ 12 - 0
errors.go

@@ -0,0 +1,12 @@
+package ezdb
+
+import "errors"
+
+// High-level EZ DB error.
+// These are not exhaustive and your chosen implementation of Collection may produce its own errors.
+var (
+	ErrClosed     = errors.New("collection is closed")
+	ErrInvalidKey = errors.New("invalid key")
+	ErrNotFound   = errors.New("not found")
+	ErrReleased   = errors.New("iterator has been released")
+)

+ 7 - 0
go.mod

@@ -0,0 +1,7 @@
+module github.com/annybs/ezdb
+
+go 1.21.4
+
+require github.com/syndtr/goleveldb v1.0.0
+
+require github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect

+ 27 - 0
go.sum

@@ -0,0 +1,27 @@
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
+github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

+ 52 - 0
interfaces.go

@@ -0,0 +1,52 @@
+package ezdb
+
+// Collection is a key-value store for documents of any type.
+type Collection[T any] interface {
+	Open() error  // Open the collection.
+	Close() error // Close the collection.
+
+	Delete(key string) error              // Delete a document by key.
+	Get(key string) (value T, err error)  // Get a document by key.
+	Has(key string) (has bool, err error) // Check whether a document exists by key.
+	Put(key string, value T) error        // Put a document into the collection.
+
+	Iter() Iterator[T] // Get an iterator for this collection.
+}
+
+// DocumentMarshaler facilitates conversion between two types - a document and its storage representation, depending on the implementation of the Collection.
+type DocumentMarshaler[T1 any, T2 any] interface {
+	Factory() T1                     // Create a new, empty document.
+	Marshal(src T1) (T2, error)      // Marshal a document to bytes.
+	Unmarshal(src T2, dest T1) error // Unmarshal bytes into a document.
+}
+
+// FilterFunc processes a document as part of a filter operation.
+// This function returns true if the document passes all checks defined in the filter.
+type FilterFunc[T any] func(key string, value T) bool
+
+// Iterator provides functionality to explore a collection.
+type Iterator[T any] interface {
+	First() bool // Move the iterator to the first document. Returns false if there is no first document.
+	Last() bool  // Move the iterator to the last document. Returns false if there is no last document.
+	Next() bool  // Move the iterator to the next document. Returns false if there is no next document.
+	Prev() bool  // Move the iterator to the previous document. Returns false if there is no previous document.
+
+	Release() // Release the iterator and any associated resources, including those of previous iterators.
+
+	Count() int // Count the number of documents in the iterator.
+
+	Get() (key string, value T, err error) // Get the key and value of the current document.
+	Key() string                           // Get the key of the current document.
+	Value() (T, error)                     // Get the value of the current document.
+
+	GetAll() (map[string]T, error) // Get all documents as a key-value map.
+	GetAllKeys() []string          // Get all document keys.
+
+	Filter(f FilterFunc[T]) Iterator[T]      // Create a new iterator with a subset of documents. The previous iterator will not be affected.
+	Sort(f SortFunc[T]) Iterator[T]          // Create a new iterator with sorted documents. The previous iterator will not be affected.
+	SortKeys(f SortFunc[string]) Iterator[T] // Create a new iterator with documents sorted by key. The previous iterator will not be affected.
+}
+
+// SortFunc compares two documents as part of a sort operation.
+// This function returns false if a is less than b.
+type SortFunc[T any] func(a T, b T) bool

+ 25 - 0
json.go

@@ -0,0 +1,25 @@
+package ezdb
+
+import "encoding/json"
+
+// JSONMarshaler is a DocumentMarshaler that converts documents to JSON data.
+type JSONMarshaler[T any] struct {
+	factory func() T
+}
+
+func (m *JSONMarshaler[T]) Factory() T {
+	return m.factory()
+}
+
+func (m *JSONMarshaler[T]) Marshal(src T) ([]byte, error) {
+	return json.Marshal(src)
+}
+
+func (m *JSONMarshaler[T]) Unmarshal(src []byte, dest T) error {
+	return json.Unmarshal(src, dest)
+}
+
+// JSON creates a DocumentMarshaler that converts documents to JSON data.
+func JSON[T any](factory func() T) *JSONMarshaler[T] {
+	return &JSONMarshaler[T]{factory: factory}
+}

+ 43 - 0
json_test.go

@@ -0,0 +1,43 @@
+package ezdb
+
+import (
+	"bytes"
+	"testing"
+)
+
+func TestJSONFactory(t *testing.T) {
+	t.Logf("creating empty student")
+	var value any = studentMarshaler.Factory()
+	if _, ok := value.(*Student); !ok {
+		t.Errorf("factory did not create correct value type (expected '*Student', got '%T')", value)
+	}
+}
+
+func TestJSONMarshal(t *testing.T) {
+	for key, value := range students {
+		t.Logf("marshaling student '%s'", key)
+		b, err := studentMarshaler.Marshal(value)
+		if err != nil {
+			t.Errorf("failed to marshal student '%s' (%q)", key, err)
+		} else if !bytes.Equal(b, studentsMarshaled[key]) {
+			t.Errorf("student '%s' incorrectly marshaled (expected '%s', got '%s')", key, studentsMarshaled[key], b)
+		}
+	}
+}
+
+func TestJSONUnmarshal(t *testing.T) {
+	for key, b := range studentsMarshaled {
+		t.Logf("unmarshaling student '%s'", key)
+		value := studentMarshaler.Factory()
+		if err := studentMarshaler.Unmarshal(b, value); err != nil {
+			t.Errorf("failed to unmarshal student \"%s\" (%q)", key, err)
+		} else {
+			if value.Name != students[key].Name {
+				t.Errorf("student '%s' name incorrectly unmarshaled (expected '%s', got '%s')", key, students[key].Name, value.Name)
+			}
+			if value.Age != students[key].Age {
+				t.Errorf("student '%s' age incorrectly unmarshaled (expected '%d', got '%d')", key, students[key].Age, value.Age)
+			}
+		}
+	}
+}

+ 10 - 0
key.go

@@ -0,0 +1,10 @@
+package ezdb
+
+// ValidateKey validates whether a key is valid for putting data into a collection.
+func ValidateKey(key string) error {
+	if key == "" {
+		return ErrInvalidKey
+	}
+
+	return nil
+}

+ 101 - 0
leveldb.go

@@ -0,0 +1,101 @@
+package ezdb
+
+import (
+	"os"
+
+	"github.com/syndtr/goleveldb/leveldb"
+)
+
+type LevelDBCollection[T any] struct {
+	path string
+
+	db *leveldb.DB
+	m  DocumentMarshaler[T, []byte]
+}
+
+func (c *LevelDBCollection[T]) Close() error {
+	if c.db != nil {
+		if err := c.db.Close(); err != nil {
+			return err
+		}
+
+		c.db = nil
+	}
+
+	return nil
+}
+
+func (c *LevelDBCollection[T]) Delete(key string) error {
+	return c.db.Delete([]byte(key), nil)
+}
+
+// Destroy the database completely, removing it from disk.
+func (c *LevelDBCollection[T]) Destroy() error {
+	if err := c.Close(); err != nil {
+		return err
+	}
+
+	return os.RemoveAll(c.path)
+}
+
+func (c *LevelDBCollection[T]) Get(key string) (T, error) {
+	dest := c.m.Factory()
+
+	src, err := c.db.Get([]byte(key), nil)
+	if err != nil {
+		return dest, err
+	}
+
+	err = c.m.Unmarshal(src, dest)
+
+	return dest, err
+}
+
+func (c *LevelDBCollection[T]) Has(key string) (bool, error) {
+	return c.db.Has([]byte(key), nil)
+}
+
+func (c *LevelDBCollection[T]) Iter() Iterator[T] {
+	i := &LevelDBIterator[T]{
+		i: c.db.NewIterator(nil, nil),
+		m: c.m,
+	}
+
+	return i
+}
+
+func (c *LevelDBCollection[T]) Open() error {
+	if c.db == nil {
+		db, err := leveldb.OpenFile(c.path, nil)
+		if err != nil {
+			return err
+		}
+
+		c.db = db
+	}
+
+	return nil
+}
+
+func (c *LevelDBCollection[T]) Put(key string, src T) error {
+	if err := ValidateKey(key); err != nil {
+		return err
+	}
+
+	dest, err := c.m.Marshal(src)
+	if err != nil {
+		return err
+	}
+
+	return c.db.Put([]byte(key), dest, nil)
+}
+
+// LevelDB creates a new collection using LevelDB storage.
+func LevelDB[T any](path string, m DocumentMarshaler[T, []byte]) *LevelDBCollection[T] {
+	c := &LevelDBCollection[T]{
+		path: path,
+
+		m: m,
+	}
+	return c
+}

+ 104 - 0
leveldb_iter.go

@@ -0,0 +1,104 @@
+package ezdb
+
+import "github.com/syndtr/goleveldb/leveldb/iterator"
+
+type LevelDBIterator[T any] struct {
+	i iterator.Iterator
+	m DocumentMarshaler[T, []byte]
+}
+
+func (i *LevelDBIterator[T]) Count() int {
+	n := 0
+	for i.Next() {
+		n++
+	}
+	return n
+}
+
+func (i *LevelDBIterator[T]) Filter(f FilterFunc[T]) Iterator[T] {
+	i.First()
+	m := map[string]T{}
+	for i.Next() {
+		key, value, err := i.Get()
+		if err != nil {
+			continue
+		}
+		m[key] = value
+	}
+	return newMemoryIterator(m, i)
+}
+
+func (i *LevelDBIterator[T]) First() bool {
+	return i.i.First()
+}
+
+func (i *LevelDBIterator[T]) Get() (string, T, error) {
+	value, err := i.Value()
+	return i.Key(), value, err
+}
+
+func (i *LevelDBIterator[T]) GetAll() (map[string]T, error) {
+	values := map[string]T{}
+	i.First()
+	key, value, err := i.Get()
+	if err != nil {
+		return values, err
+	}
+	values[key] = value
+	for i.Next() {
+		key, value, err := i.Get()
+		if err != nil {
+			return values, err
+		}
+		values[key] = value
+	}
+	return values, nil
+}
+
+func (i *LevelDBIterator[T]) GetAllKeys() []string {
+	keys := []string{}
+	i.First()
+	keys = append(keys, i.Key())
+	for i.Next() {
+		keys = append(keys, i.Key())
+	}
+	return keys
+}
+
+func (i *LevelDBIterator[T]) Key() string {
+	return string(i.i.Key())
+}
+
+func (i *LevelDBIterator[T]) Last() bool {
+	return i.i.Last()
+}
+
+func (i *LevelDBIterator[T]) Next() bool {
+	return i.i.Next()
+}
+
+func (i *LevelDBIterator[T]) Prev() bool {
+	return i.i.Prev()
+}
+
+func (i *LevelDBIterator[T]) Release() {
+	i.i.Release()
+}
+
+func (i *LevelDBIterator[T]) Sort(f SortFunc[T]) Iterator[T] {
+	all, _ := i.GetAll()
+	m := newMemoryIterator(all, i)
+	return m.Sort(f)
+}
+
+func (i *LevelDBIterator[T]) SortKeys(f SortFunc[string]) Iterator[T] {
+	all, _ := i.GetAll()
+	m := newMemoryIterator(all, i)
+	return m.SortKeys(f)
+}
+
+func (i *LevelDBIterator[T]) Value() (T, error) {
+	value := i.m.Factory()
+	err := i.m.Unmarshal(i.i.Value(), value)
+	return value, err
+}

+ 27 - 0
leveldb_test.go

@@ -0,0 +1,27 @@
+package ezdb
+
+import "testing"
+
+func TestLevelDB(t *testing.T) {
+	path := ".leveldb/leveldb_test"
+	c := LevelDB[*Student](path, studentMarshaler)
+
+	fixture := &CollectionTest{
+		C: c,
+		T: t,
+		F: map[string]func() error{},
+	}
+
+	fixture.F["close"] = func() error {
+		if err := c.Close(); err != nil {
+			return err
+		}
+		if err := c.Destroy(); err != nil {
+			return err
+		}
+		t.Logf("(leveldb) deleted data at %s", path)
+		return nil
+	}
+
+	fixture.Run()
+}

+ 116 - 0
memory.go

@@ -0,0 +1,116 @@
+package ezdb
+
+type MemoryCollection[T any] struct {
+	c Collection[T]
+	m map[string]T
+
+	open bool
+}
+
+func (c *MemoryCollection[T]) Close() error {
+	if c.c != nil {
+		return c.c.Close()
+	}
+
+	c.m = map[string]T{}
+	c.open = false
+
+	return nil
+}
+
+func (c *MemoryCollection[T]) Delete(key string) error {
+	if !c.open {
+		return ErrClosed
+	}
+
+	if c.c != nil {
+		if err := c.c.Delete(key); err != nil {
+			return err
+		}
+	}
+
+	delete(c.m, key)
+
+	return nil
+}
+
+func (c *MemoryCollection[T]) Get(key string) (T, error) {
+	if !c.open {
+		return c.m[""], ErrClosed
+	}
+
+	if value, ok := c.m[key]; ok {
+		return value, nil
+	}
+
+	return c.m[""], ErrNotFound
+}
+
+func (c *MemoryCollection[T]) Has(key string) (bool, error) {
+	if !c.open {
+		return false, ErrClosed
+	}
+
+	_, ok := c.m[key]
+
+	return ok, nil
+}
+
+func (c *MemoryCollection[T]) Iter() Iterator[T] {
+	m := newMemoryIterator[T](c.m, nil)
+	if !c.open {
+		m.Release()
+	}
+	return m
+}
+
+func (c *MemoryCollection[T]) Open() error {
+	if c.c != nil {
+		if err := c.c.Open(); err != nil {
+			return err
+		}
+		all, err := c.c.Iter().GetAll()
+		if err != nil {
+			return err
+		}
+		c.m = all
+	} else {
+		c.m = map[string]T{}
+	}
+
+	c.open = true
+
+	return nil
+}
+
+func (c *MemoryCollection[T]) Put(key string, value T) error {
+	if !c.open {
+		return ErrClosed
+	}
+
+	if err := ValidateKey(key); err != nil {
+		return err
+	}
+
+	if c.c != nil {
+		if err := c.c.Put(key, value); err != nil {
+			return err
+		}
+	}
+
+	c.m[key] = value
+
+	return nil
+}
+
+// Memory creates an in-memory collection, which offers fast access without a document marshaler.
+//
+// If the collection c is non-nil, it will be used as a persistence backend.
+//
+// If T is a pointer type, the same pointer will be used whenever a document is read from this collection, so you should take care to treat documents as immutable.
+func Memory[T any](c Collection[T]) *MemoryCollection[T] {
+	return &MemoryCollection[T]{
+		c: c,
+		m: map[string]T{},
+	}
+}

+ 181 - 0
memory_iter.go

@@ -0,0 +1,181 @@
+package ezdb
+
+import "sort"
+
+type MemoryIterator[T any] struct {
+	k []string
+	m map[string]T
+
+	pos      int
+	released bool
+
+	prev Iterator[T]
+}
+
+func (i *MemoryIterator[T]) Count() int {
+	return len(i.k)
+}
+
+func (i *MemoryIterator[T]) Filter(f FilterFunc[T]) Iterator[T] {
+	if i.released {
+		return i
+	}
+
+	m := map[string]T{}
+	i.reset()
+	for i.Next() {
+		key, value, _ := i.Get()
+		if f(key, value) {
+			m[key] = value
+		}
+	}
+	return newMemoryIterator(m, i)
+}
+
+func (i *MemoryIterator[T]) First() bool {
+	if i.released {
+		return false
+	}
+	i.pos = 0
+	return true
+}
+
+func (i *MemoryIterator[T]) Get() (string, T, error) {
+	if i.released {
+		return "", i.m[""], ErrReleased
+	}
+
+	key := i.Key()
+	return key, i.m[key], nil
+}
+
+func (i *MemoryIterator[T]) GetAll() (map[string]T, error) {
+	m := map[string]T{}
+	if i.released {
+		return m, ErrReleased
+	}
+
+	i.reset()
+	for i.Next() {
+		key, value, _ := i.Get()
+		m[key] = value
+	}
+	return m, nil
+}
+
+func (i *MemoryIterator[T]) GetAllKeys() []string {
+	keys := []string{}
+	if i.released {
+		return keys
+	}
+
+	i.reset()
+	for i.Next() {
+		keys = append(keys, i.Key())
+	}
+	return keys
+}
+
+func (i *MemoryIterator[T]) Key() string {
+	if i.pos < 0 || i.pos > i.Count() || i.released {
+		return ""
+	}
+	return i.k[i.pos]
+}
+
+func (i *MemoryIterator[T]) Last() bool {
+	if i.released {
+		return false
+	}
+	i.pos = len(i.k) - 1
+	return true
+}
+
+func (i *MemoryIterator[T]) Next() bool {
+	if i.released {
+		return false
+	}
+
+	end := i.pos+1 >= i.Count()
+	if !end {
+		i.pos++
+	}
+	return end
+}
+
+func (i *MemoryIterator[T]) Prev() bool {
+	if i.released {
+		return false
+	}
+
+	end := i.pos > 0
+	if !end {
+		i.pos--
+	}
+	return end
+}
+
+func (i *MemoryIterator[T]) Release() {
+	i.k = []string{}
+	i.m = map[string]T{}
+	i.released = true
+
+	if i.prev != nil {
+		i.prev.Release()
+	}
+}
+
+func (i *MemoryIterator[T]) Sort(f SortFunc[T]) Iterator[T] {
+	if i.released {
+		return i
+	}
+
+	s := &valueSort[T]{
+		a: makeSortable(i.m),
+		f: f,
+	}
+	sort.Stable(s)
+	m := s.Result()
+
+	return newMemoryIterator(m, i)
+}
+
+func (i *MemoryIterator[T]) SortKeys(f SortFunc[string]) Iterator[T] {
+	if i.released {
+		return i
+	}
+
+	s := &keySort[T]{
+		a: makeSortable(i.m),
+		f: f,
+	}
+	sort.Stable(s)
+	m := s.Result()
+
+	return newMemoryIterator(m, i)
+}
+
+func (i *MemoryIterator[T]) Value() (T, error) {
+	key := i.Key()
+	return i.m[key], nil
+}
+
+func (i *MemoryIterator[T]) reset() {
+	i.pos = -1
+}
+
+func newMemoryIterator[T any](m map[string]T, prev Iterator[T]) *MemoryIterator[T] {
+	i := &MemoryIterator[T]{
+		k: []string{},
+		m: m,
+
+		pos:  -1,
+		prev: prev,
+	}
+
+	for k := range i.m {
+		i.k = append(i.k, k)
+	}
+
+	return i
+}

+ 14 - 0
memory_test.go

@@ -0,0 +1,14 @@
+package ezdb
+
+import "testing"
+
+func TestMemory(t *testing.T) {
+	c := Memory[*Student](nil)
+
+	fixture := &CollectionTest{
+		C: c,
+		T: t,
+	}
+
+	fixture.Run()
+}

+ 76 - 0
sort.go

@@ -0,0 +1,76 @@
+package ezdb
+
+type sortable[T any] struct {
+	Key   string
+	Value T
+}
+
+type keySort[T any] struct {
+	a []*sortable[T]
+	f SortFunc[string]
+}
+
+func (s *keySort[T]) Len() int {
+	return len(s.a)
+}
+
+func (s *keySort[T]) Less(i, j int) bool {
+	a := s.a[i]
+	b := s.a[j]
+
+	return s.f(a.Key, b.Key)
+}
+
+func (s *keySort[T]) Result() map[string]T {
+	m := map[string]T{}
+	for _, el := range s.a {
+		m[el.Key] = el.Value
+	}
+	return m
+}
+
+func (s *keySort[T]) Swap(i, j int) {
+	a := s.a[i]
+	b := s.a[j]
+	s.a[i] = b
+	s.a[j] = a
+}
+
+type valueSort[T any] struct {
+	a []*sortable[T]
+	f SortFunc[T]
+}
+
+func (s *valueSort[T]) Len() int {
+	return len(s.a)
+}
+
+func (s *valueSort[T]) Less(i, j int) bool {
+	a := s.a[i]
+	b := s.a[j]
+
+	return s.f(a.Value, b.Value)
+}
+
+func (s *valueSort[T]) Result() map[string]T {
+	m := map[string]T{}
+	for _, el := range s.a {
+		m[el.Key] = el.Value
+	}
+	return m
+}
+
+func (s *valueSort[T]) Swap(i, j int) {
+	a := s.a[i]
+	b := s.a[j]
+	s.a[i] = b
+	s.a[j] = a
+}
+
+func makeSortable[T any](m map[string]T) []*sortable[T] {
+	a := []*sortable[T]{}
+	for key, value := range m {
+		a = append(a, &sortable[T]{Key: key, Value: value})
+	}
+	return a
+}

+ 2 - 0
staticcheck.conf

@@ -0,0 +1,2 @@
+# https://staticcheck.dev/docs/configuration/options/#checks
+checks = ["all", "-ST1000", "-ST1003", "-ST1016", "-SA1012", "-SA4006", "-SA4009"]