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): support indexing #20704

Merged
merged 83 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
76f6f32
feat: indexer base types
aaronc Jun 11, 2024
63aeb85
WIP on tests
aaronc Jun 11, 2024
216e8f8
update listener
aaronc Jun 12, 2024
b9fb6c9
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/index…
aaronc Jun 13, 2024
663ed17
rename column to field
aaronc Jun 13, 2024
4311357
delete code, simplify
aaronc Jun 13, 2024
c52655a
add error return
aaronc Jun 13, 2024
46669d3
remove ability to filter subscribed modules - this is a bit dangerous
aaronc Jun 13, 2024
0a47c39
add docs about fields
aaronc Jun 13, 2024
7fd604f
update table and entity language to object
aaronc Jun 13, 2024
4a00094
rename to type
aaronc Jun 13, 2024
0c7f529
add CHANGELOG.md
aaronc Jun 13, 2024
408ddc4
add DecodableModule interface
aaronc Jun 13, 2024
599e7cf
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/index…
aaronc Jun 14, 2024
bc98756
make compatible with go 1.12
aaronc Jun 14, 2024
68d0afc
remove CommitCatchupSync - catch-up design in flux, may be premature …
aaronc Jun 17, 2024
50a8c37
restore validation code
aaronc Jun 17, 2024
dba4b3c
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/index…
aaronc Jun 17, 2024
6bb9c48
WIP on mermaid
aaronc Jun 17, 2024
4d6e54d
mermaid sequence diagram
aaronc Jun 17, 2024
fcaa9b9
update listener docs
aaronc Jun 17, 2024
313a778
cleanup
aaronc Jun 17, 2024
acf6e52
feat(collections): support indexing
aaronc Jun 17, 2024
e8a17e2
Merge branch 'aaronc/indexer-base-validation' into aaronc/collections…
aaronc Jun 17, 2024
3bb5ef6
updates
aaronc Jun 17, 2024
dab02f7
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/index…
aaronc Jun 18, 2024
3606a04
update validation
aaronc Jun 18, 2024
7cf9678
string tests
aaronc Jun 18, 2024
df3cde1
TestKindForGoValue
aaronc Jun 18, 2024
7c7ff79
update validation
aaronc Jun 18, 2024
2eb3ed2
WIP on TestField_Validate
aaronc Jun 18, 2024
8e2db24
simplifications
aaronc Jun 18, 2024
29d5a29
WIP updates
aaronc Jun 18, 2024
36aa92d
updates
aaronc Jun 18, 2024
aa73bd2
updates
aaronc Jun 18, 2024
160c186
update kind tests
aaronc Jun 18, 2024
00dceff
good kind test coverage
aaronc Jun 18, 2024
ccb9ca4
field tests and docs
aaronc Jun 18, 2024
df727e2
enum tests
aaronc Jun 18, 2024
842d420
object type tests
aaronc Jun 18, 2024
4b18658
object type docs
aaronc Jun 18, 2024
40b5d25
object type docs
aaronc Jun 18, 2024
6ed4d2b
revert comment changes
aaronc Jun 18, 2024
3ac7b0a
revert comment changes
aaronc Jun 18, 2024
8f3391b
simplify tests
aaronc Jun 18, 2024
e94e34a
Merge branch 'main' into aaronc/indexer-base-validation
aaronc Jun 18, 2024
e198394
Merge branch 'aaronc/indexer-base-validation' into aaronc/collections…
aaronc Jun 18, 2024
7701ecf
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/colle…
aaronc Jun 18, 2024
5edf810
updates
aaronc Jun 18, 2024
1e1ffb7
refactor into standalone []Field validation methods
aaronc Jun 18, 2024
62c8f35
Merge branch 'aaronc/indexer-base-validation' into aaronc/collections…
aaronc Jun 18, 2024
0ab14f0
updates
aaronc Jun 18, 2024
cdb1758
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/colle…
aaronc Jul 3, 2024
2695659
merge wip updates
aaronc Jul 3, 2024
00b08d9
revert
aaronc Jul 3, 2024
d3e6f60
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/colle…
aaronc Jul 8, 2024
f45bcd7
reduce PR scope
aaronc Jul 8, 2024
dcb58db
refactoring, docs
aaronc Jul 8, 2024
2cec514
refactoring
aaronc Jul 8, 2024
cd9be91
refactoring
aaronc Jul 8, 2024
feab7a1
refactoring
aaronc Jul 8, 2024
d878349
docs
aaronc Jul 8, 2024
826528a
docs
aaronc Jul 8, 2024
a39f8b3
remove go.mod replace
aaronc Jul 9, 2024
64842da
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/colle…
aaronc Jul 9, 2024
73a08cc
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/colle…
aaronc Aug 6, 2024
e8c4607
tidy
aaronc Aug 6, 2024
f6801ca
CHANGELOG.md
aaronc Aug 6, 2024
d0a4266
Merge branch 'main' into aaronc/collections-indexable
aaronc Aug 6, 2024
69fba76
tidy
aaronc Aug 6, 2024
8d031e0
Merge remote-tracking branch 'origin/aaronc/collections-indexable' in…
aaronc Aug 6, 2024
9355a04
Merge branch 'main' into aaronc/collections-indexable
aaronc Aug 7, 2024
eccbf6a
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/colle…
aaronc Aug 19, 2024
333415f
tidy
aaronc Aug 19, 2024
1a3429e
Update collections/codec/indexing.go
aaronc Aug 20, 2024
5791629
Update collections/indexing.go
aaronc Aug 20, 2024
2cf88a6
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/colle…
aaronc Aug 28, 2024
b2c0fe2
go mod tidy
aaronc Aug 28, 2024
c4f9251
update to schema main
aaronc Aug 28, 2024
184da24
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/colle…
aaronc Aug 30, 2024
996eb6a
Merge branch 'main' of github.com:cosmos/cosmos-sdk into aaronc/colle…
aaronc Aug 30, 2024
2c9d4cf
go mod tidy
aaronc Aug 30, 2024
b1610ac
update to schema 0.2.0
aaronc Aug 30, 2024
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
1 change: 1 addition & 0 deletions collections/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
* [#19343](https://github.com/cosmos/cosmos-sdk/pull/19343) Simplify IndexedMap creation by allowing to infer indexes through reflection.
* [#19861](https://github.com/cosmos/cosmos-sdk/pull/19861) Add `NewJSONValueCodec` value codec as an alternative for `codec.CollValue` from the SDK for non protobuf types.
* [#21090](https://github.com/cosmos/cosmos-sdk/pull/21090) Introduces `Quad`, a composite key with four keys.
* [#20704](https://github.com/cosmos/cosmos-sdk/pull/20704) Add `ModuleCodec` method to `Schema` and `HasSchemaCodec` interface in order to support `cosmossdk.io/schema` compatible indexing.

## [v0.4.0](https://github.com/cosmos/cosmos-sdk/releases/tag/collections%2Fv0.4.0)

Expand Down
102 changes: 102 additions & 0 deletions collections/codec/indexing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package codec

import (
"encoding/json"
"fmt"

"cosmossdk.io/schema"
)

// HasSchemaCodec is an interface that all codec's should implement in order
// to properly support indexing. It is not required by KeyCodec or ValueCodec
// in order to preserve backwards compatibility, but a future version of collections
// may make it required and all codec's should aim to implement it. If it is not
// implemented, fallback defaults will be used for indexing that may be sub-optimal.
//
// Implementations of HasSchemaCodec should test that they are conformant using
// schema.ValidateObjectKey or schema.ValidateObjectValue depending on whether
// the codec is a KeyCodec or ValueCodec respectively.
type HasSchemaCodec[T any] interface {
// SchemaCodec returns the schema codec for the collections codec.
SchemaCodec() (SchemaCodec[T], error)
}

// SchemaCodec is a codec that supports converting collection codec values to and
// from schema codec values.
type SchemaCodec[T any] struct {
// Fields are the schema fields that the codec represents. If this is empty,
// it will be assumed that this codec represents no value (such as an item key
// or key set value).
Fields []schema.Field

// ToSchemaType converts a codec value of type T to a value corresponding to
// a schema object key or value (depending on whether this is a key or value
// codec). The returned value should pass validation with schema.ValidateObjectKey
// or schema.ValidateObjectValue with the fields specified in Fields.
// If this function is nil, it will be assumed that T already represents a
// value that conforms to a schema value without any further conversion.
ToSchemaType func(T) (any, error)

// FromSchemaType converts a schema object key or value to T.
// If this function is nil, it will be assumed that T already represents a
// value that conforms to a schema value without any further conversion.
FromSchemaType func(any) (T, error)
}

// KeySchemaCodec gets the schema codec for the provided KeyCodec either
// by casting to HasSchemaCodec or returning a fallback codec.
func KeySchemaCodec[K any](cdc KeyCodec[K]) (SchemaCodec[K], error) {
if indexable, ok := cdc.(HasSchemaCodec[K]); ok {
return indexable.SchemaCodec()
} else {
return FallbackSchemaCodec[K](), nil
}
}

// ValueSchemaCodec gets the schema codec for the provided ValueCodec either
// by casting to HasSchemaCodec or returning a fallback codec.
func ValueSchemaCodec[V any](cdc ValueCodec[V]) (SchemaCodec[V], error) {
if indexable, ok := cdc.(HasSchemaCodec[V]); ok {
return indexable.SchemaCodec()
} else {
return FallbackSchemaCodec[V](), nil
}
}

// FallbackSchemaCodec returns a fallback schema codec for T when one isn't explicitly
// specified with HasSchemaCodec. It maps all simple types directly to schema kinds
// and converts everything else to JSON.
func FallbackSchemaCodec[T any]() SchemaCodec[T] {
var t T
kind := schema.KindForGoValue(t)
if err := kind.Validate(); err == nil {
return SchemaCodec[T]{
Fields: []schema.Field{{
// we don't set any name so that this can be set to a good default by the caller
Name: "",
Kind: kind,
}},
// these can be nil because T maps directly to a schema value for this kind
ToSchemaType: nil,
FromSchemaType: nil,
}
} else {
// we default to encoding everything to JSON
return SchemaCodec[T]{
Fields: []schema.Field{{Kind: schema.JSONKind}},
ToSchemaType: func(t T) (any, error) {
bz, err := json.Marshal(t)
return json.RawMessage(bz), err
},
FromSchemaType: func(a any) (T, error) {
var t T
bz, ok := a.(json.RawMessage)
if !ok {
return t, fmt.Errorf("expected json.RawMessage, got %T", a)
}
err := json.Unmarshal(bz, &t)
return t, err
},
}
}
}
Comment on lines +66 to +102
Copy link
Contributor

Choose a reason for hiding this comment

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

Address undefined types and methods.

The function provides a comprehensive fallback mechanism. However, the following issues need to be addressed:

  • schema.Type is undefined.
  • schema.NewModuleSchema is undefined.
  • update.Remove is undefined in schema.KVPairUpdate.

Ensure these types and methods are correctly defined or imported.

22 changes: 22 additions & 0 deletions collections/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"io"
"math"

"cosmossdk.io/schema"

"cosmossdk.io/collections/codec"
)

Expand Down Expand Up @@ -90,6 +92,24 @@ type Collection interface {
ValueCodec() codec.UntypedValueCodec

genesisHandler

// collectionSchemaCodec returns the schema codec for this collection.
schemaCodec() (*collectionSchemaCodec, error)

// isSecondaryIndex indicates that this collection represents a secondary index
// in the schema and should be excluded from the module's user facing schema.
isSecondaryIndex() bool
}

// collectionSchemaCodec maps a collection to a schema object type and provides
// decoders and encoders to and from schema values and raw kv-store bytes.
type collectionSchemaCodec struct {
coll Collection
objectType schema.ObjectType
keyDecoder func([]byte) (any, error)
valueDecoder func([]byte) (any, error)
keyEncoder func(any) ([]byte, error)
valueEncoder func(any) ([]byte, error)
}

// Prefix defines a segregation bytes namespace for specific collections objects.
Expand Down Expand Up @@ -157,3 +177,5 @@ func (c collectionImpl[K, V]) exportGenesis(ctx context.Context, w io.Writer) er
}

func (c collectionImpl[K, V]) defaultGenesis(w io.Writer) error { return c.m.defaultGenesis(w) }

func (c collectionImpl[K, V]) isSecondaryIndex() bool { return c.m.isSecondaryIndex }
3 changes: 2 additions & 1 deletion collections/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ go 1.23
require (
cosmossdk.io/core v1.0.0
cosmossdk.io/core/testing v0.0.0-00010101000000-000000000000
cosmossdk.io/schema v0.2.0
github.com/stretchr/testify v1.9.0
github.com/tidwall/btree v1.7.0
pgregory.net/rapid v1.1.0
)

require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
2 changes: 2 additions & 0 deletions collections/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
cosmossdk.io/schema v0.2.0 h1:UH5CR1DqUq8yP+5Np8PbvG4YX0zAUsTN2Qk6yThmfMk=
cosmossdk.io/schema v0.2.0/go.mod h1:RDAhxIeNB4bYqAlF4NBJwRrgtnciMcyyg0DOKnhNZQQ=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
183 changes: 183 additions & 0 deletions collections/indexing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package collections

import (
"bytes"
"fmt"
"strings"

"cosmossdk.io/schema"
"github.com/tidwall/btree"

"cosmossdk.io/collections/codec"
)

// IndexingOptions are indexing options for the collections schema.
type IndexingOptions struct {

// RetainDeletionsFor is the list of collections to retain deletions for.
RetainDeletionsFor []string
}

// ModuleCodec returns the ModuleCodec for this schema for the provided options.
func (s Schema) ModuleCodec(opts IndexingOptions) (schema.ModuleCodec, error) {
decoder := moduleDecoder{
collectionLookup: &btree.Map[string, *collectionSchemaCodec]{},
}

retainDeletions := make(map[string]bool)
for _, collName := range opts.RetainDeletionsFor {
retainDeletions[collName] = true
}

var types []schema.Type
for _, collName := range s.collectionsOrdered {
coll := s.collectionsByName[collName]

// skip secondary indexes
if coll.isSecondaryIndex() {
continue
}

cdc, err := coll.schemaCodec()
if err != nil {
return schema.ModuleCodec{}, err
}

if retainDeletions[coll.GetName()] {
cdc.objectType.RetainDeletions = true
}

types = append(types, cdc.objectType)

decoder.collectionLookup.Set(string(coll.GetPrefix()), cdc)
}

modSchema, err := schema.CompileModuleSchema(types...)
if err != nil {
return schema.ModuleCodec{}, err
}

return schema.ModuleCodec{
Schema: modSchema,
KVDecoder: decoder.decodeKV,
}, nil
}

type moduleDecoder struct {
// collectionLookup lets us efficiently look the correct collection based on raw key bytes
collectionLookup *btree.Map[string, *collectionSchemaCodec]
}

func (m moduleDecoder) decodeKV(update schema.KVPairUpdate) ([]schema.ObjectUpdate, error) {
key := update.Key
ks := string(key)
var cd *collectionSchemaCodec
// we look for the collection whose prefix is less than this key
m.collectionLookup.Descend(ks, func(prefix string, cur *collectionSchemaCodec) bool {
bytesPrefix := cur.coll.GetPrefix()
if bytes.HasPrefix(key, bytesPrefix) {
cd = cur
return true
}
return false
})
if cd == nil {
return nil, nil
}

return cd.decodeKVPair(update)
}

func (c collectionSchemaCodec) decodeKVPair(update schema.KVPairUpdate) ([]schema.ObjectUpdate, error) {
// strip prefix
key := update.Key
key = key[len(c.coll.GetPrefix()):]

k, err := c.keyDecoder(key)
if err != nil {
return []schema.ObjectUpdate{
{TypeName: c.coll.GetName()},
}, err

}

if update.Remove {
return []schema.ObjectUpdate{
{TypeName: c.coll.GetName(), Key: k, Delete: true},
}, nil
}

v, err := c.valueDecoder(update.Value)
if err != nil {
return []schema.ObjectUpdate{
{TypeName: c.coll.GetName(), Key: k},
}, err
}

return []schema.ObjectUpdate{
{TypeName: c.coll.GetName(), Key: k, Value: v},
}, nil
}
Comment on lines +91 to +120
Copy link
Contributor

Choose a reason for hiding this comment

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

Address undefined field.

The function is well-structured and handles key-value pair updates correctly. However, the field Remove is undefined in schema.KVPairUpdate.

Ensure this field is correctly defined or imported.

Tools
GitHub Check: tests (03)

[failure] 104-104:
update.Remove undefined (type schema.KVPairUpdate has no field or method Remove)

GitHub Check: tests (02)

[failure] 104-104:
update.Remove undefined (type schema.KVPairUpdate has no field or method Remove)

GitHub Check: tests (01)

[failure] 104-104:
update.Remove undefined (type schema.KVPairUpdate has no field or method Remove)

GitHub Check: tests (00)

[failure] 104-104:
update.Remove undefined (type schema.KVPairUpdate has no field or method Remove)

GitHub Check: golangci-lint

[failure] 104-104:
update.Remove undefined (type schema.KVPairUpdate has no field or method Remove)) (typecheck)

GitHub Check: dependency-review

[failure] 104-104:
update.Remove undefined (type schema.KVPairUpdate has no field or method Remove)


func (c collectionImpl[K, V]) schemaCodec() (*collectionSchemaCodec, error) {
res := &collectionSchemaCodec{
coll: c,
}
res.objectType.Name = c.GetName()

keyDecoder, err := codec.KeySchemaCodec(c.m.kc)
if err != nil {
return nil, err
}
res.objectType.KeyFields = keyDecoder.Fields
res.keyDecoder = func(i []byte) (any, error) {
_, x, err := c.m.kc.Decode(i)
if err != nil {
return nil, err
}
return keyDecoder.ToSchemaType(x)
}
ensureFieldNames(c.m.kc, "key", res.objectType.KeyFields)

valueDecoder, err := codec.ValueSchemaCodec(c.m.vc)
if err != nil {
return nil, err
}
res.objectType.ValueFields = valueDecoder.Fields
res.valueDecoder = func(i []byte) (any, error) {
x, err := c.m.vc.Decode(i)
if err != nil {
return nil, err
}
return valueDecoder.ToSchemaType(x)
}
ensureFieldNames(c.m.vc, "value", res.objectType.ValueFields)

return res, nil
}

// ensureFieldNames makes sure that all fields have valid names - either the
// names were specified by user or they get filled
func ensureFieldNames(x any, defaultName string, cols []schema.Field) {
var names []string = nil
if hasName, ok := x.(interface{ Name() string }); ok {
name := hasName.Name()
if name != "" {
names = strings.Split(hasName.Name(), ",")
}
}
for i, col := range cols {
if names != nil && i < len(names) {
col.Name = names[i]
} else {
if col.Name == "" {
if i == 0 && len(cols) == 1 {
col.Name = defaultName
} else {
col.Name = fmt.Sprintf("%s%d", defaultName, i+1)
}
}
}
cols[i] = col
}
}
5 changes: 5 additions & 0 deletions collections/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ type Map[K, V any] struct {
sa func(context.Context) store.KVStore
prefix []byte
name string

// isSecondaryIndex indicates that this map represents a secondary index
// on another collection and that it should be skipped when generating
// a user facing schema
isSecondaryIndex bool
Comment on lines +24 to +27
Copy link
Member

Choose a reason for hiding this comment

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

q: could it be useful to still generate this optionally? Or is it absolutely unnecessary? (asking out of ignorance)

Copy link
Member Author

Choose a reason for hiding this comment

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

The target database (ex. postgres) should generally be able to produce any secondary index it wants if the primary data is present. So this isn't needed.

}

// NewMap returns a Map given a StoreKey, a Prefix, human-readable name and the relative value and key encoders.
Expand Down
5 changes: 4 additions & 1 deletion x/accounts/defaults/lockup/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ require (
github.com/cosmos/gogoproto v1.7.0
)

require github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
require (
cosmossdk.io/schema v0.2.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
)

require (
buf.build/gen/go/cometbft/cometbft/protocolbuffers/go v1.34.2-20240701160653-fedbb9acfd2f.2 // indirect
Expand Down
Loading