Skip to content

Commit 6c81e0e

Browse files
aleem1314zakir
authored and
zakir
committed
add reverse iteration to pagination (cosmos#8875)
* WIP * add tests * add tests * fix lint * fix pagination * add proto message doc * fix filtered_pagination * fix test * cleanup * add reverse flag to pagination * changelog * Update client/flags/flags.go * Update CHANGELOG.md Co-authored-by: Alessio Treglia <[email protected]> Co-authored-by: Federico Kunze <[email protected]> (cherry picked from commit a78f777)
1 parent 1925123 commit 6c81e0e

File tree

8 files changed

+297
-23
lines changed

8 files changed

+297
-23
lines changed

client/flags/flags.go

+2
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const (
7070
FlagCountTotal = "count-total"
7171
FlagTimeoutHeight = "timeout-height"
7272
FlagKeyAlgorithm = "algo"
73+
FlagReverse = "reverse"
7374

7475
// Tendermint logging flags
7576
FlagLogLevel = "log_level"
@@ -124,6 +125,7 @@ func AddPaginationFlagsToCmd(cmd *cobra.Command, query string) {
124125
cmd.Flags().Uint64(FlagOffset, 0, fmt.Sprintf("pagination offset of %s to query for", query))
125126
cmd.Flags().Uint64(FlagLimit, 100, fmt.Sprintf("pagination limit of %s to query for", query))
126127
cmd.Flags().Bool(FlagCountTotal, false, fmt.Sprintf("count total number of records in %s to query for", query))
128+
cmd.Flags().Bool(FlagReverse, false, "results are sorted in descending order")
127129
}
128130

129131
// GasSetting encapsulates the possible values passed through the --gas flag.

client/utils.go

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func ReadPageRequest(flagSet *pflag.FlagSet) (*query.PageRequest, error) {
5252
limit, _ := flagSet.GetUint64(flags.FlagLimit)
5353
countTotal, _ := flagSet.GetBool(flags.FlagCountTotal)
5454
page, _ := flagSet.GetUint64(flags.FlagPage)
55+
reverse, _ := flagSet.GetBool(flags.FlagReverse)
5556

5657
if page > 1 && offset > 0 {
5758
return nil, sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "page and offset cannot be used together")
@@ -66,6 +67,7 @@ func ReadPageRequest(flagSet *pflag.FlagSet) (*query.PageRequest, error) {
6667
Offset: offset,
6768
Limit: limit,
6869
CountTotal: countTotal,
70+
Reverse: reverse,
6971
}, nil
7072
}
7173

proto/cosmos/base/query/v1beta1/pagination.proto

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ message PageRequest {
3030
// count_total is only respected when offset is used. It is ignored when key
3131
// is set.
3232
bool count_total = 4;
33+
34+
// reverse is set to true indicates that, results to be returned in the descending order.
35+
bool reverse = 5;
3336
}
3437

3538
// PageResponse is to be embedded in gRPC response messages where the

types/query/filtered_pagination.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func FilteredPaginate(
2929
key := pageRequest.Key
3030
limit := pageRequest.Limit
3131
countTotal := pageRequest.CountTotal
32+
reverse := pageRequest.Reverse
3233

3334
if offset > 0 && key != nil {
3435
return nil, fmt.Errorf("invalid request, either offset or key is expected, got both")
@@ -42,7 +43,7 @@ func FilteredPaginate(
4243
}
4344

4445
if len(key) != 0 {
45-
iterator := prefixStore.Iterator(key, nil)
46+
iterator := getIterator(prefixStore, key, reverse)
4647
defer iterator.Close()
4748

4849
var numHits uint64
@@ -73,7 +74,7 @@ func FilteredPaginate(
7374
}, nil
7475
}
7576

76-
iterator := prefixStore.Iterator(nil, nil)
77+
iterator := getIterator(prefixStore, nil, reverse)
7778
defer iterator.Close()
7879

7980
end := offset + limit

types/query/filtered_pagination_test.go

+81-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package query_test
22

33
import (
44
"fmt"
5-
65
"github.com/cosmos/cosmos-sdk/codec"
76
"github.com/cosmos/cosmos-sdk/store/prefix"
87
sdk "github.com/cosmos/cosmos-sdk/types"
@@ -87,6 +86,87 @@ func (s *paginationTestSuite) TestFilteredPaginations() {
8786
s.Require().LessOrEqual(len(balances), 2)
8887
}
8988

89+
func (s *paginationTestSuite) TestReverseFilteredPaginations() {
90+
app, ctx, appCodec := setupTest()
91+
92+
var balances sdk.Coins
93+
for i := 0; i < numBalances; i++ {
94+
denom := fmt.Sprintf("foo%ddenom", i)
95+
balances = append(balances, sdk.NewInt64Coin(denom, 100))
96+
}
97+
98+
for i := 0; i < 10; i++ {
99+
denom := fmt.Sprintf("test%ddenom", i)
100+
balances = append(balances, sdk.NewInt64Coin(denom, 250))
101+
}
102+
103+
balances = balances.Sort()
104+
addr1 := sdk.AccAddress([]byte("addr1"))
105+
acc1 := app.AccountKeeper.NewAccountWithAddress(ctx, addr1)
106+
app.AccountKeeper.SetAccount(ctx, acc1)
107+
s.Require().NoError(app.BankKeeper.SetBalances(ctx, addr1, balances))
108+
store := ctx.KVStore(app.GetKey(authtypes.StoreKey))
109+
110+
// verify pagination with limit > total values
111+
pageReq := &query.PageRequest{Key: nil, Limit: 5, CountTotal: true, Reverse: true}
112+
balns, res, err := execFilterPaginate(store, pageReq, appCodec)
113+
s.Require().NoError(err)
114+
s.Require().NotNil(res)
115+
s.Require().Equal(5, len(balns))
116+
117+
s.T().Log("verify empty request")
118+
balns, res, err = execFilterPaginate(store, nil, appCodec)
119+
s.Require().NoError(err)
120+
s.Require().NotNil(res)
121+
s.Require().Equal(10, len(balns))
122+
s.Require().Equal(uint64(10), res.Total)
123+
s.Require().Nil(res.NextKey)
124+
125+
s.T().Log("verify default limit")
126+
pageReq = &query.PageRequest{Reverse: true}
127+
balns, res, err = execFilterPaginate(store, pageReq, appCodec)
128+
s.Require().NoError(err)
129+
s.Require().NotNil(res)
130+
s.Require().Equal(10, len(balns))
131+
s.Require().Equal(uint64(10), res.Total)
132+
133+
s.T().Log("verify nextKey is returned if there are more results")
134+
pageReq = &query.PageRequest{Limit: 2, CountTotal: true, Reverse: true}
135+
balns, res, err = execFilterPaginate(store, pageReq, appCodec)
136+
s.Require().NoError(err)
137+
s.Require().NotNil(res)
138+
s.Require().Equal(2, len(balns))
139+
s.Require().NotNil(res.NextKey)
140+
s.Require().Equal(string(res.NextKey), fmt.Sprintf("test7denom"))
141+
s.Require().Equal(uint64(10), res.Total)
142+
143+
s.T().Log("verify both key and offset can't be given")
144+
pageReq = &query.PageRequest{Key: res.NextKey, Limit: 1, Offset: 2, Reverse: true}
145+
_, _, err = execFilterPaginate(store, pageReq, appCodec)
146+
s.Require().Error(err)
147+
148+
s.T().Log("use nextKey for query and reverse true")
149+
pageReq = &query.PageRequest{Key: res.NextKey, Limit: 2, Reverse: true}
150+
balns, res, err = execFilterPaginate(store, pageReq, appCodec)
151+
s.Require().NoError(err)
152+
s.Require().NotNil(res)
153+
s.Require().Equal(2, len(balns))
154+
s.Require().NotNil(res.NextKey)
155+
s.Require().Equal(string(res.NextKey), fmt.Sprintf("test5denom"))
156+
157+
s.T().Log("verify last page records, nextKey for query and reverse true")
158+
pageReq = &query.PageRequest{Key: res.NextKey, Reverse: true}
159+
balns, res, err = execFilterPaginate(store, pageReq, appCodec)
160+
s.Require().NoError(err)
161+
s.Require().NotNil(res)
162+
s.Require().Equal(6, len(balns))
163+
s.Require().Nil(res.NextKey)
164+
165+
s.T().Log("verify Reverse pagination returns valid result")
166+
s.Require().Equal(balances[235:241].String(), balns.Sort().String())
167+
168+
}
169+
90170
func ExampleFilteredPaginate() {
91171
app, ctx, appCodec := setupTest()
92172

types/query/pagination.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"google.golang.org/grpc/status"
88

99
"github.com/cosmos/cosmos-sdk/store/types"
10+
db "github.com/tendermint/tm-db"
1011
)
1112

1213
// DefaultLimit is the default `limit` for queries
@@ -54,6 +55,7 @@ func Paginate(
5455
key := pageRequest.Key
5556
limit := pageRequest.Limit
5657
countTotal := pageRequest.CountTotal
58+
reverse := pageRequest.Reverse
5759

5860
if offset > 0 && key != nil {
5961
return nil, fmt.Errorf("invalid request, either offset or key is expected, got both")
@@ -67,13 +69,14 @@ func Paginate(
6769
}
6870

6971
if len(key) != 0 {
70-
iterator := prefixStore.Iterator(key, nil)
72+
iterator := getIterator(prefixStore, key, reverse)
7173
defer iterator.Close()
7274

7375
var count uint64
7476
var nextKey []byte
7577

7678
for ; iterator.Valid(); iterator.Next() {
79+
7780
if count == limit {
7881
nextKey = iterator.Key()
7982
break
@@ -94,7 +97,7 @@ func Paginate(
9497
}, nil
9598
}
9699

97-
iterator := prefixStore.Iterator(nil, nil)
100+
iterator := getIterator(prefixStore, nil, reverse)
98101
defer iterator.Close()
99102

100103
end := offset + limit
@@ -132,3 +135,19 @@ func Paginate(
132135

133136
return res, nil
134137
}
138+
139+
func getIterator(prefixStore types.KVStore, start []byte, reverse bool) db.Iterator {
140+
if reverse {
141+
var end []byte
142+
if start != nil {
143+
itr := prefixStore.Iterator(start, nil)
144+
defer itr.Close()
145+
if itr.Valid() {
146+
itr.Next()
147+
end = itr.Key()
148+
}
149+
}
150+
return prefixStore.ReverseIterator(nil, end)
151+
}
152+
return prefixStore.Iterator(start, nil)
153+
}

types/query/pagination.pb.go

+61-18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)