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

  1. Rewrite the statement so the first executable keyword is SELECT or WITH (e.g. use `WITH ... SELECT ...` for CTEs).
  2. If you actually need to mutate data, use the dedicated write API/endpoint instead of the readonly SQL query API.
  3. 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

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


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/712a4410f16ac18c. Report an issue: GitHub.