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): indexes for IndexedMap #14706

Merged
merged 55 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 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
92ecb87
add: a lot + examples
Jan 19, 2023
77a3e79
create helpers file for indexes + remove collect values from indexedmap
Jan 23, 2023
c16ddba
Merge branch 'main' into tip/indexed_map
Jan 23, 2023
bac20cd
Merge branch 'tip/indexed_map' into tip/indexed_map_indexes
Jan 23, 2023
6934ebf
chore: update core deps
Jan 23, 2023
9f6bbff
Merge branch 'tip/indexed_map' into tip/indexed_map_indexes
Jan 23, 2023
67b6509
chore: try to make all more easy to understand
Jan 23, 2023
830979e
Merge branch 'tip/indexed_map' into tip/indexed_map_indexes
Jan 23, 2023
d98c580
Merge branch 'main' into tip/indexed_map
Jan 24, 2023
ea601c6
Merge branch 'tip/indexed_map' into tip/indexed_map_indexes
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
2292420
Merge branch 'tip/indexed_map' into tip/indexed_map_indexes
Jan 27, 2023
e4fbc65
Merge branch 'main' into tip/indexed_map_indexes
testinginprod Jan 27, 2023
aefd405
Merge branch 'main' into tip/indexed_map_indexes
testinginprod Feb 2, 2023
16cd2b3
remove: examples since we have docs
Feb 2, 2023
32cb2db
make unique index iterator implement the Iterator interface
Feb 2, 2023
4858e82
Merge branch 'main' into tip/indexed_map_indexes
testinginprod Feb 3, 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: 3 additions & 0 deletions collections/indexes/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package indexes contains the most common indexes types to be used with a collections.IndexedMap.
// It also contains specialised helper functions to collect and query efficiently an index.
package indexes
110 changes: 110 additions & 0 deletions collections/indexes/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package indexes

import (
"context"
"cosmossdk.io/collections"
)

// Iterator defines the minimum set of methods of an index iterator
// required to work with the helpers.
type Iterator[K any] interface {
// PrimaryKey returns the iterator current primary key.
PrimaryKey() (K, error)
// Next advances the iterator by one element.
Next()
// Valid asserts if the Iterator is valid.
Valid() bool
// Close closes the iterator.
Close() error
}

// CollectKeyValues collects all the keys and the values of an indexed map index iterator.
// The Iterator is fully consumed and closed.
func CollectKeyValues[K, V any, I Iterator[K], Idx collections.Indexes[K, V]](
ctx context.Context,
indexedMap *collections.IndexedMap[K, V, Idx],
iter I) (kvs []collections.KeyValue[K, V], err error) {
err = ScanKeyValues(ctx, indexedMap, iter, func(kv collections.KeyValue[K, V]) bool {
kvs = append(kvs, kv)
return false
})
return
}

// ScanKeyValues calls the do function on every record found, in the indexed map
// from the index iterator. Returning false stops the iteration.
// The Iterator is closed when this function exits.
func ScanKeyValues[K, V any, I Iterator[K], Idx collections.Indexes[K, V]](
ctx context.Context,
indexedMap *collections.IndexedMap[K, V, Idx],
iter I,
do func(kv collections.KeyValue[K, V]) (stop bool)) (err error) {

defer iter.Close()

for ; iter.Valid(); iter.Next() {
pk, err := iter.PrimaryKey()
if err != nil {
return err
}

value, err := indexedMap.Get(ctx, pk)
if err != nil {
return err
}

kv := collections.KeyValue[K, V]{
Key: pk,
Value: value,
}

if do(kv) {
break
}
}

return nil
}

// CollectValues collects all the values from an Index iterator and the IndexedMap.
// Closes the Iterator.
func CollectValues[K, V any, I Iterator[K], Idx collections.Indexes[K, V]](
ctx context.Context,
indexedMap *collections.IndexedMap[K, V, Idx],
iter I) (values []V, err error) {
err = ScanValues(ctx, indexedMap, iter, func(value V) (stop bool) {
values = append(values, value)
return false
})
return
}

// ScanValues collects all the values from an Index iterator and the IndexedMap in a lazy way.
// The iterator is closed when this function exits.
func ScanValues[K, V any, I Iterator[K], Idx collections.Indexes[K, V]](
ctx context.Context,
indexedMap *collections.IndexedMap[K, V, Idx],
iter I,
f func(value V) (stop bool),
) error {
defer iter.Close()

for ; iter.Valid(); iter.Next() {
key, err := iter.PrimaryKey()
if err != nil {
return err
}

value, err := indexedMap.Get(ctx, key)
if err != nil {
return err
}

stop := f(value)
if stop {
return nil
}
}

return nil
}
87 changes: 87 additions & 0 deletions collections/indexes/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package indexes

