fix(jdbc): show schemas for generic JDBC connections

This commit is contained in:
t8y2 2026-07-25 21:51:11 +08:00
parent 1e21a5ad52
commit 47d48a09e9
No known key found for this signature in database
5 changed files with 144 additions and 5 deletions

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { connectionObjectTreeNodeSchema, connectionQueryExecutionSchema, effectiveDatabaseTypeForConnection, inferJdbcDialect } from "@/lib/database/jdbcDialect";
import { connectionObjectTreeNodeSchema, connectionQueryExecutionSchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, inferJdbcDialect } from "@/lib/database/jdbcDialect";
describe("jdbc dialect inference", () => {
it("detects InterSystems IRIS and Caché JDBC connections", () => {
@ -46,6 +46,24 @@ describe("jdbc dialect inference", () => {
}),
).toBe("sqlserver");
});
it("detects GaussDB-compatible JDBC connections as schema-aware", () => {
const gaussdbConnection = {
db_type: "jdbc" as const,
connection_string: "jdbc:gaussdb://localhost:8000/testdb",
jdbc_driver_class: "com.huawei.gaussdb.jdbc.Driver",
};
const opengaussConnection = {
db_type: "jdbc" as const,
connection_string: "jdbc:opengauss://localhost:5432/postgres",
jdbc_driver_class: "org.opengauss.Driver",
};
expect(inferJdbcDialect(gaussdbConnection)).toBe("gaussdb");
expect(connectionUsesDatabaseObjectTreeMode(gaussdbConnection)).toBe(false);
expect(inferJdbcDialect(opengaussConnection)).toBe("opengauss");
expect(connectionUsesDatabaseObjectTreeMode(opengaussConnection)).toBe(false);
});
});
describe("query execution schema", () => {

View File

@ -12,6 +12,8 @@ const JDBC_DIALECT_MATCHERS: Array<{ type: DatabaseType; patterns: RegExp[] }> =
{ type: "goldendb", patterns: [/jdbc:goldendb:/i, /goldendb/i] },
{ type: "hive", patterns: [/org\.apache\.hive\.jdbc\.HiveDriver/i, /hive-jdbc/i] },
{ type: "mysql", patterns: [/jdbc:mysql:/i, /mysql/i, /mariadb/i, /kyuubi/i, /hive2/i] },
{ type: "gaussdb", patterns: [/jdbc:gaussdb:/i, /com\.huawei\.gaussdb/i, /gaussdb/i] },
{ type: "opengauss", patterns: [/jdbc:opengauss:/i, /org\.opengauss/i, /opengauss/i] },
{ type: "postgres", patterns: [/jdbc:postgresql:/i, /postgres/i] },
{ type: "sqlserver", patterns: [/jdbc:sqlserver:/i, /sqlserver/i, /mssql/i] },
{ type: "oracle", patterns: [/jdbc:oracle:/i, /oracle/i] },
@ -76,6 +78,10 @@ export function connectionUsesDatabaseObjectTreeMode(connection?: JdbcDialectCon
return !usesTreeSchemaMode(dialect);
}
export function connectionShouldDiscoverJdbcSchemas(connection?: JdbcDialectConnection): boolean {
return connection?.db_type === "jdbc" && !inferJdbcDialect(connection);
}
export function connectionUsesSchemaExecutionContext(connection?: Pick<ConnectionConfig, "db_type" | "connection_string" | "jdbc_driver_class" | "jdbc_driver_paths">): boolean {
return connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend";
}
@ -103,7 +109,6 @@ export function metadataSchemaForConnection(connection: JdbcDialectConnection |
}
export function connectionObjectTreeNodeSchema(connection: JdbcDialectConnection | undefined, database: string, schema?: string): string | undefined {
if (connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend") return schema || database;
if (connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend") return schema || database;
if (connectionUsesDatabaseObjectTreeMode(connection)) return undefined;
if (schema) return schema;

View File

@ -50,6 +50,21 @@ function oracleConnection(): ConnectionConfig {
} as ConnectionConfig;
}
function genericJdbcConnection(): ConnectionConfig {
return {
id: "jdbc-1",
name: "Generic JDBC",
db_type: "jdbc",
host: "127.0.0.1",
port: 0,
username: "app",
password: "",
database: "testdb",
driver_profile: "jdbc",
connection_string: "jdbc:example://127.0.0.1/testdb",
} as ConnectionConfig;
}
function procedure(name: string): ObjectInfo {
return {
name,
@ -160,6 +175,77 @@ describe("connectionStore metadata loading", () => {
expect(node.isExpanded).toBe(true);
});
it("discovers schema nodes for unknown generic JDBC databases", async () => {
const listSchemaInfos = vi.fn().mockResolvedValue([
{ name: "app", comment: null },
{ name: "reporting", comment: null },
]);
const listTables = vi.fn().mockResolvedValue([]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
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 store = useConnectionStore();
const connection = genericJdbcConnection();
const databaseNode: TreeNode = { id: "jdbc-1:testdb", label: "testdb", type: "database", connectionId: connection.id, database: "testdb", 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, "testdb");
expect(listTables).not.toHaveBeenCalled();
expect(databaseNode.children?.map((node) => [node.type, node.label, node.schema])).toEqual([
["schema", "app", "app"],
["schema", "reporting", "reporting"],
]);
});
it("keeps the flat object tree for unknown generic JDBC databases without schemas", async () => {
const listSchemaInfos = vi.fn().mockResolvedValue([]);
const listTables = vi.fn().mockResolvedValue([{ name: "t", 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 = genericJdbcConnection();
const databaseNode: TreeNode = { id: "jdbc-1:testdb", label: "testdb", type: "database", connectionId: connection.id, database: "testdb", 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, "testdb");
expect(listTables).toHaveBeenCalled();
expect(databaseNode.children?.map((node) => [node.type, node.label, node.schema])).toEqual([["table", "t", 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);

View File

@ -47,7 +47,7 @@ import * as api from "@/lib/backend/api";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { useTunnelProfileStore } from "@/stores/tunnelProfileStore";
import { connectionIsDorisFamilyCatalogCapable, isInternalDorisCatalog, isSchemaAware, normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionShouldDiscoverJdbcSchemas, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { buildDatabaseTreeNodes, buildDuckDbConnectionTreeNodes, compareSidebarNames, sortSidebarDatabases, sortSidebarNames, shouldIncludeDefaultDatabaseNode } from "@/lib/database/databaseTree";
import { buildSqlServerDatabaseTreeNodes } from "@/lib/database/sqlServerTree";
import { collapseExpandedTreeNodes } from "@/lib/sidebar/sidebarTreeCollapse";
@ -3148,6 +3148,13 @@ export const useConnectionStore = defineStore("connection", () => {
children: [],
};
});
if (schemas.length === 0 && connectionShouldDiscoverJdbcSchemas(getConfig(connectionId))) {
// Generic JDBC drivers vary widely: prefer schema navigation when the
// driver reports schemas, but keep the legacy flat object tree when it
// reports none so non-schema engines do not expand into an empty node.
await loadTables(connectionId, database, undefined, options);
return;
}
if (isPostgresLikeForExtensions(getConfig(connectionId)?.db_type)) {
children.push(buildExtensionManagementNode(connectionId, database));
}
@ -4395,7 +4402,7 @@ export const useConnectionStore = defineStore("connection", () => {
const effectiveDbType = effectiveDatabaseTypeForConnection(config);
if (config?.db_type === "sqlserver") {
await loadSqlServerDatabaseObjects(node.connectionId, node.database, options);
} else if (usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) {
} else if ((usesTreeSchemaMode(effectiveDbType) && !connectionUsesDatabaseObjectTreeMode(config)) || connectionShouldDiscoverJdbcSchemas(config)) {
await loadSchemas(node.connectionId, node.database, options);
} else {
await loadTables(node.connectionId, node.database, undefined, options);

View File

@ -1,6 +1,6 @@
import { strict as assert } from "node:assert";
import { test } from "vitest";
import { codeMirrorSqlDialectForConnection, effectiveDatabaseTypeForConnection, inferJdbcDialect, sqlSnippetDatabaseTypeForConnection } from "../../apps/desktop/src/lib/database/jdbcDialect.ts";
import { codeMirrorSqlDialectForConnection, connectionShouldDiscoverJdbcSchemas, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, inferJdbcDialect, sqlSnippetDatabaseTypeForConnection } from "../../apps/desktop/src/lib/database/jdbcDialect.ts";
test("infers GoldenDB for generic JDBC connections", () => {
assert.equal(
@ -29,6 +29,29 @@ test("infers JDBC dialect from driver profile", () => {
);
});
test("infers GaussDB-compatible JDBC connections as schema-aware", () => {
const gaussdbConnection = {
db_type: "jdbc" as const,
connection_string: "jdbc:gaussdb://127.0.0.1:8000/testdb",
jdbc_driver_class: "com.huawei.gaussdb.jdbc.Driver",
};
const opengaussConnection = {
db_type: "jdbc" as const,
connection_string: "jdbc:opengauss://127.0.0.1:5432/postgres",
jdbc_driver_class: "org.opengauss.Driver",
};
assert.equal(inferJdbcDialect(gaussdbConnection), "gaussdb");
assert.equal(connectionUsesDatabaseObjectTreeMode(gaussdbConnection), false);
assert.equal(inferJdbcDialect(opengaussConnection), "opengauss");
assert.equal(connectionUsesDatabaseObjectTreeMode(opengaussConnection), false);
});
test("discovers schemas only for unknown generic JDBC connections", () => {
assert.equal(connectionShouldDiscoverJdbcSchemas({ db_type: "jdbc", driver_profile: "jdbc" }), true);
assert.equal(connectionShouldDiscoverJdbcSchemas({ db_type: "jdbc", connection_string: "jdbc:mysql://127.0.0.1:3306/app" }), false);
});
test("uses SQL Server editor syntax for ASE without changing its effective JDBC type", () => {
const aseConnections = [
{ db_type: "jdbc" as const, driver_profile: "ase" },