feat(impala): add full database support
This commit is contained in:
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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "チケットキャッシュ",
|
||||
|
||||
@@ -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: "티켓 캐시",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "票据缓存",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[] }> = [
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -53,6 +53,7 @@ export type DatabaseType =
|
||||
| "trino"
|
||||
| "prestosql"
|
||||
| "hive"
|
||||
| "impala"
|
||||
| "spark"
|
||||
| "db2"
|
||||
| "informix"
|
||||
|
||||
Reference in new issue
Block a user