-
Notifications
You must be signed in to change notification settings - Fork 1
fix ordering in list-action query #102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,8 @@ package keeper | |
|
|
||
| import ( | ||
| "context" | ||
| "sort" | ||
| "strconv" | ||
|
|
||
| "github.com/LumeraProtocol/lumera/x/action/v1/types" | ||
| actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" | ||
|
|
@@ -14,6 +16,64 @@ import ( | |
| "google.golang.org/grpc/status" | ||
| ) | ||
|
|
||
| func shouldUseNumericReverseOrdering(pageReq *query.PageRequest) bool { | ||
| return pageReq != nil && pageReq.Reverse && len(pageReq.Key) == 0 | ||
| } | ||
|
|
||
| func parseNumericActionID(actionID string) (uint64, bool) { | ||
| parsed, err := strconv.ParseUint(actionID, 10, 64) | ||
| if err != nil { | ||
| return 0, false | ||
| } | ||
| return parsed, true | ||
| } | ||
|
|
||
| func sortActionsByNumericID(actions []*types.Action) { | ||
| sort.SliceStable(actions, func(i, j int) bool { | ||
| leftNumericID, leftIsNumeric := parseNumericActionID(actions[i].ActionID) | ||
| rightNumericID, rightIsNumeric := parseNumericActionID(actions[j].ActionID) | ||
|
|
||
| switch { | ||
| case leftIsNumeric && rightIsNumeric: | ||
| if leftNumericID == rightNumericID { | ||
| return actions[i].ActionID < actions[j].ActionID | ||
| } | ||
| return leftNumericID < rightNumericID | ||
| case leftIsNumeric != rightIsNumeric: | ||
| return leftIsNumeric | ||
| default: | ||
| return actions[i].ActionID < actions[j].ActionID | ||
| } | ||
| }) | ||
| } | ||
|
Comment on lines
+19
to
+48
|
||
|
|
||
| func paginateActionSlice(actions []*types.Action, pageReq *query.PageRequest) ([]*types.Action, *query.PageResponse) { | ||
| if pageReq == nil { | ||
| return actions, &query.PageResponse{} | ||
| } | ||
|
|
||
| total := uint64(len(actions)) | ||
| offset := pageReq.Offset | ||
| if offset > total { | ||
| offset = total | ||
| } | ||
|
|
||
| limit := pageReq.Limit | ||
| if limit == 0 || offset+limit > total { | ||
| limit = total - offset | ||
|
Comment on lines
+61
to
+63
|
||
| } | ||
|
|
||
| end := offset + limit | ||
| page := actions[int(offset):int(end)] | ||
|
|
||
| pageRes := &query.PageResponse{} | ||
| if pageReq.CountTotal { | ||
| pageRes.Total = total | ||
| } | ||
|
|
||
| return page, pageRes | ||
| } | ||
|
|
||
| // ListActions returns a list of actions, optionally filtered by type and state | ||
| func (q queryServer) ListActions(goCtx context.Context, req *types.QueryListActionsRequest) (*types.QueryListActionsResponse, error) { | ||
| if req == nil { | ||
|
|
@@ -81,19 +141,39 @@ func (q queryServer) ListActions(goCtx context.Context, req *types.QueryListActi | |
| } else { | ||
| actionStore := prefix.NewStore(storeAdapter, []byte(ActionKeyPrefix)) | ||
|
|
||
| onResult := func(key, value []byte, accumulate bool) (bool, error) { | ||
| var act actiontypes.Action | ||
| if err := q.k.cdc.Unmarshal(value, &act); err != nil { | ||
| return false, err | ||
| } | ||
| if shouldUseNumericReverseOrdering(req.Pagination) { | ||
| iter := actionStore.Iterator(nil, nil) | ||
| defer iter.Close() | ||
|
|
||
| if accumulate { | ||
| for ; iter.Valid(); iter.Next() { | ||
| var act actiontypes.Action | ||
| if unmarshalErr := q.k.cdc.Unmarshal(iter.Value(), &act); unmarshalErr != nil { | ||
| return nil, status.Errorf(codes.Internal, "failed to unmarshal action: %v", unmarshalErr) | ||
| } | ||
| actions = append(actions, &act) | ||
| } | ||
|
Comment on lines
+144
to
154
|
||
|
|
||
| return true, nil | ||
| sortActionsByNumericID(actions) | ||
| for i, j := 0, len(actions)-1; i < j; i, j = i+1, j-1 { | ||
| actions[i], actions[j] = actions[j], actions[i] | ||
| } | ||
|
|
||
| actions, pageRes = paginateActionSlice(actions, req.Pagination) | ||
|
||
| } else { | ||
| onResult := func(key, value []byte, accumulate bool) (bool, error) { | ||
| var act actiontypes.Action | ||
| if err := q.k.cdc.Unmarshal(value, &act); err != nil { | ||
| return false, err | ||
| } | ||
|
|
||
| if accumulate { | ||
| actions = append(actions, &act) | ||
| } | ||
|
|
||
| return true, nil | ||
| } | ||
| pageRes, err = query.FilteredPaginate(actionStore, req.Pagination, onResult) | ||
| } | ||
|
Comment on lines
141
to
176
|
||
| pageRes, err = query.FilteredPaginate(actionStore, req.Pagination, onResult) | ||
| } | ||
| if err != nil { | ||
| return nil, status.Errorf(codes.Internal, "failed to paginate actions: %v", err) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,8 +9,8 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sdk "github.com/cosmos/cosmos-sdk/types" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/cosmos/cosmos-sdk/types/query" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "go.uber.org/mock/gomock" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/stretchr/testify/require" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "go.uber.org/mock/gomock" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
12
to
+13
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/stretchr/testify/require" | |
| "go.uber.org/mock/gomock" | |
| "go.uber.org/mock/gomock" | |
| "github.com/stretchr/testify/require" |
Copilot
AI
Feb 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test only covers the basic scenario with two numeric action IDs in reverse pagination. Consider adding test cases for edge cases such as: mixing numeric and non-numeric action IDs, actions with identical numeric values but different string representations, very large numeric IDs approaching uint64 limits, and pagination with offset to ensure the numeric ordering is maintained across multiple pages.
| } | |
| } | |
| func TestKeeper_ListActions_ReversePagination_MixedNumericAndNonNumeric(t *testing.T) { | |
| ctrl := gomock.NewController(t) | |
| defer ctrl.Finish() | |
| k, ctx := keepertest.ActionKeeper(t, ctrl) | |
| q := keeper.NewQueryServerImpl(k) | |
| price := sdk.NewInt64Coin("stake", 100) | |
| actions := []types.Action{ | |
| { | |
| Creator: "creator-numeric-low", | |
| ActionID: "2", | |
| ActionType: types.ActionTypeCascade, | |
| Metadata: []byte("metadata-2"), | |
| Price: price.String(), | |
| ExpirationTime: 1234567890, | |
| State: types.ActionStateApproved, | |
| BlockHeight: 10, | |
| SuperNodes: []string{"supernode-1"}, | |
| }, | |
| { | |
| Creator: "creator-numeric-high", | |
| ActionID: "10", | |
| ActionType: types.ActionTypeCascade, | |
| Metadata: []byte("metadata-10"), | |
| Price: price.String(), | |
| ExpirationTime: 1234567891, | |
| State: types.ActionStateApproved, | |
| BlockHeight: 11, | |
| SuperNodes: []string{"supernode-2"}, | |
| }, | |
| { | |
| Creator: "creator-non-numeric", | |
| ActionID: "abc", | |
| ActionType: types.ActionTypeCascade, | |
| Metadata: []byte("metadata-abc"), | |
| Price: price.String(), | |
| ExpirationTime: 1234567892, | |
| State: types.ActionStateApproved, | |
| BlockHeight: 12, | |
| SuperNodes: []string{"supernode-3"}, | |
| }, | |
| } | |
| for i := range actions { | |
| require.NoError(t, k.SetAction(ctx, &actions[i])) | |
| } | |
| resp, err := q.ListActions(ctx, &types.QueryListActionsRequest{ | |
| Pagination: &query.PageRequest{ | |
| Limit: 10, | |
| Reverse: true, | |
| }, | |
| }) | |
| require.NoError(t, err) | |
| require.Len(t, resp.Actions, 3) | |
| // Ensure all expected IDs are present. | |
| gotIDs := []string{ | |
| resp.Actions[0].ActionID, | |
| resp.Actions[1].ActionID, | |
| resp.Actions[2].ActionID, | |
| } | |
| require.ElementsMatch(t, []string{"2", "10", "abc"}, gotIDs) | |
| // Ensure numeric IDs are ordered numerically relative to each other in reverse order. | |
| // That is, "10" must appear before "2" in the result slice. | |
| var idx10, idx2 int = -1, -1 | |
| for i, id := range gotIDs { | |
| if id == "10" { | |
| idx10 = i | |
| } | |
| if id == "2" { | |
| idx2 = i | |
| } | |
| } | |
| require.NotEqual(t, -1, idx10) | |
| require.NotEqual(t, -1, idx2) | |
| require.Less(t, idx10, idx2) | |
| } | |
| func TestKeeper_ListActions_ReversePagination_IdenticalNumericDifferentStrings(t *testing.T) { | |
| ctrl := gomock.NewController(t) | |
| defer ctrl.Finish() | |
| k, ctx := keepertest.ActionKeeper(t, ctrl) | |
| q := keeper.NewQueryServerImpl(k) | |
| price := sdk.NewInt64Coin("stake", 100) | |
| actionA := types.Action{ | |
| Creator: "creator-a", | |
| ActionID: "10", | |
| ActionType: types.ActionTypeCascade, | |
| Metadata: []byte("metadata-10"), | |
| Price: price.String(), | |
| ExpirationTime: 1234567890, | |
| State: types.ActionStateApproved, | |
| BlockHeight: 20, | |
| SuperNodes: []string{"supernode-a"}, | |
| } | |
| actionB := types.Action{ | |
| Creator: "creator-b", | |
| ActionID: "0010", | |
| ActionType: types.ActionTypeCascade, | |
| Metadata: []byte("metadata-0010"), | |
| Price: price.String(), | |
| ExpirationTime: 1234567891, | |
| State: types.ActionStateApproved, | |
| BlockHeight: 21, | |
| SuperNodes: []string{"supernode-b"}, | |
| } | |
| require.NoError(t, k.SetAction(ctx, &actionA)) | |
| require.NoError(t, k.SetAction(ctx, &actionB)) | |
| resp, err := q.ListActions(ctx, &types.QueryListActionsRequest{ | |
| Pagination: &query.PageRequest{ | |
| Limit: 10, | |
| Reverse: true, | |
| }, | |
| }) | |
| require.NoError(t, err) | |
| require.Len(t, resp.Actions, 2) | |
| // Ensure both representations are present and there are no duplicates/missing entries. | |
| gotIDs := []string{ | |
| resp.Actions[0].ActionID, | |
| resp.Actions[1].ActionID, | |
| } | |
| require.ElementsMatch(t, []string{"10", "0010"}, gotIDs) | |
| } | |
| func TestKeeper_ListActions_ReversePagination_VeryLargeNumericIDs(t *testing.T) { | |
| ctrl := gomock.NewController(t) | |
| defer ctrl.Finish() | |
| k, ctx := keepertest.ActionKeeper(t, ctrl) | |
| q := keeper.NewQueryServerImpl(k) | |
| price := sdk.NewInt64Coin("stake", 100) | |
| // IDs close to uint64 max value. | |
| actionLow := types.Action{ | |
| Creator: "creator-low-large", | |
| ActionID: "18446744073709551610", | |
| ActionType: types.ActionTypeCascade, | |
| Metadata: []byte("metadata-low-large"), | |
| Price: price.String(), | |
| ExpirationTime: 1234567890, | |
| State: types.ActionStateApproved, | |
| BlockHeight: 30, | |
| SuperNodes: []string{"supernode-low"}, | |
| } | |
| actionHigh := types.Action{ | |
| Creator: "creator-high-large", | |
| ActionID: "18446744073709551615", | |
| ActionType: types.ActionTypeCascade, | |
| Metadata: []byte("metadata-high-large"), | |
| Price: price.String(), | |
| ExpirationTime: 1234567891, | |
| State: types.ActionStateApproved, | |
| BlockHeight: 31, | |
| SuperNodes: []string{"supernode-high"}, | |
| } | |
| require.NoError(t, k.SetAction(ctx, &actionLow)) | |
| require.NoError(t, k.SetAction(ctx, &actionHigh)) | |
| resp, err := q.ListActions(ctx, &types.QueryListActionsRequest{ | |
| Pagination: &query.PageRequest{ | |
| Limit: 1, | |
| Reverse: true, | |
| }, | |
| }) | |
| require.NoError(t, err) | |
| require.Len(t, resp.Actions, 1) | |
| require.Equal(t, "18446744073709551615", resp.Actions[0].ActionID) | |
| } | |
| func TestKeeper_ListActions_ReversePaginationWithOffset_MaintainsNumericOrder(t *testing.T) { | |
| ctrl := gomock.NewController(t) | |
| defer ctrl.Finish() | |
| k, ctx := keepertest.ActionKeeper(t, ctrl) | |
| q := keeper.NewQueryServerImpl(k) | |
| price := sdk.NewInt64Coin("stake", 100) | |
| ids := []string{"2", "10", "100", "1000"} | |
| for i, id := range ids { | |
| action := types.Action{ | |
| Creator: "creator-" + id, | |
| ActionID: id, | |
| ActionType: types.ActionTypeCascade, | |
| Metadata: []byte("metadata-" + id), | |
| Price: price.String(), | |
| ExpirationTime: int64(1234567890 + i), | |
| State: types.ActionStateApproved, | |
| BlockHeight: int64(40 + i), | |
| SuperNodes: []string{"supernode-" + id}, | |
| } | |
| require.NoError(t, k.SetAction(ctx, &action)) | |
| } | |
| // First page: highest two numeric IDs in reverse order. | |
| firstResp, err := q.ListActions(ctx, &types.QueryListActionsRequest{ | |
| Pagination: &query.PageRequest{ | |
| Limit: 2, | |
| Offset: 0, | |
| Reverse: true, | |
| }, | |
| }) | |
| require.NoError(t, err) | |
| require.Len(t, firstResp.Actions, 2) | |
| // Second page: next two numeric IDs in reverse order. | |
| secondResp, err := q.ListActions(ctx, &types.QueryListActionsRequest{ | |
| Pagination: &query.PageRequest{ | |
| Limit: 2, | |
| Offset: 2, | |
| Reverse: true, | |
| }, | |
| }) | |
| require.NoError(t, err) | |
| require.Len(t, secondResp.Actions, 2) | |
| // Collect all IDs from both pages. | |
| allIDs := []string{ | |
| firstResp.Actions[0].ActionID, | |
| firstResp.Actions[1].ActionID, | |
| secondResp.Actions[0].ActionID, | |
| secondResp.Actions[1].ActionID, | |
| } | |
| // Ensure we saw exactly the four IDs we inserted. | |
| require.ElementsMatch(t, []string{"2", "10", "100", "1000"}, allIDs) | |
| // Ensure global reverse numeric order across both pages. | |
| // The expected reverse numeric order is: 1000, 100, 10, 2. | |
| expectedOrder := []string{"1000", "100", "10", "2"} | |
| // Map id -> index in concatenated results. | |
| index := make(map[string]int) | |
| for i, id := range allIDs { | |
| index[id] = i | |
| } | |
| for i := 0; i < len(expectedOrder)-1; i++ { | |
| curr := expectedOrder[i] | |
| next := expectedOrder[i+1] | |
| require.Less(t, index[curr], index[next]) | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition on line 20 specifically checks for an empty pagination Key, but this means the numeric reverse ordering is only applied when pagination Key is not provided. If a client uses cursor-based pagination with a Key from a previous response, they would get lexical ordering instead of numeric ordering, leading to inconsistent results across paginated requests. This could cause confusion when paginating through results.