{"record":{"id":"dced3b3bf7166bdb","repo":"siyuan-note/siyuan","slug":"attribute-view-source-is-required","errorCode":null,"errorMessage":"attribute view source is required","messagePattern":"attribute view source is required","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/av/av.go","lineNumber":950,"sourceCode":"\t\t}\n\t}\n\tif nil == err {\n\t\terr = CheckSpec(ret)\n\t}\n\tif nil == err {\n\t\terr = ret.NormalizeRichText()\n\t}\n\treturn\n}\n\nfunc SaveAttributeView(av *AttributeView) (err error) {\n\treturn saveAttributeView(av, nil)\n}\n\n// SaveAttributeViewIfUnchanged 仅在全局数据库仍与扫描源一致时原子保存。\nfunc SaveAttributeViewIfUnchanged(av *AttributeView, original []byte) error {\n\tif original == nil {\n\t\treturn errors.New(\"attribute view source is required\")\n\t}\n\treturn saveAttributeView(av, original)\n}\n\nfunc saveAttributeView(av *AttributeView, original []byte) (err error) {\n\tif !ast.IsNodeIDPattern(av.ID) {\n\t\terr = ErrInvalidAttributeViewID\n\t\tlogging.LogErrorf(\"save attribute view failed: %s\", err)\n\t\treturn\n\t}\n\tdefer func() {\n\t\tif nil == err {\n\t\t\tav.ResetCardCoverPositionChanges()\n\t\t}\n\t}()\n\n\tif err = av.NormalizeRichText(); nil != err {\n\t\tlogging.LogErrorf(\"normalize attribute view [%s] rich text failed: %s\", av.ID, err)","sourceCodeStart":932,"sourceCodeEnd":968,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/av/av.go#L932-L968","documentation":"SaveAttributeViewIfUnchanged performs a compare-and-swap style save: it only writes if the global database still matches the `original` bytes captured by the caller. Passing nil for `original` means there is no baseline to compare against, so the API refuses the call — callers wanting unconditional saves must use SaveAttributeView instead.","triggerScenarios":"Calling SaveAttributeViewIfUnchanged(av, nil); a code path that conditionally captured the original bytes (e.g. only when the file existed) and then proceeds to the conditional save even when capture failed; refactor that swapped SaveAttributeView for SaveAttributeViewIfUnchanged without threading the original snapshot.","commonSituations":"Plugin/automation code doing optimistic concurrency saves that skipped the initial read; first-creation flows where no original data exists; merge scripts that pass through a nil baseline.","solutions":["Read the current AV data and pass those bytes as `original` before calling SaveAttributeViewIfUnchanged","If an unconditional save is intended, call SaveAttributeView(av) instead","Restructure the flow so the conditional-save path is only reached when a real snapshot was captured"],"exampleFix":"// before\nav.SaveAttributeViewIfUnchanged(attrView, nil)\n// after\noriginal, err := av.GetAttributeViewData(av.ID)\nif err != nil { return err }\nerr = av.SaveAttributeViewIfUnchanged(attrView, original)","handlingStrategy":"validation","validationCode":"if original == nil {\n  return errors.New(\"SaveAttributeViewIfUnchanged requires a non-nil original snapshot; use SaveAttributeView for unconditional saves\")\n}","typeGuard":null,"tryCatchPattern":"if err := avlib.SaveAttributeViewIfUnchanged(attrView, original); err != nil {\n  if strings.Contains(err.Error(), \"source is required\") {\n    return avlib.SaveAttributeView(attrView) // fall back to unconditional\n  }\n  return err\n}","preventionTips":["Always capture the current AV data bytes before scheduling a conditional save","Use SaveAttributeView when no baseline exists (e.g. first creation)","Keep snapshot capture and conditional save in the same code path so nil cannot slip through"],"tags":["api-usage","optimistic-concurrency","nil"],"backgroundTag":"missing-required-argument","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"}