fix(gbase8s): stabilize connections and table access
Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
1 parent
7d5a9459e4
commit
7e6923d53b
26 files changed
+375
-46
No files matched your search
@@ -51,6 +51,7 @@ import { JDBCX_DEFAULT_URL, JDBCX_DRIVER_PROFILE, JDBCX_JDBC_DRIVER_CLASS, ensur
|
||||
import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection";
|
||||
import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout";
|
||||
import { appendConnectionErrorHints, isJdbcMissingRuntimeDependencyError } from "@/lib/connection/connectionErrorHints";
|
||||
import { preventDialogDocumentSelectAll } from "@/lib/connection/dialogTextSelection";
|
||||
import { postgresTlsModeForForm } from "@/lib/connection/postgresTlsMode";
|
||||
import { buildMqKafkaConnectionExtra, mqKafkaConnectionTarget, resolveMqKafkaConnectionSource, type MqKafkaConnectionSource } from "@/lib/connection/mqKafkaConnection";
|
||||
import { assertCompleteDatabaseCategories, databaseSelectionForCategory } from "@/lib/connection/databaseCategoryOptions";
|
||||
@@ -4735,7 +4736,7 @@ function openExternalUrl(url: string) {
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogContent class="connection-dialog-content" :class="connectionDialogContentClass" :data-wide="shouldUseWideConnectionDialog ? 'true' : undefined" @interact-outside.prevent @escape-key-down="handleDialogEscape">
|
||||
<DialogContent class="connection-dialog-content" :class="connectionDialogContentClass" :data-wide="shouldUseWideConnectionDialog ? 'true' : undefined" @interact-outside.prevent @escape-key-down="handleDialogEscape" @keydown="preventDialogDocumentSelectAll">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ editingId ? t("connection.editTitle") : t("connection.title") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@@ -6000,7 +6001,10 @@ function openExternalUrl(url: string) {
|
||||
|
||||
<div v-if="form.driver_profile === 'gbase8s'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.gbaseServer") }}</Label>
|
||||
<Input v-model="form.gbase_server" class="col-span-3" placeholder="gbase01" />
|
||||
<div class="col-span-3 space-y-1">
|
||||
<Input v-model="form.gbase_server" placeholder="gbase01" />
|
||||
<p class="text-xs text-muted-foreground">{{ t("connection.gbaseServerHint") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.db_type === 'informix'" class="grid grid-cols-4 items-center gap-4">
|
||||
@@ -7126,7 +7130,7 @@ function openExternalUrl(url: string) {
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showVisibleDatabasesDialog">
|
||||
<DialogContent class="sm:max-w-[460px]">
|
||||
<DialogContent class="sm:max-w-[520px]" @keydown="preventDialogDocumentSelectAll">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t(visibleObjectTitleKey) }}</DialogTitle>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
@@ -7174,9 +7178,7 @@ function openExternalUrl(url: string) {
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
{{ t("common.loading") }}
|
||||
</div>
|
||||
<div v-else-if="visibleDatabaseError" class="p-3 text-sm text-destructive">
|
||||
{{ t(visibleObjectLoadFailedKey, { message: visibleDatabaseError }) }}
|
||||
</div>
|
||||
<textarea v-else-if="visibleDatabaseError" class="h-full w-full resize-none overflow-auto border-0 bg-transparent p-3 text-sm leading-5 text-destructive outline-none" :value="t(visibleObjectLoadFailedKey, { message: visibleDatabaseError })" readonly />
|
||||
<div v-else-if="!filteredVisibleDatabaseNames.length" class="p-3 text-sm text-muted-foreground">
|
||||
{{ t("grid.noSearchResults") }}
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,10 @@ const errorMessage = computed(() => (props.connectionId ? connectionStore.connec
|
||||
function clearError() {
|
||||
if (props.connectionId) connectionStore.clearConnectionError(props.connectionId);
|
||||
}
|
||||
|
||||
function selectErrorText(event: FocusEvent) {
|
||||
if (event.target instanceof HTMLTextAreaElement) event.target.select();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -40,15 +44,13 @@ function clearError() {
|
||||
<AlertTriangle class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent side="top" class="w-72 gap-2 p-2 text-xs" @click.stop>
|
||||
<PopoverContent side="top" class="w-96 max-w-[calc(100vw-2rem)] gap-2 p-2 text-xs" @click.stop>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="font-medium text-foreground">
|
||||
{{ t("connection.lastError") }}
|
||||
</div>
|
||||
<div class="mt-1 max-h-36 overflow-auto whitespace-pre-wrap break-words text-muted-foreground">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
<textarea class="mt-1 max-h-40 min-h-20 w-full resize-none overflow-auto rounded border bg-muted/30 p-2 font-mono text-[11px] leading-4 text-muted-foreground outline-none" :value="errorMessage" readonly @focus="selectErrorText" />
|
||||
</div>
|
||||
<button type="button" class="shrink-0 rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:bg-muted hover:text-foreground" @click="clearError">{{ t("connection.clearError") }}</button>
|
||||
</div>
|
||||
|
||||
@@ -98,6 +98,11 @@ const CASES: { name: string; message: string; key: string; params?: Record<strin
|
||||
key: "driverStore.driverUpdateBlocked",
|
||||
params: { labels: "Prod MySQL, Stage PG" },
|
||||
},
|
||||
{
|
||||
name: "GBase 8s server name mismatch",
|
||||
message: "Agent RPC error (-1): java.sql.SQLException: GBASEDBTSERVER 与 DBSERVERNAME 或 DBSERVERALIASES 不匹配。",
|
||||
key: "connection.gbaseServerMismatch",
|
||||
},
|
||||
{
|
||||
name: "Kafka topic unload unsupported",
|
||||
message: "Kafka does not support unloading topics",
|
||||
|
||||
@@ -41,6 +41,7 @@ const patterns: [RegExp, string][] = [
|
||||
[/^Custom Java runtime path is empty\. Please choose a Java executable\.$/, "connection.customJavaPathEmpty"],
|
||||
[/^Agent requires Java 21, but DBX started it with an older Java runtime\. Use DBX managed JRE 21 or select a Java 21 executable in Driver Manager\./, "connection.agentJavaTooOld"],
|
||||
[/^JDBC plugin is not installed\. Install the optional JDBC plugin to use this connection\.$/, "connection.jdbcPluginNotInstalled"],
|
||||
[/GBASEDBTSERVER[\s\S]*DBSERVERNAME[\s\S]*DBSERVERALIASES/, "connection.gbaseServerMismatch"],
|
||||
[/^ai\.configNameExists:(.+)$/, "ai.configNameExists"],
|
||||
|
||||
// Tunnel / proxy test messages
|
||||
|
||||
@@ -263,6 +263,8 @@ export default {
|
||||
hiveJvmOptions: "JVM Options",
|
||||
hiveJvmOptionsPlaceholder: "-Dsun.security.krb5.debug=true",
|
||||
gbaseServer: "GBASEDBTSERVER",
|
||||
gbaseServerHint: "Must match the server DBSERVERNAME or DBSERVERALIASES.",
|
||||
gbaseServerMismatch: "GBase 8s connection failed because GBASEDBTSERVER does not match the server DBSERVERNAME or DBSERVERALIASES. Fill GBASEDBTSERVER with the actual server name.",
|
||||
informixServer: "INFORMIXSERVER",
|
||||
sslEnable: "Enable encrypted connection",
|
||||
caCertPath: "CA Certificate",
|
||||
|
||||
@@ -248,6 +248,8 @@ export default withEnglishFallback({
|
||||
d1TokenHint: "El token necesita al menos el permiso D1 Read; las escrituras, el DDL y las importaciones también requieren D1 Write.",
|
||||
d1FieldsRequired: "Se requieren el ID de cuenta de Cloudflare, el ID de la base de datos D1 y el token de API.",
|
||||
gbaseServer: "GBASEDBTSERVER",
|
||||
gbaseServerHint: "Debe coincidir con el DBSERVERNAME del servidor o con DBSERVERALIASES.",
|
||||
gbaseServerMismatch: "Falló la conexión de GBase 8s: GBASEDBTSERVER no coincide con el DBSERVERNAME del servidor ni con sus DBSERVERALIASES. Introduce el nombre real del servidor.",
|
||||
informixServer: "INFORMIXSERVER",
|
||||
sslEnable: "Habilitar conexión cifrada",
|
||||
caCertPath: "Certificado CA",
|
||||
|
||||
@@ -247,6 +247,8 @@ export default withEnglishFallback({
|
||||
d1TokenHint: "Il token richiede almeno l'autorizzazione D1 Read; per scritture, DDL e importazioni è necessaria anche D1 Write.",
|
||||
d1FieldsRequired: "Sono obbligatori l'ID account Cloudflare, l'ID database D1 e il token API.",
|
||||
gbaseServer: "GBASEDBTSERVER",
|
||||
gbaseServerHint: "Deve corrispondere al DBSERVERNAME del server o a DBSERVERALIASES.",
|
||||
gbaseServerMismatch: "Connessione GBase 8s non riuscita: GBASEDBTSERVER non corrisponde al DBSERVERNAME del server o a uno dei DBSERVERALIASES. Inserisci il nome server effettivo.",
|
||||
informixServer: "INFORMIXSERVER",
|
||||
sslEnable: "Abilita connessione crittografata",
|
||||
caCertPath: "Certificato CA",
|
||||
|
||||
@@ -247,6 +247,8 @@ export default withEnglishFallback({
|
||||
d1TokenHint: "トークンには少なくともD1 Read権限が必要です。書き込み、DDL、インポートにはD1 Write権限も必要です。",
|
||||
d1FieldsRequired: "CloudflareアカウントID、D1データベースID、APIトークンは必須です。",
|
||||
gbaseServer: "GBASEDBTSERVER",
|
||||
gbaseServerHint: "サーバー側の DBSERVERNAME または DBSERVERALIASES と一致させる必要があります。",
|
||||
gbaseServerMismatch: "GBase 8s の接続に失敗しました。GBASEDBTSERVER がサーバー側の DBSERVERNAME または DBSERVERALIASES と一致していません。接続設定に実際のサーバー名を入力してください。",
|
||||
informixServer: "INFORMIXSERVER",
|
||||
sslEnable: "暗号化接続を有効にする",
|
||||
caCertPath: "CA証明書",
|
||||
|
||||
@@ -259,6 +259,8 @@ export default withEnglishFallback({
|
||||
hiveJvmOptions: "JVM 옵션",
|
||||
hiveJvmOptionsPlaceholder: "-Dsun.security.krb5.debug=true",
|
||||
gbaseServer: "GBASEDBTSERVER",
|
||||
gbaseServerHint: "서버의 DBSERVERNAME 또는 DBSERVERALIASES와 일치해야 합니다.",
|
||||
gbaseServerMismatch: "GBase 8s 연결 실패: GBASEDBTSERVER가 서버의 DBSERVERNAME 또는 DBSERVERALIASES와 일치하지 않습니다. 연결 설정에 실제 서버 이름을 입력하세요.",
|
||||
informixServer: "INFORMIXSERVER",
|
||||
sslEnable: "암호화된 연결 사용",
|
||||
caCertPath: "CA 인증서",
|
||||
|
||||
@@ -248,6 +248,8 @@ export default withEnglishFallback({
|
||||
d1TokenHint: "O token precisa de pelo menos a permissão D1 Read; gravações, DDL e importações também exigem D1 Write.",
|
||||
d1FieldsRequired: "O ID da conta Cloudflare, o ID do banco de dados D1 e o token da API são obrigatórios.",
|
||||
gbaseServer: "GBASEDBTSERVER",
|
||||
gbaseServerHint: "Deve corresponder ao DBSERVERNAME do servidor ou a DBSERVERALIASES.",
|
||||
gbaseServerMismatch: "Falha na conexão de GBase 8s: GBASEDBTSERVER não corresponde ao DBSERVERNAME do servidor nem a um dos DBSERVERALIASES. Informe o nome real do servidor.",
|
||||
informixServer: "INFORMIXSERVER",
|
||||
sslEnable: "Habilitar conexão criptografada",
|
||||
caCertPath: "Certificado CA",
|
||||
|
||||
@@ -265,6 +265,8 @@ export default withEnglishFallback({
|
||||
hiveJvmOptions: "JVM 参数",
|
||||
hiveJvmOptionsPlaceholder: "-Dsun.security.krb5.debug=true",
|
||||
gbaseServer: "GBASEDBTSERVER",
|
||||
gbaseServerHint: "必须与服务端 DBSERVERNAME 或 DBSERVERALIASES 一致。",
|
||||
gbaseServerMismatch: "GBase 8s 连接失败:GBASEDBTSERVER 与服务端 DBSERVERNAME 或 DBSERVERALIASES 不匹配。请在连接配置中填写实际服务名。",
|
||||
informixServer: "INFORMIXSERVER",
|
||||
sslEnable: "启用加密连接",
|
||||
caCertPath: "CA 证书",
|
||||
|
||||
@@ -248,6 +248,8 @@ export default withEnglishFallback({
|
||||
d1TokenHint: "Token 至少需要 D1 Read 權限;執行寫入、DDL 或匯入時還需要 D1 Write 權限。",
|
||||
d1FieldsRequired: "必須填寫 Cloudflare Account ID、D1 Database ID 和 API Token。",
|
||||
gbaseServer: "GBASEDBTSERVER",
|
||||
gbaseServerHint: "必須與服務端 DBSERVERNAME 或 DBSERVERALIASES 一致。",
|
||||
gbaseServerMismatch: "GBase 8s 連線失敗:GBASEDBTSERVER 與服務端 DBSERVERNAME 或 DBSERVERALIASES 不匹配。請在連線設定中填寫實際服務名。",
|
||||
informixServer: "INFORMIXSERVER",
|
||||
sslEnable: "啟用加密連線",
|
||||
caCertPath: "CA 憑證",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
connectionObjectTreeNodeSchema,
|
||||
connectionObjectTreeQuerySchema,
|
||||
connectionQueryExecutionSchema,
|
||||
connectionShouldDiscoverJdbcSchemas,
|
||||
connectionShouldLoadIdentifierQuote,
|
||||
connectionUsesDatabaseObjectTreeMode,
|
||||
effectiveDatabaseTypeForConnection,
|
||||
@@ -87,6 +88,8 @@ describe("jdbc dialect inference", () => {
|
||||
expect(connectionShouldLoadIdentifierQuote({ db_type: "jdbc", jdbc_driver_class: "org.postgresql.Driver" })).toBe(true);
|
||||
expect(connectionShouldLoadIdentifierQuote({ db_type: "kingbase" })).toBe(true);
|
||||
expect(connectionShouldLoadIdentifierQuote({ db_type: "gaussdb" })).toBe(true);
|
||||
expect(connectionShouldLoadIdentifierQuote({ db_type: "gbase", driver_profile: "gbase8s" })).toBe(true);
|
||||
expect(connectionShouldLoadIdentifierQuote({ db_type: "gbase", driver_profile: "gbase8a" })).toBe(false);
|
||||
expect(
|
||||
connectionShouldLoadIdentifierQuote({
|
||||
db_type: "jdbc",
|
||||
@@ -96,6 +99,11 @@ describe("jdbc dialect inference", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to a flat table tree when GBase 8s reports no schemas", () => {
|
||||
expect(connectionShouldDiscoverJdbcSchemas({ db_type: "gbase", driver_profile: "gbase8s" })).toBe(true);
|
||||
expect(connectionShouldDiscoverJdbcSchemas({ db_type: "gbase", driver_profile: "gbase8a" })).toBe(false);
|
||||
});
|
||||
|
||||
it("recognizes GaussDB reached through PostgreSQL-compatible JDBC drivers", () => {
|
||||
const connection = {
|
||||
db_type: "jdbc" as const,
|
||||
|
||||
@@ -34,6 +34,17 @@ describe("qualifiedTableName — SQLite attached databases", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("qualifiedTableName — GBase 8s", () => {
|
||||
it("keeps the owner unquoted when the driver reports no identifier quote support", () => {
|
||||
expect(qualifiedTableName({ databaseType: "informix", identifierQuote: "", schema: "gbasedbt", tableName: "connection_smoke" })).toBe("gbasedbt.connection_smoke");
|
||||
expect(quoteTableDataIdentifier("informix", "connection_smoke", "")).toBe("connection_smoke");
|
||||
});
|
||||
|
||||
it("keeps native Informix qualification when no driver capability was loaded", () => {
|
||||
expect(qualifiedTableName({ databaseType: "informix", schema: "gbasedbt", tableName: "connection_smoke" })).toBe("gbasedbt.connection_smoke");
|
||||
});
|
||||
});
|
||||
|
||||
describe("quoteTableIdentifier", () => {
|
||||
it("backtick-quotes mysql identifiers", () => {
|
||||
expect(quoteTableIdentifier("mysql", "orders")).toBe("`orders`");
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { preventDialogDocumentSelectAll } from "../dialogTextSelection";
|
||||
|
||||
function keyboardEvent(target: EventTarget, options: { key?: string; metaKey?: boolean; ctrlKey?: boolean; altKey?: boolean } = {}) {
|
||||
return {
|
||||
key: options.key ?? "a",
|
||||
metaKey: options.metaKey ?? false,
|
||||
ctrlKey: options.ctrlKey ?? false,
|
||||
altKey: options.altKey ?? false,
|
||||
target,
|
||||
preventDefault: vi.fn(),
|
||||
} as unknown as KeyboardEvent;
|
||||
}
|
||||
|
||||
describe("preventDialogDocumentSelectAll", () => {
|
||||
it("prevents page selection from non-text dialog surfaces", () => {
|
||||
const event = keyboardEvent(document.createElement("div"), { metaKey: true });
|
||||
|
||||
expect(preventDialogDocumentSelectAll(event)).toBe(true);
|
||||
expect(event.preventDefault).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it.each([document.createElement("input"), document.createElement("textarea")])("keeps native select-all for text controls", (target) => {
|
||||
const event = keyboardEvent(target, { ctrlKey: true });
|
||||
|
||||
expect(preventDialogDocumentSelectAll(event)).toBe(false);
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps native select-all for nested textbox content", () => {
|
||||
const textbox = document.createElement("div");
|
||||
textbox.setAttribute("role", "textbox");
|
||||
const child = document.createElement("span");
|
||||
textbox.appendChild(child);
|
||||
const event = keyboardEvent(child, { metaKey: true });
|
||||
|
||||
expect(preventDialogDocumentSelectAll(event)).toBe(false);
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not intercept ordinary typing", () => {
|
||||
const event = keyboardEvent(document.createElement("div"));
|
||||
|
||||
expect(preventDialogDocumentSelectAll(event)).toBe(false);
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
function isDialogTextInputTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof Element)) return false;
|
||||
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || (target instanceof HTMLElement && target.isContentEditable) || !!target.closest("[contenteditable='true'], [role='textbox']");
|
||||
}
|
||||
|
||||
export function preventDialogDocumentSelectAll(event: KeyboardEvent): boolean {
|
||||
const isSelectAll = event.key.toLowerCase() === "a" && (event.metaKey || event.ctrlKey) && !event.altKey;
|
||||
if (!isSelectAll || isDialogTextInputTarget(event.target)) return false;
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConne
|
||||
|
||||
export function connectionShouldLoadIdentifierQuote(connection: JdbcDialectConnection | undefined): boolean {
|
||||
if (!connection) return false;
|
||||
if (connection.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return true;
|
||||
if (connection.db_type === "kingbase") return true;
|
||||
if (gaussdbIdentifierQuoteStyle(connection) !== "auto") return false;
|
||||
if (connection.db_type === "gaussdb") return true;
|
||||
@@ -142,6 +143,9 @@ export function connectionUsesDatabaseObjectTreeMode(connection?: JdbcDialectCon
|
||||
}
|
||||
|
||||
export function connectionShouldDiscoverJdbcSchemas(connection?: JdbcDialectConnection): boolean {
|
||||
// GBase 8s exposes owner schemas only when the current database can use them
|
||||
// in DML; non-ANSI databases fall back to the flat table tree.
|
||||
if (connection?.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return true;
|
||||
return connection?.db_type === "jdbc" && !inferJdbcDialect(connection);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export function quoteTableIdentifier(databaseType: DatabaseType | undefined, nam
|
||||
|
||||
export function quoteTableDataIdentifier(databaseType: DatabaseType | undefined, name: string, identifierQuote?: string): string {
|
||||
if ((databaseType === "gaussdb" || databaseType === "opengauss" || databaseType === "postgres") && identifierQuote != null) return quoteGaussDbJdbcIdentifier(name, identifierQuote);
|
||||
if (databaseType === "kingbase" && identifierQuote != null) {
|
||||
if ((databaseType === "kingbase" || databaseType === "informix") && identifierQuote != null) {
|
||||
if (!identifierQuote) return name;
|
||||
return `${identifierQuote}${name.replaceAll(identifierQuote, identifierQuote + identifierQuote)}${identifierQuote}`;
|
||||
}
|
||||
@@ -81,6 +81,11 @@ export function qualifiedTableName(options: Pick<BuildTableSelectSqlOptions, "da
|
||||
}
|
||||
return quotedTable;
|
||||
}
|
||||
if (databaseType === "informix" && identifierQuote != null) {
|
||||
const quotedTable = quoteTableDataIdentifier(databaseType, tableName, identifierQuote);
|
||||
const trimmedSchema = schema?.trim();
|
||||
return trimmedSchema ? `${quoteTableDataIdentifier(databaseType, trimmedSchema, identifierQuote)}.${quotedTable}` : quotedTable;
|
||||
}
|
||||
if ((isSchemaAware(databaseType) || databaseType === "sqlite") && !usesDatabaseObjectTreeMode(databaseType) && schema) {
|
||||
if (databaseType === "sqlserver") {
|
||||
const linked = parseSqlServerLinkedSchema(schema);
|
||||
|
||||
@@ -88,6 +88,17 @@ function informixConnection(): ConnectionConfig {
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function gbase8sConnection(): ConnectionConfig {
|
||||
return {
|
||||
...informixConnection(),
|
||||
id: "gbase8s-1",
|
||||
name: "GBase 8s",
|
||||
db_type: "gbase",
|
||||
driver_profile: "gbase8s",
|
||||
database: "dbx_test",
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function procedure(name: string): ObjectInfo {
|
||||
return {
|
||||
name,
|
||||
@@ -270,6 +281,40 @@ describe("connectionStore metadata loading", () => {
|
||||
expect(databaseNode.children?.map((node) => [node.type, node.label, node.schema])).toEqual([["table", "t", undefined]]);
|
||||
});
|
||||
|
||||
it("keeps the flat object tree for GBase 8s databases that cannot qualify schemas in DML", async () => {
|
||||
const listSchemaInfos = vi.fn().mockResolvedValue([]);
|
||||
const listTables = vi.fn().mockResolvedValue([{ name: "connection_smoke", table_type: "TABLE", comment: null }]);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
listObjects: vi.fn().mockResolvedValue([]),
|
||||
listSchemaInfos,
|
||||
listTables,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
useSettingsStore().editorSettings.sidebarObjectDisplay = "simple";
|
||||
const connection = gbase8sConnection();
|
||||
const databaseNode: TreeNode = { id: `${connection.id}:dbx_test`, label: "dbx_test", type: "database", connectionId: connection.id, database: "dbx_test", isExpanded: false, children: [] };
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, isExpanded: true, children: [databaseNode] }];
|
||||
|
||||
await store.loadTreeNodeChildren(databaseNode, { force: true });
|
||||
|
||||
expect(listSchemaInfos).toHaveBeenCalledWith(connection.id, "dbx_test");
|
||||
expect(listTables).toHaveBeenCalled();
|
||||
expect(databaseNode.children?.map((node) => [node.type, node.label, node.schema])).toEqual([["table", "connection_smoke", undefined]]);
|
||||
});
|
||||
|
||||
it("renders simple-mode table children without waiting for supplemental objects", async () => {
|
||||
const tables: TableInfo[] = [{ name: "users", table_type: "BASE TABLE", comment: null }];
|
||||
const listTables = vi.fn().mockResolvedValue(tables);
|
||||
|
||||
Reference in new issue
Block a user