siyuan-note/siyuan · error
SQL statement is not a read-only query
Error message
SQL statement is not a read-only query
What it means
Returned by checkReadonlyStatement in kernel/sql/stmt_validate.go when isReadonlyQueryStatement(stmt) is false. This is the first-line keyword filter: after stripping leading whitespace, line comments (--) and block comments (/* */), the statement must begin with SELECT or WITH. Any other leading keyword (INSERT, UPDATE, DELETE, ATTACH, DETACH, PRAGMA, BEGIN, EXPLAIN, CREATE, etc.) is rejected here, before SQLite ever sees it, so that constructs SQLite would otherwise mark readonly (ATTACH, DETACH, transaction control) cannot bypass the guard.
Source
Thrown at kernel/sql/stmt_validate.go:191
}
// CheckReadonlyStatementInBox 在指定笔记本对应的数据库连接上检查 SQL 是否只读。
func CheckReadonlyStatementInBox(stmt, boxID string) error {
targetDB := db
if boxDB := GetEncryptedDB(boxID); nil != boxDB {
targetDB = boxDB
} else if IsEncryptedBoxFn != nil && IsEncryptedBoxFn(boxID) {
return errors.New("encrypted box db not opened for box " + boxID)
}
return checkReadonlyStatement(stmt, targetDB)
}
func checkReadonlyStatement(stmt string, targetDB *sql.DB) error {
if strings.TrimSpace(stmt) == "" {
return errors.New("SQL statement is empty")
}
if !isReadonlyQueryStatement(stmt) {
return errors.New("SQL statement is not a read-only query")
}
if nil == targetDB {
return errors.New("database is nil")
}
ctx := context.Background()
conn, err := targetDB.Conn(ctx)
if err != nil {
return err
}
defer conn.Close()
return conn.Raw(func(dc any) error {
sqliteConn, ok := dc.(*sqlite3.SQLiteConn)
if !ok {
return fmt.Errorf("SQL driver connection type is unexpected: %T", dc)
}
ds, err := sqliteConn.Prepare(stmt)
if err != nil {View on GitHub (pinned to 251596fc0d)
Solutions
- Rewrite the statement so the first executable keyword is SELECT or WITH (e.g. use `WITH ... SELECT ...` for CTEs).
- If you actually need to mutate data, use the dedicated write API/endpoint instead of the readonly SQL query API.
- If you need PRAGMA/EXPLAIN diagnostics, run them through the kernel's own tooling, not the user-facing readonly SQL surface.
Example fix
// before stmt := "PRAGMA database_list" err := sql.CheckReadonlyStatement(stmt) // after (readonly query surface only accepts SELECT/WITH) stmt := "SELECT * FROM pragma_database_list" err := sql.CheckReadonlyStatement(stmt)
Defensive patterns
Strategy: validation
Validate before calling
// Validate the statement starts with SELECT or WITH before calling the readonly API.
func isLikelyReadonlyQuery(stmt string) bool {
s := strings.TrimSpace(stmt)
for s != "" {
switch {
case strings.HasPrefix(s, "--"):
if i := strings.IndexByte(s, '\n'); i >= 0 {
s = strings.TrimSpace(s[i+1:])
continue
}
return false
case strings.HasPrefix(s, "/*"):
if i := strings.Index(s[2:], "*/"); i >= 0 {
s = strings.TrimSpace(s[i+4:])
continue
}
return false
}
break
}
end := strings.IndexFunc(s, func(r rune) bool { return !unicode.IsLetter(r) })
if end < 0 { end = len(s) }
switch strings.ToUpper(s[:end]) {
case "SELECT", "WITH":
return true
}
return false
}
if !isLikelyReadonlyQuery(stmt) {
return errors.New("refusing to send non-SELECT/WITH statement to readonly SQL API")
}
err := sql.CheckReadonlyStatement(stmt) Prevention
- Only ever submit SELECT or WITH queries through /api/query, /api/search, the MCP sql tool, or the CLI sql command.
- Route all data mutation through dedicated write APIs; never try to overload the readonly SQL surface.
- Add a unit test that asserts your statement string starts with SELECT or WITH before sending it.
When it happens
Trigger: Calling sql.CheckReadonlyStatement / CheckReadonlyStatementInBox / CheckAssetContentReadonlyStatement with a statement whose first keyword is not SELECT or WITH. These are invoked from /api/query and /api/search HTTP endpoints, the MCP sql tool, the `siyuan sql` CLI command, and SQL asset-content query paths.
Common situations: A plugin or user submits a PRAGMA or EXPLAIN QUERY PLAN through the readonly SQL API expecting it to pass; sending an INSERT/UPDATE/DELETE to a query-only endpoint; pasting a multi-line statement that starts with a comment followed by a non-SELECT keyword where the comment is stripped but the keyword still fails.
Related errors
- SQL statement is not read-only
- SQL statement is not single
- SQL statement is empty
- invalid import token
- import path is not sub path of import dir
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/712a4410f16ac18c.
Report an issue: GitHub.