Repository: NethermindEth/juno
Pull request #216 (2022-06-13 19:14:43)

Named Database
## Changes:
- Add the `NamedDatabase` type under the `internal/db` package. With this new type, we can have only one database file to store all the types, instead of having one file for each type.

## Types of changes

What types of changes does your code introduce?
_Put an `x` in the boxes that apply_

- [x] New feature (non-breaking change which adds functionality)

## Testing
**Requires testing**

- [x] Yes
- [ ] No

**In case you checked yes, did you write tests??**

- [x] Yes
- [ ] No

codecov[bot]: # [Codecov](https://codecov.io/gh/NethermindEth/juno/pull/216?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) Report
> Merging [#216](https://codecov.io/gh/NethermindEth/juno/pull/216?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) (2a432f0) into [main](https://codecov.io/gh/NethermindEth/juno/commit/b1416c97513539c1eef99ca14583d4624c455c58?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) (b1416c9) will **not change** coverage.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff            @@
##              main      #216   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           59        60    +1     
  Lines         4987      5086   +99     
=========================================
+ Hits          4987      5086   +99     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `100.00% <100.00%> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/NethermindEth/juno/pull/216?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth) | Coverage Δ | |
|---|---|---|
| [internal/db/named\_db.go](https://codecov.io/gh/NethermindEth/juno/pull/216/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth#diff-aW50ZXJuYWwvZGIvbmFtZWRfZGIuZ28=) | `100.00% <100.00%> (ø)` | |
| [pkg/rpc/server.go](https://codecov.io/gh/NethermindEth/juno/pull/216/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth#diff-cGtnL3JwYy9zZXJ2ZXIuZ28=) | `100.00% <0.00%> (ø)` | |
| [pkg/crypto/weierstrass/weierstrass.go](https://codecov.io/gh/NethermindEth/juno/pull/216/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth#diff-cGtnL2NyeXB0by93ZWllcnN0cmFzcy93ZWllcnN0cmFzcy5nbw==) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/NethermindEth/juno/pull/216?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/NethermindEth/juno/pull/216?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth). Last update [b1416c9...2a432f0](https://codecov.io/gh/NethermindEth/juno/pull/216?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NethermindEth).


Patch:
diff --git a/internal/db/named_db.go b/internal/db/named_db.go
new file mode 100644
index 00000000..3432049f
--- /dev/null
+++ b/internal/db/named_db.go
@@ -0,0 +1,197 @@
+package db
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/NethermindEth/juno/internal/log"
+
+	"go.uber.org/zap"
+
+	"github.com/torquem-ch/mdbx-go/mdbx"
+)
+
+var (
+	ErrInternal = errors.New("internal error")
+	ErrNotFound = errors.New("not found error")
+)
+
+var (
+	env    *mdbx.Env
+	logger *zap.SugaredLogger
+)
+
+type NamedDatabase struct {
+	name   string
+	dbi    mdbx.DBI
+	env    *mdbx.Env
+	logger *zap.SugaredLogger
+}
+
+func NewNamedDatabase(name string, dbi mdbx.DBI) *NamedDatabase {
+	return &NamedDatabase{
+		name:   name,
+		dbi:    dbi,
+		env:    env,
+		logger: logger.Named(name),
+	}
+}
+
+func (db *NamedDatabase) Has(key []byte) (bool, error) {
+	var exists bool
+	err := db.env.View(func(txn *mdbx.Txn) error {
+		_, err := txn.Get(db.dbi, key)
+		if err != nil {
+			if mdbx.IsNotFound(err) {
+				exists = false
+				return nil
+			}
+			return err
+		} else {
+			exists = true
+		}
+		return nil
+	})
+	if err != nil {
+		// notest
+		return false, fmt.Errorf("%w: %s", ErrInternal, err.Error())
+	}
+	return exists, nil
+}
+
+func (db *NamedDatabase) Get(key []byte) ([]byte, error) {
+	var value []byte
+	err := db.env.View(func(txn *mdbx.Txn) error {
+		_value, err := txn.Get(db.dbi, key)
+		if err != nil {
+			return err
+		}
+		value = _value
+		return nil
+	})
+	if err != nil {
+		if mdbx.IsNotFound(err) {
+			return nil, ErrNotFound
+		}
+		// notest
+		return nil, fmt.Errorf("%w: %s", ErrInternal, err.Error())
+	}
+	return value, nil
+}
+
+func (db *NamedDatabase) Put(key, value []byte) error {
+	err := db.env.Update(func(txn *mdbx.Txn) error {
+		return txn.Put(db.dbi, key, value, 0)
+	})
+	if err != nil {
+		// notest
+		return fmt.Errorf("%w: %s", ErrInternal, err.Error())
+	}
+	return nil
+}
+
+func (db *NamedDatabase) Delete(key []byte) error {
+	err := db.env.Update(func(txn *mdbx.Txn) error {
+		return txn.Del(db.dbi, key, nil)
+	})
+	if err != nil {
+		// notest
+		return fmt.Errorf("%w: %s", ErrInternal, err.Error())
+	}
+	return nil
+}
+
+func (db *NamedDatabase) NumberOfItems() (uint64, error) {
+	var numberOfItems uint64
+	err := db.env.View(func(txn *mdbx.Txn) error {
+		stats, err := txn.StatDBI(db.dbi)
+		if err != nil {
+			return err
+		}
+		numberOfItems = stats.Entries
+		return nil
+	})
+	if err != nil {
+		// notest
+		return 0, fmt.Errorf("%w: %s", ErrInternal, err.Error())
+	}
+	return numberOfItems, nil
+}
+
+func (db *NamedDatabase) Close() {
+	db.env.CloseDBI(db.dbi)
+}
+
+func (db *NamedDatabase) GetEnv() *mdbx.Env {
+	return db.env
+}
+
+func InitializeDatabaseEnv(path string, optMaxDB uint64, flags uint) error {
+	_env, err := mdbx.NewEnv()
+	if err != nil {
+		// notest
+		return fmt.Errorf("%w: %s", ErrInternal, err.Error())
+	}
+	err = _env.SetOption(mdbx.OptMaxDB, optMaxDB)
+	if err != nil {
+		// notest
+		return fmt.Errorf("%w: %s", ErrInternal, err.Error())
+	}
+	err = _env.SetGeometry(268435456, 268435456, 25769803776, 268435456, 268435456, 4096)
+	if err != nil {
+		// notest
+		return fmt.Errorf("%w: %s", ErrInternal, err.Error())
+	}
+	err = _env.Open(path, flags|mdbx.Exclusive, 0o664)
+	if err != nil {
+		// notest
+		return fmt.Errorf("%w: %s", ErrInternal, err.Error())
+	}
+	env = _env
+	logger = log.Default.Named("Database")
+	return nil
+}
+
+func GetDatabase(name string) (*NamedDatabase, error) {
+	var ndb *NamedDatabase
+	err := env.Update(func(txn *mdbx.Txn) error {
+		databases, err := txn.ListDBI()
+		if err != nil {
+			// notest
+			logger.Error(err)
+			return fmt.Errorf("%w: %s", ErrInternal, err.Error())
+		}
+		for _, db := range databases {
+			// Database already exists
+			if db == name {
+				// Open the DBI
+				// notest
+				dbi, err := txn.OpenDBISimple(name, 0)
+				if err != nil {
+					return fmt.Errorf("%w: %s", ErrInternal, err.Error())
+				}
+				ndb = NewNamedDatabase(name, dbi)
+
+				return nil
+			}
+		}
+		// Create the DBI
+		dbi, err := txn.CreateDBI(name)
+		if err != nil {
+			// notest
+			logger.With("error", err).Error("error creating a dbi")
+			return fmt.Errorf("%w: %s", ErrInternal, err.Error())
+		}
+		logger.With("database", name).Info("new named database")
+		ndb = NewNamedDatabase(name, dbi)
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+	return ndb, nil
+}
+
+func IsNotFound(err error) bool {
+	return errors.Is(err, ErrNotFound)
+}


Test patch:
diff --git a/internal/db/named_db_test.go b/internal/db/named_db_test.go
new file mode 100644
index 00000000..8a696bdc
--- /dev/null
+++ b/internal/db/named_db_test.go
@@ -0,0 +1,161 @@
+package db
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+)
+
+func TestNamedDatabase_Has(t *testing.T) {
+	dbs := initDatabases(t, 2)
+	defer closeDatabases(dbs)
+	key := []byte("key")
+	value := []byte("value")
+	err := dbs[0].Put(key, value)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if exists, err := dbs[0].Has(key); err != nil {
+		t.Errorf("unexpected error: %s", err)
+	} else if !exists {
+		t.Errorf("the key must exist")
+	}
+	if exists, err := dbs[1].Has(key); err != nil {
+		t.Errorf("unexpected error: %s", err)
+	} else if exists {
+		t.Errorf("the key must not exist")
+	}
+}
+
+func TestNamedDatabase_Get(t *testing.T) {
+	dbs := initDatabases(t, 2)
+	defer closeDatabases(dbs)
+	db1 := dbs[0]
+	db2 := dbs[1]
+	key := []byte("key")
+	value1 := []byte("value1")
+	value2 := []byte("value2")
+	if err := db1.Put(key, value1); err != nil {
+		t.Errorf("unexpected error during insert into database: %s", err)
+	}
+	if err := db2.Put(key, value2); err != nil {
+		t.Errorf("unexpected error during insert into database: %s", err)
+	}
+	if outValue1, err := db1.Get(key); err != nil {
+		t.Errorf("unexpected error searching the value on the database: %s", err)
+	} else if bytes.Compare(outValue1, value1) != 0 {
+		t.Errorf("values are different after Put & Get operations: %s, want: %s", outValue1, value2)
+	}
+	if outValue2, err := db2.Get(key); err != nil {
+		t.Errorf("unexpected error during search into database: %s", err)
+	} else if bytes.Compare(outValue2, value2) != 0 {
+		t.Errorf("values are different after Put & Get operations: %s, want: %s", outValue2, value2)
+	}
+}
+
+func TestNamedDatabase_Get_NotFound(t *testing.T) {
+	dbs := initDatabases(t, 1)
+	defer closeDatabases(dbs)
+
+	db := dbs[0]
+	value, err := db.Get([]byte("key"))
+	if err == nil || !IsNotFound(err) {
+		t.Errorf("error must be an ErrNotFound")
+	}
+	if value != nil {
+		t.Errorf("if ErrNotFound is returned, then the value must be nil")
+	}
+}
+
+func TestNamedDatabase_Delete(t *testing.T) {
+	dbs := initDatabases(t, 2)
+	defer closeDatabases(dbs)
+
+	key := []byte("key")
+	value := []byte("value")
+	for _, db := range dbs {
+		if err := db.Put(key, value); err != nil {
+			t.Error(err)
+		}
+	}
+	if err := dbs[1].Delete(key); err != nil {
+		t.Error(err)
+	}
+	if exists, err := dbs[1].Has(key); err != nil {
+		t.Error(err)
+	} else if exists {
+		t.Errorf("key exists after deletion")
+	}
+	if exists, err := dbs[0].Has(key); err != nil {
+		t.Error(err)
+	} else if !exists {
+		t.Errorf("key does no exist")
+	}
+}
+
+func TestNamedDatabase_NumberOfItems(t *testing.T) {
+	db := initDatabases(t, 1)[0]
+	defer db.Close()
+
+	assertNumberOfItems := func(value uint64) {
+		count, err := db.NumberOfItems()
+		if err != nil {
+			t.Errorf("unexpected error during getting the number of items")
+		}
+		if count != value {
+			t.Errorf("number of items does not match, %d, want: %d", count, value)
+		}
+	}
+
+	assertNumberOfItems(0)
+
+	for i := 0; i < 10; i++ {
+		err := db.Put([]byte(fmt.Sprintf("key_%d", i)), []byte("value"))
+		if err != nil {
+			t.Error(err)
+		}
+	}
+
+	assertNumberOfItems(10)
+
+	for i := 0; i < 5; i++ {
+		err := db.Delete([]byte(fmt.Sprintf("key_%d", i)))
+		if err != nil {
+			t.Error(err)
+		}
+	}
+
+	assertNumberOfItems(5)
+}
+
+func TestNamedDatabase_GetEnv(t *testing.T) {
+	dbs := initDatabases(t, 10)
+	defer closeDatabases(dbs)
+
+	for _, db := range dbs {
+		if env != db.GetEnv() {
+			t.Errorf("unexpected env")
+		}
+	}
+}
+
+func initDatabases(t *testing.T, count int) []*NamedDatabase {
+	out := make([]*NamedDatabase, count)
+	err := InitializeDatabaseEnv(t.TempDir(), uint64(count), 0)
+	if err != nil {
+		t.Fatal(err)
+	}
+	for i := 0; i < count; i++ {
+		out[i], err = GetDatabase(fmt.Sprintf("db_%d", i))
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+	return out
+}
+
+func closeDatabases(dbs []*NamedDatabase) {
+	for _, db := range dbs {
+		db.Close()
+	}
+}