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
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.
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
Example fix
// before stmt := "DELETE FROM blocks WHERE id = '...'" err := sql.CheckReadonlyStatement(stmt) // after stmt := "SELECT * FROM blocks WHERE id = '...'" err := sql.CheckReadonlyStatement(stmt)
Defensive patterns
Strategy: validation
Validate before calling
const first = stmt.trim().split(/\s+/)[0]?.toUpperCase();
if (!["SELECT", "WITH", "EXPLAIN"].includes(first)) throw new Error("only read-only SELECT allowed"); Try / catch
try {
await runQuery(stmt);
} catch (e) {
if (String(e.message).includes("not a read-only query")) showUserError("SELECT queries only");
else throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- SQL statement is not read-only
- SQL statement is not single
- Access to encrypted notebook data is not supported via this…
- access to private/internal IP is prohibited
- access to private/internal IP is prohibited
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/712a4410f16ac18c.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/sql/stmt_validate.go:203
}
// 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 9f775e8a12)