{"record":{"id":"5c531f8ca02bc3f3","repo":"siyuan-note/siyuan","slug":"database-s-moved-across-notebook-encryption-boundaries","errorCode":null,"errorMessage":"database [%s] moved across notebook encryption boundaries","messagePattern":"database \\[(.+?)\\] moved across notebook encryption boundaries","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/attribute_view_transaction.go","lineNumber":43,"sourceCode":"\t\"github.com/siyuan-note/siyuan/kernel/av\"\n\t\"github.com/siyuan-note/siyuan/kernel/filesys\"\n\t\"github.com/siyuan-note/siyuan/kernel/util\"\n)\n\n// 事务失败时恢复本次写入涉及的数据库和文档，租约保持到提交或回滚结束。\ntype attributeViewRollback struct {\n\tviews  map[string]*av.AttributeView\n\ttrees  map[string]*parse.Tree\n\tleases map[string]bool\n}\n\nfunc (tx *Transaction) readAttributeViewForMutation(avID, blockID, boxID string) (*av.AttributeView, error) {\n\tcarrierBoxID, exact, err := resolveAttributeViewCarrierBoxID(blockID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif exact && carrierBoxID != boxID {\n\t\treturn nil, fmt.Errorf(\"database [%s] moved across notebook encryption boundaries\", avID)\n\t}\n\tif tx.attributeViewRollback == nil {\n\t\ttx.attributeViewRollback = &attributeViewRollback{views: map[string]*av.AttributeView{},\n\t\t\ttrees: map[string]*parse.Tree{}, leases: map[string]bool{}}\n\t}\n\tif boxID != \"\" && !tx.attributeViewRollback.leases[boxID] {\n\t\tif err = AcquireEncryptedBoxOperation(boxID); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ttx.attributeViewRollback.leases[boxID] = true\n\t}\n\tcurrent, err := av.ParseAttributeViewForIndexInBox(avID, boxID)\n\tif err == nil && current == nil {\n\t\terr = av.ErrViewNotFound\n\t}\n\treturn current, err\n}\n","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/attribute_view_transaction.go#L25-L61","documentation":"readAttributeViewForMutation resolves which notebook actually carries the attribute view's anchor block. When the exact carrier is found (resolveAttributeViewCarrierBoxID returned exact=true) but its box ID differs from the boxID passed by the caller, the database block has been moved into a notebook with different encryption settings, and the transaction refuses to mutate it under the stale box context. This protects encrypted-notebook key-envelope boundaries from cross-notebook writes.","triggerScenarios":"Calling any mutation path that goes through readAttributeViewForMutation (replaceAttributeViewBinding, restoreEmbeddedAttributeViewHistory, flushAttributeViewBlockDeletions, restoreDeletedAttributeViewBlocks, removeAttributeViewField, or inline transaction handlers) with a boxID that no longer matches the block's actual carrier notebook, after the block (e.g. a database block) was dragged or moved between notebooks with different encryption states.","commonSituations":"A database/embed block was moved across notebooks before a queued transaction ran; replaying an undo/redo or history-restore operation recorded before the move; a client cached the old box ID after a move operation.","solutions":["Refresh the block's current carrier: re-resolve the block's notebook ID and re-issue the transaction with the correct boxID","If the move was unintended, move the database block back to its original notebook before applying the mutation","Ensure the client serializes operations correctly — do not run queued mutations that assumed the pre-move notebook"],"exampleFix":"// before\n_, err := tx.readAttributeViewForMutation(avID, blockID, oldBoxID)\n// after\ncarrierBox, _, resolveErr := resolveAttributeViewCarrierBoxID(blockID)\nif resolveErr == nil {\n    _, err = tx.readAttributeViewForMutation(avID, blockID, carrierBox)\n}","handlingStrategy":"validation","validationCode":"const carrier = await resolveCarrierBox(blockID);\nif (carrier !== txBoxID) {\n  throw new Error(\"database moved across notebooks; re-resolve boxID before mutating\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  await api.mutateAttributeView({ avID, blockID, boxID });\n} catch (e) {\n  if (String(e.msg).includes(\"moved across notebook encryption boundaries\")) {\n    const boxID = await resolveCarrierBox(blockID); // re-resolve and retry\n    await api.mutateAttributeView({ avID, blockID, boxID });\n  }\n}","preventionTips":["Re-resolve a block's notebook ID after any move/drag between notebooks before mutating its attribute view","Don't cache boxID across long-running queues or undo/redo replays","Be extra careful when notebooks have different encryption settings — operations cannot span those boundaries"],"tags":["attribute-view","encryption","notebook-boundary","stale-reference"],"backgroundTag":"incompatible-source-type","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"}