forked from cosmos/cosmos-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindexed_map.go
217 lines (194 loc) · 7.36 KB
/
indexed_map.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
package collections
import (
"context"
"errors"
"fmt"
"reflect"
"cosmossdk.io/collections/codec"
)
// Indexes represents a type which groups multiple Index
// of one Value saved with the provided PrimaryKey.
// Indexes is just meant to be a struct containing all
// the indexes to maintain relationship for.
type Indexes[PrimaryKey, Value any] interface {
// IndexesList is implemented by the Indexes type
// and returns all the grouped Index of Value.
IndexesList() []Index[PrimaryKey, Value]
}
// Index represents an index of the Value indexed using the type PrimaryKey.
type Index[PrimaryKey, Value any] interface {
// Reference creates a reference between the provided primary key and value.
// It provides a lazyOldValue function that if called will attempt to fetch
// the previous old value, returns ErrNotFound if no value existed.
Reference(ctx context.Context, pk PrimaryKey, newValue Value, lazyOldValue func() (Value, error)) error
// Unreference removes the reference between the primary key and value.
// If error is ErrNotFound then it means that the value did not exist before.
Unreference(ctx context.Context, pk PrimaryKey, lazyOldValue func() (Value, error)) error
}
// IndexedMap works like a Map but creates references between fields of Value and its PrimaryKey.
// These relationships are expressed and maintained using the Indexes type.
// Internally IndexedMap can be seen as a partitioned collection, one partition
// is a Map[PrimaryKey, Value], that maintains the object, the second
// are the Indexes.
type IndexedMap[PrimaryKey, Value, Idx any] struct {
Indexes Idx
computedIndexes []Index[PrimaryKey, Value]
m Map[PrimaryKey, Value]
}
// NewIndexedMapSafe behaves like NewIndexedMap but returns errors.
func NewIndexedMapSafe[K, V, I any](
schema *SchemaBuilder,
prefix Prefix,
name string,
pkCodec codec.KeyCodec[K],
valueCodec codec.ValueCodec[V],
indexes I,
) (im *IndexedMap[K, V, I], err error) {
var indexesList []Index[K, V]
indexesImpl, ok := any(indexes).(Indexes[K, V])
if ok {
indexesList = indexesImpl.IndexesList()
} else {
// if does not implement Indexes, then we try to infer using reflection
indexesList, err = tryInferIndexes[I, K, V](indexes)
if err != nil {
return nil, fmt.Errorf("unable to infer indexes using reflection, consider implementing Indexes interface: %w", err)
}
}
return &IndexedMap[K, V, I]{
computedIndexes: indexesList,
Indexes: indexes,
m: NewMap(schema, prefix, name, pkCodec, valueCodec),
}, nil
}
var (
// testing sentinel errors
errNotStruct = errors.New("wanted struct or pointer to a struct")
errNotIndex = errors.New("field is not an index implementation")
)
func tryInferIndexes[I, K, V any](indexes I) ([]Index[K, V], error) {
typ := reflect.TypeOf(indexes)
v := reflect.ValueOf(indexes)
// check if struct or pointer to a struct
if typ.Kind() != reflect.Struct && (typ.Kind() != reflect.Pointer || typ.Elem().Kind() != reflect.Struct) {
return nil, fmt.Errorf("%w: type %v", errNotStruct, typ)
}
// dereference
if typ.Kind() == reflect.Pointer {
v = v.Elem()
}
indexesImpl := make([]Index[K, V], v.NumField())
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
index, ok := field.Interface().(Index[K, V])
if !ok {
return nil, fmt.Errorf("%w: field number %d", errNotIndex, i)
}
indexesImpl[i] = index
}
return indexesImpl, nil
}
// NewIndexedMap instantiates a new IndexedMap. Accepts a SchemaBuilder, a Prefix,
// a humanized name that defines the name of the collection, the primary key codec
// which is basically what IndexedMap uses to encode the primary key to bytes,
// the value codec which is what the IndexedMap uses to encode the value.
// Then it expects the initialized indexes. Reflection is used to infer the
// indexes, Indexes can optionally be implemented to be explicit. Panics
// on failure to create indexes. If you want an erroring API use NewIndexedMapSafe.
func NewIndexedMap[PrimaryKey, Value, Idx any](
schema *SchemaBuilder,
prefix Prefix,
name string,
pkCodec codec.KeyCodec[PrimaryKey],
valueCodec codec.ValueCodec[Value],
indexes Idx,
) *IndexedMap[PrimaryKey, Value, Idx] {
im, err := NewIndexedMapSafe(schema, prefix, name, pkCodec, valueCodec, indexes)
if err != nil {
panic(err)
}
return im
}
// Get gets the object given its primary key.
func (m *IndexedMap[PrimaryKey, Value, Idx]) Get(ctx context.Context, pk PrimaryKey) (Value, error) {
return m.m.Get(ctx, pk)
}
// Iterate allows to iterate over the objects given a Ranger of the primary key.
func (m *IndexedMap[PrimaryKey, Value, Idx]) Iterate(ctx context.Context, ranger Ranger[PrimaryKey]) (Iterator[PrimaryKey, Value], error) {
return m.m.Iterate(ctx, ranger)
}
// Has reports if exists a value with the provided primary key.
func (m *IndexedMap[PrimaryKey, Value, Idx]) Has(ctx context.Context, pk PrimaryKey) (bool, error) {
return m.m.Has(ctx, pk)
}
// Set maps the value using the primary key. It will also iterate every index and instruct them to
// add or update the indexes.
func (m *IndexedMap[PrimaryKey, Value, Idx]) Set(ctx context.Context, pk PrimaryKey, value Value) error {
err := m.ref(ctx, pk, value)
if err != nil {
return err
}
return m.m.Set(ctx, pk, value)
}
// Remove removes the value associated with the primary key from the map. Then
// it iterates over all the indexes and instructs them to remove all the references
// associated with the removed value.
func (m *IndexedMap[PrimaryKey, Value, Idx]) Remove(ctx context.Context, pk PrimaryKey) error {
err := m.unref(ctx, pk)
if err != nil {
return err
}
return m.m.Remove(ctx, pk)
}
// Walk applies the same semantics as Map.Walk.
func (m *IndexedMap[PrimaryKey, Value, Idx]) Walk(ctx context.Context, ranger Ranger[PrimaryKey], walkFunc func(key PrimaryKey, value Value) (stop bool, err error)) error {
return m.m.Walk(ctx, ranger, walkFunc)
}
// IterateRaw iterates the IndexedMap using raw bytes keys. Follows the same semantics as Map.IterateRaw
func (m *IndexedMap[PrimaryKey, Value, Idx]) IterateRaw(ctx context.Context, start, end []byte, order Order) (Iterator[PrimaryKey, Value], error) {
return m.m.IterateRaw(ctx, start, end, order)
}
func (m *IndexedMap[PrimaryKey, Value, Idx]) KeyCodec() codec.KeyCodec[PrimaryKey] {
return m.m.KeyCodec()
}
func (m *IndexedMap[PrimaryKey, Value, Idx]) ValueCodec() codec.ValueCodec[Value] {
return m.m.ValueCodec()
}
func (m *IndexedMap[PrimaryKey, Value, Idx]) ref(ctx context.Context, pk PrimaryKey, value Value) error {
for _, index := range m.computedIndexes {
err := index.Reference(ctx, pk, value, cachedGet[PrimaryKey, Value](ctx, m, pk))
if err != nil {
return err
}
}
return nil
}
func (m *IndexedMap[PrimaryKey, Value, Idx]) unref(ctx context.Context, pk PrimaryKey) error {
for _, index := range m.computedIndexes {
err := index.Unreference(ctx, pk, cachedGet[PrimaryKey, Value](ctx, m, pk))
if err != nil {
return err
}
}
return nil
}
// cachedGet returns a function that gets the value V, given the key K but
// returns always the same result on multiple calls.
func cachedGet[K, V any, M interface {
Get(ctx context.Context, key K) (V, error)
}](ctx context.Context, m M, key K,
) func() (V, error) {
var (
value V
err error
calledOnce bool
)
return func() (V, error) {
if calledOnce {
return value, err
}
value, err = m.Get(ctx, key)
calledOnce = true
return value, err
}
}