Skip to content

Commit 8b7706b

Browse files
fix ordering in list-action query (#102)
* fix ordering in list-action query * fix(action): address PR pagination review findings * refactor(action): dedupe reverse materialization in ListActions * fix(action): reject offset+key in reverse pagination
1 parent 25b926f commit 8b7706b

2 files changed

Lines changed: 459 additions & 34 deletions

File tree

x/action/v1/keeper/query_list_actions.go

Lines changed: 229 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ package keeper
22

33
import (
44
"context"
5+
"encoding/binary"
6+
"fmt"
7+
"sort"
8+
"strconv"
59

610
"github.com/LumeraProtocol/lumera/x/action/v1/types"
711
actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types"
@@ -14,6 +18,161 @@ import (
1418
"google.golang.org/grpc/status"
1519
)
1620

21+
// shouldUseNumericReverseOrdering returns true when reverse pagination should use
22+
// numeric action ID ordering instead of lexical store-key ordering.
23+
func shouldUseNumericReverseOrdering(pageReq *query.PageRequest) bool {
24+
return pageReq != nil && pageReq.Reverse
25+
}
26+
27+
// parseNumericActionID parses action IDs that are strictly base-10 uint64 values.
28+
func parseNumericActionID(actionID string) (uint64, bool) {
29+
parsed, err := strconv.ParseUint(actionID, 10, 64)
30+
if err != nil {
31+
return 0, false
32+
}
33+
return parsed, true
34+
}
35+
36+
// sortActionsByNumericID sorts actions by ActionID using numeric ordering when
37+
// possible, falling back to lexical ordering for non-numeric IDs.
38+
func sortActionsByNumericID(actions []*types.Action) {
39+
sort.SliceStable(actions, func(i, j int) bool {
40+
leftNumericID, leftIsNumeric := parseNumericActionID(actions[i].ActionID)
41+
rightNumericID, rightIsNumeric := parseNumericActionID(actions[j].ActionID)
42+
43+
switch {
44+
case leftIsNumeric && rightIsNumeric:
45+
if leftNumericID == rightNumericID {
46+
return actions[i].ActionID < actions[j].ActionID
47+
}
48+
return leftNumericID < rightNumericID
49+
case leftIsNumeric != rightIsNumeric:
50+
return leftIsNumeric
51+
default:
52+
return actions[i].ActionID < actions[j].ActionID
53+
}
54+
})
55+
}
56+
57+
// applyNumericReverseOrderingAndPaginate applies numeric ActionID ordering in
58+
// descending order and then paginates the resulting slice.
59+
func applyNumericReverseOrderingAndPaginate(actions []*types.Action, pageReq *query.PageRequest) ([]*types.Action, *query.PageResponse, error) {
60+
sortActionsByNumericID(actions)
61+
for i, j := 0, len(actions)-1; i < j; i, j = i+1, j-1 {
62+
actions[i], actions[j] = actions[j], actions[i]
63+
}
64+
65+
return paginateActionSlice(actions, pageReq)
66+
}
67+
68+
// collectActionsFromIDIndexStore loads actions by ID from an index store whose keys
69+
// are action IDs. Stale index entries are ignored.
70+
func (q queryServer) collectActionsFromIDIndexStore(
71+
ctx sdk.Context,
72+
indexStore prefix.Store,
73+
actionTypeFilter types.ActionType,
74+
) ([]*types.Action, error) {
75+
actions := make([]*types.Action, 0)
76+
iter := indexStore.Iterator(nil, nil)
77+
defer iter.Close()
78+
79+
for ; iter.Valid(); iter.Next() {
80+
actionID := string(iter.Key())
81+
act, found := q.k.GetActionByID(ctx, actionID)
82+
if !found {
83+
continue
84+
}
85+
if actionTypeFilter != types.ActionTypeUnspecified && act.ActionType != actiontypes.ActionType(actionTypeFilter) {
86+
continue
87+
}
88+
89+
actions = append(actions, act)
90+
}
91+
92+
return actions, nil
93+
}
94+
95+
// collectActionsFromPrimaryStore loads all actions from the primary action store.
96+
func (q queryServer) collectActionsFromPrimaryStore(actionStore prefix.Store) ([]*types.Action, error) {
97+
actions := make([]*types.Action, 0)
98+
iter := actionStore.Iterator(nil, nil)
99+
defer iter.Close()
100+
101+
for ; iter.Valid(); iter.Next() {
102+
var act actiontypes.Action
103+
if unmarshalErr := q.k.cdc.Unmarshal(iter.Value(), &act); unmarshalErr != nil {
104+
return nil, status.Errorf(codes.Internal, "failed to unmarshal action: %v", unmarshalErr)
105+
}
106+
actions = append(actions, &act)
107+
}
108+
109+
return actions, nil
110+
}
111+
112+
// decodeActionPaginationOffset decodes an opaque pagination key into an offset.
113+
func decodeActionPaginationOffset(key []byte) (uint64, error) {
114+
if len(key) != 8 {
115+
return 0, fmt.Errorf("invalid key length %d", len(key))
116+
}
117+
return binary.BigEndian.Uint64(key), nil
118+
}
119+
120+
// encodeActionPaginationOffset encodes an offset as an opaque pagination key.
121+
func encodeActionPaginationOffset(offset uint64) []byte {
122+
key := make([]byte, 8)
123+
binary.BigEndian.PutUint64(key, offset)
124+
return key
125+
}
126+
127+
// paginateActionSlice paginates an already materialized action slice and returns
128+
// a PageResponse compatible with cursor- and offset-based pagination.
129+
func paginateActionSlice(actions []*types.Action, pageReq *query.PageRequest) ([]*types.Action, *query.PageResponse, error) {
130+
if pageReq == nil {
131+
return actions, &query.PageResponse{}, nil
132+
}
133+
if pageReq.Offset > 0 && pageReq.Key != nil {
134+
return nil, nil, status.Error(codes.InvalidArgument, "paginate: invalid request, either offset or key is expected, got both")
135+
}
136+
137+
total := uint64(len(actions))
138+
offset := pageReq.Offset
139+
140+
if len(pageReq.Key) > 0 {
141+
decodedOffset, err := decodeActionPaginationOffset(pageReq.Key)
142+
if err != nil {
143+
return nil, nil, status.Error(codes.InvalidArgument, "invalid pagination key")
144+
}
145+
offset = decodedOffset
146+
}
147+
148+
if offset > total {
149+
offset = total
150+
}
151+
152+
limit := pageReq.Limit
153+
if limit == 0 {
154+
limit = query.DefaultLimit
155+
}
156+
157+
remaining := total - offset
158+
if limit > remaining {
159+
limit = remaining
160+
}
161+
162+
end := offset + limit
163+
page := actions[int(offset):int(end)]
164+
165+
pageRes := &query.PageResponse{}
166+
if pageReq.CountTotal {
167+
pageRes.Total = total
168+
}
169+
if end < total {
170+
pageRes.NextKey = encodeActionPaginationOffset(end)
171+
}
172+
173+
return page, pageRes, nil
174+
}
175+
17176
// ListActions returns a list of actions, optionally filtered by type and state
18177
func (q queryServer) ListActions(goCtx context.Context, req *types.QueryListActionsRequest) (*types.QueryListActionsResponse, error) {
19178
if req == nil {
@@ -37,65 +196,101 @@ func (q queryServer) ListActions(goCtx context.Context, req *types.QueryListActi
37196
statePrefix := []byte(ActionByStatePrefix + types.ActionState(req.ActionState).String() + "/")
38197
indexStore := prefix.NewStore(storeAdapter, statePrefix)
39198

40-
onResult := func(key, _ []byte, accumulate bool) (bool, error) {
41-
actionID := string(key)
42-
act, found := q.k.GetActionByID(ctx, actionID)
43-
if !found {
44-
// Stale index entry; skip without counting
45-
return false, nil
199+
if shouldUseNumericReverseOrdering(req.Pagination) {
200+
// Numeric reverse ordering cannot be derived from lexical KV iteration, so
201+
// we materialize the matched set, sort it numerically, then paginate.
202+
actions, err = q.collectActionsFromIDIndexStore(ctx, indexStore, req.ActionType)
203+
if err != nil {
204+
return nil, err
46205
}
47206

48-
if req.ActionType != types.ActionTypeUnspecified && act.ActionType != actiontypes.ActionType(req.ActionType) {
49-
return false, nil
50-
}
207+
actions, pageRes, err = applyNumericReverseOrderingAndPaginate(actions, req.Pagination)
208+
} else {
209+
onResult := func(key, _ []byte, accumulate bool) (bool, error) {
210+
actionID := string(key)
211+
act, found := q.k.GetActionByID(ctx, actionID)
212+
if !found {
213+
// Stale index entry; skip without counting
214+
return false, nil
215+
}
216+
217+
if req.ActionType != types.ActionTypeUnspecified && act.ActionType != actiontypes.ActionType(req.ActionType) {
218+
return false, nil
219+
}
220+
221+
if accumulate {
222+
actions = append(actions, act)
223+
}
51224

52-
if accumulate {
53-
actions = append(actions, act)
225+
return true, nil
54226
}
55227

56-
return true, nil
228+
pageRes, err = query.FilteredPaginate(indexStore, req.Pagination, onResult)
57229
}
58-
59-
pageRes, err = query.FilteredPaginate(indexStore, req.Pagination, onResult)
60230
} else if useTypeIndex {
61231
// When filtering only by type, use the type index
62232
typePrefix := []byte(ActionByTypePrefix + types.ActionType(req.ActionType).String() + "/")
63233
indexStore := prefix.NewStore(storeAdapter, typePrefix)
64234

65-
onResult := func(key, _ []byte, accumulate bool) (bool, error) {
66-
actionID := string(key)
67-
act, found := q.k.GetActionByID(ctx, actionID)
68-
if !found {
69-
// Stale index entry; skip
70-
return false, nil
235+
if shouldUseNumericReverseOrdering(req.Pagination) {
236+
// Numeric reverse ordering cannot be derived from lexical KV iteration, so
237+
// we materialize the matched set, sort it numerically, then paginate.
238+
actions, err = q.collectActionsFromIDIndexStore(ctx, indexStore, types.ActionTypeUnspecified)
239+
if err != nil {
240+
return nil, err
71241
}
72242

73-
if accumulate {
74-
actions = append(actions, act)
243+
actions, pageRes, err = applyNumericReverseOrderingAndPaginate(actions, req.Pagination)
244+
} else {
245+
onResult := func(key, _ []byte, accumulate bool) (bool, error) {
246+
actionID := string(key)
247+
act, found := q.k.GetActionByID(ctx, actionID)
248+
if !found {
249+
// Stale index entry; skip
250+
return false, nil
251+
}
252+
253+
if accumulate {
254+
actions = append(actions, act)
255+
}
256+
257+
return true, nil
75258
}
76259

77-
return true, nil
260+
pageRes, err = query.FilteredPaginate(indexStore, req.Pagination, onResult)
78261
}
79-
80-
pageRes, err = query.FilteredPaginate(indexStore, req.Pagination, onResult)
81262
} else {
82263
actionStore := prefix.NewStore(storeAdapter, []byte(ActionKeyPrefix))
83264

84-
onResult := func(key, value []byte, accumulate bool) (bool, error) {
85-
var act actiontypes.Action
86-
if err := q.k.cdc.Unmarshal(value, &act); err != nil {
87-
return false, err
265+
if shouldUseNumericReverseOrdering(req.Pagination) {
266+
// Numeric reverse ordering cannot be derived from lexical KV iteration, so
267+
// we materialize the matched set, sort it numerically, then paginate.
268+
actions, err = q.collectActionsFromPrimaryStore(actionStore)
269+
if err != nil {
270+
return nil, err
88271
}
89272

90-
if accumulate {
91-
actions = append(actions, &act)
92-
}
273+
actions, pageRes, err = applyNumericReverseOrderingAndPaginate(actions, req.Pagination)
274+
} else {
275+
onResult := func(key, value []byte, accumulate bool) (bool, error) {
276+
var act actiontypes.Action
277+
if err := q.k.cdc.Unmarshal(value, &act); err != nil {
278+
return false, err
279+
}
280+
281+
if accumulate {
282+
actions = append(actions, &act)
283+
}
93284

94-
return true, nil
285+
return true, nil
286+
}
287+
pageRes, err = query.FilteredPaginate(actionStore, req.Pagination, onResult)
95288
}
96-
pageRes, err = query.FilteredPaginate(actionStore, req.Pagination, onResult)
97289
}
98290
if err != nil {
291+
if st, ok := status.FromError(err); ok && st.Code() != codes.Unknown {
292+
return nil, err
293+
}
99294
return nil, status.Errorf(codes.Internal, "failed to paginate actions: %v", err)
100295
}
101296

0 commit comments

Comments
 (0)