feat(impala): add full database support

This commit is contained in:
zipg authored and GitHub committed 2026-08-14 20:41:57 +08:00
1 parent 185ff68d78
commit a6d9e6818e
54 files changed
+1092 -148

No files matched your search

@@ -1133,6 +1133,7 @@ const driverProfiles: Record<
trino: { type: "trino", port: 8080, user: "", label: "Trino", icon: "trino" },
prestosql: { type: "prestosql", port: 8080, user: "", label: "PrestoSQL", icon: "presto" },
hive: { type: "hive", port: 10000, user: "", label: "Apache Hive", icon: "hive" },
impala: { type: "impala", port: 21050, user: "", label: "Apache Impala", icon: "impala", urlParams: "auth=noSasl" },
spark: { type: "spark", port: 10015, user: "", label: "Apache Spark", icon: "spark" },
db2: { type: "db2", port: 50000, user: "db2inst1", label: "IBM DB2", icon: "db2" },
informix: { type: "informix", port: 9088, user: "informix", label: "Informix", icon: "informix" },
@@ -2386,7 +2387,7 @@ function applyProfile(val: string, preserveConnectionFields = false) {
form.value.connection_string = undefined;
form.value.url_params = "";
}
resetHiveKerberosFields(profile.type === "hive" ? form.value : undefined);
resetHiveKerberosFields(profile.type === "hive" || profile.type === "impala" ? form.value : undefined);
}
}
@@ -2512,7 +2513,7 @@ watch(
resetVictoriaMetricsFields();
}
resetElasticsearchProxyFields(config.db_type === "elasticsearch" ? config.external_config : undefined);
resetHiveKerberosFields(config.db_type === "hive" ? config : undefined);
resetHiveKerberosFields(config.db_type === "hive" || config.db_type === "impala" ? config : undefined);
resetDamengJvmOptions(config.db_type === "dameng" ? config : undefined);
h2ConnectionMode.value = h2ConnectionModeForConfig(config);
customColorInput.value = config.color || "";
@@ -2766,6 +2767,7 @@ const iconTypeMap: Record<string, string> = {
trino: "trino",
prestosql: "prestosql",
hive: "hive",
impala: "impala",
spark: "spark",
db2: "db2",
informix: "informix",
@@ -2846,6 +2848,7 @@ const dbOptions: DbOption[] = [
{ value: "trino", label: "Trino" },
{ value: "prestosql", label: "PrestoSQL" },
{ value: "hive", label: "Hive" },
{ value: "impala", label: "Apache Impala" },
{ value: "spark", label: "Apache Spark" },
{ value: "db2", label: "DB2" },
{ value: "informix", label: "Informix" },
@@ -2891,7 +2894,7 @@ const dbCategoryDefinitions: Array<{
{
key: "analytics",
titleKey: "connection.databaseCategoryAnalytics",
optionValues: ["cloudberry", "clickhouse", "doris", "starrocks", "databend", "selectdb", "databricks", "saphana", "teradata", "vertica", "exasol", "redshift", "snowflake", "trino", "prestosql", "hive", "spark", "bigquery", "kylin", "dremio"],
optionValues: ["cloudberry", "clickhouse", "doris", "starrocks", "databend", "selectdb", "databricks", "saphana", "teradata", "vertica", "exasol", "redshift", "snowflake", "trino", "prestosql", "hive", "impala", "spark", "bigquery", "kylin", "dremio"],
},
{
key: "domestic",
@@ -3728,7 +3731,7 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
config.ssl = !!config.ssl || damengSsl.enabled;
config.url_params = applyDamengSslUrlParams(config.url_params, config.ssl, damengSsl.sslFilesPath, damengSsl.sslKeystorePassword, damengSsl.sslProtocol);
}
if (config.db_type === "hive") {
if (config.db_type === "hive" || config.db_type === "impala") {
if (hiveAuthMode.value === "kerberos" && !hivePrincipal.value.trim()) {
throw new Error(t("connection.hiveKerberosPrincipalRequired"));
}
@@ -6998,7 +7001,7 @@ function openExternalUrl(url: string) {
<p class="col-span-3 text-xs text-muted-foreground">{{ t("connection.oracleTnsPathHint") }}</p>
</div>
<template v-if="form.db_type === 'hive'">
<template v-if="form.db_type === 'hive' || form.db_type === 'impala'">
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.hiveAuthMode") }}</Label>
<div class="col-span-3 grid h-8 grid-cols-2 overflow-hidden rounded-md border border-input bg-muted/30 p-0.5">
@@ -7020,7 +7023,7 @@ function openExternalUrl(url: string) {
<template v-if="hiveAuthMode === 'kerberos'">
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">{{ t("connection.hivePrincipal") }}</Label>
<Input v-model="hivePrincipal" class="col-span-3" placeholder="hive/_HOST@EXAMPLE.COM" />
<Input v-model="hivePrincipal" class="col-span-3" :placeholder="form.db_type === 'impala' ? 'impala/_HOST@EXAMPLE.COM' : 'hive/_HOST@EXAMPLE.COM'" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelSmallClass">krb5.conf</Label>
@@ -31,7 +31,7 @@ watch(
);
function submit() {
if (resolving.value || !password.value) return;
if (resolving.value) return;
resolving.value = true;
promptStore.submit(password.value, rememberPassword.value);
}
@@ -67,7 +67,7 @@ function cancel() {
<Button variant="outline" :disabled="resolving" @click="cancel">
{{ t("connection.promptPasswordCancel") }}
</Button>
<Button :disabled="resolving || !password" @click="submit">
<Button :disabled="resolving" @click="submit">
{{ t("connection.promptPasswordSubmit") }}
</Button>
</DialogFooter>
+13
View File
@@ -31,6 +31,19 @@ describe("DatabaseIcon", () => {
app.unmount();
});
it("uses the Apache Impala asset instead of the Hive asset", async () => {
const container = document.createElement("div");
document.body.appendChild(container);
const app = createApp(DatabaseIcon, { dbType: "impala" });
app.mount(container);
await nextTick();
const icon = container.querySelector("img");
expect(icon?.getAttribute("src")).toBe("/icons/database/impala.svg");
expect(icon?.classList.contains("database-logo-impala")).toBe(true);
app.unmount();
});
it("uses the Meilisearch asset", async () => {
const container = document.createElement("div");
document.body.appendChild(container);
@@ -55,6 +55,7 @@ const assetIcons: Record<string, string> = {
presto: "presto",
prestosql: "presto",
hive: "hive",
impala: "impala",
hbase: "hbase",
phoenix: "phoenix",
spark: "spark-logo.png",
@@ -122,7 +123,7 @@ const assetSrc = computed(() => {
</script>
<template>
<img v-if="assetName" :src="assetSrc" alt="" class="database-logo object-contain" :class="{ 'database-logo-light': useLightIconInDarkMode }" aria-hidden="true" />
<img v-if="assetName" :src="assetSrc" alt="" class="database-logo object-contain" :class="{ 'database-logo-light': useLightIconInDarkMode, 'database-logo-impala': normalizedType === 'impala' }" aria-hidden="true" />
<Database v-else class="text-blue-400" />
</template>
@@ -135,4 +136,8 @@ const assetSrc = computed(() => {
.database-logo-light {
filter: brightness(0) invert(82%);
}
.database-logo-impala {
transform: scale(1.55);
}
</style>
+2 -2
View File
@@ -448,7 +448,7 @@ export default {
disconnectAndForgetPassword: "Disconnect and Forget Password",
passwordForgotten: "Password forgotten. Reconnect will ask for it again.",
promptPasswordTitle: "Enter password",
promptPasswordMessage: "Enter the password for connection “{connection}”.",
promptPasswordMessage: "Enter the password for connection “{connection}”. For an empty password, connect directly.",
promptPasswordPlaceholder: "Password",
promptPasswordSubmit: "Connect",
promptPasswordCancel: "Cancel",
@@ -510,7 +510,7 @@ export default {
hiveAuthMode: "Auth",
hiveAuthNone: "None",
hivePrincipal: "Principal",
hiveKerberosPrincipalRequired: "Hive Kerberos principal is required",
hiveKerberosPrincipalRequired: "Kerberos principal is required",
hiveKrb5ConfBrowse: "Choose krb5.conf",
hiveJaasConfigBrowse: "Choose JAAS config",
hiveTicketCache: "Ticket Cache",
+1 -1
View File
@@ -449,7 +449,7 @@ export default withEnglishFallback({
disconnectAndForgetPassword: "Desconectar y olvidar la contraseña",
passwordForgotten: "Contraseña olvidada. La próxima conexión la solicitará de nuevo.",
promptPasswordTitle: "Introducir contraseña",
promptPasswordMessage: "Introduzca la contraseña de la conexión “{connection}”.",
promptPasswordMessage: "Introduzca la contraseña de la conexión “{connection}”. Si está vacía, conéctese directamente.",
promptPasswordPlaceholder: "Contraseña",
promptPasswordSubmit: "Conectar",
promptPasswordCancel: "Cancelar",
+1 -1
View File
@@ -448,7 +448,7 @@ export default withEnglishFallback({
disconnectAndForgetPassword: "Disconnetti e dimentica la password",
passwordForgotten: "Password dimenticata. La prossima connessione la richiederà di nuovo.",
promptPasswordTitle: "Inserisci password",
promptPasswordMessage: "Inserisci la password per la connessione “{connection}”.",
promptPasswordMessage: "Inserisci la password per la connessione “{connection}”. Se è vuota, connettiti direttamente.",
promptPasswordPlaceholder: "Password",
promptPasswordSubmit: "Connetti",
promptPasswordCancel: "Annulla",
+2 -2
View File
@@ -448,7 +448,7 @@ export default withEnglishFallback({
disconnectAndForgetPassword: "切断してこのパスワードを忘れる",
passwordForgotten: "パスワードを忘れました。次回の接続で再入力が必要です。",
promptPasswordTitle: "パスワードを入力",
promptPasswordMessage: "接続「{connection}」のパスワードを入力してください。",
promptPasswordMessage: "接続「{connection}」のパスワードを入力してください。空のパスワードの場合は、そのまま接続できます。",
promptPasswordPlaceholder: "パスワード",
promptPasswordSubmit: "接続",
promptPasswordCancel: "キャンセル",
@@ -1104,7 +1104,7 @@ export default withEnglishFallback({
hiveAuthMode: "認証",
hiveAuthNone: "なし",
hivePrincipal: "プリンシパル",
hiveKerberosPrincipalRequired: "Hive Kerberos プリンシパルは必須です",
hiveKerberosPrincipalRequired: "Kerberos プリンシパルは必須です",
hiveKrb5ConfBrowse: "krb5.conf を選択",
hiveJaasConfigBrowse: "JAAS 設定を選択",
hiveTicketCache: "チケットキャッシュ",
+2 -2
View File
@@ -442,7 +442,7 @@ export default withEnglishFallback({
disconnectAndForgetPassword: "연결 끊고 비밀번호 잊기",
passwordForgotten: "비밀번호를 잊었습니다. 다음 연결 시 다시 입력해야 합니다.",
promptPasswordTitle: "비밀번호 입력",
promptPasswordMessage: "연결 “{connection}”의 비밀번호를 입력하세요.",
promptPasswordMessage: "연결 “{connection}”의 비밀번호를 입력하세요. 빈 비밀번호라면 바로 연결할 수 있습니다.",
promptPasswordPlaceholder: "비밀번호",
promptPasswordSubmit: "연결",
promptPasswordCancel: "취소",
@@ -504,7 +504,7 @@ export default withEnglishFallback({
hiveAuthMode: "인증",
hiveAuthNone: "없음",
hivePrincipal: "프린시펄",
hiveKerberosPrincipalRequired: "Hive Kerberos 프린시펄은 필수입니다",
hiveKerberosPrincipalRequired: "Kerberos 프린시펄은 필수입니다",
hiveKrb5ConfBrowse: "krb5.conf 선택",
hiveJaasConfigBrowse: "JAAS 구성 선택",
hiveTicketCache: "티켓 캐시",
+1 -1
View File
@@ -449,7 +449,7 @@ export default withEnglishFallback({
disconnectAndForgetPassword: "Desconectar e Esquecer a Senha",
passwordForgotten: "Senha esquecida. A próxima conexão pedirá novamente.",
promptPasswordTitle: "Digite a senha",
promptPasswordMessage: "Digite a senha da conexão “{connection}”.",
promptPasswordMessage: "Digite a senha da conexão “{connection}”. Se estiver vazia, conecte-se diretamente.",
promptPasswordPlaceholder: "Senha",
promptPasswordSubmit: "Conectar",
promptPasswordCancel: "Cancelar",
+2 -2
View File
@@ -373,7 +373,7 @@ export default withEnglishFallback({
disconnectAndForgetPassword: "断开并忘记本次密码",
passwordForgotten: "已忘记密码,下次连接需重新输入。",
promptPasswordTitle: "输入密码",
promptPasswordMessage: "请输入连接“{connection}”的密码。",
promptPasswordMessage: "请输入连接“{connection}”的密码;若密码为空,可直接连接",
promptPasswordPlaceholder: "密码",
promptPasswordSubmit: "连接",
promptPasswordCancel: "取消",
@@ -435,7 +435,7 @@ export default withEnglishFallback({
hiveAuthMode: "认证",
hiveAuthNone: "无",
hivePrincipal: "Principal",
hiveKerberosPrincipalRequired: "Hive Kerberos principal 不能为空",
hiveKerberosPrincipalRequired: "Kerberos principal 不能为空",
hiveKrb5ConfBrowse: "选择 krb5.conf",
hiveJaasConfigBrowse: "选择 JAAS 配置",
hiveTicketCache: "票据缓存",
+1 -1
View File
@@ -449,7 +449,7 @@ export default withEnglishFallback({
disconnectAndForgetPassword: "中斷連線並忘記本次密碼",
passwordForgotten: "已忘記密碼,下次連線需重新輸入。",
promptPasswordTitle: "輸入密碼",
promptPasswordMessage: "請輸入連線「{connection}」的密碼。",
promptPasswordMessage: "請輸入連線「{connection}」的密碼;若密碼為空,可直接連線",
promptPasswordPlaceholder: "密碼",
promptPasswordSubmit: "連線",
promptPasswordCancel: "取消",
@@ -18,6 +18,10 @@ describe("connectionAttemptTimeout", () => {
expect(connectionAttemptTimeoutMs({ db_type: "prestosql", connect_timeout_secs: 5, transport_layers: [] })).toBe(AGENT_DRIVER_MIN_CONNECT_TIMEOUT_SECS * 1000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS);
});
it("uses the startup floor for Impala agent connections", () => {
expect(connectionAttemptTimeoutMs({ db_type: "impala", connect_timeout_secs: 5, transport_layers: [] })).toBe(AGENT_DRIVER_MIN_CONNECT_TIMEOUT_SECS * 1000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS);
});
it("uses the startup floor for generic JDBC plugin connections", () => {
expect(connectionAttemptTimeoutMs({ db_type: "jdbc", connect_timeout_secs: 5, transport_layers: [] })).toBe(AGENT_DRIVER_MIN_CONNECT_TIMEOUT_SECS * 1000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS);
});
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { connectionNeedsPasswordPrompt, connectionUsesPasswordlessAuthentication } from "@/lib/connection/connectionPassword";
import type { ConnectionConfig } from "@/types/database";
function connection(overrides: Partial<ConnectionConfig> = {}): ConnectionConfig {
return {
id: "connection-1",
name: "Connection",
db_type: "impala",
host: "127.0.0.1",
port: 21050,
username: "",
password: "",
save_password: false,
...overrides,
} as ConnectionConfig;
}
describe("connection password prompting", () => {
it("treats Impala's default and explicit NOSASL modes as passwordless", () => {
expect(connectionUsesPasswordlessAuthentication(connection())).toBe(true);
expect(connectionUsesPasswordlessAuthentication(connection({ url_params: "transportMode=binary;auth=noSasl" }))).toBe(true);
expect(connectionNeedsPasswordPrompt(connection({ url_params: "AUTH=NOSASL&fetchSize=1000" }))).toBe(false);
});
it("still prompts for Impala authentication modes that can require credentials", () => {
expect(connectionNeedsPasswordPrompt(connection({ url_params: "auth=LDAP" }))).toBe(true);
expect(connectionNeedsPasswordPrompt(connection({ url_params: "principal=impala/_HOST@EXAMPLE.COM" }))).toBe(true);
expect(connectionNeedsPasswordPrompt(connection({ url_params: "auth=noSasl;principal=impala/_HOST@EXAMPLE.COM" }))).toBe(true);
});
it("does not change password prompting for other database types", () => {
expect(connectionNeedsPasswordPrompt(connection({ db_type: "mysql", port: 3306, url_params: "auth=noSasl" }))).toBe(true);
expect(connectionNeedsPasswordPrompt(connection({ db_type: "postgres", port: 5432, save_password: true }))).toBe(false);
});
});
@@ -49,6 +49,12 @@ describe("tableEditing", () => {
expect(isTableDataEditable("oracle", [DBX_ROWID_COLUMN], "VIEW")).toBe(false);
});
it("keeps Impala table data readonly", () => {
expect(isTableDataEditable("impala", ["id"], "TABLE")).toBe(false);
expect(canEditExistingTableRows("impala", undefined, ["id"])).toBe(false);
expect(supportsDataGridTransaction("impala")).toBe(false);
});
it("does not include Oracle ROWID for view data tabs", () => {
expect(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN], "VIEW")).toBe(false);
expect(usesSyntheticRowIdKey("oracle", [DBX_ROWID_COLUMN], "MATERIALIZED_VIEW")).toBe(false);
@@ -9,6 +9,13 @@ describe("DuckDB driver installation", () => {
});
});
describe("Impala driver installation", () => {
it("reuses the downloadable Hive driver", () => {
expect(agentDriverInstallKey("impala")).toBe("hive");
expect(showAgentDriverInstallHint("impala", [{ db_type: "hive", installed: true }])).toBe(false);
});
});
describe("driverStoreFocusForInstallError", () => {
it("focuses the missing agent driver for driver-not-installed errors", () => {
expect(driverStoreFocusForInstallError("zookeeper driver is not installed. Please install it from the Driver Manager.", "zookeeper", undefined)).toEqual({
@@ -24,6 +24,7 @@ export function hasInstalledAgentVersion(drivers: readonly AgentDriverInstallSta
}
export function agentDriverInstallKey(dbType: DatabaseType | undefined, driverProfile?: string): string | undefined {
if (dbType === "impala") return "hive";
if (dbType === "oracle") return "oracle";
if (dbType === "mongodb") return "mongodb";
if (dbType === "dameng") return "dameng";
@@ -30,6 +30,7 @@ const DRIVER_STARTUP_FLOOR_TYPES = new Set<DatabaseType>([
"prestosql",
"jdbc",
"hive",
"impala",
"spark",
"db2",
"informix",
@@ -0,0 +1,30 @@
import type { ConnectionConfig } from "@/types/database";
type PasswordAuthenticationConfig = Pick<ConnectionConfig, "db_type" | "driver_profile" | "url_params">;
function hiveServerUrlParams(urlParams?: string): Map<string, string> {
const params = new Map<string, string>();
for (const part of (urlParams || "").replace(/^[?&;]+|[?&;]+$/g, "").split(/[;&]/)) {
const trimmed = part.trim();
if (!trimmed) continue;
const equals = trimmed.indexOf("=");
const key = (equals < 0 ? trimmed : trimmed.slice(0, equals)).trim().toLowerCase();
const value = (equals < 0 ? "" : trimmed.slice(equals + 1)).trim();
if (key) params.set(key, value);
}
return params;
}
export function connectionUsesPasswordlessAuthentication(config: PasswordAuthenticationConfig): boolean {
const profile = config.driver_profile || config.db_type;
if (config.db_type !== "impala" && profile !== "impala") return false;
const params = hiveServerUrlParams(config.url_params);
if (params.get("principal")) return false;
const auth = (params.get("auth") || "nosasl").toLowerCase();
return auth === "nosasl";
}
export function connectionNeedsPasswordPrompt(config: ConnectionConfig): boolean {
return config.save_password === false && !config.password && !connectionUsesPasswordlessAuthentication(config);
}
@@ -4,7 +4,7 @@ import { manifestDatabaseTypes } from "@/lib/database/databaseDriverManifest";
import { supportsTransfer } from "@/lib/database/databaseFeatureSupport";
import type { DatabaseType } from "@/types/database";
const TABLE_ONLY_TRANSFER_DATABASES: DatabaseType[] = ["sqlite", "rqlite", "cloudflare-d1", "duckdb", "clickhouse", "mongodb", "questdb", "hive", "spark"];
const TABLE_ONLY_TRANSFER_DATABASES: DatabaseType[] = ["sqlite", "rqlite", "cloudflare-d1", "duckdb", "clickhouse", "mongodb", "questdb", "hive", "impala", "spark"];
describe("transferObjectKinds", () => {
it("groups databases into transfer families", () => {
@@ -27,6 +27,7 @@ export const SCHEMA_AWARE_TYPES = new Set<DatabaseType>([
"trino",
"prestosql",
"hive",
"impala",
"spark",
"databend",
"db2",
@@ -179,7 +179,7 @@ export function supportsObjectBrowserTreeNode(dbType: DatabaseType | undefined,
}
export function supportsTableTruncate(dbType?: DatabaseType): boolean {
return !!dbType && dbType !== "sqlite" && dbType !== "rqlite" && dbType !== "turso" && dbType !== "cloudflare-d1" && dbType !== "duckdb" && dbType !== "influxdb" && dbType !== "victoriametrics" && dbType !== "manticoresearch";
return !!dbType && dbType !== "impala" && dbType !== "sqlite" && dbType !== "rqlite" && dbType !== "turso" && dbType !== "cloudflare-d1" && dbType !== "duckdb" && dbType !== "influxdb" && dbType !== "victoriametrics" && dbType !== "manticoresearch";
}
export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean {
@@ -66,6 +66,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = {
trino: { database: "schema" },
prestosql: { database: "schema" },
hive: { deferred: "Hive database creation needs agent metadata validation first" },
impala: { deferred: "Impala database creation needs dedicated metadata validation first" },
spark: { deferred: "Spark database creation needs agent metadata validation first" },
db2: { database: "schema" },
informix: { database: "schema" },
@@ -69,6 +69,7 @@ const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
["doris", TABLE_VIEW_OBJECTS],
["starrocks", TABLE_VIEW_MV_OBJECTS],
["hive", TABLE_VIEW_OBJECTS],
["impala", TABLE_VIEW_OBJECTS],
["spark", TABLE_VIEW_OBJECTS],
["trino", TABLE_VIEW_OBJECTS],
["prestosql", TABLE_VIEW_OBJECTS],
@@ -66,6 +66,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = {
trino: { deferred: "catalog properties are connector-managed" },
prestosql: { deferred: "catalog properties are connector-managed" },
hive: { deferred: "database properties need agent metadata validation first" },
impala: { deferred: "database properties need Impala-specific validation first" },
spark: { deferred: "database properties need agent metadata validation first" },
db2: { deferred: "schema properties need product-specific handling" },
informix: { deferred: "schema properties need product-specific handling" },
@@ -90,6 +90,17 @@ const NAVICAT_STYLE_TABLE_DATA_TYPES = new Set<DatabaseType>([
]);
const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<DatabaseCapability>>> = {
impala: {
tableData: {
insert: false,
updateRequiresPrimaryKey: false,
deleteRequiresPrimaryKey: false,
requiresTransactionalTableForExistingRows: false,
existingRowsReadonly: true,
transaction: false,
readonly: true,
},
},
hive: {
tableData: {
insert: true,
+1 -1
View File
@@ -11,7 +11,7 @@ const GAUSSDB_IDENTIFIER_QUOTE_STYLE_KEY = "gaussdbIdentifierQuoteStyle";
export const GAUSSDB_M_JDBC_DRIVER_PROFILE = "gaussdb-m";
export const GAUSSDB_M_JDBC_DRIVER_CLASS = "com.huawei.gaussdb.jdbc.Driver";
const DATABASE_AS_EXECUTION_SCHEMA_TYPES = new Set<DatabaseType>(["hive", "spark"]);
const DATABASE_AS_EXECUTION_SCHEMA_TYPES = new Set<DatabaseType>(["hive", "impala", "spark"]);
const CONNECTION_ROOT_SCHEMA_TYPES = new Set<DatabaseType>(["oracle", "dameng", "oceanbase-oracle"]);
const JDBC_DIALECT_MATCHERS: Array<{ type: DatabaseType; patterns: RegExp[] }> = [
+1 -1
View File
@@ -33,7 +33,7 @@ export function quoteTableIdentifier(databaseType: DatabaseType | undefined, nam
// (DatabaseMetaData.getIdentifierQuoteString()) — pass through unquoted.
if (databaseType === "jdbc") return name;
if (databaseType === "bigquery") return `\`${name.replace(/`/g, "\\`")}\``;
if (databaseType === "mysql" || databaseType === "clickhouse" || databaseType === "hive" || databaseType === "spark" || databaseType === "databend" || databaseType === "tdengine" || databaseType === "access" || databaseType === "doris" || databaseType === "starrocks")
if (databaseType === "mysql" || databaseType === "clickhouse" || databaseType === "hive" || databaseType === "impala" || databaseType === "spark" || databaseType === "databend" || databaseType === "tdengine" || databaseType === "access" || databaseType === "doris" || databaseType === "starrocks")
return `\`${name.replace(/`/g, "``")}\``;
if (databaseType === "informix" && /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)) return name;
if (databaseType === "neo4j") return quoteCypherIdentifier(name);
@@ -130,6 +130,62 @@ describe("connectionStore save_password opt-out", () => {
expect(store.getConfig("pg-1")?.password).toBe("");
});
it("connects with an explicitly submitted empty password", async () => {
installApiMocks();
installPasswordPromptMock();
requestPassword.mockResolvedValue({ password: "", rememberPassword: false });
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
const connection = postgresConnection({ id: "pg-1", save_password: false, password: "" });
store.connections = [connection];
await store.connect(connection);
const { connectDb } = await import("@/lib/backend/api");
expect(connectDb).toHaveBeenCalledWith(expect.objectContaining({ id: "pg-1", password: "" }), expect.any(Number));
expect(store.getConfig("pg-1")).toEqual(expect.objectContaining({ password: "", save_password: false }));
});
it("connects to NOSASL Impala without prompting for an empty password", async () => {
installApiMocks();
installPasswordPromptMock();
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
const connection = postgresConnection({
id: "impala-1",
name: "Impala",
db_type: "impala",
port: 21050,
username: "",
password: "",
save_password: false,
url_params: "auth=noSasl",
});
store.connections = [connection];
await store.connect(connection);
expect(requestPassword).not.toHaveBeenCalled();
const { connectDb } = await import("@/lib/backend/api");
expect(connectDb).toHaveBeenCalledWith(expect.objectContaining({ id: "impala-1", password: "", save_password: false }), expect.any(Number));
});
it("remembers an explicitly submitted empty password", async () => {
installApiMocks();
installPasswordPromptMock();
requestPassword.mockResolvedValue({ password: "", rememberPassword: true });
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
const connection = postgresConnection({ id: "pg-1", save_password: false, password: "" });
store.connections = [connection];
await store.connect(connection);
const { saveConnections } = await import("@/lib/backend/api");
expect(saveConnections).toHaveBeenLastCalledWith(expect.arrayContaining([expect.objectContaining({ id: "pg-1", password: "", save_password: true })]));
expect(store.getConfig("pg-1")).toEqual(expect.objectContaining({ password: "", save_password: true }));
});
it("connect cancels when the prompt is dismissed", async () => {
installApiMocks();
installPasswordPromptMock();
+12 -18
View File
@@ -116,6 +116,7 @@ import { REDIS_SCAN_PAGE_SIZE_DEFAULT } from "@/lib/redis/redisKeyPattern";
import { normalizeRedisDatabaseAliases, redisDatabaseAlias, redisDatabaseLabel } from "@/lib/redis/redisDatabaseAlias";
import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, hasInstalledAgentVersion, type AgentDriverInstallState } from "@/lib/connection/agentDriverInstallHint";
import { appendConnectionErrorHints, isMysqlMissingPasswordFailure } from "@/lib/connection/connectionErrorHints";
import { connectionNeedsPasswordPrompt } from "@/lib/connection/connectionPassword";
import { appendVisibleDatabaseSelection } from "@/lib/connection/connectionVisibleDatabases";
import { buildXuguTypeMemberNodes, isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers";
import { isXuguPublicSynonymScope, xuguSchemaDisplayName, XUGU_PUBLIC_SYNONYM_SCOPE } from "@/lib/sidebar/xuguPublicSynonyms";
@@ -1055,6 +1056,7 @@ export const useConnectionStore = defineStore("connection", () => {
trino: "Trino",
prestosql: "PrestoSQL",
hive: "Hive",
impala: "Apache Impala",
spark: "Apache Spark",
db2: "DB2",
informix: "Informix",
@@ -3084,12 +3086,12 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId: config.id,
connectionName: config.name,
});
if (!result?.password) throw new Error(CONNECTION_PASSWORD_REQUIRED_MESSAGE);
if (!result) throw new Error(CONNECTION_PASSWORD_REQUIRED_MESSAGE);
return { config: { ...config, password: result.password }, rememberPassword: result.rememberPassword };
}
async function persistRememberedConnectionPassword(config: ConnectionConfig, rememberPassword: boolean, expectedConfigFingerprint: string): Promise<void> {
if (!rememberPassword || !config.password) return;
if (!rememberPassword) return;
const index = connections.value.findIndex((connection) => connection.id === config.id);
if (index < 0) return;
if (connectionConfigFingerprint(connections.value[index]) !== expectedConfigFingerprint) return;
@@ -3125,14 +3127,10 @@ export const useConnectionStore = defineStore("connection", () => {
const localAttempt = beginLocalConnectionAttempt(config.id);
try {
let rememberPassword = false;
if (config.save_password === false && !config.password) {
// 本次运行期已在会话凭据仓库暂存密码(save_password=false):免弹窗,
// 直接以空密码 connectDb,后端会从会话凭据仓库补主密码。
if (!(await hasSessionCredential(config.id))) {
const prompted = await ensureConnectionPassword(config);
config = prompted.config;
rememberPassword = prompted.rememberPassword;
}
if (connectionNeedsPasswordPrompt(config) && !(await hasSessionCredential(config.id))) {
const prompted = await ensureConnectionPassword(config);
config = prompted.config;
rememberPassword = prompted.rememberPassword;
}
await beforeConnectHandler?.(config);
if (config.db_type === "sqlserver") {
@@ -3340,14 +3338,10 @@ export const useConnectionStore = defineStore("connection", () => {
// Fast-path the common case (password saved or no password needed) so the
// in-flight dedup above keeps its exact microtask cadence; only await the
// interactive prompt when the connection actually needs a typed password.
if (config.save_password === false && !config.password) {
// 本次运行期已在会话凭据仓库暂存密码(save_password=false):免弹窗,
// 直接以空密码 connectDb,后端会从会话凭据仓库补主密码。
if (!(await hasSessionCredential(connectionId))) {
const prompted = await ensureConnectionPassword(config);
config = prompted.config;
rememberPassword = prompted.rememberPassword;
}
if (connectionNeedsPasswordPrompt(config) && !(await hasSessionCredential(connectionId))) {
const prompted = await ensureConnectionPassword(config);
config = prompted.config;
rememberPassword = prompted.rememberPassword;
}
await beforeConnectHandler?.(config);
if (config.db_type === "sqlserver") {
+1
View File
@@ -53,6 +53,7 @@ export type DatabaseType =
| "trino"
| "prestosql"
| "hive"
| "impala"
| "spark"
| "db2"
| "informix"