Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
930d5b3
feat(rpc): implement universal system RPC methods (getServerStats, ge…
ZhuchkaTriplesix Jul 14, 2026
be32466
fix(driver): fix URL and ssl mapping for connections. Closes #43
ZhuchkaTriplesix Jul 30, 2026
33008b1
style(formatting): run cargo fmt to satisfy CI
ZhuchkaTriplesix Jul 30, 2026
065020e
style(clippy): fix collapsible_if warnings
ZhuchkaTriplesix Jul 30, 2026
5860467
Merge pull request #44 from QueryaHub/issue/43-fix-connection-parsing
ZhuchkaTriplesix Jul 30, 2026
88dc02a
fix(packaging): handle Windows binary extension (.exe) in manifest an…
ZhuchkaTriplesix Sep 27, 2026
6d6b9a2
Merge pull request #61 from QueryaHub/issue/45-handle-windows-binary-…
ZhuchkaTriplesix Sep 27, 2026
7439eca
feat(manifest): expand engines.querya_desktop compatibility range. Cl…
ZhuchkaTriplesix Sep 27, 2026
602d812
Merge pull request #62 from QueryaHub/issue/46-expand-engines-querya-…
ZhuchkaTriplesix Sep 27, 2026
d1ed268
fix(security): prevent SQL injection in schema introspection and cont…
ZhuchkaTriplesix Sep 27, 2026
1fa8415
fix(security): sanitize and validate queryId and mutationId in cancel…
ZhuchkaTriplesix Sep 27, 2026
751393e
fix(security): prevent Safe Mode precheck bypass on multi-statement S…
ZhuchkaTriplesix Sep 27, 2026
6158e2c
fix(security): isolate temporary scratch directory per user and restr…
ZhuchkaTriplesix Sep 27, 2026
d56eca8
Merge pull request #66 from QueryaHub/issue/55-secure-scratch-directory
ZhuchkaTriplesix Sep 28, 2026
6771ec3
fix(schema): implement robust delimiter escaping for nodeId path segm…
ZhuchkaTriplesix Sep 28, 2026
5c63ae2
Merge pull request #67 from QueryaHub/issue/56-nodeid-delimiter-escaping
ZhuchkaTriplesix Sep 28, 2026
e6b6888
fix(schema): fix format mismatch in getObjectMetadata and getServerSt…
ZhuchkaTriplesix Sep 28, 2026
4b3a32a
Merge pull request #68 from QueryaHub/issue/57-fix-format-mismatch-sc…
ZhuchkaTriplesix Sep 28, 2026
420c0f9
fix(query): support CTE WITH queries, leading comments, and trailing …
ZhuchkaTriplesix Sep 28, 2026
75ddfbb
Merge pull request #69 from QueryaHub/issue/58-cte-comments-trailing-…
ZhuchkaTriplesix Sep 28, 2026
7d6961e
test(query): add regression coverage for escaped/doubled quote handli…
ZhuchkaTriplesix Sep 29, 2026
93a9f4e
Merge pull request #70 from QueryaHub/issue/59-escaped-doubled-quote-…
ZhuchkaTriplesix Sep 29, 2026
129b515
fix(actions): use type-appropriate Column Profiler query for Array/Ma…
ZhuchkaTriplesix Sep 29, 2026
f84a3cf
Merge pull request #71 from QueryaHub/issue/60-column-profiler-comple…
ZhuchkaTriplesix Sep 29, 2026
ee6bbbf
perf(query): enforce limit parameter and reduce compact row parsing a…
ZhuchkaTriplesix Sep 29, 2026
01250a8
Merge pull request #72 from QueryaHub/issue/47-enforce-query-limit-an…
ZhuchkaTriplesix Sep 29, 2026
ae71272
feat(rpc): support Command Palette actions via commands.execute and m…
ZhuchkaTriplesix Sep 29, 2026
15c8f6b
Merge pull request #73 from QueryaHub/issue/48-command-palette-actions
ZhuchkaTriplesix Sep 29, 2026
0b0e8ad
perf(driver): stream ClickHouse HTTP responses instead of full-text b…
ZhuchkaTriplesix Sep 29, 2026
3b12c8b
Merge pull request #74 from QueryaHub/issue/49-stream-clickhouse-http…
ZhuchkaTriplesix Sep 29, 2026
5946303
perf(transport): eliminate full-string heap reallocation in write_ndj…
ZhuchkaTriplesix Sep 29, 2026
2dd17bc
Merge pull request #75 from QueryaHub/issue/50-write-ndjson-zero-allo…
ZhuchkaTriplesix Sep 29, 2026
01c7db7
perf(safemode): zero-allocation token scanner for Safe Mode SQL prech…
ZhuchkaTriplesix Sep 29, 2026
d444071
Merge pull request #76 from QueryaHub/issue/51-safemode-zero-alloc-pr…
ZhuchkaTriplesix Sep 29, 2026
6d42f67
chore(release): bump version to 1.0.2
ZhuchkaTriplesix Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
[package]
name = "clickhouse-query-ext"
version = "1.0.1"
version = "1.0.2"
edition = "2024"
authors = ["Querya Community"]
description = "High-performance ClickHouse driver for Querya Desktop"

[dependencies]
# Async runtime & I/O
tokio = { version = "1", features = ["rt-multi-thread", "io-std", "sync", "macros", "time", "net"] }
tokio-util = { version = "0.7", features = ["codec"] }
tokio-util = { version = "0.7", features = ["codec", "io"] }
futures = "0.3"
bytes = "1"

# HTTP & TLS for ClickHouse API
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] }
Expand All @@ -32,6 +33,9 @@ regex = "1"
thiserror = "2.0"
anyhow = "1.0"

