diff --git a/collections/CHANGELOG.md b/collections/CHANGELOG.md index 101b5146f3..6b850be66e 100644 --- a/collections/CHANGELOG.md +++ b/collections/CHANGELOG.md @@ -31,16 +31,59 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +## [v1.1.0](https://github.com/cosmos/cosmos-sdk/releases/tag/collections%2Fv1.1.0) + +### Improvements + +* [#23515](https://github.com/cosmos/cosmos-sdk/pull/23515) Bring in `collections/protocodec` go module as package within `collections` module. + +## [v1.0.0](https://github.com/cosmos/cosmos-sdk/releases/tag/collections%2Fv1.0.0) + +### Features + +* [#22641](https://github.com/cosmos/cosmos-sdk/pull/22641) Add reverse iterator support for `Triple`. +* [#17656](https://github.com/cosmos/cosmos-sdk/pull/17656) Introduces `Vec`, a collection type that allows to represent a growable array on top of a KVStore. +* [#18933](https://github.com/cosmos/cosmos-sdk/pull/18933) Add LookupMap implementation. It is basic wrapping of the standard Map methods but is not iterable. +* [#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. +* [#20538](https://github.com/cosmos/cosmos-sdk/pull/20538) Add `Nameable` variations to `KeyCodec` and `ValueCodec` to allow for better indexing of `collections` types. +* [#22544](https://github.com/cosmos/cosmos-sdk/pull/22544) Schema's `ModuleCodec` will now also return Enum descriptors to be registered with the indexer. + +## [v0.4.0](https://github.com/cosmos/cosmos-sdk/releases/tag/collections%2Fv0.4.0) + +### Features + +* [#17024](https://github.com/cosmos/cosmos-sdk/pull/17024) Introduces `Triple`, a composite key with three keys. + +### API Breaking + +* [#17290](https://github.com/cosmos/cosmos-sdk/pull/17290) Collections iteration methods (Iterate, Walk) will not error when the collection is empty. + +### Improvements + +* [#17021](https://github.com/cosmos/cosmos-sdk/pull/17021) Make collections implement the `appmodule.HasGenesis` interface. + +## [v0.3.0](https://github.com/cosmos/cosmos-sdk/releases/tag/collections%2Fv0.3.0) + +### Features + +* [#16074](https://github.com/cosmos/cosmos-sdk/pull/16607) Introduces `Clear` method for `Map` and `KeySet` +* [#16773](https://github.com/cosmos/cosmos-sdk/pull/16773) + * Adds `AltValueCodec` which provides a way to decode a value in two ways. + * Adds the possibility to specify an alternative way to decode the values of `KeySet`, `indexes.Multi`, `indexes.ReversePair`. + ## [v0.2.0](https://github.com/cosmos/cosmos-sdk/releases/tag/collections%2Fv0.2.0) ### Features -* [#16074](https://github.com/cosmos/cosmos-sdk/pull/16074) – Makes the generic Collection interface public, still highly unstable. +* [#16074](https://github.com/cosmos/cosmos-sdk/pull/16074) Makes the generic Collection interface public, still highly unstable. ### API Breaking -* [#16127](https://github.com/cosmos/cosmos-sdk/pull/16127) – In the `Walk` method the call back function being passed is allowed to error. +* [#16127](https://github.com/cosmos/cosmos-sdk/pull/16127) In the `Walk` method the call back function being passed is allowed to error. ## [v0.1.0](https://github.com/cosmos/cosmos-sdk/releases/tag/collections%2Fv0.1.0) -Collections `v0.1.0` is released! Check out the [docs](https://docs.cosmos.network/main/packages/collections) to know how to use the APIs. +Collections `v0.1.0` is released! Check out the [docs](https://docs.cosmos.network/main/build/packages/collections) to know how to use the APIs. diff --git a/collections/README.md b/collections/README.md index 7f8278233c..b8d8a62f1d 100644 --- a/collections/README.md +++ b/collections/README.md @@ -23,11 +23,12 @@ go get cosmossdk.io/collections ## Core types Collections offers 5 different APIs to work with state, which will be explored in the next sections, these APIs are: -- ``Map``: to work with typed arbitrary KV pairings. -- ``KeySet``: to work with just typed keys -- ``Item``: to work with just one typed value -- ``Sequence``: which is a monotonically increasing number. -- ``IndexedMap``: which combines ``Map`` and `KeySet` to provide a `Map` with indexing capabilities. + +* ``Map``: to work with typed arbitrary KV pairings. +* ``KeySet``: to work with just typed keys +* ``Item``: to work with just one typed value +* ``Sequence``: which is a monotonically increasing number. +* ``IndexedMap``: which combines ``Map`` and `KeySet` to provide a `Map` with indexing capabilities. ## Preliminary components @@ -82,15 +83,17 @@ The second argument passed to our ``KeySet`` is a `collections.Prefix`, a prefix where all the state of a specific collection will be saved. Since a module can have multiple collections, the following is expected: -- module params will become a `collections.Item` -- the `AllowList` is a `collections.KeySet` + +* module params will become a `collections.Item` +* the `AllowList` is a `collections.KeySet` We don't want a collection to write over the state of the other collection so we pass it a prefix, which defines a storage partition owned by the collection. -If you already built modules, the prefix translates to the items you were creating in your ``types/keys.go`` file, example: https://github.com/cosmos/cosmos-sdk/blob/main/x/feegrant/key.go#L27 +If you already built modules, the prefix translates to the items you were creating in your ``types/keys.go`` file, example: https://github.com/cosmos/cosmos-sdk/blob/v0.52.0-rc.1/x/feegrant/key.go#L16~L22 your old: + ```go var ( // FeeAllowanceKeyPrefix is the set of the kvstore for fee allowance data @@ -102,7 +105,9 @@ var ( FeeAllowanceQueueKeyPrefix = []byte{0x01} ) ``` + becomes: + ```go var ( // FeeAllowanceKeyPrefix is the set of the kvstore for fee allowance data @@ -130,6 +135,7 @@ prefix2 := collections.NewPrefix("prefix") // THIS IS BAD! prefix1 := collections.NewPrefix("a") prefix2 := collections.NewPrefix("aa") // prefix2 starts with the same as prefix1: BAD!!! ``` + ### Human-Readable Name The third parameter we pass to a collection is a string, which is a human-readable name. @@ -157,7 +163,7 @@ You might need to implement them only if you're migrating to collections and the Let's explore an example: -````go +```go package collections import ( @@ -180,14 +186,14 @@ func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper { IDs: collections.NewMap(sb, IDsPrefix, "ids", collections.StringKey, collections.Uint64Value), } } -```` +``` We're now instantiating a map where the key is string and the value is `uint64`. We already know the first three arguments of the ``NewMap`` function. The fourth parameter is our `KeyCodec`, we know that the ``Map`` has `string` as key so we pass it a `KeyCodec` that handles strings as keys. -The fifth parameter is our `ValueCodec`, we know that the `Map` as a `uint64` as value so we pass it a `ValueCodec` that handles uint64. +The fifth parameter is our `ValueCodec`, we know that the `Map` has a `uint64` as value so we pass it a `ValueCodec` that handles uint64. Collections already comes with all the required implementations for golang primitive types. @@ -390,6 +396,7 @@ func (k Keeper) RemoveValidator(ctx sdk.Context, validator sdk.ValAddress) error return nil } ``` + The first difference we notice is that `KeySet` needs use to specify only one type parameter: the key (`sdk.ValAddress` in this case). The second difference we notice is that `KeySet` in its `NewKeySet` function does not require us to specify a `ValueCodec` but only a `KeyCodec`. This is because a `KeySet` only saves keys and not values. @@ -431,7 +438,7 @@ import ( storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + stakingtypes "cosmossdk.io/x/staking/types" ) var ParamsPrefix = collections.NewPrefix(0) @@ -580,7 +587,7 @@ Let's analyse each method in the example and how it makes use of the `Iterate` a In `GetAllAccounts` we pass to our `Iterate` a nil `Ranger`. This means that the returned `Iterator` will include all the existing keys within the collection. -Then we use some the `Values` method from the returned `Iterator` API to collect all the values into a slice. +Then we use the `Values` method from the returned `Iterator` API to collect all the values into a slice. `Iterator` offers other methods such as `Keys()` to collect only the keys and not the values and `KeyValues` to collect all the keys and values. @@ -595,9 +602,9 @@ we instruct it to report us results in reverse order through `Descending` Then we pass the range instruction to `Iterate` and get an `Iterator`, which will contain only the results we specified in the range. -Then we use again th `Values` method of the `Iterator` to collect all the results. +Then we use again the `Values` method of the `Iterator` to collect all the results. -`collections.Range` also offers a `Prefix` API which is not appliable to all keys types, +`collections.Range` also offers a `Prefix` API which is not applicable to all keys types, for example uint64 cannot be prefix because it is of constant size, but a `string` key can be prefixed. @@ -670,7 +677,7 @@ func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper { Balances: collections.NewMap( sb, BalancesPrefix, "balances", collections.PairKeyCodec(sdk.AccAddressKey, collections.StringKey), - math.IntValue, + sdk.IntValue, ), } } @@ -679,9 +686,10 @@ func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper { #### The Map Key definition First of all we can see that in order to define a composite key of two elements we use the `collections.Pair` type: -````go + +```go collections.Map[collections.Pair[sdk.AccAddress, string], math.Int] -```` +``` `collections.Pair` defines a key composed of two other keys, in our case the first part is `sdk.AccAddress`, the second part is `string`. @@ -698,7 +706,7 @@ encode the second part of the key. Let's expand on the example we used before: -````go +```go var BalancesPrefix = collections.NewPrefix(1) type Keeper struct { @@ -712,7 +720,7 @@ func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper { Balances: collections.NewMap( sb, BalancesPrefix, "balances", collections.PairKeyCodec(sdk.AccAddressKey, collections.StringKey), - math.IntValue, + sdk.IntValue, ), } } @@ -758,7 +766,7 @@ func (k Keeper) GetAllAddressBalancesBetween(ctx sdk.Context, address sdk.AccAdd } ... } -```` +``` #### SetBalance @@ -769,7 +777,7 @@ We use the `collections.Join` function to generate the composite key. `collections.Pair` contains the two keys we have joined, it also exposes two methods: `K1` to fetch the 1st part of the key and `K2` to fetch the second part. -As always, we use the `collections.Map.Set` method to map the composite key to our value (`math.Int`in this case) +As always, we use the `collections.Map.Set` method to map the composite key to our value (`math.Int` in this case) #### GetBalance @@ -788,7 +796,7 @@ in `Pair` keys iterations. ``` As we can see here we're passing the type parameters of the `collections.Pair` because golang type inference -with respect to generics is not as permissive as other languages, so we need to explitly say what are the types of the pair key. +with respect to generics is not as permissive as other languages, so we need to explicitly say what are the types of the pair key. #### GetAllAddressesBalancesBetween @@ -811,7 +819,7 @@ type BaseAccount struct { ``` First of all, when we save our accounts in state we map them using a primary key `sdk.AccAddress`. -If it were to be a `collections.Map` it would be `collections.Map[sdk.AccAddres, authtypes.BaseAccount]`. +If it were to be a `collections.Map` it would be `collections.Map[sdk.AccAddress, authtypes.BaseAccount]`. Then we also want to be able to get an account not only by its `sdk.AccAddress`, but also by its `AccountNumber`. @@ -829,10 +837,6 @@ type AccountsIndexes struct { Number *indexes.Unique[uint64, sdk.AccAddress, authtypes.BaseAccount] } -func (a AccountsIndexes) IndexesList() []collections.Index[sdk.AccAddress, authtypes.BaseAccount] { - return []collections.Index[sdk.AccAddress, authtypes.BaseAccount]{a.Number} -} - func NewAccountIndexes(sb *collections.SchemaBuilder) AccountsIndexes { return AccountsIndexes{ Number: indexes.NewUnique( @@ -856,18 +860,26 @@ Then we can see in our `AccountIndexes` struct the `Number` field is defined as: ``` Where the first type parameter is `uint64`, which is the field type of our index. -The second type parameter is the primary key `sdk.AccAddress` +The second type parameter is the primary key `sdk.AccAddress`. And the third type parameter is the actual object we're storing `authtypes.BaseAccount`. -Then we implement a function called `IndexesList` on our `AccountIndexes` struct, this will be used -by the `IndexedMap` to keep the underlying map in sync with the indexes, in our case `Number`. -This function just needs to return the slice of indexes contained in the struct. - Then we create a `NewAccountIndexes` function that instantiates and returns the `AccountsIndexes` struct. The function takes a `SchemaBuilder`. Then we instantiate our `indexes.Unique`, let's analyse the arguments we pass to `indexes.NewUnique`. +#### NOTE: indexes list + +The `AccountsIndexes` struct contains the indexes, the `NewIndexedMap` function will infer the indexes form that struct +using reflection, this happens only at init and is not computationally expensive. In case you want to explicitly declare +indexes: implement the `Indexes` interface in the `AccountsIndexes` struct: + +```go +func (a AccountsIndexes) IndexesList() []collections.Index[sdk.AccAddress, authtypes.BaseAccount] { + return []collections.Index[sdk.AccAddress, authtypes.BaseAccount]{a.Number} +} +``` + #### Instantiating a `indexes.Unique` The first three arguments, we already know them, they are: `SchemaBuilder`, `Prefix` which is our index prefix (the partition @@ -1117,3 +1129,82 @@ func (k Keeper) GetAccount(ctx sdk.context, addr sdk.AccAddress) (sdk.AccountI, return k.Accounts.Get(ctx, addr) } ``` + +## Triple key + +The `collections.Triple` is a special type of key composed of three keys, it's identical to `collections.Pair`. + +Let's see an example. + +```go +package example + +import ( + "context" + + "cosmossdk.io/collections" + storetypes "cosmossdk.io/store/types" + "github.com/cosmos/cosmos-sdk/codec" +) + +type AccAddress = string +type ValAddress = string + +type Keeper struct { + // let's simulate we have redelegations which are stored as a triple key composed of + // the delegator, the source validator and the destination validator. + Redelegations collections.KeySet[collections.Triple[AccAddress, ValAddress, ValAddress]] +} + +func NewKeeper(storeKey *storetypes.KVStoreKey) Keeper { + sb := collections.NewSchemaBuilder(sdk.OpenKVStore(storeKey)) + return Keeper{ + Redelegations: collections.NewKeySet(sb, collections.NewPrefix(0), "redelegations", collections.TripleKeyCodec(collections.StringKey, collections.StringKey, collections.StringKey) + } +} + +// RedelegationsByDelegator iterates over all the redelegations of a given delegator and calls onResult providing +// each redelegation from source validator towards the destination validator. +func (k Keeper) RedelegationsByDelegator(ctx context.Context, delegator AccAddress, onResult func(src, dst ValAddress) (stop bool, err error)) error { + rng := collections.NewPrefixedTripleRange[AccAddress, ValAddress, ValAddress](delegator) + return k.Redelegations.Walk(ctx, rng, func(key collections.Triple[AccAddress, ValAddress, ValAddress]) (stop bool, err error) { + return onResult(key.K2(), key.K3()) + }) +} + +// RedelegationsByDelegatorAndValidator iterates over all the redelegations of a given delegator and its source validator and calls onResult for each +// destination validator. +func (k Keeper) RedelegationsByDelegatorAndValidator(ctx context.Context, delegator AccAddress, validator ValAddress, onResult func(dst ValAddress) (stop bool, err error)) error { + rng := collections.NewSuperPrefixedTripleRange[AccAddress, ValAddress, ValAddress](delegator, validator) + return k.Redelegations.Walk(ctx, rng, func(key collections.Triple[AccAddress, ValAddress, ValAddress]) (stop bool, err error) { + return onResult(key.K3()) + }) +} +``` + +## Advanced Usages + +### Alternative Value Codec + +The `codec.AltValueCodec` allows a collection to decode values using a different codec than the one used to encode them. +Basically it enables to decode two different byte representations of the same concrete value. +It can be used to lazily migrate values from one bytes representation to another, as long as the new representation is +not able to decode the old one. + +A concrete example can be found in `x/bank` where the balance was initially stored as `Coin` and then migrated to `Int`. + +```go + +var BankBalanceValueCodec = codec.NewAltValueCodec(sdk.IntValue, func(b []byte) (sdk.Int, error) { + coin := sdk.Coin{} + err := coin.Unmarshal(b) + if err != nil { + return sdk.Int{}, err + } + return coin.Amount, nil +}) +``` + +The above example shows how to create an `AltValueCodec` that can decode both `sdk.Int` and `sdk.Coin` values. The provided +decoder function will be used as a fallback in case the default decoder fails. When the value will be encoded back into state +it will use the default encoder. This allows to lazily migrate values to a new bytes representation. diff --git a/collections/codec/alternative_value.go b/collections/codec/alternative_value.go new file mode 100644 index 0000000000..4d7902a4be --- /dev/null +++ b/collections/codec/alternative_value.go @@ -0,0 +1,47 @@ +package codec + +// NewAltValueCodec returns a new AltValueCodec. canonicalValueCodec is the codec that you want the value +// to be encoded and decoded as, alternativeDecoder is a function that will attempt to decode the value +// in case the canonicalValueCodec fails to decode it. +func NewAltValueCodec[V any](canonicalValueCodec ValueCodec[V], alternativeDecoder func([]byte) (V, error)) ValueCodec[V] { + return AltValueCodec[V]{ + canonicalValueCodec: canonicalValueCodec, + alternativeDecoder: alternativeDecoder, + } +} + +// AltValueCodec is a codec that can decode a value from state in an alternative format. +// This is useful for migrating data from one format to another. For example, in x/bank +// balances were initially encoded as sdk.Coin, now they are encoded as math.Int. +// The AltValueCodec will be trying to decode the value as math.Int, and if that fails, +// it will attempt to decode it as sdk.Coin. +// NOTE: if the canonical format can also decode the alternative format, then this codec +// will produce undefined and undesirable behavior. +type AltValueCodec[V any] struct { + canonicalValueCodec ValueCodec[V] + alternativeDecoder func([]byte) (V, error) +} + +// Decode will attempt to decode the value from state using the canonical value codec. +// If it fails to decode, it will attempt to decode the value using the alternative decoder. +func (a AltValueCodec[V]) Decode(b []byte) (V, error) { + v, err := a.canonicalValueCodec.Decode(b) + if err != nil { + return a.alternativeDecoder(b) + } + return v, nil +} + +// Below there is the implementation of ValueCodec relying on the canonical value codec. + +func (a AltValueCodec[V]) Encode(value V) ([]byte, error) { return a.canonicalValueCodec.Encode(value) } + +func (a AltValueCodec[V]) EncodeJSON(value V) ([]byte, error) { + return a.canonicalValueCodec.EncodeJSON(value) +} + +func (a AltValueCodec[V]) DecodeJSON(b []byte) (V, error) { return a.canonicalValueCodec.DecodeJSON(b) } + +func (a AltValueCodec[V]) Stringify(value V) string { return a.canonicalValueCodec.Stringify(value) } + +func (a AltValueCodec[V]) ValueType() string { return a.canonicalValueCodec.ValueType() } diff --git a/collections/codec/alternative_value_test.go b/collections/codec/alternative_value_test.go new file mode 100644 index 0000000000..358395427b --- /dev/null +++ b/collections/codec/alternative_value_test.go @@ -0,0 +1,53 @@ +package codec_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/collections/codec" + "cosmossdk.io/collections/colltest" +) + +type altValue struct { + Value uint64 `json:"value"` +} + +func TestAltValueCodec(t *testing.T) { + // we assume we want to migrate the value from json(altValue) to just be + // the raw value uint64. + canonical := codec.KeyToValueCodec(codec.NewUint64Key[uint64]()) + alternative := func(v []byte) (uint64, error) { + var alt altValue + err := json.Unmarshal(v, &alt) + if err != nil { + return 0, err + } + return alt.Value, nil + } + + cdc := codec.NewAltValueCodec(canonical, alternative) + + t.Run("decodes alternative value", func(t *testing.T) { + expected := uint64(100) + alternativeEncodedBytes, err := json.Marshal(altValue{Value: expected}) + require.NoError(t, err) + got, err := cdc.Decode(alternativeEncodedBytes) + require.NoError(t, err) + require.Equal(t, expected, got) + }) + + t.Run("decodes canonical value", func(t *testing.T) { + expected := uint64(100) + canonicalEncodedBytes, err := cdc.Encode(expected) + require.NoError(t, err) + got, err := cdc.Decode(canonicalEncodedBytes) + require.NoError(t, err) + require.Equal(t, expected, got) + }) + + t.Run("conformance", func(t *testing.T) { + colltest.TestValueCodec(t, cdc, uint64(100)) + }) +} diff --git a/collections/codec/bool.go b/collections/codec/bool.go index 4016c8d68d..8278def711 100644 --- a/collections/codec/bool.go +++ b/collections/codec/bool.go @@ -6,7 +6,7 @@ import ( "strconv" ) -func NewBoolKey[T ~bool]() KeyCodec[T] { return boolKey[T]{} } +func NewBoolKey[T ~bool]() NameableKeyCodec[T] { return boolKey[T]{} } type boolKey[T ~bool] struct{} @@ -33,7 +33,7 @@ func (b boolKey[T]) Decode(buffer []byte) (int, T, error) { } } -func (b boolKey[T]) Size(key T) int { return 1 } +func (b boolKey[T]) Size(_ T) int { return 1 } func (b boolKey[T]) EncodeJSON(value T) ([]byte, error) { return json.Marshal(value) @@ -64,3 +64,7 @@ func (b boolKey[T]) DecodeNonTerminal(buffer []byte) (int, T, error) { func (b boolKey[T]) SizeNonTerminal(key T) int { return b.Size(key) } + +func (b boolKey[T]) WithName(name string) KeyCodec[T] { + return NamedKeyCodec[T]{KeyCodec: b, Name: name} +} diff --git a/collections/codec/bytes.go b/collections/codec/bytes.go index 28334795e3..0fb056782c 100644 --- a/collections/codec/bytes.go +++ b/collections/codec/bytes.go @@ -10,7 +10,7 @@ import ( // using the BytesKey KeyCodec. const MaxBytesKeyNonTerminalSize = math.MaxUint8 -func NewBytesKey[T ~[]byte]() KeyCodec[T] { return bytesKey[T]{} } +func NewBytesKey[T ~[]byte]() NameableKeyCodec[T] { return bytesKey[T]{} } type bytesKey[T ~[]byte] struct{} @@ -77,3 +77,7 @@ func (bytesKey[T]) DecodeNonTerminal(buffer []byte) (int, T, error) { func (bytesKey[T]) SizeNonTerminal(key T) int { return len(key) + 1 } + +func (b bytesKey[T]) WithName(name string) KeyCodec[T] { + return NamedKeyCodec[T]{KeyCodec: b, Name: name} +} diff --git a/collections/codec/codec.go b/collections/codec/codec.go index 2988c9f524..b8f5cf9946 100644 --- a/collections/codec/codec.go +++ b/collections/codec/codec.go @@ -125,7 +125,9 @@ type UntypedValueCodec struct { } // KeyToValueCodec converts a KeyCodec into a ValueCodec. -func KeyToValueCodec[K any](keyCodec KeyCodec[K]) ValueCodec[K] { return keyToValueCodec[K]{keyCodec} } +func KeyToValueCodec[K any](keyCodec KeyCodec[K]) NameableValueCodec[K] { + return keyToValueCodec[K]{kc: keyCodec} +} // keyToValueCodec is a ValueCodec that wraps a KeyCodec to make it behave like a ValueCodec. type keyToValueCodec[K any] struct { @@ -167,3 +169,7 @@ func (k keyToValueCodec[K]) Stringify(value K) string { func (k keyToValueCodec[K]) ValueType() string { return k.kc.KeyType() } + +func (k keyToValueCodec[K]) WithName(name string) ValueCodec[K] { + return NamedValueCodec[K]{ValueCodec: k, Name: name} +} diff --git a/collections/codec/codec_test.go b/collections/codec/codec_test.go index 16fd10553f..65eb0704ee 100644 --- a/collections/codec/codec_test.go +++ b/collections/codec/codec_test.go @@ -7,7 +7,7 @@ import ( ) func TestUntypedValueCodec(t *testing.T) { - vc := NewUntypedValueCodec(KeyToValueCodec(NewStringKeyCodec[string]())) + vc := NewUntypedValueCodec(ValueCodec[string](KeyToValueCodec(KeyCodec[string](NewStringKeyCodec[string]())))) t.Run("encode/decode", func(t *testing.T) { _, err := vc.Encode(0) diff --git a/collections/codec/indexing.go b/collections/codec/indexing.go new file mode 100644 index 0000000000..01f3740bd5 --- /dev/null +++ b/collections/codec/indexing.go @@ -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 String. +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 String + return SchemaCodec[T]{ + Fields: []schema.Field{{Kind: schema.StringKind}}, + ToSchemaType: func(t T) (any, error) { + bz, err := json.Marshal(t) + return string(json.RawMessage(bz)), err + }, + FromSchemaType: func(a any) (T, error) { + var t T + sz, ok := a.(string) + if !ok { + return t, fmt.Errorf("expected string, got %T", a) + } + err := json.Unmarshal([]byte(sz), &t) + return t, err + }, + } + } +} diff --git a/collections/codec/int.go b/collections/codec/int.go index 1300efde4d..9fb2a824f2 100644 --- a/collections/codec/int.go +++ b/collections/codec/int.go @@ -7,7 +7,7 @@ import ( "strconv" ) -func NewInt64Key[T ~int64]() KeyCodec[T] { return int64Key[T]{} } +func NewInt64Key[T ~int64]() NameableKeyCodec[T] { return int64Key[T]{} } type int64Key[T ~int64] struct{} @@ -64,7 +64,11 @@ func (i int64Key[T]) SizeNonTerminal(_ T) int { return 8 } -func NewInt32Key[T ~int32]() KeyCodec[T] { +func (i int64Key[T]) WithName(name string) KeyCodec[T] { + return NamedKeyCodec[T]{KeyCodec: i, Name: name} +} + +func NewInt32Key[T ~int32]() NameableKeyCodec[T] { return int32Key[T]{} } @@ -121,3 +125,7 @@ func (i int32Key[T]) DecodeNonTerminal(buffer []byte) (int, T, error) { func (i int32Key[T]) SizeNonTerminal(_ T) int { return 4 } + +func (i int32Key[T]) WithName(name string) KeyCodec[T] { + return NamedKeyCodec[T]{KeyCodec: i, Name: name} +} diff --git a/collections/codec/naming.go b/collections/codec/naming.go new file mode 100644 index 0000000000..ffaeeae58d --- /dev/null +++ b/collections/codec/naming.go @@ -0,0 +1,63 @@ +package codec + +import "fmt" + +// NameableKeyCodec is a KeyCodec that can be named. +type NameableKeyCodec[T any] interface { + KeyCodec[T] + + // WithName returns the KeyCodec with the provided name. + WithName(name string) KeyCodec[T] +} + +// NameableValueCodec is a ValueCodec that can be named. +type NameableValueCodec[T any] interface { + ValueCodec[T] + + // WithName returns the ValueCodec with the provided name. + WithName(name string) ValueCodec[T] +} + +// NamedKeyCodec wraps a KeyCodec with a name. +// The underlying key codec MUST have exactly one field in its schema. +type NamedKeyCodec[T any] struct { + KeyCodec[T] + + // Name is the name of the KeyCodec in the schema. + Name string +} + +// SchemaCodec returns the schema codec for the named key codec. +func (n NamedKeyCodec[T]) SchemaCodec() (SchemaCodec[T], error) { + cdc, err := KeySchemaCodec[T](n.KeyCodec) + if err != nil { + return SchemaCodec[T]{}, err + } + return withName(cdc, n.Name) +} + +// NamedValueCodec wraps a ValueCodec with a name. +// The underlying value codec MUST have exactly one field in its schema. +type NamedValueCodec[T any] struct { + ValueCodec[T] + + // Name is the name of the ValueCodec in the schema. + Name string +} + +// SchemaCodec returns the schema codec for the named value codec. +func (n NamedValueCodec[T]) SchemaCodec() (SchemaCodec[T], error) { + cdc, err := ValueSchemaCodec[T](n.ValueCodec) + if err != nil { + return SchemaCodec[T]{}, err + } + return withName(cdc, n.Name) +} + +func withName[T any](cdc SchemaCodec[T], name string) (SchemaCodec[T], error) { + if len(cdc.Fields) != 1 { + return SchemaCodec[T]{}, fmt.Errorf("expected exactly one field to be named, got %d", len(cdc.Fields)) + } + cdc.Fields[0].Name = name + return cdc, nil +} diff --git a/collections/codec/string.go b/collections/codec/string.go index 3189b8bc9c..7b3cdf0faa 100644 --- a/collections/codec/string.go +++ b/collections/codec/string.go @@ -6,7 +6,7 @@ import ( "fmt" ) -func NewStringKeyCodec[T ~string]() KeyCodec[T] { return stringKey[T]{} } +func NewStringKeyCodec[T ~string]() NameableKeyCodec[T] { return stringKey[T]{} } const ( // StringDelimiter defines the delimiter of a string key when used in non-terminal encodings. @@ -66,3 +66,7 @@ func (stringKey[T]) Stringify(key T) string { func (stringKey[T]) KeyType() string { return "string" } + +func (s stringKey[T]) WithName(name string) KeyCodec[T] { + return NamedKeyCodec[T]{KeyCodec: s, Name: name} +} diff --git a/collections/codec/uint.go b/collections/codec/uint.go index 658235d385..83efa2ec64 100644 --- a/collections/codec/uint.go +++ b/collections/codec/uint.go @@ -7,7 +7,7 @@ import ( "strconv" ) -func NewUint64Key[T ~uint64]() KeyCodec[T] { return uint64Key[T]{} } +func NewUint64Key[T ~uint64]() NameableKeyCodec[T] { return uint64Key[T]{} } type uint64Key[T ~uint64] struct{} @@ -55,7 +55,11 @@ func (uint64Key[T]) KeyType() string { return "uint64" } -func NewUint32Key[T ~uint32]() KeyCodec[T] { return uint32Key[T]{} } +func (u uint64Key[T]) WithName(name string) KeyCodec[T] { + return NamedKeyCodec[T]{KeyCodec: u, Name: name} +} + +func NewUint32Key[T ~uint32]() NameableKeyCodec[T] { return uint32Key[T]{} } type uint32Key[T ~uint32] struct{} @@ -95,7 +99,11 @@ func (u uint32Key[T]) DecodeNonTerminal(buffer []byte) (int, T, error) { return func (uint32Key[T]) SizeNonTerminal(_ T) int { return 4 } -func NewUint16Key[T ~uint16]() KeyCodec[T] { return uint16Key[T]{} } +func (u uint32Key[T]) WithName(name string) KeyCodec[T] { + return NamedKeyCodec[T]{KeyCodec: u, Name: name} +} + +func NewUint16Key[T ~uint16]() NameableKeyCodec[T] { return uint16Key[T]{} } type uint16Key[T ~uint16] struct{} @@ -135,6 +143,10 @@ func (u uint16Key[T]) DecodeNonTerminal(buffer []byte) (int, T, error) { return func (u uint16Key[T]) SizeNonTerminal(key T) int { return u.Size(key) } +func (u uint16Key[T]) WithName(name string) KeyCodec[T] { + return NamedKeyCodec[T]{KeyCodec: u, Name: name} +} + func uintEncodeJSON(value uint64) ([]byte, error) { str := `"` + strconv.FormatUint(value, 10) + `"` return []byte(str), nil diff --git a/collections/collections.go b/collections/collections.go index 0f6b322533..2c67de4604 100644 --- a/collections/collections.go +++ b/collections/collections.go @@ -3,10 +3,11 @@ package collections import ( "context" "errors" - io "io" + "io" "math" "cosmossdk.io/collections/codec" + "cosmossdk.io/schema" ) var ( @@ -29,7 +30,7 @@ var ( Uint64Key = codec.NewUint64Key[uint64]() // Int32Key can be used to encode int32 keys. Encoding retains ordering by toggling the MSB. Int32Key = codec.NewInt32Key[int32]() - // Int64Key can be used to encode int64. Encoding retains ordering by toggling the MSB. + // Int64Key can be used to encode int64 keys. Encoding retains ordering by toggling the MSB. Int64Key = codec.NewInt64Key[int64]() // StringKey can be used to encode string keys. The encoding just converts the string // to bytes. @@ -90,6 +91,22 @@ 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.StateObjectType + keyDecoder func([]byte) (any, error) + valueDecoder func([]byte) (any, error) } // Prefix defines a segregation bytes namespace for specific collections objects. @@ -157,3 +174,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 } diff --git a/collections/collections_test.go b/collections/collections_test.go index f9a799fbf2..e6d6c3f824 100644 --- a/collections/collections_test.go +++ b/collections/collections_test.go @@ -5,49 +5,16 @@ import ( "math" "testing" - db "github.com/cosmos/cosmos-db" "github.com/stretchr/testify/require" "cosmossdk.io/core/store" + "cosmossdk.io/core/testing" ) -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() + ctx := coretesting.Context() + kv := coretesting.KVStoreService(ctx, "test") + return kv, ctx } func TestPrefix(t *testing.T) { diff --git a/collections/colltest/codec.go b/collections/colltest/codec.go index c0bb503819..54249ebf11 100644 --- a/collections/colltest/codec.go +++ b/collections/colltest/codec.go @@ -40,6 +40,11 @@ func TestKeyCodec[T any](t *testing.T, keyCodec codec.KeyCodec[T], key T) { decoded, err := keyCodec.DecodeJSON(keyJSON) require.NoError(t, err) require.Equal(t, key, decoded, "json encoding and decoding did not produce the same results") + + // check type + require.NotEmpty(t, keyCodec.KeyType()) + // check string + _ = keyCodec.Stringify(key) } // TestValueCodec asserts the correct behavior of a ValueCodec over the type T. @@ -119,7 +124,7 @@ func (m mockValueCodec[T]) Decode(b []byte) (t T, err error) { 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) + return t, fmt.Errorf("unknown 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() diff --git a/collections/genesis.go b/collections/genesis.go index 454ba496e1..0c593e4adf 100644 --- a/collections/genesis.go +++ b/collections/genesis.go @@ -32,7 +32,7 @@ func (m Map[K, V]) importGenesis(ctx context.Context, reader io.Reader) error { } func (m Map[K, V]) exportGenesis(ctx context.Context, writer io.Writer) error { - _, err := writer.Write([]byte("[")) + _, err := io.WriteString(writer, "[") if err != nil { return err } @@ -48,7 +48,7 @@ func (m Map[K, V]) exportGenesis(ctx context.Context, writer io.Writer) error { // add a comma before encoding the object // for all objects besides the first one. if !first { - _, err = writer.Write([]byte(",")) + _, err = io.WriteString(writer, ",") if err != nil { return err } @@ -91,7 +91,7 @@ func (m Map[K, V]) exportGenesis(ctx context.Context, writer io.Writer) error { } } - _, err = writer.Write([]byte("]")) + _, err = io.WriteString(writer, "]") return err } @@ -148,6 +148,6 @@ func (m Map[K, V]) doDecodeJSON(reader io.Reader, onEntry func(key K, value V) e } func (m Map[K, V]) defaultGenesis(writer io.Writer) error { - _, err := writer.Write([]byte(`[]`)) + _, err := io.WriteString(writer, `[]`) return err } diff --git a/collections/genesis_test.go b/collections/genesis_test.go index 3d7fd3c549..34fb60e141 100644 --- a/collections/genesis_test.go +++ b/collections/genesis_test.go @@ -91,6 +91,7 @@ type testFixture struct { } func initFixture(t *testing.T) *testFixture { + t.Helper() sk, ctx := deps() schemaBuilder := NewSchemaBuilder(sk) m := NewMap(schemaBuilder, NewPrefix(1), "map", StringKey, Uint64Value) @@ -110,6 +111,7 @@ func initFixture(t *testing.T) *testFixture { } func createTestGenesisSource(t *testing.T) appmodule.GenesisSource { + t.Helper() expectedOrder := []string{"item", "key_set", "map", "sequence"} currentIndex := 0 return func(field string) (io.ReadCloser, error) { @@ -149,6 +151,7 @@ func (b *bufCloser) Close() error { } func newBufCloser(t *testing.T, str string) *bufCloser { + t.Helper() b := &bufCloser{ Buffer: bytes.NewBufferString(str), closed: false, diff --git a/collections/go.mod b/collections/go.mod index d45f38f666..6d28c2301f 100644 --- a/collections/go.mod +++ b/collections/go.mod @@ -1,53 +1,52 @@ module cosmossdk.io/collections -go 1.23 +go 1.23.2 require ( - cosmossdk.io/core v0.11.0 - github.com/cosmos/cosmos-db v1.0.2 - github.com/stretchr/testify v1.9.0 + cosmossdk.io/core v1.0.0 + cosmossdk.io/core/testing v0.0.2 + cosmossdk.io/schema v1.0.0 + github.com/cosmos/cosmos-db v1.1.1 + github.com/cosmos/gogoproto v1.7.0 + github.com/google/go-cmp v0.6.0 + github.com/stretchr/testify v1.10.0 + github.com/tidwall/btree v1.7.0 + google.golang.org/protobuf v1.36.4 pgregory.net/rapid v1.1.0 ) require ( - cosmossdk.io/api v0.7.5 // indirect - cosmossdk.io/depinject v1.0.0-alpha.4 // indirect - github.com/DataDog/zstd v1.5.5 // indirect + github.com/DataDog/zstd v1.4.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cockroachdb/errors v1.11.1 // indirect + github.com/cockroachdb/errors v1.11.3 // indirect + github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect - github.com/cockroachdb/pebble v1.1.0 // indirect + github.com/cockroachdb/pebble v1.1.2 // indirect github.com/cockroachdb/redact v1.1.5 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect - github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/getsentry/sentry-go v0.27.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/btree v1.1.2 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/klauspost/compress v1.16.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/linxGnu/grocksdb v1.8.12 // indirect - github.com/onsi/gomega v1.20.0 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.18.0 // indirect - github.com/prometheus/client_model v0.6.0 // indirect - github.com/prometheus/common v0.47.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect - golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c // indirect - google.golang.org/grpc v1.58.3 // indirect - google.golang.org/protobuf v1.33.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.12.0 // indirect + github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/collections/go.sum b/collections/go.sum index 8932e2415b..e11c39e98d 100644 --- a/collections/go.sum +++ b/collections/go.sum @@ -1,218 +1,582 @@ -cosmossdk.io/api v0.7.5 h1:eMPTReoNmGUm8DeiQL9DyM8sYDjEhWzL1+nLbI9DqtQ= -cosmossdk.io/api v0.7.5/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= -cosmossdk.io/core v0.11.0 h1:vtIafqUi+1ZNAE/oxLOQQ7Oek2n4S48SWLG8h/+wdbo= -cosmossdk.io/core v0.11.0/go.mod h1:LaTtayWBSoacF5xNzoF8tmLhehqlA9z1SWiPuNC6X1w= -cosmossdk.io/depinject v1.0.0-alpha.4 h1:PLNp8ZYAMPTUKyG9IK2hsbciDWqna2z1Wsl98okJopc= -cosmossdk.io/depinject v1.0.0-alpha.4/go.mod h1:HeDk7IkR5ckZ3lMGs/o91AVUc7E596vMaOmslGFM3yU= -github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= -github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cosmossdk.io/core v1.0.0 h1:e7XBbISOytLBOXMVwpRPixThXqEkeLGlg8no/qpgS8U= +cosmossdk.io/core v1.0.0/go.mod h1:mKIp3RkoEmtqdEdFHxHwWAULRe+79gfdOvmArrLDbDc= +cosmossdk.io/core/testing v0.0.2 h1:TXAmsnHa1C5lLiVN1z+rnkiyNS6sNwcGJCb6OouaNv8= +cosmossdk.io/core/testing v0.0.2/go.mod h1:6Y3QaoNXsgeHiV4+9oYF/IqMFqCK5cJClonw/OXxHlY= +cosmossdk.io/schema v1.0.0 h1:/diH4XJjpV1JQwuIozwr+A4uFuuwanFdnw2kKeiXwwQ= +cosmossdk.io/schema v1.0.0/go.mod h1:RDAhxIeNB4bYqAlF4NBJwRrgtnciMcyyg0DOKnhNZQQ= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= -github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= -github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/pebble v1.1.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4= -github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= +github.com/cockroachdb/pebble v1.1.2 h1:CUh2IPtR4swHlEj48Rhfzw6l/d0qA31fItcIszQVIsA= +github.com/cockroachdb/pebble v1.1.2/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= -github.com/cosmos/cosmos-db v1.0.2 h1:hwMjozuY1OlJs/uh6vddqnk9j7VamLv+0DBlbEXbAKs= -github.com/cosmos/cosmos-db v1.0.2/go.mod h1:Z8IXcFJ9PqKK6BIsVOB3QXtkKoqUOp1vRvPT39kOXEA= -github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= -github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= +github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= +github.com/cosmos/cosmos-db v1.1.1/go.mod h1:AghjcIPqdhSLP/2Z0yha5xPH3nLnskz81pBx3tcVSAw= +github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fro= +github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/linxGnu/grocksdb v1.8.12 h1:1/pCztQUOa3BX/1gR3jSZDoaKFpeHFvQ1XrqZpSvZVo= github.com/linxGnu/grocksdb v1.8.12/go.mod h1:xZCIb5Muw+nhbDK4Y5UJuOrin5MceOuiXkVUR7vp4WY= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.20.0 h1:8W0cWlwFkflGPLltQvLRB7ZVD5HuP6ng320w2IS245Q= -github.com/onsi/gomega v1.20.0/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= -github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= -github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= -github.com/prometheus/common v0.47.0 h1:p5Cz0FNHo7SnWOmWmoRozVcjEp0bIVU8cV7OShpjL1k= -github.com/prometheus/common v0.47.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg= +github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y= +github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= -github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= +github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c h1:jHkCUWkseRf+W+edG5hMzr/Uh1xkDREY4caybAq4dpY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0= -google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= -google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM= +google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/collections/indexed_map.go b/collections/indexed_map.go index 4a45edbe45..d763bb721a 100644 --- a/collections/indexed_map.go +++ b/collections/indexed_map.go @@ -2,6 +2,9 @@ package collections import ( "context" + "errors" + "fmt" + "reflect" "cosmossdk.io/collections/codec" ) @@ -32,17 +35,77 @@ type Index[PrimaryKey, Value any] interface { // 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 any, Idx Indexes[PrimaryKey, Value]] struct { - Indexes Idx - m Map[PrimaryKey, Value] +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. -func NewIndexedMap[PrimaryKey, Value any, Idx Indexes[PrimaryKey, 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, @@ -50,10 +113,11 @@ func NewIndexedMap[PrimaryKey, Value any, Idx Indexes[PrimaryKey, Value]]( valueCodec codec.ValueCodec[Value], indexes Idx, ) *IndexedMap[PrimaryKey, Value, Idx] { - return &IndexedMap[PrimaryKey, Value, Idx]{ - Indexes: indexes, - m: NewMap(schema, prefix, name, pkCodec, valueCodec), + im, err := NewIndexedMapSafe(schema, prefix, name, pkCodec, valueCodec, indexes) + if err != nil { + panic(err) } + return im } // Get gets the object given its primary key. @@ -111,7 +175,7 @@ func (m *IndexedMap[PrimaryKey, Value, Idx]) ValueCodec() codec.ValueCodec[Value } func (m *IndexedMap[PrimaryKey, Value, Idx]) ref(ctx context.Context, pk PrimaryKey, value Value) error { - for _, index := range m.Indexes.IndexesList() { + for _, index := range m.computedIndexes { err := index.Reference(ctx, pk, value, cachedGet[PrimaryKey, Value](ctx, m, pk)) if err != nil { return err @@ -121,7 +185,7 @@ func (m *IndexedMap[PrimaryKey, Value, Idx]) ref(ctx context.Context, pk Primary } func (m *IndexedMap[PrimaryKey, Value, Idx]) unref(ctx context.Context, pk PrimaryKey) error { - for _, index := range m.Indexes.IndexesList() { + for _, index := range m.computedIndexes { err := index.Unreference(ctx, pk, cachedGet[PrimaryKey, Value](ctx, m, pk)) if err != nil { return err diff --git a/collections/indexed_map_internal_test.go b/collections/indexed_map_internal_test.go new file mode 100644 index 0000000000..8f315ca1e5 --- /dev/null +++ b/collections/indexed_map_internal_test.go @@ -0,0 +1,30 @@ +package collections + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTryInferIndex(t *testing.T) { + invalidIdx := 5 + + t.Run("not a pointer to struct", func(t *testing.T) { + _, err := tryInferIndexes[*int, string, string](&invalidIdx) + require.ErrorIs(t, err, errNotStruct) + }) + + t.Run("not a struct", func(t *testing.T) { + _, err := tryInferIndexes[int, string, string](invalidIdx) + require.ErrorIs(t, err, errNotStruct) + }) + + t.Run("not an index field", func(t *testing.T) { + type invalidIndex struct { + A int + } + + _, err := tryInferIndexes[invalidIndex, string, string](invalidIndex{}) + require.ErrorIs(t, err, errNotIndex) + }) +} diff --git a/collections/indexed_map_test.go b/collections/indexed_map_test.go index ea883cb92e..f7eba42d40 100644 --- a/collections/indexed_map_test.go +++ b/collections/indexed_map_test.go @@ -8,6 +8,7 @@ import ( "cosmossdk.io/collections" "cosmossdk.io/collections/colltest" "cosmossdk.io/collections/indexes" + "cosmossdk.io/core/testing" ) type company struct { @@ -44,7 +45,9 @@ func newTestIndexedMap(schema *collections.SchemaBuilder) *collections.IndexedMa } func TestIndexedMap(t *testing.T) { - sk, ctx := colltest.MockStore() + ctx := coretesting.Context() + sk := coretesting.KVStoreService(ctx, "test") + schema := collections.NewSchemaBuilder(sk) im := newTestIndexedMap(schema) @@ -104,3 +107,27 @@ func TestIndexedMap(t *testing.T) { require.NoError(t, err) require.Equal(t, company{"milan", 4}, v) } + +type inferIndex struct { + City *indexes.Multi[string, string, company] + Vat *indexes.Unique[uint64, string, company] +} + +func newInferIndex(schema *collections.SchemaBuilder) *inferIndex { + return &inferIndex{ + City: indexes.NewMulti(schema, collections.NewPrefix(1), "companies_by_city", collections.StringKey, collections.StringKey, func(pk string, value company) (string, error) { + return value.City, nil + }), + Vat: indexes.NewUnique(schema, collections.NewPrefix(2), "companies_by_vat", collections.Uint64Key, collections.StringKey, func(pk string, value company) (uint64, error) { + return value.Vat, nil + }), + } +} + +func TestIndexedMapInfer(t *testing.T) { + sk := coretesting.KVStoreService(coretesting.Context(), "test") + schema := collections.NewSchemaBuilder(sk) + + _, err := collections.NewIndexedMapSafe(schema, collections.NewPrefix(0), "im", collections.StringKey, colltest.MockValueCodec[company](), newInferIndex(schema)) + require.NoError(t, err) +} diff --git a/collections/indexes/indexes_test.go b/collections/indexes/indexes_test.go index 352ffb690f..470b6d4a62 100644 --- a/collections/indexes/indexes_test.go +++ b/collections/indexes/indexes_test.go @@ -3,50 +3,14 @@ package indexes import ( "context" - db "github.com/cosmos/cosmos-db" - "cosmossdk.io/core/store" + "cosmossdk.io/core/testing" ) -// 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() + ctx := coretesting.Context() + kv := coretesting.KVStoreService(ctx, "test") + return kv, ctx } type company struct { diff --git a/collections/indexes/multi.go b/collections/indexes/multi.go index 9e567aaab4..3554346906 100644 --- a/collections/indexes/multi.go +++ b/collections/indexes/multi.go @@ -8,6 +8,21 @@ import ( "cosmossdk.io/collections/codec" ) +type multiOptions struct { + uncheckedValue bool +} + +// WithMultiUncheckedValue is an option that can be passed to NewMulti to +// ignore index values different from '[]byte{}' and continue with the operation. +// This should be used only to behave nicely in case you have used values different +// from '[]byte{}' in your storage before migrating to collections. Refer to +// WithKeySetUncheckedValue for more information. +func WithMultiUncheckedValue() func(*multiOptions) { + return func(o *multiOptions) { + o.uncheckedValue = true + } +} + // 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. @@ -27,10 +42,35 @@ func NewMulti[ReferenceKey, PrimaryKey, Value any]( refCodec codec.KeyCodec[ReferenceKey], pkCodec codec.KeyCodec[PrimaryKey], getRefKeyFunc func(pk PrimaryKey, value Value) (ReferenceKey, error), + options ...func(*multiOptions), ) *Multi[ReferenceKey, PrimaryKey, Value] { + o := new(multiOptions) + for _, opt := range options { + opt(o) + } + if o.uncheckedValue { + return &Multi[ReferenceKey, PrimaryKey, Value]{ + getRefKey: getRefKeyFunc, + refKeys: collections.NewKeySet( + schema, + prefix, + name, + collections.PairKeyCodec(refCodec, pkCodec), + collections.WithKeySetUncheckedValue(), + collections.WithKeySetSecondaryIndex(), + ), + } + } + return &Multi[ReferenceKey, PrimaryKey, Value]{ getRefKey: getRefKeyFunc, - refKeys: collections.NewKeySet(schema, prefix, name, collections.PairKeyCodec(refCodec, pkCodec)), + refKeys: collections.NewKeySet( + schema, + prefix, + name, + collections.PairKeyCodec(refCodec, pkCodec), + collections.WithKeySetSecondaryIndex(), + ), } } diff --git a/collections/indexes/multi_test.go b/collections/indexes/multi_test.go index 428b880bfb..a0b1313b85 100644 --- a/collections/indexes/multi_test.go +++ b/collections/indexes/multi_test.go @@ -16,7 +16,7 @@ func TestMultiIndex(t *testing.T) { return value.City, nil }) - // we crete two reference keys for primary key 1 and 2 associated with "milan" + // we create two reference keys for primary key 1 and 2 associated with "milan" require.NoError(t, mi.Reference(ctx, 1, company{City: "milan"}, func() (company, error) { return company{}, collections.ErrNotFound })) require.NoError(t, mi.Reference(ctx, 2, company{City: "milan"}, func() (company, error) { return company{}, collections.ErrNotFound })) @@ -61,3 +61,52 @@ func TestMultiIndex(t *testing.T) { require.False(t, iter.Valid()) require.NoError(t, iter.Close()) } + +func TestMultiUnchecked(t *testing.T) { + sk, ctx := deps() + schema := collections.NewSchemaBuilder(sk) + + uncheckedMi := NewMulti(schema, collections.NewPrefix("prefix"), "multi_index", collections.StringKey, collections.Uint64Key, func(_ uint64, value company) (string, error) { + return value.City, nil + }, WithMultiUncheckedValue()) + + mi := NewMulti(schema, collections.NewPrefix("prefix"), "multi_index", collections.StringKey, collections.Uint64Key, func(_ uint64, value company) (string, error) { + return value.City, nil + }) + + rawKey, err := collections.EncodeKeyWithPrefix( + collections.NewPrefix("prefix"), + uncheckedMi.KeyCodec(), + collections.Join("milan", uint64(2))) + require.NoError(t, err) + + // set value to be something different from []byte{} + require.NoError(t, sk.OpenKVStore(ctx).Set(rawKey, []byte("something"))) + + // normal multi index will fail. + err = mi.Walk(ctx, nil, func(indexingKey string, indexedKey uint64) (stop bool, err error) { + return true, err + }) + require.ErrorIs(t, err, collections.ErrEncoding) + + // unchecked multi index will not fail. + err = uncheckedMi.Walk(ctx, nil, func(indexingKey string, indexedKey uint64) (stop bool, err error) { + require.Equal(t, "milan", indexingKey) + require.Equal(t, uint64(2), indexedKey) + return true, err + }) + require.NoError(t, err) + + // unchecked multi will also reset the value + err = mi.Reference(ctx, 2, company{City: "milan"}, func() (company, error) { + return company{ + City: "milan", + }, nil + }) + require.NoError(t, err) + + // value reset to []byte{} + rawValue, err := sk.OpenKVStore(ctx).Get(rawKey) + require.NoError(t, err) + require.Equal(t, []byte{}, rawValue) +} diff --git a/collections/indexes/reverse_pair.go b/collections/indexes/reverse_pair.go index 243b2d289a..4b85b62c6c 100644 --- a/collections/indexes/reverse_pair.go +++ b/collections/indexes/reverse_pair.go @@ -7,6 +7,21 @@ import ( "cosmossdk.io/collections/codec" ) +type reversePairOptions struct { + uncheckedValue bool +} + +// WithReversePairUncheckedValue is an option that can be passed to NewReversePair to +// ignore index values different from '[]byte{}' and continue with the operation. +// This should be used only if you are migrating to collections and have used a different +// placeholder value in your storage index keys. +// Refer to WithKeySetUncheckedValue for more information. +func WithReversePairUncheckedValue() func(*reversePairOptions) { + return func(o *reversePairOptions) { + o.uncheckedValue = true + } +} + // ReversePair is an index that is used with collections.Pair keys. It indexes objects by their second part of the key. // When the value is being indexed by collections.IndexedMap then ReversePair will create a relationship between // the second part of the primary key and the first part. @@ -31,10 +46,34 @@ func NewReversePair[Value, K1, K2 any]( prefix collections.Prefix, name string, pairCodec codec.KeyCodec[collections.Pair[K1, K2]], + options ...func(*reversePairOptions), ) *ReversePair[K1, K2, Value] { pkc := pairCodec.(pairKeyCodec[K1, K2]) + o := new(reversePairOptions) + for _, option := range options { + option(o) + } + if o.uncheckedValue { + return &ReversePair[K1, K2, Value]{ + refKeys: collections.NewKeySet( + sb, + prefix, + name, + collections.PairKeyCodec(pkc.KeyCodec2(), pkc.KeyCodec1()), + collections.WithKeySetUncheckedValue(), + collections.WithKeySetSecondaryIndex(), + ), + } + } + mi := &ReversePair[K1, K2, Value]{ - refKeys: collections.NewKeySet(sb, prefix, name, collections.PairKeyCodec(pkc.KeyCodec2(), pkc.KeyCodec1())), + refKeys: collections.NewKeySet( + sb, + prefix, + name, + collections.PairKeyCodec(pkc.KeyCodec2(), pkc.KeyCodec1()), + collections.WithKeySetSecondaryIndex(), + ), } return mi diff --git a/collections/indexes/reverse_pair_test.go b/collections/indexes/reverse_pair_test.go index e4036aaaaf..da1c1c9611 100644 --- a/collections/indexes/reverse_pair_test.go +++ b/collections/indexes/reverse_pair_test.go @@ -64,6 +64,45 @@ func TestReversePair(t *testing.T) { // assert if we remove address1 atom balance, we can no longer find it in the index err = indexedMap.Remove(ctx, collections.Join("address1", "atom")) require.NoError(t, err) - _, err = indexedMap.Indexes.Denom.MatchExact(ctx, "atom") - require.ErrorIs(t, collections.ErrInvalidIterator, err) + iter, err = indexedMap.Indexes.Denom.MatchExact(ctx, "atom") + require.NoError(t, err) + defer iter.Close() + pks, err = iter.PrimaryKeys() + require.NoError(t, err) + require.Empty(t, pks) +} + +func TestUncheckedReversePair(t *testing.T) { + sk, ctx := deps() + sb := collections.NewSchemaBuilder(sk) + prefix := collections.NewPrefix("prefix") + keyCodec := collections.PairKeyCodec(collections.StringKey, collections.StringKey) + + uncheckedRp := NewReversePair[Amount](sb, prefix, "denom_index", keyCodec, WithReversePairUncheckedValue()) + rp := NewReversePair[Amount](sb, prefix, "denom_index", keyCodec) + + rawKey, err := collections.EncodeKeyWithPrefix(prefix, uncheckedRp.KeyCodec(), collections.Join("atom", "address1")) + require.NoError(t, err) + + require.NoError(t, sk.OpenKVStore(ctx).Set(rawKey, []byte("i should not be here"))) + + // normal reverse pair fails + err = rp.Walk(ctx, nil, func(denom, address string) (bool, error) { + return false, nil + }) + require.ErrorIs(t, err, collections.ErrEncoding) + + // unchecked reverse pair succeeds + err = uncheckedRp.Walk(ctx, nil, func(indexingKey, indexedKey string) (stop bool, err error) { + require.Equal(t, "atom", indexingKey) + return true, nil + }) + require.NoError(t, err) + + // unchecked reverse pair lazily updates + err = uncheckedRp.Reference(ctx, collections.Join("address1", "atom"), 0, nil) + require.NoError(t, err) + rawValue, err := sk.OpenKVStore(ctx).Get(rawKey) + require.NoError(t, err) + require.Equal(t, []byte{}, rawValue) } diff --git a/collections/indexing.go b/collections/indexing.go new file mode 100644 index 0000000000..8a482c3981 --- /dev/null +++ b/collections/indexing.go @@ -0,0 +1,258 @@ +package collections + +import ( + "bytes" + "fmt" + "reflect" + "strings" + + "github.com/cosmos/gogoproto/proto" + "github.com/tidwall/btree" + "google.golang.org/protobuf/reflect/protoreflect" + + "cosmossdk.io/collections/codec" + "cosmossdk.io/schema" +) + +// 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 + } + + // this part below is a bit hacky, it will try to convert to a proto.Message + // in order to get any enum types inside of it. + emptyVal, err := coll.ValueCodec().Decode([]byte{}) + if err == nil { + // convert to proto.Message + pt, err := toProtoMessage(emptyVal) + if err == nil { + msgName := proto.MessageName(pt) + desc, err := proto.HybridResolver.FindDescriptorByName(protoreflect.FullName(msgName)) + if err != nil { + return schema.ModuleCodec{}, fmt.Errorf("could not find descriptor for %s: %w", msgName, err) + } + msgDesc := desc.(protoreflect.MessageDescriptor) + + // go through enum descriptors and add them to types + for i := 0; i < msgDesc.Fields().Len(); i++ { + field := msgDesc.Fields().Get(i) + enum := field.Enum() + if enum == nil { + continue + } + + enumType := schema.EnumType{ + Name: strings.ReplaceAll(string(enum.FullName()), ".", "_"), // make it compatible with schema + } + for j := 0; j < enum.Values().Len(); j++ { + val := enum.Values().Get(j) + enumType.Values = append(enumType.Values, schema.EnumValueDefinition{ + Name: string(val.Name()), + Value: int32(val.Number()), + }) + } + types = append(types, enumType) + } + + } + } + + 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.StateObjectUpdate, 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.StateObjectUpdate, error) { + // strip prefix + key := update.Key + key = key[len(c.coll.GetPrefix()):] + + k, err := c.keyDecoder(key) + if err != nil { + return []schema.StateObjectUpdate{ + {TypeName: c.coll.GetName()}, + }, err + } + + if update.Remove { + return []schema.StateObjectUpdate{ + {TypeName: c.coll.GetName(), Key: k, Delete: true}, + }, nil + } + + v, err := c.valueDecoder(update.Value) + if err != nil { + return []schema.StateObjectUpdate{ + {TypeName: c.coll.GetName(), Key: k}, + }, err + } + + return []schema.StateObjectUpdate{ + {TypeName: c.coll.GetName(), Key: k, Value: v}, + }, nil +} + +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 + } + if keyDecoder.ToSchemaType == nil { + return x, nil + } + 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 + } + + if valueDecoder.ToSchemaType == nil { + return x, nil + } + + 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 + } +} + +// toProtoMessage is a helper to convert a value to a proto.Message. +func toProtoMessage(value interface{}) (proto.Message, error) { + if value == nil { + return nil, fmt.Errorf("value is nil") + } + + // Check if the value already implements proto.Message + if msg, ok := value.(proto.Message); ok { + return msg, nil + } + + // Use reflection to handle non-pointer values + v := reflect.ValueOf(value) + if v.Kind() == reflect.Ptr { + // Already a pointer, but doesn't implement proto.Message + return nil, fmt.Errorf("value is a pointer but does not implement proto.Message") + } + + // If not a pointer, create a pointer to the value dynamically + ptr := reflect.New(v.Type()) + ptr.Elem().Set(v) + + // Assert if the pointer implements proto.Message + msg, ok := ptr.Interface().(proto.Message) + if !ok { + return nil, fmt.Errorf("value does not implement proto.Message") + } + + return msg, nil +} diff --git a/collections/iter.go b/collections/iter.go index c728f19941..ea87082a26 100644 --- a/collections/iter.go +++ b/collections/iter.go @@ -1,6 +1,7 @@ package collections import ( + "bytes" "context" "errors" "fmt" @@ -129,39 +130,51 @@ func (r *Range[K]) RangeValues() (start, end *RangeKey[K], order Order, err erro return r.start, r.end, r.order, nil } -// iteratorFromRanger generates an Iterator instance, with the proper prefixing and ranging. -// a nil Ranger can be seen as an ascending iteration over all the possible keys. -func iteratorFromRanger[K, V any](ctx context.Context, m Map[K, V], r Ranger[K]) (iter Iterator[K, V], err error) { +// parseRangeInstruction converts a Ranger into start bytes, end bytes and order of a store iteration. +func parseRangeInstruction[K any](prefix []byte, keyCodec codec.KeyCodec[K], r Ranger[K]) ([]byte, []byte, Order, error) { var ( start *RangeKey[K] end *RangeKey[K] order = OrderAscending + err error ) if r != nil { start, end, order, err = r.RangeValues() if err != nil { - return iter, err + return nil, nil, 0, err } } - startBytes := m.prefix + startBytes := prefix if start != nil { - startBytes, err = encodeRangeBound(m.prefix, m.kc, start) + startBytes, err = encodeRangeBound(prefix, keyCodec, start) if err != nil { - return iter, err + return nil, nil, 0, err } } var endBytes []byte if end != nil { - endBytes, err = encodeRangeBound(m.prefix, m.kc, end) + endBytes, err = encodeRangeBound(prefix, keyCodec, end) if err != nil { - return iter, err + return nil, nil, 0, err } } else { - endBytes = nextBytesPrefixKey(m.prefix) + endBytes = nextBytesPrefixKey(prefix) } + if bytes.Compare(startBytes, endBytes) == 1 { + return nil, nil, 0, ErrInvalidIterator + } + return startBytes, endBytes, order, nil +} +// iteratorFromRanger generates an Iterator instance, with the proper prefixing and ranging. +// a nil Ranger can be seen as an ascending iteration over all the possible keys. +func iteratorFromRanger[K, V any](ctx context.Context, m Map[K, V], r Ranger[K]) (iter Iterator[K, V], err error) { + startBytes, endBytes, order, err := parseRangeInstruction(m.prefix, m.kc, r) + if err != nil { + return Iterator[K, V]{}, err + } return newIterator(ctx, startBytes, endBytes, order, m) } @@ -182,9 +195,6 @@ func newIterator[K, V any](ctx context.Context, start, end []byte, order Order, if err != nil { return Iterator[K, V]{}, err } - if !iter.Valid() { - return Iterator[K, V]{}, ErrInvalidIterator - } return Iterator[K, V]{ kc: m.kc, @@ -194,7 +204,7 @@ func newIterator[K, V any](ctx context.Context, start, end []byte, order Order, }, nil } -// Iterator defines a generic wrapper around an storetypes.Iterator. +// Iterator defines a generic wrapper around a storetypes.Iterator. // This iterator provides automatic key and value encoding, // it assumes all the keys and values contained within the storetypes.Iterator // range are the same. diff --git a/collections/iter_test.go b/collections/iter_test.go index dfe690bb97..8721561969 100644 --- a/collections/iter_test.go +++ b/collections/iter_test.go @@ -1,6 +1,7 @@ package collections import ( + "errors" "fmt" "testing" @@ -10,11 +11,13 @@ import ( func TestIteratorBasic(t *testing.T) { sk, ctx := deps() // safety check to ensure that iteration does not cross prefix boundaries - sk.OpenKVStore(ctx).Set([]byte{0, 0}, []byte("before prefix")) - sk.OpenKVStore(ctx).Set([]byte{2, 1}, []byte("after prefix")) + err := sk.OpenKVStore(ctx).Set([]byte{0, 0}, []byte("before prefix")) + require.NoError(t, err) + err = sk.OpenKVStore(ctx).Set([]byte{2, 1}, []byte("after prefix")) + require.NoError(t, err) schemaBuilder := NewSchemaBuilder(sk) m := NewMap(schemaBuilder, NewPrefix(1), "m", StringKey, Uint64Value) - _, err := schemaBuilder.Build() + _, err = schemaBuilder.Build() require.NoError(t, err) for i := uint64(1); i <= 2; i++ { @@ -186,7 +189,7 @@ func TestWalk(t *testing.T) { }) require.NoError(t, err) - sentinelErr := fmt.Errorf("sentinel error") + sentinelErr := errors.New("sentinel error") err = m.Walk(ctx, nil, func(key, value uint64) (stop bool, err error) { require.LessOrEqual(t, key, uint64(3)) // asserts that after the number three we stop if key == 3 { diff --git a/collections/json.go b/collections/json.go new file mode 100644 index 0000000000..6b6866ecfb --- /dev/null +++ b/collections/json.go @@ -0,0 +1,58 @@ +package collections + +import ( + "encoding/json" + "fmt" + + "cosmossdk.io/collections/codec" +) + +func NewJSONValueCodec[T any]() codec.ValueCodec[T] { + return jsonValue[T]{ + typeName: fmt.Sprintf("%T", new(T)), + } +} + +type jsonValue[T any] struct { + typeName string +} + +// Decode implements codec.ValueCodec. +func (jsonValue[T]) Decode(b []byte) (T, error) { + var t T + if err := json.Unmarshal(b, &t); err != nil { + return t, err + } + + return t, nil +} + +// DecodeJSON implements codec.ValueCodec. +func (jsonValue[T]) DecodeJSON(b []byte) (T, error) { + var t T + if err := json.Unmarshal(b, &t); err != nil { + return t, err + } + + return t, nil +} + +// Encode implements codec.ValueCodec. +func (jsonValue[T]) Encode(value T) ([]byte, error) { + return json.Marshal(value) +} + +// EncodeJSON implements codec.ValueCodec. +func (jsonValue[T]) EncodeJSON(value T) ([]byte, error) { + return json.Marshal(value) +} + +// Stringify implements codec.ValueCodec. +func (jsonValue[T]) Stringify(value T) string { + return fmt.Sprintf("%v", value) +} + +// ValueType implements codec.ValueCodec. +func (jv jsonValue[T]) ValueType() string { + return fmt.Sprintf("json(%s)", jv.typeName) +} diff --git a/collections/keyset.go b/collections/keyset.go index 408086b672..ee8e5ba011 100644 --- a/collections/keyset.go +++ b/collections/keyset.go @@ -8,14 +8,53 @@ import ( "cosmossdk.io/collections/codec" ) +// WithKeySetUncheckedValue changes the behavior of the KeySet when it encounters +// a value different from '[]byte{}', by default the KeySet errors when this happens. +// This option allows to ignore the value and continue with the operation, in turn +// the value will be cleared out and set to '[]byte{}'. +// You should never use this option if you're creating a new state object from scratch. +// This should be used only to behave nicely in case you have used values different +// from '[]byte{}' in your storage before migrating to collections. +func WithKeySetUncheckedValue() func(opt *keySetOptions) { + return func(opt *keySetOptions) { + opt.uncheckedValue = true + } +} + +// WithKeySetSecondaryIndex changes the behavior of the KeySet to be a secondary index. +func WithKeySetSecondaryIndex() func(opt *keySetOptions) { + return func(opt *keySetOptions) { + opt.isSecondaryIndex = true + } +} + +type keySetOptions struct { + uncheckedValue bool + isSecondaryIndex bool +} + // KeySet builds on top of a Map and represents a collection retaining only a set // of keys and no value. It can be used, for example, in an allow list. type KeySet[K any] Map[K, NoValue] // NewKeySet returns a KeySet given a Schema, Prefix a human name for the collection // and a KeyCodec for the key K. -func NewKeySet[K any](schema *SchemaBuilder, prefix Prefix, name string, keyCodec codec.KeyCodec[K]) KeySet[K] { - return (KeySet[K])(NewMap(schema, prefix, name, keyCodec, noValueCodec)) +func NewKeySet[K any]( + schema *SchemaBuilder, + prefix Prefix, + name string, + keyCodec codec.KeyCodec[K], + options ...func(opt *keySetOptions), +) KeySet[K] { + o := new(keySetOptions) + for _, opt := range options { + opt(o) + } + vc := noValueCodec + if o.uncheckedValue { + vc = codec.NewAltValueCodec(vc, func(_ []byte) (NoValue, error) { return NoValue{}, nil }) + } + return (KeySet[K])(NewMap(schema, prefix, name, keyCodec, vc, withMapSecondaryIndex(o.isSecondaryIndex))) } // Set adds the key to the KeySet. Errors on encoding problems. @@ -57,6 +96,12 @@ func (k KeySet[K]) Walk(ctx context.Context, ranger Ranger[K], walkFunc func(key return (Map[K, NoValue])(k).Walk(ctx, ranger, func(key K, value NoValue) (bool, error) { return walkFunc(key) }) } +// Clear clears the KeySet using the provided Ranger. Refer to Map.Clear for +// behavioral documentation. +func (k KeySet[K]) Clear(ctx context.Context, ranger Ranger[K]) error { + return (Map[K, NoValue])(k).Clear(ctx, ranger) +} + func (k KeySet[K]) KeyCodec() codec.KeyCodec[K] { return (Map[K, NoValue])(k).KeyCodec() } func (k KeySet[K]) ValueCodec() codec.ValueCodec[NoValue] { return (Map[K, NoValue])(k).ValueCodec() } @@ -73,6 +118,7 @@ var noValueCodec codec.ValueCodec[NoValue] = NoValue{} const noValueValueType = "no_value" +// NoValue is a type that can be used to represent a non-existing value. type NoValue struct{} func (n NoValue) EncodeJSON(_ NoValue) ([]byte, error) { @@ -92,7 +138,7 @@ func (NoValue) Encode(_ NoValue) ([]byte, error) { func (NoValue) Decode(b []byte) (NoValue, error) { if !bytes.Equal(b, []byte{}) { - return NoValue{}, fmt.Errorf("%w: invalid value, wanted an empty non-nil byte slice", ErrEncoding) + return NoValue{}, fmt.Errorf("%w: invalid value, wanted an empty non-nil byte slice, got: %x", ErrEncoding, b) } return NoValue{}, nil } diff --git a/collections/keyset_test.go b/collections/keyset_test.go index 6fe3e7eedf..cabc2ead86 100644 --- a/collections/keyset_test.go +++ b/collections/keyset_test.go @@ -67,3 +67,34 @@ func Test_noValue(t *testing.T) { _, err = noValueCodec.Decode([]byte("bad")) require.ErrorIs(t, err, ErrEncoding) } + +func TestUncheckedKeySet(t *testing.T) { + sk, ctx := deps() + schema := NewSchemaBuilder(sk) + uncheckedKs := NewKeySet(schema, NewPrefix("keyset"), "keyset", StringKey, WithKeySetUncheckedValue()) + ks := NewKeySet(schema, NewPrefix("keyset"), "keyset", StringKey) + // we set a NoValue unfriendly value. + require.NoError(t, sk.OpenKVStore(ctx).Set([]byte("keyset1"), []byte("A"))) + require.NoError(t, sk.OpenKVStore(ctx).Set([]byte("keyset2"), []byte("B"))) + + // the standard KeySet errors here, because it doesn't like the fact that the value is []byte("A") + // and not []byte{}. + err := ks.Walk(ctx, nil, func(key string) (stop bool, err error) { + return true, nil + }) + require.ErrorIs(t, err, ErrEncoding) + + // the unchecked KeySet doesn't care about the value, so it works. + err = uncheckedKs.Walk(ctx, nil, func(key string) (stop bool, err error) { + require.Equal(t, "1", key) + return true, nil + }) + require.NoError(t, err) + + // now we set it again + require.NoError(t, uncheckedKs.Set(ctx, "1")) + // and we will see that the value which was []byte("A") has been cleared to be []byte{} + raw, err := sk.OpenKVStore(ctx).Get([]byte("keyset1")) + require.NoError(t, err) + require.Equal(t, []byte{}, raw) +} diff --git a/collections/lookup_map.go b/collections/lookup_map.go new file mode 100644 index 0000000000..fcb7c4466e --- /dev/null +++ b/collections/lookup_map.go @@ -0,0 +1,104 @@ +package collections + +import ( + "context" + "fmt" + + "cosmossdk.io/collections/codec" +) + +// LookupMap represents a map that is not iterable. +type LookupMap[K, V any] Map[K, V] + +// NewLookupMap creates a new LookupMap. +func NewLookupMap[K, V any]( + schemaBuilder *SchemaBuilder, + prefix Prefix, + name string, + keyCodec codec.KeyCodec[K], + valueCodec codec.ValueCodec[V], +) LookupMap[K, V] { + m := LookupMap[K, V](NewMap[K, V](schemaBuilder, prefix, name, keyCodec, valueCodec)) + return m +} + +// GetName returns the name of the collection. +func (m LookupMap[K, V]) GetName() string { + return m.name +} + +// GetPrefix returns the prefix of the collection. +func (m LookupMap[K, V]) GetPrefix() []byte { + return m.prefix +} + +// Set maps the provided value to the provided key in the store. +// Errors with ErrEncoding if key or value encoding fails. +func (m LookupMap[K, V]) Set(ctx context.Context, key K, value V) error { + bytesKey, err := EncodeKeyWithPrefix(m.prefix, m.kc, key) + if err != nil { + return err + } + + valueBytes, err := m.vc.Encode(value) + if err != nil { + return fmt.Errorf("%w: value encode: %w", ErrEncoding, err) + } + + kvStore := m.sa(ctx) + return kvStore.Set(bytesKey, valueBytes) +} + +// Get returns the value associated with the provided key, +// errors with ErrNotFound if the key does not exist, or +// with ErrEncoding if the key or value decoding fails. +func (m LookupMap[K, V]) Get(ctx context.Context, key K) (v V, err error) { + bytesKey, err := EncodeKeyWithPrefix(m.prefix, m.kc, key) + if err != nil { + return v, err + } + + kvStore := m.sa(ctx) + valueBytes, err := kvStore.Get(bytesKey) + if err != nil { + return v, err + } + if valueBytes == nil { + return v, fmt.Errorf("%w: key '%s' of type %s", ErrNotFound, m.kc.Stringify(key), m.vc.ValueType()) + } + + v, err = m.vc.Decode(valueBytes) + if err != nil { + return v, fmt.Errorf("%w: value decode: %w", ErrEncoding, err) + } + return v, nil +} + +// Has reports whether the key is present in storage or not. +// Errors with ErrEncoding if key encoding fails. +func (m LookupMap[K, V]) Has(ctx context.Context, key K) (bool, error) { + bytesKey, err := EncodeKeyWithPrefix(m.prefix, m.kc, key) + if err != nil { + return false, err + } + kvStore := m.sa(ctx) + return kvStore.Has(bytesKey) +} + +// Remove removes the key from the storage. +// Errors with ErrEncoding if key encoding fails. +// If the key does not exist then this is a no-op. +func (m LookupMap[K, V]) Remove(ctx context.Context, key K) error { + bytesKey, err := EncodeKeyWithPrefix(m.prefix, m.kc, key) + if err != nil { + return err + } + kvStore := m.sa(ctx) + return kvStore.Delete(bytesKey) +} + +// KeyCodec returns the Map's KeyCodec. +func (m LookupMap[K, V]) KeyCodec() codec.KeyCodec[K] { return m.kc } + +// ValueCodec returns the Map's ValueCodec. +func (m LookupMap[K, V]) ValueCodec() codec.ValueCodec[V] { return m.vc } diff --git a/collections/lookup_map_test.go b/collections/lookup_map_test.go new file mode 100644 index 0000000000..74d3cbac6b --- /dev/null +++ b/collections/lookup_map_test.go @@ -0,0 +1,42 @@ +package collections_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/collections" + "cosmossdk.io/core/testing" +) + +func TestLookupMap(t *testing.T) { + ctx := coretesting.Context() + sk := coretesting.KVStoreService(ctx, "test") + schema := collections.NewSchemaBuilder(sk) + + lm := collections.NewLookupMap(schema, collections.NewPrefix("hi"), "lm", collections.Uint64Key, collections.Uint64Value) + _, err := schema.Build() + require.NoError(t, err) + + // test not has + has, err := lm.Has(ctx, 1) + require.NoError(t, err) + require.False(t, has) + // test get error + _, err = lm.Get(ctx, 1) + require.ErrorIs(t, err, collections.ErrNotFound) + + // test set/get + err = lm.Set(ctx, 1, 100) + require.NoError(t, err) + v, err := lm.Get(ctx, 1) + require.NoError(t, err) + require.Equal(t, uint64(100), v) + + // test remove + err = lm.Remove(ctx, 1) + require.NoError(t, err) + has, err = lm.Has(ctx, 1) + require.NoError(t, err) + require.False(t, has) +} diff --git a/collections/map.go b/collections/map.go index 525110cdf9..3fda806df3 100644 --- a/collections/map.go +++ b/collections/map.go @@ -1,6 +1,7 @@ package collections import ( + "bytes" "context" "fmt" @@ -19,6 +20,22 @@ 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 +} + +// withMapSecondaryIndex changes the behavior of the Map to be a secondary index. +func withMapSecondaryIndex(isSecondaryIndex bool) func(opt *mapOptions) { + return func(opt *mapOptions) { + opt.isSecondaryIndex = isSecondaryIndex + } +} + +type mapOptions struct { + isSecondaryIndex bool } // NewMap returns a Map given a StoreKey, a Prefix, human-readable name and the relative value and key encoders. @@ -30,13 +47,19 @@ func NewMap[K, V any]( name string, keyCodec codec.KeyCodec[K], valueCodec codec.ValueCodec[V], + options ...func(opt *mapOptions), ) Map[K, V] { + o := new(mapOptions) + for _, opt := range options { + opt(o) + } m := Map[K, V]{ - kc: keyCodec, - vc: valueCodec, - sa: schemaBuilder.schema.storeAccessor, - prefix: prefix.Bytes(), - name: name, + kc: keyCodec, + vc: valueCodec, + sa: schemaBuilder.schema.storeAccessor, + prefix: prefix.Bytes(), + name: name, + isSecondaryIndex: o.isSecondaryIndex, } schemaBuilder.addCollection(collectionImpl[K, V]{m}) return m @@ -60,12 +83,11 @@ func (m Map[K, V]) Set(ctx context.Context, key K, value V) error { valueBytes, err := m.vc.Encode(value) if err != nil { - return fmt.Errorf("%w: value encode: %s", ErrEncoding, err) // TODO: use multi err wrapping in go1.20: https://github.com/golang/go/issues/53435 + return fmt.Errorf("%w: value encode: %w", ErrEncoding, err) } kvStore := m.sa(ctx) - kvStore.Set(bytesKey, valueBytes) - return nil + return kvStore.Set(bytesKey, valueBytes) } // Get returns the value associated with the provided key, @@ -88,7 +110,7 @@ func (m Map[K, V]) Get(ctx context.Context, key K) (v V, err error) { v, err = m.vc.Decode(valueBytes) if err != nil { - return v, fmt.Errorf("%w: value decode: %s", ErrEncoding, err) // TODO: use multi err wrapping in go1.20: https://github.com/golang/go/issues/53435 + return v, fmt.Errorf("%w: value decode: %w", ErrEncoding, err) } return v, nil } @@ -149,6 +171,58 @@ func (m Map[K, V]) Walk(ctx context.Context, ranger Ranger[K], walkFunc func(key return nil } +// Clear clears the collection contained within the provided key range. +// A nil ranger equals to clearing the whole collection. +// NOTE: this API needs to be used with care, considering that as of today +// cosmos-sdk stores the deletion records to be committed in a memory cache, +// clearing a lot of data might make the node go OOM. +func (m Map[K, V]) Clear(ctx context.Context, ranger Ranger[K]) error { + startBytes, endBytes, _, err := parseRangeInstruction(m.prefix, m.kc, ranger) + if err != nil { + return err + } + return deleteDomain(m.sa(ctx), startBytes, endBytes) +} + +const clearBatchSize = 10000 + +// deleteDomain deletes the domain of an iterator, the key difference +// is that it uses batches to clear the store meaning that it will read +// the keys within the domain close the iterator and then delete them. +func deleteDomain(s store.KVStore, start, end []byte) error { + for { + iter, err := s.Iterator(start, end) + if err != nil { + return err + } + + keys := make([][]byte, 0, clearBatchSize) + for ; iter.Valid() && len(keys) < clearBatchSize; iter.Next() { + keys = append(keys, iter.Key()) + } + + // we close the iterator here instead of deferring + err = iter.Close() + if err != nil { + return err + } + + for _, key := range keys { + err = s.Delete(key) + if err != nil { + return err + } + } + + // If we've retrieved less than the batchSize, we're done. + if len(keys) < clearBatchSize { + break + } + } + + return nil +} + // IterateRaw iterates over the collection. The iteration range is untyped, it uses raw // bytes. The resulting Iterator is typed. // A nil start iterates from the first key contained in the collection. @@ -164,6 +238,10 @@ func (m Map[K, V]) IterateRaw(ctx context.Context, start, end []byte, order Orde prefixedEnd = append(m.prefix, end...) } + if bytes.Compare(prefixedStart, prefixedEnd) == 1 { + return Iterator[K, V]{}, ErrInvalidIterator + } + s := m.sa(ctx) var ( storeIter store.Iterator @@ -181,9 +259,6 @@ func (m Map[K, V]) IterateRaw(ctx context.Context, start, end []byte, order Orde return Iterator[K, V]{}, err } - if !storeIter.Valid() { - return Iterator[K, V]{}, ErrInvalidIterator - } return Iterator[K, V]{ kc: m.kc, vc: m.vc, @@ -209,7 +284,7 @@ func EncodeKeyWithPrefix[K any](prefix []byte, kc codec.KeyCodec[K], key K) ([]b // put key _, err := kc.Encode(keyBytes[prefixLen:], key) if err != nil { - return nil, fmt.Errorf("%w: key encode: %s", ErrEncoding, err) // TODO: use multi err wrapping in go1.20: https://github.com/golang/go/issues/53435 + return nil, fmt.Errorf("%w: key encode: %w", ErrEncoding, err) } return keyBytes, nil } diff --git a/collections/map_test.go b/collections/map_test.go index 2887693d53..5f1ae176df 100644 --- a/collections/map_test.go +++ b/collections/map_test.go @@ -1,6 +1,8 @@ package collections import ( + "context" + "errors" "testing" "github.com/stretchr/testify/require" @@ -36,6 +38,42 @@ func TestMap(t *testing.T) { require.False(t, has) } +func TestMap_Clear(t *testing.T) { + makeTest := func() (context.Context, Map[uint64, uint64]) { + sk, ctx := deps() + m := NewMap(NewSchemaBuilder(sk), NewPrefix(0), "test", Uint64Key, Uint64Value) + for i := uint64(0); i < clearBatchSize*2; i++ { + require.NoError(t, m.Set(ctx, i, i)) + } + return ctx, m + } + + t.Run("nil ranger", func(t *testing.T) { + ctx, m := makeTest() + err := m.Clear(ctx, nil) + require.NoError(t, err) + err = m.Walk(ctx, nil, func(key, value uint64) (bool, error) { + return false, errors.New("should never be called") + }) + require.NoError(t, err) + }) + + t.Run("custom ranger", func(t *testing.T) { + ctx, m := makeTest() + // delete from 0 to 100 + err := m.Clear(ctx, new(Range[uint64]).StartInclusive(0).EndInclusive(100)) + require.NoError(t, err) + + iter, err := m.Iterate(ctx, nil) + require.NoError(t, err) + keys, err := iter.Keys() + require.NoError(t, err) + require.Len(t, keys, clearBatchSize*2-101) + require.Equal(t, keys[0], uint64(101)) + require.Equal(t, keys[len(keys)-1], uint64(clearBatchSize*2-1)) + }) +} + func TestMap_IterateRaw(t *testing.T) { sk, ctx := deps() // safety check to ensure prefix boundaries are not crossed @@ -69,6 +107,15 @@ func TestMap_IterateRaw(t *testing.T) { keys, err = iter.Keys() require.NoError(t, err) require.Equal(t, []uint64{2, 1, 0}, keys) + + // test invalid iter + _, err = m.IterateRaw(ctx, []byte{0x2, 0x0}, []byte{0x0, 0x0}, OrderAscending) + require.ErrorIs(t, err, ErrInvalidIterator) + + // test on empty collection iterating does not error + require.NoError(t, m.Clear(ctx, nil)) + _, err = m.IterateRaw(ctx, nil, nil, OrderAscending) + require.NoError(t, err) } func Test_encodeKey(t *testing.T) { diff --git a/collections/naming_test.go b/collections/naming_test.go new file mode 100644 index 0000000000..39b275d920 --- /dev/null +++ b/collections/naming_test.go @@ -0,0 +1,59 @@ +package collections + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/collections/codec" +) + +func TestNaming(t *testing.T) { + expectKeyCodecName(t, "u16", Uint16Key.WithName("u16")) + expectKeyCodecName(t, "u32", Uint32Key.WithName("u32")) + expectKeyCodecName(t, "u64", Uint64Key.WithName("u64")) + expectKeyCodecName(t, "i32", Int32Key.WithName("i32")) + expectKeyCodecName(t, "i64", Int64Key.WithName("i64")) + expectKeyCodecName(t, "str", StringKey.WithName("str")) + expectKeyCodecName(t, "bytes", BytesKey.WithName("bytes")) + expectKeyCodecName(t, "bool", BoolKey.WithName("bool")) + + expectValueCodecName(t, "vu16", Uint16Value.WithName("vu16")) + expectValueCodecName(t, "vu32", Uint32Value.WithName("vu32")) + expectValueCodecName(t, "vu64", Uint64Value.WithName("vu64")) + expectValueCodecName(t, "vi32", Int32Value.WithName("vi32")) + expectValueCodecName(t, "vi64", Int64Value.WithName("vi64")) + expectValueCodecName(t, "vstr", StringValue.WithName("vstr")) + expectValueCodecName(t, "vbytes", BytesValue.WithName("vbytes")) + expectValueCodecName(t, "vbool", BoolValue.WithName("vbool")) + + expectKeyCodecNames(t, NamedPairKeyCodec[bool, string]("abc", BoolKey, "def", StringKey), "abc", "def") + expectKeyCodecNames(t, NamedTripleKeyCodec[bool, string, int32]("abc", BoolKey, "def", StringKey, "ghi", Int32Key), "abc", "def", "ghi") + expectKeyCodecNames(t, NamedQuadKeyCodec[bool, string, int32, uint64]("abc", BoolKey, "def", StringKey, "ghi", Int32Key, "jkl", Uint64Key), "abc", "def", "ghi", "jkl") +} + +func expectKeyCodecName[T any](t *testing.T, name string, cdc codec.KeyCodec[T]) { + t.Helper() + schema, err := codec.KeySchemaCodec(cdc) + require.NoError(t, err) + require.Equal(t, 1, len(schema.Fields)) + require.Equal(t, name, schema.Fields[0].Name) +} + +func expectValueCodecName[T any](t *testing.T, name string, cdc codec.ValueCodec[T]) { + t.Helper() + schema, err := codec.ValueSchemaCodec(cdc) + require.NoError(t, err) + require.Equal(t, 1, len(schema.Fields)) + require.Equal(t, name, schema.Fields[0].Name) +} + +func expectKeyCodecNames[T any](t *testing.T, cdc codec.KeyCodec[T], names ...string) { + t.Helper() + schema, err := codec.KeySchemaCodec(cdc) + require.NoError(t, err) + require.Equal(t, len(names), len(schema.Fields)) + for i, name := range names { + require.Equal(t, name, schema.Fields[i].Name) + } +} diff --git a/collections/pair.go b/collections/pair.go index f12aaac1b5..bf22a6711d 100644 --- a/collections/pair.go +++ b/collections/pair.go @@ -6,6 +6,7 @@ import ( "strings" "cosmossdk.io/collections/codec" + "cosmossdk.io/schema" ) // Pair defines a key composed of two keys. @@ -54,9 +55,22 @@ func PairKeyCodec[K1, K2 any](keyCodec1 codec.KeyCodec[K1], keyCodec2 codec.KeyC } } +// NamedPairKeyCodec instantiates a new KeyCodec instance that can encode the Pair, given the KeyCodec of the +// first part of the key and the KeyCodec of the second part of the key. +// It also provides names for the keys which are used for indexing purposes. +func NamedPairKeyCodec[K1, K2 any](key1Name string, keyCodec1 codec.KeyCodec[K1], key2Name string, keyCodec2 codec.KeyCodec[K2]) codec.KeyCodec[Pair[K1, K2]] { + return pairKeyCodec[K1, K2]{ + key1Name: key1Name, + key2Name: key2Name, + keyCodec1: keyCodec1, + keyCodec2: keyCodec2, + } +} + type pairKeyCodec[K1, K2 any] struct { - keyCodec1 codec.KeyCodec[K1] - keyCodec2 codec.KeyCodec[K2] + key1Name, key2Name string + keyCodec1 codec.KeyCodec[K1] + keyCodec2 codec.KeyCodec[K2] } func (p pairKeyCodec[K1, K2]) KeyCodec1() codec.KeyCodec[K1] { return p.keyCodec1 } @@ -216,6 +230,94 @@ func (p pairKeyCodec[K1, K2]) DecodeJSON(b []byte) (Pair[K1, K2], error) { return Join(k1, k2), nil } +func (p pairKeyCodec[K1, K2]) Name() string { + return fmt.Sprintf("%s,%s", p.key1Name, p.key2Name) +} + +func (p pairKeyCodec[K1, K2]) SchemaCodec() (codec.SchemaCodec[Pair[K1, K2]], error) { + field1, err := getNamedKeyField(p.keyCodec1, p.key1Name) + if err != nil { + return codec.SchemaCodec[Pair[K1, K2]]{}, fmt.Errorf("error getting key1 field: %w", err) + } + + field2, err := getNamedKeyField(p.keyCodec2, p.key2Name) + if err != nil { + return codec.SchemaCodec[Pair[K1, K2]]{}, fmt.Errorf("error getting key2 field: %w", err) + } + + codec1, err := codec.KeySchemaCodec(p.keyCodec1) + if err != nil { + return codec.SchemaCodec[Pair[K1, K2]]{}, fmt.Errorf("error getting key1 schema codec: %w", err) + } + + codec2, err := codec.KeySchemaCodec(p.keyCodec2) + if err != nil { + return codec.SchemaCodec[Pair[K1, K2]]{}, fmt.Errorf("error getting key2 schema codec: %w", err) + } + + return codec.SchemaCodec[Pair[K1, K2]]{ + Fields: []schema.Field{field1, field2}, + ToSchemaType: func(pair Pair[K1, K2]) (any, error) { + k1, err := toKeySchemaType(codec1, pair.K1()) + if err != nil { + return nil, err + } + k2, err := toKeySchemaType(codec2, pair.K2()) + if err != nil { + return nil, err + } + return []interface{}{k1, k2}, nil + }, + FromSchemaType: func(a any) (Pair[K1, K2], error) { + aSlice, ok := a.([]interface{}) + if !ok || len(aSlice) != 2 { + return Pair[K1, K2]{}, fmt.Errorf("expected slice of length 2, got %T", a) + } + k1, err := fromKeySchemaType(codec1, aSlice[0]) + if err != nil { + return Pair[K1, K2]{}, err + } + k2, err := fromKeySchemaType(codec2, aSlice[1]) + if err != nil { + return Pair[K1, K2]{}, err + } + return Join(k1, k2), nil + }, + }, nil +} + +func getNamedKeyField[T any](keyCdc codec.KeyCodec[T], name string) (schema.Field, error) { + keySchema, err := codec.KeySchemaCodec(keyCdc) + if err != nil { + return schema.Field{}, err + } + if len(keySchema.Fields) != 1 { + return schema.Field{}, fmt.Errorf("key schema in composite key has more than one field, got %v", keySchema.Fields) + } + field := keySchema.Fields[0] + field.Name = name + return field, nil +} + +func toKeySchemaType[T any](cdc codec.SchemaCodec[T], key T) (any, error) { + if cdc.ToSchemaType != nil { + return cdc.ToSchemaType(key) + } + return key, nil +} + +func fromKeySchemaType[T any](cdc codec.SchemaCodec[T], key any) (T, error) { + if cdc.FromSchemaType != nil { + return cdc.FromSchemaType(key) + } + tKey, ok := key.(T) + if !ok { + var zero T + return zero, fmt.Errorf("expected type %T, got %T", zero, key) + } + return tKey, nil +} + // NewPrefixUntilPairRange defines a collection query which ranges until the provided Pair prefix. // Unstable: this API might change in the future. func NewPrefixUntilPairRange[K1, K2 any](prefix K1) *PairRange[K1, K2] { diff --git a/collections/protocodec/collections.go b/collections/protocodec/collections.go new file mode 100644 index 0000000000..f989cddbcd --- /dev/null +++ b/collections/protocodec/collections.go @@ -0,0 +1,137 @@ +package protocodec + +import ( + "fmt" + + "github.com/cosmos/gogoproto/proto" + gogotypes "github.com/cosmos/gogoproto/types" + "google.golang.org/protobuf/encoding/protojson" + protov2 "google.golang.org/protobuf/proto" + + "cosmossdk.io/collections" + collcodec "cosmossdk.io/collections/codec" + corecodec "cosmossdk.io/core/codec" +) + +// BoolValue implements a ValueCodec that saves the bool value +// as if it was a prototypes.BoolValue. Required for backwards +// compatibility of state. +var BoolValue collcodec.ValueCodec[bool] = boolValue{} + +type boolValue struct{} + +func (boolValue) Encode(value bool) ([]byte, error) { + return (&gogotypes.BoolValue{Value: value}).Marshal() +} + +func (boolValue) Decode(b []byte) (bool, error) { + v := new(gogotypes.BoolValue) + err := v.Unmarshal(b) + return v.Value, err +} + +func (boolValue) EncodeJSON(value bool) ([]byte, error) { + return collections.BoolValue.EncodeJSON(value) +} + +func (boolValue) DecodeJSON(b []byte) (bool, error) { + return collections.BoolValue.DecodeJSON(b) +} + +func (boolValue) Stringify(value bool) string { + return collections.BoolValue.Stringify(value) +} + +func (boolValue) ValueType() string { + return "protobuf/bool" +} + +type protoMessage[T any] interface { + *T + proto.Message +} + +// CollValue inits a collections.ValueCodec for a generic gogo protobuf message. +func CollValue[T any, PT protoMessage[T]](cdc interface { + Marshal(proto.Message) ([]byte, error) + Unmarshal([]byte, proto.Message) error +}, +) collcodec.ValueCodec[T] { + return &collValue[T, PT]{cdc.(corecodec.Codec), proto.MessageName(PT(new(T)))} +} + +type collValue[T any, PT protoMessage[T]] struct { + cdc corecodec.Codec + messageName string +} + +func (c collValue[T, PT]) Encode(value T) ([]byte, error) { + return c.cdc.Marshal(PT(&value)) +} + +func (c collValue[T, PT]) Decode(b []byte) (value T, err error) { + err = c.cdc.Unmarshal(b, PT(&value)) + return value, err +} + +func (c collValue[T, PT]) EncodeJSON(value T) ([]byte, error) { + return c.cdc.MarshalJSON(PT(&value)) +} + +func (c collValue[T, PT]) DecodeJSON(b []byte) (value T, err error) { + err = c.cdc.UnmarshalJSON(b, PT(&value)) + return +} + +func (c collValue[T, PT]) Stringify(value T) string { + return PT(&value).String() +} + +func (c collValue[T, PT]) ValueType() string { + return "github.com/cosmos/gogoproto/" + c.messageName +} + +type protoMessageV2[T any] interface { + *T + protov2.Message +} + +// CollValueV2 is used for protobuf values of the newest google.golang.org/protobuf API. +func CollValueV2[T any, PT protoMessageV2[T]]() collcodec.ValueCodec[PT] { + return &collValue2[T, PT]{ + messageName: string(PT(new(T)).ProtoReflect().Descriptor().FullName()), + } +} + +type collValue2[T any, PT protoMessageV2[T]] struct { + messageName string +} + +func (c collValue2[T, PT]) Encode(value PT) ([]byte, error) { + protov2MarshalOpts := protov2.MarshalOptions{Deterministic: true} + return protov2MarshalOpts.Marshal(value) +} + +func (c collValue2[T, PT]) Decode(b []byte) (PT, error) { + var value T + err := protov2.Unmarshal(b, PT(&value)) + return &value, err +} + +func (c collValue2[T, PT]) EncodeJSON(value PT) ([]byte, error) { + return protojson.Marshal(value) +} + +func (c collValue2[T, PT]) DecodeJSON(b []byte) (PT, error) { + var value T + err := protojson.Unmarshal(b, PT(&value)) + return &value, err +} + +func (c collValue2[T, PT]) Stringify(value PT) string { + return fmt.Sprintf("%v", value) +} + +func (c collValue2[T, PT]) ValueType() string { + return "google.golang.org/protobuf/" + c.messageName +} diff --git a/collections/protocodec/collections_test.go b/collections/protocodec/collections_test.go new file mode 100644 index 0000000000..cf2af632f7 --- /dev/null +++ b/collections/protocodec/collections_test.go @@ -0,0 +1,55 @@ +package protocodec_test + +import ( + "testing" + + gogotypes "github.com/cosmos/gogoproto/types" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/wrapperspb" + + "cosmossdk.io/collections/colltest" + codec "cosmossdk.io/collections/protocodec" +) + +func TestCollectionsCorrectness(t *testing.T) { + t.Run("CollValueV2", func(t *testing.T) { + // NOTE: we cannot use colltest.TestValueCodec because protov2 has different + // compare semantics than protov1. We need to use protocmp.Transform() alongside + // cmp to ensure equality. + encoder := codec.CollValueV2[wrapperspb.UInt64Value]() + value := &wrapperspb.UInt64Value{Value: 500} + encodedValue, err := encoder.Encode(value) + require.NoError(t, err) + decodedValue, err := encoder.Decode(encodedValue) + require.NoError(t, err) + require.True(t, cmp.Equal(value, decodedValue, protocmp.Transform()), "encoding and decoding produces different values") + + encodedJSONValue, err := encoder.EncodeJSON(value) + require.NoError(t, err) + decodedJSONValue, err := encoder.DecodeJSON(encodedJSONValue) + require.NoError(t, err) + require.True(t, cmp.Equal(value, decodedJSONValue, protocmp.Transform()), "encoding and decoding produces different values") + require.NotEmpty(t, encoder.ValueType()) + + _ = encoder.Stringify(value) + }) + + t.Run("BoolValue", func(t *testing.T) { + colltest.TestValueCodec(t, codec.BoolValue, true) + colltest.TestValueCodec(t, codec.BoolValue, false) + + // asserts produced bytes are equal + valueAssert := func(b bool) { + wantBytes, err := (&gogotypes.BoolValue{Value: b}).Marshal() + require.NoError(t, err) + gotBytes, err := codec.BoolValue.Encode(b) + require.NoError(t, err) + require.Equal(t, wantBytes, gotBytes) + } + + valueAssert(true) + valueAssert(false) + }) +} diff --git a/collections/quad.go b/collections/quad.go new file mode 100644 index 0000000000..48b98a5265 --- /dev/null +++ b/collections/quad.go @@ -0,0 +1,435 @@ +package collections + +import ( + "encoding/json" + "fmt" + "strings" + + "cosmossdk.io/collections/codec" + "cosmossdk.io/schema" +) + +// Quad defines a multipart key composed of four keys. +type Quad[K1, K2, K3, K4 any] struct { + k1 *K1 + k2 *K2 + k3 *K3 + k4 *K4 +} + +// Join4 instantiates a new Quad instance composed of the four provided keys, in order. +func Join4[K1, K2, K3, K4 any](k1 K1, k2 K2, k3 K3, k4 K4) Quad[K1, K2, K3, K4] { + return Quad[K1, K2, K3, K4]{&k1, &k2, &k3, &k4} +} + +// K1 returns the first part of the key. If nil, the zero value is returned. +func (t Quad[K1, K2, K3, K4]) K1() (x K1) { + if t.k1 != nil { + return *t.k1 + } + return x +} + +// K2 returns the second part of the key. If nil, the zero value is returned. +func (t Quad[K1, K2, K3, K4]) K2() (x K2) { + if t.k2 != nil { + return *t.k2 + } + return x +} + +// K3 returns the third part of the key. If nil, the zero value is returned. +func (t Quad[K1, K2, K3, K4]) K3() (x K3) { + if t.k3 != nil { + return *t.k3 + } + return x +} + +// K4 returns the fourth part of the key. If nil, the zero value is returned. +func (t Quad[K1, K2, K3, K4]) K4() (x K4) { + if t.k4 != nil { + return *t.k4 + } + return x +} + +// QuadPrefix creates a new Quad instance composed only of the first part of the key. +func QuadPrefix[K1, K2, K3, K4 any](k1 K1) Quad[K1, K2, K3, K4] { + return Quad[K1, K2, K3, K4]{k1: &k1} +} + +// QuadSuperPrefix creates a new Quad instance composed only of the first two parts of the key. +func QuadSuperPrefix[K1, K2, K3, K4 any](k1 K1, k2 K2) Quad[K1, K2, K3, K4] { + return Quad[K1, K2, K3, K4]{k1: &k1, k2: &k2} +} + +// QuadSuperPrefix3 creates a new Quad instance composed only of the first three parts of the key. +func QuadSuperPrefix3[K1, K2, K3, K4 any](k1 K1, k2 K2, k3 K3) Quad[K1, K2, K3, K4] { + return Quad[K1, K2, K3, K4]{k1: &k1, k2: &k2, k3: &k3} +} + +// QuadKeyCodec instantiates a new KeyCodec instance that can encode the Quad, given +// the KeyCodecs of the four parts of the key, in order. +func QuadKeyCodec[K1, K2, K3, K4 any](keyCodec1 codec.KeyCodec[K1], keyCodec2 codec.KeyCodec[K2], keyCodec3 codec.KeyCodec[K3], keyCodec4 codec.KeyCodec[K4]) codec.KeyCodec[Quad[K1, K2, K3, K4]] { + return quadKeyCodec[K1, K2, K3, K4]{ + keyCodec1: keyCodec1, + keyCodec2: keyCodec2, + keyCodec3: keyCodec3, + keyCodec4: keyCodec4, + } +} + +// NamedQuadKeyCodec instantiates a new KeyCodec instance that can encode the Quad, given +// the KeyCodecs of the four parts of the key, in order. +// The provided names are used to identify the parts of the key in the schema for indexing. +func NamedQuadKeyCodec[K1, K2, K3, K4 any](key1Name string, keyCodec1 codec.KeyCodec[K1], key2Name string, keyCodec2 codec.KeyCodec[K2], key3Name string, keyCodec3 codec.KeyCodec[K3], key4Name string, keyCodec4 codec.KeyCodec[K4]) codec.KeyCodec[Quad[K1, K2, K3, K4]] { + return quadKeyCodec[K1, K2, K3, K4]{ + name1: key1Name, + keyCodec1: keyCodec1, + name2: key2Name, + keyCodec2: keyCodec2, + name3: key3Name, + keyCodec3: keyCodec3, + name4: key4Name, + keyCodec4: keyCodec4, + } +} + +type quadKeyCodec[K1, K2, K3, K4 any] struct { + name1, name2, name3, name4 string + keyCodec1 codec.KeyCodec[K1] + keyCodec2 codec.KeyCodec[K2] + keyCodec3 codec.KeyCodec[K3] + keyCodec4 codec.KeyCodec[K4] +} + +type jsonQuadKey [4]json.RawMessage + +// EncodeJSON encodes Quads to json +func (t quadKeyCodec[K1, K2, K3, K4]) EncodeJSON(value Quad[K1, K2, K3, K4]) ([]byte, error) { + json1, err := t.keyCodec1.EncodeJSON(*value.k1) + if err != nil { + return nil, err + } + + json2, err := t.keyCodec2.EncodeJSON(*value.k2) + if err != nil { + return nil, err + } + + json3, err := t.keyCodec3.EncodeJSON(*value.k3) + if err != nil { + return nil, err + } + + json4, err := t.keyCodec4.EncodeJSON(*value.k4) + if err != nil { + return nil, err + } + + return json.Marshal(jsonQuadKey{json1, json2, json3, json4}) +} + +// DecodeJSON decodes json to Quads +func (t quadKeyCodec[K1, K2, K3, K4]) DecodeJSON(b []byte) (Quad[K1, K2, K3, K4], error) { + var jsonKey jsonQuadKey + err := json.Unmarshal(b, &jsonKey) + if err != nil { + return Quad[K1, K2, K3, K4]{}, err + } + + key1, err := t.keyCodec1.DecodeJSON(jsonKey[0]) + if err != nil { + return Quad[K1, K2, K3, K4]{}, err + } + + key2, err := t.keyCodec2.DecodeJSON(jsonKey[1]) + if err != nil { + return Quad[K1, K2, K3, K4]{}, err + } + + key3, err := t.keyCodec3.DecodeJSON(jsonKey[2]) + if err != nil { + return Quad[K1, K2, K3, K4]{}, err + } + + key4, err := t.keyCodec4.DecodeJSON(jsonKey[3]) + if err != nil { + return Quad[K1, K2, K3, K4]{}, err + } + + return Join4(key1, key2, key3, key4), nil +} + +// Stringify converts Quads to string +func (t quadKeyCodec[K1, K2, K3, K4]) Stringify(key Quad[K1, K2, K3, K4]) string { + b := new(strings.Builder) + b.WriteByte('(') + if key.k1 != nil { + b.WriteByte('"') + b.WriteString(t.keyCodec1.Stringify(*key.k1)) + b.WriteByte('"') + } else { + b.WriteString("") + } + + b.WriteString(", ") + if key.k2 != nil { + b.WriteByte('"') + b.WriteString(t.keyCodec2.Stringify(*key.k2)) + b.WriteByte('"') + } else { + b.WriteString("") + } + + b.WriteString(", ") + if key.k3 != nil { + b.WriteByte('"') + b.WriteString(t.keyCodec3.Stringify(*key.k3)) + b.WriteByte('"') + } else { + b.WriteString("") + } + + b.WriteString(", ") + if key.k4 != nil { + b.WriteByte('"') + b.WriteString(t.keyCodec4.Stringify(*key.k4)) + b.WriteByte('"') + } else { + b.WriteString("") + } + + b.WriteByte(')') + return b.String() +} + +func (t quadKeyCodec[K1, K2, K3, K4]) KeyType() string { + return fmt.Sprintf("Quad[%s,%s,%s,%s]", t.keyCodec1.KeyType(), t.keyCodec2.KeyType(), t.keyCodec3.KeyType(), t.keyCodec4.KeyType()) +} + +func (t quadKeyCodec[K1, K2, K3, K4]) Encode(buffer []byte, key Quad[K1, K2, K3, K4]) (int, error) { + writtenTotal := 0 + if key.k1 != nil { + written, err := t.keyCodec1.EncodeNonTerminal(buffer, *key.k1) + if err != nil { + return 0, err + } + writtenTotal += written + } + if key.k2 != nil { + written, err := t.keyCodec2.EncodeNonTerminal(buffer[writtenTotal:], *key.k2) + if err != nil { + return 0, err + } + writtenTotal += written + } + if key.k3 != nil { + written, err := t.keyCodec3.EncodeNonTerminal(buffer[writtenTotal:], *key.k3) + if err != nil { + return 0, err + } + writtenTotal += written + } + if key.k4 != nil { + written, err := t.keyCodec4.Encode(buffer[writtenTotal:], *key.k4) + if err != nil { + return 0, err + } + writtenTotal += written + } + return writtenTotal, nil +} + +func (t quadKeyCodec[K1, K2, K3, K4]) Decode(buffer []byte) (int, Quad[K1, K2, K3, K4], error) { + readTotal := 0 + read, key1, err := t.keyCodec1.DecodeNonTerminal(buffer) + if err != nil { + return 0, Quad[K1, K2, K3, K4]{}, err + } + readTotal += read + read, key2, err := t.keyCodec2.DecodeNonTerminal(buffer[readTotal:]) + if err != nil { + return 0, Quad[K1, K2, K3, K4]{}, err + } + readTotal += read + read, key3, err := t.keyCodec3.DecodeNonTerminal(buffer[readTotal:]) + if err != nil { + return 0, Quad[K1, K2, K3, K4]{}, err + } + readTotal += read + read, key4, err := t.keyCodec4.Decode(buffer[readTotal:]) + if err != nil { + return 0, Quad[K1, K2, K3, K4]{}, err + } + readTotal += read + return readTotal, Join4(key1, key2, key3, key4), nil +} + +func (t quadKeyCodec[K1, K2, K3, K4]) Size(key Quad[K1, K2, K3, K4]) int { + size := 0 + if key.k1 != nil { + size += t.keyCodec1.SizeNonTerminal(*key.k1) + } + if key.k2 != nil { + size += t.keyCodec2.SizeNonTerminal(*key.k2) + } + if key.k3 != nil { + size += t.keyCodec3.SizeNonTerminal(*key.k3) + } + if key.k4 != nil { + size += t.keyCodec4.Size(*key.k4) + } + return size +} + +func (t quadKeyCodec[K1, K2, K3, K4]) EncodeNonTerminal(buffer []byte, key Quad[K1, K2, K3, K4]) (int, error) { + writtenTotal := 0 + if key.k1 != nil { + written, err := t.keyCodec1.EncodeNonTerminal(buffer, *key.k1) + if err != nil { + return 0, err + } + writtenTotal += written + } + if key.k2 != nil { + written, err := t.keyCodec2.EncodeNonTerminal(buffer[writtenTotal:], *key.k2) + if err != nil { + return 0, err + } + writtenTotal += written + } + if key.k3 != nil { + written, err := t.keyCodec3.EncodeNonTerminal(buffer[writtenTotal:], *key.k3) + if err != nil { + return 0, err + } + writtenTotal += written + } + if key.k4 != nil { + written, err := t.keyCodec4.EncodeNonTerminal(buffer[writtenTotal:], *key.k4) + if err != nil { + return 0, err + } + writtenTotal += written + } + return writtenTotal, nil +} + +func (t quadKeyCodec[K1, K2, K3, K4]) DecodeNonTerminal(buffer []byte) (int, Quad[K1, K2, K3, K4], error) { + readTotal := 0 + read, key1, err := t.keyCodec1.DecodeNonTerminal(buffer) + if err != nil { + return 0, Quad[K1, K2, K3, K4]{}, err + } + readTotal += read + read, key2, err := t.keyCodec2.DecodeNonTerminal(buffer[readTotal:]) + if err != nil { + return 0, Quad[K1, K2, K3, K4]{}, err + } + readTotal += read + read, key3, err := t.keyCodec3.DecodeNonTerminal(buffer[readTotal:]) + if err != nil { + return 0, Quad[K1, K2, K3, K4]{}, err + } + readTotal += read + read, key4, err := t.keyCodec4.DecodeNonTerminal(buffer[readTotal:]) + if err != nil { + return 0, Quad[K1, K2, K3, K4]{}, err + } + readTotal += read + return readTotal, Join4(key1, key2, key3, key4), nil +} + +func (t quadKeyCodec[K1, K2, K3, K4]) SizeNonTerminal(key Quad[K1, K2, K3, K4]) int { + size := 0 + if key.k1 != nil { + size += t.keyCodec1.SizeNonTerminal(*key.k1) + } + if key.k2 != nil { + size += t.keyCodec2.SizeNonTerminal(*key.k2) + } + if key.k3 != nil { + size += t.keyCodec3.SizeNonTerminal(*key.k3) + } + if key.k4 != nil { + size += t.keyCodec4.SizeNonTerminal(*key.k4) + } + return size +} + +func (t quadKeyCodec[K1, K2, K3, K4]) SchemaCodec() (codec.SchemaCodec[Quad[K1, K2, K3, K4]], error) { + field1, err := getNamedKeyField(t.keyCodec1, t.name1) + if err != nil { + return codec.SchemaCodec[Quad[K1, K2, K3, K4]]{}, fmt.Errorf("error getting key1 field: %w", err) + } + + field2, err := getNamedKeyField(t.keyCodec2, t.name2) + if err != nil { + return codec.SchemaCodec[Quad[K1, K2, K3, K4]]{}, fmt.Errorf("error getting key2 field: %w", err) + } + + field3, err := getNamedKeyField(t.keyCodec3, t.name3) + if err != nil { + return codec.SchemaCodec[Quad[K1, K2, K3, K4]]{}, fmt.Errorf("error getting key3 field: %w", err) + } + + field4, err := getNamedKeyField(t.keyCodec4, t.name4) + if err != nil { + return codec.SchemaCodec[Quad[K1, K2, K3, K4]]{}, fmt.Errorf("error getting key4 field: %w", err) + } + + return codec.SchemaCodec[Quad[K1, K2, K3, K4]]{ + Fields: []schema.Field{field1, field2, field3, field4}, + ToSchemaType: func(q Quad[K1, K2, K3, K4]) (any, error) { + return []interface{}{q.K1(), q.K2(), q.K3(), q.K4()}, nil + }, + FromSchemaType: func(a any) (Quad[K1, K2, K3, K4], error) { + aSlice, ok := a.([]interface{}) + if !ok || len(aSlice) != 4 { + return Quad[K1, K2, K3, K4]{}, fmt.Errorf("expected slice of length 4, got %T", a) + } + return Join4(aSlice[0].(K1), aSlice[1].(K2), aSlice[2].(K3), aSlice[3].(K4)), nil + }, + }, nil +} + +// NewPrefixUntilQuadRange defines a collection query which ranges until the provided Quad prefix. +// Unstable: this API might change in the future. +func NewPrefixUntilQuadRange[K1, K2, K3, K4 any](k1 K1) Ranger[Quad[K1, K2, K3, K4]] { + key := QuadPrefix[K1, K2, K3, K4](k1) + return &Range[Quad[K1, K2, K3, K4]]{ + end: RangeKeyPrefixEnd(key), + } +} + +// NewPrefixedQuadRange provides a Range for all keys prefixed with the given +// first part of the Quad key. +func NewPrefixedQuadRange[K1, K2, K3, K4 any](k1 K1) Ranger[Quad[K1, K2, K3, K4]] { + key := QuadPrefix[K1, K2, K3, K4](k1) + return &Range[Quad[K1, K2, K3, K4]]{ + start: RangeKeyExact(key), + end: RangeKeyPrefixEnd(key), + } +} + +// NewSuperPrefixedQuadRange provides a Range for all keys prefixed with the given +// first and second parts of the Quad key. +func NewSuperPrefixedQuadRange[K1, K2, K3, K4 any](k1 K1, k2 K2) Ranger[Quad[K1, K2, K3, K4]] { + key := QuadSuperPrefix[K1, K2, K3, K4](k1, k2) + return &Range[Quad[K1, K2, K3, K4]]{ + start: RangeKeyExact(key), + end: RangeKeyPrefixEnd(key), + } +} + +// NewSuperPrefixedQuadRange3 provides a Range for all keys prefixed with the given +// first, second and third parts of the Quad key. +func NewSuperPrefixedQuadRange3[K1, K2, K3, K4 any](k1 K1, k2 K2, k3 K3) Ranger[Quad[K1, K2, K3, K4]] { + key := QuadSuperPrefix3[K1, K2, K3, K4](k1, k2, k3) + return &Range[Quad[K1, K2, K3, K4]]{ + start: RangeKeyExact(key), + end: RangeKeyPrefixEnd(key), + } +} diff --git a/collections/quad_test.go b/collections/quad_test.go new file mode 100644 index 0000000000..0a4b3cc779 --- /dev/null +++ b/collections/quad_test.go @@ -0,0 +1,61 @@ +package collections_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/collections" + "cosmossdk.io/collections/colltest" + coretesting "cosmossdk.io/core/testing" +) + +func TestQuad(t *testing.T) { + kc := collections.QuadKeyCodec(collections.Uint64Key, collections.StringKey, collections.BytesKey, collections.BoolKey) + + t.Run("conformance", func(t *testing.T) { + colltest.TestKeyCodec(t, kc, collections.Join4(uint64(1), "2", []byte("3"), true)) + }) +} + +func TestQuadRange(t *testing.T) { + ctx := coretesting.Context() + sk := coretesting.KVStoreService(ctx, "test") + schema := collections.NewSchemaBuilder(sk) + // this is a key composed of 4 parts: uint64, string, []byte, bool + kc := collections.QuadKeyCodec(collections.Uint64Key, collections.StringKey, collections.BytesKey, collections.BoolKey) + + keySet := collections.NewKeySet(schema, collections.NewPrefix(0), "Quad", kc) + + keys := []collections.Quad[uint64, string, []byte, bool]{ + collections.Join4(uint64(1), "A", []byte("1"), true), + collections.Join4(uint64(1), "A", []byte("2"), true), + collections.Join4(uint64(1), "B", []byte("3"), false), + collections.Join4(uint64(2), "B", []byte("4"), false), + } + + for _, k := range keys { + require.NoError(t, keySet.Set(ctx, k)) + } + + // we prefix over (1) we expect 3 results + iter, err := keySet.Iterate(ctx, collections.NewPrefixedQuadRange[uint64, string, []byte, bool](uint64(1))) + require.NoError(t, err) + gotKeys, err := iter.Keys() + require.NoError(t, err) + require.Equal(t, keys[:3], gotKeys) + + // we super prefix over Join(1, "A") we expect 2 results + iter, err = keySet.Iterate(ctx, collections.NewSuperPrefixedQuadRange[uint64, string, []byte, bool](1, "A")) + require.NoError(t, err) + gotKeys, err = iter.Keys() + require.NoError(t, err) + require.Equal(t, keys[:2], gotKeys) + + // we super prefix 3 over Join(1, "A", []byte("1")) we expect 1 result + iter, err = keySet.Iterate(ctx, collections.NewSuperPrefixedQuadRange3[uint64, string, []byte, bool](1, "A", []byte("1"))) + require.NoError(t, err) + gotKeys, err = iter.Keys() + require.NoError(t, err) + require.Equal(t, keys[:1], gotKeys) +} diff --git a/collections/schema.go b/collections/schema.go index e05058f144..8cc851166f 100644 --- a/collections/schema.go +++ b/collections/schema.go @@ -161,6 +161,12 @@ func NewSchemaFromAccessor(accessor func(context.Context) store.KVStore) Schema } } +// IsOnePerModuleType implements the depinject.OnePerModuleType interface. +func (s Schema) IsOnePerModuleType() {} + +// IsAppModule implements the appmodule.AppModule interface. +func (s Schema) IsAppModule() {} + // DefaultGenesis implements the appmodule.HasGenesis.DefaultGenesis method. func (s Schema) DefaultGenesis(target appmodule.GenesisTarget) error { for _, name := range s.collectionsOrdered { diff --git a/collections/sequence.go b/collections/sequence.go index 2290d27a5d..5503baf332 100644 --- a/collections/sequence.go +++ b/collections/sequence.go @@ -32,7 +32,7 @@ func (s Sequence) Peek(ctx context.Context) (uint64, error) { } } -// Next returns the next sequence number, and sets the next expected sequence. +// Next returns the current sequence number, and sets the next expected sequence. // Errors on encoding issues. func (s Sequence) Next(ctx context.Context) (uint64, error) { seq, err := s.Peek(ctx) diff --git a/collections/sonar-project.properties b/collections/sonar-project.properties new file mode 100644 index 0000000000..63d047f3ad --- /dev/null +++ b/collections/sonar-project.properties @@ -0,0 +1,15 @@ +sonar.projectKey=cosmos-sdk-collections +sonar.organization=cosmos + +sonar.projectName=Cosmos SDK - Collections +sonar.project.monorepo.enabled=true + +sonar.sources=. +sonar.exclusions=**/*_test.go,**/*.pb.go,**/*.pulsar.go,**/*.pb.gateway.go +sonar.tests=. +sonar.test.inclusions=**/*_test.go +sonar.go.coverage.reportPaths=coverage.out + +sonar.sourceEncoding=UTF-8 +sonar.scm.provider=git +sonar.scm.forceReloadAll=true diff --git a/collections/triple.go b/collections/triple.go new file mode 100644 index 0000000000..7474e01ccd --- /dev/null +++ b/collections/triple.go @@ -0,0 +1,387 @@ +package collections + +import ( + "encoding/json" + "fmt" + "strings" + + "cosmossdk.io/collections/codec" + "cosmossdk.io/schema" +) + +// Triple defines a multipart key composed of three keys. +type Triple[K1, K2, K3 any] struct { + k1 *K1 + k2 *K2 + k3 *K3 +} + +// Join3 instantiates a new Triple instance composed of the three provided keys, in order. +func Join3[K1, K2, K3 any](k1 K1, k2 K2, k3 K3) Triple[K1, K2, K3] { + return Triple[K1, K2, K3]{&k1, &k2, &k3} +} + +// K1 returns the first part of the key. If nil, the zero value is returned. +func (t Triple[K1, K2, K3]) K1() (x K1) { + if t.k1 != nil { + return *t.k1 + } + return x +} + +// K2 returns the second part of the key. If nil, the zero value is returned. +func (t Triple[K1, K2, K3]) K2() (x K2) { + if t.k2 != nil { + return *t.k2 + } + return x +} + +// K3 returns the third part of the key. If nil, the zero value is returned. +func (t Triple[K1, K2, K3]) K3() (x K3) { + if t.k3 != nil { + return *t.k3 + } + return x +} + +// TriplePrefix creates a new Triple instance composed only of the first part of the key. +func TriplePrefix[K1, K2, K3 any](k1 K1) Triple[K1, K2, K3] { + return Triple[K1, K2, K3]{k1: &k1} +} + +// TripleSuperPrefix creates a new Triple instance composed only of the first two parts of the key. +func TripleSuperPrefix[K1, K2, K3 any](k1 K1, k2 K2) Triple[K1, K2, K3] { + return Triple[K1, K2, K3]{k1: &k1, k2: &k2} +} + +// TripleKeyCodec instantiates a new KeyCodec instance that can encode the Triple, given +// the KeyCodecs of the three parts of the key, in order. +func TripleKeyCodec[K1, K2, K3 any](keyCodec1 codec.KeyCodec[K1], keyCodec2 codec.KeyCodec[K2], keyCodec3 codec.KeyCodec[K3]) codec.KeyCodec[Triple[K1, K2, K3]] { + return tripleKeyCodec[K1, K2, K3]{ + keyCodec1: keyCodec1, + keyCodec2: keyCodec2, + keyCodec3: keyCodec3, + } +} + +func NamedTripleKeyCodec[K1, K2, K3 any](key1Name string, keyCodec1 codec.KeyCodec[K1], key2Name string, keyCodec2 codec.KeyCodec[K2], key3Name string, keyCodec3 codec.KeyCodec[K3]) codec.KeyCodec[Triple[K1, K2, K3]] { + return tripleKeyCodec[K1, K2, K3]{ + key1Name: key1Name, + key2Name: key2Name, + key3Name: key3Name, + keyCodec1: keyCodec1, + keyCodec2: keyCodec2, + keyCodec3: keyCodec3, + } +} + +type tripleKeyCodec[K1, K2, K3 any] struct { + key1Name, key2Name, key3Name string + keyCodec1 codec.KeyCodec[K1] + keyCodec2 codec.KeyCodec[K2] + keyCodec3 codec.KeyCodec[K3] +} + +type jsonTripleKey [3]json.RawMessage + +// EncodeJSON convert triple keys to json +func (t tripleKeyCodec[K1, K2, K3]) EncodeJSON(value Triple[K1, K2, K3]) ([]byte, error) { + json1, err := t.keyCodec1.EncodeJSON(*value.k1) + if err != nil { + return nil, err + } + + json2, err := t.keyCodec2.EncodeJSON(*value.k2) + if err != nil { + return nil, err + } + + json3, err := t.keyCodec3.EncodeJSON(*value.k3) + if err != nil { + return nil, err + } + + return json.Marshal(jsonTripleKey{json1, json2, json3}) +} + +// DecodeJSON convert json to triple keys +func (t tripleKeyCodec[K1, K2, K3]) DecodeJSON(b []byte) (Triple[K1, K2, K3], error) { + var jsonKey jsonTripleKey + err := json.Unmarshal(b, &jsonKey) + if err != nil { + return Triple[K1, K2, K3]{}, err + } + + key1, err := t.keyCodec1.DecodeJSON(jsonKey[0]) + if err != nil { + return Triple[K1, K2, K3]{}, err + } + + key2, err := t.keyCodec2.DecodeJSON(jsonKey[1]) + if err != nil { + return Triple[K1, K2, K3]{}, err + } + + key3, err := t.keyCodec3.DecodeJSON(jsonKey[2]) + if err != nil { + return Triple[K1, K2, K3]{}, err + } + + return Join3(key1, key2, key3), nil +} + +// Stringify convert triple keys to string +func (t tripleKeyCodec[K1, K2, K3]) Stringify(key Triple[K1, K2, K3]) string { + b := new(strings.Builder) + b.WriteByte('(') + if key.k1 != nil { + b.WriteByte('"') + b.WriteString(t.keyCodec1.Stringify(*key.k1)) + b.WriteByte('"') + } else { + b.WriteString("") + } + + b.WriteString(", ") + if key.k2 != nil { + b.WriteByte('"') + b.WriteString(t.keyCodec2.Stringify(*key.k2)) + b.WriteByte('"') + } else { + b.WriteString("") + } + + b.WriteString(", ") + if key.k3 != nil { + b.WriteByte('"') + b.WriteString(t.keyCodec3.Stringify(*key.k3)) + b.WriteByte('"') + } else { + b.WriteString("") + } + + b.WriteByte(')') + return b.String() +} + +func (t tripleKeyCodec[K1, K2, K3]) KeyType() string { + return fmt.Sprintf("Triple[%s,%s,%s]", t.keyCodec1.KeyType(), t.keyCodec2.KeyType(), t.keyCodec3.KeyType()) +} + +func (t tripleKeyCodec[K1, K2, K3]) Encode(buffer []byte, key Triple[K1, K2, K3]) (int, error) { + writtenTotal := 0 + if key.k1 != nil { + written, err := t.keyCodec1.EncodeNonTerminal(buffer, *key.k1) + if err != nil { + return 0, err + } + writtenTotal += written + } + if key.k2 != nil { + written, err := t.keyCodec2.EncodeNonTerminal(buffer[writtenTotal:], *key.k2) + if err != nil { + return 0, err + } + writtenTotal += written + } + if key.k3 != nil { + written, err := t.keyCodec3.Encode(buffer[writtenTotal:], *key.k3) + if err != nil { + return 0, err + } + writtenTotal += written + } + return writtenTotal, nil +} + +func (t tripleKeyCodec[K1, K2, K3]) Decode(buffer []byte) (int, Triple[K1, K2, K3], error) { + readTotal := 0 + read, key1, err := t.keyCodec1.DecodeNonTerminal(buffer) + if err != nil { + return 0, Triple[K1, K2, K3]{}, err + } + readTotal += read + read, key2, err := t.keyCodec2.DecodeNonTerminal(buffer[readTotal:]) + if err != nil { + return 0, Triple[K1, K2, K3]{}, err + } + readTotal += read + read, key3, err := t.keyCodec3.Decode(buffer[readTotal:]) + if err != nil { + return 0, Triple[K1, K2, K3]{}, err + } + readTotal += read + return readTotal, Join3(key1, key2, key3), nil +} + +func (t tripleKeyCodec[K1, K2, K3]) Size(key Triple[K1, K2, K3]) int { + size := 0 + if key.k1 != nil { + size += t.keyCodec1.SizeNonTerminal(*key.k1) + } + if key.k2 != nil { + size += t.keyCodec2.SizeNonTerminal(*key.k2) + } + if key.k3 != nil { + size += t.keyCodec3.Size(*key.k3) + } + return size +} + +func (t tripleKeyCodec[K1, K2, K3]) EncodeNonTerminal(buffer []byte, key Triple[K1, K2, K3]) (int, error) { + writtenTotal := 0 + if key.k1 != nil { + written, err := t.keyCodec1.EncodeNonTerminal(buffer, *key.k1) + if err != nil { + return 0, err + } + writtenTotal += written + } + if key.k2 != nil { + written, err := t.keyCodec2.EncodeNonTerminal(buffer[writtenTotal:], *key.k2) + if err != nil { + return 0, err + } + writtenTotal += written + } + if key.k3 != nil { + written, err := t.keyCodec3.EncodeNonTerminal(buffer[writtenTotal:], *key.k3) + if err != nil { + return 0, err + } + writtenTotal += written + } + return writtenTotal, nil +} + +func (t tripleKeyCodec[K1, K2, K3]) DecodeNonTerminal(buffer []byte) (int, Triple[K1, K2, K3], error) { + readTotal := 0 + read, key1, err := t.keyCodec1.DecodeNonTerminal(buffer) + if err != nil { + return 0, Triple[K1, K2, K3]{}, err + } + readTotal += read + read, key2, err := t.keyCodec2.DecodeNonTerminal(buffer[readTotal:]) + if err != nil { + return 0, Triple[K1, K2, K3]{}, err + } + readTotal += read + read, key3, err := t.keyCodec3.DecodeNonTerminal(buffer[readTotal:]) + if err != nil { + return 0, Triple[K1, K2, K3]{}, err + } + readTotal += read + return readTotal, Join3(key1, key2, key3), nil +} + +func (t tripleKeyCodec[K1, K2, K3]) SizeNonTerminal(key Triple[K1, K2, K3]) int { + size := 0 + if key.k1 != nil { + size += t.keyCodec1.SizeNonTerminal(*key.k1) + } + if key.k2 != nil { + size += t.keyCodec2.SizeNonTerminal(*key.k2) + } + if key.k3 != nil { + size += t.keyCodec3.SizeNonTerminal(*key.k3) + } + return size +} + +func (t tripleKeyCodec[K1, K2, K3]) Name() string { + return fmt.Sprintf("%s,%s,%s", t.key1Name, t.key2Name, t.key3Name) +} + +func (t tripleKeyCodec[K1, K2, K3]) SchemaCodec() (codec.SchemaCodec[Triple[K1, K2, K3]], error) { + field1, err := getNamedKeyField(t.keyCodec1, t.key1Name) + if err != nil { + return codec.SchemaCodec[Triple[K1, K2, K3]]{}, fmt.Errorf("error getting key1 field: %w", err) + } + + field2, err := getNamedKeyField(t.keyCodec2, t.key2Name) + if err != nil { + return codec.SchemaCodec[Triple[K1, K2, K3]]{}, fmt.Errorf("error getting key2 field: %w", err) + } + + field3, err := getNamedKeyField(t.keyCodec3, t.key3Name) + if err != nil { + return codec.SchemaCodec[Triple[K1, K2, K3]]{}, fmt.Errorf("error getting key3 field: %w", err) + } + + return codec.SchemaCodec[Triple[K1, K2, K3]]{ + Fields: []schema.Field{field1, field2, field3}, + ToSchemaType: func(t Triple[K1, K2, K3]) (any, error) { + return []interface{}{t.K1(), t.K2(), t.K3()}, nil + }, + FromSchemaType: func(a any) (Triple[K1, K2, K3], error) { + aSlice, ok := a.([]interface{}) + if !ok || len(aSlice) != 3 { + return Triple[K1, K2, K3]{}, fmt.Errorf("expected slice of length 3, got %T", a) + } + return Join3(aSlice[0].(K1), aSlice[1].(K2), aSlice[2].(K3)), nil + }, + }, nil +} + +// NewPrefixUntilTripleRange defines a collection query which ranges until the provided Pair prefix. +// Unstable: this API might change in the future. +func NewPrefixUntilTripleRange[K1, K2, K3 any](k1 K1) Ranger[Triple[K1, K2, K3]] { + key := TriplePrefix[K1, K2, K3](k1) + return &Range[Triple[K1, K2, K3]]{ + end: RangeKeyPrefixEnd(key), + } +} + +// NewPrefixedTripleRange provides a Range for all keys prefixed with the given +// first part of the Triple key. +func NewPrefixedTripleRange[K1, K2, K3 any](k1 K1) Ranger[Triple[K1, K2, K3]] { + key := TriplePrefix[K1, K2, K3](k1) + return &Range[Triple[K1, K2, K3]]{ + start: RangeKeyExact(key), + end: RangeKeyPrefixEnd(key), + } +} + +// NewSuperPrefixedTripleRange provides a Range for all keys prefixed with the given +// first and second parts of the Triple key. +func NewSuperPrefixedTripleRange[K1, K2, K3 any](k1 K1, k2 K2) Ranger[Triple[K1, K2, K3]] { + key := TripleSuperPrefix[K1, K2, K3](k1, k2) + return &Range[Triple[K1, K2, K3]]{ + start: RangeKeyExact(key), + end: RangeKeyPrefixEnd(key), + } +} + +// NewPrefixUntilTripleRangeReversed defines a collection query which ranges until the provided Pair prefix +// in reverse order. +// Unstable: this API might change in the future. +func NewPrefixUntilTripleRangeReversed[K1, K2, K3 any](k1 K1) Ranger[Triple[K1, K2, K3]] { + key := TriplePrefix[K1, K2, K3](k1) + return &Range[Triple[K1, K2, K3]]{ + end: RangeKeyPrefixEnd(key), + order: OrderDescending, + } +} + +// NewPrefixedTripleRangeReversed provides a Range for all keys prefixed with the given +// first part of the Triple key in reverse order. +func NewPrefixedTripleRangeReversed[K1, K2, K3 any](k1 K1) Ranger[Triple[K1, K2, K3]] { + key := TriplePrefix[K1, K2, K3](k1) + return &Range[Triple[K1, K2, K3]]{ + start: RangeKeyExact(key), + end: RangeKeyPrefixEnd(key), + order: OrderDescending, + } +} + +// NewSuperPrefixedTripleRangeReversed provides a Range for all keys prefixed with the given +// first and second parts of the Triple key in reverse order. +func NewSuperPrefixedTripleRangeReversed[K1, K2, K3 any](k1 K1, k2 K2) Ranger[Triple[K1, K2, K3]] { + key := TripleSuperPrefix[K1, K2, K3](k1, k2) + return &Range[Triple[K1, K2, K3]]{ + start: RangeKeyExact(key), + end: RangeKeyPrefixEnd(key), + order: OrderDescending, + } +} diff --git a/collections/triple_test.go b/collections/triple_test.go new file mode 100644 index 0000000000..c605a7aeab --- /dev/null +++ b/collections/triple_test.go @@ -0,0 +1,74 @@ +package collections_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/collections" + "cosmossdk.io/collections/colltest" + coretesting "cosmossdk.io/core/testing" +) + +func TestTriple(t *testing.T) { + kc := collections.TripleKeyCodec(collections.Uint64Key, collections.StringKey, collections.BytesKey) + + t.Run("conformance", func(t *testing.T) { + colltest.TestKeyCodec(t, kc, collections.Join3(uint64(1), "2", []byte("3"))) + }) +} + +func TestTripleRange(t *testing.T) { + ctx := coretesting.Context() + sk := coretesting.KVStoreService(ctx, "test") + schema := collections.NewSchemaBuilder(sk) + // this is a key composed of 3 parts: uint64, string, []byte + kc := collections.TripleKeyCodec(collections.Uint64Key, collections.StringKey, collections.BytesKey) + + keySet := collections.NewKeySet(schema, collections.NewPrefix(0), "triple", kc) + + keys := []collections.Triple[uint64, string, []byte]{ + collections.Join3(uint64(1), "A", []byte("1")), + collections.Join3(uint64(1), "A", []byte("2")), + collections.Join3(uint64(1), "B", []byte("3")), + collections.Join3(uint64(2), "B", []byte("4")), + } + + for _, k := range keys { + require.NoError(t, keySet.Set(ctx, k)) + } + + // we prefix over (1) we expect 3 results + iter, err := keySet.Iterate(ctx, collections.NewPrefixedTripleRange[uint64, string, []byte](uint64(1))) + require.NoError(t, err) + gotKeys, err := iter.Keys() + require.NoError(t, err) + require.Equal(t, keys[:3], gotKeys) + + // we prefix over (1) with "reverse" enabled, we expect 3 results in reverse order + iter, err = keySet.Iterate(ctx, collections.NewPrefixedTripleRangeReversed[uint64, string, []byte](uint64(1))) + require.NoError(t, err) + gotKeys, err = iter.Keys() + require.NoError(t, err) + require.Len(t, gotKeys, 3) + for i := range gotKeys { + require.Equal(t, gotKeys[i], keys[len(gotKeys)-i-1]) + } + + // we super prefix over Join(1, "A") we expect 2 results + iter, err = keySet.Iterate(ctx, collections.NewSuperPrefixedTripleRange[uint64, string, []byte](1, "A")) + require.NoError(t, err) + gotKeys, err = iter.Keys() + require.NoError(t, err) + require.Equal(t, keys[:2], gotKeys) + + // we prefix over Join(1, "A") with "reverse" enabled, we expect 2 results in reverse order + iter, err = keySet.Iterate(ctx, collections.NewSuperPrefixedTripleRangeReversed[uint64, string, []byte](1, "A")) + require.NoError(t, err) + gotKeys, err = iter.Keys() + require.NoError(t, err) + require.Len(t, gotKeys, 2) + for i := range gotKeys { + require.Equal(t, gotKeys[i], keys[len(gotKeys)-i-1]) + } +} diff --git a/collections/vec.go b/collections/vec.go new file mode 100644 index 0000000000..58ea87fe62 --- /dev/null +++ b/collections/vec.go @@ -0,0 +1,141 @@ +package collections + +import ( + "context" + "errors" + "fmt" + + "cosmossdk.io/collections/codec" +) + +var ( + // ErrEmptyVec is returned when trying to pop an element from an empty Vec. + ErrEmptyVec = errors.New("vec is empty") + // ErrOutOfBounds is returned when trying to do an operation on an index that is out of bounds. + ErrOutOfBounds = errors.New("vec index is out of bounds") +) + +const ( + VecElementsNameSuffix = "_elements" + VecLengthNameSuffix = "_length" + VecElementsPrefixSuffix = 0x0 + VecLengthPrefixSuffix = 0x1 +) + +// NewVec creates a new Vec instance. Since Vec relies on two collections, one for the length +// and the other for the elements, it will register two state objects on the schema builder. +// The first is the length which is an item, whose prefix is the provided prefix with a suffix +// which equals to VecLengthPrefixSuffix, the name is also suffixed with VecLengthNameSuffix. +// The second is the elements which is a map, whose prefix is the provided prefix with a suffix +// which equals to VecElementsPrefixSuffix, the name is also suffixed with VecElementsNameSuffix. +func NewVec[T any](sb *SchemaBuilder, prefix Prefix, name string, vc codec.ValueCodec[T]) Vec[T] { + return Vec[T]{ + length: NewItem(sb, append(prefix, VecLengthPrefixSuffix), name+VecLengthNameSuffix, Uint64Value), + elements: NewMap(sb, append(prefix, VecElementsPrefixSuffix), name+VecElementsNameSuffix, Uint64Key, vc), + } +} + +// Vec works like a slice sitting on top of a KVStore. +// It relies on two collections, one for the length which is an Item[uint64], +// the other for the elements which is a Map[uint64, T]. +type Vec[T any] struct { + length Item[uint64] + elements Map[uint64, T] +} + +// Push adds an element to the end of the Vec. +func (v Vec[T]) Push(ctx context.Context, elem T) error { + length, err := v.length.Get(ctx) + if err != nil && !errors.Is(err, ErrNotFound) { + return err + } + err = v.elements.Set(ctx, length, elem) + if err != nil { + return err + } + err = v.length.Set(ctx, length+1) + if err != nil { + return err + } + return nil +} + +// Pop removes an element from the end of the Vec and returns it. Fails +// if the Vec is empty. +func (v Vec[T]) Pop(ctx context.Context) (elem T, err error) { + length, err := v.length.Get(ctx) + if err != nil && !errors.Is(err, ErrNotFound) { + return elem, err + } + if length == 0 { + return elem, ErrEmptyVec + } + length -= 1 + elem, err = v.elements.Get(ctx, length) + if err != nil { + return elem, err + } + err = v.elements.Remove(ctx, length) + if err != nil { + return elem, err + } + err = v.length.Set(ctx, length) + if err != nil { + return elem, err + } + return elem, nil +} + +// Replace replaces an element at a given index. Fails if the index is out of bounds. +func (v Vec[T]) Replace(ctx context.Context, index uint64, elem T) error { + length, err := v.length.Get(ctx) + if err != nil && !errors.Is(err, ErrNotFound) { + return err + } + if index >= length { + return fmt.Errorf("%w: length %d", ErrOutOfBounds, length) + } + return v.elements.Set(ctx, index, elem) +} + +// Get returns an element at a given index. Returns ErrOutOfBounds +// if the index is out of bounds. +func (v Vec[T]) Get(ctx context.Context, index uint64) (elem T, err error) { + elem, err = v.elements.Get(ctx, index) + switch { + case err == nil: + return elem, nil + case errors.Is(err, ErrNotFound): + return elem, fmt.Errorf("%w: index %d", ErrOutOfBounds, index) + default: + return elem, err + } +} + +// Len returns the length of the Vec. +func (v Vec[T]) Len(ctx context.Context) (uint64, error) { + length, err := v.length.Get(ctx) + switch { + // no error, return length as the vec is populated + case err == nil: + return length, nil + // not found, return 0 as the vec is empty + case errors.Is(err, ErrNotFound): + return 0, nil + // something else happened + default: + return 0, err + } +} + +// Iterate iterates over the Vec. It returns an Iterator whose key is the index +// and the value is the element at that index. +func (v Vec[T]) Iterate(ctx context.Context, rng Ranger[uint64]) (Iterator[uint64, T], error) { + return v.elements.Iterate(ctx, rng) +} + +// Walk walks over the Vec. It calls the walkFn for each element in the Vec, +// where the key is the index and the value is the element at that index. +func (v Vec[T]) Walk(ctx context.Context, rng Ranger[uint64], walkFn func(index uint64, elem T) (stop bool, err error)) error { + return v.elements.Walk(ctx, rng, walkFn) +} diff --git a/collections/vec_test.go b/collections/vec_test.go new file mode 100644 index 0000000000..d7640aec70 --- /dev/null +++ b/collections/vec_test.go @@ -0,0 +1,63 @@ +package collections + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVec(t *testing.T) { + sk, ctx := deps() + schemaBuilder := NewSchemaBuilder(sk) + vec := NewVec(schemaBuilder, NewPrefix(0), "vec", StringValue) + _, err := schemaBuilder.Build() + require.NoError(t, err) + + // length when empty + length, err := vec.Len(ctx) + require.NoError(t, err) + require.Equal(t, uint64(0), length) + + // pop when empty should error with an empty vec error + _, err = vec.Pop(ctx) + require.ErrorIs(t, err, ErrEmptyVec) + + // replace when out of bounds should error with an out of bounds error + err = vec.Replace(ctx, 0, "foo") + require.ErrorIs(t, err, ErrOutOfBounds) + + // get out of bounds should error with an out of bounds error + _, err = vec.Get(ctx, 0) + require.ErrorIs(t, err, ErrOutOfBounds) + + // push + err = vec.Push(ctx, "foo") + require.NoError(t, err) + + // push more + err = vec.Push(ctx, "bar") + require.NoError(t, err) + + // check length + length, err = vec.Len(ctx) + require.NoError(t, err) + require.Equal(t, uint64(2), length) + + // get + v, err := vec.Get(ctx, 0) + require.NoError(t, err) + require.Equal(t, "foo", v) + + // replace + err = vec.Replace(ctx, 0, "bar") + require.NoError(t, err) + + v, err = vec.Get(ctx, 0) + require.NoError(t, err) + require.Equal(t, "bar", v) + + // pop + v, err = vec.Pop(ctx) + require.NoError(t, err) + require.Equal(t, "bar", v) +}