{"record":{"id":"d480a9bf790ed361","repo":"gastownhall/beads","slug":"invalid-offset-d-a-query-offset-must-be-zero-or","errorCode":null,"errorMessage":"invalid offset %d: a query offset must be zero or greater","messagePattern":"invalid offset (.+?): a query offset must be zero or greater","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workapi/query.go","lineNumber":72,"sourceCode":"// Filter gets NO limit — the predicate must see every candidate row, or the\n// page it produces is an arbitrary prefix of the answer reported as the whole\n// of it (issueops/querier.go:118-133). The front doors used to bound that query\n// at max(3*Limit, 100) and filter what came back; that bound is gone.\n//\n// It reads no configuration and touches no store, so both implementations share\n// it without supplying a config source.\nfunc BuildQueryPlan(in issueops.QueryRequest) (QueryPlan, error) {\n\texpression := strings.TrimSpace(in.Expression)\n\tif expression == \"\" {\n\t\treturn QueryPlan{}, invalidQueryExpression(\"an expression is required\")\n\t}\n\tlimit := LimitOr(in.Limit, DefaultQueryLimit)\n\tif limit < 0 {\n\t\treturn QueryPlan{}, fmt.Errorf(\"invalid limit %d: a query limit must be zero or greater; 0 means unlimited%.0w\",\n\t\t\tlimit, issueops.ErrValidation)\n\t}\n\tif in.Offset < 0 {\n\t\treturn QueryPlan{}, fmt.Errorf(\"invalid offset %d: a query offset must be zero or greater%.0w\",\n\t\t\tin.Offset, issueops.ErrValidation)\n\t}\n\tif in.Offset > 0 && in.SortBy != \"\" {\n\t\treturn QueryPlan{}, fmt.Errorf(\n\t\t\t\"invalid offset: an offset cannot be combined with a display order, because the order is applied to the rows the query bounded and each page would be sorted for itself%.0w\",\n\t\t\tissueops.ErrValidation)\n\t}\n\n\tnode, err := query.Parse(expression)\n\tif err != nil {\n\t\treturn QueryPlan{}, invalidQueryExpression(err.Error())\n\t}\n\tresult, err := query.NewEvaluator(time.Now()).Evaluate(node)\n\tif err != nil {\n\t\treturn QueryPlan{}, invalidQueryExpression(err.Error())\n\t}\n\n\tplan := QueryPlan{","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workapi/query.go#L54-L90","documentation":"BuildQueryPlan rejects a negative offset with this message; offsets must be zero or greater. Like the limit check, it runs before query parsing and wraps issueops.ErrValidation for errors.Is classification. A negative offset has no meaning in row pagination, so it is refused rather than clamped.","triggerScenarios":"Calling BuildQueryPlan with issueops.QueryRequest.Offset < 0, typically from computed pagination (page-1)*pageSize when page was 0 or negative, or a client sending offset=-20.","commonSituations":"Page-number arithmetic bugs in API clients; an 'unset' sentinel of -1 leaking into the request; deserializing a JSON body where offset was omitted and a default of -1 applied.","solutions":["Compute offset as max(0, (page-1)*pageSize) before building the request","Use 0 for the first page and omit Offset when not paginating","Treat 'unset' as 0 or a nil pointer, not -1"],"exampleFix":"// before\noffset := (page - 1) * pageSize // page=0 → offset=-20\n// after\nif page < 1 { page = 1 }\noffset := (page - 1) * pageSize","handlingStrategy":"validation","validationCode":"if offset < 0 {\n\treturn errors.New(\"offset must be >= 0\")\n}","typeGuard":null,"tryCatchPattern":"plan, err := BuildQueryPlan(req)\nif err != nil && errors.Is(err, issueops.ErrValidation) {\n\t// refuse the request; do not retry\n}","preventionTips":["Compute offset as max(0, (page-1)*pageSize)","Model 'unset' with nil/0, never -1","Unit-test pagination math for page 0 and negative page inputs"],"tags":["validation","query","pagination"],"backgroundTag":"invalid-pagination-parameter","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}