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(collections): IndexedMap #14397

Merged
merged 43 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4619c3c
add: init pair
Dec 14, 2022
39e3d8f
Merge branch 'main' into tip/collections-pairkeys
Dec 16, 2022
f9f6773
add: refactor ranging
Dec 16, 2022
28bd97e
remove useless test
Dec 16, 2022
a1861f9
add: testing + proper pair impl
Dec 16, 2022
d75e33c
Merge branch 'main' into tip/collections-pairkeys
Dec 19, 2022
e683ac0
Merge branch 'main' into tip/collections-pairkeys
Dec 20, 2022
5044359
add: tmp
Dec 21, 2022
8ec3d87
add: pair key accessor
Dec 21, 2022
11ee08b
Merge branch 'tip/collections-pairkeys' into tip/indexed_map
Dec 21, 2022
dd6d29b
add: MultiIndex tests
Dec 21, 2022
f43b832
add: unique index tests
Dec 22, 2022
8ebdfd7
change: unique index propagates pk
Dec 22, 2022
036a7ee
change: propagate primary key in indexes multi
Dec 22, 2022
6060a4a
Merge branch 'main' into tip/collections-pairkeys
Dec 31, 2022
638c582
chore: merge
Dec 31, 2022
f3772f0
Merge branch 'tip/collections-pairkeys' into tip/indexed_map
Dec 31, 2022
6b71609
chore: merge
Dec 31, 2022
61d5a00
add: has method
Dec 31, 2022
fc1db55
Merge branch 'main' into tip/indexed_map
Jan 13, 2023
96b7c88
merge main
Jan 13, 2023
b1ddef0
Merge branch 'main' into tip/indexed_map
testinginprod Jan 17, 2023
f20ce08
chore: merge main
Jan 17, 2023
50acaa9
chore: merge main2
Jan 17, 2023
399a6b4
change: make indexes generic
Jan 18, 2023
7d3f205
change indexed map test to depend on generic unique index
Jan 18, 2023
71e2242
change move unique index to a separate pkg
Jan 18, 2023
1507e38
add: generic multi index testing
Jan 19, 2023
0489736
chore: improve coverage
Jan 19, 2023
b404df7
chore: add indexes unique testing
Jan 19, 2023
1adc55c
remove: specific indexes
Jan 19, 2023
99d5140
Merge branch 'main' into tip/indexed_map
testinginprod Jan 19, 2023
4e4d0d8
chore: CHANGELOG.md
Jan 19, 2023
c16ddba
Merge branch 'main' into tip/indexed_map
Jan 23, 2023
6934ebf
chore: update core deps
Jan 23, 2023
67b6509
chore: try to make all more easy to understand
Jan 23, 2023
d98c580
Merge branch 'main' into tip/indexed_map
Jan 24, 2023
360727e
Merge branch 'main' into tip/indexed_map
tac0turtle Jan 27, 2023
207c4be
Merge branch 'main' into tip/indexed_map
Jan 27, 2023
e7088c3
chore: cleanups
Jan 27, 2023
7e34f77
docs collections/indexes_generic_multi.go
testinginprod Jan 27, 2023
838295e
Update collections/indexed_map.go
testinginprod Jan 27, 2023
c7fc993
chore: docs
Jan 27, 2023
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
3 changes: 2 additions & 1 deletion collections/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [#14351](https://github.com/cosmos/cosmos-sdk/pull/14351) Add keyset
* [#14364](https://github.com/cosmos/cosmos-sdk/pull/14364) Add sequence
* [#14468](https://github.com/cosmos/cosmos-sdk/pull/14468) Add Map.IterateRaw API.
* [#14310](https://github.com/cosmos/cosmos-sdk/pull/14310) Add Pair keys
* [#14310](https://github.com/cosmos/cosmos-sdk/pull/14310) Add Pair keys
* [#14397](https://github.com/cosmos/cosmos-sdk/pull/14397) Add IndexedMap
Comment on lines +38 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH these changelog entries are superfluous -- as a dev, they tell me nothing useful.

Honestly, we can probably have a single collections CL entry that outlines all the additions and changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it makes sense to me. When we release the first "stable" version we can add a comprehensive list of the features in the changelog, and then apply the classic by PR changelog after the first release.

2 changes: 2 additions & 0 deletions collections/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ var (
ErrNotFound = errors.New("collections: not found")
// ErrEncoding is returned when something fails during key or value encoding/decoding.
ErrEncoding = errors.New("collections: encoding error")
// ErrConflict is returned when there are conflicts, for example in UniqueIndex.
ErrConflict = errors.New("collections: conflict")
)

// collection is the interface that all collections support. It will eventually
Expand Down
1 change: 0 additions & 1 deletion collections/collections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ func (t testStore) Has(key []byte) (bool, error) {

func (t testStore) Set(key, value []byte) error {
return t.db.Set(key, value)

}

func (t testStore) Delete(key []byte) error {
Expand Down
106 changes: 105 additions & 1 deletion collections/colltest/codec.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package colltest

import (
"encoding/json"
"fmt"
"reflect"
"testing"

"cosmossdk.io/collections"

"github.com/stretchr/testify/require"
"testing"
)

// TestKeyCodec asserts the correct behaviour of a KeyCodec over the type T.
Expand All @@ -21,6 +26,7 @@ func TestKeyCodec[T any](t *testing.T, keyCodec collections.KeyCodec[T], key T)
pairKey := collections.Join(key, "TEST")
buffer = make([]byte, pairCodec.Size(pairKey))
written, err = pairCodec.Encode(buffer, pairKey)
require.Equal(t, len(buffer), written, "the pair buffer should have been fully written")
require.NoError(t, err)
read, decodedPairKey, err := pairCodec.Decode(buffer)
require.NoError(t, err)
Expand Down Expand Up @@ -53,3 +59,101 @@ func TestValueCodec[T any](t *testing.T, encoder collections.ValueCodec[T], valu

_ = encoder.Stringify(value)
}

// MockValueCodec returns a mock of collections.ValueCodec for type T, it
// can be used for collections Values testing. It also supports interfaces.
// For the interfaces cases, in order for an interface to be decoded it must
// have been encoded first. Not concurrency safe.
// EG:
// Let's say the value is interface Animal
// if I want to decode Dog which implements Animal, then I need to first encode
// it in order to make the type known by the MockValueCodec.
func MockValueCodec[T any]() collections.ValueCodec[T] {
typ := reflect.ValueOf(new(T)).Elem().Type()
isInterface := false
if typ.Kind() == reflect.Interface {
isInterface = true
}
return &mockValueCodec[T]{
isInterface: isInterface,
seenTypes: map[string]reflect.Type{},
valueType: fmt.Sprintf("%s.%s", typ.PkgPath(), typ.Name()),
}
}

type mockValueJSON struct {
TypeName string `json:"type_name"`
Value json.RawMessage `json:"value"`
}

type mockValueCodec[T any] struct {
isInterface bool
seenTypes map[string]reflect.Type
valueType string
}

func (m mockValueCodec[T]) Encode(value T) ([]byte, error) {
typeName := m.getTypeName(value)
valueBytes, err := json.Marshal(value)
if err != nil {
return nil, err
}

return json.Marshal(mockValueJSON{
TypeName: typeName,
Value: valueBytes,
})
}

func (m mockValueCodec[T]) Decode(b []byte) (t T, err error) {
wrappedValue := mockValueJSON{}
err = json.Unmarshal(b, &wrappedValue)
if err != nil {
return
}
if !m.isInterface {
err = json.Unmarshal(wrappedValue.Value, &t)
return t, err
}

typ, exists := m.seenTypes[wrappedValue.TypeName]
if !exists {
return t, fmt.Errorf("uknown type %s, you're dealing with interfaces... in order to make the interface types known for the MockValueCodec, you need to first encode them", wrappedValue.TypeName)
}

newT := reflect.New(typ).Interface()
err = json.Unmarshal(wrappedValue.Value, newT)
if err != nil {
return t, err
}

iface := new(T)
reflect.ValueOf(iface).Elem().Set(reflect.ValueOf(newT).Elem())
return *iface, nil
}

func (m mockValueCodec[T]) EncodeJSON(value T) ([]byte, error) {
return m.Encode(value)
}

func (m mockValueCodec[T]) DecodeJSON(b []byte) (T, error) {
return m.Decode(b)
}

func (m mockValueCodec[T]) Stringify(value T) string {
return fmt.Sprintf("%#v", value)
}

func (m mockValueCodec[T]) ValueType() string {
return m.valueType
}

func (m mockValueCodec[T]) getTypeName(value T) string {
if !m.isInterface {
return m.valueType
}
typ := reflect.TypeOf(value)
name := fmt.Sprintf("%s.%s", typ.PkgPath(), typ.Name())
m.seenTypes[name] = typ
return name
}
48 changes: 48 additions & 0 deletions collections/colltest/codec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package colltest

import "testing"

type animal interface {
name() string
}

type dog struct {
Name string `json:"name"`
BarksLoudly bool `json:"barks_loudly"`
}

type cat struct {
Name string `json:"name"`
Scratches bool `json:"scratches"`
}

func (d *cat) name() string { return d.Name }

func (d dog) name() string { return d.Name }

func TestMockValueCodec(t *testing.T) {
t.Run("primitive type", func(t *testing.T) {
x := MockValueCodec[string]()
TestValueCodec(t, x, "hello")
})

t.Run("struct type", func(t *testing.T) {
x := MockValueCodec[dog]()
TestValueCodec(t, x, dog{
Name: "kernel",
BarksLoudly: true,
})
})

t.Run("interface type", func(t *testing.T) {
x := MockValueCodec[animal]()
TestValueCodec[animal](t, x, dog{
Name: "kernel",
BarksLoudly: true,
})
TestValueCodec[animal](t, x, &cat{
Name: "echo",
Scratches: true,
})
})
}
49 changes: 49 additions & 0 deletions collections/colltest/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package colltest

import (
"context"

"cosmossdk.io/core/store"
db "github.com/cosmos/cosmos-db"
)

// MockStore returns a mock store.KVStoreService and a mock context.Context.
// They can be used to test collections.
func MockStore() (store.KVStoreService, context.Context) {
kv := db.NewMemDB()
return &testStore{kv}, context.Background()
}

type testStore struct {
db db.DB
}

func (t testStore) OpenKVStore(ctx context.Context) store.KVStore {
return t
}

func (t testStore) Get(key []byte) ([]byte, error) {
return t.db.Get(key)
}

func (t testStore) Has(key []byte) (bool, error) {
return t.db.Has(key)
}

func (t testStore) Set(key, value []byte) error {
return t.db.Set(key, value)
}

func (t testStore) Delete(key []byte) error {
return t.db.Delete(key)
}

func (t testStore) Iterator(start, end []byte) (store.Iterator, error) {
return t.db.Iterator(start, end)
}

func (t testStore) ReverseIterator(start, end []byte) (store.Iterator, error) {
return t.db.ReverseIterator(start, end)
}

var _ store.KVStore = testStore{}
3 changes: 2 additions & 1 deletion collections/correctness_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package collections_test

import (
"testing"

"cosmossdk.io/collections"
"cosmossdk.io/collections/colltest"
"testing"
)

func TestKeyCorrectness(t *testing.T) {
Expand Down
12 changes: 6 additions & 6 deletions collections/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ type jsonMapEntry struct {
}

func (m Map[K, V]) validateGenesis(reader io.Reader) error {
return m.doDecodeJson(reader, func(key K, value V) error {
return m.doDecodeJSON(reader, func(key K, value V) error {
return nil
})
}

func (m Map[K, V]) importGenesis(ctx context.Context, reader io.Reader) error {
return m.doDecodeJson(reader, func(key K, value V) error {
return m.doDecodeJSON(reader, func(key K, value V) error {
return m.Set(ctx, key, value)
})
}
Expand Down Expand Up @@ -95,7 +95,7 @@ func (m Map[K, V]) exportGenesis(ctx context.Context, writer io.Writer) error {
return err
}

func (m Map[K, V]) doDecodeJson(reader io.Reader, onEntry func(key K, value V) error) error {
func (m Map[K, V]) doDecodeJSON(reader io.Reader, onEntry func(key K, value V) error) error {
decoder := json.NewDecoder(reader)
token, err := decoder.Token()
if err != nil {
Expand All @@ -107,14 +107,14 @@ func (m Map[K, V]) doDecodeJson(reader io.Reader, onEntry func(key K, value V) e
}

for decoder.More() {
var rawJson json.RawMessage
err := decoder.Decode(&rawJson)
var rawJSON json.RawMessage
err := decoder.Decode(&rawJSON)
if err != nil {
return err
}

var mapEntry jsonMapEntry
err = json.Unmarshal(rawJson, &mapEntry)
err = json.Unmarshal(rawJSON, &mapEntry)
if err != nil {
return err
}
Expand Down
Loading