{"record":{"id":"78130773fe6f2e33","repo":"siyuan-note/siyuan","slug":"sql-statement-is-not-read-only","errorCode":null,"errorMessage":"SQL statement is not read-only","messagePattern":"SQL statement is not read-only","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/sql/stmt_validate.go","lineNumber":231,"sourceCode":"\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 {\n\t\t\treturn err\n\t\t}\n\t\tdefer ds.Close()\n\n\t\tsst, ok := ds.(*sqlite3.SQLiteStmt)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"SQL driver statement type is unexpected: %T\", ds)\n\t\t}\n\t\tif !sst.Readonly() {\n\t\t\treturn errors.New(\"SQL statement is not read-only\")\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// isReadonlyQueryStatement 仅允许查询语句进入 SQLite prepare，提前拒绝会被 sqlite3_stmt_readonly\n// 视为只读的 ATTACH、DETACH 和事务控制语句。WITH 中的写操作仍由 sqlite3_stmt_readonly 拒绝。\nfunc isReadonlyQueryStatement(stmt string) bool {\n\tstmt = strings.TrimSpace(stmt)\n\tfor \"\" != stmt {\n\t\tswitch {\n\t\tcase strings.HasPrefix(stmt, \"--\"):\n\t\t\tif lineEnd := strings.IndexByte(stmt, '\\n'); 0 <= lineEnd {\n\t\t\t\tstmt = strings.TrimSpace(stmt[lineEnd+1:])\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn false\n\t\tcase strings.HasPrefix(stmt, \"/*\"):","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/sql/stmt_validate.go#L213-L249","documentation":"This is the second line of defense: even if the textual pre-check passes, the kernel executes the statement via the SQLite driver and calls SQLiteStmt.Readonly() (sqlite3_stmt_readonly) on the prepared statement. If SQLite itself reports the statement is not read-only, execution is aborted. This catches statements that pass keyword heuristics but actually mutate state.","triggerScenarios":"A statement containing write sub-expressions or side effects that slipped past the string-level check (e.g. cleverly crafted statements, functions with side effects, or unsupported statement kinds that the keyword check misclassifies).","commonSituations":"Custom SQL built dynamically by plugins that embed write keywords inside an otherwise SELECT-looking statement; version changes where the driver's readonly semantics became stricter.","solutions":["Rewrite the SQL so the whole statement is a genuine read-only SELECT","Remove any embedded write constructs (writable CTEs, chained statements)","If you legitimately need writes, use the kernel's write transaction APIs, not the read-only validator"],"exampleFix":"// before\nstmt := \"SELECT * FROM blocks; PRAGMA journal_mode=WAL\" // rejected by sqlite3_stmt_readonly\n// after\nstmt := \"SELECT * FROM blocks\"","handlingStrategy":"validation","validationCode":"// Keep statements trivially read-only; avoid chained statements and side-effecting constructs\nif (stmt.includes(\";\")) throw new Error(\"multiple statements not allowed\");","typeGuard":null,"tryCatchPattern":"try { await runQuery(stmt); } catch (e) { if (String(e.message).includes(\"not read-only\")) logAndReject(stmt); else throw e; }","preventionTips":["Avoid multi-statement strings and pragmas in query paths","Treat the keyword pre-check as advisory: SQLite's readonly flag is the authority","Keep statements simple SELECTs over known tables"],"tags":["sql","sqlite","security"],"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"}