{"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/9f775e8a12daef8255556097396f9b2739078892/kernel/sql/stmt_validate.go#L137-L173","documentation":"CheckSingleStatement validates that a user-supplied SQL string contains exactly one statement. containsMultipleStatements (semicolon-aware parsing) detects more than one statement (e.g. `SELECT 1; DROP TABLE x`), which the read-only query path forbids because multi-statement input could smuggle writes.","triggerScenarios":"Calling CheckSingleStatement via sqlQuery, searchAssetContentBySQL, validateDocumentStatQuery, QueryAssetContentNoLimit, or SelectAssetContentsRawStmt with SQL containing multiple semicolon-separated statements.","commonSituations":"Users pasting multi-statement SQL into SQL query panels; generated SQL that appends a trailing extra statement; copy-pasted snippets ending with two statements instead of one.","solutions":["Reduce the SQL to a single statement, removing any semicolon-separated statements after the first","Remove trailing semicolons/empty statements if the validator counts them as extra statements","Use only one SELECT per API call"],"exampleFix":"// before\nconst q = \"SELECT * FROM blocks LIMIT 10; SELECT count(*) FROM blocks;\"\n// after\nconst q = \"SELECT * FROM blocks LIMIT 10;\"","handlingStrategy":"validation","validationCode":"const stmtCount = sql.split(\";\").map(s => s.trim()).filter(Boolean).length;\nif (stmtCount > 1) throw new Error(\"Only a single SQL statement is allowed\");","typeGuard":null,"tryCatchPattern":"if err := sql.CheckSingleStatement(userSQL); err != nil {\n    return showSqlValidationError(err)\n}","preventionTips":["Strip or reject extra semicolon-separated statements in user input before submitting","Validate SQL client-side before calling query APIs","Never concatenate multiple statements when building queries programmatically"],"tags":["sql","validation","security"],"backgroundTag":"invalid-argument-value","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"}