{"record":{"id":"af665ca48e418cf2","repo":"bytebase/bytebase","slug":"status-value-must-be-a-string","errorCode":null,"errorMessage":"status value must be a string","messagePattern":"status value must be a string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"backend/store/access_grant.go","lineNumber":400,"sourceCode":"\t\t\t\t\t_, accessGrantID, err := common.GetProjectIDAccessGrantID(nameStr)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, errors.Wrapf(err, \"invalid access grant name %q\", nameStr)\n\t\t\t\t\t}\n\t\t\t\t\treturn qb.Q().Space(\"access_grant.id = ?\", accessGrantID), nil\n\t\t\t\tcase \"creator\":\n\t\t\t\t\tcreatorStr, ok := value.(string)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\treturn nil, errors.Errorf(\"creator value must be a string\")\n\t\t\t\t\t}\n\t\t\t\t\tif !strings.HasPrefix(creatorStr, \"users/\") {\n\t\t\t\t\t\treturn nil, errors.Errorf(\"creator must have format \\\"users/{email}\\\", got %q\", creatorStr)\n\t\t\t\t\t}\n\t\t\t\t\tcreatorEmail := strings.TrimPrefix(creatorStr, \"users/\")\n\t\t\t\t\treturn qb.Q().Space(\"access_grant.creator = ?\", creatorEmail), nil\n\t\t\t\tcase \"status\":\n\t\t\t\t\tstatusStr, ok := value.(string)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\treturn nil, errors.Errorf(\"status value must be a string\")\n\t\t\t\t\t}\n\t\t\t\t\treturn getAccessGrantStatusFilter(statusStr)\n\t\t\t\tcase \"query\":\n\t\t\t\t\tqueryStr, ok := value.(string)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\treturn nil, errors.Errorf(\"query value must be a string\")\n\t\t\t\t\t}\n\t\t\t\t\t// Trim the same whitespace set on both sides (boundary\n\t\t\t\t\t// only) so the run-time JIT match in preCheckAccess\n\t\t\t\t\t// survives invisible boundary differences — most\n\t\t\t\t\t// commonly a trailing \\n that Monaco's getValue() emits\n\t\t\t\t\t// in the request drawer but that the editor's\n\t\t\t\t\t// getActiveStatement() doesn't.\n\t\t\t\t\t//\n\t\t\t\t\t// We deliberately do NOT collapse internal whitespace.\n\t\t\t\t\t// Doing so would let \"SELECT * FROM t --\\nWHERE x=1\"\n\t\t\t\t\t// compare equal to \"SELECT * FROM t -- WHERE x=1\",\n\t\t\t\t\t// silently authorizing a query with the WHERE clause","sourceCodeStart":382,"sourceCodeEnd":418,"githubUrl":"https://github.com/bytebase/bytebase/blob/1870550677fe08f0d2a78c07acd27541464eb945/backend/store/access_grant.go#L382-L418","documentation":"The access-grant CEL filter compiler in backend/store/access_grant.go converts a CEL filter expression into SQL. When it encounters `status == <value>` and the CEL literal is not a Go string, it refuses to build the predicate and returns this error. It guards the typed conversion before the value reaches getAccessGrantStatusFilter.","triggerScenarios":"Calling ListAccessGrants (or SearchAccessGrants) with a filter like `status == 1`, `status == true`, or `status in [1]`-style comparisons where the compared value is a number, bool, list, or bytes literal instead of a string.","commonSituations":"Hand-written CEL filters with unquoted enum values (`status == ACTIVE` resolves to an identifier, or `status == 0`), SDK clients passing numeric status codes from older API versions, and code generators that emit the wire enum number rather than the string state name.","solutions":["Quote the status value in the filter: `status == \"ACTIVE\"`.","Use the exact string constant accepted by getAccessGrantStatusFilter (e.g. STATE_ACTIVE / STATE_DISABLED per the proto enum names).","Fix the client/SDK that is serializing the enum as a number into the filter string.","If the value type is legitimately variable, add an explicit string cast or a numeric branch in the compiler before the type assertion."],"exampleFix":"// before\nfilter = \"status == 1\"\n// after\nfilter = \"status == \\\"ACTIVE\\\"\"","handlingStrategy":"validation","validationCode":"// Before sending the filter, ensure the status literal is a quoted string:\nif !strings.Contains(filter, `status == \"`) && strings.Contains(filter, \"status ==\") {\n    return fmt.Errorf(\"filter %q: status literal must be a quoted string\", filter)\n}","typeGuard":"func isStringLiteral(v any) (string, bool) {\n    s, ok := v.(string)\n    return s, ok\n}","tryCatchPattern":"qq, err := store.ListAccessGrants(ctx, filter)\nif err != nil {\n    if strings.Contains(err.Error(), \"status value must be a string\") {\n        // fall back to a corrected/quoted filter or surface a 400 to the client\n    }\n    return err\n}","preventionTips":["Always quote CEL string literals; never compare enums as numbers","Keep filter strings next to proto enum definitions and derive names from the enum","Add a unit test per filter field asserting literal types","Validate filters with common.ParseCELFilter in a dry run before issuing list calls"],"tags":["cel","filter","type-mismatch","sql-builder"],"backgroundTag":"type-mismatch","analyzedSha":"1870550677fe08f0d2a78c07acd27541464eb945","analyzedAt":"2026-09-06T21:16:13.665Z","contentChangedAt":"2026-09-06T21:16:13.665Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}