{"record":{"id":"a5084a8cf2466e7a","repo":"siyuan-note/siyuan","slug":"prev-must-be-a-string","errorCode":null,"errorMessage":"prev must be a string","messagePattern":"prev must be a string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/tools/database.go","lineNumber":346,"sourceCode":"\tkey := &av.Key{ID: keyID, Name: name, Type: av.KeyType(keyType), Icon: icon, DateFormat: av.DateDisplayFormatFull}\n\tif nil != attrView {\n\t\tif storedKey, getErr := attrView.GetKey(keyID); nil == getErr {\n\t\t\tkey = storedKey\n\t\t}\n\t}\n\treturn databaseSuccess(\"key_add\", map[string]any{\"id\": id, \"key\": key})\n}\n\nfunc databasePreviousKeyID(attrView *av.AttributeView, args map[string]any) (ret string, err error) {\n\tview, err := attrView.GetFirstView()\n\tif nil != err {\n\t\treturn \"\", err\n\t}\n\tfieldIDs := databaseViewFieldIDs(view)\n\tif value, specified := args[\"prev\"]; specified {\n\t\tprev, ok := value.(string)\n\t\tif !ok {\n\t\t\treturn \"\", errors.New(\"prev must be a string\")\n\t\t}\n\t\tif \"\" == prev {\n\t\t\treturn \"\", nil\n\t\t}\n\t\tfor _, fieldID := range fieldIDs {\n\t\t\tif fieldID == prev {\n\t\t\t\treturn prev, nil\n\t\t\t}\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"previous key not found in current view: %s\", prev)\n\t}\n\tif 0 < len(fieldIDs) {\n\t\treturn fieldIDs[len(fieldIDs)-1], nil\n\t}\n\treturn \"\", nil\n}\n\nfunc databaseKeySetTemplate(args map[string]any) (CallToolResult, error) {","sourceCodeStart":328,"sourceCodeEnd":364,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/mcp/tools/database.go#L328-L364","documentation":"databasePreviousKeyID (kernel/mcp/tools/database.go) resolves the key after which a new key is inserted. If the caller explicitly supplies the optional `prev` argument, it must be a string key/field ID; any other JSON type is rejected with this error.","triggerScenarios":"Calling database key add with prev set to a number, boolean, null-ish object, or array — e.g. prev: 3 or prev: [\"id\"].","commonSituations":"Clients passing position indexes instead of IDs; LLM-generated args guessing prev is numeric; template code that interpolates a non-string variable.","solutions":["Pass prev as a string field ID (e.g. \"20240101120000-abcdefg\")","Omit prev entirely to let the key be appended after the current view's last field (empty string means view start position) in the default flow","Coerce the value with String(prev) only if it genuinely is an ID","Check the current view's field IDs via database tooling before choosing prev"],"exampleFix":"// before\n{prev: 2}\n// after\n{prev: \"20240101120000-abcdefg\"}","handlingStrategy":"validation","validationCode":"if (\"prev\" in args && typeof args.prev !== \"string\") throw new Error(\"prev must be a string field ID\");","typeGuard":"const isFieldId = (v) => typeof v === \"string\" && /^[0-9]{14}-[a-z0-9]+$/.test(v);","tryCatchPattern":"try { await call(\"database_key_add\", args) } catch (e) { if (String(e).includes(\"prev must be a string\")) { args.prev = String(args.prev); } else throw e; }","preventionTips":["Never pass numeric positions as prev","Pass field-ID strings or omit prev","Coerce interpolations to string explicitly"],"tags":["mcp","database","type-mismatch","validation"],"backgroundTag":"type-mismatch","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}