From 4bf4f3b77fb7d6c6c3a08dc9e93034a9d00c288c Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Thu, 7 May 2026 01:02:56 +0800 Subject: [PATCH] feat: add DM (Dameng) database support via ODBC Connect to DM8 databases using the odbc-api crate with unixODBC. Includes schema browsing, query execution, DDL generation, and full frontend integration with download link to DM ODBC driver. --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 6 +- Cargo.lock | 630 +++++++++++++++--- Dockerfile | 3 +- crates/dbx-core/Cargo.toml | 1 + crates/dbx-core/src/connection.rs | 22 +- crates/dbx-core/src/db/dm_driver.rs | 334 ++++++++++ crates/dbx-core/src/db/mod.rs | 1 + crates/dbx-core/src/models/connection.rs | 5 + crates/dbx-core/src/query.rs | 14 + crates/dbx-core/src/schema.rs | 97 +++ src-tauri/src/commands/connection.rs | 21 + .../connection/ConnectionDialog.vue | 11 +- .../diagram/SchemaDiagramDialog.vue | 4 +- src/components/diff/SchemaDiffDialog.vue | 3 +- .../search/DatabaseSearchDialog.vue | 2 +- src/components/sidebar/TreeItem.vue | 32 +- .../transfer/DataTransferDialog.vue | 21 +- src/composables/useSchemaOptions.ts | 2 +- src/i18n/locales/en.ts | 3 +- src/i18n/locales/zh-CN.ts | 3 +- src/lib/tableSelectSql.ts | 10 +- src/stores/connectionStore.ts | 3 +- src/types/database.ts | 3 +- 24 files changed, 1111 insertions(+), 122 deletions(-) create mode 100644 crates/dbx-core/src/db/dm_driver.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edb316be1..689cfac4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: - name: Install system dependencies (Linux) run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev unixodbc-dev - name: Cargo fmt check run: cargo fmt --check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c7eb2200..891227351 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,10 @@ jobs: shared-key: release-${{ matrix.target }}-${{ steps.deps-hash.outputs.hash }} cache-on-failure: true + - name: Install system dependencies (macOS) + if: startsWith(matrix.platform, 'macos') + run: brew install unixodbc + - name: Install Apple certificate (macOS) if: startsWith(matrix.platform, 'macos') env: @@ -79,7 +83,7 @@ jobs: if: startsWith(matrix.platform, 'ubuntu') run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev unixodbc-dev - name: Setup Tauri signing key shell: bash diff --git a/Cargo.lock b/Cargo.lock index 1f75d903b..016c8552b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,31 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.11.1", + "cc", + "jni 0.22.4", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + [[package]] name = "android_log-sys" version = "0.3.2" @@ -664,13 +689,22 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + [[package]] name = "block2" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2", + "objc2 0.6.4", ] [[package]] @@ -854,6 +888,20 @@ dependencies = [ "zip 4.6.1", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.11.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + [[package]] name = "camino" version = "1.2.2" @@ -1186,6 +1234,19 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + [[package]] name = "core-graphics" version = "0.25.0" @@ -1194,11 +1255,22 @@ checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ "bitflags 2.11.1", "core-foundation 0.10.1", - "core-graphics-types", + "core-graphics-types 0.2.0", "foreign-types 0.5.0", "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -1462,6 +1534,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + [[package]] name = "curve25519-dalek" version = "5.0.0-pre.6" @@ -1590,6 +1668,7 @@ dependencies = [ "futures", "log", "mongodb", + "odbc-api", "oracle-rs 0.1.6", "percent-encoding", "portpicker", @@ -1775,9 +1854,15 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + [[package]] name = "dispatch2" version = "0.3.1" @@ -1785,9 +1870,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.6.2", "libc", - "objc2", + "objc2 0.6.4", ] [[package]] @@ -1801,6 +1886,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.2" @@ -2094,7 +2188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2802,6 +2896,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -3080,7 +3180,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -4020,15 +4120,15 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2", - "objc2-app-kit", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "once_cell", "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4080,6 +4180,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -4113,7 +4219,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4219,6 +4325,22 @@ dependencies = [ "libc", ] +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + [[package]] name = "objc2" version = "0.6.4" @@ -4229,6 +4351,22 @@ dependencies = [ "objc2-exception-helper", ] +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + [[package]] name = "objc2-app-kit" version = "0.3.2" @@ -4236,10 +4374,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.11.1", - "block2", - "objc2", + "block2 0.6.2", + "objc2 0.6.4", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -4249,8 +4400,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ "bitflags 2.11.1", - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -4259,8 +4433,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -4271,7 +4445,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.11.1", "dispatch2", - "objc2", + "objc2 0.6.4", ] [[package]] @@ -4282,19 +4456,43 @@ checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ "bitflags 2.11.1", "dispatch2", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + [[package]] name = "objc2-core-image" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", ] [[package]] @@ -4303,8 +4501,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -4314,7 +4512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ "bitflags 2.11.1", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", ] @@ -4334,6 +4532,19 @@ dependencies = [ "cc", ] +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + [[package]] name = "objc2-foundation" version = "0.3.2" @@ -4341,9 +4552,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.6.2", "libc", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -4354,10 +4565,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ "bitflags 2.11.1", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", ] +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-osa-kit" version = "0.3.2" @@ -4365,9 +4600,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ "bitflags 2.11.1", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", ] [[package]] @@ -4377,9 +4625,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ "bitflags 2.11.1", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-core-location 0.2.2", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications 0.2.2", ] [[package]] @@ -4389,18 +4668,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.11.1", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", + "objc2-core-image 0.3.2", + "objc2-core-location 0.3.2", "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-user-notifications 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.11.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -4409,8 +4712,8 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", ] [[package]] @@ -4420,13 +4723,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ "bitflags 2.11.1", - "block2", - "objc2", - "objc2-app-kit", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", ] +[[package]] +name = "odbc-api" +version = "25.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9144219b357fb103b928a1ef1b4e466a84b8f52aa9f5d5e538c409e1ffb9d26" +dependencies = [ + "atoi", + "log", + "odbc-sys", + "thiserror 2.0.18", + "widestring", + "winit", +] + +[[package]] +name = "odbc-sys" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245cb4fe8236df4fd352ba96075d754233c6509d654d9f1c1482158b7d6c083d" + [[package]] name = "once_cell" version = "1.21.4" @@ -4575,6 +4898,16 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "orbclient" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a570f6bca41d29acb2139229a7c873ec99bc9a313bd10804081d89bfac8ff329" +dependencies = [ + "libc", + "libredox", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -4582,7 +4915,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -4591,8 +4924,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", "objc2-osa-kit", "serde", "serde_json", @@ -4869,6 +5202,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -5003,6 +5356,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -5411,6 +5778,15 @@ dependencies = [ "url", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -5607,17 +5983,17 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" dependencies = [ - "block2", + "block2 0.6.2", "dispatch2", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", "log", - "objc2", - "objc2-app-kit", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", @@ -5899,7 +6275,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6000,7 +6376,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6636,6 +7012,15 @@ dependencies = [ "serde", ] +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.6.3" @@ -6643,7 +7028,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6655,11 +7040,11 @@ dependencies = [ "bytemuck", "js-sys", "ndk", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", "raw-window-handle", "redox_syscall 0.5.18", "tracing", @@ -7134,9 +7519,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" dependencies = [ "bitflags 2.11.1", - "block2", + "block2 0.6.2", "core-foundation 0.10.1", - "core-graphics", + "core-graphics 0.25.0", "crossbeam-channel", "dbus", "dispatch2", @@ -7150,10 +7535,10 @@ dependencies = [ "log", "ndk", "ndk-sys", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", "once_cell", "parking_lot", "percent-encoding", @@ -7223,10 +7608,10 @@ dependencies = [ "log", "mime", "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", "objc2-web-kit", "percent-encoding", "plist", @@ -7358,7 +7743,7 @@ dependencies = [ "dunce", "glob", "log", - "objc2-foundation", + "objc2-foundation 0.3.2", "percent-encoding", "schemars 0.8.22", "serde", @@ -7382,8 +7767,8 @@ dependencies = [ "byte-unit", "fern", "log", - "objc2", - "objc2-foundation", + "objc2 0.6.4", + "objc2-foundation 0.3.2", "serde", "serde_json", "serde_repr", @@ -7469,8 +7854,8 @@ dependencies = [ "gtk", "http", "jni 0.21.1", - "objc2", - "objc2-ui-kit", + "objc2 0.6.4", + "objc2-ui-kit 0.3.2", "objc2-web-kit", "raw-window-handle", "serde", @@ -7493,8 +7878,8 @@ dependencies = [ "http", "jni 0.21.1", "log", - "objc2", - "objc2-app-kit", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "once_cell", "percent-encoding", "raw-window-handle", @@ -7568,7 +7953,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8058,16 +8443,16 @@ dependencies = [ "dirs", "libappindicator", "muda", - "objc2", - "objc2-app-kit", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation", + "objc2-foundation 0.3.2", "once_cell", "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8679,7 +9064,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -8694,10 +9079,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2", - "objc2-app-kit", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation", + "objc2-foundation 0.3.2", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -9219,6 +9604,46 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "android-activity", + "atomic-waker", + "bitflags 2.11.1", + "block2 0.5.1", + "calloop", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "xkbcommon-dl", +] + [[package]] name = "winnow" version = "0.5.40" @@ -9360,7 +9785,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ "base64 0.22.1", - "block2", + "block2 0.6.2", "cookie", "crossbeam-channel", "dirs", @@ -9374,11 +9799,11 @@ dependencies = [ "jni 0.21.1", "libc", "ndk", - "objc2", - "objc2-app-kit", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", "objc2-core-foundation", - "objc2-foundation", - "objc2-ui-kit", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", "objc2-web-kit", "once_cell", "percent-encoding", @@ -9437,6 +9862,25 @@ dependencies = [ "rustix 1.1.4", ] +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.11.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "yoke" version = "0.8.2" diff --git a/Dockerfile b/Dockerfile index 5ffc000fd..bd74c8517 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ ARG TARGETARCH WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential cmake pkg-config perl python3-pip gcc-aarch64-linux-gnu gcc-x86-64-linux-gnu \ + unixodbc-dev libodbc2:amd64 libodbc2:arm64 \ && pip3 install --break-system-packages ziglang \ && cargo install cargo-zigbuild \ && rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu \ @@ -59,7 +60,7 @@ RUN case "$TARGETARCH" in \ # Stage 3: Final image FROM debian:bookworm-slim ARG TARGETPLATFORM -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 unixodbc && rm -rf /var/lib/apt/lists/* COPY --from=backend /out/${TARGETPLATFORM}/dbx-web /usr/local/bin/ COPY --from=frontend /app/dist /app/static ENV DBX_STATIC_DIR=/app/static diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index b69638b4e..8eaecd0b6 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -27,3 +27,4 @@ russh = "0.60" portpicker = "0.1.1" csv = "1" calamine = "0.30.1" +odbc-api = "25" diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs index f9923c049..7c69994c7 100644 --- a/crates/dbx-core/src/connection.rs +++ b/crates/dbx-core/src/connection.rs @@ -28,6 +28,7 @@ pub enum PoolKind { SqlServer(Arc>), Oracle(Arc>), Elasticsearch(db::elasticsearch_driver::EsClient), + Dameng(Arc>), } pub struct AppState { @@ -60,7 +61,7 @@ impl AppState { return Ok(connection_id.to_string()); } - let is_single_conn = matches!(db_type, Some(DatabaseType::Oracle)); + let is_single_conn = matches!(db_type, Some(DatabaseType::Oracle) | Some(DatabaseType::Dameng)); let pool_key = if is_single_conn { connection_id.to_string() } else { @@ -95,7 +96,7 @@ impl AppState { let mut db_config = config.clone(); if let Some(db) = database { - if db_config.db_type != DatabaseType::Oracle { + if db_config.db_type != DatabaseType::Oracle && db_config.db_type != DatabaseType::Dameng { db_config.database = Some(db.to_string()); } } @@ -162,6 +163,17 @@ impl AppState { db::elasticsearch_driver::test_connection(&client).await?; PoolKind::Elasticsearch(client) } + DatabaseType::Dameng => { + let client = db::dm_driver::connect( + &host, + port, + db_config.database.as_deref().unwrap_or(""), + &db_config.username, + &db_config.password, + ) + .await?; + PoolKind::Dameng(Arc::new(std::sync::Mutex::new(client))) + } }; self.connections.lock().await.insert(pool_key.clone(), pool); @@ -205,7 +217,11 @@ impl AppState { let configs = self.configs.lock().await; configs .get(connection_id) - .map(|c| c.db_type == DatabaseType::Oracle || c.db_type == DatabaseType::Elasticsearch) + .map(|c| { + c.db_type == DatabaseType::Oracle + || c.db_type == DatabaseType::Elasticsearch + || c.db_type == DatabaseType::Dameng + }) .unwrap_or(false) }; let pool_key = if is_single_conn { diff --git a/crates/dbx-core/src/db/dm_driver.rs b/crates/dbx-core/src/db/dm_driver.rs new file mode 100644 index 000000000..463e8a12e --- /dev/null +++ b/crates/dbx-core/src/db/dm_driver.rs @@ -0,0 +1,334 @@ +use std::time::Instant; + +use odbc_api::{buffers::TextRowSet, ConnectionOptions, Cursor, Environment, ResultSetMetadata}; + +use crate::types::{ColumnInfo, DatabaseInfo, ForeignKeyInfo, IndexInfo, QueryResult, TableInfo, TriggerInfo}; + +use super::CONNECTION_TIMEOUT_SECS; + +static ENV: std::sync::LazyLock = + std::sync::LazyLock::new(|| Environment::new().expect("Failed to initialize ODBC environment")); + +pub struct DmClient { + conn: odbc_api::Connection<'static>, +} + +unsafe impl Send for DmClient {} + +impl DmClient { + pub fn query_rows(&self, sql: &str) -> Result>, String> { + match self.conn.execute(sql, (), None).map_err(|e| e.to_string())? { + Some(cursor) => read_cursor(cursor), + None => Ok(vec![]), + } + } + + fn query_single_column(&self, sql: &str) -> Result, String> { + Ok(self.query_rows(sql)?.into_iter().filter_map(|r| r.into_iter().next()).collect()) + } +} + +fn read_cursor(cursor: impl Cursor) -> Result>, String> { + let mut cursor = cursor; + let col_count = cursor.num_result_cols().map_err(|e| e.to_string())? as u16; + let buffer = TextRowSet::for_cursor(1000, &mut cursor, Some(8192)).map_err(|e| e.to_string())?; + let mut row_cursor = cursor.bind_buffer(buffer).map_err(|e| e.to_string())?; + let mut rows = Vec::new(); + while let Some(batch) = row_cursor.fetch().map_err(|e| e.to_string())? { + for row_idx in 0..batch.num_rows() { + let vals: Vec = (0..col_count as usize) + .map(|col| { + batch.at(col, row_idx).and_then(|bytes| std::str::from_utf8(bytes).ok()).unwrap_or("").to_string() + }) + .collect(); + rows.push(vals); + } + } + Ok(rows) +} + +pub async fn connect(host: &str, port: u16, database: &str, user: &str, pass: &str) -> Result { + let conn_str = format!( + "Driver={{DM8 ODBC DRIVER}};Server={host};TCP_PORT={port};DATABASE={db};UID={user};PWD={pass}", + host = host, + port = port, + db = database, + user = user, + pass = pass, + ); + + let result = tokio::time::timeout( + std::time::Duration::from_secs(CONNECTION_TIMEOUT_SECS), + tokio::task::spawn_blocking(move || { + ENV.connect_with_connection_string(&conn_str, ConnectionOptions::default()) + .map_err(|e| { + let msg = e.to_string(); + if msg.contains("Data source name not found") || msg.contains("Driver") { + format!( + "DM8 ODBC driver not found. Please install the DM8 ODBC driver \ + and register it in odbcinst.ini (Linux/macOS) or the ODBC Data Source Administrator (Windows). \ + Original error: {msg}" + ) + } else { + format!("DM connection failed: {msg}") + } + }) + .map(|conn| DmClient { conn }) + }), + ) + .await + .map_err(|_| format!("DM connection timed out ({CONNECTION_TIMEOUT_SECS}s)"))? + .map_err(|e| format!("DM connection task failed: {e}"))?; + + result +} + +pub fn list_databases(client: &DmClient) -> Result, String> { + let rows = client.query_single_column("SELECT USERNAME FROM ALL_USERS ORDER BY USERNAME")?; + Ok(rows.into_iter().map(|name| DatabaseInfo { name }).collect()) +} + +pub fn list_schemas(client: &DmClient) -> Result, String> { + client.query_single_column("SELECT USERNAME FROM ALL_USERS ORDER BY USERNAME") +} + +pub fn list_tables(client: &DmClient, schema: &str) -> Result, String> { + let s = schema.replace('\'', "''"); + let sql = format!( + "SELECT TABLE_NAME, 'TABLE' AS TABLE_TYPE FROM ALL_TABLES WHERE OWNER = '{s}' \ + UNION ALL \ + SELECT VIEW_NAME, 'VIEW' FROM ALL_VIEWS WHERE OWNER = '{s}' \ + ORDER BY 1" + ); + let rows = client.query_rows(&sql)?; + Ok(rows + .into_iter() + .map(|r| TableInfo { + name: r.first().cloned().unwrap_or_default(), + table_type: r.get(1).cloned().unwrap_or_else(|| "TABLE".to_string()), + }) + .collect()) +} + +pub fn get_columns(client: &DmClient, schema: &str, table: &str) -> Result, String> { + let s = schema.replace('\'', "''"); + let t = table.replace('\'', "''"); + + let pk_rows = client.query_single_column(&format!( + "SELECT cols.COLUMN_NAME FROM ALL_CONS_COLUMNS cols \ + JOIN ALL_CONSTRAINTS cons ON cols.CONSTRAINT_NAME = cons.CONSTRAINT_NAME AND cols.OWNER = cons.OWNER \ + WHERE cons.CONSTRAINT_TYPE = 'P' AND cons.OWNER = '{s}' AND cons.TABLE_NAME = '{t}'" + ))?; + let pk_names: std::collections::HashSet = pk_rows.into_iter().collect(); + + let col_rows = client.query_rows(&format!( + "SELECT COLUMN_NAME, DATA_TYPE, NULLABLE, DATA_PRECISION, DATA_SCALE, DATA_LENGTH, CHAR_LENGTH \ + FROM ALL_TAB_COLUMNS \ + WHERE OWNER = '{s}' AND TABLE_NAME = '{t}' \ + ORDER BY COLUMN_ID" + ))?; + + Ok(col_rows + .into_iter() + .map(|r| { + let name = r.first().cloned().unwrap_or_default(); + let base = r.get(1).cloned().unwrap_or_default(); + let num_prec = r.get(3).and_then(|v| v.parse::().ok()); + let num_scale = r.get(4).and_then(|v| v.parse::().ok()); + let data_len = r.get(5).and_then(|v| v.parse::().ok()); + let char_len = r.get(6).and_then(|v| v.parse::().ok()); + let data_type = match base.to_uppercase().as_str() { + "VARCHAR2" | "NVARCHAR2" | "VARCHAR" | "CHAR" | "NCHAR" => { + let len = char_len.or(data_len); + match len { + Some(n) => format!("{base}({n})"), + None => base, + } + } + "NUMBER" | "NUMERIC" | "DECIMAL" => match (num_prec, num_scale) { + (Some(p), Some(s)) if s > 0 => format!("{base}({p},{s})"), + (Some(p), _) if p > 0 => format!("{base}({p})"), + _ => base, + }, + "RAW" => match data_len { + Some(n) => format!("RAW({n})"), + None => "RAW".to_string(), + }, + _ => base, + }; + ColumnInfo { + is_primary_key: pk_names.contains(&name), + name, + data_type, + is_nullable: r.get(2).map(|v| v == "Y").unwrap_or(false), + column_default: None, + extra: None, + comment: None, + numeric_precision: num_prec, + numeric_scale: num_scale, + character_maximum_length: char_len, + } + }) + .collect()) +} + +pub fn list_indexes(client: &DmClient, schema: &str, table: &str) -> Result, String> { + let s = schema.replace('\'', "''"); + let t = table.replace('\'', "''"); + let sql = format!( + "SELECT i.INDEX_NAME, \ + LISTAGG(ic.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY ic.COLUMN_POSITION) AS COLUMNS, \ + i.UNIQUENESS, \ + CASE WHEN c.CONSTRAINT_TYPE = 'P' THEN 1 ELSE 0 END AS IS_PK, \ + i.INDEX_TYPE \ + FROM ALL_INDEXES i \ + JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.OWNER = ic.INDEX_OWNER AND i.TABLE_OWNER = ic.TABLE_OWNER \ + LEFT JOIN ALL_CONSTRAINTS c ON i.INDEX_NAME = c.INDEX_NAME AND i.TABLE_OWNER = c.OWNER \ + AND c.CONSTRAINT_TYPE = 'P' \ + WHERE i.TABLE_OWNER = '{s}' AND i.TABLE_NAME = '{t}' \ + GROUP BY i.INDEX_NAME, i.UNIQUENESS, c.CONSTRAINT_TYPE, i.INDEX_TYPE \ + ORDER BY i.INDEX_NAME" + ); + let rows = client.query_rows(&sql)?; + Ok(rows + .into_iter() + .map(|r| { + let cols_str = r.get(1).cloned().unwrap_or_default(); + IndexInfo { + name: r.first().cloned().unwrap_or_default(), + columns: cols_str.split(',').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), + is_unique: r.get(2).map(|v| v == "UNIQUE").unwrap_or(false), + is_primary: r.get(3).map(|v| v == "1").unwrap_or(false), + filter: None, + index_type: r.get(4).cloned(), + included_columns: None, + comment: None, + } + }) + .collect()) +} + +pub fn list_foreign_keys(client: &DmClient, schema: &str, table: &str) -> Result, String> { + let s = schema.replace('\'', "''"); + let t = table.replace('\'', "''"); + let sql = format!( + "SELECT c.CONSTRAINT_NAME, cc.COLUMN_NAME, rc.TABLE_NAME, rcc.COLUMN_NAME \ + FROM ALL_CONSTRAINTS c \ + JOIN ALL_CONS_COLUMNS cc ON c.CONSTRAINT_NAME = cc.CONSTRAINT_NAME AND c.OWNER = cc.OWNER \ + JOIN ALL_CONSTRAINTS rc ON c.R_CONSTRAINT_NAME = rc.CONSTRAINT_NAME AND c.R_OWNER = rc.OWNER \ + JOIN ALL_CONS_COLUMNS rcc ON rc.CONSTRAINT_NAME = rcc.CONSTRAINT_NAME AND rc.OWNER = rcc.OWNER \ + WHERE c.CONSTRAINT_TYPE = 'R' AND c.OWNER = '{s}' AND c.TABLE_NAME = '{t}' \ + ORDER BY c.CONSTRAINT_NAME" + ); + let rows = client.query_rows(&sql)?; + Ok(rows + .into_iter() + .map(|r| ForeignKeyInfo { + name: r.first().cloned().unwrap_or_default(), + column: r.get(1).cloned().unwrap_or_default(), + ref_table: r.get(2).cloned().unwrap_or_default(), + ref_column: r.get(3).cloned().unwrap_or_default(), + }) + .collect()) +} + +pub fn list_triggers(client: &DmClient, schema: &str, table: &str) -> Result, String> { + let s = schema.replace('\'', "''"); + let t = table.replace('\'', "''"); + let sql = format!( + "SELECT TRIGGER_NAME, TRIGGERING_EVENT, TRIGGER_TYPE \ + FROM ALL_TRIGGERS \ + WHERE OWNER = '{s}' AND TABLE_NAME = '{t}' \ + ORDER BY TRIGGER_NAME" + ); + let rows = client.query_rows(&sql)?; + Ok(rows + .into_iter() + .map(|r| TriggerInfo { + name: r.first().cloned().unwrap_or_default(), + event: r.get(1).cloned().unwrap_or_default(), + timing: r.get(2).cloned().unwrap_or_default(), + }) + .collect()) +} + +pub fn execute_query_sync(client: &DmClient, sql: &str) -> Result { + let start = Instant::now(); + let sql = sql.trim().trim_end_matches(';'); + let trimmed = sql.to_uppercase(); + + if trimmed.starts_with("SELECT") + || trimmed.starts_with("WITH") + || trimmed.starts_with("SHOW") + || trimmed.starts_with("DESCRIBE") + || trimmed.starts_with("EXPLAIN") + { + match client.conn.execute(sql, (), None).map_err(|e| e.to_string())? { + Some(mut cursor) => { + let col_count = cursor.num_result_cols().map_err(|e| e.to_string())? as u16; + let columns: Vec = (1..=col_count) + .map(|i| cursor.col_name(i).map_err(|e| e.to_string()).unwrap_or_else(|_| format!("col{i}"))) + .collect(); + + let buffer = TextRowSet::for_cursor(1000, &mut cursor, Some(8192)).map_err(|e| e.to_string())?; + let mut row_cursor = cursor.bind_buffer(buffer).map_err(|e| e.to_string())?; + + let mut rows = Vec::new(); + while let Some(batch) = row_cursor.fetch().map_err(|e| e.to_string())? { + for row_idx in 0..batch.num_rows() { + let vals: Vec = (0..col_count as usize) + .map(|col| { + batch + .at(col, row_idx) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .map(|s| serde_json::Value::String(s.to_string())) + .unwrap_or(serde_json::Value::Null) + }) + .collect(); + rows.push(vals); + if rows.len() >= crate::query::MAX_ROWS { + break; + } + } + if rows.len() >= crate::query::MAX_ROWS { + break; + } + } + + let truncated = rows.len() >= crate::query::MAX_ROWS; + Ok(QueryResult { + columns, + rows, + affected_rows: 0, + execution_time_ms: start.elapsed().as_millis(), + truncated, + }) + } + None => Ok(QueryResult { + columns: vec![], + rows: vec![], + affected_rows: 0, + execution_time_ms: start.elapsed().as_millis(), + truncated: false, + }), + } + } else { + match client.conn.execute(sql, (), None) { + Ok(Some(_cursor)) => Ok(QueryResult { + columns: vec![], + rows: vec![], + affected_rows: 0, + execution_time_ms: start.elapsed().as_millis(), + truncated: false, + }), + Ok(None) => Ok(QueryResult { + columns: vec![], + rows: vec![], + affected_rows: 0, + execution_time_ms: start.elapsed().as_millis(), + truncated: false, + }), + Err(e) => Err(e.to_string()), + } + } +} diff --git a/crates/dbx-core/src/db/mod.rs b/crates/dbx-core/src/db/mod.rs index a77f56fb1..da872f2bd 100644 --- a/crates/dbx-core/src/db/mod.rs +++ b/crates/dbx-core/src/db/mod.rs @@ -1,4 +1,5 @@ pub mod clickhouse_driver; +pub mod dm_driver; pub mod duckdb_driver; pub mod elasticsearch_driver; pub mod file_validator; diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs index 783018277..fd76823ea 100644 --- a/crates/dbx-core/src/models/connection.rs +++ b/crates/dbx-core/src/models/connection.rs @@ -70,6 +70,7 @@ pub enum DatabaseType { #[serde(rename = "starrocks")] StarRocks, Redshift, + Dameng, } impl ConnectionConfig { @@ -128,6 +129,7 @@ impl ConnectionConfig { } DatabaseType::Oracle => format!("oracle://{host}:{port}{db_part}"), DatabaseType::Elasticsearch => format!("http://{host}:{port}"), + DatabaseType::Dameng => format!("dm://{host}:{port}{db_part}"), } } @@ -186,6 +188,9 @@ impl ConnectionConfig { format!("oracle://{}:{}@{host}:{port}{db_part}", username, password) } DatabaseType::Elasticsearch => format!("http://{host}:{port}"), + DatabaseType::Dameng => { + format!("dm://{}:{}@{host}:{port}{db_part}", username, password) + } } } diff --git a/crates/dbx-core/src/query.rs b/crates/dbx-core/src/query.rs index e548c7077..e56c40979 100644 --- a/crates/dbx-core/src/query.rs +++ b/crates/dbx-core/src/query.rs @@ -214,6 +214,20 @@ pub async fn do_execute( PoolKind::Elasticsearch(_) => Err("Use document browser for Elasticsearch".to_string()), PoolKind::Redis(_) => Err("Use Redis-specific commands".to_string()), PoolKind::MongoDb(_) => Err("Use MongoDB-specific commands".to_string()), + PoolKind::Dameng(client) => { + let client = client.clone(); + let sql = sql.to_string(); + drop(connections); + wait_for_query(cancel_token, async move { + let task = tokio::task::spawn_blocking(move || { + let client = client.lock().map_err(|e| e.to_string())?; + db::dm_driver::execute_query_sync(&client, &sql) + }); + task.await.map_err(|e| e.to_string())? + }) + .await + .map(truncate_result) + } } } diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs index a0dd5f624..5d7581d37 100644 --- a/crates/dbx-core/src/schema.rs +++ b/crates/dbx-core/src/schema.rs @@ -100,6 +100,16 @@ pub fn extract_oracle( } } +pub fn extract_dameng( + connections: &HashMap, + key: &str, +) -> Option>> { + match connections.get(key)? { + PoolKind::Dameng(client) => Some(client.clone()), + _ => None, + } +} + pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Result, String> { { let connections = state.connections.lock().await; @@ -117,6 +127,11 @@ pub async fn list_databases_core(state: &AppState, connection_id: &str) -> Resul let client = client.lock().await; return db::oracle_driver::list_databases(&*client).await; } + if let Some(client) = extract_dameng(&connections, connection_id) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::dm_driver::list_databases(&client); + } } let connections = state.connections.lock().await; @@ -146,6 +161,11 @@ pub async fn list_schemas_core(state: &AppState, connection_id: &str, database: let client = client.lock().await; return db::oracle_driver::list_schemas(&*client).await; } + if let Some(client) = extract_dameng(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::dm_driver::list_schemas(&client); + } } let connections = state.connections.lock().await; @@ -186,6 +206,11 @@ pub async fn list_tables_core( let client = client.lock().await; return db::oracle_driver::list_tables(&*client, schema).await; } + if let Some(client) = extract_dameng(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::dm_driver::list_tables(&client, schema); + } } let connections = state.connections.lock().await; @@ -229,6 +254,11 @@ pub async fn get_columns_core( let client = client.lock().await; return db::oracle_driver::get_columns(&*client, schema, table).await; } + if let Some(client) = extract_dameng(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::dm_driver::get_columns(&client, schema, table); + } } let connections = state.connections.lock().await; @@ -263,6 +293,11 @@ pub async fn list_indexes_core( let client = client.lock().await; return db::oracle_driver::list_indexes(&*client, schema, table).await; } + if let Some(client) = extract_dameng(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::dm_driver::list_indexes(&client, schema, table); + } } let connections = state.connections.lock().await; @@ -297,6 +332,11 @@ pub async fn list_foreign_keys_core( let client = client.lock().await; return db::oracle_driver::list_foreign_keys(&*client, schema, table).await; } + if let Some(client) = extract_dameng(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::dm_driver::list_foreign_keys(&client, schema, table); + } } let connections = state.connections.lock().await; @@ -331,6 +371,11 @@ pub async fn list_triggers_core( let client = client.lock().await; return db::oracle_driver::list_triggers(&*client, schema, table).await; } + if let Some(client) = extract_dameng(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return db::dm_driver::list_triggers(&client, schema, table); + } } let connections = state.connections.lock().await; @@ -391,6 +436,11 @@ pub async fn get_table_ddl_core( let client = client.lock().await; return build_oracle_ddl(&*client, schema, table).await; } + if let Some(client) = extract_dameng(&connections, &pool_key) { + drop(connections); + let client = client.lock().map_err(|e| e.to_string())?; + return build_dameng_ddl(&client, schema, table); + } } let connections = state.connections.lock().await; @@ -598,3 +648,50 @@ pub async fn build_oracle_ddl( } Ok(ddl) } + +pub fn build_dameng_ddl(client: &db::dm_driver::DmClient, schema: &str, table: &str) -> Result { + let columns = db::dm_driver::get_columns(client, schema, table)?; + let indexes = db::dm_driver::list_indexes(client, schema, table)?; + let fkeys = db::dm_driver::list_foreign_keys(client, schema, table)?; + + let mut ddl = format!("CREATE TABLE \"{schema}\".\"{table}\" (\n"); + let col_lines: Vec = columns + .iter() + .map(|c| { + let mut line = format!(" \"{}\" {}", c.name, c.data_type); + if !c.is_nullable { + line.push_str(" NOT NULL"); + } + if let Some(ref def) = c.column_default { + line.push_str(&format!(" DEFAULT {def}")); + } + line + }) + .collect(); + ddl.push_str(&col_lines.join(",\n")); + + let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect(); + if !pks.is_empty() { + ddl.push_str(&format!( + ",\n PRIMARY KEY ({})", + pks.iter().map(|k| format!("\"{k}\"")).collect::>().join(", ") + )); + } + for fk in &fkeys { + ddl.push_str(&format!( + ",\n CONSTRAINT \"{}\" FOREIGN KEY (\"{}\") REFERENCES \"{}\"(\"{}\")", + fk.name, fk.column, fk.ref_table, fk.ref_column + )); + } + ddl.push_str("\n);\n"); + + for idx in &indexes { + if idx.is_primary { + continue; + } + let unique = if idx.is_unique { "UNIQUE " } else { "" }; + let cols = idx.columns.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "); + ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\" ({cols});", idx.name)); + } + Ok(ddl) +} diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index e9f635420..06079305b 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -112,6 +112,15 @@ pub async fn test_connection(state: State<'_, Arc>, config: Connection db::elasticsearch_driver::EsClient::new(&url, Some(&config.username), Some(&config.password)); db::elasticsearch_driver::test_connection(&client).await.map(|_| "Connection successful".to_string()) } + DatabaseType::Dameng => db::dm_driver::connect( + &host, + port, + config.database.as_deref().unwrap_or(""), + &config.username, + &config.password, + ) + .await + .map(|_| "Connection successful".to_string()), }, }; @@ -179,6 +188,17 @@ pub async fn connect_db(state: State<'_, Arc>, config: ConnectionConfi db::elasticsearch_driver::test_connection(&client).await?; PoolKind::Elasticsearch(client) } + DatabaseType::Dameng => { + let client = db::dm_driver::connect( + &host, + port, + config.database.as_deref().unwrap_or(""), + &config.username, + &config.password, + ) + .await?; + PoolKind::Dameng(std::sync::Arc::new(std::sync::Mutex::new(client))) + } }; state.connections.lock().await.insert(id.clone(), pool); @@ -205,6 +225,7 @@ pub async fn disconnect_db(state: State<'_, Arc>, connection_id: Strin PoolKind::SqlServer(_) => {} PoolKind::Oracle(_) => {} PoolKind::Elasticsearch(_) => {} + PoolKind::Dameng(_) => {} } } } diff --git a/src/components/connection/ConnectionDialog.vue b/src/components/connection/ConnectionDialog.vue index 95e58301f..4ea7f0fef 100644 --- a/src/components/connection/ConnectionDialog.vue +++ b/src/components/connection/ConnectionDialog.vue @@ -166,7 +166,7 @@ const driverProfiles: Record< label: "CockroachDB", icon: "cockroachdb", }, - dm: { type: "postgres", port: 5236, user: "SYSDBA", label: "DM (Dameng)", icon: "dm" }, + dm: { type: "dameng", port: 5236, user: "SYSDBA", label: "DM (Dameng)", icon: "dm" }, tdengine: { type: "mysql", port: 6030, user: "root", label: "TDengine", icon: "tdengine" }, custom_mysql: { type: "mysql", @@ -322,6 +322,7 @@ const dbOptions = [ { value: "oracle", label: "Oracle" }, { value: "elasticsearch", label: "Elasticsearch" }, { value: "mariadb", label: "MariaDB" }, + { value: "dm", label: "DM (Dameng)" }, ]; const mysqlCompat = [ @@ -340,7 +341,6 @@ const pgCompat = [ { value: "gaussdb", label: "GaussDB" }, { value: "kingbase", label: "KingBase" }, { value: "vastbase", label: "Vastbase" }, - { value: "dm", label: "DM (Dameng)" }, { value: "redshift", label: "Redshift" }, { value: "cockroachdb", label: "CockroachDB" }, { value: "custom_postgres", label: "Custom" }, @@ -844,6 +844,13 @@ async function browseDbFilePath() {

{{ t("connection.dmCompatHint") }} + + {{ t("connection.dmDownload") }} +

diff --git a/src/components/diagram/SchemaDiagramDialog.vue b/src/components/diagram/SchemaDiagramDialog.vue index 43cb5fd89..c7c2d2bae 100644 --- a/src/components/diagram/SchemaDiagramDialog.vue +++ b/src/components/diagram/SchemaDiagramDialog.vue @@ -49,8 +49,8 @@ const props = defineProps<{ focusTableName?: string; }>(); -const SQL_TYPES: DatabaseType[] = ["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"]; -const SCHEMA_AWARE_TYPES: DatabaseType[] = ["postgres", "sqlserver", "oracle", "redshift"]; +const SQL_TYPES: DatabaseType[] = ["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift", "dameng"]; +const SCHEMA_AWARE_TYPES: DatabaseType[] = ["postgres", "sqlserver", "oracle", "redshift", "dameng"]; const CARD_WIDTH = 270; const COLUMN_ROW_HEIGHT = 24; const CARD_HEADER_HEIGHT = 44; diff --git a/src/components/diff/SchemaDiffDialog.vue b/src/components/diff/SchemaDiffDialog.vue index cc8535c3b..985393b8a 100644 --- a/src/components/diff/SchemaDiffDialog.vue +++ b/src/components/diff/SchemaDiffDialog.vue @@ -76,7 +76,8 @@ async function resolveSchema(connectionId: string, database: string): Promise(["postgres", "sqlserver", "oracle", "redshift"]); +const SCHEMA_AWARE_TYPES = new Set(["postgres", "sqlserver", "oracle", "redshift", "dameng"]); const SYSTEM_SCHEMAS = new Set([ "information_schema", "pg_catalog", diff --git a/src/components/sidebar/TreeItem.vue b/src/components/sidebar/TreeItem.vue index 903315283..644e43ccc 100644 --- a/src/components/sidebar/TreeItem.vue +++ b/src/components/sidebar/TreeItem.vue @@ -87,7 +87,7 @@ const emit = defineEmits<{ }>(); const sqlFileUnsupportedTypes = new Set(["redis", "mongodb", "elasticsearch"]); -const diagramSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"]); +const diagramSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift", "dameng"]); const databaseSearchSupportedTypes = new Set([ "mysql", "postgres", @@ -97,6 +97,7 @@ const databaseSearchSupportedTypes = new Set([ "redshift", "duckdb", "clickhouse", + "dameng", ]); const tableImportSupportedTypes = new Set([ "mysql", @@ -109,9 +110,18 @@ const tableImportSupportedTypes = new Set([ "doris", "starrocks", "redshift", + "dameng", ]); const tableStructureSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver"]); -const fieldLineageSupportedTypes = new Set(["mysql", "postgres", "sqlite", "sqlserver", "oracle", "redshift"]); +const fieldLineageSupportedTypes = new Set([ + "mysql", + "postgres", + "sqlite", + "sqlserver", + "oracle", + "redshift", + "dameng", +]); const isExportingDatabase = ref(false); function currentDatabaseType(): DatabaseType | undefined { @@ -123,7 +133,7 @@ function quoteIdent(name: string): string { } function isSchemaAwareDbType(dbType?: DatabaseType): boolean { - return dbType === "postgres" || dbType === "oracle" || dbType === "sqlserver"; + return dbType === "postgres" || dbType === "oracle" || dbType === "sqlserver" || dbType === "dameng"; } function qualifiedTableName(tableName: string, schema?: string): string { @@ -277,7 +287,11 @@ async function openData() { if (!config) throw new Error("Connection config not found"); const qualifiedName = - (config.db_type === "postgres" || config.db_type === "oracle" || config.db_type === "sqlserver") && node.schema + (config.db_type === "postgres" || + config.db_type === "oracle" || + config.db_type === "sqlserver" || + config.db_type === "dameng") && + node.schema ? `${quoteIdent(node.schema)}.${quoteIdent(node.label)}` : quoteIdent(node.label); @@ -286,7 +300,7 @@ async function openData() { const pks = columns.filter((c) => c.is_primary_key).map((c) => c.name); const order = pks.length ? ` ORDER BY ${pks.map((pk) => `${quoteIdent(pk)} ASC`).join(", ")}` : ""; let sql: string; - if (config.db_type === "oracle") { + if (config.db_type === "oracle" || config.db_type === "dameng") { sql = `SELECT * FROM ${qualifiedName}${order} FETCH FIRST 100 ROWS ONLY`; } else if (config.db_type === "sqlserver") { sql = `SELECT TOP 100 * FROM ${qualifiedName}${order}`; @@ -462,7 +476,7 @@ async function confirmDuplicateStructure() { sql = `CREATE TABLE ${target} (LIKE ${source} INCLUDING ALL);`; } else if (dbType === "sqlserver") { sql = `SELECT TOP 0 * INTO ${target} FROM ${source};`; - } else if (dbType === "oracle") { + } else if (dbType === "oracle" || dbType === "dameng") { sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 1=0`; } else { sql = `CREATE TABLE ${target} AS SELECT * FROM ${source} WHERE 0;`; @@ -653,7 +667,11 @@ async function exportData(format: "csv" | "json" | "sql") { try { await connectionStore.ensureConnected(node.connectionId); const qualifiedName = - (config.db_type === "postgres" || config.db_type === "oracle" || config.db_type === "sqlserver") && node.schema + (config.db_type === "postgres" || + config.db_type === "oracle" || + config.db_type === "sqlserver" || + config.db_type === "dameng") && + node.schema ? `${quoteIdent(node.schema)}.${quoteIdent(node.label)}` : quoteIdent(node.label); const result = await api.executeQuery(node.connectionId, node.database, `SELECT * FROM ${qualifiedName}`); diff --git a/src/components/transfer/DataTransferDialog.vue b/src/components/transfer/DataTransferDialog.vue index 96fdf5c86..32cae1db1 100644 --- a/src/components/transfer/DataTransferDialog.vue +++ b/src/components/transfer/DataTransferDialog.vue @@ -25,7 +25,16 @@ const props = defineProps<{ const store = useConnectionStore(); -const SQL_TYPES: DatabaseType[] = ["mysql", "postgres", "sqlite", "sqlserver", "oracle", "clickhouse", "duckdb"]; +const SQL_TYPES: DatabaseType[] = [ + "mysql", + "postgres", + "sqlite", + "sqlserver", + "oracle", + "clickhouse", + "duckdb", + "dameng", +]; const sqlConnections = computed(() => store.connections.filter((c) => SQL_TYPES.includes(c.db_type))); @@ -122,7 +131,10 @@ async function loadTables() { try { const config = store.getConfig(sourceConnectionId.value); const needsSchema = - config?.db_type === "postgres" || config?.db_type === "sqlserver" || config?.db_type === "oracle"; + config?.db_type === "postgres" || + config?.db_type === "sqlserver" || + config?.db_type === "oracle" || + config?.db_type === "dameng"; if (needsSchema) { const schemas = await api.listSchemas(sourceConnectionId.value, sourceDatabase.value); sourceSchema.value = schemas.includes("public") ? "public" : (schemas[0] ?? ""); @@ -210,7 +222,10 @@ async function startTransfer() { // Auto-detect target schema const targetConfig = store.getConfig(targetConnectionId.value); const targetNeedsSchema = - targetConfig?.db_type === "postgres" || targetConfig?.db_type === "sqlserver" || targetConfig?.db_type === "oracle"; + targetConfig?.db_type === "postgres" || + targetConfig?.db_type === "sqlserver" || + targetConfig?.db_type === "oracle" || + targetConfig?.db_type === "dameng"; if (targetNeedsSchema && !targetSchema.value) { try { const schemas = await api.listSchemas(targetConnectionId.value, targetDatabase.value); diff --git a/src/composables/useSchemaOptions.ts b/src/composables/useSchemaOptions.ts index dda19d742..2aba72a74 100644 --- a/src/composables/useSchemaOptions.ts +++ b/src/composables/useSchemaOptions.ts @@ -14,7 +14,7 @@ export function useSchemaOptions() { function isSchemaAware(connectionId: string): boolean { const dbType = connectionStore.getConfig(connectionId)?.db_type; - return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle"; + return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle" || dbType === "dameng"; } async function loadSchemaOptions(connectionId: string, database: string) { diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index e96a1d4b5..a0dbdd46a 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -111,7 +111,8 @@ export default { sshKeyPassphrasePlaceholder: "Leave empty if key is not encrypted", sshKeyPathBrowse: "Browse", sshExposeLan: "Expose tunnel to LAN", - dmCompatHint: "Requires DM PG compatibility mode (set COMPATIBLE_MODE=7 in dm.ini and restart)", + dmCompatHint: "Requires DM8 ODBC driver installed on your system.", + dmDownload: "Download from Dameng", mongoLegacyHint: "For older MongoDB servers, enter authSource=admin&authMechanism=SCRAM-SHA-1 here; add directConnection=true only when needed for direct standalone connections.", compatible: "Compatible", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index f3d0f2ae4..12c8bd44c 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -110,7 +110,8 @@ export default { sshKeyPassphrasePlaceholder: "密钥未加密则留空", sshKeyPathBrowse: "浏览", sshExposeLan: "允许局域网访问隧道", - dmCompatHint: "需要开启达梦 PG 兼容模式(dm.ini 中设置 COMPATIBLE_MODE=7 并重启服务)", + dmCompatHint: "需要在系统上安装达梦 DM8 ODBC 驱动程序。", + dmDownload: "前往达梦官网下载", mongoLegacyHint: "连接旧版 MongoDB 时,可在此填写 authSource=admin&authMechanism=SCRAM-SHA-1;直连单节点时可按需追加 directConnection=true。", compatible: "兼容", diff --git a/src/lib/tableSelectSql.ts b/src/lib/tableSelectSql.ts index fd3137053..316fb76ba 100644 --- a/src/lib/tableSelectSql.ts +++ b/src/lib/tableSelectSql.ts @@ -22,7 +22,13 @@ export function qualifiedTableName( options: Pick, ): string { const { databaseType, schema, tableName } = options; - if ((databaseType === "postgres" || databaseType === "oracle" || databaseType === "sqlserver") && schema) { + if ( + (databaseType === "postgres" || + databaseType === "oracle" || + databaseType === "sqlserver" || + databaseType === "dameng") && + schema + ) { return `${quoteTableIdentifier(databaseType, schema)}.${quoteTableIdentifier(databaseType, tableName)}`; } return quoteTableIdentifier(databaseType, tableName); @@ -47,7 +53,7 @@ export function buildTableSelectSql(options: BuildTableSelectSqlOptions): string const orderBy = options.orderBy ?? defaultOrderBy; const order = orderBy ? ` ORDER BY ${orderBy}` : ""; - if (databaseType === "oracle") { + if (databaseType === "oracle" || databaseType === "dameng") { const offset = options.offset ? ` OFFSET ${options.offset} ROWS` : ""; return `SELECT * FROM ${table}${where}${order}${offset} FETCH FIRST ${limit} ROWS ONLY`; } diff --git a/src/stores/connectionStore.ts b/src/stores/connectionStore.ts index 508f4533b..a9dbcad93 100644 --- a/src/stores/connectionStore.ts +++ b/src/stores/connectionStore.ts @@ -128,6 +128,7 @@ export const useConnectionStore = defineStore("connection", () => { doris: "Doris", starrocks: "StarRocks", redshift: "Redshift", + dameng: "DM (Dameng)", }; return { ...config, @@ -652,7 +653,7 @@ export const useConnectionStore = defineStore("connection", () => { function isSchemaAwareDatabase(connectionId: string): boolean { const dbType = getConfig(connectionId)?.db_type; - return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle"; + return dbType === "postgres" || dbType === "sqlserver" || dbType === "oracle" || dbType === "dameng"; } async function listCompletionTables(connectionId: string, database: string): Promise { diff --git a/src/types/database.ts b/src/types/database.ts index 5d49a9d21..84e30fd05 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -11,7 +11,8 @@ export type DatabaseType = | "elasticsearch" | "doris" | "starrocks" - | "redshift"; + | "redshift" + | "dameng"; export interface ConnectionConfig { id: string;