{"record":{"id":"dced2329df9b873d","repo":"gastownhall/beads","slug":"invalid-limit-d-a-query-limit-must-be-zero-or-gr","errorCode":null,"errorMessage":"invalid limit %d: a query limit must be zero or greater; 0 means unlimited","messagePattern":"invalid limit (.+?): a query limit must be zero or greater; 0 means unlimited","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workapi/query.go","lineNumber":68,"sourceCode":"// evaluation, the default closed exclusion, the limit defaulting, and the row\n// bound each shape of query may carry.\n//\n// THE ROW BOUND IS THE WHOLE REASON THIS IS ONE FUNCTION. A predicate query's\n// 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 {","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workapi/query.go#L50-L86","documentation":"BuildQueryPlan rejects a negative limit with this message, explaining that a query limit must be zero or greater and that 0 means unlimited. The limit is defaulted via LimitOr before the check, so this fires only when a caller explicitly supplies a negative number. The error carries issueops.ErrValidation so callers can classify it with errors.Is as a 400-style refusal.","triggerScenarios":"Calling BuildQueryPlan with an issueops.QueryRequest whose Limit is negative, e.g. Limit=-1 from a CLI flag parse or an API client computing a page size.","commonSituations":"Subtraction-based pagination code going negative on the last page; a config where pageSize defaults to -1 meaning 'unset'; a client misreading 0 as 'unset' and using -1 instead.","solutions":["Pass 0 for unlimited or omit Limit to get DefaultQueryLimit instead of a negative value","Clamp/validate page-size inputs at the boundary: if limit < 0 { limit = 0 }","Fix the upstream arithmetic (e.g. remaining-page calculation) that produces the negative number"],"exampleFix":"// before\nplan, err := BuildQueryPlan(issueops.QueryRequest{Expression: \"status = open\", Limit: -1})\n// after\nlimit := pageSize\nif limit < 0 { limit = 0 } // 0 means unlimited\nplan, err := BuildQueryPlan(issueops.QueryRequest{Expression: \"status = open\", Limit: limit})","handlingStrategy":"validation","validationCode":"if limit < 0 {\n\treturn errors.New(\"limit must be >= 0 (0 = unlimited)\")\n}","typeGuard":null,"tryCatchPattern":"plan, err := BuildQueryPlan(req)\nif err != nil && errors.Is(err, issueops.ErrValidation) {\n\t// client-input error; surface as 400, do not retry\n}","preventionTips":["Treat 0 as unlimited and omit Limit for the default; never use -1 as a sentinel","Clamp page-size inputs at the boundary","Use typed option structs where Limit is *int so 'unset' is nil, not negative"],"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"}