{"record":{"id":"1c50221bf35550a2","repo":"siyuan-note/siyuan","slug":"a-document-tree-plan-must-be-applied-in-a-single-t","errorCode":null,"errorMessage":"a document tree plan must be applied in a single transaction","messagePattern":"a document tree plan must be applied in a single transaction","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/template_doc_tree.go","lineNumber":456,"sourceCode":"\t\t\"title\":    node.Title,\n\t\t\"id\":       node.ID,\n\t\t\"parentID\": node.ParentID,\n\t\t\"rootID\":   node.RootID,\n\t\t\"hPath\":    node.HPath,\n\t\t\"name\":     \"\",\n\t\t\"alias\":    \"\",\n\t}\n}\n\n// AttachTemplateDocTreePlans 将一次性计划转换为内核事务操作，父文档内容与全部子文档共用一条撤销记录。\nfunc AttachTemplateDocTreePlans(transactions []*Transaction) (attached bool, err error) {\n\tvar target *Transaction\n\tfor _, transaction := range transactions {\n\t\tif nil == transaction || \"\" == transaction.TemplateDocTreePlanID {\n\t\t\tcontinue\n\t\t}\n\t\tif nil != target || 1 != len(transactions) {\n\t\t\treturn false, errors.New(\"a document tree plan must be applied in a single transaction\")\n\t\t}\n\t\ttarget = transaction\n\t}\n\tif nil == target {\n\t\treturn false, nil\n\t}\n\tif target.isReplay {\n\t\treturn false, errors.New(\"template document tree plans cannot be attached to replay transactions\")\n\t}\n\tif 0 == len(target.DoOperations) || 0 == len(target.UndoOperations) {\n\t\treturn false, errors.New(\"template document tree plan requires reversible parent operations\")\n\t}\n\tif err = validateTemplateDocTreeParentOperations(target); nil != err {\n\t\treturn false, err\n\t}\n\tfor _, operation := range append(append([]*Operation{}, target.DoOperations...), target.UndoOperations...) {\n\t\tif nil != operation && (\"restoreCreatedDoc\" == operation.Action || \"removeCreatedDoc\" == operation.Action) {\n\t\t\treturn false, errors.New(\"template document tree transaction contains a reserved operation\")","sourceCodeStart":438,"sourceCodeEnd":474,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/template_doc_tree.go#L438-L474","documentation":"AttachTemplateDocTreePlans scans a batch of transactions for one carrying a TemplateDocTreePlanID. A plan is only valid when it is the sole transaction in the batch; if more than one transaction is submitted, or a plan transaction coexists with normal ones, the error is thrown. Document-tree plans must commit atomically so their generated documents can be cleaned up or undone as a unit.","triggerScenarios":"Calling the transactions API with an array containing a transaction whose TemplateDocTreePlanID is set alongside another transaction; or including two plan-bearing transactions in one request; performTransactions invokes this during commit.","commonSituations":"Client-side code batching a template-doc-tree apply with unrelated edits in one request; retry logic re-sending a plan transaction after a partial failure, duplicating it in the queue; plugin code constructing multiple transactions but submitting them together.","solutions":["Submit the document-tree plan transaction alone in its own /api/transactions request","Split unrelated block edits into a separate, prior or subsequent transaction call","Ensure only one transaction in any single batch carries a TemplateDocTreePlanID","Review retry/queue code so a failed plan transaction is not re-batched with others"],"exampleFix":"// before: plan batched with ordinary edits\nperformTransactions([]*Transaction{planTx, normalTx})\n// after: plan committed alone\nperformTransactions([]*Transaction{planTx})\nperformTransactions([]*Transaction{normalTx})","handlingStrategy":"validation","validationCode":"// Go: ensure a plan transaction is submitted alone\nsingle := len(txs) == 1 && txs[0].TemplateDocTreePlanID != \"\"\nif !single {\n    return errors.New(\"submit the document-tree plan transaction by itself\")\n}","typeGuard":null,"tryCatchPattern":"ok, err := AttachTemplateDocTreePlans(transactions)\nif err != nil && strings.Contains(err.Error(), \"single transaction\") {\n    return splitAndRetryIndividually(transactions)\n}","preventionTips":["Always send a template document-tree plan in its own transactions request","Never batch plan transactions with ordinary block-edit transactions","Audit client queue/retry code for transaction batching","Include at most one plan-bearing transaction per API call"],"tags":["transaction","template-doc-tree","atomicity","siyuan-kernel"],"backgroundTag":"invalid-state-transition","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}