{"record":{"id":"366ce70a5fd518d2","repo":"siyuan-note/siyuan","slug":"document-cannot-be-deleted-as-a-block-s","errorCode":null,"errorMessage":"document cannot be deleted as a block [%s]","messagePattern":"document cannot be deleted as a block \\[(.+?)\\]","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/block_operation.go","lineNumber":49,"sourceCode":"\tswitch operation.Action {\n\tcase \"appendInsert\", \"insert\":\n\t\tif operation.Action == \"appendInsert\" || operation.PreviousID == \"\" && operation.NextID == \"\" {\n\t\t\tif err = treenode.CheckContainerParent(operation.ParentID); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\tcase \"delete\":\n\t\t// 外部删除请求必须命中现存节点，编辑器内部仍可使用幂等删除。\n\t\ttree, loadErr := LoadTreeByBlockID(operation.ID)\n\t\tif loadErr != nil {\n\t\t\treturn nil, loadErr\n\t\t}\n\t\tnode := treenode.GetNodeInTree(tree, operation.ID)\n\t\tif node == nil {\n\t\t\treturn nil, fmt.Errorf(\"block not found [%s]\", operation.ID)\n\t\t}\n\t\tif node == tree.Root {\n\t\t\treturn nil, fmt.Errorf(\"document cannot be deleted as a block [%s]\", operation.ID)\n\t\t}\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported block operation [%s]\", operation.Action)\n\t}\n\n\ttx := &Transaction{DoOperations: []*Operation{operation}}\n\tif err = performTxSyncLocked(tx); err != nil {\n\t\treturn nil, err\n\t}\n\treturn []*Transaction{tx}, nil\n}\n","sourceCodeStart":31,"sourceCodeEnd":61,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/block_operation.go#L31-L61","documentation":"PerformBlockOperation explicitly rejects deleting the document root node as if it were an ordinary block: when the resolved node is the tree's Root, it returns \"document cannot be deleted as a block [%s]\". Document removal must go through the document API instead of the block-transaction path.","triggerScenarios":"Calling PerformBlockOperation with Action=\"delete\" and an ID that is the document (root) block ID — typically the rootID of a .sy file rather than a child block ID.","commonSituations":"Scripts iterating over /api/query/sql results that include root blocks (type 'd') and deleting every row; code that captured the document ID instead of a paragraph/heading ID from a selection.","solutions":["Filter out document-type blocks (root IDs) before issuing delete block operations.","Use /api/filetree/removeDoc for document deletion instead of the block operation API.","Check the block's type from /api/block/getBlockInfo (data.root_id / type 'd') and branch accordingly."],"exampleFix":"// before\nrows.forEach(r => deleteBlock(r.id)); // includes type 'd' rows\n// after\nrows.filter(r => r.type !== 'd').forEach(r => deleteBlock(r.id));\nrows.filter(r => r.type === 'd').forEach(r => fetchPost('/api/filetree/removeDoc', { notebook: r.box, path: r.path }));","handlingStrategy":"validation","validationCode":"async function isDocumentBlock(id) {\n  const res = await fetchPost('/api/query/sql', { stmt: `SELECT type, root_id, path, box FROM blocks WHERE id='${id.replace(/'/g, \"''\")}'` });\n  return res.data[0] && res.data[0].type === 'd';\n}","typeGuard":null,"tryCatchPattern":"try { await deleteBlock(id); } catch (e) { if (String(e.msg).includes('document cannot be deleted as a block')) await fetchPost('/api/filetree/removeDoc', { notebook: box, path }); else throw e; }","preventionTips":["Exclude root/document rows (type 'd') from block-level delete loops.","Route document deletions to /api/filetree/removeDoc.","Check data.root_id vs id: equal means it's a document root."],"tags":["unsupported-operation","document-root","delete","kernel"],"backgroundTag":"unsupported-operation","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"}