package orm import ( "testing" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/types/query" "github.com/cosmos/cosmos-sdk/x/group/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var _ Indexable = &nilRowGetterBuilder{} type nilRowGetterBuilder struct{} func (b *nilRowGetterBuilder) RowGetter() RowGetter { return nil } func (b *nilRowGetterBuilder) AddAfterSetInterceptor(AfterSetInterceptor) {} func (b *nilRowGetterBuilder) AddAfterDeleteInterceptor(AfterDeleteInterceptor) {} func TestNewIndex(t *testing.T) { interfaceRegistry := types.NewInterfaceRegistry() cdc := codec.NewProtoCodec(interfaceRegistry) myTable, err := NewAutoUInt64Table(AutoUInt64TablePrefix, AutoUInt64TableSeqPrefix, &testdata.TableModel{}, cdc) require.NoError(t, err) indexer := func(val interface{}) ([]interface{}, error) { return []interface{}{val.(*testdata.TableModel).Metadata}, nil } testCases := []struct { name string table Indexable expectErr bool expectedErr string indexKey interface{} }{ { name: "nil indexKey", table: myTable, expectErr: true, expectedErr: "indexKey must not be nil", indexKey: nil, }, { name: "nil rowGetter", table: &nilRowGetterBuilder{}, expectErr: true, expectedErr: "rowGetter must not be nil", indexKey: []byte{}, }, { name: "all not nil", table: myTable, expectErr: false, indexKey: []byte{}, }, { name: "index key type not allowed", table: myTable, expectErr: true, indexKey: 1, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { index, err := NewIndex(tc.table, AutoUInt64TableSeqPrefix, indexer, tc.indexKey) if tc.expectErr { require.Error(t, err) require.Contains(t, err.Error(), tc.expectedErr) } else { require.NoError(t, err) require.NotEmpty(t, index) } }) } } func TestIndexPrefixScan(t *testing.T) { interfaceRegistry := types.NewInterfaceRegistry() cdc := codec.NewProtoCodec(interfaceRegistry) tb, err := NewAutoUInt64Table(AutoUInt64TablePrefix, AutoUInt64TableSeqPrefix, &testdata.TableModel{}, cdc) require.NoError(t, err) idx, err := NewIndex(tb, AutoUInt64TableModelByMetadataPrefix, func(val interface{}) ([]interface{}, error) { i := []interface{}{val.(*testdata.TableModel).Metadata} return i, nil }, testdata.TableModel{}.Metadata) require.NoError(t, err) strIdx, err := NewIndex(tb, 0x1, func(val interface{}) ([]interface{}, error) { i := []interface{}{val.(*testdata.TableModel).Name} return i, nil }, testdata.TableModel{}.Name) require.NoError(t, err) ctx := NewMockContext() store := ctx.KVStore(sdk.NewKVStoreKey("test")) g1 := testdata.TableModel{ Id: 1, Name: "my test 1", Metadata: []byte("metadata-a"), } g2 := testdata.TableModel{ Id: 2, Name: "my test 2", Metadata: []byte("metadata-b"), } g3 := testdata.TableModel{ Id: 3, Name: "my test 3", Metadata: []byte("metadata-b"), } for _, g := range []testdata.TableModel{g1, g2, g3} { _, err := tb.Create(store, &g) require.NoError(t, err) } specs := map[string]struct { start, end interface{} expResult []testdata.TableModel expRowIDs []RowID expError *sdkerrors.Error method func(store sdk.KVStore, start, end interface{}) (Iterator, error) }{ "exact match with a single result": { start: []byte("metadata-a"), end: []byte("metadata-b"), method: idx.PrefixScan, expResult: []testdata.TableModel{g1}, expRowIDs: []RowID{EncodeSequence(1)}, }, "one result by prefix": { start: []byte("metadata"), end: []byte("metadata-b"), method: idx.PrefixScan, expResult: []testdata.TableModel{g1}, expRowIDs: []RowID{EncodeSequence(1)}, }, "multi key elements by exact match": { start: []byte("metadata-b"), end: []byte("metadata-c"), method: idx.PrefixScan, expResult: []testdata.TableModel{g2, g3}, expRowIDs: []RowID{EncodeSequence(2), EncodeSequence(3)}, }, "open end query": { start: []byte("metadata-b"), end: nil, method: idx.PrefixScan, expResult: []testdata.TableModel{g2, g3}, expRowIDs: []RowID{EncodeSequence(2), EncodeSequence(3)}, }, "open start query": { start: nil, end: []byte("metadata-b"), method: idx.PrefixScan, expResult: []testdata.TableModel{g1}, expRowIDs: []RowID{EncodeSequence(1)}, }, "open start and end query": { start: nil, end: nil, method: idx.PrefixScan, expResult: []testdata.TableModel{g1, g2, g3}, expRowIDs: []RowID{EncodeSequence(1), EncodeSequence(2), EncodeSequence(3)}, }, "all matching prefix": { start: []byte("admin"), end: nil, method: idx.PrefixScan, expResult: []testdata.TableModel{g1, g2, g3}, expRowIDs: []RowID{EncodeSequence(1), EncodeSequence(2), EncodeSequence(3)}, }, "non matching prefix": { start: []byte("metadata-c"), end: nil, method: idx.PrefixScan, expResult: []testdata.TableModel{}, }, "start equals end": { start: []byte("any"), end: []byte("any"), method: idx.PrefixScan, expError: errors.ErrORMInvalidArgument, }, "start after end": { start: []byte("b"), end: []byte("a"), method: idx.PrefixScan, expError: errors.ErrORMInvalidArgument, }, "reverse: exact match with a single result": { start: []byte("metadata-a"), end: []byte("metadata-b"), method: idx.ReversePrefixScan, expResult: []testdata.TableModel{g1}, expRowIDs: []RowID{EncodeSequence(1)}, }, "reverse: one result by prefix": { start: []byte("metadata"), end: []byte("metadata-b"), method: idx.ReversePrefixScan, expResult: []testdata.TableModel{g1}, expRowIDs: []RowID{EncodeSequence(1)}, }, "reverse: multi key elements by exact match": { start: []byte("metadata-b"), end: []byte("metadata-c"), method: idx.ReversePrefixScan, expResult: []testdata.TableModel{g3, g2}, expRowIDs: []RowID{EncodeSequence(3), EncodeSequence(2)}, }, "reverse: open end query": { start: []byte("metadata-b"), end: nil, method: idx.ReversePrefixScan, expResult: []testdata.TableModel{g3, g2}, expRowIDs: []RowID{EncodeSequence(3), EncodeSequence(2)}, }, "reverse: open start query": { start: nil, end: []byte("metadata-b"), method: idx.ReversePrefixScan, expResult: []testdata.TableModel{g1}, expRowIDs: []RowID{EncodeSequence(1)}, }, "reverse: open start and end query": { start: nil, end: nil, method: idx.ReversePrefixScan, expResult: []testdata.TableModel{g3, g2, g1}, expRowIDs: []RowID{EncodeSequence(3), EncodeSequence(2), EncodeSequence(1)}, }, "reverse: all matching prefix": { start: []byte("admin"), end: nil, method: idx.ReversePrefixScan, expResult: []testdata.TableModel{g3, g2, g1}, expRowIDs: []RowID{EncodeSequence(3), EncodeSequence(2), EncodeSequence(1)}, }, "reverse: non matching prefix": { start: []byte("metadata-c"), end: nil, method: idx.ReversePrefixScan, expResult: []testdata.TableModel{}, }, "reverse: start equals end": { start: []byte("any"), end: []byte("any"), method: idx.ReversePrefixScan, expError: errors.ErrORMInvalidArgument, }, "reverse: start after end": { start: []byte("b"), end: []byte("a"), method: idx.ReversePrefixScan, expError: errors.ErrORMInvalidArgument, }, "exact match with a single result using string based index": { start: "my test 1", end: "my test 2", method: strIdx.PrefixScan, expResult: []testdata.TableModel{g1}, expRowIDs: []RowID{EncodeSequence(1)}, }, } for msg, spec := range specs { t.Run(msg, func(t *testing.T) { it, err := spec.method(store, spec.start, spec.end) require.True(t, spec.expError.Is(err), "expected #+v but got #+v", spec.expError, err) if spec.expError != nil { return } var loaded []testdata.TableModel rowIDs, err := ReadAll(it, &loaded) require.NoError(t, err) assert.Equal(t, spec.expResult, loaded) assert.Equal(t, spec.expRowIDs, rowIDs) }) } } func TestUniqueIndex(t *testing.T) { interfaceRegistry := types.NewInterfaceRegistry() cdc := codec.NewProtoCodec(interfaceRegistry) myTable, err := NewPrimaryKeyTable(PrimaryKeyTablePrefix, &testdata.TableModel{}, cdc) require.NoError(t, err) uniqueIdx, err := NewUniqueIndex(myTable, 0x10, func(val interface{}) (interface{}, error) { return []byte{val.(*testdata.TableModel).Metadata[0]}, nil }, []byte{}) require.NoError(t, err) ctx := NewMockContext() store := ctx.KVStore(sdk.NewKVStoreKey("test")) m := testdata.TableModel{ Id: 1, Name: "my test", Metadata: []byte("metadata"), } err = myTable.Create(store, &m) require.NoError(t, err) indexedKey := []byte{'m'} // Has exists, err := uniqueIdx.Has(store, indexedKey) require.NoError(t, err) assert.True(t, exists) // Get it, err := uniqueIdx.Get(store, indexedKey) require.NoError(t, err) var loaded testdata.TableModel rowID, err := it.LoadNext(&loaded) require.NoError(t, err) require.Equal(t, RowID(PrimaryKey(&m)), rowID) require.Equal(t, m, loaded) // GetPaginated cases := map[string]struct { pageReq *query.PageRequest expErr bool }{ "nil key": { pageReq: &query.PageRequest{Key: nil}, expErr: false, }, "after indexed key": { pageReq: &query.PageRequest{Key: indexedKey}, expErr: true, }, } for testName, tc := range cases { t.Run(testName, func(t *testing.T) { it, err := uniqueIdx.GetPaginated(store, indexedKey, tc.pageReq) require.NoError(t, err) rowID, err := it.LoadNext(&loaded) if tc.expErr { // iterator done require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, RowID(PrimaryKey(&m)), rowID) require.Equal(t, m, loaded) } }) } // PrefixScan match it, err = uniqueIdx.PrefixScan(store, indexedKey, nil) require.NoError(t, err) rowID, err = it.LoadNext(&loaded) require.NoError(t, err) require.Equal(t, RowID(PrimaryKey(&m)), rowID) require.Equal(t, m, loaded) // PrefixScan no match it, err = uniqueIdx.PrefixScan(store, []byte{byte('n')}, nil) require.NoError(t, err) _, err = it.LoadNext(&loaded) require.Error(t, errors.ErrORMIteratorDone, err) // ReversePrefixScan match it, err = uniqueIdx.ReversePrefixScan(store, indexedKey, nil) require.NoError(t, err) rowID, err = it.LoadNext(&loaded) require.NoError(t, err) require.Equal(t, RowID(PrimaryKey(&m)), rowID) require.Equal(t, m, loaded) // ReversePrefixScan no match it, err = uniqueIdx.ReversePrefixScan(store, []byte{byte('l')}, nil) require.NoError(t, err) _, err = it.LoadNext(&loaded) require.Error(t, errors.ErrORMIteratorDone, err) // create with same index key should fail new := testdata.TableModel{ Id: 1, Name: "my test", Metadata: []byte("my-metadata"), } err = myTable.Create(store, &new) require.Error(t, errors.ErrORMUniqueConstraint, err) // and when delete err = myTable.Delete(store, &m) require.NoError(t, err) // then no persistent element exists, err = uniqueIdx.Has(store, indexedKey) require.NoError(t, err) assert.False(t, exists) } func TestPrefixRange(t *testing.T) { cases := map[string]struct { src []byte expStart []byte expEnd []byte expPanic bool }{ "normal": {src: []byte{1, 3, 4}, expStart: []byte{1, 3, 4}, expEnd: []byte{1, 3, 5}}, "normal short": {src: []byte{79}, expStart: []byte{79}, expEnd: []byte{80}}, "empty case": {src: []byte{}}, "roll-over example 1": {src: []byte{17, 28, 255}, expStart: []byte{17, 28, 255}, expEnd: []byte{17, 29, 0}}, "roll-over example 2": {src: []byte{15, 42, 255, 255}, expStart: []byte{15, 42, 255, 255}, expEnd: []byte{15, 43, 0, 0}}, "pathological roll-over": {src: []byte{255, 255, 255, 255}, expStart: []byte{255, 255, 255, 255}}, "nil prohibited": {expPanic: true}, } for testName, tc := range cases { t.Run(testName, func(t *testing.T) { if tc.expPanic { require.Panics(t, func() { PrefixRange(tc.src) }) return } start, end := PrefixRange(tc.src) assert.Equal(t, tc.expStart, start) assert.Equal(t, tc.expEnd, end) }) } }