import (
"cosmossdk.io/collections"
"github.com/stretchr/testify/require"
"testing"
)

func TestHelpers(t *testing.T) {
// uses MultiPair scenario.
// We store balances as:
// Key: Pair[Address=string, Denom=string] => Value: Amount=uint64

sk, ctx := deps()
sb := collections.NewSchemaBuilder(sk)

keyCodec := collections.PairKeyCodec(collections.StringKey, collections.StringKey)
indexedMap := collections.NewIndexedMap(
sb,
collections.NewPrefix("balances"), "balances",
keyCodec,
collections.Uint64Value,
balanceIndex{
Denom: NewMultiPair[Amount](sb, collections.NewPrefix("denom_index"), "denom_index", keyCodec),
},
)

err := indexedMap.Set(ctx, collections.Join("address1", "atom"), 100)
require.NoError(t, err)

err = indexedMap.Set(ctx, collections.Join("address1", "osmo"), 200)
require.NoError(t, err)

err = indexedMap.Set(ctx, collections.Join("address2", "osmo"), 300)
require.NoError(t, err)

// test collect values
iter, err := indexedMap.Indexes.Denom.MatchExact(ctx, "osmo")
require.NoError(t, err)

values, err := CollectValues(ctx, indexedMap, iter)
require.NoError(t, err)
require.Equal(t, []Amount{200, 300}, values)

// test collect key values

iter, err = indexedMap.Indexes.Denom.MatchExact(ctx, "osmo")
require.NoError(t, err)
kvs, err := CollectKeyValues(ctx, indexedMap, iter)
require.NoError(t, err)

require.Equal(t, []collections.KeyValue[collections.Pair[Address, Denom], Amount]{
{
Key: collections.Join("address1", "osmo"),
Value: 200,
},
{
Key: collections.Join("address2", "osmo"),
Value: 300,
},
}, kvs)

// test scan values with early termination
iter, err = indexedMap.Indexes.Denom.MatchExact(ctx, "osmo")
require.NoError(t, err)
numCalled := 0
err = ScanValues(ctx, indexedMap, iter, func(v Amount) bool {
require.Equal(t, Amount(200), v)
numCalled++
require.Equal(t, numCalled, 1)
return true // says to stop
})
require.NoError(t, err)

// test scan kv with early termination
iter, err = indexedMap.Indexes.Denom.MatchExact(ctx, "osmo")
require.NoError(t, err)
numCalled = 0
err = ScanKeyValues(ctx, indexedMap, iter, func(kv collections.KeyValue[collections.Pair[Address, Denom], Amount]) bool {
require.Equal(t, Amount(200), kv.Value)
require.Equal(t, collections.Join("address1", "osmo"), kv.Key)
numCalled++
require.Equal(t, numCalled, 1)
return true // says to stop
})
require.NoError(t, err)
}
53 changes: 53 additions & 0 deletions collections/indexes/indexes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package indexes

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

// TODO remove this when we add testStore to core/store.

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{}

func deps() (store.KVStoreService, context.Context) {
kv := db.NewMemDB()
return &testStore{kv}, context.Background()
}

type company struct {
City string
Vat uint64
}
104 changes: 104 additions & 0 deletions collections/indexes/multi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package indexes

import (
"context"
"cosmossdk.io/collections"
)

// Multi defines the most common index. It can be used to create a reference between
// a field of value and its primary key. Multiple primary keys can be mapped to the same
// reference key as the index does not enforce uniqueness constraints.
type Multi[ReferenceKey, PrimaryKey, Value any] collections.GenericMultiIndex[ReferenceKey, PrimaryKey, PrimaryKey, Value]

// NewMulti instantiates a new Multi instance given a schema,
// a Prefix, the humanized name for the index, the reference key key codec
// and the primary key key codec. The getRefKeyFunc is a function that
// given the primary key and value returns the referencing key.
func NewMulti[ReferenceKey, PrimaryKey, Value any](
schema *collections.SchemaBuilder,
prefix collections.Prefix,
name string,
refCodec collections.KeyCodec[ReferenceKey],
pkCodec collections.KeyCodec[PrimaryKey],
getRefKeyFunc func(pk PrimaryKey, value Value) (ReferenceKey, error),
) *Multi[ReferenceKey, PrimaryKey, Value] {
i := collections.NewGenericMultiIndex(
schema, prefix, name, refCodec, pkCodec,
func(pk PrimaryKey, value Value) ([]collections.IndexReference[ReferenceKey, PrimaryKey], error) {
ref, err := getRefKeyFunc(pk, value)
if err != nil {
return nil, err
}
return []collections.IndexReference[ReferenceKey, PrimaryKey]{
collections.NewIndexReference(ref, pk),
}, nil
},
)

return (*Multi[ReferenceKey, PrimaryKey, Value])(i)
}

