From acc336ef9279ca3a3d86cf1b46ac3ee507356620 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sat, 11 Jul 2026 15:54:25 +0300 Subject: [PATCH 1/2] Release 1.0.1: Safe Mode readonly fix and packaging improvements. Retry HTTP requests without readonly=1 when ClickHouse already enforces read-only mode (Code 164). Add database field to the connection form and ensure packaged binaries are executable. --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 7 +- assets/connection_form.json | 8 ++ manifest.json | 2 +- scripts/package_qext.sh | 4 + src/driver/client.rs | 176 ++++++++++++++++++++++++++++++------ src/rpc/handlers/query.rs | 45 ++------- src/rpc/handlers/schema.rs | 41 +-------- 9 files changed, 179 insertions(+), 108 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b1556b..5d25989 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,7 +82,7 @@ dependencies = [ [[package]] name = "clickhouse-query-ext" -version = "1.0.0" +version = "1.0.1" dependencies = [ "anyhow", "futures", diff --git a/Cargo.toml b/Cargo.toml index d35d01b..904d8b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "clickhouse-query-ext" -version = "1.0.0" +version = "1.0.1" edition = "2024" authors = ["Querya Community"] description = "High-performance ClickHouse driver for Querya Desktop" diff --git a/README.md b/README.md index b56c296..03df55a 100644 --- a/README.md +++ b/README.md @@ -99,9 +99,14 @@ cargo test --verbose --all # Сборка релизного бинарного файла драйвера cargo build --release + +# Упаковка .qext для Querya Desktop (manifest + assets + bin/) +./scripts/package_qext.sh ``` -После успешной сборки исполняемый файл будет доступен по пути `target/release/clickhouse-query-ext`. +После успешной сборки: +- Бинарник: `target/release/clickhouse-query-ext` +- Пакет для установки: `dist/clickhouse-query-ext-1.0.1.qext` (+ `.sha256`) --- diff --git a/assets/connection_form.json b/assets/connection_form.json index 7cec2c8..61bcb6a 100644 --- a/assets/connection_form.json +++ b/assets/connection_form.json @@ -23,6 +23,14 @@ "required": true, "defaultValue": "default" }, + { + "key": "database", + "label": "База данных по умолчанию", + "type": "text", + "required": false, + "defaultValue": "default", + "helperText": "Имя базы ClickHouse для подключения и дерева схемы" + }, { "key": "password", "label": "Пароль", diff --git a/manifest.json b/manifest.json index 6eb143b..32c86eb 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "queryahub.clickhouse-driver", "name": "ClickHouse Database Driver (Analyst Edition)", - "version": "1.0.0", + "version": "1.0.1", "publisher": "Querya Community", "description": "Изолированный нативный Rust-драйвер для аналитической СУБД ClickHouse с полной поддержкой MergeTree, словарей, партиций и SDUI-интроспекции.", "type": "database_driver", diff --git a/scripts/package_qext.sh b/scripts/package_qext.sh index 24f9689..37461d5 100755 --- a/scripts/package_qext.sh +++ b/scripts/package_qext.sh @@ -69,6 +69,10 @@ cp "${BIN_PATH}" "${STAGING_DIR}/bin/${DEST_BIN_NAME}" if [ "${DEST_BIN_NAME}" != "${BIN_NAME}" ] && [ ! -f "${STAGING_DIR}/bin/${BIN_NAME}" ]; then cp "${BIN_PATH}" "${STAGING_DIR}/bin/${BIN_NAME}" fi +chmod +x "${STAGING_DIR}/bin/${DEST_BIN_NAME}" +if [ -f "${STAGING_DIR}/bin/${BIN_NAME}" ]; then + chmod +x "${STAGING_DIR}/bin/${BIN_NAME}" +fi # 3. Create .qext (.zip) archive using python zipfile module or zip CLI ARCHIVE_NAME="clickhouse-query-ext-${VERSION}-${TARGET}.qext" diff --git a/src/driver/client.rs b/src/driver/client.rs index ca2f637..8a04bda 100644 --- a/src/driver/client.rs +++ b/src/driver/client.rs @@ -1,7 +1,9 @@ use crate::error::DriverError; use crate::utils::secret_guard::ConnectionSecretsPool; -use reqwest::{Client, ClientBuilder}; +use reqwest::{Client, ClientBuilder, RequestBuilder}; use serde::Deserialize; +use std::future::Future; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use url::Url; @@ -18,13 +20,16 @@ pub struct ConnectParams { pub readonly: Option, } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct ClickHouseClient { pub connection_id: u64, pub base_url: String, pub user: String, pub database: String, pub readonly: bool, + /// When true, omit `readonly=1` on HTTP requests because the server/user + /// profile already enforces read-only mode. + server_enforces_readonly: AtomicBool, pub http_client: Client, } @@ -52,8 +57,7 @@ impl ClickHouseClient { } else { params.database.unwrap_or_else(|| "default".to_string()) }; - // Safe Mode: default to readonly = true unless explicitly disabled - let readonly = params.readonly.unwrap_or(true); + let readonly = params.readonly.unwrap_or(false); let base = format!("{}://{}:{}", scheme, host, port); (base, user, database, readonly) } @@ -62,7 +66,7 @@ impl ClickHouseClient { let port = params.port.unwrap_or(8123); let user = params.user.unwrap_or_else(|| "default".to_string()); let database = params.database.unwrap_or_else(|| "default".to_string()); - let readonly = params.readonly.unwrap_or(true); + let readonly = params.readonly.unwrap_or(false); ( format!("http://{}:{}", host, port), user, @@ -82,31 +86,51 @@ impl ClickHouseClient { user, database, readonly, + server_enforces_readonly: AtomicBool::new(false), http_client, }) } - /// Check connection health by executing `SELECT version()` against ClickHouse. - /// Utilizes zero-trust `ConnectionSecretsPool` for authentication headers without persisting secrets in struct memory. - pub async fn ping_connection(&self) -> Result { - if self.base_url.starts_with("mock://") || self.base_url.starts_with("test://") { - return Ok("mock-clickhouse-23.8.1.1".to_string()); - } + /// Appends Safe Mode session settings to a ClickHouse HTTP URL. + pub fn append_safe_mode_settings(&self, url: &mut Url) { + self.append_safe_mode_settings_with(url, self.omit_readonly_setting()); + } - let mut url = Url::parse(&self.base_url)?; - url.query_pairs_mut() - .append_pair("query", "SELECT version()") - .append_pair("database", &self.database); - if self.readonly { - url.query_pairs_mut() - .append_pair("readonly", "1") - .append_pair("max_execution_time", "300") - .append_pair("max_memory_usage", "10000000000"); + fn omit_readonly_setting(&self) -> bool { + self.server_enforces_readonly.load(Ordering::Relaxed) + } + + fn append_safe_mode_settings_with(&self, url: &mut Url, omit_readonly_setting: bool) { + if !self.readonly { + return; } + if !omit_readonly_setting { + url.query_pairs_mut().append_pair("readonly", "1"); + } + url.query_pairs_mut() + .append_pair("max_execution_time", "300") + .append_pair("max_memory_usage", "10000000000"); + } - let mut req = self.http_client.get(url); + fn mark_server_readonly_enforced(&self) { + self.server_enforces_readonly + .store(true, Ordering::Relaxed); + } - // Retrieve secret securely just-in-time from ConnectionSecretsPool + /// Returns true when ClickHouse rejects `readonly=1` because the session is + /// already read-only at server/profile level. + pub fn is_readonly_setting_conflict(err: &DriverError) -> bool { + match err { + DriverError::Client(msg) => { + msg.contains("Cannot modify 'readonly' setting in readonly mode") + || msg.contains("Code: 164") + || msg.contains("(READONLY)") + } + _ => false, + } + } + + fn apply_auth(&self, mut req: RequestBuilder) -> RequestBuilder { if let Some(secrets) = ConnectionSecretsPool::global().get(self.connection_id) { if let Some(jwt) = secrets.expose_jwt_token() { req = req.header("Authorization", format!("Bearer {}", jwt)); @@ -120,8 +144,10 @@ impl ClickHouseClient { } else { req = req.header("X-ClickHouse-User", &self.user); } + req + } - let resp = req.send().await?; + async fn read_response(resp: reqwest::Response) -> Result { if !resp.status().is_success() { let status = resp.status(); let text = resp.text().await.unwrap_or_default(); @@ -130,9 +156,81 @@ impl ClickHouseClient { status, text ))); } + Ok(resp.text().await?) + } - let version = resp.text().await?.trim().to_string(); - Ok(version) + pub async fn execute_with_readonly_retry(&self, mut run: F) -> Result + where + F: FnMut(bool) -> Fut, + Fut: Future>, + { + let mut omit = self.omit_readonly_setting(); + loop { + match run(omit).await { + Ok(text) => return Ok(text), + Err(err) if self.readonly && !omit && Self::is_readonly_setting_conflict(&err) => { + self.mark_server_readonly_enforced(); + omit = true; + } + Err(err) => return Err(err), + } + } + } + + /// Executes a GET request with optional SQL in the `query` URL parameter. + pub async fn get_with_query(&self, sql: &str) -> Result { + if self.base_url.starts_with("mock://") || self.base_url.starts_with("test://") { + return Ok("mock-clickhouse-23.8.1.1".to_string()); + } + + self.execute_with_readonly_retry(|omit_readonly| async move { + let mut url = Url::parse(&self.base_url)?; + url.query_pairs_mut() + .append_pair("query", sql) + .append_pair("database", &self.database); + self.append_safe_mode_settings_with(&mut url, omit_readonly); + let req = self.apply_auth(self.http_client.get(url)); + Self::read_response(req.send().await?).await + }) + .await + } + + /// Executes a POST request with SQL in the request body. + pub async fn post_sql( + &self, + sql: &str, + mut extra_params: impl FnMut(&mut Url), + ) -> Result { + if self.base_url.starts_with("mock://") || self.base_url.starts_with("test://") { + return Ok(String::new()); + } + + let sql = sql.to_string(); + let mut omit = self.omit_readonly_setting(); + loop { + let mut url = Url::parse(&self.base_url)?; + url.query_pairs_mut() + .append_pair("database", &self.database); + extra_params(&mut url); + self.append_safe_mode_settings_with(&mut url, omit); + let req = self + .apply_auth(self.http_client.post(url)) + .body(sql.clone()); + match Self::read_response(req.send().await?).await { + Ok(text) => return Ok(text), + Err(err) if self.readonly && !omit && Self::is_readonly_setting_conflict(&err) => { + self.mark_server_readonly_enforced(); + omit = true; + } + Err(err) => return Err(err), + } + } + } + + /// Check connection health by executing `SELECT version()` against ClickHouse. + pub async fn ping_connection(&self) -> Result { + let version = self.get_with_query("SELECT version()").await?; + Ok(version.trim().to_string()) } } @@ -145,6 +243,7 @@ mod tests { let params = ConnectParams { connection_id: 1, connection_string: Some("http://admin@localhost:8123/analytics?readonly=1".to_string()), + readonly: Some(true), ..Default::default() }; let client = ClickHouseClient::from_params(params).unwrap(); @@ -169,7 +268,6 @@ mod tests { #[test] fn test_from_params_safe_mode_aliases() { - // Test parsing safe_mode alias via serde let json_val = serde_json::json!({ "connectionId": 10, "host": "localhost", @@ -188,4 +286,30 @@ mod tests { let client_off = ClickHouseClient::from_params(params_off).unwrap(); assert!(!client_off.readonly); } + + #[test] + fn test_append_safe_mode_settings_omits_readonly_when_server_enforces() { + let client = ClickHouseClient::from_params(ConnectParams { + connection_id: 3, + host: Some("localhost".to_string()), + readonly: Some(true), + ..Default::default() + }) + .unwrap(); + client.mark_server_readonly_enforced(); + + let mut url = Url::parse("http://localhost:8123").unwrap(); + client.append_safe_mode_settings(&mut url); + let query = url.query().unwrap_or_default(); + assert!(!query.contains("readonly=1")); + assert!(query.contains("max_execution_time=300")); + } + + #[test] + fn test_is_readonly_setting_conflict() { + let err = DriverError::Client( + "ClickHouse HTTP error 500 Internal Server Error: Code: 164. DB::Exception: Cannot modify 'readonly' setting in readonly mode. (READONLY)".to_string(), + ); + assert!(ClickHouseClient::is_readonly_setting_conflict(&err)); + } } diff --git a/src/rpc/handlers/query.rs b/src/rpc/handlers/query.rs index fc941e8..a233daa 100644 --- a/src/rpc/handlers/query.rs +++ b/src/rpc/handlers/query.rs @@ -231,44 +231,13 @@ pub async fn handle_query(params: Option) -> Result { } // 3. Real ClickHouse HTTP request - let mut url = Url::parse(&client.base_url)?; - url.query_pairs_mut() - .append_pair("database", &client.database) - .append_pair("query_id", &actual_query_id); - if client.readonly { - url.query_pairs_mut() - .append_pair("readonly", "1") - .append_pair("max_execution_time", "300") - .append_pair("max_memory_usage", "10000000000"); - } - - let mut req = client.http_client.post(url).body(sql_to_run); - - if let Some(secrets) = ConnectionSecretsPool::global().get(client.connection_id) { - if let Some(jwt) = secrets.expose_jwt_token() { - req = req.header("Authorization", format!("Bearer {}", jwt)); - } else if let Some(pass) = secrets.expose_password() { - req = req - .header("X-ClickHouse-User", &client.user) - .header("X-ClickHouse-Key", pass); - } else { - req = req.header("X-ClickHouse-User", &client.user); - } - } else { - req = req.header("X-ClickHouse-User", &client.user); - } - - let resp = req.send().await?; - if !resp.status().is_success() { - let status = resp.status(); - let text = resp.text().await.unwrap_or_default(); - return Err(DriverError::Client(format!( - "ClickHouse SQL error {}: {}", - status, text - ))); - } - - let text = resp.text().await?; + let actual_query_id_for_url = actual_query_id.clone(); + let text = client + .post_sql(&sql_to_run, |url| { + url.query_pairs_mut() + .append_pair("query_id", &actual_query_id_for_url); + }) + .await?; let elapsed = start_time.elapsed().as_millis() as u64; if is_tabular_query { diff --git a/src/rpc/handlers/schema.rs b/src/rpc/handlers/schema.rs index 4c6404a..9457340 100644 --- a/src/rpc/handlers/schema.rs +++ b/src/rpc/handlers/schema.rs @@ -1,11 +1,9 @@ use crate::driver::pool::ConnectionPool; use crate::error::DriverError; use crate::sdui::tree::*; -use crate::utils::secret_guard::ConnectionSecretsPool; use serde::Deserialize; use serde_json::{Value, json}; use tracing::info; -use url::Url; #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -34,44 +32,7 @@ async fn run_introspection_query(connection_id: u64, sql: &str) -> Result Date: Sat, 11 Jul 2026 15:57:10 +0300 Subject: [PATCH 2/2] Fix rustfmt in client.rs and simplify README. Apply cargo fmt fixes for CI and replace the verbose architecture overview with a concise product-focused README. --- README.md | 172 +++++++++++-------------------------------- src/driver/client.rs | 8 +- 2 files changed, 47 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index 03df55a..a4c729a 100644 --- a/README.md +++ b/README.md @@ -1,153 +1,65 @@ -# 🚀 clickhouse-query-ext (Querya ClickHouse Database Extension) +# clickhouse-query-ext [![Rust CI](https://github.com/QueryaHub/clickhouse-query-ext/actions/workflows/rust.yml/badge.svg)](https://github.com/QueryaHub/clickhouse-query-ext/actions/workflows/rust.yml) -[![Rust Edition](https://img.shields.io/badge/Edition-2024-brightgreen.svg)](https://doc.rust-lang.org/edition-guide/rust-2024/) -[![Protocol](https://img.shields.io/badge/Protocol-JSON--RPC%202.0%20over%20NDJSON%20%2F%20stdio-blue.svg)](#architecture) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -**`clickhouse-query-ext`** — это высокопроизводительный, отказоустойчивый асинхронный драйвер и расширение СУБД [ClickHouse](https://clickhouse.com/) для платформы **Querya Desktop (Analyst Edition)**. -Драйвер построен на **Rust (Edition 2024)** и работает как изолированный подпроцесс (Zero-Trust Sandbox), взаимодействуя с хостом Querya через протокол **JSON-RPC 2.0 (NDJSON over `stdin` / `stdout`)**. - ---- - -## 📐 Архитектурная схема расширения - -```mermaid -graph LR - subgraph Querya [Querya Desktop Host] - UI[💻 Generative SDUI] - Bridge[🔌 Bridge Process Manager] - end - - subgraph RustSandbox [clickhouse-query-ext (Rust Subprocess)] - Reader[📥 LineStream stdin] --> Router[🔄 JSON-RPC 2.0 Router] - Router --> Sys[⚙️ system.* Handshake / Ping / Secrets] - Router --> Conn[🔌 db.connect / disconnect] - Router --> Query[📊 db.query / execute / cancel] - Router --> Sdui[🎨 SDUI Tree & Form Schemas] - - Sys --> Pool[🔒 ConnectionSecretsPool zeroize] - Query --> Safe[🛡️ Safe Mode AST Filter & Limits] - Sdui --> Parser[🌳 SYSTEM.* Introspection] - - Router --> Writer[📤 NDJSON stdout Mutex] - Writer --> Bridge - end - - Query -->|HTTP/HTTPS ClickHouse Client| CH[(🌐 ClickHouse Server)] -``` +Расширение базы данных [ClickHouse](https://clickhouse.com/) для [Querya Desktop](https://github.com/QueryaHub). Устанавливается как `.qext`-пакет и добавляет ClickHouse в список подключений: форма настройки, дерево схемы, просмотр таблиц и SQL-редактор. ---- +## Возможности -## ✨ Ключевые возможности и этапы реализации +- Подключение к ClickHouse по HTTP/HTTPS +- Дерево объектов: базы, таблицы, представления, колонки, партиции +- Выполнение SQL-запросов и просмотр результатов +- Аналитический режим (Safe Mode): ограничение опасных операций и лимиты на сессию +- Контекстные действия для таблиц и партиций (статистика, DDL, optimize и др.) -Все 6 этапов технического задания полностью реализованы, проверены и покрыты автоматическими тестами: +## Требования -### 1️⃣ Каркас, асинхронный I/O и NDJSON-транспорт ([Stage 1/6]) -- Чтение потока `stdin` через асинхронный `tokio_util::codec::LinesCodec` без блокировки главного потока. -- Запись ответов в `stdout` через потокобезопасный `Mutex` с автоматической очисткой символов перевода строки (`\n`, `\r\n`), гарантирующая 100% валидный **NDJSON (Newline Delimited JSON)**. -- Разделение быстрых методов (`system.*` — задержка `< 5ms`) и тяжёлых SQL-выборок (выполняются в пуле задач `tokio::spawn`). +- Querya Desktop 2.0+ +- Для сборки из исходников: Rust stable (1.85+) -### 2️⃣ Жизненный цикл, управление секретами и логирование ([Stage 2/6]) -- **`system.handshake`**: обмен версиями и регистрация возможностей драйвера (`db.connect`, `db.query`, `db.getSchemaTree`, `sdui.contextActions` и др.). -- **`system.ping`**: Watchdog-таймер мгновенного ответа для предотвращения зависаний (`result: "pong"`). -- **`system.injectCredentials`**: передача паролей и JWT в изолированный `ConnectionSecretsPool`. -- **Защита памяти (`zeroize` & `secrecy`)**: пароли и токены хранятся в защищённой памяти и зануляются при удалении соединения или аварийном завершении (`clear_all`). -- **Санитазированный логгер ([src/utils/logger.rs](src/utils/logger.rs))**: все логи направляются исключительно в `stderr` с автоматическим маскированием паролей и HTTP-заголовков авторизации. +## Сборка -### 3️⃣ Исполнение SQL, конвертация типов и Safe Mode ([Stage 3/6]) -- **`db.query` / `db.execute` / `db.cancelQuery`**: выполнение SQL-запросов через HTTP API ClickHouse с поддержкой стримингового парсинга формата `FORMAT JSONCompactEachRowWithNamesAndTypes`. -- **Конвертер типов ([src/mapper/types.rs](src/mapper/types.rs))**: полная поддержка `Int64/UInt64/Int128/UInt256`, `Decimal(P, S)`, `DateTime64`, `Array(T)`, `Tuple(...)`, `Map(K, V)`, `Nullable(T)` и `LowCardinality(T)`. Большие числа автоматически сериализуются в строки (`"18446744073709551615"`), предотвращая потерю точности в JS. -- **🛡️ Аналитический Safe Mode (Read-Only)**: - - Пре-фильтрация AST на стороне Rust (мгновенная блокировка `DROP DATABASE`, `TRUNCATE TABLE`, `ALTER ... DROP COLUMN` до отправки на сервер). - - Установка сессионных квот на сервере ClickHouse (`readonly=1`, `max_execution_time=300`, `max_memory_usage=10000000000`). +```bash +cargo fmt --all -- --check +cargo clippy --all-targets --all-features -- -D warnings +cargo test +cargo build --release +./scripts/package_qext.sh +``` -### 4️⃣ Интроспекция схемы и ленивое дерево ([Stage 4/6]) -- **`db.getSchemaTree` & `db.expandTreeNode`**: иерархическая навигация по объектам СУБД с поддержкой ленивой дозагрузки. -- Отображение баз данных (`SYSTEM.databases`), таблиц и представлений (`SYSTEM.tables`), словарей (`SYSTEM.dictionaries` с метриками `HitRate`), колонок (`SYSTEM.columns`) и партиций (`SYSTEM.parts` с расчётом количества строк и сжатого размера на диске). +Артефакты: -### 5️⃣ Генератор SDUI-форм и контекстные действия ([Stage 5/6]) -- **`db.getConnectionFormSchema`**: генерация формы настройки подключения (`assets/connection_form.json`). -- **`sdui.contextActions` ([src/sdui/actions.rs](src/sdui/actions.rs))**: контекстное меню для аналитиков без написания DDL: - - **Для таблиц (`table`)**: выборка *Top 100 Rows*, *Column Statistics* (быстрый профайлер), *Optimize Table (FINAL)*, *Deduplicate*, *Show DDL*. - - **Для партиций (`partition`)**: *Drop Partition*, *Freeze Partition* (создание бэкапа/hardlink), *Detach Partition*. - - **Для баз данных (`database`, `view`)**: мониторинг активных мутаций (`SYSTEM.mutations`) и процессов (`SYSTEM.processes`). +| Файл | Назначение | +|------|------------| +| `target/release/clickhouse-query-ext` | Бинарник драйвера | +| `dist/clickhouse-query-ext-1.0.1.qext` | Пакет для установки в Querya Desktop | +| `dist/clickhouse-query-ext-1.0.1.qext.sha256` | Контрольная сумма | -### 6️⃣ Отказоустойчивость, автовосстановление и Panic Hook ([Stage 6/6]) -- **`std::panic::set_hook`**: перехват любых паник Rust, вывод диагностического отчета в `stderr`, зануление всех секретов в памяти (`zeroize`) и завершение с кодом **`101`** для корректного запуска экспоненциального backoff-перезапуска (`SandboxAutoRecovery`). -- Проверка и восстановление структуры scratch-директорий (`ensure_scratch_directories`) при каждом запуске драйвера. +Установка: **Querya Desktop → Extensions → Install from file** → выбрать `.qext`. ---- +## Кросс-компиляция + +```bash +./scripts/build_cross.sh x86_64-unknown-linux-gnu +./scripts/package_qext.sh --target x86_64-unknown-linux-gnu +``` -## 🛠️ Сборка и тестирование +Поддерживаемые цели: `x86_64-unknown-linux-gnu`, `aarch64-apple-darwin`, `x86_64-pc-windows-msvc`. -### Требования -- **Rust toolchain:** `stable` (edition 2024, Rust 1.85+) -- **OS:** Linux / macOS / Windows +## Релиз -### Команды сборки и проверки +Тег `v*` запускает GitHub Actions: сборка для всех платформ и публикация `.qext` в Releases. ```bash -# Проверка форматирования -cargo fmt --all -- --check +git tag v1.0.1 +git push origin v1.0.1 +``` -# Запуск строгого линтера -cargo clippy --all-targets --all-features -- -D warnings +## Документация -# Запуск полного комплекта unit- и интеграционных тестов (48+ тестов) -cargo test --verbose --all +Подробная спецификация RPC, SDUI и аналитических функций — в каталоге [`docs/`](docs/). -# Сборка релизного бинарного файла драйвера -cargo build --release +## Лицензия -# Упаковка .qext для Querya Desktop (manifest + assets + bin/) -./scripts/package_qext.sh -``` - -После успешной сборки: -- Бинарник: `target/release/clickhouse-query-ext` -- Пакет для установки: `dist/clickhouse-query-ext-1.0.1.qext` (+ `.sha256`) - ---- - -## 📋 Спецификация JSON-RPC методов - -| Метод | Описание | Назначение | -| :--- | :--- | :--- | -| `system.handshake` | Обмен версиями и `capabilities` | Инициализация сессии Querya Host ↔ Rust | -| `system.ping` | Watchdog heartbeat | Быстрая проверка жизнеспособности (`< 5ms`) | -| `system.injectCredentials` | Передача пароля/JWT | Безопасное сохранение в In-Memory Pool | -| `system.shutdown` | Завершение работы | Очистка памяти и выход с кодом `0` | -| `db.connect` | Создание HTTP-клиента | Инициализация TLS и проверка соединения | -| `db.disconnect` | Закрытие сессии | Удаление клиента из глобального пула | -| `db.query` | Выборка строк (`SELECT`) | Возврат `RowCompact` с маппингом типов | -| `db.execute` | Выполнение DDL/DML | Возврат количества затронутых строк (`affectedRows`) | -| `db.cancelQuery` | Отмена запроса (`KILL QUERY`) | Остановка долгих вычислений по `query_id` | -| `db.getSchemaTree` | Список баз данных | Корневой уровень SDUI-дерева | -| `db.expandTreeNode` | Разворачивание узла | Подгрузка таблиц, вьюх, колонок и партиций | -| `db.getConnectionFormSchema`| Форма подключения | Отдача JSON-схемы настроек СУБД | -| `sdui.contextActions` | Контекстное меню | Генерация аналитических команд для UI | - ---- - -## 👥 Структура репозитория - -```text -clickhouse-query-ext/ -├── assets/ -│ ├── connection_form.json # JSON-схема формы подключения -│ └── icon.svg # Иконка расширения -├── docs/ -│ ├── 01_TZ_RUST_ARCHITECTURE.md -│ └── 02_CLICKHOUSE_ANALYST_FEATURES.md -├── src/ -│ ├── main.rs # Точка входа, инициализация Sandbox и асинхронный цикл -│ ├── error.rs # Доменные ошибки DriverError и маппинг в коды JSON-RPC (-3260x) -│ ├── transport/ # Асинхронный NDJSON-транспорт (stdio.rs, framing.rs) -│ ├── rpc/ # Роутер и обработчики JSON-RPC 2.0 -│ ├── driver/ # HTTP/TLS клиент, пул соединений, настройки сессий -│ ├── mapper/ # Парсер типов ClickHouse и Row Format -│ ├── sdui/ # Generative SDUI: дерево, формы и контекстные действия -│ └── utils/ # Panic hook, recovery, zeroize секреты, санитазированный логгер -└── manifest.json # Манифест расширения для Querya Desktop -``` +MIT diff --git a/src/driver/client.rs b/src/driver/client.rs index 8a04bda..ce605cf 100644 --- a/src/driver/client.rs +++ b/src/driver/client.rs @@ -113,8 +113,7 @@ impl ClickHouseClient { } fn mark_server_readonly_enforced(&self) { - self.server_enforces_readonly - .store(true, Ordering::Relaxed); + self.server_enforces_readonly.store(true, Ordering::Relaxed); } /// Returns true when ClickHouse rejects `readonly=1` because the session is @@ -159,7 +158,10 @@ impl ClickHouseClient { Ok(resp.text().await?) } - pub async fn execute_with_readonly_retry(&self, mut run: F) -> Result + pub async fn execute_with_readonly_retry( + &self, + mut run: F, + ) -> Result where F: FnMut(bool) -> Fut, Fut: Future>,