{"record":{"id":"72e654c2bae1f1bf","repo":"usememos/memos","slug":"expression-must-be-a-literal","errorCode":null,"errorMessage":"expression must be a literal","messagePattern":"expression must be a literal","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"internal/filter/render.go","lineNumber":850,"sourceCode":"func (r *renderer) addBoolArg(value bool) string {\n\tvar v any\n\tswitch r.dialect {\n\tcase DialectSQLite:\n\t\tif value {\n\t\t\tv = 1\n\t\t} else {\n\t\t\tv = 0\n\t\t}\n\tdefault:\n\t\tv = value\n\t}\n\treturn r.addArg(v)\n}\n\nfunc expectLiteral(expr ValueExpr) (any, error) {\n\tlit, ok := expr.(*LiteralValue)\n\tif !ok {\n\t\treturn nil, errors.New(\"expression must be a literal\")\n\t}\n\treturn lit.Value, nil\n}\n\nfunc expectBool(expr ValueExpr) (bool, error) {\n\tlit, err := expectLiteral(expr)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tvalue, ok := lit.(bool)\n\tif !ok {\n\t\treturn false, errors.New(\"boolean literal required\")\n\t}\n\treturn value, nil\n}\n\nfunc expectNumericLiteral(expr ValueExpr) (int64, error) {\n\tlit, err := expectLiteral(expr)","sourceCodeStart":832,"sourceCodeEnd":868,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/internal/filter/render.go#L832-L868","documentation":"A generic guard in the filter renderer (expectLiteral) that rejects any value expression which is not a *LiteralValue node. The SQL rendering path can only bind concrete constant values; references, function calls, or arithmetic cannot be translated to placeholders. Callers that wrap it (tag membership, boolean checks, numeric limits) surface this raw message.","triggerScenarios":"Compiling a filter where a position requiring a constant gets a non-literal AST node, e.g. `limit in [row_count]`, `pinned == NOT true`, or any identifier/expression used where expectLiteral/expectBool/expectNumericLiteral is invoked.","commonSituations":"Hand-written filter strings saved in memo queries or API calls that reference fields or functions on the right-hand side; filters generated programmatically that forget to stringify/quote values before serialization.","solutions":["Replace the right-hand side with a literal constant (string in quotes, number, true/false)","If generating filters in code, interpolate evaluated values as literals, not raw symbols","Check the filter grammar docs for which operators accept literals only"],"exampleFix":"// before\nengine.CompileToStatement(ctx, \"tag in [tag_name]\", opts)\n// after\nengine.CompileToStatement(ctx, \"tag in [\\\"tag_name\\\"]\", opts)","handlingStrategy":"validation","validationCode":"// Reject filters whose RHS of ==/in contains a bare identifier (non-literal)\nvar bareIdentRHS = regexp.MustCompile(`==\\s*[A-Za-z_][A-Za-z0-9_]*\\s*$`)\nfunc hasBareIdentifierRHS(filter string) bool { return bareIdentRHS.MatchString(filter) }","typeGuard":null,"tryCatchPattern":"// Treat compile errors as user input errors (400), not server faults\nif _, err := engine.CompileToStatement(ctx, filter, opts); err != nil {\n  return status.Errorf(codes.InvalidArgument, \"invalid filter: %v\", err)\n}","preventionTips":["Only use literal constants on the right-hand side of comparisons","Evaluate dynamic values client-side and inline them as literals","Validate filters with a compile step before saving"],"tags":["filter","validation","ast"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}