{"record":{"id":"712a4410f16ac18c","repo":"siyuan-note/siyuan","slug":"sql-statement-is-not-a-read-only-query","errorCode":null,"errorMessage":"SQL statement is not a read-only query","messagePattern":"SQL statement is not a read-only query","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/sql/stmt_validate.go","lineNumber":203,"sourceCode":"}\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\n\t}\n\tdefer conn.Close()\n\n\treturn conn.Raw(func(dc any) error {\n\t\tsqliteConn, ok := dc.(*sqlite3.SQLiteConn)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"SQL driver connection type is unexpected: %T\", dc)\n\t\t}\n\t\tds, err := sqliteConn.Prepare(stmt)\n\t\tif err != nil {","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/sql/stmt_validate.go#L185-L221","documentation":"After confirming the statement is non-empty, checkReadonlyStatement requires it to be a read-only query (SELECT/EXPLAIN etc.) via isReadonlyQueryStatement before it is prepared. Any statement that is not recognized as a read-only query is rejected, preventing writes through APIs meant for querying. The kernel deliberately rejects anything that could mutate data.","triggerScenarios":"Passing INSERT/UPDATE/DELETE/DROP/ALTER/ATTACH or even PRAGMA through CheckReadonlyStatement/CheckAssetContentReadonlyStatement/CheckReadonlyStatementInBox; also CTEs or syntax variants that the textual pre-check fails to recognize as a query.","commonSituations":"A caller tries to modify siyuan.db through the SQL query API; a plugin builds a write statement; a multi-statement string like 'SELECT 1; DROP TABLE' is rejected.","solutions":["Use only single read-only SELECT statements (WITH ... SELECT is fine if supported by the pre-check)","Perform writes through the proper kernel APIs/transactions instead of the query path","Check isReadonlyQueryStatement's accepted syntax and align your statement with it"],"exampleFix":"// before\nstmt := \"DELETE FROM blocks WHERE id = '...'\"\nerr := sql.CheckReadonlyStatement(stmt)\n// after\nstmt := \"SELECT * FROM blocks WHERE id = '...'\"\nerr := sql.CheckReadonlyStatement(stmt)","handlingStrategy":"validation","validationCode":"const first = stmt.trim().split(/\\s+/)[0]?.toUpperCase();\nif (![\"SELECT\", \"WITH\", \"EXPLAIN\"].includes(first)) throw new Error(\"only read-only SELECT allowed\");","typeGuard":null,"tryCatchPattern":"try {\n  await runQuery(stmt);\n} catch (e) {\n  if (String(e.message).includes(\"not a read-only query\")) showUserError(\"SELECT queries only\");\n  else throw e;\n}","preventionTips":["Restrict query builders to producing single SELECT statements","Never concatenate user input into write statements for read paths","Use the kernel's dedicated transaction APIs for writes"],"tags":["sql","security","readonly"],"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"}