{"record":{"id":"288fd2a4a08459e9","repo":"siyuan-note/siyuan","slug":"invalid-database-binding-replay","errorCode":null,"errorMessage":"invalid database binding replay","messagePattern":"invalid database binding replay","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/attribute_view_binding_undo.go","lineNumber":41,"sourceCode":"\n\t\"github.com/88250/lute/ast\"\n\t\"github.com/88250/lute/parse\"\n\t\"github.com/siyuan-note/siyuan/kernel/av\"\n\t\"github.com/siyuan-note/siyuan/kernel/util\"\n)\n\n// 换绑快照只保留主键值，条目 ID 和其他字段值始终保持不变。\ntype attributeViewBindingSnapshot struct {\n\tavID, blockID, boxID string\n\tbefore, after        *av.Value\n\tduplicateItemID      string\n}\n\nfunc (tx *Transaction) replaceAttributeViewBinding(op *Operation) error {\n\tstate := op.attributeViewBinding\n\tif state != nil {\n\t\tif !tx.isReplay || state.avID != op.AvID || state.before.BlockID != op.PreviousID {\n\t\t\treturn fmt.Errorf(\"invalid database binding replay\")\n\t\t}\n\t\tcurrent, err := tx.readAttributeViewForMutation(state.avID, state.blockID, state.boxID)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif state.duplicateItemID != \"\" {\n\t\t\top.RetData = map[string]any{\"targetItemID\": state.duplicateItemID, \"duplicate\": true}\n\t\t\treturn nil\n\t\t}\n\t\texpected, desired := state.before, state.after\n\t\tif op.attributeViewBindingUndo {\n\t\t\texpected, desired = desired, expected\n\t\t}\n\t\tvalue := current.GetBlockValue(expected.BlockID)\n\t\tif value == nil || value.Block == nil || value.ID != expected.ID || value.KeyID != expected.KeyID ||\n\t\t\tvalue.IsDetached != expected.IsDetached || value.Block.ID != expected.Block.ID {\n\t\t\treturn fmt.Errorf(\"database entry binding [%s] changed\", expected.BlockID)\n\t\t}","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/attribute_view_binding_undo.go#L23-L59","documentation":"Raised during undo/redo replay of a database binding replacement. The stored binding snapshot must match the replayed operation (same attribute view ID and same previous block ID); if it does not, the transaction refuses to apply a replay that would desynchronize the attribute view from the recorded undo state.","triggerScenarios":"doReplaceAttrViewBlock invoked with isReplay=true (undo/redo) while op.attributeViewBinding is nil, its avID differs from op.AvID, or snapshot before.BlockID differs from op.PreviousID — i.e. the undo stack contains an operation recorded against a different database or block than the one being replayed.","commonSituations":"Undoing a multi-step session where database binding ops were interleaved with other transactions; attribute view deleted/re-created between the original operation and the undo; custom or scripted transaction construction that omits attributeViewBinding snapshots.","solutions":["Do not construct replaceAttrViewBlock operations manually; use the UI or API so the binding snapshot is recorded","Re-open the document so the undo stack is rebuilt consistently, then redo the binding change","Check whether an extension/plugin is issuing raw transactions and fix it to fill op.attributeViewBinding","Report/reproduce with the transaction log if the built-in undo produces this; it indicates an undo-stack bug"],"exampleFix":"// before (raw op, no snapshot)\ntx.appendOp(&Operation{Action: 'replaceAttrViewBlock', AvID: avID, PreviousID: oldID, NextID: newID})\n// after (record binding state so undo/redo can replay)\nop.attributeViewBinding = captureAttributeViewBinding(avID, oldID)\ntx.appendOp(op)","handlingStrategy":"try-catch","validationCode":"// before replaying, confirm the recorded snapshot matches the op\nif (!op.attributeViewBinding || op.attributeViewBinding.avID !== op.avID ||\n    op.attributeViewBinding.before.blockID !== op.previousID) {\n  skipReplay(op);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await tx.doReplaceAttrViewBlock(op);\n} catch (e) {\n  if (e.message.indexOf('invalid database binding replay') !== -1) {\n    rebuildUndoStack();\n  }\n}","preventionTips":["Never issue raw replaceAttrViewBlock transactions without capturing attributeViewBinding state","Avoid mixing plugin-issued transactions with UI undo history","Keep undo stacks within one session; reload after sync to resync state","Test undo/redo after database binding changes in your plugin workflows"],"tags":["database","undo-redo","transaction","replay"],"backgroundTag":"invalid-state-transition","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"}