feat(orm): add module db (#10991)
## Description
This PR adds a `ModuleDB` interface which can be used directly by Cosmos SDK modules. A simplified bank example with Mint/Send/Burn functionality against Balance and Supply tables is included in the tests.
This PR also:
* adds simplified `Get` and `Has` methods to `Table` which use the primary key values in the message instead of `...interface{}`
* adds a stable deterministic proto JSON marshaler and updates the `Entry.String` methods to use it because the golden tests are not deterministic without this. This code is currently internal but can be extracted to a public `codec` or `cosmos-proto` package eventually.
---
### Author Checklist
*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*
I have...
- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules)
- [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing)
- [ ] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed
### Reviewers Checklist
*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*
I have...
- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
This commit is contained in:
parent
87bb06c9fc
commit
6ea2049944
@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"github.com/cosmos/cosmos-sdk/orm/internal/stablejson"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// Entry defines a logical representation of a kv-store entry for ORM instances.
|
||||
@ -45,7 +45,12 @@ func (p *PrimaryKeyEntry) String() string {
|
||||
if p.Value == nil {
|
||||
return fmt.Sprintf("PK %s %s -> _", p.TableName, fmtValues(p.Key))
|
||||
} else {
|
||||
return fmt.Sprintf("PK %s %s -> %s", p.TableName, fmtValues(p.Key), p.Value)
|
||||
valBz, err := stablejson.Marshal(p.Value)
|
||||
valStr := string(valBz)
|
||||
if err != nil {
|
||||
valStr = fmt.Sprintf("ERR %v", err)
|
||||
}
|
||||
return fmt.Sprintf("PK %s %s -> %s", p.TableName, fmtValues(p.Key), valStr)
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,19 +61,7 @@ func fmtValues(values []protoreflect.Value) string {
|
||||
|
||||
parts := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
val, err := structpb.NewValue(v.Interface())
|
||||
if err != nil {
|
||||
parts[i] = "ERR"
|
||||
continue
|
||||
}
|
||||
|
||||
bz, err := protojson.Marshal(val)
|
||||
if err != nil {
|
||||
parts[i] = "ERR"
|
||||
continue
|
||||
}
|
||||
|
||||
parts[i] = string(bz)
|
||||
parts[i] = fmt.Sprintf("%v", v.Interface())
|
||||
}
|
||||
|
||||
return strings.Join(parts, "/")
|
||||
|
||||
@ -19,7 +19,7 @@ func TestPrimaryKeyEntry(t *testing.T) {
|
||||
Key: encodeutil.ValuesOf(uint32(1), "abc"),
|
||||
Value: &testpb.ExampleTable{I32: -1},
|
||||
}
|
||||
assert.Equal(t, `PK testpb.ExampleTable 1/"abc" -> i32:-1`, entry.String())
|
||||
assert.Equal(t, `PK testpb.ExampleTable 1/abc -> {"i32":-1}`, entry.String())
|
||||
assert.Equal(t, aFullName, entry.GetTableName())
|
||||
|
||||
// prefix key
|
||||
@ -28,7 +28,7 @@ func TestPrimaryKeyEntry(t *testing.T) {
|
||||
Key: encodeutil.ValuesOf(uint32(1), "abc"),
|
||||
Value: nil,
|
||||
}
|
||||
assert.Equal(t, `PK testpb.ExampleTable 1/"abc" -> _`, entry.String())
|
||||
assert.Equal(t, `PK testpb.ExampleTable 1/abc -> _`, entry.String())
|
||||
assert.Equal(t, aFullName, entry.GetTableName())
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ func TestIndexKeyEntry(t *testing.T) {
|
||||
IndexValues: encodeutil.ValuesOf(uint32(10), int32(-1), "abc"),
|
||||
PrimaryKey: encodeutil.ValuesOf("abc", int32(-1)),
|
||||
}
|
||||
assert.Equal(t, `IDX testpb.ExampleTable u32/i32/str : 10/-1/"abc" -> "abc"/-1`, entry.String())
|
||||
assert.Equal(t, `IDX testpb.ExampleTable u32/i32/str : 10/-1/abc -> abc/-1`, entry.String())
|
||||
assert.Equal(t, aFullName, entry.GetTableName())
|
||||
|
||||
entry = &ormkv.IndexKeyEntry{
|
||||
@ -50,7 +50,7 @@ func TestIndexKeyEntry(t *testing.T) {
|
||||
IndexValues: encodeutil.ValuesOf(uint32(10)),
|
||||
PrimaryKey: encodeutil.ValuesOf("abc", int32(-1)),
|
||||
}
|
||||
assert.Equal(t, `UNIQ testpb.ExampleTable u32 : 10 -> "abc"/-1`, entry.String())
|
||||
assert.Equal(t, `UNIQ testpb.ExampleTable u32 : 10 -> abc/-1`, entry.String())
|
||||
assert.Equal(t, aFullName, entry.GetTableName())
|
||||
|
||||
// prefix key
|
||||
@ -70,6 +70,6 @@ func TestIndexKeyEntry(t *testing.T) {
|
||||
IsUnique: true,
|
||||
IndexValues: encodeutil.ValuesOf("abc", int32(1)),
|
||||
}
|
||||
assert.Equal(t, `UNIQ testpb.ExampleTable str/i32 : "abc"/1 -> _`, entry.String())
|
||||
assert.Equal(t, `UNIQ testpb.ExampleTable str/i32 : abc/1 -> _`, entry.String())
|
||||
assert.Equal(t, aFullName, entry.GetTableName())
|
||||
}
|
||||
|
||||
10
orm/go.mod
10
orm/go.mod
@ -4,8 +4,9 @@ go 1.17
|
||||
|
||||
require (
|
||||
github.com/cosmos/cosmos-proto v1.0.0-alpha6
|
||||
github.com/cosmos/cosmos-sdk/api v0.1.0-alpha2
|
||||
github.com/cosmos/cosmos-sdk/api v0.1.0-alpha3
|
||||
github.com/cosmos/cosmos-sdk/errors v1.0.0-beta.2
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/tendermint/tm-db v0.6.6
|
||||
google.golang.org/protobuf v1.27.1
|
||||
gotest.tools/v3 v3.1.0
|
||||
@ -15,6 +16,7 @@ require (
|
||||
require (
|
||||
github.com/DataDog/zstd v1.4.5 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.2 // indirect
|
||||
github.com/dgraph-io/ristretto v0.0.3 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
@ -22,19 +24,25 @@ require (
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect
|
||||
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.3-0.20201103224600-674baa8c7fc3 // indirect
|
||||
github.com/google/btree v1.0.0 // indirect
|
||||
github.com/google/go-cmp v0.5.5 // indirect
|
||||
github.com/jmhodges/levigo v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca // indirect
|
||||
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f // indirect
|
||||
golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb // indirect
|
||||
google.golang.org/grpc v1.43.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||
)
|
||||
|
||||
replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1
|
||||
|
||||
11
orm/go.sum
11
orm/go.sum
@ -23,11 +23,10 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cosmos/cosmos-proto v1.0.0-alpha4/go.mod h1:msdDWOvfStHLG+Z2y2SJ0dcqimZ2vc8M1MPnZ4jOF7U=
|
||||
github.com/cosmos/cosmos-proto v1.0.0-alpha6 h1:N2BvV2AyzGAXCJnvlw1pMzEQ+76tj5FDBrkYQYIDCdU=
|
||||
github.com/cosmos/cosmos-proto v1.0.0-alpha6/go.mod h1:msdDWOvfStHLG+Z2y2SJ0dcqimZ2vc8M1MPnZ4jOF7U=
|
||||
github.com/cosmos/cosmos-sdk/api v0.1.0-alpha2 h1:47aK2mZ8oh3wyr5Q4OiZxyrMkQZRW67Ah/HfC8dW8hs=
|
||||
github.com/cosmos/cosmos-sdk/api v0.1.0-alpha2/go.mod h1:xWm3hne2f6upv80eIS+fJnnUaed/R2EJno1It4Zb9aw=
|
||||
github.com/cosmos/cosmos-sdk/api v0.1.0-alpha3 h1:tqpedvX/1UgQyX3W2hlW6Xg801FyojYT/NOHbW0oG+s=
|
||||
github.com/cosmos/cosmos-sdk/api v0.1.0-alpha3/go.mod h1:Ht15guGn9F8b0lv8NkjXE9/asAvVUOt2n4gvQ4q5MyU=
|
||||
github.com/cosmos/cosmos-sdk/errors v1.0.0-beta.2 h1:bBglNlra8ZHb4dmbEE8V85ihLA+DkriSm7tcx6x/JWo=
|
||||
github.com/cosmos/cosmos-sdk/errors v1.0.0-beta.2/go.mod h1:Gi7pzVRnvZ1N16JAXpLADzng0ePoE7YeEHaULSFB2Ts=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
@ -100,8 +99,10 @@ github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U
|
||||
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
|
||||
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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@ -122,6 +123,7 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
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_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4=
|
||||
github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
@ -237,6 +239,7 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
|
||||
google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb h1:ZrsicilzPCS/Xr8qtBZZLpy4P9TYXAfl49ctG1/5tgw=
|
||||
google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
@ -247,6 +250,7 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
google.golang.org/grpc v1.43.0 h1:Eeu7bZtDZ2DpRCsLhUlcrLnvYaMK1Gz86a+hMVvELmM=
|
||||
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
|
||||
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=
|
||||
@ -263,6 +267,7 @@ google.golang.org/protobuf v1.27.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
|
||||
93
orm/internal/stablejson/encode.go
Normal file
93
orm/internal/stablejson/encode.go
Normal file
@ -0,0 +1,93 @@
|
||||
package stablejson
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protopath"
|
||||
"google.golang.org/protobuf/reflect/protorange"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
// Marshal marshals the provided message to JSON with a stable ordering based
|
||||
// on ascending field numbers.
|
||||
func Marshal(message proto.Message) ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
firstStack := []bool{true}
|
||||
err := protorange.Options{
|
||||
Stable: true,
|
||||
}.Range(message.ProtoReflect(),
|
||||
func(p protopath.Values) error {
|
||||
// Starting printing the value.
|
||||
if !firstStack[len(firstStack)-1] {
|
||||
_, _ = fmt.Fprintf(buf, ",")
|
||||
}
|
||||
firstStack[len(firstStack)-1] = false
|
||||
|
||||
// Print the key.
|
||||
var fd protoreflect.FieldDescriptor
|
||||
last := p.Index(-1)
|
||||
beforeLast := p.Index(-2)
|
||||
switch last.Step.Kind() {
|
||||
case protopath.FieldAccessStep:
|
||||
fd = last.Step.FieldDescriptor()
|
||||
_, _ = fmt.Fprintf(buf, "%q:", fd.Name())
|
||||
case protopath.ListIndexStep:
|
||||
fd = beforeLast.Step.FieldDescriptor() // lists always appear in the context of a repeated field
|
||||
case protopath.MapIndexStep:
|
||||
fd = beforeLast.Step.FieldDescriptor() // maps always appear in the context of a repeated field
|
||||
_, _ = fmt.Fprintf(buf, "%v:", last.Step.MapIndex().Interface())
|
||||
case protopath.AnyExpandStep:
|
||||
_, _ = fmt.Fprintf(buf, `"@type":%q`, last.Value.Message().Descriptor().FullName())
|
||||
return nil
|
||||
case protopath.UnknownAccessStep:
|
||||
_, _ = fmt.Fprintf(buf, "?: ")
|
||||
}
|
||||
|
||||
switch v := last.Value.Interface().(type) {
|
||||
case protoreflect.Message:
|
||||
_, _ = fmt.Fprintf(buf, "{")
|
||||
firstStack = append(firstStack, true)
|
||||
case protoreflect.List:
|
||||
_, _ = fmt.Fprintf(buf, "[")
|
||||
firstStack = append(firstStack, true)
|
||||
case protoreflect.Map:
|
||||
_, _ = fmt.Fprintf(buf, "{")
|
||||
firstStack = append(firstStack, true)
|
||||
case protoreflect.EnumNumber:
|
||||
var ev protoreflect.EnumValueDescriptor
|
||||
if fd != nil {
|
||||
ev = fd.Enum().Values().ByNumber(v)
|
||||
}
|
||||
if ev != nil {
|
||||
_, _ = fmt.Fprintf(buf, "%v", ev.Name())
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(buf, "%v", v)
|
||||
}
|
||||
case string, []byte:
|
||||
_, _ = fmt.Fprintf(buf, "%q", v)
|
||||
default:
|
||||
_, _ = fmt.Fprintf(buf, "%v", v)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
func(p protopath.Values) error {
|
||||
last := p.Index(-1)
|
||||
switch last.Value.Interface().(type) {
|
||||
case protoreflect.Message:
|
||||
if last.Step.Kind() != protopath.AnyExpandStep {
|
||||
_, _ = fmt.Fprintf(buf, "}")
|
||||
}
|
||||
case protoreflect.List:
|
||||
_, _ = fmt.Fprintf(buf, "]")
|
||||
firstStack = firstStack[:len(firstStack)-1]
|
||||
case protoreflect.Map:
|
||||
_, _ = fmt.Fprintf(buf, "}")
|
||||
firstStack = firstStack[:len(firstStack)-1]
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
return buf.Bytes(), err
|
||||
}
|
||||
37
orm/internal/stablejson/encode_test.go
Normal file
37
orm/internal/stablejson/encode_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
package stablejson_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
bankv1beta1 "github.com/cosmos/cosmos-sdk/api/cosmos/bank/v1beta1"
|
||||
basev1beta1 "github.com/cosmos/cosmos-sdk/api/cosmos/base/v1beta1"
|
||||
txv1beta1 "github.com/cosmos/cosmos-sdk/api/cosmos/tx/v1beta1"
|
||||
"github.com/cosmos/cosmos-sdk/orm/internal/stablejson"
|
||||
)
|
||||
|
||||
func TestStableJSON(t *testing.T) {
|
||||
msg, err := anypb.New(&bankv1beta1.MsgSend{
|
||||
FromAddress: "foo213325",
|
||||
ToAddress: "foo32t5sdfh",
|
||||
Amount: []*basev1beta1.Coin{
|
||||
{
|
||||
Denom: "bar",
|
||||
Amount: "1234",
|
||||
},
|
||||
{
|
||||
Denom: "baz",
|
||||
Amount: "321",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
bz, err := stablejson.Marshal(&txv1beta1.TxBody{Messages: []*anypb.Any{msg}})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t,
|
||||
`{"messages":[{"@type":"cosmos.bank.v1beta1.MsgSend","from_address":"foo213325","to_address":"foo32t5sdfh","amount":[{"denom":"bar","amount":"1234"},{"denom":"baz","amount":"321"}]}]}`,
|
||||
string(bz))
|
||||
}
|
||||
@ -3,6 +3,8 @@ package testkv
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/orm/internal/stablejson"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/orm/encoding/ormkv"
|
||||
@ -210,10 +212,15 @@ type debugHooks struct {
|
||||
}
|
||||
|
||||
func (d debugHooks) OnInsert(message proto.Message) error {
|
||||
jsonBz, err := stablejson.Marshal(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.debugger.Log(fmt.Sprintf(
|
||||
"ORM INSERT %s %s",
|
||||
message.ProtoReflect().Descriptor().FullName(),
|
||||
message,
|
||||
jsonBz,
|
||||
))
|
||||
if d.hooks != nil {
|
||||
return d.hooks.OnInsert(message)
|
||||
@ -222,11 +229,21 @@ func (d debugHooks) OnInsert(message proto.Message) error {
|
||||
}
|
||||
|
||||
func (d debugHooks) OnUpdate(existing, new proto.Message) error {
|
||||
existingJson, err := stablejson.Marshal(existing)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newJson, err := stablejson.Marshal(new)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.debugger.Log(fmt.Sprintf(
|
||||
"ORM UPDATE %s %s -> %s",
|
||||
existing.ProtoReflect().Descriptor().FullName(),
|
||||
existing,
|
||||
new,
|
||||
existingJson,
|
||||
newJson,
|
||||
))
|
||||
if d.hooks != nil {
|
||||
return d.hooks.OnUpdate(existing, new)
|
||||
@ -235,10 +252,15 @@ func (d debugHooks) OnUpdate(existing, new proto.Message) error {
|
||||
}
|
||||
|
||||
func (d debugHooks) OnDelete(message proto.Message) error {
|
||||
jsonBz, err := stablejson.Marshal(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.debugger.Log(fmt.Sprintf(
|
||||
"ORM DELETE %s %s",
|
||||
message.ProtoReflect().Descriptor().FullName(),
|
||||
message,
|
||||
jsonBz,
|
||||
))
|
||||
if d.hooks != nil {
|
||||
return d.hooks.OnDelete(message)
|
||||
|
||||
31
orm/internal/testpb/bank.proto
Normal file
31
orm/internal/testpb/bank.proto
Normal file
@ -0,0 +1,31 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package testpb;
|
||||
|
||||
import "cosmos/orm/v1alpha1/orm.proto";
|
||||
|
||||
option go_package = "github.com/cosmos/cosmos-sdk/orm/internal/testpb";
|
||||
|
||||
// This is a simulated bank schema used for testing.
|
||||
|
||||
message Balance {
|
||||
option (cosmos.orm.v1alpha1.table) = {
|
||||
id: 1;
|
||||
primary_key:{fields: "address,denom"}
|
||||
index: {id: 1 fields: "denom"}
|
||||
};
|
||||
|
||||
string address = 1;
|
||||
string denom = 2;
|
||||
uint64 amount = 3;
|
||||
}
|
||||
|
||||
message Supply {
|
||||
option (cosmos.orm.v1alpha1.table) = {
|
||||
id: 2;
|
||||
primary_key:{fields: "denom"}
|
||||
};
|
||||
|
||||
string denom = 1;
|
||||
uint64 amount = 2;
|
||||
}
|
||||
1228
orm/internal/testpb/bank.pulsar.go
Normal file
1228
orm/internal/testpb/bank.pulsar.go
Normal file
File diff suppressed because it is too large
Load Diff
125
orm/model/ormdb/file.go
Normal file
125
orm/model/ormdb/file.go
Normal file
@ -0,0 +1,125 @@
|
||||
package ormdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoregistry"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/orm/encoding/encodeutil"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/orm/encoding/ormkv"
|
||||
"github.com/cosmos/cosmos-sdk/orm/types/ormerrors"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/orm/model/ormtable"
|
||||
)
|
||||
|
||||
type fileDescriptorDBOptions struct {
|
||||
Prefix []byte
|
||||
ID uint32
|
||||
TypeResolver ormtable.TypeResolver
|
||||
JSONValidator func(proto.Message) error
|
||||
GetBackend func(context.Context) (ormtable.Backend, error)
|
||||
GetReadBackend func(context.Context) (ormtable.ReadBackend, error)
|
||||
}
|
||||
|
||||
type fileDescriptorDB struct {
|
||||
id uint32
|
||||
prefix []byte
|
||||
tablesById map[uint32]ormtable.Table
|
||||
tablesByName map[protoreflect.FullName]ormtable.Table
|
||||
fileDescriptor protoreflect.FileDescriptor
|
||||
}
|
||||
|
||||
func newFileDescriptorDB(fileDescriptor protoreflect.FileDescriptor, options fileDescriptorDBOptions) (*fileDescriptorDB, error) {
|
||||
prefix := encodeutil.AppendVarUInt32(options.Prefix, options.ID)
|
||||
|
||||
schema := &fileDescriptorDB{
|
||||
id: options.ID,
|
||||
prefix: prefix,
|
||||
tablesById: map[uint32]ormtable.Table{},
|
||||
tablesByName: map[protoreflect.FullName]ormtable.Table{},
|
||||
fileDescriptor: fileDescriptor,
|
||||
}
|
||||
|
||||
resolver := options.TypeResolver
|
||||
if resolver == nil {
|
||||
resolver = protoregistry.GlobalTypes
|
||||
}
|
||||
|
||||
messages := fileDescriptor.Messages()
|
||||
n := messages.Len()
|
||||
for i := 0; i < n; i++ {
|
||||
messageDescriptor := messages.Get(i)
|
||||
tableName := messageDescriptor.FullName()
|
||||
messageType, err := resolver.FindMessageByName(tableName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
table, err := ormtable.Build(ormtable.Options{
|
||||
Prefix: prefix,
|
||||
MessageType: messageType,
|
||||
TypeResolver: resolver,
|
||||
JSONValidator: options.JSONValidator,
|
||||
GetReadBackend: options.GetReadBackend,
|
||||
GetBackend: options.GetBackend,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := table.ID()
|
||||
if _, ok := schema.tablesById[id]; ok {
|
||||
return nil, ormerrors.InvalidTableId.Wrapf("duplicate ID %d for %s", id, tableName)
|
||||
}
|
||||
schema.tablesById[id] = table
|
||||
|
||||
if _, ok := schema.tablesByName[tableName]; ok {
|
||||
return nil, ormerrors.InvalidTableDefinition.Wrapf("duplicate table %s", tableName)
|
||||
}
|
||||
schema.tablesByName[tableName] = table
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
func (f fileDescriptorDB) DecodeEntry(k, v []byte) (ormkv.Entry, error) {
|
||||
r := bytes.NewReader(k)
|
||||
err := encodeutil.SkipPrefix(r, f.prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := binary.ReadUvarint(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if id > math.MaxUint32 {
|
||||
return nil, ormerrors.UnexpectedDecodePrefix.Wrapf("uint32 varint id out of range %d", id)
|
||||
}
|
||||
|
||||
table, ok := f.tablesById[uint32(id)]
|
||||
if !ok {
|
||||
return nil, ormerrors.UnexpectedDecodePrefix.Wrapf("can't find table with id %d", id)
|
||||
}
|
||||
|
||||
return table.DecodeEntry(k, v)
|
||||
}
|
||||
|
||||
func (f fileDescriptorDB) EncodeEntry(entry ormkv.Entry) (k, v []byte, err error) {
|
||||
table, ok := f.tablesByName[entry.GetTableName()]
|
||||
if !ok {
|
||||
return nil, nil, ormerrors.BadDecodeEntry.Wrapf("can't find table %s", entry.GetTableName())
|
||||
}
|
||||
|
||||
return table.EncodeEntry(entry)
|
||||
}
|
||||
|
||||
var _ ormkv.EntryCodec = fileDescriptorDB{}
|
||||
162
orm/model/ormdb/module.go
Normal file
162
orm/model/ormdb/module.go
Normal file
@ -0,0 +1,162 @@
|
||||
package ormdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protodesc"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/orm/encoding/encodeutil"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/orm/encoding/ormkv"
|
||||
"github.com/cosmos/cosmos-sdk/orm/model/ormtable"
|
||||
"github.com/cosmos/cosmos-sdk/orm/types/ormerrors"
|
||||
)
|
||||
|
||||
// ModuleSchema describes the ORM schema for a module.
|
||||
type ModuleSchema struct {
|
||||
// FileDescriptors are the file descriptors that contain ORM tables to use in this schema.
|
||||
// Each file descriptor must have an unique non-zero uint32 ID associated with it.
|
||||
FileDescriptors map[uint32]protoreflect.FileDescriptor
|
||||
|
||||
// Prefix is an optional prefix to prepend to all keys. It is recommended
|
||||
// to leave it empty.
|
||||
Prefix []byte
|
||||
}
|
||||
|
||||
// ModuleDB defines the ORM database type to be used by modules.
|
||||
type ModuleDB interface {
|
||||
ormkv.EntryCodec
|
||||
|
||||
// GetTable returns the table for the provided message type or nil.
|
||||
GetTable(message proto.Message) ormtable.Table
|
||||
}
|
||||
|
||||
type moduleDB struct {
|
||||
prefix []byte
|
||||
filesById map[uint32]*fileDescriptorDB
|
||||
tablesByName map[protoreflect.FullName]ormtable.Table
|
||||
}
|
||||
|
||||
// ModuleDBOptions are options for constructing a ModuleDB.
|
||||
type ModuleDBOptions struct {
|
||||
|
||||
// TypeResolver is an optional type resolver to be used when unmarshaling
|
||||
// protobuf messages. If it is nil, protoregistry.GlobalTypes will be used.
|
||||
TypeResolver ormtable.TypeResolver
|
||||
|
||||
// FileResolver is an optional file resolver that can be used to retrieve
|
||||
// pinned file descriptors that may be different from those available at
|
||||
// runtime. The file descriptor versions returned by this resolver will be
|
||||
// used instead of the ones provided at runtime by the ModuleSchema.
|
||||
FileResolver protodesc.Resolver
|
||||
|
||||
// JSONValidator is an optional validator that can be used for validating
|
||||
// messaging when using ValidateJSON. If it is nil, DefaultJSONValidator
|
||||
// will be used
|
||||
JSONValidator func(proto.Message) error
|
||||
|
||||
// GetBackend is the function used to retrieve the table backend.
|
||||
// See ormtable.Options.GetBackend for more details.
|
||||
GetBackend func(context.Context) (ormtable.Backend, error)
|
||||
|
||||
// GetReadBackend is the function used to retrieve a table read backend.
|
||||
// See ormtable.Options.GetReadBackend for more details.
|
||||
GetReadBackend func(context.Context) (ormtable.ReadBackend, error)
|
||||
}
|
||||
|
||||
// NewModuleDB constructs a ModuleDB instance from the provided schema and options.
|
||||
func NewModuleDB(schema ModuleSchema, options ModuleDBOptions) (ModuleDB, error) {
|
||||
prefix := schema.Prefix
|
||||
db := &moduleDB{
|
||||
prefix: prefix,
|
||||
filesById: map[uint32]*fileDescriptorDB{},
|
||||
tablesByName: map[protoreflect.FullName]ormtable.Table{},
|
||||
}
|
||||
|
||||
for id, fileDescriptor := range schema.FileDescriptors {
|
||||
if id == 0 {
|
||||
return nil, ormerrors.InvalidFileDescriptorID.Wrapf("for %s", fileDescriptor.Path())
|
||||
}
|
||||
|
||||
opts := fileDescriptorDBOptions{
|
||||
ID: id,
|
||||
Prefix: prefix,
|
||||
TypeResolver: options.TypeResolver,
|
||||
JSONValidator: options.JSONValidator,
|
||||
GetBackend: options.GetBackend,
|
||||
GetReadBackend: options.GetReadBackend,
|
||||
}
|
||||
|
||||
if options.FileResolver != nil {
|
||||
// if a FileResolver is provided, we use that to resolve the file
|
||||
// and not the one provided as a different pinned file descriptor
|
||||
// may have been provided
|
||||
var err error
|
||||
fileDescriptor, err = options.FileResolver.FindFileByPath(fileDescriptor.Path())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
fdSchema, err := newFileDescriptorDB(fileDescriptor, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.filesById[id] = fdSchema
|
||||
for name, table := range fdSchema.tablesByName {
|
||||
if _, ok := db.tablesByName[name]; ok {
|
||||
return nil, ormerrors.UnexpectedError.Wrapf("duplicate table %s", name)
|
||||
}
|
||||
|
||||
db.tablesByName[name] = table
|
||||
}
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func (m moduleDB) DecodeEntry(k, v []byte) (ormkv.Entry, error) {
|
||||
r := bytes.NewReader(k)
|
||||
err := encodeutil.SkipPrefix(r, m.prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id, err := binary.ReadUvarint(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if id > math.MaxUint32 {
|
||||
return nil, ormerrors.UnexpectedDecodePrefix.Wrapf("uint32 varint id out of range %d", id)
|
||||
}
|
||||
|
||||
fileSchema, ok := m.filesById[uint32(id)]
|
||||
if !ok {
|
||||
return nil, ormerrors.UnexpectedDecodePrefix.Wrapf("can't find FileDescriptor schema with id %d", id)
|
||||
}
|
||||
|
||||
return fileSchema.DecodeEntry(k, v)
|
||||
}
|
||||
|
||||
func (m moduleDB) EncodeEntry(entry ormkv.Entry) (k, v []byte, err error) {
|
||||
tableName := entry.GetTableName()
|
||||
table, ok := m.tablesByName[tableName]
|
||||
if !ok {
|
||||
return nil, nil, ormerrors.BadDecodeEntry.Wrapf("can't find table %s", tableName)
|
||||
}
|
||||
|
||||
return table.EncodeEntry(entry)
|
||||
}
|
||||
|
||||
func (m moduleDB) GetTable(message proto.Message) ormtable.Table {
|
||||
return m.tablesByName[message.ProtoReflect().Descriptor().FullName()]
|
||||
}
|
||||
220
orm/model/ormdb/module_test.go
Normal file
220
orm/model/ormdb/module_test.go
Normal file
@ -0,0 +1,220 @@
|
||||
package ormdb_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/golden"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/orm/internal/testkv"
|
||||
"github.com/cosmos/cosmos-sdk/orm/internal/testpb"
|
||||
"github.com/cosmos/cosmos-sdk/orm/model/ormdb"
|
||||
"github.com/cosmos/cosmos-sdk/orm/model/ormtable"
|
||||
)
|
||||
|
||||
// These tests use a simulated bank keeper. Addresses and balances use
|
||||
// string and uint64 types respectively for simplicity.
|
||||
|
||||
var TestBankSchema = ormdb.ModuleSchema{
|
||||
FileDescriptors: map[uint32]protoreflect.FileDescriptor{
|
||||
1: testpb.File_testpb_bank_proto,
|
||||
},
|
||||
}
|
||||
|
||||
type keeper struct {
|
||||
balanceTable ormtable.Table
|
||||
balanceAddressDenomIndex ormtable.UniqueIndex
|
||||
balanceDenomIndex ormtable.Index
|
||||
|
||||
supplyTable ormtable.Table
|
||||
supplyDenomIndex ormtable.UniqueIndex
|
||||
}
|
||||
|
||||
func (k keeper) Send(ctx context.Context, from, to, denom string, amount uint64) error {
|
||||
err := k.safeSubBalance(ctx, from, denom, amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return k.addBalance(ctx, to, denom, amount)
|
||||
}
|
||||
|
||||
func (k keeper) Mint(ctx context.Context, acct, denom string, amount uint64) error {
|
||||
supply := &testpb.Supply{Denom: denom}
|
||||
_, err := k.supplyTable.Get(ctx, supply)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
supply.Amount = supply.Amount + amount
|
||||
err = k.supplyTable.Save(ctx, supply)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return k.addBalance(ctx, acct, denom, amount)
|
||||
}
|
||||
|
||||
func (k keeper) Burn(ctx context.Context, acct, denom string, amount uint64) error {
|
||||
supply := &testpb.Supply{Denom: denom}
|
||||
found, err := k.supplyTable.Get(ctx, supply)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("no supply for %s", denom)
|
||||
}
|
||||
|
||||
if amount > supply.Amount {
|
||||
return fmt.Errorf("insufficient supply")
|
||||
}
|
||||
|
||||
supply.Amount = supply.Amount - amount
|
||||
|
||||
if supply.Amount == 0 {
|
||||
err = k.supplyTable.Delete(ctx, supply)
|
||||
} else {
|
||||
err = k.supplyTable.Save(ctx, supply)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return k.safeSubBalance(ctx, acct, denom, amount)
|
||||
}
|
||||
|
||||
func (k keeper) Balance(ctx context.Context, acct, denom string) (uint64, error) {
|
||||
balance := &testpb.Balance{Address: acct, Denom: denom}
|
||||
_, err := k.balanceTable.Get(ctx, balance)
|
||||
return balance.Amount, err
|
||||
}
|
||||
|
||||
func (k keeper) Supply(ctx context.Context, denom string) (uint64, error) {
|
||||
supply := &testpb.Supply{Denom: denom}
|
||||
_, err := k.supplyTable.Get(ctx, supply)
|
||||
return supply.Amount, err
|
||||
}
|
||||
|
||||
func (k keeper) addBalance(ctx context.Context, acct, denom string, amount uint64) error {
|
||||
balance := &testpb.Balance{Address: acct, Denom: denom}
|
||||
_, err := k.balanceTable.Get(ctx, balance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
balance.Amount = balance.Amount + amount
|
||||
return k.balanceTable.Save(ctx, balance)
|
||||
}
|
||||
|
||||
func (k keeper) safeSubBalance(ctx context.Context, acct, denom string, amount uint64) error {
|
||||
balance := &testpb.Balance{Address: acct, Denom: denom}
|
||||
found, err := k.balanceTable.Get(ctx, balance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("acct %x has no balance for %s", acct, denom)
|
||||
}
|
||||
|
||||
if amount > balance.Amount {
|
||||
return fmt.Errorf("insufficient funds")
|
||||
}
|
||||
|
||||
balance.Amount = balance.Amount - amount
|
||||
|
||||
if balance.Amount == 0 {
|
||||
return k.balanceTable.Delete(ctx, balance)
|
||||
} else {
|
||||
return k.balanceTable.Save(ctx, balance)
|
||||
}
|
||||
}
|
||||
|
||||
func newKeeper(db ormdb.ModuleDB) keeper {
|
||||
k := keeper{
|
||||
balanceTable: db.GetTable(&testpb.Balance{}),
|
||||
supplyTable: db.GetTable(&testpb.Supply{}),
|
||||
}
|
||||
|
||||
k.balanceAddressDenomIndex = k.balanceTable.GetUniqueIndex("address,denom")
|
||||
k.balanceDenomIndex = k.balanceTable.GetIndex("denom")
|
||||
k.supplyDenomIndex = k.supplyTable.GetUniqueIndex("denom")
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
func TestModuleDB(t *testing.T) {
|
||||
// create db & debug context
|
||||
db, err := ormdb.NewModuleDB(TestBankSchema, ormdb.ModuleDBOptions{})
|
||||
assert.NilError(t, err)
|
||||
debugBuf := &strings.Builder{}
|
||||
store := testkv.NewDebugBackend(
|
||||
testkv.NewSharedMemBackend(),
|
||||
&testkv.EntryCodecDebugger{
|
||||
EntryCodec: db,
|
||||
Print: func(s string) { debugBuf.WriteString(s + "\n") },
|
||||
},
|
||||
)
|
||||
ctx := ormtable.WrapContextDefault(store)
|
||||
|
||||
// create keeper
|
||||
k := newKeeper(db)
|
||||
assert.Assert(t, k.balanceTable != nil)
|
||||
assert.Assert(t, k.balanceAddressDenomIndex != nil)
|
||||
assert.Assert(t, k.balanceDenomIndex != nil)
|
||||
assert.Assert(t, k.supplyTable != nil)
|
||||
assert.Assert(t, k.supplyDenomIndex != nil)
|
||||
|
||||
// mint coins
|
||||
denom := "foo"
|
||||
acct1 := "bob"
|
||||
err = k.Mint(ctx, acct1, denom, 100)
|
||||
assert.NilError(t, err)
|
||||
bal, err := k.Balance(ctx, acct1, denom)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, uint64(100), bal)
|
||||
supply, err := k.Supply(ctx, denom)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, uint64(100), supply)
|
||||
|
||||
// send coins
|
||||
acct2 := "sally"
|
||||
err = k.Send(ctx, acct1, acct2, denom, 30)
|
||||
bal, err = k.Balance(ctx, acct1, denom)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, uint64(70), bal)
|
||||
bal, err = k.Balance(ctx, acct2, denom)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, uint64(30), bal)
|
||||
|
||||
// burn coins
|
||||
err = k.Burn(ctx, acct2, denom, 3)
|
||||
bal, err = k.Balance(ctx, acct2, denom)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, uint64(27), bal)
|
||||
supply, err = k.Supply(ctx, denom)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, uint64(97), supply)
|
||||
|
||||
// check debug output
|
||||
golden.Assert(t, debugBuf.String(), "bank_scenario.golden")
|
||||
|
||||
// check decode & encode
|
||||
it, err := store.CommitmentStore().Iterator(nil, nil)
|
||||
assert.NilError(t, err)
|
||||
for it.Valid() {
|
||||
entry, err := db.DecodeEntry(it.Key(), it.Value())
|
||||
assert.NilError(t, err)
|
||||
k, v, err := db.EncodeEntry(entry)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, bytes.Equal(k, it.Key()))
|
||||
assert.Assert(t, bytes.Equal(v, it.Value()))
|
||||
it.Next()
|
||||
}
|
||||
}
|
||||
58
orm/model/ormdb/testdata/bank_scenario.golden
vendored
Normal file
58
orm/model/ormdb/testdata/bank_scenario.golden
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
GET 010200666f6f
|
||||
PK testpb.Supply foo -> {"denom":"foo"}
|
||||
GET 010200666f6f
|
||||
PK testpb.Supply foo -> {"denom":"foo"}
|
||||
ORM INSERT testpb.Supply {"denom":"foo","amount":100}
|
||||
SET 010200666f6f 1064
|
||||
PK testpb.Supply foo -> {"denom":"foo","amount":100}
|
||||
GET 010100626f6200666f6f
|
||||
PK testpb.Balance bob/foo -> {"address":"bob","denom":"foo"}
|
||||
GET 010100626f6200666f6f
|
||||
PK testpb.Balance bob/foo -> {"address":"bob","denom":"foo"}
|
||||
ORM INSERT testpb.Balance {"address":"bob","denom":"foo","amount":100}
|
||||
SET 010100626f6200666f6f 1864
|
||||
PK testpb.Balance bob/foo -> {"address":"bob","denom":"foo","amount":100}
|
||||
SET 010101666f6f00626f62
|
||||
IDX testpb.Balance denom/address : foo/bob -> bob/foo
|
||||
GET 010100626f6200666f6f 1864
|
||||
PK testpb.Balance bob/foo -> {"address":"bob","denom":"foo","amount":100}
|
||||
GET 010200666f6f 1064
|
||||
PK testpb.Supply foo -> {"denom":"foo","amount":100}
|
||||
GET 010100626f6200666f6f 1864
|
||||
PK testpb.Balance bob/foo -> {"address":"bob","denom":"foo","amount":100}
|
||||
GET 010100626f6200666f6f 1864
|
||||
PK testpb.Balance bob/foo -> {"address":"bob","denom":"foo","amount":100}
|
||||
ORM UPDATE testpb.Balance {"address":"bob","denom":"foo","amount":100} -> {"address":"bob","denom":"foo","amount":70}
|
||||
SET 010100626f6200666f6f 1846
|
||||
PK testpb.Balance bob/foo -> {"address":"bob","denom":"foo","amount":70}
|
||||
GET 01010073616c6c7900666f6f
|
||||
PK testpb.Balance sally/foo -> {"address":"sally","denom":"foo"}
|
||||
GET 01010073616c6c7900666f6f
|
||||
PK testpb.Balance sally/foo -> {"address":"sally","denom":"foo"}
|
||||
ORM INSERT testpb.Balance {"address":"sally","denom":"foo","amount":30}
|
||||
SET 01010073616c6c7900666f6f 181e
|
||||
PK testpb.Balance sally/foo -> {"address":"sally","denom":"foo","amount":30}
|
||||
SET 010101666f6f0073616c6c79
|
||||
IDX testpb.Balance denom/address : foo/sally -> sally/foo
|
||||
GET 010100626f6200666f6f 1846
|
||||
PK testpb.Balance bob/foo -> {"address":"bob","denom":"foo","amount":70}
|
||||
GET 01010073616c6c7900666f6f 181e
|
||||
PK testpb.Balance sally/foo -> {"address":"sally","denom":"foo","amount":30}
|
||||
GET 010200666f6f 1064
|
||||
PK testpb.Supply foo -> {"denom":"foo","amount":100}
|
||||
GET 010200666f6f 1064
|
||||
PK testpb.Supply foo -> {"denom":"foo","amount":100}
|
||||
ORM UPDATE testpb.Supply {"denom":"foo","amount":100} -> {"denom":"foo","amount":97}
|
||||
SET 010200666f6f 1061
|
||||
PK testpb.Supply foo -> {"denom":"foo","amount":97}
|
||||
GET 01010073616c6c7900666f6f 181e
|
||||
PK testpb.Balance sally/foo -> {"address":"sally","denom":"foo","amount":30}
|
||||
GET 01010073616c6c7900666f6f 181e
|
||||
PK testpb.Balance sally/foo -> {"address":"sally","denom":"foo","amount":30}
|
||||
ORM UPDATE testpb.Balance {"address":"sally","denom":"foo","amount":30} -> {"address":"sally","denom":"foo","amount":27}
|
||||
SET 01010073616c6c7900666f6f 181b
|
||||
PK testpb.Balance sally/foo -> {"address":"sally","denom":"foo","amount":27}
|
||||
GET 01010073616c6c7900666f6f 181b
|
||||
PK testpb.Balance sally/foo -> {"address":"sally","denom":"foo","amount":27}
|
||||
GET 010200666f6f 1061
|
||||
PK testpb.Supply foo -> {"denom":"foo","amount":97}
|
||||
@ -33,18 +33,22 @@ func (p primaryKeyIndex) Iterator(ctx context.Context, options ...ormlist.Option
|
||||
|
||||
func (p primaryKeyIndex) doNotImplement() {}
|
||||
|
||||
func (p primaryKeyIndex) Has(context context.Context, key ...interface{}) (found bool, err error) {
|
||||
ctx, err := p.getReadBackend(context)
|
||||
func (p primaryKeyIndex) Has(ctx context.Context, key ...interface{}) (found bool, err error) {
|
||||
backend, err := p.getReadBackend(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
keyBz, err := p.EncodeKey(encodeutil.ValuesOf(key...))
|
||||
return p.has(backend, encodeutil.ValuesOf(key...))
|
||||
}
|
||||
|
||||
func (p primaryKeyIndex) has(backend ReadBackend, values []protoreflect.Value) (found bool, err error) {
|
||||
keyBz, err := p.EncodeKey(values)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return ctx.CommitmentStoreReader().Has(keyBz)
|
||||
return backend.CommitmentStoreReader().Has(keyBz)
|
||||
}
|
||||
|
||||
func (p primaryKeyIndex) Get(ctx context.Context, message proto.Message, values ...interface{}) (found bool, err error) {
|
||||
@ -56,6 +60,15 @@ func (p primaryKeyIndex) Get(ctx context.Context, message proto.Message, values
|
||||
return p.get(backend, message, encodeutil.ValuesOf(values...))
|
||||
}
|
||||
|
||||
func (p primaryKeyIndex) get(backend ReadBackend, message proto.Message, values []protoreflect.Value) (found bool, err error) {
|
||||
key, err := p.EncodeKey(values)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return p.getByKeyBytes(backend, key, values, message)
|
||||
}
|
||||
|
||||
func (t primaryKeyIndex) DeleteByKey(ctx context.Context, primaryKeyValues ...interface{}) error {
|
||||
return t.doDeleteByKey(ctx, encodeutil.ValuesOf(primaryKeyValues...))
|
||||
}
|
||||
@ -109,15 +122,6 @@ func (t primaryKeyIndex) doDeleteByKey(ctx context.Context, primaryKeyValues []p
|
||||
return writer.Write()
|
||||
}
|
||||
|
||||
func (p primaryKeyIndex) get(backend ReadBackend, message proto.Message, values []protoreflect.Value) (found bool, err error) {
|
||||
key, err := p.EncodeKey(values)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return p.getByKeyBytes(backend, key, values, message)
|
||||
}
|
||||
|
||||
func (p primaryKeyIndex) getByKeyBytes(store ReadBackend, key []byte, keyValues []protoreflect.Value, message proto.Message) (found bool, err error) {
|
||||
bz, err := store.CommitmentStoreReader().Get(key)
|
||||
if err != nil {
|
||||
|
||||
@ -22,11 +22,11 @@ func TestSingleton(t *testing.T) {
|
||||
assert.NilError(t, err)
|
||||
store := ormtable.WrapContextDefault(testkv.NewSplitMemBackend())
|
||||
|
||||
found, err := singleton.Has(store)
|
||||
found, err := singleton.Has(store, val)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !found)
|
||||
assert.NilError(t, singleton.Save(store, val))
|
||||
found, err = singleton.Has(store)
|
||||
found, err = singleton.Has(store, val)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, found)
|
||||
|
||||
|
||||
@ -17,7 +17,17 @@ import (
|
||||
// systems, for instance to enable backwards compatibility when a major
|
||||
// migration needs to be performed.
|
||||
type View interface {
|
||||
UniqueIndex
|
||||
Index
|
||||
|
||||
// Has returns true if there is an entity in the table with the same
|
||||
// primary key as message. Other fields besides the primary key fields will not
|
||||
// be used for retrieval.
|
||||
Has(ctx context.Context, message proto.Message) (found bool, err error)
|
||||
|
||||
// Get retrieves the message if one exists for the primary key fields
|
||||
// set on the message. Other fields besides the primary key fields will not
|
||||
// be used for retrieval.
|
||||
Get(ctx context.Context, message proto.Message) (found bool, err error)
|
||||
|
||||
// GetIndex returns the index referenced by the provided fields if
|
||||
// one exists or nil. Note that some concrete indexes can be retrieved by
|
||||
@ -54,14 +64,16 @@ type Table interface {
|
||||
|
||||
// Insert inserts the provided entry in the store and fails if there is
|
||||
// an unique key violation. See Save for more details on behavior.
|
||||
Insert(context context.Context, message proto.Message) error
|
||||
Insert(ctx context.Context, message proto.Message) error
|
||||
|
||||
// Update updates the provided entry in the store and fails if an entry
|
||||
// with a matching primary key does not exist. See Save for more details
|
||||
// on behavior.
|
||||
Update(context context.Context, message proto.Message) error
|
||||
Update(ctx context.Context, message proto.Message) error
|
||||
|
||||
// Delete deletes the entry with the provided primary key from the store.
|
||||
// Delete deletes the entry with the with primary key fields set on message
|
||||
// if one exists. Other fields besides the primary key fields will not
|
||||
// be used for retrieval.
|
||||
//
|
||||
// If store implement the Hooks interface, the OnDelete hook method will
|
||||
// be called.
|
||||
@ -69,7 +81,7 @@ type Table interface {
|
||||
// Delete attempts to be atomic with respect to the underlying store,
|
||||
// meaning that either the full save operation is written or the store is
|
||||
// left unchanged, unless there is an error with the underlying store.
|
||||
Delete(context context.Context, message proto.Message) error
|
||||
Delete(ctx context.Context, message proto.Message) error
|
||||
|
||||
// DefaultJSON returns default JSON that can be used as a template for
|
||||
// genesis files.
|
||||
|
||||
@ -364,6 +364,29 @@ func (t tableImpl) ID() uint32 {
|
||||
return t.tableId
|
||||
}
|
||||
|
||||
func (t tableImpl) Has(ctx context.Context, message proto.Message) (found bool, err error) {
|
||||
backend, err := t.getReadBackend(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
keyValues := t.primaryKeyIndex.PrimaryKeyCodec.GetKeyValues(message.ProtoReflect())
|
||||
return t.primaryKeyIndex.has(backend, keyValues)
|
||||
}
|
||||
|
||||
// Get retrieves the message if one exists for the primary key fields
|
||||
// set on the message. Other fields besides the primary key fields will not
|
||||
// be used for retrieval.
|
||||
func (t tableImpl) Get(ctx context.Context, message proto.Message) (found bool, err error) {
|
||||
backend, err := t.getReadBackend(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
keyValues := t.primaryKeyIndex.PrimaryKeyCodec.GetKeyValues(message.ProtoReflect())
|
||||
return t.primaryKeyIndex.get(backend, message, keyValues)
|
||||
}
|
||||
|
||||
var _ Table = &tableImpl{}
|
||||
|
||||
type saveMode int
|
||||
|
||||
@ -371,14 +371,15 @@ func runTestScenario(t *testing.T, table ormtable.Table, backend ormtable.Backen
|
||||
data = append(data, &testpb.ExampleTable{U32: 9})
|
||||
err = table.Save(ctx, data[10])
|
||||
assert.NilError(t, err)
|
||||
found, err = table.Get(ctx, &a, uint32(9), int64(0), "")
|
||||
pkIndex := table.GetUniqueIndex("u32,i64,str")
|
||||
found, err = pkIndex.Get(ctx, &a, uint32(9), int64(0), "")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, found)
|
||||
assert.DeepEqual(t, data[10], &a, protocmp.Transform())
|
||||
// and update it
|
||||
data[10].B = true
|
||||
assert.NilError(t, table.Save(ctx, data[10]))
|
||||
found, err = table.Get(ctx, &a, uint32(9), int64(0), "")
|
||||
found, err = pkIndex.Get(ctx, &a, uint32(9), int64(0), "")
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, found)
|
||||
assert.DeepEqual(t, data[10], &a, protocmp.Transform())
|
||||
@ -401,10 +402,10 @@ func runTestScenario(t *testing.T, table ormtable.Table, backend ormtable.Backen
|
||||
|
||||
// let's delete item 5
|
||||
key5 := []interface{}{uint32(7), int64(-2), "abe"}
|
||||
err = table.DeleteByKey(ctx, key5...)
|
||||
err = pkIndex.DeleteByKey(ctx, key5...)
|
||||
assert.NilError(t, err)
|
||||
// it should be gone
|
||||
found, err = table.Has(ctx, key5...)
|
||||
found, err = pkIndex.Has(ctx, key5...)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, !found)
|
||||
// and missing from the iterator
|
||||
|
||||
16
orm/model/ormtable/testdata/test_auto_inc.golden
vendored
16
orm/model/ormtable/testdata/test_auto_inc.golden
vendored
@ -1,31 +1,31 @@
|
||||
GET 03000000000000000005
|
||||
PK testpb.ExampleAutoIncrementTable 5 -> id:5
|
||||
PK testpb.ExampleAutoIncrementTable 5 -> {"id":5}
|
||||
GET 03808002
|
||||
SEQ testpb.ExampleAutoIncrementTable 0
|
||||
GET 03000000000000000001
|
||||
PK testpb.ExampleAutoIncrementTable 1 -> id:1
|
||||
ORM INSERT testpb.ExampleAutoIncrementTable id:1 x:"foo" y:5
|
||||
PK testpb.ExampleAutoIncrementTable 1 -> {"id":1}
|
||||
ORM INSERT testpb.ExampleAutoIncrementTable {"id":1,"x":"foo","y":5}
|
||||
HAS 0301666f6f
|
||||
ERR:EOF
|
||||
SET 03000000000000000001 1203666f6f1805
|
||||
PK testpb.ExampleAutoIncrementTable 1 -> id:1 x:"foo" y:5
|
||||
PK testpb.ExampleAutoIncrementTable 1 -> {"id":1,"x":"foo","y":5}
|
||||
SET 03808002 01
|
||||
SEQ testpb.ExampleAutoIncrementTable 1
|
||||
SET 0301666f6f 0000000000000001
|
||||
UNIQ testpb.ExampleAutoIncrementTable x : "foo" -> 1
|
||||
UNIQ testpb.ExampleAutoIncrementTable x : foo -> 1
|
||||
GET 03808002 01
|
||||
SEQ testpb.ExampleAutoIncrementTable 1
|
||||
ITERATOR 0300 -> 0301
|
||||
VALID true
|
||||
KEY 03000000000000000001 1203666f6f1805
|
||||
PK testpb.ExampleAutoIncrementTable 1 -> id:1 x:"foo" y:5
|
||||
PK testpb.ExampleAutoIncrementTable 1 -> {"id":1,"x":"foo","y":5}
|
||||
NEXT
|
||||
VALID false
|
||||
ITERATOR 0300 -> 0301
|
||||
VALID true
|
||||
KEY 03000000000000000001 1203666f6f1805
|
||||
PK testpb.ExampleAutoIncrementTable 1 -> id:1 x:"foo" y:5
|
||||
PK testpb.ExampleAutoIncrementTable 1 -> {"id":1,"x":"foo","y":5}
|
||||
KEY 03000000000000000001 1203666f6f1805
|
||||
PK testpb.ExampleAutoIncrementTable 1 -> id:1 x:"foo" y:5
|
||||
PK testpb.ExampleAutoIncrementTable 1 -> {"id":1,"x":"foo","y":5}
|
||||
NEXT
|
||||
VALID false
|
||||
|
||||
522
orm/model/ormtable/testdata/test_scenario.golden
vendored
522
orm/model/ormtable/testdata/test_scenario.golden
vendored
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,7 @@ var (
|
||||
NotFoundOnUpdate = errors.New(codespace, 10, "can't update object which doesn't exist")
|
||||
PrimaryKeyInvalidOnUpdate = errors.New(codespace, 11, "can't update object with missing or invalid primary key")
|
||||
AutoIncrementKeyAlreadySet = errors.New(codespace, 12, "can't create with auto-increment primary key already set")
|
||||
CantFindIndexer = errors.New(codespace, 13, "can't find indexer")
|
||||
CantFindIndex = errors.New(codespace, 13, "can't find index")
|
||||
UnexpectedDecodePrefix = errors.New(codespace, 14, "unexpected prefix while trying to decode an entry")
|
||||
BytesFieldTooLong = errors.New(codespace, 15, "bytes field is longer than 255 bytes")
|
||||
UnsupportedOperation = errors.New(codespace, 16, "unsupported operation")
|
||||
@ -30,4 +30,5 @@ var (
|
||||
JSONImportError = errors.New(codespace, 23, "json import error")
|
||||
UniqueKeyViolation = errors.New(codespace, 24, "unique key violation")
|
||||
InvalidTableDefinition = errors.New(codespace, 25, "invalid table definition")
|
||||
InvalidFileDescriptorID = errors.New(codespace, 26, "invalid file descriptor ID")
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user