{"record":{"id":"c4d6c8d9bb643c1c","repo":"siyuan-note/siyuan","slug":"query-database-bound-blocks-in-notebook-s-faile","errorCode":null,"errorMessage":"query database-bound blocks in notebook [%s] failed: %w","messagePattern":"query database-bound blocks in notebook \\[(.+?)\\] failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/mount.go","lineNumber":205,"sourceCode":"\tif !ast.IsNodeIDPattern(boxID) {\n\t\treturn errors.New(\"invalid notebook ID\")\n\t}\n\tif _, loaded := boxLock.LoadOrStore(boxID, true); loaded {\n\t\terr = errors.New(Conf.language(239))\n\t\treturn\n\t}\n\tdefer boxLock.Delete(boxID)\n\n\tif util.IsReservedFilename(boxID) {\n\t\treturn fmt.Errorf(\"can not remove [%s] caused by it is a reserved file\", boxID)\n\t}\n\n\tFlushTxQueue()\n\tsql.FlushQueue()\n\t// 索引和笔记本目录删除后无法再读取 custom-avs，需提前收集；实际删除成功后再清理绑定行。\n\tdeletedAttrViewBlockIDs, err := collectBoxDeletedAttributeViewBlocks(boxID)\n\tif nil != err {\n\t\treturn fmt.Errorf(\"query database-bound blocks in notebook [%s] failed: %w\", boxID, err)\n\t}\n\tisUserGuide := IsUserGuide(boxID)\n\tlocalPath := filepath.Join(util.DataDir, boxID)\n\tif !filelock.IsExist(localPath) {\n\t\tforgetRuntimeNormalBox(boxID)\n\t\tremoveMasterPasswordMigrationBox(boxID)\n\t\treturn\n\t}\n\tif !gulu.File.IsDir(localPath) {\n\t\treturn fmt.Errorf(\"can not remove [%s] caused by it is not a dir\", boxID)\n\t}\n\n\t// 删目录前固定加密状态，确保后续历史、资源和索引清理始终使用同一个安全边界。\n\tisEncrypted := IsEncryptedBox(boxID)\n\tif isEncrypted {\n\t\t// 加密索引先持有生命周期租约再获取索引锁，因此删除也必须先结束生命周期，保持锁顺序一致。\n\t\tunmount0(boxID)\n\t}","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/mount.go#L187-L223","documentation":"Thrown by RemoveBox in kernel/model/mount.go:205 when collecting attribute-view-bound blocks before deleting a notebook. It wraps an underlying error from collectBoxDeletedAttributeViewBlocks -> sql.QueryBoundBlockAVIDsInBox, so the SQLite query that finds which database (attribute view) blocks live in the notebook failed. The notebook is not removed and the transaction is aborted.","triggerScenarios":"Calling /api/notebook/removeNotebook (model.RemoveBox) while the SQLite database (siyuan.db) is locked, the schema is mid-migration, the index queue is corrupt, or the kernel is being shut down. FlushTxQueue and sql.FlushQueue run first, so any failure inside the subsequent SQL read against siyuan.db surfaces here.","commonSituations":"Concurrent notebook operations hitting SQLite contention, a damaged siyuan.db after a crash, antivirus/file-locking on Windows holding the DB file, or running remove while a reindex is still draining the sql queue.","solutions":["Retry the remove after the kernel reports it is idle (no indexing/reindex in progress); the boxLock and queue flush serialize correctly on a clean kernel.","Check kernel logs for the wrapped error (%w) — it identifies the exact SQL failure (busy, locked, no such table) and points to the next step.","If the wrapped error is 'database is locked', ensure no second SiYuan process or sync job holds siyuan.db; close other instances and retry.","If the DB schema is corrupt, restore from history/repo or run the kernel's index rebuild before retrying the remove."],"exampleFix":"// before\nerr := model.RemoveBox(boxID)\n// after — surface the wrapped cause to the user instead of swallowing it\nif err := model.RemoveBox(boxID); err != nil {\n    logging.LogErrorf(\"remove notebook %s failed: %s\", boxID, err)\n    return err // includes the wrapped SQL cause via %%w\n}","handlingStrategy":"try-catch","validationCode":"// Avoid calling RemoveBox while indexing is active.\nif model.IsIndexing() {\n    return errors.New(\"wait for indexing to finish before removing a notebook\")\n}\nerr := model.RemoveBox(boxID)","typeGuard":"// boxID must be a 22-char base32-style ID accepted by ast.IsNodeIDPattern.\nfunc validBoxID(id string) bool { return ast.IsNodeIDPattern(id) }","tryCatchPattern":"err := model.RemoveBox(boxID)\nif err != nil {\n    if strings.Contains(err.Error(), \"query database-bound blocks\") {\n        // transient — retry once after queues drain, then report the wrapped cause\n        time.Sleep(2 * time.Second)\n        err = model.RemoveBox(boxID)\n    }\n    return err\n}","preventionTips":["Do not run RemoveBox concurrently with sync or reindex — both touch siyuan.db.","Always log the wrapped %%w cause; the surface message alone hides whether it is contention or corruption.","Keep one kernel instance per workspace to avoid cross-process SQLite locks."],"tags":["notebook","attribute-view","sqlite","remove"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}