[target.'cfg(unix)'.dependencies]
libc = "0.2"

[profile.release]
lto = true
strip = true
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ cargo build --release
| Файл | Назначение |
|------|------------|
| `target/release/clickhouse-query-ext` | Бинарник драйвера |
| `dist/clickhouse-query-ext-1.0.1.qext` | Пакет для установки в Querya Desktop |
| `dist/clickhouse-query-ext-1.0.1.qext.sha256` | Контрольная сумма |
| `dist/clickhouse-query-ext-1.0.2.qext` | Пакет для установки в Querya Desktop |
| `dist/clickhouse-query-ext-1.0.2.qext.sha256` | Контрольная сумма |

Установка: **Querya Desktop → Extensions → Install from file** → выбрать `.qext`.

Expand Down
26 changes: 24 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"id": "queryahub.clickhouse-driver",
"name": "ClickHouse Database Driver (Analyst Edition)",
"version": "1.0.1",
"version": "1.0.2",
"publisher": "Querya Community",
"description": "Изолированный нативный Rust-драйвер для аналитической СУБД ClickHouse с полной поддержкой MergeTree, словарей, партиций и SDUI-интроспекции.",
"type": "database_driver",
"main": "bin/clickhouse_rpc_server",
"icon": "assets/icon.svg",
"engines": {
"querya_desktop": "^2.0.0"
"querya_desktop": ">=0.4.0 <3.0.0"
},
"capabilities": {
"databaseDriver": true,
Expand Down Expand Up @@ -41,6 +41,28 @@
"defaultPort": 8123,
"connectionFormSchema": "assets/connection_form.json"
}
],
"commands": [
{
"id": "clickhouse.optimizeFinal",
"title": "ClickHouse: Optimize Table (FINAL)",
"category": "ClickHouse"
},
{
"id": "clickhouse.deduplicate",
"title": "ClickHouse: Deduplicate Table",
"category": "ClickHouse"
},
{
"id": "clickhouse.serverStats",
"title": "ClickHouse: Show Server Statistics",
"category": "ClickHouse"
},
{
"id": "clickhouse.dropPartition",
"title": "ClickHouse: Drop Selected Partition",
"category": "ClickHouse"
}
]
}
}
18 changes: 18 additions & 0 deletions scripts/package_qext.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,29 @@ mkdir -p "${STAGING_DIR}/bin" "${STAGING_DIR}/assets"
cp manifest.json "${STAGING_DIR}/"
cp -r assets/* "${STAGING_DIR}/assets/"

# For Windows targets, adjust manifest.json main entry point to include .exe extension
if [[ "$TARGET" == *"windows"* ]] || [[ "$DEST_BIN_NAME" == *".exe" ]]; then
python3 -c "
import json, sys
manifest_path = sys.argv[1]
dest_bin = sys.argv[2]
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
manifest['main'] = f'bin/{dest_bin}'
with open(manifest_path, 'w', encoding='utf-8') as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
" "${STAGING_DIR}/manifest.json" "${DEST_BIN_NAME}"
fi

# 2. Copy binary into bin/ under both the manifest main entry and original name
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
# On Windows packages, also provide the extensionless binary name for backward compatibility
if [[ "$DEST_BIN_NAME" == *".exe" ]] && [ ! -f "${STAGING_DIR}/bin/clickhouse_rpc_server" ]; then
cp "${BIN_PATH}" "${STAGING_DIR}/bin/clickhouse_rpc_server" 2>/dev/null || true
fi
chmod +x "${STAGING_DIR}/bin/${DEST_BIN_NAME}"
if [ -f "${STAGING_DIR}/bin/${BIN_NAME}" ]; then
chmod +x "${STAGING_DIR}/bin/${BIN_NAME}"
Expand Down
75 changes: 65 additions & 10 deletions src/driver/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ pub struct ConnectParams {
pub connection_string: Option<String>,
pub host: Option<String>,
pub port: Option<u16>,
#[serde(alias = "username")]
pub user: Option<String>,
pub database: Option<String>,
#[serde(alias = "safe_mode", alias = "safeMode")]
pub readonly: Option<bool>,
#[serde(alias = "sslMode")]
pub ssl_mode: Option<String>,
}

#[derive(Debug)]
Expand All @@ -44,8 +47,17 @@ impl ClickHouseClient {
} else {
let parsed = Url::parse(&cs)?;
let host = parsed.host_str().unwrap_or("localhost");
let port = parsed.port().unwrap_or(8123);
let scheme = parsed.scheme();
let port_str = match parsed.port() {
Some(p) => format!(":{}", p),
None => {
if scheme == "http" {
":8123".to_string()
} else {
String::new()
}
}
};
let user = if !parsed.username().is_empty() {
parsed.username().to_string()
} else {
Expand All @@ -58,17 +70,24 @@ impl ClickHouseClient {
params.database.unwrap_or_else(|| "default".to_string())
};
let readonly = params.readonly.unwrap_or(false);
let base = format!("{}://{}:{}", scheme, host, port);
let base = format!("{}://{}{}", scheme, host, port_str);
(base, user, database, readonly)
}
} else {
let host = params.host.unwrap_or_else(|| "localhost".to_string());
let port = params.port.unwrap_or(8123);
let scheme = match params.ssl_mode.as_deref() {
Some("prefer") | Some("require") => "https",
_ => "http",
};
let mut port = params.port.unwrap_or(8123);
if scheme == "https" && port == 8123 {
port = 8443;
}
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(false);
(
format!("http://{}:{}", host, port),
format!("{}://{}:{}", scheme, host, port),
user,
database,
readonly,
Expand Down Expand Up @@ -146,14 +165,15 @@ impl ClickHouseClient {
req
}

async fn error_from_response(resp: reqwest::Response) -> DriverError {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
DriverError::Client(format!("ClickHouse HTTP error {}: {}", status, text))
}

async fn read_response(resp: reqwest::Response) -> Result<String, DriverError> {
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(DriverError::Client(format!(
"ClickHouse HTTP error {}: {}",
status, text
)));
return Err(Self::error_from_response(resp).await);
}
Ok(resp.text().await?)
}
Expand Down Expand Up @@ -229,6 +249,41 @@ impl ClickHouseClient {
}
}

/// Like `post_sql`, but on success returns the raw streaming `reqwest::Response`
/// instead of buffering the whole body into a `String`, so a large analytical
/// result set can be parsed incrementally as it arrives over the network via
/// `crate::driver::streaming::stream_compact_output` (issue #49). Callers must
/// gate on mock/test connections themselves, same as before calling `post_sql`.
pub async fn post_sql_response(
&self,
sql: &str,
mut extra_params: impl FnMut(&mut Url),
) -> Result<reqwest::Response, DriverError> {
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());
let resp = req.send().await?;
if resp.status().is_success() {
return Ok(resp);
}
let err = Self::error_from_response(resp).await;
if self.readonly && !omit && Self::is_readonly_setting_conflict(&err) {
self.mark_server_readonly_enforced();
omit = true;
continue;
}
return Err(err);
}
}

/// Check connection health by executing `SELECT version()` against ClickHouse.
pub async fn ping_connection(&self) -> Result<String, DriverError> {
let version = self.get_with_query("SELECT version()").await?;
Expand Down
1 change: 1 addition & 0 deletions src/driver/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! ClickHouse HTTP client and session management.
pub mod client;
pub mod pool;
pub mod streaming;
Loading
Loading