diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index 11b37ecf08..1d37c327bc 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -680,6 +680,7 @@ const gaussdbDriverMode = computed({ }, }); const isGaussdbMJdbcConnection = computed(() => gaussdbDriverMode.value === "m-jdbc"); +const isGaussdbDwsConnection = computed(() => gaussdbDriverMode.value === "dws"); const showGaussdbIdentifierQuoteStyle = computed(() => supportsGaussdbIdentifierQuoteStyle(form.value)); const gaussdbQuoteStyle = computed({ get: () => gaussdbIdentifierQuoteStyle(form.value), @@ -10239,10 +10240,11 @@ function openExternalUrl(url: string) { {{ t("connection.gaussdbConnectionModeNative") }} {{ t("connection.gaussdbConnectionModeMJdbc") }} + {{ t("connection.gaussdbConnectionModeDws") }}

- {{ isGaussdbMJdbcConnection ? t("connection.gaussdbConnectionModeMJdbcHint") : t("connection.gaussdbConnectionModeNativeHint") }} + {{ isGaussdbMJdbcConnection ? t("connection.gaussdbConnectionModeMJdbcHint") : isGaussdbDwsConnection ? t("connection.gaussdbConnectionModeDwsHint") : t("connection.gaussdbConnectionModeNativeHint") }}

diff --git a/apps/desktop/src/i18n/locales/az.ts b/apps/desktop/src/i18n/locales/az.ts index 8721797500..6efcab7b41 100644 --- a/apps/desktop/src/i18n/locales/az.ts +++ b/apps/desktop/src/i18n/locales/az.ts @@ -1119,6 +1119,9 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "M rejimi (istehsalçının JDBC drayveri)", gaussdbConnectionModeNativeHint: "GaussDB A/PG/Oracle uyğunluq rejimləri üçün DBX-in daxili PostgreSQL ilə uyğun protokolundan istifadə edir.", gaussdbConnectionModeMJdbcHint: "GaussDB M rejimi üçün com.huawei.gaussdb.jdbc.Driver ilə jdbc:gaussdb-dən istifadə edir.", + gaussdbConnectionModeDws: "Huawei DWS (PostgreSQL ilə uyğun daxili drayver)", + gaussdbConnectionModeDwsHint: + "Bu bağlantını açıq şəkildə Huawei DWS kimi işarələyir. Plugin Host Estimated Plan API açıq DWS profili üçün serverin kiçik versiyasından asılı olmayaraq aktiv olur; adi GaussDB və M-JDBC bağlantıları dəstəklənmir. Daxili PostgreSQL-ə uyğun drayverdən istifadə edilir.", gaussdbMJdbcDriver: "GaussDB JDBC drayveri", gaussdbMJdbcDriverPlaceholder: "Server versiyasına uyğun gaussdbjdbc.jar seçin", gaussdbMJdbcDriverHint: "Drayver hər GaussDB server versiyası ilə birlikdə buraxılır. gaussdbjdbc.jar faylını uyğun GaussDB drayver paketindən əldə edin.", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 9c269338c3..f025493992 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1768,6 +1768,9 @@ export default { gaussdbConnectionModeMJdbc: "M mode (vendor JDBC driver)", gaussdbConnectionModeNativeHint: "Uses DBX's built-in PostgreSQL-compatible protocol for GaussDB A/PG/Oracle compatibility modes.", gaussdbConnectionModeMJdbcHint: "Uses jdbc:gaussdb with com.huawei.gaussdb.jdbc.Driver for GaussDB M mode.", + gaussdbConnectionModeDws: "Huawei DWS (native PostgreSQL-compatible driver)", + gaussdbConnectionModeDwsHint: + "Explicitly marks this connection as Huawei DWS. The Estimated Plan Host API is enabled for this profile without pinning a GaussDB minor version; generic GaussDB and M-JDBC connections remain unsupported. It uses the built-in PostgreSQL-compatible driver. Select only for verified DWS connections.", gaussdbMJdbcDriver: "GaussDB JDBC driver", gaussdbMJdbcDriverPlaceholder: "Select gaussdbjdbc.jar matching the server version", gaussdbMJdbcDriverHint: "The driver is released with each GaussDB server version. Obtain gaussdbjdbc.jar from the matching GaussDB driver package.", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index e2a2ef49e7..e0e3034e9d 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1422,6 +1422,9 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "Modo M (controlador JDBC del proveedor)", gaussdbConnectionModeNativeHint: "Usa el protocolo compatible con PostgreSQL integrado en DBX para los modos A/PG/Oracle de GaussDB.", gaussdbConnectionModeMJdbcHint: "Usa jdbc:gaussdb con com.huawei.gaussdb.jdbc.Driver para el modo M de GaussDB.", + gaussdbConnectionModeDws: "Huawei DWS (controlador nativo compatible con PostgreSQL)", + gaussdbConnectionModeDwsHint: + "Marca explícitamente esta conexión como Huawei DWS. La API Estimated Plan de Plugin Host se activa para este perfil sin limitarse a una versión menor de GaussDB; las conexiones genéricas de GaussDB y M-JDBC siguen sin estar admitidas. Se utiliza el controlador integrado compatible con PostgreSQL.", gaussdbMJdbcDriver: "Controlador JDBC de GaussDB", gaussdbMJdbcDriverPlaceholder: "Seleccione gaussdbjdbc.jar compatible con la versión del servidor", gaussdbMJdbcDriverHint: "El controlador se publica con cada versión de GaussDB. Obtenga gaussdbjdbc.jar del paquete de controladores correspondiente.", diff --git a/apps/desktop/src/i18n/locales/id.ts b/apps/desktop/src/i18n/locales/id.ts index 34be656981..e591744f0b 100644 --- a/apps/desktop/src/i18n/locales/id.ts +++ b/apps/desktop/src/i18n/locales/id.ts @@ -1384,6 +1384,9 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "Mode M (driver JDBC vendor)", gaussdbConnectionModeNativeHint: "Menggunakan protokol kompatibel PostgreSQL bawaan DBX untuk mode kompatibilitas GaussDB A/PG/Oracle.", gaussdbConnectionModeMJdbcHint: "Menggunakan jdbc:gaussdb dengan com.huawei.gaussdb.jdbc.Driver untuk mode M GaussDB.", + gaussdbConnectionModeDws: "Huawei DWS (driver bawaan kompatibel PostgreSQL)", + gaussdbConnectionModeDwsHint: + "Menandai koneksi ini secara eksplisit sebagai Huawei DWS. API Estimated Plan Plugin Host aktif untuk profil ini tanpa membatasi versi minor GaussDB; koneksi GaussDB umum dan M-JDBC tetap tidak didukung. Koneksi menggunakan driver bawaan yang kompatibel dengan PostgreSQL.", gaussdbMJdbcDriver: "Driver JDBC GaussDB", gaussdbMJdbcDriverPlaceholder: "Pilih gaussdbjdbc.jar yang sesuai dengan versi server", gaussdbMJdbcDriverHint: "Driver dirilis bersama setiap versi server GaussDB. Dapatkan gaussdbjdbc.jar dari paket driver GaussDB yang sesuai.", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index c5f02850cd..a0cb6707d2 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1345,6 +1345,9 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "Modalità M (driver JDBC del fornitore)", gaussdbConnectionModeNativeHint: "Usa il protocollo compatibile con PostgreSQL integrato in DBX per le modalità A/PG/Oracle di GaussDB.", gaussdbConnectionModeMJdbcHint: "Usa jdbc:gaussdb con com.huawei.gaussdb.jdbc.Driver per la modalità M di GaussDB.", + gaussdbConnectionModeDws: "Huawei DWS (driver nativo compatibile con PostgreSQL)", + gaussdbConnectionModeDwsHint: + "Identifica esplicitamente questa connessione come Huawei DWS. L'API Estimated Plan di Plugin Host si attiva per questo profilo senza vincolarsi a una versione secondaria di GaussDB; le connessioni GaussDB generiche e M-JDBC restano non supportate. La connessione usa il driver integrato compatibile con PostgreSQL.", gaussdbMJdbcDriver: "Driver JDBC GaussDB", gaussdbMJdbcDriverPlaceholder: "Seleziona gaussdbjdbc.jar corrispondente alla versione del server", gaussdbMJdbcDriverHint: "Il driver viene distribuito con ogni versione di GaussDB. Ottieni gaussdbjdbc.jar dal pacchetto driver corrispondente.", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 9f92b4debf..0dca3151b0 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1315,6 +1315,9 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "M モード(ベンダー JDBC ドライバー)", gaussdbConnectionModeNativeHint: "GaussDB の A/PG/Oracle 互換モードでは、DBX 内蔵の PostgreSQL 互換プロトコルを使用します。", gaussdbConnectionModeMJdbcHint: "GaussDB M モードでは、jdbc:gaussdb と com.huawei.gaussdb.jdbc.Driver を使用します。", + gaussdbConnectionModeDws: "Huawei DWS(PostgreSQL 互換の内蔵ドライバー)", + gaussdbConnectionModeDwsHint: + "この接続を Huawei DWS として明示的に識別します。このプロファイルでは GaussDB のマイナーバージョンを固定せずに Plugin Host の Estimated Plan API を有効にします。一般的な GaussDB 接続と M-JDBC は引き続きサポートされません。接続には PostgreSQL 互換の内蔵ドライバーを使用します。", gaussdbMJdbcDriver: "GaussDB JDBC ドライバー", gaussdbMJdbcDriverPlaceholder: "サーバーバージョンに一致する gaussdbjdbc.jar を選択", gaussdbMJdbcDriverHint: "ドライバーは GaussDB サーバーの各バージョンとともに配布されます。対応するドライバーパッケージから gaussdbjdbc.jar を取得してください。", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index ddfba90624..01f7ed1afc 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1441,6 +1441,8 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "M 모드(공급업체 JDBC 드라이버)", gaussdbConnectionModeNativeHint: "GaussDB A/PG/Oracle 호환 모드에서는 DBX 내장 PostgreSQL 호환 프로토콜을 사용합니다.", gaussdbConnectionModeMJdbcHint: "GaussDB M 모드에서는 jdbc:gaussdb와 com.huawei.gaussdb.jdbc.Driver를 사용합니다.", + gaussdbConnectionModeDws: "Huawei DWS (PostgreSQL 호환 내장 드라이버)", + gaussdbConnectionModeDwsHint: "이 연결을 Huawei DWS로 명시적으로 표시합니다. 이 프로필에서는 GaussDB 마이너 버전을 고정하지 않고 Plugin Host Estimated Plan API를 활성화합니다. 일반 GaussDB 및 M-JDBC 연결은 계속 지원되지 않습니다. PostgreSQL 호환 내장 드라이버를 사용합니다.", gaussdbMJdbcDriver: "GaussDB JDBC 드라이버", gaussdbMJdbcDriverPlaceholder: "서버 버전에 맞는 gaussdbjdbc.jar 선택", gaussdbMJdbcDriverHint: "드라이버는 각 GaussDB 서버 버전과 함께 배포됩니다. 해당 GaussDB 드라이버 패키지에서 gaussdbjdbc.jar를 가져오세요.", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 8071487994..14f22afb71 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1346,6 +1346,9 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "Modo M (driver JDBC do fornecedor)", gaussdbConnectionModeNativeHint: "Usa o protocolo compatível com PostgreSQL integrado ao DBX para os modos A/PG/Oracle do GaussDB.", gaussdbConnectionModeMJdbcHint: "Usa jdbc:gaussdb com com.huawei.gaussdb.jdbc.Driver para o modo M do GaussDB.", + gaussdbConnectionModeDws: "Huawei DWS (driver nativo compatível com PostgreSQL)", + gaussdbConnectionModeDwsHint: + "Marca explicitamente esta conexão como Huawei DWS. A API Estimated Plan do Plugin Host é ativada para este perfil sem limitar uma versão secundária do GaussDB; conexões genéricas do GaussDB e M-JDBC continuam sem suporte. A conexão usa o driver integrado compatível com PostgreSQL.", gaussdbMJdbcDriver: "Driver JDBC do GaussDB", gaussdbMJdbcDriverPlaceholder: "Selecione gaussdbjdbc.jar compatível com a versão do servidor", gaussdbMJdbcDriverHint: "O driver é distribuído com cada versão do GaussDB. Obtenha gaussdbjdbc.jar no pacote de driver correspondente.", diff --git a/apps/desktop/src/i18n/locales/ru.ts b/apps/desktop/src/i18n/locales/ru.ts index b046d13910..36881bdd13 100644 --- a/apps/desktop/src/i18n/locales/ru.ts +++ b/apps/desktop/src/i18n/locales/ru.ts @@ -1534,6 +1534,9 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "Режим M (JDBC-драйвер производителя)", gaussdbConnectionModeNativeHint: "Использует встроенный в DBX протокол, совместимый с PostgreSQL, для режимов совместимости GaussDB A/PG/Oracle.", gaussdbConnectionModeMJdbcHint: "Использует jdbc:gaussdb с com.huawei.gaussdb.jdbc.Driver для режима GaussDB M.", + gaussdbConnectionModeDws: "Huawei DWS (встроенный драйвер с совместимостью PostgreSQL)", + gaussdbConnectionModeDwsHint: + "Явно указывает, что это подключение Huawei DWS. Для этого профиля API Estimated Plan Plugin Host включается без привязки к минорной версии GaussDB; обычные подключения GaussDB и M-JDBC по-прежнему не поддерживаются. Используется встроенный драйвер с совместимостью PostgreSQL.", gaussdbMJdbcDriver: "JDBC-драйвер GaussDB", gaussdbMJdbcDriverPlaceholder: "Выберите gaussdbjdbc.jar, соответствующий версии сервера", gaussdbMJdbcDriverHint: "Драйвер выпускается для каждой версии сервера GaussDB. Получите gaussdbjdbc.jar из соответствующего пакета драйверов GaussDB.", diff --git a/apps/desktop/src/i18n/locales/th.ts b/apps/desktop/src/i18n/locales/th.ts index 0dfc98adac..ce36c308e5 100644 --- a/apps/desktop/src/i18n/locales/th.ts +++ b/apps/desktop/src/i18n/locales/th.ts @@ -1730,6 +1730,8 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "โหมด M (Driver JDBC ของผู้จำหน่าย)", gaussdbConnectionModeNativeHint: "ใช้โปรโตคอลที่เข้ากันได้กับ PostgreSQL ในตัวของ DBX สำหรับโหมดความเข้ากันได้กับ GaussDB A/PG/Oracle", gaussdbConnectionModeMJdbcHint: "ใช้ jdbc:gaussdb กับ com.huawei.gaussdb.jdbc.Driver สำหรับโหมด GaussDB M", + gaussdbConnectionModeDws: "Huawei DWS (Driver ในตัวที่รองรับ PostgreSQL)", + gaussdbConnectionModeDwsHint: "ระบุการเชื่อมต่อนี้อย่างชัดเจนว่าเป็น Huawei DWS โดยโปรไฟล์นี้จะเปิดใช้ API Estimated Plan ของ Plugin Host โดยไม่จำกัดเวอร์ชันย่อยของ GaussDB การเชื่อมต่อ GaussDB ทั่วไปและ M-JDBC ยังไม่รองรับ และยังใช้ไดรเวอร์ในตัวที่รองรับ PostgreSQL", gaussdbMJdbcDriver: "Driver JDBC ของ GaussDB", gaussdbMJdbcDriverPlaceholder: "เลือก gaussdbjdbc.jar ที่ตรงกับเวอร์ชัน Server", gaussdbMJdbcDriverHint: "Driver ออกพร้อมกับ GaussDB แต่ละเวอร์ชัน รับ gaussdbjdbc.jar จากแพ็กเกจ Driver ของ GaussDB ที่ตรงกัน", diff --git a/apps/desktop/src/i18n/locales/tr.ts b/apps/desktop/src/i18n/locales/tr.ts index 99943daf03..2a7cdd29d9 100644 --- a/apps/desktop/src/i18n/locales/tr.ts +++ b/apps/desktop/src/i18n/locales/tr.ts @@ -1119,6 +1119,8 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "M modu (üreticinin JDBC sürücüsü)", gaussdbConnectionModeNativeHint: "GaussDB A/PG/Oracle uyumluluk modları için DBX'in yerleşik PostgreSQL uyumlu protokolünü kullanır.", gaussdbConnectionModeMJdbcHint: "GaussDB M modu için com.huawei.gaussdb.jdbc.Driver ile jdbc:gaussdb kullanır.", + gaussdbConnectionModeDws: "Huawei DWS (yerel PostgreSQL uyumlu sürücü)", + gaussdbConnectionModeDwsHint: "Bu bağlantıyı açıkça Huawei DWS olarak işaretler. Plugin Host Estimated Plan API'si bu profil için GaussDB alt sürümüne sabitlenmeden etkinleşir; genel GaussDB ve M-JDBC bağlantıları desteklenmez. Yerleşik PostgreSQL uyumlu sürücü kullanılmaya devam eder.", gaussdbMJdbcDriver: "GaussDB JDBC sürücüsü", gaussdbMJdbcDriverPlaceholder: "Sunucu sürümüyle eşleşen gaussdbjdbc.jar dosyasını seçin", gaussdbMJdbcDriverHint: "Sürücü her GaussDB sunucu sürümüyle birlikte yayımlanır. gaussdbjdbc.jar dosyasını eşleşen GaussDB sürücü paketinden alın.", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 9b64efd7ac..13477ad8ae 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1703,6 +1703,8 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "M 模式(厂商 JDBC 驱动)", gaussdbConnectionModeNativeHint: "使用 DBX 内置的 PostgreSQL 兼容协议,适用于 GaussDB 的 A/PG/Oracle 兼容模式。", gaussdbConnectionModeMJdbcHint: "使用 jdbc:gaussdb 协议和 com.huawei.gaussdb.jdbc.Driver,适用于 GaussDB M 模式。", + gaussdbConnectionModeDws: "Huawei DWS(内置 PostgreSQL 兼容驱动)", + gaussdbConnectionModeDwsHint: "显式将此连接标记为 Huawei DWS。该 profile 下按 DWS 身份启用 Plugin Host Estimated Plan API,不再限制 GaussDB 小版本;普通 GaussDB 和 M-JDBC 连接仍不支持。连接仍使用内置 PostgreSQL 兼容驱动。", gaussdbMJdbcDriver: "GaussDB JDBC 驱动", gaussdbMJdbcDriverPlaceholder: "选择与服务端版本匹配的 gaussdbjdbc.jar", gaussdbMJdbcDriverHint: "驱动包随 GaussDB 服务端版本发布,请从对应版本的 GaussDB 驱动包中获取 gaussdbjdbc.jar。", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index eb6e67f185..dec2c649a5 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1355,6 +1355,8 @@ export default withEnglishFallback({ gaussdbConnectionModeMJdbc: "M 模式(原廠 JDBC 驅動程式)", gaussdbConnectionModeNativeHint: "使用 DBX 內建的 PostgreSQL 相容協定,適用於 GaussDB 的 A/PG/Oracle 相容模式。", gaussdbConnectionModeMJdbcHint: "使用 jdbc:gaussdb 協定與 com.huawei.gaussdb.jdbc.Driver,適用於 GaussDB M 模式。", + gaussdbConnectionModeDws: "Huawei DWS(內建 PostgreSQL 相容驅動程式)", + gaussdbConnectionModeDwsHint: "明確將此連線標記為 Huawei DWS。此 profile 依據 DWS 身分啟用 Plugin Host Estimated Plan API,不再限制 GaussDB 小版本;一般 GaussDB 與 M-JDBC 連線仍不支援。連線仍使用內建 PostgreSQL 相容驅動程式。", gaussdbMJdbcDriver: "GaussDB JDBC 驅動程式", gaussdbMJdbcDriverPlaceholder: "選擇與伺服器版本相符的 gaussdbjdbc.jar", gaussdbMJdbcDriverHint: "驅動程式隨 GaussDB 伺服器版本發布,請從對應版本的 GaussDB 驅動程式套件取得 gaussdbjdbc.jar。", diff --git a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts index 8c15f33789..dda3447c46 100644 --- a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest"; import type { ConnectionConfig } from "@/types/database"; import { databaseObjectTreeNodeSchema } from "@/lib/database/databaseFeatureSupport"; import { + GAUSSDB_DWS_PRODUCT_PROFILE, + GAUSSDB_DWS_PRODUCT_PROFILE_KEY, GAUSSDB_M_JDBC_DRIVER_CLASS, connectionDatabaseMetadataSchema, connectionObjectTreeNodeSchema, @@ -389,6 +391,25 @@ describe("GaussDB connection mode", () => { expect(connection.jdbc_driver_class).toBeUndefined(); }); + it("marks DWS explicitly while retaining the native GaussDB driver profile", () => { + const connection = { db_type: "gaussdb", driver_profile: "gaussdb", external_config: { retained: true } } as ConnectionConfig; + + setGaussdbConnectionMode(connection, "dws"); + expect(gaussdbConnectionMode(connection)).toBe("dws"); + expect(connection.driver_profile).toBe("gaussdb"); + expect(connection.jdbc_driver_class).toBeUndefined(); + expect(connection.external_config).toEqual({ retained: true, [GAUSSDB_DWS_PRODUCT_PROFILE_KEY]: GAUSSDB_DWS_PRODUCT_PROFILE }); + expect(effectiveDatabaseTypeForConnection(connection)).toBe("gaussdb"); + + setGaussdbConnectionMode(connection, "m-jdbc"); + expect(gaussdbConnectionMode(connection)).toBe("m-jdbc"); + expect(connection.external_config).toEqual({ retained: true }); + + setGaussdbConnectionMode(connection, "native"); + expect(gaussdbConnectionMode(connection)).toBe("native"); + expect(connection.external_config).toEqual({ retained: true }); + }); + it("uses the driver default and preserves targetServerType from legacy URL fields", () => { const connection = { db_type: "gaussdb", driver_profile: "gaussdb-m" } as ConnectionConfig; diff --git a/apps/desktop/src/lib/database/jdbcDialect.ts b/apps/desktop/src/lib/database/jdbcDialect.ts index e34bc532f9..a274453f96 100644 --- a/apps/desktop/src/lib/database/jdbcDialect.ts +++ b/apps/desktop/src/lib/database/jdbcDialect.ts @@ -5,15 +5,17 @@ import type { CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect export type JdbcDialectConnection = Partial>; export type GaussdbIdentifierQuoteStyle = "auto" | "double" | "backtick"; -export type GaussdbConnectionMode = "native" | "m-jdbc"; +export type GaussdbConnectionMode = "native" | "m-jdbc" | "dws"; export type GaussdbTargetServerType = "master" | "slave" | "any"; export type GaussdbCountQueryDop = 1 | 2 | 4 | 8 | 16; const GAUSSDB_IDENTIFIER_QUOTE_STYLE_KEY = "gaussdbIdentifierQuoteStyle"; +export const GAUSSDB_DWS_PRODUCT_PROFILE_KEY = "gaussdbProductProfile"; const GAUSSDB_TARGET_SERVER_TYPE_KEY = "gaussdbTargetServerType"; const GAUSSDB_COUNT_QUERY_DOP_KEY = "gaussdbCountQueryDop"; export const GAUSSDB_M_JDBC_DRIVER_PROFILE = "gaussdb-m"; export const GAUSSDB_M_JDBC_DRIVER_CLASS = "com.huawei.gaussdb.jdbc.Driver"; +export const GAUSSDB_DWS_PRODUCT_PROFILE = "huawei-dws"; const DATABASE_AS_EXECUTION_SCHEMA_TYPES = new Set(["hive", "kyuubi", "impala", "argo", "transwarp", "spark"]); const CONNECTION_ROOT_SCHEMA_TYPES = new Set(["oracle", "dameng", "oceanbase-oracle"]); @@ -171,11 +173,24 @@ export function gaussdbIdentifierQuoteOverride(connection: JdbcDialectConnection } export function gaussdbConnectionMode(connection: JdbcDialectConnection | undefined): GaussdbConnectionMode { - return connection?.db_type === "gaussdb" && connection.driver_profile?.toLowerCase() === GAUSSDB_M_JDBC_DRIVER_PROFILE ? "m-jdbc" : "native"; + if (connection?.db_type !== "gaussdb") return "native"; + const profile = connection.driver_profile?.toLowerCase(); + if (profile === GAUSSDB_M_JDBC_DRIVER_PROFILE) return "m-jdbc"; + if ((profile === undefined || profile === "gaussdb") && externalConfigRecord(connection.external_config)[GAUSSDB_DWS_PRODUCT_PROFILE_KEY] === GAUSSDB_DWS_PRODUCT_PROFILE) { + return "dws"; + } + return "native"; } export function setGaussdbConnectionMode(connection: JdbcDialectConnection, mode: GaussdbConnectionMode) { if (connection.db_type !== "gaussdb") return; + const external = externalConfigRecord(connection.external_config); + if (mode === "dws") { + external[GAUSSDB_DWS_PRODUCT_PROFILE_KEY] = GAUSSDB_DWS_PRODUCT_PROFILE; + } else { + delete external[GAUSSDB_DWS_PRODUCT_PROFILE_KEY]; + } + connection.external_config = Object.keys(external).length > 0 ? external : undefined; connection.driver_profile = mode === "m-jdbc" ? GAUSSDB_M_JDBC_DRIVER_PROFILE : "gaussdb"; connection.driver_label = "GaussDB"; connection.jdbc_driver_class = mode === "m-jdbc" ? GAUSSDB_M_JDBC_DRIVER_CLASS : undefined; diff --git a/apps/desktop/src/stores/__tests__/connectionStore.databaseInfo.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.databaseInfo.spec.ts index 2445baad45..6bcd580f48 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.databaseInfo.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.databaseInfo.spec.ts @@ -26,6 +26,20 @@ function mysqlConnection(): ConnectionConfig { }; } +function gaussdbDwsConnection(): ConnectionConfig { + return { + ...mysqlConnection(), + id: "gaussdb-dws-info", + name: "Huawei DWS", + db_type: "gaussdb", + driver_profile: "gaussdb", + driver_label: "GaussDB", + port: 8000, + external_config: { gaussdbProductProfile: "huawei-dws" }, + database_info: { productName: "GaussDB", productVersion: "PostgreSQL 9.2.4 (GaussDB 9.1.1)" }, + }; +} + describe("connectionStore database info", () => { beforeEach(() => { vi.resetModules(); @@ -72,6 +86,73 @@ describe("connectionStore database info", () => { }); expect(store.getConfig(config.id)?.database_info?.productVersion).toBe("8.0.34"); expect(store.connectedIds.has(config.id)).toBe(true); + }, 30_000); + + it.each(["connect", "ensureConnected"] as const)( + "refreshes DWS product metadata before %s exposes the connection", + async (method) => { + const config = gaussdbDwsConnection(); + let resolveDatabaseInfo!: (value: { productName: string; productVersion: string }) => void; + const databaseInfo = new Promise<{ productName: string; productVersion: string }>((resolve) => { + resolveDatabaseInfo = resolve; + }); + const connectionDatabaseInfo = vi.fn(() => databaseInfo); + const connectDb = vi.fn().mockResolvedValue(config.id); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + connectDb, + connectionDatabaseInfo, + saveConnectionDatabaseInfo: vi.fn().mockResolvedValue(undefined), + saveConnections: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + connectionIdentifierQuote: vi.fn().mockResolvedValue(undefined), + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const { gaussdbConnectionMode } = await import("@/lib/database/jdbcDialect"); + const store = useConnectionStore(); + expect(gaussdbConnectionMode(config)).toBe("dws"); + await store.addConnection(config); + expect(gaussdbConnectionMode(store.getConfig(config.id))).toBe("dws"); + const connecting = method === "connect" ? store.connect(config) : store.ensureConnected(config.id); + await vi.waitFor(() => expect(connectDb).toHaveBeenCalledWith(expect.objectContaining({ id: config.id }), expect.any(Number))); + await vi.waitFor(() => expect(connectionDatabaseInfo).toHaveBeenCalledWith(config.id)); + + // The connection is not exposed as ready until DBX refreshes its product metadata. + expect(store.connectedIds.has(config.id)).toBe(false); + resolveDatabaseInfo({ productName: "GaussDB", productVersion: "GaussDB 9.2.0" }); + await connecting; + + expect(store.connectedIds.has(config.id)).toBe(true); + expect(store.getConfig(config.id)?.database_info?.productVersion).toBe("GaussDB 9.2.0"); + }, + 30_000, + ); + + it("keeps a DWS connection usable when optional product metadata refresh fails", async () => { + const config = gaussdbDwsConnection(); + const connectionDatabaseInfo = vi.fn().mockRejectedValue(new Error("metadata unavailable")); + const saveConnectionDatabaseInfo = vi.fn().mockResolvedValue(undefined); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + connectDb: vi.fn().mockResolvedValue(config.id), + connectionDatabaseInfo, + saveConnectionDatabaseInfo, + saveConnections: vi.fn().mockResolvedValue(undefined), + saveSidebarLayout: vi.fn().mockResolvedValue(undefined), + connectionIdentifierQuote: vi.fn().mockResolvedValue(undefined), + })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + await store.addConnection(config); + await expect(store.connect(config)).resolves.toBe(config.id); + + expect(connectionDatabaseInfo).toHaveBeenCalledWith(config.id); + expect(saveConnectionDatabaseInfo).not.toHaveBeenCalled(); + expect(store.connectedIds.has(config.id)).toBe(true); }); it("preserves the connection tree node while background database info is stored", async () => { diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 7df455c223..a8437b5f6c 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -5,6 +5,7 @@ import { uuid } from "@/lib/common/utils"; import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin"; import { ref, computed, watch, markRaw, reactive } from "vue"; import { DEFAULT_QUERY_TIMEOUT_SECS, normalizeManualTransactionTimeoutSecs } from "@/lib/connection/timeoutLimits"; +import { gaussdbConnectionMode } from "@/lib/database/jdbcDialect"; import { notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents"; import type { ColumnInfo, @@ -4760,6 +4761,14 @@ export const useConnectionStore = defineStore("connection", () => { await ensureLocalConnectionAttemptActiveAfterConnectResult(config.id, localAttempt, id); await syncMongoLegacyDriverFallback(id, config); await ensureLocalConnectionAttemptActiveAfterConnectResult(config.id, localAttempt, id); + const refreshDwsMetadataBeforeReady = config.db_type === "gaussdb" && gaussdbConnectionMode(config) === "dws"; + if (config.db_type !== "plugin" && refreshDwsMetadataBeforeReady) { + // Refresh the product version before exposing the connection so `dbVersion` + // is current. Support comes from the explicit profile, not the version. + // Metadata remains optional: refreshConnectedDatabaseInfo swallows failures. + await refreshConnectedDatabaseInfo(id, { ...config, id }); + await ensureLocalConnectionAttemptActiveAfterConnectResult(config.id, localAttempt, id); + } passwordChangeReconnectRequired.delete(config.id); activeConnectionId.value = id; markConnectionConnected(id); @@ -4767,7 +4776,7 @@ export const useConnectionStore = defineStore("connection", () => { clearConnectionClosedByUser(config.id); clearConnectionClosedByUser(id); if (config.db_type !== "plugin") { - void refreshConnectedDatabaseInfo(id, { ...config, id }); + if (!refreshDwsMetadataBeforeReady) void refreshConnectedDatabaseInfo(id, { ...config, id }); await refreshConnectionIdentifierQuote(id, { ...config, id }); ensureLocalConnectionAttemptActive(config.id, localAttempt); // Compatibility modes warm asynchronously; the QueryEditor watcher and the @@ -5228,11 +5237,18 @@ export const useConnectionStore = defineStore("connection", () => { await ensureLocalConnectionAttemptActiveAfterConnectResult(connectionId, localAttempt, id); await syncMongoLegacyDriverFallback(connectionId, config); await ensureLocalConnectionAttemptActiveAfterConnectResult(connectionId, localAttempt, id); + const refreshDwsMetadataBeforeReady = config.db_type === "gaussdb" && gaussdbConnectionMode(config) === "dws"; + if (config.db_type !== "plugin" && refreshDwsMetadataBeforeReady) { + // Refresh the product version before reconnect completion so `dbVersion` + // reflects the new server session; support comes from the explicit profile. + await refreshConnectedDatabaseInfo(connectionId, config); + await ensureLocalConnectionAttemptActiveAfterConnectResult(connectionId, localAttempt, id); + } markConnectionConnected(connectionId); // 任何路径真的连上之后,"用户已关闭"标记就不能再拦后续的被动唤醒。 clearConnectionClosedByUser(connectionId); if (config.db_type !== "plugin") { - void refreshConnectedDatabaseInfo(connectionId, config); + if (!refreshDwsMetadataBeforeReady) void refreshConnectedDatabaseInfo(connectionId, config); await refreshConnectionIdentifierQuote(connectionId, config); ensureLocalConnectionAttemptActive(connectionId, localAttempt); void refreshConnectionDatabaseModes(connectionId, config); diff --git a/crates/dbx-core/src/connection/mod.rs b/crates/dbx-core/src/connection/mod.rs index f7e06031f0..52eb9ef772 100644 --- a/crates/dbx-core/src/connection/mod.rs +++ b/crates/dbx-core/src/connection/mod.rs @@ -250,6 +250,7 @@ enum ConnectionDatabaseInfoSource { session: Arc, }, NativeMysql(db::mysql::MySqlPool), + NativeGaussdbDws(deadpool_postgres::Pool), NativeHBase(db::hbase_driver::HBaseClient), NativeMongo(mongodb::Client, Option), Meilisearch(db::meilisearch_driver::MeilisearchClient), @@ -261,6 +262,12 @@ enum ConnectionDatabaseInfoSource { MessageQueue, } +async fn gaussdb_dws_product_version_from_pool(pool: &deadpool_postgres::Pool) -> Option { + let result = db::postgres::execute_query_with_max_rows(pool, "SELECT version()", Some(1)).await.ok()?; + let server_version = result.rows.first()?.first()?.as_str()?; + crate::query::plugin_plan::gaussdb_product_version_from_server_version(server_version) +} + /// Held connection for a manual transaction session pub enum TxnConnection { /// Isolated native SQL Server client, discarded after an interrupted stream. @@ -5469,6 +5476,11 @@ impl AppState { session: session.clone(), }) } + Some(PoolKind::Postgres(pool)) + if crate::query::plugin_plan::is_explicit_gaussdb_dws_profile(&config) => + { + Some(ConnectionDatabaseInfoSource::NativeGaussdbDws(pool.clone())) + } Some(PoolKind::Mysql(pool, _)) => Some(ConnectionDatabaseInfoSource::NativeMysql(pool.clone())), Some(PoolKind::HBase(client)) => Some(ConnectionDatabaseInfoSource::NativeHBase(client.clone())), Some(PoolKind::MongoDb(client)) => { @@ -5516,6 +5528,21 @@ impl AppState { Some(ConnectionDatabaseInfoSource::NativeMysql(pool)) => { db::mysql::database_connection_info(&pool, db::mysql::protocol_product_name(&config)).await.map(Some) } + Some(ConnectionDatabaseInfoSource::NativeGaussdbDws(pool)) => { + // Version discovery is optional metadata. If the query or product + // marker is unavailable, return versionless info; plan support is + // determined by the explicit DWS profile, not the product version. + let product_version = gaussdb_dws_product_version_from_pool(&pool).await; + Ok(Some(DatabaseConnectionInfo { + product_name: Some("GaussDB".to_string()), + product_version: product_version.map(|version| format!("GaussDB {version}")), + current_database: database + .map(str::to_string) + .or_else(|| config.effective_database().map(str::to_string)), + driver_name: Some("DBX PostgreSQL-compatible driver".to_string()), + ..Default::default() + })) + } Some(ConnectionDatabaseInfoSource::NativeHBase(client)) => { db::hbase_driver::database_connection_info(&client).await } diff --git a/crates/dbx-core/src/query/plugin_plan.rs b/crates/dbx-core/src/query/plugin_plan.rs index 31afa6dbd9..20028dd2c2 100644 --- a/crates/dbx-core/src/query/plugin_plan.rs +++ b/crates/dbx-core/src/query/plugin_plan.rs @@ -25,8 +25,8 @@ use crate::connection::AppState; use crate::db::QueryResult; use crate::models::connection::{ConnectionConfig, DatabaseType}; use crate::query_execution_sql::{ - build_explain_sql, estimated_plan_strategy, supports_explain_plan, EstimatedPlanAcquisition, EstimatedPlanFormat, - EstimatedPlanStrategy, ExplainFormat, ExplainSqlOptions, + build_explain_sql, build_gaussdb_dws_explain_sql, estimated_plan_strategy, gaussdb_dws_estimated_plan_strategy, + EstimatedPlanAcquisition, EstimatedPlanFormat, EstimatedPlanStrategy, ExplainFormat, ExplainSqlOptions, }; use super::{execute_sql_statement_with_options_typed, QueryExecutionMode, QueryExecutionOptions}; @@ -65,6 +65,8 @@ const WARNING_PLAN_ROWS_TRUNCATED: &str = "plan_rows_truncated"; /// connection rejects the capability probe instead of succeeding and leaving /// the plan request to fail later. const CONNECTION_NOT_OPEN_ERROR: &str = "Connection is not open"; +const GAUSSDB_DWS_PROFILE_KEY: &str = "gaussdbProductProfile"; +const GAUSSDB_DWS_PROFILE_VALUE: &str = "huawei-dws"; #[derive(Debug, Clone, PartialEq, Eq)] struct PlanPayload { @@ -98,8 +100,8 @@ pub struct PluginPlanRequest { #[serde(rename_all = "camelCase")] pub struct PluginPlanCapabilities { pub db_type: String, - /// Server product version when DBX already learned it for this connection. - /// Absent when unknown — the probe is never run on the plugin's behalf. + /// Server product version when DBX already learned and stored it for this + /// connection. The Host API never probes the server on the plugin's behalf. #[serde(default, skip_serializing_if = "Option::is_none")] pub db_version: Option, pub supports: PluginPlanSupports, @@ -135,10 +137,10 @@ pub struct PluginPlanResult { pub warnings: Vec, } -/// Reports what the plugin may expect from this connection without connecting -/// to it, running any SQL, or reading anything beyond the stored config. The -/// connection must already be open, so a closed one is refused here rather than -/// after a plan request has already been prepared. +/// Reports what the plugin may expect from this connection without opening a +/// connection or probing the server. DWS support comes from the explicit +/// connection profile; product version is reported only from metadata DBX +/// already stored. A closed connection is refused before any capability is reported. pub async fn plugin_plan_capabilities(state: &AppState, connection_id: &str) -> Result { let connection_id = require_connection_id(connection_id)?; let config = connection_config(state, connection_id).await?; @@ -147,7 +149,9 @@ pub async fn plugin_plan_capabilities(state: &AppState, connection_id: &str) -> Ok(PluginPlanCapabilities { db_type: database_type.as_str().to_string(), db_version: database_version(&config), - supports: PluginPlanSupports { estimated_plan: supports_explain_plan(Some(database_type)) }, + supports: PluginPlanSupports { + estimated_plan: estimated_plan_strategy_for_connection(&config, database_type).is_some(), + }, limits: PluginPlanLimits { max_timeout_ms: plugin_plan_timeout_ceiling_ms(&config), max_plan_bytes: MAX_PLUGIN_PLAN_BYTES, @@ -166,8 +170,8 @@ pub async fn explain_estimated_plan(state: &AppState, request: PluginPlanRequest let config = connection_config(state, &request.connection_id).await?; require_open_connection(state, &request.connection_id).await?; let database_type = explain_database_type(&config); - let strategy = - estimated_plan_strategy(Some(database_type)).ok_or_else(|| unsupported_dialect_message(database_type))?; + let strategy = estimated_plan_strategy_for_connection(&config, database_type) + .ok_or_else(|| unsupported_dialect_message(database_type))?; let timeout_secs = plugin_plan_timeout_secs(request.timeout_ms, &config); let (plan_text, rows_truncated) = match strategy.acquisition() { @@ -217,14 +221,19 @@ async fn native_estimated_plan( strategy: EstimatedPlanStrategy, timeout_secs: u64, ) -> Result<(String, bool), String> { - let built = build_explain_sql(ExplainSqlOptions { + let explain_options = ExplainSqlOptions { database_type: Some(database_type), // Estimated only: `analyze` is exactly what turns this into a statement // that runs the caller's SQL. analyze: None, format: Some(ExplainFormat::Json), sql: request.sql.clone(), - }); + }; + let built = if database_type == DatabaseType::Gaussdb { + build_gaussdb_dws_explain_sql(explain_options) + } else { + build_explain_sql(explain_options) + }; if !built.ok { return Err(explain_build_error(built.reason.as_deref(), database_type)); } @@ -441,13 +450,81 @@ fn unsupported_dialect_message(database_type: DatabaseType) -> String { format!("Estimated execution plans are not available for '{}' connections", database_type.as_str()) } +/// An explicit profile is required in addition to the generic GaussDB type. +/// The DWS marker is ignored for M-JDBC and other non-native driver profiles. +pub fn is_explicit_gaussdb_dws_profile(config: &ConnectionConfig) -> bool { + if config.db_type != DatabaseType::Gaussdb + || config.driver_profile.as_deref().is_some_and(|profile| !profile.eq_ignore_ascii_case("gaussdb")) + { + return false; + } + config + .external_config + .as_ref() + .and_then(Value::as_object) + .and_then(|values| values.get(GAUSSDB_DWS_PROFILE_KEY)) + .and_then(Value::as_str) + == Some(GAUSSDB_DWS_PROFILE_VALUE) +} + +fn estimated_plan_strategy_for_connection( + config: &ConnectionConfig, + database_type: DatabaseType, +) -> Option { + if is_explicit_gaussdb_dws_profile(config) { + return Some(gaussdb_dws_estimated_plan_strategy()); + } + estimated_plan_strategy(Some(database_type)) +} + +/// Discard a cached DWS product version before reconnecting so the plan API +/// does not report metadata from the previous connection while fresh metadata is +/// being learned. DWS support itself is selected by the explicit profile, not +/// the product version. Other metadata and non-DWS configs remain untouched. +pub fn invalidate_gaussdb_dws_product_version(config: &mut ConnectionConfig) { + if !is_explicit_gaussdb_dws_profile(config) { + return; + } + if let Some(database_info) = config.database_info.as_mut() { + database_info.product_version = None; + } +} + +/// Extract only the product version, discarding PostgreSQL compatibility and +/// build/commit metadata from `version()` before it is persisted or exposed. +pub(crate) fn gaussdb_product_version_from_server_version(version: &str) -> Option { + let normalized = version.to_ascii_lowercase(); + let marker = "gaussdb "; + let marker_index = normalized.match_indices(marker).find_map(|(index, _)| { + let has_product_boundary = index == 0 + || !normalized[..index] + .chars() + .next_back() + .is_some_and(|character| character.is_ascii_alphanumeric() || character == '_'); + has_product_boundary.then_some(index) + })?; + let suffix = version.get(marker_index + marker.len()..)?; + let version_length = suffix.bytes().take_while(|byte| byte.is_ascii_digit() || *byte == b'.').count(); + let product_version = &suffix[..version_length]; + if product_version.is_empty() + || !product_version.split('.').all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit())) + { + return None; + } + Some(product_version.to_string()) +} + fn database_version(config: &ConnectionConfig) -> Option { - config.database_info.as_ref().and_then(|info| info.product_version.clone()) + let version = config.database_info.as_ref()?.product_version.as_deref()?; + if is_explicit_gaussdb_dws_profile(config) { + return gaussdb_product_version_from_server_version(version) + .map(|product_version| format!("GaussDB {product_version}")); + } + Some(version.to_string()) } async fn connection_config(state: &AppState, connection_id: &str) -> Result { - let configs = state.configs.read().await; - configs.get(connection_id).cloned().ok_or_else(|| "Connection config not found".to_string()) + state.configs.read().await.get(connection_id).cloned().ok_or_else(|| "Connection config not found".to_string()) } /// Enforces the boundary both plan entry points document: a plugin may plan on @@ -777,7 +854,6 @@ mod tests { ] { let strategy = estimated_plan_strategy(Some(database_type)) .unwrap_or_else(|| panic!("{database_type:?} must have an acquisition strategy")); - assert!(supports_explain_plan(Some(database_type)), "{database_type:?}"); assert_eq!(strategy.acquisition(), expected_acquisition, "{database_type:?}"); } } @@ -785,7 +861,7 @@ mod tests { #[test] fn rejects_unsupported_dialects_and_unsafe_statements() { let redis = config(DatabaseType::Redis, 0); - assert!(!supports_explain_plan(Some(explain_database_type(&redis)))); + assert!(estimated_plan_strategy(Some(explain_database_type(&redis))).is_none()); // `build_explain_sql` is the same gate the plugin path relies on. for sql in [ @@ -809,6 +885,66 @@ mod tests { } } + #[test] + fn gaussdb_dws_requires_an_explicit_native_profile_but_not_a_specific_version() { + let mut config = config(DatabaseType::Gaussdb, 30); + assert!(!is_explicit_gaussdb_dws_profile(&config)); + assert!(estimated_plan_strategy_for_connection(&config, DatabaseType::Gaussdb).is_none()); + + config.external_config = Some(serde_json::json!({ + GAUSSDB_DWS_PROFILE_KEY: GAUSSDB_DWS_PROFILE_VALUE + })); + assert!(is_explicit_gaussdb_dws_profile(&config)); + assert!(estimated_plan_strategy_for_connection(&config, DatabaseType::Gaussdb).is_some()); + assert_eq!(database_version(&config), None); + + config.database_info = Some(crate::models::connection::DatabaseConnectionInfo { + product_name: Some("GaussDB".to_string()), + product_version: Some("PostgreSQL 9.2.4 (GaussDB 9.1.1 (Build metadata))".to_string()), + ..Default::default() + }); + let strategy = estimated_plan_strategy_for_connection(&config, DatabaseType::Gaussdb).unwrap(); + assert_eq!(strategy.format(), EstimatedPlanFormat::Json); + assert_eq!(strategy.acquisition(), EstimatedPlanAcquisition::GeneratedSql); + assert_eq!(database_version(&config).as_deref(), Some("GaussDB 9.1.1")); + + let mut stale_config = config.clone(); + invalidate_gaussdb_dws_product_version(&mut stale_config); + assert!(estimated_plan_strategy_for_connection(&stale_config, DatabaseType::Gaussdb).is_some()); + assert_eq!(database_version(&stale_config), None); + assert_eq!(stale_config.database_info.as_ref().unwrap().product_name.as_deref(), Some("GaussDB")); + + assert_eq!( + gaussdb_product_version_from_server_version("PostgreSQL 9.2.4 (GaussDB 9.1.10)"), + Some("9.1.10".to_string()) + ); + assert_eq!( + gaussdb_product_version_from_server_version("PostgreSQL 9.2.4 (GaussDB 9.1.1.2)"), + Some("9.1.1.2".to_string()) + ); + for unrecognized in ["PostgreSQL 9.2.4", "OpenGaussDB 9.1.1", "PostgreSQL 9.2.4 (GaussDB x.y)"] { + assert_eq!(gaussdb_product_version_from_server_version(unrecognized), None, "{unrecognized}"); + } + + config.database_info.as_mut().unwrap().product_version = Some("GaussDB 9.2.0".to_string()); + assert!(estimated_plan_strategy_for_connection(&config, DatabaseType::Gaussdb).is_some()); + assert_eq!(database_version(&config).as_deref(), Some("GaussDB 9.2.0")); + config.driver_profile = Some("gaussdb-m".to_string()); + assert!(!is_explicit_gaussdb_dws_profile(&config)); + assert!(estimated_plan_strategy_for_connection(&config, DatabaseType::Gaussdb).is_none()); + + let mut postgres = config_for(DatabaseType::Postgres, "pg-conn", 30); + postgres.external_config = Some(serde_json::json!({ + GAUSSDB_DWS_PROFILE_KEY: GAUSSDB_DWS_PROFILE_VALUE + })); + postgres.database_info = Some(crate::models::connection::DatabaseConnectionInfo { + product_version: Some("GaussDB 9.1.1".to_string()), + ..Default::default() + }); + assert!(estimated_plan_strategy_for_connection(&postgres, DatabaseType::Postgres).is_some()); + assert!(!is_explicit_gaussdb_dws_profile(&postgres)); + } + #[test] fn reports_capability_limits_and_dialect_without_connecting() { // `plugin_plan_capabilities` only reads the stored config, so the parts @@ -864,6 +1000,56 @@ mod tests { ); } + #[tokio::test] + async fn gaussdb_capabilities_use_explicit_dws_profile_without_probing() { + let native = config_for(DatabaseType::Gaussdb, "gaussdb-native", 30); + let mut dws_without_version = config_for(DatabaseType::Gaussdb, "gaussdb-dws", 30); + dws_without_version.external_config = Some(serde_json::json!({ + GAUSSDB_DWS_PROFILE_KEY: GAUSSDB_DWS_PROFILE_VALUE + })); + + let mut dws_versioned = config_for(DatabaseType::Gaussdb, "gaussdb-dws-versioned", 30); + dws_versioned.external_config = dws_without_version.external_config.clone(); + dws_versioned.database_info = Some(crate::models::connection::DatabaseConnectionInfo { + product_name: Some("GaussDB".to_string()), + product_version: Some("PostgreSQL 9.2.4 (GaussDB 9.1.1)".to_string()), + ..Default::default() + }); + + let mut dws_other_version = config_for(DatabaseType::Gaussdb, "gaussdb-dws-other-version", 30); + dws_other_version.external_config = dws_without_version.external_config.clone(); + dws_other_version.database_info = Some(crate::models::connection::DatabaseConnectionInfo { + product_name: Some("GaussDB".to_string()), + product_version: Some("PostgreSQL 9.2.4 (GaussDB 9.2.0)".to_string()), + ..Default::default() + }); + + let mut m_jdbc = config_for(DatabaseType::Gaussdb, "gaussdb-m-jdbc", 30); + m_jdbc.driver_profile = Some("gaussdb-m".to_string()); + m_jdbc.external_config = dws_without_version.external_config.clone(); + + let configs = [native, dws_without_version, dws_versioned, dws_other_version, m_jdbc]; + let (state, _dir) = saved_connection_state(&configs).await; + for config in &configs { + open_connection(&state, &config.id).await; + } + + // `open_connection` installs a Redis test stub, not a PostgreSQL pool. + // Explicitly profiled DWS remains supported without a version probe; + // the other product versions are exposed only when already stored. + for (config, expected_support) in configs.iter().zip([false, true, true, true, false]) { + let capabilities = plugin_plan_capabilities(&state, &config.id).await.unwrap(); + assert_eq!(capabilities.db_type, "gaussdb"); + assert_eq!(capabilities.supports.estimated_plan, expected_support, "{}", config.id); + match config.id.as_str() { + "gaussdb-dws" => assert_eq!(capabilities.db_version, None), + "gaussdb-dws-versioned" => assert_eq!(capabilities.db_version.as_deref(), Some("GaussDB 9.1.1")), + "gaussdb-dws-other-version" => assert_eq!(capabilities.db_version.as_deref(), Some("GaussDB 9.2.0")), + _ => {} + } + } + } + #[tokio::test] async fn capabilities_resolve_mysql_profiles_before_selecting_plan_strategy() { let cases = [ diff --git a/crates/dbx-sql-core/src/query_execution_sql.rs b/crates/dbx-sql-core/src/query_execution_sql.rs index 608f43d716..8058c84d7b 100644 --- a/crates/dbx-sql-core/src/query_execution_sql.rs +++ b/crates/dbx-sql-core/src/query_execution_sql.rs @@ -123,7 +123,26 @@ pub fn build_explain_sql(options: ExplainSqlOptions) -> ExplainSqlBuildResult { let Some(strategy) = estimated_plan_strategy(options.database_type) else { return explain_err("unsupported"); }; + build_explain_sql_with_strategy(options, strategy) +} +/// Builds the only validated GaussDB(DWS) Estimated Plan form. This does not +/// make `DatabaseType::Gaussdb` generally supported: callers must first verify +/// the explicit DWS connection profile and supported server version. +pub fn build_gaussdb_dws_explain_sql(options: ExplainSqlOptions) -> ExplainSqlBuildResult { + if options.database_type != Some(DatabaseType::Gaussdb) { + return explain_err("unsupported"); + } + if options.analyze == Some(true) { + return explain_err("unsafe"); + } + build_explain_sql_with_strategy(options, gaussdb_dws_estimated_plan_strategy()) +} + +fn build_explain_sql_with_strategy( + options: ExplainSqlOptions, + strategy: EstimatedPlanStrategy, +) -> ExplainSqlBuildResult { let source = strip_trailing_semicolons(options.sql.trim()); if source.is_empty() { return explain_err("empty"); @@ -239,6 +258,16 @@ pub fn estimated_plan_strategy(database_type: Option) -> Option EstimatedPlanStrategy { + EstimatedPlanStrategy { + sql_generation: EstimatedPlanSqlGeneration::Postgres, + acquisition: EstimatedPlanAcquisition::GeneratedSql, + format: EstimatedPlanFormat::Json, + } +} + /// Payload encoding defined by the estimated-plan strategy for this dialect. /// Keep the historical JSON fallback for unsupported/unspecified dialects; /// callers must check capability separately before using it. @@ -2137,6 +2166,56 @@ mod tests { } } + #[test] + fn gaussdb_dws_strategy_is_explicit_and_keeps_the_generic_dialect_closed() { + use EstimatedPlanAcquisition::GeneratedSql; + use EstimatedPlanFormat::Json; + + let strategy = gaussdb_dws_estimated_plan_strategy(); + assert_eq!(strategy.acquisition(), GeneratedSql); + assert_eq!(strategy.format(), Json); + assert!(estimated_plan_strategy(Some(DatabaseType::Gaussdb)).is_none()); + assert!(!supports_explain_plan(Some(DatabaseType::Gaussdb))); + + let result = build_gaussdb_dws_explain_sql(ExplainSqlOptions { + database_type: Some(DatabaseType::Gaussdb), + format: None, + analyze: None, + sql: "SELECT 1".to_string(), + }); + assert_eq!(result.sql.as_deref(), Some("EXPLAIN (FORMAT JSON) SELECT 1")); + assert!(result.ok); + + for options in [ + ExplainSqlOptions { + database_type: Some(DatabaseType::Gaussdb), + format: None, + analyze: Some(true), + sql: "SELECT 1".to_string(), + }, + ExplainSqlOptions { + database_type: Some(DatabaseType::Gaussdb), + format: None, + analyze: None, + sql: "DELETE FROM users".to_string(), + }, + ] { + assert!(!build_gaussdb_dws_explain_sql(options).ok); + } + + assert_eq!( + build_gaussdb_dws_explain_sql(ExplainSqlOptions { + database_type: Some(DatabaseType::Postgres), + format: None, + analyze: None, + sql: "SELECT 1".to_string(), + }) + .reason + .as_deref(), + Some("unsupported") + ); + } + #[test] fn estimated_plan_strategy_matches_capability_sql_format_and_acquisition() { use EstimatedPlanAcquisition::{DriverNative, GeneratedSql, SqlServerShowPlanSession}; diff --git a/crates/dbx-web/src/routes/connection.rs b/crates/dbx-web/src/routes/connection.rs index 5d1bb86ea6..96f9125b4c 100644 --- a/crates/dbx-web/src/routes/connection.rs +++ b/crates/dbx-web/src/routes/connection.rs @@ -493,7 +493,8 @@ pub async fn connect_db( // 只有本次请求确实写入/替换了会话凭据(输入了非空密码)时,才需要在连接 // 失败时回滚它;无密码重连复用的是既有有效凭据,失败不应清掉它,否则瞬时的 // 数据库/网络抖动就会让"本次会话内记住密码"失效、下次又弹窗。 - let (runtime_config, runtime_secrets) = prepare_runtime_config(config.clone()); + let (mut runtime_config, runtime_secrets) = prepare_runtime_config(config.clone()); + dbx_core::query::plugin_plan::invalidate_gaussdb_dws_product_version(&mut runtime_config); let session_credential_writes = record_session_credentials( app, &owner, diff --git a/packages/app-tests/pluginPlanHostApi.test.ts b/packages/app-tests/pluginPlanHostApi.test.ts index 4beb85c224..a1161d5bf4 100644 --- a/packages/app-tests/pluginPlanHostApi.test.ts +++ b/packages/app-tests/pluginPlanHostApi.test.ts @@ -2,15 +2,10 @@ import { strict as assert } from "node:assert"; import { readFileSync } from "node:fs"; import { test } from "vitest"; import { PluginHostBridge } from "../../apps/desktop/src/lib/plugins/pluginHostBridge.ts"; -import { - MAX_PLUGIN_PLAN_BYTES, - MAX_PLUGIN_PLAN_SQL_CHARS, - MAX_PLUGIN_PLAN_TIMEOUT_MS, - PLUGIN_PLAN_PERMISSION, - PLUGIN_PLAN_WARNING, -} from "../../apps/desktop/src/types/pluginPlan.ts"; +import { MAX_PLUGIN_PLAN_BYTES, MAX_PLUGIN_PLAN_SQL_CHARS, MAX_PLUGIN_PLAN_TIMEOUT_MS, PLUGIN_PLAN_PERMISSION, PLUGIN_PLAN_WARNING } from "../../apps/desktop/src/types/pluginPlan.ts"; import type { PluginPlanCapabilities, PluginPlanRequest } from "../../apps/desktop/src/types/pluginPlan.ts"; import type { InstalledPlugin, PluginWorkbenchContribution } from "../../apps/desktop/src/types/database.ts"; +import { GAUSSDB_DWS_PRODUCT_PROFILE, GAUSSDB_DWS_PRODUCT_PROFILE_KEY } from "../../apps/desktop/src/lib/database/jdbcDialect.ts"; /** * The plan Host API (#9675) spans three languages: the Rust core that owns @@ -95,14 +90,18 @@ test("plan limits and mode match the Rust core", () => { // raised in Rust without the matching TS change must fail this test. assert.equal(rustConst(planCoreSource, "MAX_PLUGIN_PLAN_TIMEOUT_MS").replaceAll("_", ""), String(MAX_PLUGIN_PLAN_TIMEOUT_MS)); assert.equal(rustConst(planCoreSource, "MAX_PLUGIN_PLAN_SQL_CHARS").replaceAll("_", ""), String(MAX_PLUGIN_PLAN_SQL_CHARS)); - assert.equal( - rustConst(planCoreSource, "MAX_PLUGIN_PLAN_BYTES").replaceAll("_", "").replaceAll(" ", ""), - `4*1024*1024`, - ); + assert.equal(rustConst(planCoreSource, "MAX_PLUGIN_PLAN_BYTES").replaceAll("_", "").replaceAll(" ", ""), `4*1024*1024`); assert.equal(MAX_PLUGIN_PLAN_BYTES, 4 * 1024 * 1024); assert.equal(rustConst(planCoreSource, "PLUGIN_PLAN_MODE_ESTIMATED"), '"estimated"'); }); +test("the explicit Huawei DWS profile marker matches the Rust Host gate without a version pin", () => { + assert.equal(rustConst(planCoreSource, "GAUSSDB_DWS_PROFILE_KEY"), JSON.stringify(GAUSSDB_DWS_PRODUCT_PROFILE_KEY)); + assert.equal(rustConst(planCoreSource, "GAUSSDB_DWS_PROFILE_VALUE"), JSON.stringify(GAUSSDB_DWS_PRODUCT_PROFILE)); + assert.match(planCoreSource, /if is_explicit_gaussdb_dws_profile\(config\)/); + assert.equal(planCoreSource.includes("SUPPORTED_GAUSSDB_DWS_VERSION"), false); +}); + test("plan warning codes match the Rust core", () => { assert.equal(rustConst(planCoreSource, "WARNING_PLAN_NOT_JSON"), `"${PLUGIN_PLAN_WARNING.notJson}"`); assert.equal(rustConst(planCoreSource, "WARNING_PLAN_TRUNCATED"), `"${PLUGIN_PLAN_WARNING.truncated}"`); @@ -193,7 +192,11 @@ test("mode is required and only estimated is served", async () => { }, }); - for (const [id, mode] of [["actual", "actual"], ["analyze", "analyze"], ["missing", undefined]] as const) { + for (const [id, mode] of [ + ["actual", "actual"], + ["analyze", "analyze"], + ["missing", undefined], + ] as const) { send(bridge, target, "host.explainPlan", { connectionId: "c1", sql: "SELECT 1", mode }, id); } await until(() => messages.length === 3, "the three rejections"); diff --git a/src-tauri/src/commands/connection.rs b/src-tauri/src/commands/connection.rs index a6e96285a4..d832b6bd45 100644 --- a/src-tauri/src/commands/connection.rs +++ b/src-tauri/src/commands/connection.rs @@ -2095,7 +2095,8 @@ async fn connect_db_inner( config: ConnectionConfig, client_attempt: Option, ) -> Result { - let config = config.canonicalized(); + let mut config = config.canonicalized(); + dbx_core::query::plugin_plan::invalidate_gaussdb_dws_product_version(&mut config); if config.db_type == DatabaseType::Sqlite && !db::sqlite_worker::sqlite_remote_worker_requested(&config) { db::sqlite::validate_persistent_attachments( &config.host,