fix(jdbc): show schemas for generic JDBC connections
This commit is contained in:
parent
1e21a5ad52
commit
47d48a09e9
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
|
|
|
|||
Loading…
Reference in New Issue