diff --git a/README.md b/README.md index 7faa376..1bff5f9 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ ### 📊 结构化数据可视化 - **JSON / XML / YAML** —— 可折叠**层级树**,以及卡片 + 连线的**关系图**两种可视化 -- **SQL** —— 插件式执行器(内存库 / `.sqlite` 文件 / **MySQL**,可在设置中配置连接、运行时选择数据源),结果渲染为**表格**,失败显示具体错误;执行历史与实时运行一致 +- **SQL** —— 插件式执行器(内存库 / `.sqlite` 文件 / **MySQL** / **PostgreSQL** / **ClickHouse** / **DuckDB**,可在设置中配置连接、运行时选择数据源),结果渲染为**表格**,失败显示具体错误;执行历史与实时运行一致 - **图表可视化** —— SQL 结果一键切换为图表:**拖拽**字段到「维度 / 指标」即可成图,自动识别数值列,支持聚合(求和/计数/平均/最大/最小)、排序、Top N。基于 ECharts,内置 **27 种**图表:柱状图 · 折线图 · 面积图 · 饼图/环形图 · 玫瑰图 · 散点图 · 涟漪散点图 · 雷达图 · 漏斗图 · 热力图 · 仪表盘 · 桑基图 · 关系图 · 旭日图 · 矩形树图 · 树图 · 箱线图 · K 线图 · 平行坐标 · 主题河流 · 日历热力图 · 极坐标柱状图 · 象形柱图 · 词云 · 水球图 · 中国地图 · 世界地图(配色跟随主题,支持导出 PNG)。配置面板表驱动,组件与数据源解耦,后续 CSV 等本地数据可复用 - **CSV / TSV** —— 解析为**数据表**(支持引号转义、字段内换行、自动识别分隔符、Web Worker 后台解析 + 进度),并可一键切换为上述 27 种图表(与 SQL 共用图表面板) - **Excel(.xlsx / .xls)** —— 用 SheetJS 解析,**多工作表**切换,同样可切表格 / 27 种图表 / 导出 CSV @@ -84,6 +84,7 @@ + @@ -106,6 +107,7 @@ + @@ -120,7 +122,7 @@
-`Python` · `Node.js` · `TypeScript` · `JavaScript` · `Go` · `Rust` · `Java` · `Kotlin` · `Scala` · `Groovy` · `Clojure` · `C` · `C++` · `Objective-C/C++` · `Swift` · `Ruby` · `PHP` · `R` · `Lua` · `Haskell` · `Cangjie` · `Shell` · `AppleScript` · `SQL` · `HTML` · `CSS` · `SVG` · `JSON` · `XML` · `YAML` · `Markdown` · `CSV` · `TSV` · `Excel` · `Text` +`Python` · `Node.js` · `TypeScript` · `JavaScript` · `React` · `Go` · `Rust` · `Java` · `Kotlin` · `Scala` · `Groovy` · `Clojure` · `C` · `C++` · `Objective-C/C++` · `Swift` · `Ruby` · `PHP` · `R` · `Lua` · `Haskell` · `Cangjie` · `Shell` · `AppleScript` · `SQL` · `HTML` · `CSS` · `Less` · `SVG` · `JSON` · `XML` · `YAML` · `Markdown` · `CSV` · `TSV` · `Excel` · `Text`
@@ -152,7 +154,7 @@ pnpm tauri build | 层 | 技术 | | --- | --- | | 前端 | Vue 3 · TypeScript · Tailwind CSS · CodeMirror 6 · ECharts | -| 后端 | Rust · Tauri 2(rusqlite · mysql) | +| 后端 | Rust · Tauri 2(rusqlite · mysql · postgres · clickhouse(HTTP)) | | 存储 | SQLite(执行历史 / AI 对话 / 代码片段 / 应用配置统一入库) | | 架构 | 插件化语言支持系统 · 插件式数据库执行器 · 可复用图表组件 · LSP 桥接 | diff --git a/package.json b/package.json index b3c0f68..37b63fe 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "vscode-languageserver-protocol": "^3.18.0", "vue": "^3.5.13", "vue-codemirror": "^6.1.1", + "vue-i18n": "^11.4.5", "vue3-markdown-it": "^1.0.10", "xlsx": "^0.18.5" }, diff --git a/public/icons/duckdb.svg b/public/icons/duckdb.svg new file mode 100644 index 0000000..15a4e6b --- /dev/null +++ b/public/icons/duckdb.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/icons/less.svg b/public/icons/less.svg new file mode 100644 index 0000000..4a5a199 --- /dev/null +++ b/public/icons/less.svg @@ -0,0 +1,10 @@ + + + + + + + + + Less + diff --git a/public/icons/react.svg b/public/icons/react.svg new file mode 100644 index 0000000..85fad80 --- /dev/null +++ b/public/icons/react.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f4f47d7..cf72807 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "async-trait", "chrono", "dirs", + "duckdb", "fern", "fix-path-env", "flate2", @@ -17,6 +18,7 @@ dependencies = [ "mysql", "notify", "portable-pty", + "postgres", "regex", "reqwest 0.11.27", "rfd 0.15.4", @@ -33,9 +35,10 @@ dependencies = [ "tauri-plugin-shell", "tempfile", "tokio", + "ureq", "uuid", "xz2", - "zip", + "zip 2.4.2", "zstd", ] @@ -65,6 +68,17 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -72,6 +86,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", + "const-random", + "getrandom 0.3.3", "once_cell", "version_check", "zerocopy", @@ -137,6 +153,170 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "arrow" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb98341a7e051bb79731ecb33ec00cbd6e0e315a542d6732b46d462c9215ea2" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce4751cbc4bcccfeeea79df9571ff1dc066d61e44723c7604d11c7937f5b560" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num", +] + +[[package]] +name = "arrow-array" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02ccba2e977a3aabb4384036109ca32f552399a2bc0588f925f91ed073ce70c" +dependencies = [ + "ahash 0.8.12", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.16.1", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90f8bece6a9ee316a699fbbfde368a206676a1206ce89b50f07937648e76c3c" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-cast" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ffe645cfb4e80b1ca37a3a106ce7b4af66ccdd60c655a57e6b9aab096164a7" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "atoi", + "base64 0.22.1", + "chrono", + "comfy-table", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-data" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78468c813909465dd0f858950c8a0614eb63608134acf95c602ec21381258b28" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num", +] + +[[package]] +name = "arrow-ord" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aed58a38c3db0a2cf75ef70e3cb6bc4bd0da0a3d390de37c36139b31fae826e8" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "079ced0517daf4f09b070d09ff641cee7cc331aa216bebcb25d1a6474ad53086" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a0d5eb3fe25337ff83e8333a08379bdd1540b0961b1c888f6e505d971c198e1" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "arrow-select" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2368a78bd32902dba39d52519d70f63799c8b5dc8a9477129a30c2fd3dc70c19" +dependencies = [ + "ahash 0.8.12", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num", +] + +[[package]] +name = "arrow-string" +version = "56.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dece58a130b9187756ded8bc071bd8ee9dd7a146566af244b297c7e632fd1ef7" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num", + "regex", + "regex-syntax", +] + [[package]] name = "ashpd" version = "0.11.0" @@ -335,6 +515,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -444,6 +633,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -484,6 +685,30 @@ dependencies = [ "piper", ] +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "brotli" version = "8.0.1" @@ -526,6 +751,28 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.23.2" @@ -633,6 +880,12 @@ dependencies = [ "toml 0.9.5", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.63" @@ -748,6 +1001,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "comfy-table" +version = "7.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d05af1e006a2407bedef5af410552494ce5be9090444dbbcb57258c1af3d56" +dependencies = [ + "strum 0.26.3", + "strum_macros 0.26.4", + "unicode-width", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -757,6 +1021,26 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -925,6 +1209,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1226,6 +1516,23 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" +[[package]] +name = "duckdb" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8685352ce688883098b61a361e86e87df66fc8c444f4a2411e884c16d5243a65" +dependencies = [ + "arrow", + "cast", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink 0.10.0", + "libduckdb-sys", + "num-integer", + "rust_decimal", + "strum 0.27.2", +] + [[package]] name = "dunce" version = "1.0.5" @@ -1347,6 +1654,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -1439,6 +1752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "libz-sys", "miniz_oxide", ] @@ -1527,6 +1841,12 @@ dependencies = [ "libc", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-channel" version = "0.3.31" @@ -1534,6 +1854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1728,8 +2049,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1919,11 +2242,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -1931,7 +2269,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -1945,6 +2283,12 @@ dependencies = [ "foldhash 0.1.5", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashbrown" version = "0.17.1" @@ -1960,6 +2304,15 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -2129,6 +2482,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.3.1", + "hyper 1.6.0", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.7", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -2569,6 +2938,63 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + [[package]] name = "libappindicator" version = "0.9.0" @@ -2608,6 +3034,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libduckdb-sys" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78bacb8933586cee3b550c39b610d314f9b7a48701ac7a914a046165a4ad8da" +dependencies = [ + "cc", + "flate2", + "pkg-config", + "reqwest 0.12.28", + "serde", + "serde_json", + "tar", + "vcpkg", + "zip 6.0.0", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2628,6 +3071,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.9" @@ -2650,6 +3099,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" +dependencies = [ + "zlib-rs", +] + [[package]] name = "libz-sys" version = "1.1.29" @@ -2698,6 +3156,12 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "lzma-rs" version = "0.3.0" @@ -2730,6 +3194,16 @@ dependencies = [ "web_atoms", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.5" @@ -2993,6 +3467,20 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3003,6 +3491,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3018,6 +3515,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3025,6 +3544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3630,6 +4150,49 @@ dependencies = [ "winreg 0.10.1", ] +[[package]] +name = "postgres" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c48ece1c6cda0db61b058c1721378da76855140e9214339fa1317decacb176" +dependencies = [ + "bytes", + "fallible-iterator 0.2.0", + "futures-util", + "log", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491" +dependencies = [ + "base64 0.22.1", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "hmac", + "md-5", + "memchr", + "rand 0.9.2", + "sha2", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" +dependencies = [ + "bytes", + "fallible-iterator 0.2.0", + "postgres-protocol", +] + [[package]] name = "potential_utf" version = "0.1.2" @@ -3721,22 +4284,97 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quick-xml" version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.0", + "thiserror 2.0.12", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ - "memchr", + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "web-time", ] [[package]] -name = "quick-xml" -version = "0.38.1" +name = "quinn-udp" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "memchr", + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.0", + "tracing", + "windows-sys 0.60.2", ] [[package]] @@ -3754,6 +4392,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.6" @@ -3888,6 +4532,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.27" @@ -3930,6 +4583,46 @@ dependencies = [ "winreg 0.50.0", ] +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.7", +] + [[package]] name = "reqwest" version = "0.13.4" @@ -4026,6 +4719,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -4033,13 +4755,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" dependencies = [ "bitflags 2.12.1", - "fallible-iterator", + "fallible-iterator 0.3.0", "fallible-streaming-iterator", - "hashlink", + "hashlink 0.9.1", "libsqlite3-sys", "smallvec", ] +[[package]] +name = "rust_decimal" +version = "1.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.6", + "rkyv", + "serde", + "serde_json", + "wasm-bindgen", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -4083,6 +4822,7 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -4113,6 +4853,7 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ + "web-time", "zeroize", ] @@ -4227,6 +4968,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.11.1" @@ -4585,6 +5332,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "siphasher" version = "1.0.1" @@ -4707,6 +5460,17 @@ dependencies = [ "quote", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strip-ansi-escapes" version = "0.2.1" @@ -4722,6 +5486,46 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.104", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "subprocess" version = "0.2.9" @@ -4756,6 +5560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", + "quote", "unicode-ident", ] @@ -4881,6 +5686,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -5315,6 +6126,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -5325,6 +6145,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.47.1" @@ -5352,6 +6187,42 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-postgres" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b40d66d9b2cfe04b628173409368e58247e8eddbbd3b0e6c6ba1d09f20f6c9e" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator 0.2.0", + "futures-channel", + "futures-util", + "log", + "parking_lot", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.9.2", + "socket2 0.6.0", + "tokio", + "tokio-util", + "whoami", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -5663,24 +6534,67 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + [[package]] name = "url" version = "2.5.4" @@ -5816,6 +6730,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.122" @@ -5825,6 +6745,7 @@ dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] @@ -5967,6 +6888,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.4" @@ -6087,6 +7018,17 @@ dependencies = [ "windows-core", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" @@ -6668,6 +7610,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" @@ -6916,6 +7867,26 @@ dependencies = [ "zstd", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap 2.14.0", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" + [[package]] name = "zopfli" version = "0.8.3" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2206837..ef05be5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -36,6 +36,8 @@ rfd = "0.15" fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } async-trait = "0.1" mysql = { version = "25", default-features = false, features = ["minimal", "rustls-tls"] } +postgres = "0.19" +ureq = { version = "2", features = ["tls"] } # 固定 mysql_common 的构建依赖 subprocess 到不依赖 let 链的旧版,兼容当前 Rust 工具链 subprocess = "=0.2.9" portable-pty = "0.8" @@ -45,3 +47,8 @@ tar = "0.4" xz2 = "0.1" zstd = "0.13" notify = "6" + +# DuckDB 走 bundled 编译,其 vendored C++(fmt) 在部分新版 MSVC 上编译失败, +# 故仅在非 Windows 平台启用;Windows 不提供 DuckDB 数据源。 +[target.'cfg(not(target_os = "windows"))'.dependencies] +duckdb = { version = "1", features = ["bundled"] } diff --git a/src-tauri/src/db/clickhouse.rs b/src-tauri/src/db/clickhouse.rs new file mode 100644 index 0000000..26543ad --- /dev/null +++ b/src-tauri/src/db/clickhouse.rs @@ -0,0 +1,90 @@ +use super::{DataSource, DbExecutor, SqlResultSet, SqlRunResult, split_sql}; +use serde_json::Value as JsonValue; + +pub(crate) struct ClickhouseExecutor; + +impl DbExecutor for ClickhouseExecutor { + fn handles(&self, kind: &str) -> bool { + kind == "clickhouse" + } + + fn run(&self, sql: &str, source: &DataSource) -> SqlRunResult { + let mut result = SqlRunResult::new(); + + let host = source.host.as_deref().unwrap_or("127.0.0.1"); + let port = source.port.unwrap_or(8123); + let database = source.database.as_deref().unwrap_or("default"); + let user = source.user.as_deref().unwrap_or("default"); + // 走 HTTP 接口,SELECT 以 JSONCompact 返回,DDL/写入返回空体 + let url = format!( + "http://{}:{}/?default_format=JSONCompact&database={}", + host, + port, + urlencode(database) + ); + + for stmt in split_sql(sql) { + let mut req = ureq::post(&url).set("X-ClickHouse-User", user); + if let Some(pwd) = source.password.as_deref() { + req = req.set("X-ClickHouse-Key", pwd); + } + match req.send_string(&stmt) { + Ok(resp) => { + let body = resp.into_string().unwrap_or_default(); + let trimmed = body.trim_start(); + if trimmed.starts_with('{') { + match parse_json_compact(&body) { + Some(rs) => result.result_sets.push(rs), + None => result.messages.push("OK".to_string()), + } + } else { + result.messages.push("OK".to_string()); + } + } + Err(ureq::Error::Status(code, resp)) => { + let msg = resp.into_string().unwrap_or_default(); + result.error = Some(format!("ClickHouse 错误 {}: {}", code, msg.trim())); + break; + } + Err(e) => { + result.error = Some(format!("连接 ClickHouse 失败: {}", e)); + break; + } + } + } + result + } +} + +/// 解析 JSONCompact 响应:{"meta":[{"name":..}], "data":[[..],..]} +fn parse_json_compact(body: &str) -> Option { + let v: JsonValue = serde_json::from_str(body).ok()?; + let meta = v.get("meta")?.as_array()?; + let data = v.get("data")?.as_array()?; + let columns: Vec = meta + .iter() + .map(|m| { + m.get("name") + .and_then(|n| n.as_str()) + .unwrap_or("") + .to_string() + }) + .collect(); + let rows: Vec> = data + .iter() + .map(|row| row.as_array().cloned().unwrap_or_default()) + .collect(); + Some(SqlResultSet { columns, rows }) +} + +/// 最小 URL 编码(数据库名等) +fn urlencode(s: &str) -> String { + s.bytes() + .map(|b| match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + (b as char).to_string() + } + _ => format!("%{:02X}", b), + }) + .collect() +} diff --git a/src-tauri/src/db/duckdb.rs b/src-tauri/src/db/duckdb.rs new file mode 100644 index 0000000..27798a7 --- /dev/null +++ b/src-tauri/src/db/duckdb.rs @@ -0,0 +1,99 @@ +use super::{DataSource, DbExecutor, SqlResultSet, SqlRunResult, split_sql}; +use duckdb::Connection; +use serde_json::Value as JsonValue; + +pub(crate) struct DuckdbExecutor; + +fn value_to_json(v: duckdb::types::Value) -> JsonValue { + use duckdb::types::Value::*; + match v { + Null => JsonValue::Null, + Boolean(b) => JsonValue::from(b), + TinyInt(i) => JsonValue::from(i), + SmallInt(i) => JsonValue::from(i), + Int(i) => JsonValue::from(i), + BigInt(i) => JsonValue::from(i), + UTinyInt(i) => JsonValue::from(i), + USmallInt(i) => JsonValue::from(i), + UInt(i) => JsonValue::from(i), + UBigInt(i) => JsonValue::from(i), + HugeInt(i) => JsonValue::from(i.to_string()), + Float(f) => JsonValue::from(f as f64), + Double(f) => JsonValue::from(f), + Text(s) => JsonValue::from(s), + Blob(b) => JsonValue::from(format!("", b.len())), + // 时间/小数/嵌套等复杂类型统一以可读字符串呈现 + other => JsonValue::from(format!("{:?}", other)), + } +} + +impl DbExecutor for DuckdbExecutor { + fn handles(&self, kind: &str) -> bool { + kind == "duckdb" + } + + fn run(&self, sql: &str, source: &DataSource) -> SqlRunResult { + let mut result = SqlRunResult::new(); + let conn = match source.file.as_deref() { + Some(p) if !p.trim().is_empty() => Connection::open(p), + _ => Connection::open_in_memory(), + }; + let conn = match conn { + Ok(c) => c, + Err(e) => { + result.error = Some(format!("打开 DuckDB 失败: {}", e)); + return result; + } + }; + + 'stmts: for stmt_sql in split_sql(sql) { + let mut stmt = match conn.prepare(&stmt_sql) { + Ok(s) => s, + Err(e) => { + result.error = Some(e.to_string()); + break 'stmts; + } + }; + let ncol = stmt.column_count(); + if ncol > 0 { + let columns: Vec = + stmt.column_names().iter().map(|s| s.to_string()).collect(); + let rows_iter = stmt.query_map([], |row| { + let mut v = Vec::with_capacity(ncol); + for idx in 0..ncol { + v.push(value_to_json(row.get(idx)?)); + } + Ok(v) + }); + match rows_iter { + Ok(it) => { + let mut rows = Vec::new(); + for r in it { + match r { + Ok(rw) => rows.push(rw), + Err(e) => { + result.error = Some(e.to_string()); + break 'stmts; + } + } + } + result.result_sets.push(SqlResultSet { columns, rows }); + } + Err(e) => { + result.error = Some(e.to_string()); + break 'stmts; + } + } + } else { + match stmt.execute([]) { + Ok(affected) => result.messages.push(format!("OK,影响 {} 行", affected)), + Err(e) => { + result.error = Some(e.to_string()); + break 'stmts; + } + } + } + } + result + } +} diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs index a4eed95..bd6eae0 100644 --- a/src-tauri/src/db/mod.rs +++ b/src-tauri/src/db/mod.rs @@ -1,7 +1,12 @@ //! 数据库执行器:插件式架构。 //! 每种数据库类型实现 `DbExecutor` 并在 `executors()` 中注册一行,新增类型互不影响。 +mod clickhouse; +// DuckDB bundled 的 vendored C++ 在部分新版 MSVC 上编译失败,仅在非 Windows 启用 +#[cfg(not(target_os = "windows"))] +mod duckdb; mod mysql; +mod postgres; mod sqlite; use serde::{Deserialize, Serialize}; @@ -63,6 +68,10 @@ fn executors() -> Vec> { vec![ Box::new(sqlite::SqliteExecutor), Box::new(mysql::MysqlExecutor), + Box::new(postgres::PostgresExecutor), + Box::new(clickhouse::ClickhouseExecutor), + #[cfg(not(target_os = "windows"))] + Box::new(duckdb::DuckdbExecutor), ] } @@ -204,3 +213,57 @@ pub async fn run_sql( Ok(result) } + +/// 分页执行单条查询:把语句包成子查询加 LIMIT/OFFSET,按需拉取一页,避免一次性取全量。 +/// 仅供前端对单条 SELECT/WITH 调用;record=true 时按原始 SQL 记入历史(首页传 true,翻页传 false)。 +#[tauri::command] +pub async fn run_sql_paged( + sql: String, + source: DataSource, + limit: u32, + offset: u32, + record: bool, + history: tauri::State<'_, crate::execution::ExecutionHistory>, +) -> Result { + let original = sql.clone(); + let result = tokio::task::spawn_blocking(move || { + let inner = sql.trim().trim_end_matches(';').trim(); + let wrapped = format!( + "SELECT * FROM (\n{}\n) AS __cf_page LIMIT {} OFFSET {}", + inner, limit, offset + ); + let start = std::time::Instant::now(); + let execs = executors(); + let mut result = match execs.iter().find(|e| e.handles(&source.kind)) { + Some(exec) => exec.run(&wrapped, &source), + None => { + let mut r = SqlRunResult::new(); + r.error = Some(format!("不支持的数据源类型: {}", source.kind)); + r + } + }; + result.elapsed_ms = start.elapsed().as_millis(); + result + }) + .await + .map_err(|e| format!("SQL 任务失败: {}", e))?; + + if record { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let rec = crate::plugins::ExecutionResult { + id: None, + success: result.error.is_none(), + code: original, + stdout: serde_json::to_string(&result).unwrap_or_default(), + stderr: result.error.clone().unwrap_or_default(), + execution_time: result.elapsed_ms, + timestamp, + language: "sql".to_string(), + }; + let _ = history.insert(&rec); + } + Ok(result) +} diff --git a/src-tauri/src/db/postgres.rs b/src-tauri/src/db/postgres.rs new file mode 100644 index 0000000..343c546 --- /dev/null +++ b/src-tauri/src/db/postgres.rs @@ -0,0 +1,84 @@ +use super::{DataSource, DbExecutor, SqlResultSet, SqlRunResult}; +use postgres::{Config, NoTls, SimpleQueryMessage}; +use serde_json::Value as JsonValue; + +pub(crate) struct PostgresExecutor; + +impl DbExecutor for PostgresExecutor { + fn handles(&self, kind: &str) -> bool { + kind == "postgres" || kind == "postgresql" + } + + fn run(&self, sql: &str, source: &DataSource) -> SqlRunResult { + let mut result = SqlRunResult::new(); + + let mut cfg = Config::new(); + cfg.host(source.host.as_deref().unwrap_or("127.0.0.1")) + .port(source.port.unwrap_or(5432)) + .user(source.user.as_deref().unwrap_or("postgres")); + if let Some(pwd) = source.password.as_deref() { + cfg.password(pwd); + } + if let Some(db) = source.database.as_deref() { + cfg.dbname(db); + } + + // SSL/SSH 隧道为独立议题(#93),此处暂用 NoTls + let mut client = match cfg.connect(NoTls) { + Ok(c) => c, + Err(e) => { + result.error = Some(format!("连接 PostgreSQL 失败: {}", e)); + return result; + } + }; + + // simple_query 一次执行整段脚本,按消息流切分结果集与影响行数 + let messages = match client.simple_query(sql) { + Ok(m) => m, + Err(e) => { + result.error = Some(e.to_string()); + return result; + } + }; + + let mut columns: Vec = Vec::new(); + let mut rows: Vec> = Vec::new(); + let flush = |columns: &mut Vec, + rows: &mut Vec>, + result: &mut SqlRunResult| { + if !columns.is_empty() { + result.result_sets.push(SqlResultSet { + columns: std::mem::take(columns), + rows: std::mem::take(rows), + }); + } + }; + + for msg in messages { + match msg { + SimpleQueryMessage::Row(row) => { + if columns.is_empty() { + columns = row.columns().iter().map(|c| c.name().to_string()).collect(); + } + let vals = (0..row.len()) + .map(|i| match row.get(i) { + Some(s) => JsonValue::from(s.to_string()), + None => JsonValue::Null, + }) + .collect(); + rows.push(vals); + } + SimpleQueryMessage::CommandComplete(n) => { + if columns.is_empty() { + result.messages.push(format!("OK,影响 {} 行", n)); + } else { + flush(&mut columns, &mut rows, &mut result); + } + } + _ => {} + } + } + flush(&mut columns, &mut rows, &mut result); + result + } +} diff --git a/src-tauri/src/lsp.rs b/src-tauri/src/lsp.rs index af6c8c0..9cb1495 100644 --- a/src-tauri/src/lsp.rs +++ b/src-tauri/src/lsp.rs @@ -38,7 +38,7 @@ fn server_cmd(language: &str) -> Option<(&'static str, Vec<&'static str>)> { match language { "python3" | "python2" | "python" => Some(("pyright-langserver", vec!["--stdio"])), "typescript" | "typescript-nodejs" | "typescript-browser" | "javascript-nodejs" - | "javascript-browser" | "javascript-jquery" | "nodejs" => { + | "javascript-browser" | "javascript-jquery" | "nodejs" | "react" => { Some(("typescript-language-server", vec!["--stdio"])) } "rust" => Some(("rust-analyzer", vec![])), @@ -48,7 +48,7 @@ fn server_cmd(language: &str) -> Option<(&'static str, Vec<&'static str>)> { "php" => Some(("intelephense", vec!["--stdio"])), "ruby" => Some(("solargraph", vec!["stdio"])), "html" => Some(("vscode-html-language-server", vec!["--stdio"])), - "css" => Some(("vscode-css-language-server", vec!["--stdio"])), + "css" | "less" | "scss" => Some(("vscode-css-language-server", vec!["--stdio"])), "json" => Some(("vscode-json-language-server", vec!["--stdio"])), "java" => Some(("jdtls", vec![])), "kotlin" => Some(("kotlin-language-server", vec![])), diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index e0656b5..444f908 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -37,7 +37,7 @@ use crate::custom_plugin_commands::{ add_custom_plugin, get_custom_plugins, remove_custom_plugin, save_custom_icon, update_custom_plugin, }; -use crate::db::run_sql; +use crate::db::{run_sql, run_sql_paged}; use crate::env_commands::{ EnvironmentManagerState, download_and_install_version, get_environment_info, get_supported_environment_languages, switch_environment_version, uninstall_environment_version, @@ -232,6 +232,7 @@ fn main() { terminal_kill, // SQL 执行 run_sql, + run_sql_paged, // LSP 桥接 lsp_available, lsp_start, diff --git a/src-tauri/src/plugins/less.rs b/src-tauri/src/plugins/less.rs new file mode 100644 index 0000000..967c3d5 --- /dev/null +++ b/src-tauri/src/plugins/less.rs @@ -0,0 +1,55 @@ +use super::{LanguagePlugin, PluginConfig}; +use std::vec; + +pub struct LessPlugin; + +impl LanguagePlugin for LessPlugin { + fn get_order(&self) -> i32 { + 22 + } + + fn get_language_name(&self) -> &'static str { + "Less" + } + + fn get_language_key(&self) -> &'static str { + "less" + } + + fn get_file_extension(&self) -> String { + self.get_config() + .map(|config| config.extension.clone()) + .unwrap_or_else(|| "less".to_string()) + } + + fn get_version_args(&self) -> Vec<&'static str> { + vec!["--version"] + } + + fn get_path_command(&self) -> String { + "lessc --version".to_string() + } + + fn get_default_config(&self) -> PluginConfig { + PluginConfig { + enabled: true, + language: String::from("less"), + before_compile: None, + extension: String::from("less"), + execute_home: None, + // 用 lessc 编译为 CSS 输出到控制台 + run_command: Some(String::from("lessc $filename")), + after_compile: None, + template: Some(String::from("// 在这里输入 Less 代码")), + timeout: Some(30), + console_type: Some(String::from("console")), + icon_path: None, + } + } + + fn get_default_command(&self) -> String { + self.get_config() + .and_then(|config| config.run_command.clone()) + .unwrap_or_else(|| "lessc".to_string()) + } +} diff --git a/src-tauri/src/plugins/manager.rs b/src-tauri/src/plugins/manager.rs index 53b9f55..c079b08 100644 --- a/src-tauri/src/plugins/manager.rs +++ b/src-tauri/src/plugins/manager.rs @@ -17,6 +17,7 @@ use crate::plugins::javascript_jquery::JavaScriptJQueryPlugin; use crate::plugins::javascript_nodejs::JavaScriptNodeJsPlugin; use crate::plugins::json::JsonPlugin; use crate::plugins::kotlin::KotlinPlugin; +use crate::plugins::less::LessPlugin; use crate::plugins::lua::LuaPlugin; use crate::plugins::markdown::MarkdownPlugin; use crate::plugins::nodejs::NodeJSPlugin; @@ -26,6 +27,7 @@ use crate::plugins::php::PHPPlugin; use crate::plugins::python2::Python2Plugin; use crate::plugins::python3::Python3Plugin; use crate::plugins::r::RPlugin; +use crate::plugins::react::ReactPlugin; use crate::plugins::ruby::RubyPlugin; use crate::plugins::rust::RustPlugin; use crate::plugins::scala::ScalaPlugin; @@ -69,10 +71,12 @@ impl PluginManager { ("ruby".to_string(), Box::new(RubyPlugin)), ("applescript".to_string(), Box::new(AppleScriptPlugin)), ("typescript".to_string(), Box::new(TypeScriptPlugin)), + ("react".to_string(), Box::new(ReactPlugin)), ("cpp".to_string(), Box::new(CppPlugin)), ("groovy".to_string(), Box::new(GroovyPlugin)), ("html".to_string(), Box::new(HtmlPlugin)), ("css".to_string(), Box::new(CssPlugin)), + ("less".to_string(), Box::new(LessPlugin)), ("svg".to_string(), Box::new(SvgPlugin)), ("json".to_string(), Box::new(JsonPlugin)), ("xml".to_string(), Box::new(XmlPlugin)), diff --git a/src-tauri/src/plugins/mod.rs b/src-tauri/src/plugins/mod.rs index 8cdbca7..66c0ff6 100644 --- a/src-tauri/src/plugins/mod.rs +++ b/src-tauri/src/plugins/mod.rs @@ -416,6 +416,7 @@ pub mod javascript_jquery; pub mod javascript_nodejs; pub mod json; pub mod kotlin; +pub mod less; pub mod lua; pub mod manager; pub mod markdown; @@ -426,6 +427,7 @@ pub mod php; pub mod python2; pub mod python3; pub mod r; +pub mod react; pub mod ruby; pub mod rust; pub mod scala; diff --git a/src-tauri/src/plugins/react.rs b/src-tauri/src/plugins/react.rs new file mode 100644 index 0000000..695733b --- /dev/null +++ b/src-tauri/src/plugins/react.rs @@ -0,0 +1,60 @@ +use super::{LanguagePlugin, PluginConfig}; +use std::vec; + +pub struct ReactPlugin; + +impl LanguagePlugin for ReactPlugin { + fn get_order(&self) -> i32 { + 14 + } + + fn get_language_name(&self) -> &'static str { + "React (JSX)" + } + + fn get_language_key(&self) -> &'static str { + "react" + } + + fn get_file_extension(&self) -> String { + self.get_config() + .map(|config| config.extension.clone()) + .unwrap_or_else(|| "jsx".to_string()) + } + + fn get_version_args(&self) -> Vec<&'static str> { + vec!["--"] + } + + fn get_path_command(&self) -> String { + "which node".to_string() + } + + fn get_default_config(&self) -> PluginConfig { + PluginConfig { + enabled: true, + language: self.get_language_key().to_string(), + before_compile: None, + extension: String::from("jsx"), + execute_home: None, + // 浏览器预览:CDN 引入 React/ReactDOM + Babel,以 text/babel 在浏览器内转译 JSX。 + // 用户代码可向 #root 渲染:ReactDOM.createRoot(document.getElementById('root')).render(...) + run_command: Some(String::from( + "echo
\n\n\n\n", + )), + after_compile: None, + template: Some(String::from( + "function App() {\n return

Hello, React!

;\n}\n\nReactDOM.createRoot(document.getElementById('root')).render();", + )), + timeout: Some(30), + console_type: Some(String::from("web")), + icon_path: None, + } + } + + fn get_default_command(&self) -> String { + self.get_config() + .and_then(|config| config.run_command.clone()) + .unwrap_or_else(|| "node".to_string()) + } +} diff --git a/src/App.vue b/src/App.vue index b4b79b2..ffe2a55 100644 --- a/src/App.vue +++ b/src/App.vue @@ -184,6 +184,9 @@ :output="output" :is-running="isRunning" :execution-time="lastExecutionTime" + :paging="sqlPaging" + @prev="sqlPrevPage" + @next="sqlNextPage" @clear="clearOutput"/> @@ -376,6 +379,9 @@ +
+ + + @@ -395,7 +415,7 @@ import {computed, nextTick, onMounted, onUnmounted, reactive, ref, shallowRef, watch} from 'vue' import {debounce} from 'lodash-es' import {formatDocument, formatSelection, renameSymbol} from 'codemirror-languageserver' -import {runGotoDefinition, lspSupportsLanguage} from './editor/lspExtension' +import {runGotoDefinition, lspSupportsLanguage, triggerCodeActions, applyCodeAction} from './editor/lspExtension' import {ChevronRight, Code2, CornerDownRight, Eye, FolderOpen, GitBranch, GitCompare, History, ListTree, Maximize2, Monitor, Moon, PanelBottom, PanelLeft, PanelRight, Play, Plus, Save, Search, Settings as SettingsIcon, Sparkles, Sun, Terminal as TerminalIcon, X} from 'lucide-vue-next' import {ExecutionResult, LayoutMode, SplitDirection} from './types/app.ts' import AppHeader from './components/AppHeader.vue' @@ -1120,6 +1140,39 @@ const runEditorCommand = (cmd: (v: any) => boolean) => { } } +// ===== LSP 代码操作选择菜单 ===== +const codeActionMenu = reactive<{visible: boolean; x: number; y: number; actions: any[]}>({ + visible: false, x: 0, y: 0, actions: [] +}) +// 编辑器扩展请求完成后派发 lsp:code-actions:有结果则弹菜单,无则提示 +const onLspCodeActions = (e: Event) => { + const detail = (e as CustomEvent).detail as {actions: any[]; x: number; y: number} + const actions = detail?.actions ?? [] + if (!actions.length) { + toast.info('当前位置没有可用的代码操作') + return + } + codeActionMenu.actions = actions + codeActionMenu.x = Math.min(detail.x, window.innerWidth - 430) + codeActionMenu.y = Math.min(detail.y, window.innerHeight - 340) + codeActionMenu.visible = true +} +const pickCodeAction = async (action: any) => { + codeActionMenu.visible = false + if (!editorView.value) { + return + } + try { + const {otherFiles} = await applyCodeAction(editorView.value, action) + if (otherFiles > 0) { + toast.info(`该操作还涉及 ${otherFiles} 个其它文件的修改,暂未自动应用`) + } + } + catch (err) { + toast.error('应用代码操作失败: ' + err) + } +} + // 全局替换后:刷新涉及到的已打开标签(保留有未保存修改的标签) const reloadAffectedFiles = async (paths: string[]) => { const set = new Set(paths) @@ -1422,20 +1475,72 @@ const showRunPrompt = ref(false) // 运行选中片段:以选中文本作为临时代码运行(不就地、不关联文件) // SQL 走专用执行(结构化结果 + 错误 + 数据源:内存/SQLite/MySQL) const {resolveActiveSource} = useDbConnections() + +// 结果分页:超大结果集按页拉取,避免一次性取全量 +const SQL_PAGE_SIZE = 500 +const sqlPage = reactive<{ active: boolean; sql: string; source: any; offset: number; hasMore: boolean }>({ + active: false, sql: '', source: null, offset: 0, hasMore: false +}) +const sqlPaging = computed(() => ({active: sqlPage.active, offset: sqlPage.offset, pageSize: SQL_PAGE_SIZE, hasMore: sqlPage.hasMore})) +// 单条 SELECT/WITH 才可分页(去掉尾分号后无其它分号,且以 select/with 开头) +const isPageableSql = (sql: string): boolean => { + const s = sql.trim().replace(/;\s*$/, '') + return !s.includes(';') && /^(select|with)\b/i.test(s) +} + +const loadSqlPage = async (offset: number, record: boolean) => { + if (layoutMode.value === 'editor') { + showConsole.value = true + } + isRunning.value = true + try { + const res = await invoke('run_sql_paged', { + sql: sqlPage.sql, source: sqlPage.source, limit: SQL_PAGE_SIZE, offset, record + }) + output.value = JSON.stringify(res) + isSuccess.value = !res.error + lastExecutionTime.value = res.elapsed_ms || 0 + sqlPage.offset = offset + sqlPage.hasMore = ((res.result_sets || [])[0]?.rows || []).length === SQL_PAGE_SIZE + if (res.error) { + toast.error('SQL 执行失败') + } + } + catch (error) { + output.value = JSON.stringify({result_sets: [], messages: [], error: String(error)}) + toast.error('SQL 执行失败: ' + error) + } + finally { + isRunning.value = false + } +} +const sqlPrevPage = () => sqlPage.offset > 0 && loadSqlPage(Math.max(0, sqlPage.offset - SQL_PAGE_SIZE), false) +const sqlNextPage = () => sqlPage.hasMore && loadSqlPage(sqlPage.offset + SQL_PAGE_SIZE, false) + const runSql = async (sqlOverride?: string) => { const sql = sqlOverride ?? code.value if (!sql.trim()) { toast.info('没有可执行的 SQL') return } + const source = resolveActiveSource() + output.value = '' + isSuccess.value = false + // 可分页查询:走分页拉取(首页记入历史) + if (isPageableSql(sql)) { + sqlPage.active = true + sqlPage.sql = sql + sqlPage.source = source + sqlPage.offset = 0 + await loadSqlPage(0, true) + return + } + sqlPage.active = false if (layoutMode.value === 'editor') { showConsole.value = true } isRunning.value = true - output.value = '' - isSuccess.value = false try { - const source = resolveActiveSource() const res = await invoke('run_sql', {sql, source}) output.value = JSON.stringify(res) isSuccess.value = !res.error @@ -1734,6 +1839,7 @@ onMounted(async () => { window.addEventListener('keydown', onGlobalKeydown, true) window.addEventListener('lsp:open-location', onLspOpenLocation) + window.addEventListener('lsp:code-actions', onLspCodeActions) window.addEventListener('contextmenu', onEditorContext) // 触发 app-ready 事件,通知主进程 @@ -1744,6 +1850,7 @@ onUnmounted(() => { cleanupEventListeners() window.removeEventListener('keydown', onGlobalKeydown, true) window.removeEventListener('lsp:open-location', onLspOpenLocation) + window.removeEventListener('lsp:code-actions', onLspCodeActions) window.removeEventListener('contextmenu', onEditorContext) }) diff --git a/src/components/GitPanel.vue b/src/components/GitPanel.vue index 612f03e..73348e8 100644 --- a/src/components/GitPanel.vue +++ b/src/components/GitPanel.vue @@ -67,13 +67,13 @@
- - -
diff --git a/src/components/PivotTable.vue b/src/components/PivotTable.vue new file mode 100644 index 0000000..ed66b7b --- /dev/null +++ b/src/components/PivotTable.vue @@ -0,0 +1,217 @@ + + + diff --git a/src/components/SchemaBrowser.vue b/src/components/SchemaBrowser.vue index dc977de..213ac54 100644 --- a/src/components/SchemaBrowser.vue +++ b/src/components/SchemaBrowser.vue @@ -144,7 +144,7 @@ const filteredDatabases = computed(() => { return databases.value.filter(d => d.name.toLowerCase().includes(q)) }) -const quote = (kind: string, name: string) => (kind === 'mysql' ? `\`${name}\`` : `"${name}"`) +const quote = (kind: string, name: string) => (kind === 'mysql' || kind === 'clickhouse' ? `\`${name}\`` : `"${name}"`) const esc = (s: string) => s.replace(/'/g, "''") const tablesSql = (kind: string, db?: string): string => { @@ -153,6 +153,23 @@ const tablesSql = (kind: string, db?: string): string => { + `FROM information_schema.columns WHERE table_schema = '${esc(db || '')}' ` + 'ORDER BY table_name, ordinal_position' } + if (kind === 'postgres') { + return 'SELECT table_name AS tbl, column_name AS col, data_type AS typ ' + + 'FROM information_schema.columns ' + + "WHERE table_schema NOT IN ('pg_catalog', 'information_schema') " + + 'ORDER BY table_name, ordinal_position' + } + if (kind === 'clickhouse') { + return 'SELECT table AS tbl, name AS col, type AS typ ' + + 'FROM system.columns WHERE database = currentDatabase() ' + + 'ORDER BY table, position' + } + if (kind === 'duckdb') { + return 'SELECT table_name AS tbl, column_name AS col, data_type AS typ ' + + 'FROM information_schema.columns ' + + "WHERE table_schema NOT IN ('information_schema', 'pg_catalog') " + + 'ORDER BY table_name, ordinal_position' + } return 'SELECT m.name AS tbl, p.name AS col, p.type AS typ ' + 'FROM sqlite_master m JOIN pragma_table_info(m.name) p ' + "WHERE m.type IN ('table','view') AND m.name NOT LIKE 'sqlite\\_%' ESCAPE '\\' " @@ -268,12 +285,16 @@ const exportCsv = async (name: string, db?: string) => { const copyDdl = async (name: string, db?: string) => { try { const source = resolveActiveSource() + if (source.kind === 'postgres' || source.kind === 'duckdb') { + toast.info(`${source.kind === 'duckdb' ? 'DuckDB' : 'PostgreSQL'} 暂不支持一键复制建表语句`) + return + } const qualified = db ? `${quote(source.kind, db)}.${quote(source.kind, name)}` : quote(source.kind, name) - const sql = source.kind === 'mysql' + const sql = source.kind === 'mysql' || source.kind === 'clickhouse' ? `SHOW CREATE TABLE ${qualified}` : `SELECT sql FROM sqlite_master WHERE name = '${esc(name)}'` const rows = await runRows(sql) - // MySQL: 第 2 列为建表语句;SQLite: 第 1 列 + // MySQL: 第 2 列为建表语句;ClickHouse/SQLite: 第 1 列 const ddl = String((source.kind === 'mysql' ? rows[0]?.[1] : rows[0]?.[0]) ?? '') if (!ddl) { toast.error('未获取到建表语句') diff --git a/src/components/Settings.vue b/src/components/Settings.vue index ecfd946..d3c4706 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -1,5 +1,5 @@