func (m *Multi[ReferenceKey, PrimaryKey, Value]) Reference(ctx context.Context, pk PrimaryKey, newValue Value, oldValue *Value) error {
return (*collections.GenericMultiIndex[ReferenceKey, PrimaryKey, PrimaryKey, Value])(m).Reference(ctx, pk, newValue, oldValue)
}

func (m *Multi[ReferenceKey, PrimaryKey, Value]) Unreference(ctx context.Context, pk PrimaryKey, value Value) error {
return (*collections.GenericMultiIndex[ReferenceKey, PrimaryKey, PrimaryKey, Value])(m).Unreference(ctx, pk, value)
}

func (m *Multi[ReferenceKey, PrimaryKey, Value]) Iterate(ctx context.Context, ranger collections.Ranger[collections.Pair[ReferenceKey, PrimaryKey]]) (MultiIterator[ReferenceKey, PrimaryKey], error) {
iter, err := (*collections.GenericMultiIndex[ReferenceKey, PrimaryKey, PrimaryKey, Value])(m).Iterate(ctx, ranger)
return (MultiIterator[ReferenceKey, PrimaryKey])(iter), err
}

// MatchExact returns a MultiIterator containing all the primary keys referenced by the provided reference key.
func (m *Multi[ReferenceKey, PrimaryKey, Value]) MatchExact(ctx context.Context, refKey ReferenceKey) (MultiIterator[ReferenceKey, PrimaryKey], error) {
return m.Iterate(ctx, collections.NewPrefixedPairRange[ReferenceKey, PrimaryKey](refKey))
}

// MultiIterator is just a KeySetIterator with key as Pair[ReferenceKey, PrimaryKey].
type MultiIterator[ReferenceKey, PrimaryKey any] collections.KeySetIterator[collections.Pair[ReferenceKey, PrimaryKey]]

// PrimaryKey returns the iterator's current primary key.
func (i MultiIterator[ReferenceKey, PrimaryKey]) PrimaryKey() (PrimaryKey, error) {
fullKey, err := i.FullKey()
return fullKey.K2(), err
}

// PrimaryKeys fully consumes the iterator and returns the list of primary keys.
func (i MultiIterator[ReferenceKey, PrimaryKey]) PrimaryKeys() ([]PrimaryKey, error) {
fullKeys, err := i.FullKeys()
if err != nil {
return nil, err
}
pks := make([]PrimaryKey, len(fullKeys))
for i, fullKey := range fullKeys {
pks[i] = fullKey.K2()
}
return pks, nil
}

// FullKey returns the current full reference key as Pair[ReferenceKey, PrimaryKey].
func (i MultiIterator[ReferenceKey, PrimaryKey]) FullKey() (collections.Pair[ReferenceKey, PrimaryKey], error) {
return (collections.KeySetIterator[collections.Pair[ReferenceKey, PrimaryKey]])(i).Key()
}

// FullKeys fully consumes the iterator and returns all the list of full reference keys.
func (i MultiIterator[ReferenceKey, PrimaryKey]) FullKeys() ([]collections.Pair[ReferenceKey, PrimaryKey], error) {
return (collections.KeySetIterator[collections.Pair[ReferenceKey, PrimaryKey]])(i).Keys()
}

// Next advances the iterator.
func (i MultiIterator[ReferenceKey, PrimaryKey]) Next() {
(collections.KeySetIterator[collections.Pair[ReferenceKey, PrimaryKey]])(i).Next()
}

// Valid asserts if the iterator is still valid or not.
func (i MultiIterator[ReferenceKey, PrimaryKey]) Valid() bool {
return (collections.KeySetIterator[collections.Pair[ReferenceKey, PrimaryKey]])(i).Valid()
}

// Close closes the iterator.
func (i MultiIterator[ReferenceKey, PrimaryKey]) Close() error {
return (collections.KeySetIterator[collections.Pair[ReferenceKey, PrimaryKey]])(i).Close()
}
Loading