feat(plugin-plan): add estimated plan support for huawei dws

This commit is contained in:
jearhe
2026-10-19 13:30:05 +08:00
committed by GitHub
parent e08b81e2ce
commit 9944e1e04c
24 changed files with 503 additions and 37 deletions
@@ -680,6 +680,7 @@ const gaussdbDriverMode = computed<GaussdbConnectionMode>({
},
});
const isGaussdbMJdbcConnection = computed(() => gaussdbDriverMode.value === "m-jdbc");
const isGaussdbDwsConnection = computed(() => gaussdbDriverMode.value === "dws");
const showGaussdbIdentifierQuoteStyle = computed(() => supportsGaussdbIdentifierQuoteStyle(form.value));
const gaussdbQuoteStyle = computed<GaussdbIdentifierQuoteStyle>({
get: () => gaussdbIdentifierQuoteStyle(form.value),
@@ -10239,10 +10240,11 @@ function openExternalUrl(url: string) {
<SelectContent>
<SelectItem value="native">{{ t("connection.gaussdbConnectionModeNative") }}</SelectItem>
<SelectItem value="m-jdbc">{{ t("connection.gaussdbConnectionModeMJdbc") }}</SelectItem>
<SelectItem value="dws">{{ t("connection.gaussdbConnectionModeDws") }}</SelectItem>
</SelectContent>
</Select>
<p class="text-xs leading-5 text-muted-foreground">
{{ isGaussdbMJdbcConnection ? t("connection.gaussdbConnectionModeMJdbcHint") : t("connection.gaussdbConnectionModeNativeHint") }}
{{ isGaussdbMJdbcConnection ? t("connection.gaussdbConnectionModeMJdbcHint") : isGaussdbDwsConnection ? t("connection.gaussdbConnectionModeDwsHint") : t("connection.gaussdbConnectionModeNativeHint") }}
</p>
</div>
</div>
+3
View File
@@ -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.",
+3
View File
@@ -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.",
+3
View File
@@ -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.",
+3
View File
@@ -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.",
+3
View File
@@ -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.",
+3
View File
@@ -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 を取得してください。",
+2
View File
@@ -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를 가져오세요.",
+3
View File
@@ -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.",
+3
View File
@@ -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.",
+2
View File
@@ -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 ที่ตรงกัน",
+2
View File
@@ -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.",
+2
View File
@@ -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。",
+2
View File
@@ -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。",
@@ -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;
+17 -2
View File
@@ -5,15 +5,17 @@ import type { CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect
export type JdbcDialectConnection = Partial<Pick<ConnectionConfig, "db_type" | "driver_profile" | "driver_label" | "connection_string" | "url_params" | "jdbc_driver_class" | "jdbc_driver_paths" | "database_info" | "external_config" | "username">>;
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<DatabaseType>(["hive", "kyuubi", "impala", "argo", "transwarp", "spark"]);
const CONNECTION_ROOT_SCHEMA_TYPES = new Set<DatabaseType>(["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;
@@ -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 () => {
+18 -2
View File
@@ -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);
+27
View File
@@ -250,6 +250,7 @@ enum ConnectionDatabaseInfoSource {
session: Arc<PluginDriverSession>,
},
NativeMysql(db::mysql::MySqlPool),
NativeGaussdbDws(deadpool_postgres::Pool),
NativeHBase(db::hbase_driver::HBaseClient),
NativeMongo(mongodb::Client, Option<String>),
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<String> {
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
}
+204 -18
View File
@@ -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<String>,
pub supports: PluginPlanSupports,
@@ -135,10 +137,10 @@ pub struct PluginPlanResult {
pub warnings: Vec<String>,
}
/// 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<PluginPlanCapabilities, String> {
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<EstimatedPlanStrategy> {
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<String> {
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<String> {
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<ConnectionConfig, String> {
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 = [
@@ -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<DatabaseType>) -> Option<Es
Some(strategy)
}
/// The version-gated strategy used only for an explicitly identified and
/// validated Huawei DWS connection. Generic GaussDB remains unsupported.
pub const fn gaussdb_dws_estimated_plan_strategy() -> 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};
+2 -1
View File
@@ -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,
+15 -12
View File
@@ -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");
+2 -1
View File
@@ -2095,7 +2095,8 @@ async fn connect_db_inner(
config: ConnectionConfig,
client_attempt: Option<u64>,
) -> Result<String, String> {
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,