{"record":{"id":"780096015262a7da","repo":"siyuan-note/siyuan","slug":"encrypted-box-db-not-opened-for-box","errorCode":null,"errorMessage":"encrypted box db not opened for box ","messagePattern":"encrypted box db not opened for box ","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/sql/stmt_validate.go","lineNumber":181,"sourceCode":"// 注意：若字符串里在语法上还有第二条及以后的语句，本函数只针对「首条」对应的 stmt 做判断，\n// 不会拒绝多语句。与 CheckSingleStatement 组合即可得到「单条 + 只读」策略。\n// 仅允许 SELECT 和 WITH 查询，避免 SQLite 将 ATTACH、DETACH 和事务控制语句标记为只读后放行。\nfunc CheckReadonlyStatement(stmt string) error {\n\treturn checkReadonlyStatement(stmt, db)\n}\n\n// CheckAssetContentReadonlyStatement 在资源文件内容数据库连接上检查 SQL 是否只读。\nfunc CheckAssetContentReadonlyStatement(stmt string) error {\n\treturn checkReadonlyStatement(stmt, assetContentDB)\n}\n\n// CheckReadonlyStatementInBox 在指定笔记本对应的数据库连接上检查 SQL 是否只读。\nfunc CheckReadonlyStatementInBox(stmt, boxID string) error {\n\ttargetDB := db\n\tif boxDB := GetEncryptedDB(boxID); nil != boxDB {\n\t\ttargetDB = boxDB\n\t} else if IsEncryptedBoxFn != nil && IsEncryptedBoxFn(boxID) {\n\t\treturn errors.New(\"encrypted box db not opened for box \" + boxID)\n\t}\n\treturn checkReadonlyStatement(stmt, targetDB)\n}\n\nfunc checkReadonlyStatement(stmt string, targetDB *sql.DB) error {\n\tif strings.TrimSpace(stmt) == \"\" {\n\t\treturn errors.New(\"SQL statement is empty\")\n\t}\n\tif !isReadonlyQueryStatement(stmt) {\n\t\treturn errors.New(\"SQL statement is not a read-only query\")\n\t}\n\tif nil == targetDB {\n\t\treturn errors.New(\"database is nil\")\n\t}\n\tctx := context.Background()\n\tconn, err := targetDB.Conn(ctx)\n\tif err != nil {\n\t\treturn err","sourceCodeStart":163,"sourceCodeEnd":199,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/afa823b6b4e4f183511e0bc0a3be93caa94c7c97/kernel/sql/stmt_validate.go#L163-L199","documentation":"Thrown by sql.CheckReadonlyStatementInBox when the target notebook is configured as encrypted but its per-box SQLCipher database connection has not been opened yet. SiYuan keeps encrypted notebooks in separate SQLCipher databases registered in a process map (encryptedDBs); until UnlockBox calls sql.OpenEncryptedDB(boxID, dek), GetEncryptedDB returns nil and the validator refuses to run rather than silently validating the statement against the wrong (global siyuan.db) connection.","triggerScenarios":"Calling a SQL API that routes by boxID while the box is encrypted but locked: /api/query with a stmt referencing the encrypted box (kernel/api/sql.go:70), /api/search with box-filtered SQL (kernel/api/search.go:446), MCP execute-sql with boxID (kernel/mcp/tools/sql.go:81), or document statistics (kernel/model/document_stat.go:151). IsEncryptedBoxFn(boxID) returns true but GetEncryptedDB(boxID) is nil, i.e. the box was never unlocked in this kernel session.","commonSituations":"Kernel restarted so previously unlocked encrypted boxes are locked again; automation scripts (CLI/MCP) run against a workspace before unlocking the notebook; workspace synced to a new device and SQL jobs run before first unlock; a wrong boxID that happens to belong to an encrypted box.","solutions":["Unlock the encrypted notebook in the SiYuan UI first (UnlockBox opens the SQLCipher db via sql.OpenEncryptedDB), then re-issue the SQL query","Verify the boxID with /api/notebook/lsbook and confirm the box really is the one you want; a mistyped ID pointing at a locked encrypted box triggers this","If automating via API/MCP, perform the unlock step for encrypted boxes before issuing any box-scoped SQL","Query only non-encrypted boxes with raw SQL, or use search APIs that do not require an open box db connection"],"exampleFix":"// before\nerr := sql.CheckReadonlyStatementInBox(stmt, boxID)\n\n// after\nif sql.GetEncryptedDB(boxID) == nil && sql.IsEncryptedBoxFn != nil && sql.IsEncryptedBoxFn(boxID) {\n    // unlock the box first: UnlockBox derives the dek and calls sql.OpenEncryptedDB(boxID, dek)\n    return fmt.Errorf(\"box %s is locked; unlock it before querying\", boxID)\n}\nerr := sql.CheckReadonlyStatementInBox(stmt, boxID)","handlingStrategy":"validation","validationCode":"if sql.IsEncryptedBoxFn != nil && sql.IsEncryptedBoxFn(boxID) && sql.GetEncryptedDB(boxID) == nil {\n    return fmt.Errorf(\"encrypted box %s is locked; unlock it before running SQL\", boxID)\n}\nerr := sql.CheckReadonlyStatementInBox(stmt, boxID)","typeGuard":null,"tryCatchPattern":"err := sql.CheckReadonlyStatementInBox(stmt, boxID)\nif err != nil {\n    if strings.HasPrefix(err.Error(), \"encrypted box db not opened\") {\n        // unlock the box, then retry once\n    }\n}","preventionTips":["Unlock encrypted notebooks before running any box-scoped SQL automation","Treat GetEncryptedDB(boxID) == nil on an encrypted box as a precondition failure, not a query failure","In test code, register a test db via encryptedDBs.Store and stub IsEncryptedBoxFn like kernel/sql/stmt_validate_test.go does"],"tags":["sql","encryption","notebook","sqlcipher","locked-box"],"backgroundTag":"locked-encrypted-database","analyzedSha":"afa823b6b4e4f183511e0bc0a3be93caa94c7c97","analyzedAt":"2026-08-18T17:04:10.865Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}