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

  1. Use only single read-only SELECT statements (WITH ... SELECT is fine if supported by the pre-check)
  2. Perform writes through the proper kernel APIs/transactions instead of the query path
  3. 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

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


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)