gastownhall/beads · error

invalid limit %d: a query limit must be zero or greater; 0 m

Error message

invalid limit %d: a query limit must be zero or greater; 0 means unlimited

What it means

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.

Source

Thrown at internal/workapi/query.go:68

// evaluation, the default closed exclusion, the limit defaulting, and the row
// bound each shape of query may carry.
//
// THE ROW BOUND IS THE WHOLE REASON THIS IS ONE FUNCTION. A predicate query's
// Filter gets NO limit — the predicate must see every candidate row, or the
// page it produces is an arbitrary prefix of the answer reported as the whole
// of it (issueops/querier.go:118-133). The front doors used to bound that query
// at max(3*Limit, 100) and filter what came back; that bound is gone.
//
// It reads no configuration and touches no store, so both implementations share
// it without supplying a config source.
func BuildQueryPlan(in issueops.QueryRequest) (QueryPlan, error) {
	expression := strings.TrimSpace(in.Expression)
	if expression == "" {
		return QueryPlan{}, invalidQueryExpression("an expression is required")
	}
	limit := LimitOr(in.Limit, DefaultQueryLimit)
	if limit < 0 {
		return QueryPlan{}, fmt.Errorf("invalid limit %d: a query limit must be zero or greater; 0 means unlimited%.0w",
			limit, issueops.ErrValidation)
	}
	if in.Offset < 0 {
		return QueryPlan{}, fmt.Errorf("invalid offset %d: a query offset must be zero or greater%.0w",
			in.Offset, issueops.ErrValidation)
	}
	if in.Offset > 0 && in.SortBy != "" {
		return QueryPlan{}, fmt.Errorf(
			"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",
			issueops.ErrValidation)
	}

	node, err := query.Parse(expression)
	if err != nil {
		return QueryPlan{}, invalidQueryExpression(err.Error())
	}
	result, err := query.NewEvaluator(time.Now()).Evaluate(node)
	if err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Pass 0 for unlimited or omit Limit to get DefaultQueryLimit instead of a negative value
  2. Clamp/validate page-size inputs at the boundary: if limit < 0 { limit = 0 }
  3. Fix the upstream arithmetic (e.g. remaining-page calculation) that produces the negative number

Example fix

// before
plan, err := BuildQueryPlan(issueops.QueryRequest{Expression: "status = open", Limit: -1})
// after
limit := pageSize
if limit < 0 { limit = 0 } // 0 means unlimited
plan, err := BuildQueryPlan(issueops.QueryRequest{Expression: "status = open", Limit: limit})
Defensive patterns

Strategy: validation

Validate before calling

if limit < 0 {
	return errors.New("limit must be >= 0 (0 = unlimited)")
}

Try / catch

plan, err := BuildQueryPlan(req)
if err != nil && errors.Is(err, issueops.ErrValidation) {
	// client-input error; surface as 400, do not retry
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/dced2329df9b873d. Report an issue: GitHub.