793 lines
24 KiB
Go
793 lines
24 KiB
Go
package ormtable_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
dbm "github.com/tendermint/tm-db"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/reflect/protoreflect"
|
|
"google.golang.org/protobuf/testing/protocmp"
|
|
"gotest.tools/v3/assert"
|
|
"gotest.tools/v3/golden"
|
|
"pgregory.net/rapid"
|
|
|
|
"github.com/cosmos/cosmos-sdk/orm/types/kv"
|
|
|
|
queryv1beta1 "github.com/cosmos/cosmos-sdk/api/cosmos/base/query/v1beta1"
|
|
sdkerrors "github.com/cosmos/cosmos-sdk/errors"
|
|
"github.com/cosmos/cosmos-sdk/orm/encoding/ormkv"
|
|
"github.com/cosmos/cosmos-sdk/orm/internal/testkv"
|
|
"github.com/cosmos/cosmos-sdk/orm/internal/testpb"
|
|
"github.com/cosmos/cosmos-sdk/orm/internal/testutil"
|
|
"github.com/cosmos/cosmos-sdk/orm/model/ormlist"
|
|
"github.com/cosmos/cosmos-sdk/orm/model/ormtable"
|
|
"github.com/cosmos/cosmos-sdk/orm/types/ormerrors"
|
|
)
|
|
|
|
func TestScenario(t *testing.T) {
|
|
table, err := ormtable.Build(ormtable.Options{
|
|
MessageType: (&testpb.ExampleTable{}).ProtoReflect().Type(),
|
|
})
|
|
assert.NilError(t, err)
|
|
|
|
// first run tests with a split index-commitment store
|
|
runTestScenario(t, table, testkv.NewSplitMemBackend())
|
|
|
|
// now run tests with a shared index-commitment store
|
|
|
|
// we're going to wrap this test in a debug store and save the decoded debug
|
|
// messages, these will be checked against a golden file at the end of the
|
|
// test. the golden file can be used for fine-grained debugging of kv-store
|
|
// layout
|
|
debugBuf := &strings.Builder{}
|
|
store := testkv.NewDebugBackend(
|
|
testkv.NewSharedMemBackend(),
|
|
&testkv.EntryCodecDebugger{
|
|
EntryCodec: table,
|
|
Print: func(s string) { debugBuf.WriteString(s + "\n") },
|
|
},
|
|
)
|
|
|
|
runTestScenario(t, table, store)
|
|
|
|
// we're going to store debug data in a golden file to make sure that
|
|
// logical decoding works successfully
|
|
// run `go test pkgname -test.update-golden` to update the golden file
|
|
// see https://pkg.go.dev/gotest.tools/v3/golden for docs
|
|
golden.Assert(t, debugBuf.String(), "test_scenario.golden")
|
|
|
|
checkEncodeDecodeEntries(t, table, store.IndexStoreReader())
|
|
}
|
|
|
|
// isolated test for bug - https://github.com/cosmos/cosmos-sdk/issues/11431
|
|
func TestPaginationLimitCountTotal(t *testing.T) {
|
|
table, err := ormtable.Build(ormtable.Options{
|
|
MessageType: (&testpb.ExampleTable{}).ProtoReflect().Type(),
|
|
})
|
|
backend := testkv.NewSplitMemBackend()
|
|
ctx := ormtable.WrapContextDefault(backend)
|
|
store, err := testpb.NewExampleTableTable(table)
|
|
assert.NilError(t, err)
|
|
|
|
assert.NilError(t, store.Insert(ctx, &testpb.ExampleTable{U32: 4, I64: 2, Str: "co"}))
|
|
assert.NilError(t, store.Insert(ctx, &testpb.ExampleTable{U32: 5, I64: 2, Str: "sm"}))
|
|
assert.NilError(t, store.Insert(ctx, &testpb.ExampleTable{U32: 6, I64: 2, Str: "os"}))
|
|
|
|
it, err := store.List(ctx, &testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{Limit: 3, CountTotal: true}))
|
|
assert.NilError(t, err)
|
|
assert.Check(t, it.Next())
|
|
|
|
it, err = store.List(ctx, &testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{Limit: 4, CountTotal: true}))
|
|
assert.NilError(t, err)
|
|
assert.Check(t, it.Next())
|
|
|
|
it, err = store.List(ctx, &testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{Limit: 1, CountTotal: true}))
|
|
assert.NilError(t, err)
|
|
for it.Next() {
|
|
}
|
|
pr := it.PageResponse()
|
|
assert.Check(t, pr != nil)
|
|
assert.Equal(t, uint64(3), pr.Total)
|
|
|
|
}
|
|
|
|
func TestImportedMessageIterator(t *testing.T) {
|
|
table, err := ormtable.Build(ormtable.Options{
|
|
MessageType: (&testpb.ExampleTimestamp{}).ProtoReflect().Type(),
|
|
})
|
|
backend := testkv.NewSplitMemBackend()
|
|
ctx := ormtable.WrapContextDefault(backend)
|
|
store, err := testpb.NewExampleTimestampTable(table)
|
|
assert.NilError(t, err)
|
|
|
|
past, err := time.Parse("2006-01-02", "2000-01-01")
|
|
assert.NilError(t, err)
|
|
middle, err := time.Parse("2006-01-02", "2020-01-01")
|
|
assert.NilError(t, err)
|
|
future, err := time.Parse("2006-01-02", "2049-01-01")
|
|
assert.NilError(t, err)
|
|
|
|
pastPb, middlePb, futurePb := timestamppb.New(past), timestamppb.New(middle), timestamppb.New(future)
|
|
timeOrder := [3]*timestamppb.Timestamp{pastPb, middlePb, futurePb}
|
|
|
|
assert.NilError(t, store.Insert(ctx, &testpb.ExampleTimestamp{
|
|
Name: "foo",
|
|
Ts: pastPb,
|
|
}))
|
|
assert.NilError(t, store.Insert(ctx, &testpb.ExampleTimestamp{
|
|
Name: "bar",
|
|
Ts: middlePb,
|
|
}))
|
|
assert.NilError(t, store.Insert(ctx, &testpb.ExampleTimestamp{
|
|
Name: "baz",
|
|
Ts: futurePb,
|
|
}))
|
|
|
|
from, to := testpb.ExampleTimestampTsIndexKey{}.WithTs(timestamppb.New(past)), testpb.ExampleTimestampTsIndexKey{}.WithTs(timestamppb.New(future))
|
|
it, err := store.ListRange(ctx, from, to)
|
|
assert.NilError(t, err)
|
|
|
|
i := 0
|
|
for it.Next() {
|
|
v, err := it.Value()
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, timeOrder[i].String(), v.Ts.String())
|
|
i++
|
|
}
|
|
}
|
|
|
|
// check that the ormkv.Entry's decode and encode to the same bytes
|
|
func checkEncodeDecodeEntries(t *testing.T, table ormtable.Table, store kv.ReadonlyStore) {
|
|
it, err := store.Iterator(nil, nil)
|
|
assert.NilError(t, err)
|
|
for it.Valid() {
|
|
key := it.Key()
|
|
value := it.Value()
|
|
entry, err := table.DecodeEntry(key, value)
|
|
assert.NilError(t, err)
|
|
k, v, err := table.EncodeEntry(entry)
|
|
assert.Assert(t, bytes.Equal(key, k), "%x %x %s", key, k, entry)
|
|
assert.Assert(t, bytes.Equal(value, v), "%x %x %s", value, v, entry)
|
|
it.Next()
|
|
}
|
|
}
|
|
|
|
func runTestScenario(t *testing.T, table ormtable.Table, backend ormtable.Backend) {
|
|
ctx := ormtable.WrapContextDefault(backend)
|
|
store, err := testpb.NewExampleTableTable(table)
|
|
|
|
// let's create 10 data items we'll use later and give them indexes
|
|
data := []*testpb.ExampleTable{
|
|
{U32: 4, I64: -2, Str: "abc", U64: 7}, // 0
|
|
{U32: 4, I64: -2, Str: "abd", U64: 7}, // 1
|
|
{U32: 4, I64: -1, Str: "abc", U64: 8}, // 2
|
|
{U32: 5, I64: -2, Str: "abd", U64: 8}, // 3
|
|
{U32: 5, I64: -2, Str: "abe", U64: 9}, // 4
|
|
{U32: 7, I64: -2, Str: "abe", U64: 10}, // 5
|
|
{U32: 7, I64: -1, Str: "abe", U64: 11}, // 6
|
|
{U32: 8, I64: -4, Str: "abc", U64: 11}, // 7
|
|
{U32: 8, I64: 1, Str: "abc", U64: 12}, // 8
|
|
{U32: 8, I64: 1, Str: "abd", U64: 10}, // 9
|
|
}
|
|
|
|
// let's make a function to match what's in our iterator with what we
|
|
// expect using indexes in the data array above
|
|
assertIteratorItems := func(it ormtable.Iterator, xs ...int) {
|
|
for _, i := range xs {
|
|
assert.Assert(t, it.Next())
|
|
msg, err := it.GetMessage()
|
|
assert.NilError(t, err)
|
|
//t.Logf("data[%d] %v == %v", i, data[i], msg)
|
|
assert.DeepEqual(t, data[i], msg, protocmp.Transform())
|
|
}
|
|
// make sure the iterator is done
|
|
assert.Assert(t, !it.Next())
|
|
}
|
|
|
|
// insert one record
|
|
err = store.Insert(ctx, data[0])
|
|
// trivial prefix query has one record
|
|
it, err := store.List(ctx, testpb.ExampleTablePrimaryKey{})
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 0)
|
|
|
|
// insert one record
|
|
err = store.Insert(ctx, data[1])
|
|
// trivial prefix query has two records
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{})
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 0, 1)
|
|
|
|
// insert the other records
|
|
assert.NilError(t, err)
|
|
for i := 2; i < len(data); i++ {
|
|
err = store.Insert(ctx, data[i])
|
|
assert.NilError(t, err)
|
|
}
|
|
|
|
// let's do a prefix query on the primary key
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}.WithU32(8))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 7, 8, 9)
|
|
|
|
// let's try a reverse prefix query
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}.WithU32(4), ormlist.Reverse())
|
|
assert.NilError(t, err)
|
|
defer it.Close()
|
|
assertIteratorItems(it, 2, 1, 0)
|
|
|
|
// let's try a range query
|
|
it, err = store.ListRange(ctx,
|
|
testpb.ExampleTablePrimaryKey{}.WithU32I64(4, -1),
|
|
testpb.ExampleTablePrimaryKey{}.WithU32(7),
|
|
)
|
|
assert.NilError(t, err)
|
|
defer it.Close()
|
|
assertIteratorItems(it, 2, 3, 4, 5, 6)
|
|
|
|
// and another range query
|
|
it, err = store.ListRange(ctx,
|
|
testpb.ExampleTablePrimaryKey{}.WithU32I64(5, -3),
|
|
testpb.ExampleTablePrimaryKey{}.WithU32I64Str(8, 1, "abc"),
|
|
)
|
|
assert.NilError(t, err)
|
|
defer it.Close()
|
|
assertIteratorItems(it, 3, 4, 5, 6, 7, 8)
|
|
|
|
// now a reverse range query on a different index
|
|
strU32Index := table.GetIndex("str,u32")
|
|
assert.Assert(t, strU32Index != nil)
|
|
it, err = store.ListRange(ctx,
|
|
testpb.ExampleTableStrU32IndexKey{}.WithStr("abc"),
|
|
testpb.ExampleTableStrU32IndexKey{}.WithStr("abd"),
|
|
ormlist.Reverse(),
|
|
)
|
|
assertIteratorItems(it, 9, 3, 1, 8, 7, 2, 0)
|
|
|
|
// another prefix query forwards
|
|
|
|
it, err = store.List(ctx,
|
|
testpb.ExampleTableStrU32IndexKey{}.WithStrU32("abe", 7),
|
|
)
|
|
assertIteratorItems(it, 5, 6)
|
|
// and backwards
|
|
it, err = store.List(ctx,
|
|
testpb.ExampleTableStrU32IndexKey{}.WithStrU32("abc", 4),
|
|
ormlist.Reverse(),
|
|
)
|
|
assertIteratorItems(it, 2, 0)
|
|
|
|
// try filtering
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}, ormlist.Filter(func(message proto.Message) bool {
|
|
ex := message.(*testpb.ExampleTable)
|
|
return ex.U64 != 10
|
|
}))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 0, 1, 2, 3, 4, 6, 7, 8)
|
|
|
|
// try a cursor
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{})
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, it.Next())
|
|
assert.Assert(t, it.Next())
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}, ormlist.Cursor(it.Cursor()))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 2, 3, 4, 5, 6, 7, 8, 9)
|
|
|
|
// try an unique index
|
|
found, err := store.HasByU64Str(ctx, 12, "abc")
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, found)
|
|
a, err := store.GetByU64Str(ctx, 12, "abc")
|
|
assert.NilError(t, err)
|
|
assert.DeepEqual(t, data[8], a, protocmp.Transform())
|
|
|
|
// let's try paginating some stuff
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{
|
|
Limit: 4,
|
|
CountTotal: true,
|
|
}))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 0, 1, 2, 3)
|
|
res := it.PageResponse()
|
|
assert.Assert(t, res != nil)
|
|
assert.Equal(t, uint64(10), res.Total)
|
|
assert.Assert(t, res.NextKey != nil)
|
|
|
|
// let's use a default limit
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{},
|
|
ormlist.DefaultLimit(4),
|
|
ormlist.Paginate(&queryv1beta1.PageRequest{
|
|
CountTotal: true,
|
|
}))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 0, 1, 2, 3)
|
|
res = it.PageResponse()
|
|
assert.Assert(t, res != nil)
|
|
assert.Equal(t, uint64(10), res.Total)
|
|
assert.Assert(t, res.NextKey != nil)
|
|
|
|
// read another page
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{
|
|
Key: res.NextKey,
|
|
Limit: 4,
|
|
}))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 4, 5, 6, 7)
|
|
res = it.PageResponse()
|
|
assert.Assert(t, res != nil)
|
|
assert.Assert(t, res.NextKey != nil)
|
|
|
|
// and the last page
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{
|
|
Key: res.NextKey,
|
|
Limit: 4,
|
|
}))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 8, 9)
|
|
res = it.PageResponse()
|
|
assert.Assert(t, res != nil)
|
|
assert.Assert(t, res.NextKey == nil)
|
|
|
|
// let's go backwards
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{
|
|
Limit: 2,
|
|
CountTotal: true,
|
|
Reverse: true,
|
|
}))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 9, 8)
|
|
res = it.PageResponse()
|
|
assert.Assert(t, res != nil)
|
|
assert.Assert(t, res.NextKey != nil)
|
|
assert.Equal(t, uint64(10), res.Total)
|
|
|
|
// a bit more
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{
|
|
Key: res.NextKey,
|
|
Limit: 2,
|
|
Reverse: true,
|
|
}))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 7, 6)
|
|
res = it.PageResponse()
|
|
assert.Assert(t, res != nil)
|
|
assert.Assert(t, res.NextKey != nil)
|
|
|
|
// range query
|
|
it, err = store.ListRange(ctx,
|
|
testpb.ExampleTablePrimaryKey{}.WithU32I64Str(4, -1, "abc"),
|
|
testpb.ExampleTablePrimaryKey{}.WithU32I64Str(7, -2, "abe"),
|
|
ormlist.Paginate(&queryv1beta1.PageRequest{
|
|
Limit: 10,
|
|
}))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 2, 3, 4, 5)
|
|
res = it.PageResponse()
|
|
assert.Assert(t, res != nil)
|
|
assert.Assert(t, res.NextKey == nil)
|
|
|
|
// let's try an offset
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{
|
|
Limit: 2,
|
|
CountTotal: true,
|
|
Offset: 3,
|
|
}))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 3, 4)
|
|
res = it.PageResponse()
|
|
assert.Assert(t, res != nil)
|
|
assert.Assert(t, res.NextKey != nil)
|
|
assert.Equal(t, uint64(10), res.Total)
|
|
|
|
// and reverse
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{
|
|
Limit: 3,
|
|
CountTotal: true,
|
|
Offset: 5,
|
|
Reverse: true,
|
|
}))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 4, 3, 2)
|
|
res = it.PageResponse()
|
|
assert.Assert(t, res != nil)
|
|
assert.Assert(t, res.NextKey != nil)
|
|
assert.Equal(t, uint64(10), res.Total)
|
|
|
|
// now an offset that's slightly too big
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{}, ormlist.Paginate(&queryv1beta1.PageRequest{
|
|
Limit: 1,
|
|
CountTotal: true,
|
|
Offset: 10,
|
|
}))
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, !it.Next())
|
|
res = it.PageResponse()
|
|
assert.Assert(t, res != nil)
|
|
assert.Assert(t, res.NextKey == nil)
|
|
assert.Equal(t, uint64(10), res.Total)
|
|
|
|
// now let's update some things
|
|
for i := 0; i < 5; i++ {
|
|
data[i].U64 = data[i].U64 * 2
|
|
data[i].Bz = []byte(data[i].Str)
|
|
err = store.Update(ctx, data[i])
|
|
assert.NilError(t, err)
|
|
}
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{})
|
|
assert.NilError(t, err)
|
|
// we should still get everything in the same order
|
|
assertIteratorItems(it, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
|
|
|
|
// let's use SAVE_MODE_DEFAULT and add something
|
|
data = append(data, &testpb.ExampleTable{U32: 9})
|
|
err = store.Save(ctx, data[10])
|
|
assert.NilError(t, err)
|
|
a, err = store.Get(ctx, 9, 0, "")
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, a != nil)
|
|
assert.DeepEqual(t, data[10], a, protocmp.Transform())
|
|
// and update it
|
|
data[10].B = true
|
|
assert.NilError(t, table.Save(ctx, data[10]))
|
|
a, err = store.Get(ctx, 9, 0, "")
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, a != nil)
|
|
assert.DeepEqual(t, data[10], a, protocmp.Transform())
|
|
// and iterate
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{})
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
|
|
|
// let's export and import JSON and use a read-only backend
|
|
buf := &bytes.Buffer{}
|
|
readBackend := ormtable.NewReadBackend(ormtable.ReadBackendOptions{
|
|
CommitmentStoreReader: backend.CommitmentStoreReader(),
|
|
IndexStoreReader: backend.IndexStoreReader(),
|
|
})
|
|
assert.NilError(t, table.ExportJSON(ormtable.WrapContextDefault(readBackend), buf))
|
|
assert.NilError(t, table.ValidateJSON(bytes.NewReader(buf.Bytes())))
|
|
store2 := ormtable.WrapContextDefault(testkv.NewSplitMemBackend())
|
|
assert.NilError(t, table.ImportJSON(store2, bytes.NewReader(buf.Bytes())))
|
|
assertTablesEqual(t, table, ctx, store2)
|
|
|
|
// let's delete item 5
|
|
err = store.DeleteBy(ctx, testpb.ExampleTableU32I64StrIndexKey{}.WithU32I64Str(7, -2, "abe"))
|
|
assert.NilError(t, err)
|
|
// it should be gone
|
|
found, err = store.Has(ctx, 7, -2, "abe")
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, !found)
|
|
// and missing from the iterator
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{})
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 0, 1, 2, 3, 4, 6, 7, 8, 9, 10)
|
|
|
|
// let's do a batch delete
|
|
// first iterate over the items we'll delete to check that iterator
|
|
it, err = store.List(ctx, testpb.ExampleTableStrU32IndexKey{}.WithStr("abd"))
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 1, 3, 9)
|
|
// now delete them
|
|
assert.NilError(t, store.DeleteBy(ctx, testpb.ExampleTableStrU32IndexKey{}.WithStr("abd")))
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{})
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 0, 2, 4, 6, 7, 8, 10)
|
|
|
|
// Let's do a range delete
|
|
assert.NilError(t, store.DeleteRange(ctx,
|
|
testpb.ExampleTableStrU32IndexKey{}.WithStrU32("abc", 8),
|
|
testpb.ExampleTableStrU32IndexKey{}.WithStrU32("abe", 5),
|
|
))
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{})
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 0, 2, 6, 10)
|
|
|
|
// Let's delete something directly
|
|
assert.NilError(t, store.Delete(ctx, data[0]))
|
|
it, err = store.List(ctx, testpb.ExampleTablePrimaryKey{})
|
|
assert.NilError(t, err)
|
|
assertIteratorItems(it, 2, 6, 10)
|
|
|
|
}
|
|
|
|
func TestRandomTableData(t *testing.T) {
|
|
testTable(t, TableDataGen(testutil.GenA, 100).Example().(*TableData))
|
|
}
|
|
|
|
func testTable(t *testing.T, tableData *TableData) {
|
|
for _, index := range tableData.table.Indexes() {
|
|
indexModel := &IndexModel{
|
|
TableData: tableData,
|
|
index: index.(TestIndex),
|
|
}
|
|
sort.Sort(indexModel)
|
|
if _, ok := index.(ormtable.UniqueIndex); ok {
|
|
testUniqueIndex(t, indexModel)
|
|
}
|
|
testIndex(t, indexModel)
|
|
}
|
|
}
|
|
|
|
func testUniqueIndex(t *testing.T, model *IndexModel) {
|
|
index := model.index.(ormtable.UniqueIndex)
|
|
t.Logf("testing unique index %T %s", index, index.Fields())
|
|
for i := 0; i < len(model.data); i++ {
|
|
x := model.data[i]
|
|
ks, _, err := index.(ormkv.IndexCodec).EncodeKeyFromMessage(x.ProtoReflect())
|
|
assert.NilError(t, err)
|
|
|
|
values := protoValuesToInterfaces(ks)
|
|
|
|
found, err := index.Has(model.context, values...)
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, found)
|
|
|
|
msg := model.table.MessageType().New().Interface()
|
|
found, err = index.Get(model.context, msg, values...)
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, found)
|
|
assert.DeepEqual(t, x, msg, protocmp.Transform())
|
|
}
|
|
}
|
|
|
|
func testIndex(t *testing.T, model *IndexModel) {
|
|
index := model.index
|
|
if index.IsFullyOrdered() {
|
|
t.Logf("testing index %T %s", index, index.Fields())
|
|
|
|
it, err := model.index.List(model.context, nil)
|
|
assert.NilError(t, err)
|
|
checkIteratorAgainstSlice(t, it, model.data)
|
|
|
|
it, err = model.index.List(model.context, nil, ormlist.Reverse())
|
|
assert.NilError(t, err)
|
|
checkIteratorAgainstSlice(t, it, reverseData(model.data))
|
|
|
|
rapid.Check(t, func(t *rapid.T) {
|
|
i := rapid.IntRange(0, len(model.data)-2).Draw(t, "i").(int)
|
|
j := rapid.IntRange(i+1, len(model.data)-1).Draw(t, "j").(int)
|
|
|
|
start, _, err := model.index.(ormkv.IndexCodec).EncodeKeyFromMessage(model.data[i].ProtoReflect())
|
|
assert.NilError(t, err)
|
|
end, _, err := model.index.(ormkv.IndexCodec).EncodeKeyFromMessage(model.data[j].ProtoReflect())
|
|
assert.NilError(t, err)
|
|
|
|
startVals := protoValuesToInterfaces(start)
|
|
endVals := protoValuesToInterfaces(end)
|
|
|
|
it, err = model.index.ListRange(model.context, startVals, endVals)
|
|
assert.NilError(t, err)
|
|
checkIteratorAgainstSlice(t, it, model.data[i:j+1])
|
|
|
|
it, err = model.index.ListRange(model.context, startVals, endVals, ormlist.Reverse())
|
|
assert.NilError(t, err)
|
|
checkIteratorAgainstSlice(t, it, reverseData(model.data[i:j+1]))
|
|
})
|
|
} else {
|
|
t.Logf("testing unordered index %T %s", index, index.Fields())
|
|
|
|
// get all the data
|
|
it, err := model.index.List(model.context, nil)
|
|
assert.NilError(t, err)
|
|
var data2 []proto.Message
|
|
for it.Next() {
|
|
msg, err := it.GetMessage()
|
|
assert.NilError(t, err)
|
|
data2 = append(data2, msg)
|
|
}
|
|
assert.Equal(t, len(model.data), len(data2))
|
|
|
|
// sort it
|
|
model2 := &IndexModel{
|
|
TableData: &TableData{
|
|
table: model.table,
|
|
data: data2,
|
|
context: model.context,
|
|
},
|
|
index: model.index,
|
|
}
|
|
sort.Sort(model2)
|
|
|
|
// compare
|
|
for i := 0; i < len(data2); i++ {
|
|
assert.DeepEqual(t, model.data[i], data2[i], protocmp.Transform())
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func reverseData(data []proto.Message) []proto.Message {
|
|
n := len(data)
|
|
reverse := make([]proto.Message, n)
|
|
for i := 0; i < n; i++ {
|
|
reverse[n-i-1] = data[i]
|
|
}
|
|
return reverse
|
|
}
|
|
|
|
func checkIteratorAgainstSlice(t assert.TestingT, iterator ormtable.Iterator, data []proto.Message) {
|
|
i := 0
|
|
for iterator.Next() {
|
|
if i >= len(data) {
|
|
for iterator.Next() {
|
|
i++
|
|
}
|
|
t.Log(fmt.Sprintf("too many elements in iterator, len(data) = %d, i = %d", len(data), i))
|
|
t.FailNow()
|
|
}
|
|
msg, err := iterator.GetMessage()
|
|
assert.NilError(t, err)
|
|
assert.DeepEqual(t, data[i], msg, protocmp.Transform())
|
|
i++
|
|
}
|
|
}
|
|
|
|
func TableDataGen(elemGen *rapid.Generator, n int) *rapid.Generator {
|
|
return rapid.Custom(func(t *rapid.T) *TableData {
|
|
prefix := rapid.SliceOfN(rapid.Byte(), 0, 5).Draw(t, "prefix").([]byte)
|
|
message := elemGen.Draw(t, "message").(proto.Message)
|
|
table, err := ormtable.Build(ormtable.Options{
|
|
Prefix: prefix,
|
|
MessageType: message.ProtoReflect().Type(),
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
data := make([]proto.Message, n)
|
|
store := ormtable.WrapContextDefault(testkv.NewSplitMemBackend())
|
|
|
|
for i := 0; i < n; {
|
|
message = elemGen.Draw(t, fmt.Sprintf("message[%d]", i)).(proto.Message)
|
|
err := table.Insert(store, message)
|
|
if sdkerrors.IsOf(err, ormerrors.PrimaryKeyConstraintViolation, ormerrors.UniqueKeyViolation) {
|
|
continue
|
|
} else if err != nil {
|
|
panic(err)
|
|
}
|
|
data[i] = message
|
|
i++
|
|
}
|
|
|
|
return &TableData{
|
|
data: data,
|
|
table: table,
|
|
context: store,
|
|
}
|
|
})
|
|
}
|
|
|
|
type TableData struct {
|
|
table ormtable.Table
|
|
data []proto.Message
|
|
context context.Context
|
|
}
|
|
|
|
type IndexModel struct {
|
|
*TableData
|
|
index TestIndex
|
|
}
|
|
|
|
// TestIndex exposes methods that all index implementations expose publicly
|
|
// but on private structs because they are intended only to be used for testing.
|
|
type TestIndex interface {
|
|
ormtable.Index
|
|
|
|
// CompareKeys the two keys against the underlying IndexCodec, returning a
|
|
// negative value if key1 is less than key2, 0 if they are equal, and a
|
|
// positive value otherwise.
|
|
CompareKeys(key1, key2 []protoreflect.Value) int
|
|
|
|
// IsFullyOrdered returns true if all of the fields in the index are
|
|
// considered "well-ordered" in terms of sorted iteration.
|
|
IsFullyOrdered() bool
|
|
}
|
|
|
|
func (m *IndexModel) Len() int {
|
|
return len(m.data)
|
|
}
|
|
|
|
func (m *IndexModel) Less(i, j int) bool {
|
|
is, _, err := m.index.(ormkv.IndexCodec).EncodeKeyFromMessage(m.data[i].ProtoReflect())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
js, _, err := m.index.(ormkv.IndexCodec).EncodeKeyFromMessage(m.data[j].ProtoReflect())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return m.index.CompareKeys(is, js) < 0
|
|
}
|
|
|
|
func (m *IndexModel) Swap(i, j int) {
|
|
x := m.data[i]
|
|
m.data[i] = m.data[j]
|
|
m.data[j] = x
|
|
}
|
|
|
|
var _ sort.Interface = &IndexModel{}
|
|
|
|
func TestJSONExportImport(t *testing.T) {
|
|
table, err := ormtable.Build(ormtable.Options{
|
|
MessageType: (&testpb.ExampleTable{}).ProtoReflect().Type(),
|
|
})
|
|
assert.NilError(t, err)
|
|
store := ormtable.WrapContextDefault(testkv.NewSplitMemBackend())
|
|
|
|
for i := 0; i < 100; {
|
|
x := testutil.GenA.Example().(proto.Message)
|
|
err = table.Insert(store, x)
|
|
if sdkerrors.IsOf(err, ormerrors.PrimaryKeyConstraintViolation, ormerrors.UniqueKeyViolation) {
|
|
continue
|
|
} else {
|
|
assert.NilError(t, err)
|
|
}
|
|
i++
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
assert.NilError(t, table.ExportJSON(store, buf))
|
|
|
|
assert.NilError(t, table.ValidateJSON(bytes.NewReader(buf.Bytes())))
|
|
|
|
store2 := ormtable.WrapContextDefault(testkv.NewSplitMemBackend())
|
|
assert.NilError(t, table.ImportJSON(store2, bytes.NewReader(buf.Bytes())))
|
|
|
|
assertTablesEqual(t, table, store, store2)
|
|
}
|
|
|
|
func assertTablesEqual(t assert.TestingT, table ormtable.Table, ctx, ctx2 context.Context) {
|
|
it, err := table.List(ctx, nil)
|
|
assert.NilError(t, err)
|
|
it2, err := table.List(ctx2, nil)
|
|
assert.NilError(t, err)
|
|
|
|
for {
|
|
have := it.Next()
|
|
have2 := it2.Next()
|
|
assert.Equal(t, have, have2)
|
|
if !have {
|
|
break
|
|
}
|
|
|
|
msg1, err := it.GetMessage()
|
|
assert.NilError(t, err)
|
|
msg2, err := it.GetMessage()
|
|
assert.NilError(t, err)
|
|
|
|
assert.DeepEqual(t, msg1, msg2, protocmp.Transform())
|
|
}
|
|
}
|
|
|
|
func protoValuesToInterfaces(ks []protoreflect.Value) []interface{} {
|
|
values := make([]interface{}, len(ks))
|
|
for i := 0; i < len(ks); i++ {
|
|
values[i] = ks[i].Interface()
|
|
}
|
|
|
|
return values
|
|
}
|
|
|
|
func TestReadonly(t *testing.T) {
|
|
table, err := ormtable.Build(ormtable.Options{
|
|
MessageType: (&testpb.ExampleTable{}).ProtoReflect().Type(),
|
|
})
|
|
assert.NilError(t, err)
|
|
readBackend := ormtable.NewReadBackend(ormtable.ReadBackendOptions{
|
|
CommitmentStoreReader: dbm.NewMemDB(),
|
|
IndexStoreReader: dbm.NewMemDB(),
|
|
})
|
|
ctx := ormtable.WrapContextDefault(readBackend)
|
|
assert.ErrorIs(t, ormerrors.ReadOnly, table.Insert(ctx, &testpb.ExampleTable{}))
|
|
}
|