From b4515708b7fa52e92973a503cb91513875e885fb Mon Sep 17 00:00:00 2001 From: wyhaya Date: Wed, 3 Jun 2026 21:36:03 +0800 Subject: [PATCH] Support chDB --- Cargo.lock | 10 + src-crates/chdb/examples/chdb.rs | 16 ++ src-crates/chdb/src/ffi.rs | 86 +++++++++ src-crates/chdb/src/lib.rs | 174 ++++++++++++++++++ .../connection-config/connection_config.rs | 12 ++ src-crates/database/Cargo.toml | 1 + src-crates/database/src/chdb.rs | 66 +++++++ src-crates/database/src/lib.rs | 10 + src-tauri/sql.rs | 4 +- src-web/pages/backup/utils.tsx | 1 + src-web/pages/connections/connections.tsx | 2 + .../pages/connections/options-editor/chdb.tsx | 31 ++++ src-web/pages/connections/utils.ts | 22 +++ src-web/pages/database/ai/services.ts | 3 + src-web/pages/database/db/db.ts | 48 +++++ src-web/pages/database/db/escape.ts | 3 + src-web/pages/database/hooks/use-db.ts | 3 +- src-web/pages/database/hooks/use-store.ts | 1 + .../database/sql-editor/language/index.ts | 1 + src-web/tauri/database/config.ts | 12 ++ src-web/ui/database-icon.tsx | 1 + 21 files changed, 505 insertions(+), 2 deletions(-) create mode 100644 src-crates/chdb/examples/chdb.rs create mode 100644 src-crates/chdb/src/ffi.rs create mode 100644 src-crates/chdb/src/lib.rs create mode 100644 src-crates/database/src/chdb.rs create mode 100644 src-web/pages/connections/options-editor/chdb.tsx diff --git a/Cargo.lock b/Cargo.lock index 21a6554..57bec85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1637,6 +1637,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chdb" +version = "0.0.0" +dependencies = [ + "dylib", + "query", + "tokio", +] + [[package]] name = "chrono" version = "0.4.39" @@ -2412,6 +2421,7 @@ dependencies = [ "batch-insert", "bigquery", "bytes", + "chdb", "clickhouse", "client", "cloudflare-d1", diff --git a/src-crates/chdb/examples/chdb.rs b/src-crates/chdb/examples/chdb.rs new file mode 100644 index 0000000..cc1e5fb --- /dev/null +++ b/src-crates/chdb/examples/chdb.rs @@ -0,0 +1,16 @@ +use chdb::Connection; + +#[tokio::main] +async fn main() { + let conn = Connection::connect(":memory:").await.unwrap(); + + conn.execute(r"create table if not exists test (id UInt32, name String)") + .unwrap(); + conn.execute("insert into test (id, name) values (1, 'Alice')") + .unwrap(); + conn.execute("insert into test (id, name) values (2, 'Bob')") + .unwrap(); + + let query = conn.query(r#"select * from test"#).unwrap(); + dbg!(query); +} diff --git a/src-crates/chdb/src/ffi.rs b/src-crates/chdb/src/ffi.rs new file mode 100644 index 0000000..34664d0 --- /dev/null +++ b/src-crates/chdb/src/ffi.rs @@ -0,0 +1,86 @@ +use dylib::ffi::{ErrorMessage, StringRef, TypedValue}; +use std::ffi::c_void; + +#[repr(C)] +pub struct Meta { + pub column_count: usize, + pub row_count: usize, + pub duration: u32, +} + +#[repr(C)] +pub struct Column { + pub name: StringRef, + pub datatype: StringRef, +} + +#[repr(C)] +#[derive(Debug, PartialEq, Eq)] +#[allow(dead_code)] +pub enum DataKind { + Null, + Bool, + I8, + I16, + I32, + I64, + U8, + U16, + U32, + U64, + F32, + F64, + String, +} + +#[repr(C)] +pub union Data { + pub null: (), + pub bool: bool, + pub i8: i8, + pub i16: i16, + pub i32: i32, + pub i64: i64, + pub u8: u8, + pub u16: u16, + pub u32: u32, + pub u64: u64, + pub f32: f32, + pub f64: f64, + pub string: StringRef, +} + +#[repr(C)] +pub struct ConnectOptions { + pub path: StringRef, +} + +pub const CONNECT: &[u8] = b"df_connect"; +pub type ConnectFn = + extern "C" fn(options: ConnectOptions, error: *mut ErrorMessage) -> *mut c_void; + +pub const CLOSE: &[u8] = b"df_close"; +pub type CloseFn = extern "C" fn(conn: *mut c_void); + +pub const EXECUTE: &[u8] = b"df_execute"; +pub type ExecuteFn = extern "C" fn(conn: *mut c_void, sql: StringRef, error: *mut ErrorMessage); + +pub const QUERY: &[u8] = b"df_query"; +pub type QueryFn = + extern "C" fn(conn: *mut c_void, sql: StringRef, error: *mut ErrorMessage) -> *mut c_void; + +pub const QUERY_META: &[u8] = b"df_query_meta"; +pub type QueryMetaFn = extern "C" fn(query: *mut c_void) -> Meta; + +pub const QUERY_COLUMN: &[u8] = b"df_query_column"; +pub type QueryColumnFn = extern "C" fn(query: *mut c_void, index: usize) -> Column; + +pub const QUERY_VALUE: &[u8] = b"df_query_value"; +pub type QueryValueFn = + extern "C" fn(query: *mut c_void, row: usize, col: usize) -> TypedValue; + +pub const FREE_QUERY: &[u8] = b"df_free_query"; +pub type FreeQueryFn = extern "C" fn(query: *mut c_void); + +pub const FREE_ERROR: &[u8] = b"df_free_error"; +pub type FreeErrorFn = extern "C" fn(error: ErrorMessage); diff --git a/src-crates/chdb/src/lib.rs b/src-crates/chdb/src/lib.rs new file mode 100644 index 0000000..14d4b4a --- /dev/null +++ b/src-crates/chdb/src/lib.rs @@ -0,0 +1,174 @@ +mod ffi; + +use dylib::Dylib; +use dylib::driver::{Error, Result}; +use dylib::ffi::{ErrorMessage, StringRef}; +use ffi::*; +use query::{Query, QueryColumn, Value}; +use std::{ffi::c_void, sync::Mutex}; + +// NOTE: +// Do not update manually +// Use `node ./src-dylib/driver-update.mjs` update the sha256 values. + +const CHDB_DRIVER_VERSION: &str = "20260603"; +#[cfg(all(target_os = "macos", target_arch = "aarch64"))] +const CHDB_SHA256: &str = "82af2fa737c04737b0f15f2deb802338117d7c36279f828220dc2955cfa5c8fc"; +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +const CHDB_SHA256: &str = "ecddfd0861931a9a43c623f76882873f08a3d0a312afc6ac893318237445ca78"; +#[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))] +const CHDB_SHA256: &str = "b60cf82d932fe24d38144513c9669118949a1003eff0325f8252f3096f06ef57"; +#[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] +const CHDB_SHA256: &str = "6e77f2d46e51c71b9740fe50122e660732d325b4507ed9530c28e1decf311384"; +#[cfg(all(target_os = "windows", target_arch = "aarch64", target_env = "msvc"))] +const CHDB_SHA256: &str = ""; // Unsupported platform +#[cfg(all(target_os = "windows", target_arch = "x86_64", target_env = "msvc"))] +const CHDB_SHA256: &str = ""; // Unsupported platform + +#[derive(Debug)] +pub struct Connection { + conn: Mutex<*mut c_void>, + dylib: Dylib, +} + +unsafe impl Send for Connection {} +unsafe impl Sync for Connection {} + +impl Drop for Connection { + fn drop(&mut self) { + let _ = self.close(); + } +} + +fn free_error(dylib: &Dylib, error: ErrorMessage) -> Result, Error> { + if !error.is_null() { + let msg = error.as_str().to_string(); + dylib.symbol::(FREE_ERROR)?(error); + return Ok(Some(Error::Message(msg))); + } + Ok(None) +} + +impl Connection { + pub async fn connect(path: &str) -> Result { + if CHDB_SHA256.is_empty() { + return Err(Error::Message( + "chDB is not supported on the current platform.".into(), + )); + } + let mut error = ErrorMessage::null(); + let dylib = Dylib::try_load("chdb", CHDB_DRIVER_VERSION, CHDB_SHA256).await?; + let options = ConnectOptions { + path: StringRef::new(path), + }; + let conn = dylib.symbol::(CONNECT)?(options, &mut error); + if let Some(err) = free_error(&dylib, error)? { + return Err(err); + } + Ok(Self { + conn: Mutex::new(conn), + dylib, + }) + } + + fn close(&self) -> Result<(), Error> { + let conn = self.conn.lock().map_err(|_| Error::Mutex)?; + self.dylib.symbol::(CLOSE)?(*conn); + Ok(()) + } + + pub fn execute(&self, sql: &str) -> Result<(), Error> { + let conn = self.conn.lock().map_err(|_| Error::Mutex)?; + let mut error = ErrorMessage::null(); + self.dylib.symbol::(EXECUTE)?(*conn, StringRef::new(sql), &mut error); + if let Some(err) = free_error(&self.dylib, error)? { + return Err(err); + } + Ok(()) + } + + pub fn query(&self, sql: &str) -> Result { + let conn = self.conn.lock().map_err(|_| Error::Mutex)?; + let mut error = ErrorMessage::null(); + let query = self.dylib.symbol::(QUERY)?(*conn, StringRef::new(sql), &mut error); + if let Some(err) = free_error(&self.dylib, error)? { + return Err(err); + } + let meta = self.dylib.symbol::(QUERY_META)?(query); + let query_column = self.dylib.symbol::(QUERY_COLUMN)?; + let query_value = self.dylib.symbol::(QUERY_VALUE)?; + let columns = (0..meta.column_count) + .map(|i| { + let col = query_column(query, i); + QueryColumn { + name: col.name.as_str().to_string(), + datatype: col.datatype.as_str().to_string(), + } + }) + .collect::>(); + let mut rows = Vec::with_capacity(meta.row_count); + for y in 0..meta.row_count { + let mut row = Vec::with_capacity(meta.column_count); + for x in 0..meta.column_count { + let data = query_value(query, y, x); + row.push(unsafe { + match data.kind { + DataKind::Null => Value::Null, + DataKind::Bool => Value::Bool(data.value.bool), + DataKind::I8 => Value::I8(data.value.i8), + DataKind::I16 => Value::I16(data.value.i16), + DataKind::I32 => Value::I32(data.value.i32), + DataKind::I64 => Value::I64(data.value.i64), + DataKind::U8 => Value::U8(data.value.u8), + DataKind::U16 => Value::U16(data.value.u16), + DataKind::U32 => Value::U32(data.value.u32), + DataKind::U64 => Value::U64(data.value.u64), + DataKind::F32 => Value::F32(data.value.f32), + DataKind::F64 => Value::F64(data.value.f64), + DataKind::String => Value::String(data.value.string.as_str().to_string()), + } + }); + } + rows.push(row); + } + self.dylib.symbol::(FREE_QUERY)?(query); + Ok(Query { + columns, + rows, + // TODO + rows_affected: None, + duration: meta.duration, + }) + } + + pub fn select(&self, sql: &str) -> Result>, Error> { + self.query(sql).map(|q| q.rows) + } +} + +#[cfg(test)] +mod tests { + use crate::*; + use query::{QueryColumn, Value}; + + #[tokio::test] + async fn test_query() { + let conn = Connection::connect(":memory:").await.unwrap(); + let mut query = conn.query("select 'hello' as hello").unwrap(); + assert_eq!(query.columns.len(), 1); + assert_eq!( + query.columns.remove(0), + QueryColumn { + name: "hello".into(), + datatype: "String".into() + } + ); + assert_eq!(query.rows.len(), 1); + assert_eq!(query.rows[0].len(), 1); + assert_eq!( + query.rows.remove(0).remove(0), + Value::String("hello".into()) + ); + assert_eq!(query.rows_affected, None); + } +} diff --git a/src-crates/connection-config/connection_config.rs b/src-crates/connection-config/connection_config.rs index 32945c2..5d2fdc1 100644 --- a/src-crates/connection-config/connection_config.rs +++ b/src-crates/connection-config/connection_config.rs @@ -18,6 +18,9 @@ pub enum ConnectionConfig { ManticoreSearch(ManticoreSearchConfig), MSSQL(MsSqlConfig), ClickHouse(ClickHouseConfig), + #[serde(rename = "chDB")] + #[strum(serialize = "chDB")] + ChDb(ChDbConfig), Databend(DatabendConfig), BigQuery(BigQueryConfig), Trino(TrinoConfig), @@ -74,6 +77,8 @@ pub enum SqlDatabaseType { ManticoreSearch, MSSQL, ClickHouse, + #[serde(rename = "chDB")] + ChDb, Databend, BigQuery, Trino, @@ -117,6 +122,13 @@ pub struct ClickHouseConfig { pub proxy: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChDbConfig { + pub path: String, + pub readonly: bool, + pub initial: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DatabendConfig { pub protocol: ConnectProtocol, diff --git a/src-crates/database/Cargo.toml b/src-crates/database/Cargo.toml index 14a43f6..10f36fa 100644 --- a/src-crates/database/Cargo.toml +++ b/src-crates/database/Cargo.toml @@ -25,6 +25,7 @@ r2sql = { path = "../r2sql" } mysql = { path = "../mysql" } pgsq = { path = "../pgsq" } duckdb = { path = "../duckdb" } +chdb = { path = "../chdb" } clickhouse = { path = "../clickhouse" } databend = { path = "../databend" } bigquery = { path = "../bigquery" } diff --git a/src-crates/database/src/chdb.rs b/src-crates/database/src/chdb.rs new file mode 100644 index 0000000..a2b9f4d --- /dev/null +++ b/src-crates/database/src/chdb.rs @@ -0,0 +1,66 @@ +use crate::utils::FirstCell; +use crate::{ChDbConfig, ChunkInsert, Database, Result, Value}; +use chdb::Connection; +use query::Query; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct ChDbConnection { + conn: Arc, +} + +impl ChDbConnection { + async fn conn(config: ChDbConfig) -> Result { + let conn = Connection::connect(&config.path).await?; + if let Some(sql) = config.initial { + conn.execute(&sql)?; + } + Ok(conn) + } + + pub(crate) async fn test(config: ChDbConfig) -> Result> { + Self::conn(config) + .await? + .query("SELECT concat('chDB version: ', version());")? + .rows + .first_cell_string() + .map(Some) + } + + pub(crate) async fn connect(config: ChDbConfig) -> Result { + Ok(Database::ChDb(Self { + conn: Arc::new(Self::conn(config).await?), + })) + } + + pub(crate) async fn select(&self, sql: String) -> Result>> { + let query = self.conn.query(&sql)?; + Ok(query.rows) + } + + pub(crate) async fn execute(&self, sql: String) -> Result<()> { + self.conn.execute(&sql)?; + Ok(()) + } + + // TODO + pub(crate) async fn transaction(&self, sqls: Vec) -> Result<()> { + for sql in sqls { + self.conn.execute(&sql)?; + } + Ok(()) + } + + pub(crate) async fn query(&self, sql: String) -> Result { + let query = self.conn.query(&sql)?; + Ok(query) + } + + // TODO + pub(crate) async fn batch_insert(&self, insert: ChunkInsert) -> Result<()> { + for sql in insert { + self.conn.execute(&sql)?; + } + Ok(()) + } +} diff --git a/src-crates/database/src/lib.rs b/src-crates/database/src/lib.rs index 6bcaff1..501a2cc 100644 --- a/src-crates/database/src/lib.rs +++ b/src-crates/database/src/lib.rs @@ -1,4 +1,5 @@ mod bigquery; +mod chdb; mod clickhouse; mod d1; mod databend; @@ -20,6 +21,7 @@ mod utils; mod workers_analytics_engine; use bigquery::BigQueryConnection; +use chdb::ChDbConnection; use clickhouse::ClickHouseConnection; use d1::D1Connection; use databend::DatabendConnection; @@ -60,6 +62,7 @@ pub enum Database { MySql(MySqlConnection), MsSql(MsSqlConnection), ClickHouse(ClickHouseConnection), + ChDb(ChDbConnection), Databend(DatabendConnection), BigQuery(BigQueryConnection), Trino(TrinoConnection), @@ -91,6 +94,7 @@ impl Database { } ConnectionConfig::MSSQL(config) => MsSqlConnection::test(config).await, ConnectionConfig::ClickHouse(config) => ClickHouseConnection::test(config).await, + ConnectionConfig::ChDb(config) => ChDbConnection::test(config).await, ConnectionConfig::Databend(config) => DatabendConnection::test(config).await, ConnectionConfig::BigQuery(config) => BigQueryConnection::test(config).await, ConnectionConfig::Trino(config) => TrinoConnection::test(config).await, @@ -133,6 +137,7 @@ impl Database { } ConnectionConfig::MSSQL(config) => MsSqlConnection::connect(config).await, ConnectionConfig::ClickHouse(config) => ClickHouseConnection::connect(config).await, + ConnectionConfig::ChDb(config) => ChDbConnection::connect(config).await, ConnectionConfig::Databend(config) => DatabendConnection::connect(config).await, ConnectionConfig::BigQuery(config) => BigQueryConnection::connect(config).await, ConnectionConfig::Trino(config) => TrinoConnection::connect(config).await, @@ -168,6 +173,7 @@ impl Database { Self::MySql(db) => db.select(sql).await, Self::MsSql(db) => db.select(sql).await, Self::ClickHouse(db) => db.select(sql).await, + Self::ChDb(db) => db.select(sql).await, Self::Databend(db) => db.select(sql).await, Self::BigQuery(db) => db.select(sql).await, Self::Trino(db) => db.select(sql).await, @@ -192,6 +198,7 @@ impl Database { Self::MySql(db) => db.execute(sql).await, Self::MsSql(db) => db.execute(sql).await, Self::ClickHouse(db) => db.execute(sql).await, + Self::ChDb(db) => db.execute(sql).await, Self::Databend(db) => db.execute(sql).await, Self::BigQuery(db) => db.execute(sql).await, Self::Trino(db) => db.execute(sql).await, @@ -216,6 +223,7 @@ impl Database { Self::MySql(db) => db.transaction(sqls).await, Self::MsSql(db) => db.transaction(sqls).await, Self::ClickHouse(db) => db.transaction(sqls).await, + Self::ChDb(db) => db.transaction(sqls).await, Self::Databend(db) => db.transaction(sqls).await, Self::BigQuery(db) => db.transaction(sqls).await, Self::Trino(db) => db.transaction(sqls).await, @@ -240,6 +248,7 @@ impl Database { Self::MySql(db) => db.query(sql).await, Self::MsSql(db) => db.query(sql).await, Self::ClickHouse(db) => db.query(sql).await, + Self::ChDb(db) => db.query(sql).await, Self::Databend(db) => db.query(sql).await, Self::BigQuery(db) => db.query(sql).await, Self::Trino(db) => db.query(sql).await, @@ -264,6 +273,7 @@ impl Database { Self::MySql(db) => db.batch_insert(insert).await, Self::MsSql(db) => db.batch_insert(insert).await, Self::ClickHouse(db) => db.batch_insert(insert).await, + Self::ChDb(db) => db.batch_insert(insert).await, Self::Databend(db) => db.batch_insert(insert).await, Self::BigQuery(db) => db.batch_insert(insert).await, Self::Trino(db) => db.batch_insert(insert).await, diff --git a/src-tauri/sql.rs b/src-tauri/sql.rs index c568a33..bf88e3a 100644 --- a/src-tauri/sql.rs +++ b/src-tauri/sql.rs @@ -20,7 +20,9 @@ fn to_dialect(db: SqlDatabaseType) -> Box { Box::new(MySqlDialect {}) } SqlDatabaseType::MSSQL => Box::new(MsSqlDialect {}), - SqlDatabaseType::ClickHouse | SqlDatabaseType::Databend => Box::new(ClickHouseDialect {}), + SqlDatabaseType::ClickHouse | SqlDatabaseType::ChDb | SqlDatabaseType::Databend => { + Box::new(ClickHouseDialect {}) + } SqlDatabaseType::DuckDB => Box::new(DuckDbDialect {}), SqlDatabaseType::BigQuery => Box::new(BigQueryDialect {}), // Trino / Presto are ANSI SQL compliant query engines: https://trino.io/docs/current/language.html diff --git a/src-web/pages/backup/utils.tsx b/src-web/pages/backup/utils.tsx index 2798172..5a72546 100644 --- a/src-web/pages/backup/utils.tsx +++ b/src-web/pages/backup/utils.tsx @@ -122,6 +122,7 @@ export const toBackupConfig = async (conn: Connection): Promise => case SqlDatabaseType.ManticoreSearch: case SqlDatabaseType.MsSql: case SqlDatabaseType.ClickHouse: + case SqlDatabaseType.ChDb: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: case SqlDatabaseType.BigQuery: diff --git a/src-web/pages/connections/connections.tsx b/src-web/pages/connections/connections.tsx index b8e59fc..eca0048 100644 --- a/src-web/pages/connections/connections.tsx +++ b/src-web/pages/connections/connections.tsx @@ -18,6 +18,7 @@ import { useConnections } from './hooks' import { ConnectionList } from './list' import { NewConnectionMenu } from './new' import { BigQueryConnection } from './options-editor/bigquery' +import { ChDbConnection } from './options-editor/chdb' import { ClickHouseConnection } from './options-editor/clickhouse' import { CloudflareD1Connection } from './options-editor/cloudflare-d1' import { CloudflareKvConnection } from './options-editor/cloudflare-kv' @@ -127,6 +128,7 @@ const CurrentConnection = (props: ConnectionEditorOptions): JSX.Element => [SqlDatabaseType.ManticoreSearch]: , [SqlDatabaseType.MsSql]: , [SqlDatabaseType.ClickHouse]: , + [SqlDatabaseType.ChDb]: , [SqlDatabaseType.Databend]: , [SqlDatabaseType.Databricks]: , [SqlDatabaseType.BigQuery]: , diff --git a/src-web/pages/connections/options-editor/chdb.tsx b/src-web/pages/connections/options-editor/chdb.tsx new file mode 100644 index 0000000..ba15d36 --- /dev/null +++ b/src-web/pages/connections/options-editor/chdb.tsx @@ -0,0 +1,31 @@ +import { t } from '../../../i18n' +import { ChDbConfig } from '../../../tauri' +import { ConnectionEditorOptions } from '../connections' +import { InitialSQL, DatabasePathSelect, Item, Readonly } from '../from' +import { useOptions } from '../hooks' +import { ConnectionTab } from '../tabs' + +export const ChDbConnection = ({ data, onChange }: ConnectionEditorOptions) => { + const { name, options, setName, setOpt } = useOptions(data, onChange) + + const general = ( + <> + + setOpt('path', path)} /> + + ) + + const security = ( + setOpt('readonly', val)} /> + ) + + const initSQL = ( + setOpt('initial', val)} + /> + ) + + return +} diff --git a/src-web/pages/connections/utils.ts b/src-web/pages/connections/utils.ts index 6ca02e1..7b7c81c 100644 --- a/src-web/pages/connections/utils.ts +++ b/src-web/pages/connections/utils.ts @@ -24,6 +24,7 @@ import { DuckDbConfig, ConnectProtocol, ClickHouseConfig, + ChDbConfig, DatabendConfig, TrinoConfig, TrinoAuthType, @@ -203,6 +204,16 @@ function defaultConfig(type: DatabaseType): DatabaseConfig { } } } + case SqlDatabaseType.ChDb: { + return { + type, + options: { + path: '', + readonly: false, + initial: null + } + } + } case SqlDatabaseType.Databend: { return { type, @@ -487,6 +498,11 @@ export const toConnectionURL = async (conn: Connection) => { opt.path = config.options.path break } + case SqlDatabaseType.ChDb: { + opt.scheme = 'chdb' + opt.path = config.options.path + break + } case SqlDatabaseType.Turso: { opt.scheme = 'turso' switch (config.options.database.type) { @@ -748,6 +764,12 @@ export const parseConnectionURL = async (url: string) => { conn.config.options.path = opt.path return conn } + case 'chdb': { + let conn = createConnectionConfig(SqlDatabaseType.ChDb) as Connection + applyQuery(conn) + conn.config.options.path = opt.path + return conn + } case 'rqlite': { let conn = createConnectionConfig(SqlDatabaseType.Rqlite) as Connection applyQuery(conn) diff --git a/src-web/pages/database/ai/services.ts b/src-web/pages/database/ai/services.ts index a1a4e94..2c0d915 100644 --- a/src-web/pages/database/ai/services.ts +++ b/src-web/pages/database/ai/services.ts @@ -156,6 +156,9 @@ const databaseName = (type: SqlDatabaseType): string => { case SqlDatabaseType.Presto: { return 'PrestoDB' } + case SqlDatabaseType.ChDb: { + return 'ClickHouse(chDB)' + } case SqlDatabaseType.Postgres: case SqlDatabaseType.CockroachDB: case SqlDatabaseType.QuestDB: diff --git a/src-web/pages/database/db/db.ts b/src-web/pages/database/db/db.ts index 1a6aca0..8f25a17 100644 --- a/src-web/pages/database/db/db.ts +++ b/src-web/pages/database/db/db.ts @@ -116,6 +116,7 @@ class Db { case SqlDatabaseType.WorkersAnalyticsEngine: case SqlDatabaseType.R2Sql: case SqlDatabaseType.CockroachDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -150,6 +151,7 @@ class Db { case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: case SqlDatabaseType.MsSql: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -188,6 +190,7 @@ class Db { case SqlDatabaseType.Trino: { return `CREATE SCHEMA ${this.escape.id(schema)};` } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.BigQuery: { @@ -215,6 +218,7 @@ class Db { case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: case SqlDatabaseType.MsSql: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -238,6 +242,7 @@ class Db { await this.execute(`ALTER SCHEMA ${f} RENAME TO ${t};`) return null } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { await this.execute(`RENAME DATABASE ${f} TO ${t};`) return null @@ -275,6 +280,7 @@ class Db { switch (this.type) { case SqlDatabaseType.Postgres: case SqlDatabaseType.CockroachDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Presto: @@ -320,6 +326,7 @@ class Db { case SqlDatabaseType.MsSql: case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.QuestDB: @@ -363,6 +370,7 @@ class Db { } return null } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.BigQuery: { @@ -489,6 +497,7 @@ class Db { case SqlDatabaseType.ManticoreSearch: { return true } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.WorkersAnalyticsEngine: case SqlDatabaseType.R2Sql: @@ -555,6 +564,7 @@ class Db { ? `CREATE TABLE ${newTable} LIKE ${table} WITH DATA;` : `CREATE TABLE ${newTable} LIKE ${table};` } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.WorkersAnalyticsEngine: case SqlDatabaseType.R2Sql: @@ -572,6 +582,7 @@ class Db { case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: case SqlDatabaseType.MsSql: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -607,6 +618,7 @@ class Db { } case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: { sql = `SELECT DATABASE(), schema_name FROM INFORMATION_SCHEMA.SCHEMATA ORDER BY schema_name;` @@ -661,6 +673,7 @@ class Db { case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: case SqlDatabaseType.MsSql: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -730,6 +743,7 @@ class Db { } case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: { sql = `SELECT DATABASE(), schema_name FROM INFORMATION_SCHEMA.SCHEMATA ORDER BY schema_name;` @@ -869,6 +883,7 @@ WHERE c.catalog_name = current_catalog() ORDER BY c.schema_name, t.table_name;` break } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { sql = ` SELECT @@ -1065,6 +1080,7 @@ SELECT DISTINCT 'index', index_name FROM ${set}.INFORMATION_SCHEMA.SEARCH_INDEXE } return struct } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { sql = ` SELECT 'schema' AS type, schema_name AS name FROM INFORMATION_SCHEMA.SCHEMATA @@ -1303,6 +1319,7 @@ SELECT DISTINCT 'column', column_name FROM INFORMATION_SCHEMA.COLUMNS;` ]) ).flat() } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { return (await import('./static/clickhouse-keywords')).default } @@ -1386,6 +1403,7 @@ SELECT DISTINCT 'column', column_name FROM INFORMATION_SCHEMA.COLUMNS;` ) return rows.map(([val]) => val) } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { const rows = await this.select<[string]>(`SELECT DISTINCT name FROM system.functions;`) return rows.map(([val]) => val) @@ -1468,6 +1486,7 @@ UNION SELECT DISTINCT name FROM system.user_functions;` case SqlDatabaseType.MsSql: { return (await import('./static/mssql-datatypes')).default } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { return (await import('./static/clickhouse-datatypes')).default } @@ -1510,6 +1529,7 @@ UNION SELECT DISTINCT name FROM system.user_functions;` case SqlDatabaseType.WorkersAnalyticsEngine: case SqlDatabaseType.R2Sql: case SqlDatabaseType.MsSql: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.QuestDB: @@ -1548,6 +1568,7 @@ UNION SELECT DISTINCT name FROM system.user_functions;` case SqlDatabaseType.MsSql: case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.QuestDB: @@ -1617,6 +1638,7 @@ UNION SELECT DISTINCT name FROM system.user_functions;` case SqlDatabaseType.Trino: { return [DeleteTableType.Delete, DeleteTableType.Truncate, DeleteTableType.Drop] } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.QuestDB: { return [DeleteTableType.Truncate, DeleteTableType.Drop] @@ -1648,6 +1670,7 @@ UNION SELECT DISTINCT name FROM system.user_functions;` } case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.QuestDB: // Workers Analytics Engine / R2 SQL not supported @@ -1706,6 +1729,7 @@ UNION SELECT DISTINCT name FROM system.user_functions;` sql = `ALTER SCHEMA ${to} TRANSFER ${from}.${table};` break } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { sql = `RENAME TABLE ${from}.${table} TO ${to}.${table};` break @@ -1739,6 +1763,7 @@ UNION SELECT DISTINCT name FROM system.user_functions;` case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: case SqlDatabaseType.MsSql: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { return true } @@ -1856,6 +1881,7 @@ WHERE table_schema = ${schema} AND table_name = ${table} ORDER BY ordinal_position;` break } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { sql = ` SELECT @@ -2019,6 +2045,7 @@ WHERE case SqlDatabaseType.QuestDB: case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -2067,6 +2094,7 @@ WHERE case SqlDatabaseType.MariaDB: case SqlDatabaseType.ManticoreSearch: case SqlDatabaseType.MsSql: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -2124,6 +2152,7 @@ WHERE case SqlDatabaseType.SqlCipher: case SqlDatabaseType.CloudflareD1: case SqlDatabaseType.WorkersAnalyticsEngine: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -2241,6 +2270,7 @@ SELECT column_name, referenced_table_schema, referenced_table_name, referenced_c break } // Foreign keys not supported + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.BigQuery: @@ -2381,6 +2411,7 @@ SELECT table_schema, table_name, column_name, referenced_column_name FROM foreig sql = `SELECT kcu.table_schema, kcu.table_name, kcu.column_name, ccu.column_name FROM information_schema.key_column_usage AS kcu JOIN information_schema.referential_constraints AS rc ON kcu.constraint_catalog = rc.constraint_catalog AND kcu.constraint_schema = rc.constraint_schema AND kcu.constraint_name = rc.constraint_name JOIN information_schema.key_column_usage AS ccu ON ccu.constraint_catalog = rc.unique_constraint_catalog AND ccu.constraint_schema = rc.unique_constraint_schema AND ccu.constraint_name = rc.unique_constraint_name AND ccu.ordinal_position = kcu.ordinal_position WHERE ccu.table_catalog = current_catalog() AND ccu.table_schema = ${schema} AND ccu.table_name = ${table};` break } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.BigQuery: @@ -2547,6 +2578,7 @@ WHERE break } // Foreign keys not supported + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.BigQuery: @@ -2659,6 +2691,7 @@ WHERE // Can query column_name: SELECT * from duckdb_constraints; case SqlDatabaseType.DuckDB: // ??? + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -2782,6 +2815,7 @@ WHERE // TODO: DuckDB currently can only get explicitly defined indexes, no good way to directly get the index list, leaving empty for now case SqlDatabaseType.DuckDB: // ??? + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -2987,6 +3021,7 @@ ORDER BY c.table_name, c.ordinal_position;` break } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { sql = ` SELECT @@ -3184,6 +3219,7 @@ ORDER BY c.table_name, c.ordinal_position;` case SqlDatabaseType.R2Sql: case SqlDatabaseType.DuckDB: // Uncertain + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.BigQuery: @@ -3274,6 +3310,7 @@ ORDER BY ROUTINE_NAME;` case SqlDatabaseType.WorkersAnalyticsEngine: case SqlDatabaseType.R2Sql: case SqlDatabaseType.DuckDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.QuestDB: // ??? @@ -3317,6 +3354,7 @@ ORDER BY ROUTINE_NAME;` case SqlDatabaseType.MsSql: // TODO: DuckDB can actually support duckdb_extensions() case SqlDatabaseType.DuckDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -3372,6 +3410,7 @@ ORDER BY e.name;` case SqlDatabaseType.ManticoreSearch: case SqlDatabaseType.MsSql: case SqlDatabaseType.DuckDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -3417,6 +3456,7 @@ ORDER BY e.name;` case SqlDatabaseType.CockroachDB: // DuckDB doesn't support triggers yet https://github.com/duckdb/duckdb/issues/750 case SqlDatabaseType.DuckDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -3562,6 +3602,7 @@ ORDER BY table_name;` } case SqlDatabaseType.CockroachDB: case SqlDatabaseType.DuckDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -3629,6 +3670,7 @@ ORDER BY table_name;` case SqlDatabaseType.CloudflareD1: case SqlDatabaseType.WorkersAnalyticsEngine: case SqlDatabaseType.R2Sql: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -3684,6 +3726,7 @@ ORDER BY table_name;` case SqlDatabaseType.CloudflareD1: case SqlDatabaseType.WorkersAnalyticsEngine: case SqlDatabaseType.R2Sql: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -3745,6 +3788,7 @@ ORDER BY table_name;` return `DROP SEARCH INDEX ${this.escape.id(indexName)} ON ${this.escape.entry(entry)};` } // This should never be called + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -3775,6 +3819,7 @@ ORDER BY table_name;` case SqlDatabaseType.ManticoreSearch: case SqlDatabaseType.MsSql: case SqlDatabaseType.DuckDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -3807,6 +3852,7 @@ ORDER BY table_name;` case SqlDatabaseType.ManticoreSearch: case SqlDatabaseType.MsSql: case SqlDatabaseType.DuckDB: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: @@ -3881,6 +3927,7 @@ ORDER BY table_name;` case SqlDatabaseType.Trino: { return `UPDATE ${t} SET ${v} WHERE ${w};` } + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: { return `ALTER TABLE ${t} UPDATE ${v} WHERE ${w};` } @@ -3906,6 +3953,7 @@ ORDER BY table_name;` case SqlDatabaseType.CloudflareD1: case SqlDatabaseType.WorkersAnalyticsEngine: case SqlDatabaseType.R2Sql: + case SqlDatabaseType.ChDb: case SqlDatabaseType.ClickHouse: case SqlDatabaseType.Databend: case SqlDatabaseType.BigQuery: diff --git a/src-web/pages/database/db/escape.ts b/src-web/pages/database/db/escape.ts index d92b30a..bf12b77 100644 --- a/src-web/pages/database/db/escape.ts +++ b/src-web/pages/database/db/escape.ts @@ -22,6 +22,7 @@ export class Escape { case SqlDatabaseType.DuckDB: case SqlDatabaseType.CockroachDB: case SqlDatabaseType.ClickHouse: + case SqlDatabaseType.ChDb: case SqlDatabaseType.Databend: case SqlDatabaseType.QuestDB: case SqlDatabaseType.Presto: @@ -85,6 +86,7 @@ export class Escape { case SqlDatabaseType.MySql: case SqlDatabaseType.MariaDB: case SqlDatabaseType.ClickHouse: + case SqlDatabaseType.ChDb: case SqlDatabaseType.Databricks: case SqlDatabaseType.Presto: case SqlDatabaseType.Trino: @@ -130,6 +132,7 @@ export class Escape { case SqlDatabaseType.CockroachDB: case SqlDatabaseType.DuckDB: case SqlDatabaseType.ClickHouse: + case SqlDatabaseType.ChDb: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: case SqlDatabaseType.BigQuery: diff --git a/src-web/pages/database/hooks/use-db.ts b/src-web/pages/database/hooks/use-db.ts index 3a6d770..f92631d 100644 --- a/src-web/pages/database/hooks/use-db.ts +++ b/src-web/pages/database/hooks/use-db.ts @@ -47,7 +47,8 @@ const defaultSchema = (config: SqlDatabaseConfig): string => { case SqlDatabaseType.Databricks: { return config.options.schema ?? 'default' } - case SqlDatabaseType.R2Sql: { + case SqlDatabaseType.R2Sql: + case SqlDatabaseType.ChDb: { return 'default' } } diff --git a/src-web/pages/database/hooks/use-store.ts b/src-web/pages/database/hooks/use-store.ts index a7c0122..17d1cf8 100644 --- a/src-web/pages/database/hooks/use-store.ts +++ b/src-web/pages/database/hooks/use-store.ts @@ -79,6 +79,7 @@ const isKv = (type: DatabaseType): boolean => { case SqlDatabaseType.ManticoreSearch: case SqlDatabaseType.MsSql: case SqlDatabaseType.ClickHouse: + case SqlDatabaseType.ChDb: case SqlDatabaseType.Databend: case SqlDatabaseType.Databricks: case SqlDatabaseType.BigQuery: diff --git a/src-web/pages/database/sql-editor/language/index.ts b/src-web/pages/database/sql-editor/language/index.ts index b5120cc..399d2fb 100644 --- a/src-web/pages/database/sql-editor/language/index.ts +++ b/src-web/pages/database/sql-editor/language/index.ts @@ -39,6 +39,7 @@ export const setMonarchTokensProvider = async (language: LanguageHighLight) => { case SqlDatabaseType.Postgres: case SqlDatabaseType.CockroachDB: case SqlDatabaseType.ClickHouse: + case SqlDatabaseType.ChDb: case SqlDatabaseType.QuestDB: case SqlDatabaseType.Databend: { def = (await import('./postgres')).default diff --git a/src-web/tauri/database/config.ts b/src-web/tauri/database/config.ts index 9698275..ac9fe4d 100644 --- a/src-web/tauri/database/config.ts +++ b/src-web/tauri/database/config.ts @@ -144,6 +144,15 @@ export interface ClickHouseConfig { } } +export interface ChDbConfig { + type: SqlDatabaseType.ChDb + options: { + path: string + readonly: boolean + initial: string | null + } +} + export interface DatabendConfig { type: SqlDatabaseType.Databend options: { @@ -364,6 +373,7 @@ export type SqlDatabaseConfig = | ManticoreSearchConfig | MsSqlConfig | ClickHouseConfig + | ChDbConfig | DatabendConfig | BigQueryConfig | TrinoConfig @@ -392,6 +402,7 @@ export const enum SqlDatabaseType { ManticoreSearch = 'Manticore Search', MsSql = 'MSSQL', ClickHouse = 'ClickHouse', + ChDb = 'chDB', Databend = 'Databend', BigQuery = 'BigQuery', Trino = 'Trino', @@ -414,6 +425,7 @@ export const enum KvDatabaseType { export const ALL_DATABASE_TYPE: DatabaseType[] = [ SqlDatabaseType.BigQuery, + SqlDatabaseType.ChDb, SqlDatabaseType.ClickHouse, SqlDatabaseType.CloudflareD1, KvDatabaseType.CloudflareWorkersKv, diff --git a/src-web/ui/database-icon.tsx b/src-web/ui/database-icon.tsx index d043682..1e48531 100644 --- a/src-web/ui/database-icon.tsx +++ b/src-web/ui/database-icon.tsx @@ -102,6 +102,7 @@ export const ConnectionIcon = ({ /> ) case SqlDatabaseType.ClickHouse: + case SqlDatabaseType.ChDb: return (