{"record":{"id":"3369fb9e6b857730","repo":"gastownhall/beads","slug":"invalid-query-expression-s","errorCode":null,"errorMessage":"invalid query expression: %s","messagePattern":"invalid query expression: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workapi/query.go","lineNumber":131,"sourceCode":"\t\t// backend, and the probe row is genuinely last.\n\t\tplan.Filter.Limit = limit\n\t\tplan.Filter.SortBy = in.SortBy\n\t\tplan.Filter.SortDesc = in.Reverse\n\t}\n\n\t// The default closed exclusion, applied only to an expression that has no\n\t// opinion of its own about status (issueops/querier.go:29-41).\n\tif !in.IncludeClosed && plan.Filter.Status == nil && !mentionsStatus(node) {\n\t\tplan.Filter.ExcludeStatus = append(plan.Filter.ExcludeStatus, types.StatusClosed)\n\t}\n\treturn plan, nil\n}\n\n// invalidQueryExpression is the one refusal shape a bad expression takes. The\n// prefix is load-bearing: internal/httpapi maps it back to the `q` parameter\n// so an unparseable expression is the documented 400 rather than a 500.\nfunc invalidQueryExpression(detail string) error {\n\treturn fmt.Errorf(\"invalid query expression: %s%.0w\", detail, issueops.ErrValidation)\n}\n\n// mentionsStatus reports whether the expression compares `status` anywhere.\n// An expression that does keeps its own answer about closed rows; one that\n// does not gets the default exclusion.\nfunc mentionsStatus(node query.Node) bool {\n\tswitch n := node.(type) {\n\tcase *query.ComparisonNode:\n\t\treturn n.Field == \"status\"\n\tcase *query.AndNode:\n\t\treturn mentionsStatus(n.Left) || mentionsStatus(n.Right)\n\tcase *query.OrNode:\n\t\treturn mentionsStatus(n.Left) || mentionsStatus(n.Right)\n\tcase *query.NotNode:\n\t\treturn mentionsStatus(n.Operand)\n\tdefault:\n\t\treturn false\n\t}","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workapi/query.go#L113-L149","documentation":"invalidQueryExpression is the single refusal shape for any expression that fails — an empty expression, or one query.Parse cannot parse — always formatted as 'invalid query expression: <detail>' and wrapping issueops.ErrValidation. The prefix is load-bearing: internal/httpapi maps it back to the `q` parameter so a bad expression surfaces as the documented 400 rather than a 500.","triggerScenarios":"Calling BuildQueryPlan with in.Expression empty after trimming, or containing syntax query.Parse rejects — unbalanced quotes/parens, unknown operators, malformed comparisons like `status =`.","commonSituations":"Users typing ad-hoc `bd query 'status == open'` with wrong operator syntax; templates interpolating an empty variable into the expression; quoting issues in shells stripping inner quotes; schema changes renaming a field an old saved expression references.","solutions":["Read the detail after the prefix — it names the parse failure or 'an expression is required'","Check errors.Is(err, issueops.ErrValidation) to confirm it is a client-input 400, not a server fault","Validate the expression syntax against the query language docs (comparison operators, quoted values)","Guard against empty/whitespace input before calling: strings.TrimSpace(expr) != \"\""],"exampleFix":"// before\nbd query 'status = '\n// after\nbd query 'status = open'","handlingStrategy":"try-catch","validationCode":"expr := strings.TrimSpace(userExpr)\nif expr == \"\" {\n\treturn errors.New(\"query expression is required\")\n}","typeGuard":null,"tryCatchPattern":"plan, err := BuildQueryPlan(req)\nvar verr error\nif err != nil {\n\tif errors.Is(err, issueops.ErrValidation) && strings.HasPrefix(err.Error(), \"invalid query expression\") {\n\t\t// documented 400 path: map detail back to the q parameter\n\t}\n\treturn err\n}","preventionTips":["Trim and require a non-empty expression before calling","Test user-supplied expressions with query.Parse before shipping them","Quote values with spaces; use the documented operators only","Re-validate saved expressions after field renames"],"tags":["validation","query","parser","http-400"],"backgroundTag":"invalid-query-expression","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}