{"record":{"id":"13883d5ffad209cb","repo":"siyuan-note/siyuan","slug":"sql-statement-is-not-single","errorCode":null,"errorMessage":"SQL statement is not single","messagePattern":"SQL statement is not single","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/sql/stmt_validate.go","lineNumber":155,"sourceCode":"\t\t\ti++\n\t\tcase '/' == ch && next == '*':\n\t\t\tinBlockComment = true\n\t\t\ti++\n\t\tcase ';' == ch:\n\t\t\ttail := string(runes[i+1:])\n\t\t\tif tailIsOnlyWhitespaceOrSQLComments(tail) {\n\t\t\t\t// 分号后仅有空白与 SQL 注释时，SQLite 仍视为同一条语句末尾，不应判为多语句。\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc CheckSingleStatement(stmt string) error {\n\tif containsMultipleStatements(stmt) {\n\t\treturn errors.New(\"SQL statement is not single\")\n\t}\n\treturn nil\n}\n\n// CheckReadonlyStatement 对整段 SQL 做 prepare（不执行），用 sqlite3_stmt_readonly 判断首条语句是否只读。\n// 见 https://sqlite.org/c3ref/stmt_readonly.html\n//\n// 注意：若字符串里在语法上还有第二条及以后的语句，本函数只针对「首条」对应的 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}","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/sql/stmt_validate.go#L137-L173","documentation":"CheckSingleStatement rejects an SQL string whose text contains more than one statement — containsMultipleStatements detected a `;` followed by non-whitespace, non-comment content. This guards the SQL query API (used by the embedded SQLite / SiYuan SQL endpoint) against statement stacking/injection. It is a hard validation, not a runtime fault.","triggerScenarios":"Submitting a query like `SELECT * FROM blocks; DROP TABLE blocks;` or `SELECT 1; SELECT 2` to the SQL API (/api/query/sql or CLI sql). Any `;` that is not trailing-whitespace-or-comment triggers it.","commonSituations":"User pastes multiple queries into the SQL box; a script concatenates queries with `;`; SQL client semicolon-terminates even single queries where the trailing content is another statement.","solutions":["Send only one SQL statement per request; remove the second statement.","If a trailing `;` is the only extra content, the validator allows trailing whitespace/comments — verify there is no real second statement after it.","For batch needs, issue separate API calls per statement rather than stacking."],"exampleFix":"// before\nstmt := \"SELECT * FROM blocks LIMIT 1; SELECT * FROM blocks LIMIT 2\"\n// after\nstmt := \"SELECT * FROM blocks LIMIT 1\"","handlingStrategy":"validation","validationCode":"// Caller side: reject multi-statement SQL before calling the kernel API.\nfunction isSingleStatement(stmt: string): boolean {\n  // crude guard: no ';' except possibly one trailing\n  const trimmed = stmt.replace(/--[^\n]*\n/g, ' ').replace(/\\/\\*[\\s\\S]*?\\*\\//g, ' ').trim()\n  if (!trimmed.endsWith(';')) return trimmed.split(';').length === 1\n  return trimmed.slice(0, -1).split(';').filter(s => s.trim()).length <= 1\n}","typeGuard":null,"tryCatchPattern":"try { await api.querySQL(stmt) }\ncatch (e) {\n  if (/not single/i.test(String(e))) console.warn('split into one statement per request')\n  else throw e\n}","preventionTips":["Send one statement per API call; never stack with `;`.","Validate user-supplied SQL on the client before submission.","Remember a lone trailing `;` followed by whitespace/comments is allowed."],"tags":["sql","sqlite","validation","injection-guard","api"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}