fix(apiserver): page tokens keep 63-bit job IDs exact (bug-1903 partial)

findPageTokenIndex decoded the token's unique_job_id into a float64;
array element IDs set bit 62, where float64 spacing is 1024, so paging
skipped rows or restarted at row 0 -- an endless loop for callers that
page until the token is empty (control/query of arrays over 1000
elements). Decode with UseNumber and keep integers as int64.

Found by the job-array-one-based-index feature validation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-01 15:48:02 +00:00
parent faa228d592
commit efff6b658f
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
2 changed files with 54 additions and 1 deletions

View file

@ -2,6 +2,7 @@ package store
import (
"apiserver/utils"
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
@ -378,10 +379,23 @@ func findPageTokenIndex(rows []utils.OrderMap, pageToken string, orders []utils.
if err != nil {
return 0
}
// bug-1903: decode numbers exactly; a float64 cannot hold a 63-bit
// unique_job_id (array element IDs set bit 62).
var tokenData map[string]interface{}
if err := json.Unmarshal(jsonBytes, &tokenData); err != nil {
dec := json.NewDecoder(bytes.NewReader(jsonBytes))
dec.UseNumber()
if err := dec.Decode(&tokenData); err != nil {
return 0
}
for k, v := range tokenData {
if num, ok := v.(json.Number); ok {
if i, err := num.Int64(); err == nil {
tokenData[k] = i
} else if f, err := num.Float64(); err == nil {
tokenData[k] = f
}
}
}
for i, row := range rows {
match := true

View file

@ -204,3 +204,42 @@ func TestExecuteQuery_Predicates_SetAfterBuildQueryOptions(t *testing.T) {
t.Fatalf("field selection after predicates: row = %v, want only job_id", got[0].Data)
}
}
// bug-1903: the page token carried unique_job_id through JSON into a
// float64. Array element IDs set tag bit 62, where float64 spacing is
// 1024, so the token no longer named the page's last row: pages skipped
// rows or restarted at row 0 (an endless loop for callers that page until
// the token is empty, e.g. control of a >1000-element array).
func TestPaginateRowsArrayElementIDsRoundTripPageToken(t *testing.T) {
const n = 2500
base := int64(1)<<62 | int64(1700000000)<<20 | int64(32768)
rows := make([]utils.OrderMap, n)
for k := 1; k <= n; k++ {
rows[k-1] = utils.OrderMap{Data: map[string]interface{}{"unique_job_id": base + int64(k)}}
}
orders := []utils.SortOrder{{Column: "unique_job_id", Ascending: true}}
var got []int64
token := ""
for page := 0; ; page++ {
if page > 10 {
t.Fatalf("paging did not terminate after %d pages (%d rows seen)", page, len(got))
}
res, next := PaginateRows(rows, 1000, token, orders)
for _, r := range res {
got = append(got, r.Data["unique_job_id"].(int64)-base)
}
if next == "" {
break
}
token = next
}
if len(got) != n {
t.Fatalf("got %d rows, want %d", len(got), n)
}
for i, k := range got {
if k != int64(i+1) {
t.Fatalf("row %d is element %d, want %d", i, k, i+1)
}
}
}