fix(jdbc): show Oracle schemas for custom drivers

Closes #5638
This commit is contained in:
t8y2 2026-08-09 18:05:23 +08:00
parent 8f597a0516
commit 1daaac03aa
No known key found for this signature in database
7 changed files with 128 additions and 16 deletions

View File

@ -7,6 +7,7 @@ import {
connectionQueryExecutionSchema,
connectionShouldDiscoverJdbcSchemas,
connectionShouldLoadIdentifierQuote,
connectionUsesConnectionRootSchemaMode,
connectionUsesDatabaseObjectTreeMode,
effectiveDatabaseTypeForConnection,
gaussdbConnectionMode,
@ -64,6 +65,22 @@ describe("jdbc dialect inference", () => {
).toBe("sqlserver");
});
it.each([
["jdbc:oracle:thin:@//localhost:1521/XE", "oracle"],
["jdbc:dm://localhost:5236/DAMENG", "dameng"],
] as const)("uses connection-root schemas for %s", (connectionString, dialect) => {
const connection = {
db_type: "jdbc" as const,
connection_string: connectionString,
};
expect(inferJdbcDialect(connection)).toBe(dialect);
expect(connectionUsesConnectionRootSchemaMode(connection)).toBe(true);
expect(connectionUsesDatabaseObjectTreeMode(connection)).toBe(false);
expect(connectionObjectTreeQuerySchema(connection, "DBX_TEST", "DBX_TEST")).toBe("DBX_TEST");
expect(connectionObjectTreeNodeSchema(connection, "DBX_TEST", "DBX_TEST")).toBe("DBX_TEST");
});
it("detects GaussDB-compatible JDBC connections as schema-aware", () => {
const gaussdbConnection = {
db_type: "jdbc" as const,

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { filterSchemaNamesForConnection, filterSchemaNamesForVisiblePicker, isSystemSchemaName } from "@/lib/database/visibleDatabases";
import { connectionUsesVisibleSchemaFilter, filterSchemaNamesForConnection, filterSchemaNamesForVisiblePicker, isSystemSchemaName } from "@/lib/database/visibleDatabases";
describe("visibleDatabases schema filtering", () => {
it("hides common Kingbase system schemas by default", () => {
@ -19,6 +19,17 @@ describe("visibleDatabases schema filtering", () => {
expect(filterSchemaNamesForConnection(["DBX_TEST", "DIP", "SYSTEM"], { db_type: "oracle", database: "XE" }, "XE")).toEqual(["DBX_TEST", "DIP"]);
});
it("uses Oracle schema filtering for inferred JDBC connections", () => {
const connection = {
db_type: "jdbc" as const,
connection_string: "jdbc:oracle:thin:@//localhost:1521/XE",
username: "DBX_TEST",
};
expect(connectionUsesVisibleSchemaFilter(connection)).toBe(true);
expect(filterSchemaNamesForConnection(["ANONYMOUS", "DBX_TEST", "SYS", "SYSTEM"], connection, "")).toEqual(["DBX_TEST"]);
});
it("keeps the Dameng login schema visible while hiding default system schemas", () => {
expect(filterSchemaNamesForConnection(["APP", "SYS", "SYSDBA", "SYSDBO", "SYSAUDITOR"], { db_type: "dameng", username: "SYSDBA" }, "")).toEqual(["APP", "SYSDBA"]);
});

View File

@ -2,7 +2,7 @@ import type { ConnectionConfig, DatabaseType } from "@/types/database";
import { isSchemaAware, usesDatabaseObjectTreeMode, usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
import type { CodeMirrorSqlDialectName } from "@/lib/editor/codemirrorSqlDialect";
type JdbcDialectConnection = Pick<ConnectionConfig, "db_type"> & Partial<Pick<ConnectionConfig, "driver_profile" | "driver_label" | "connection_string" | "jdbc_driver_class" | "jdbc_driver_paths" | "database_info" | "external_config">>;
type JdbcDialectConnection = Partial<Pick<ConnectionConfig, "db_type" | "driver_profile" | "driver_label" | "connection_string" | "jdbc_driver_class" | "jdbc_driver_paths" | "database_info" | "external_config">>;
export type GaussdbIdentifierQuoteStyle = "auto" | "double" | "backtick";
export type GaussdbConnectionMode = "native" | "m-jdbc";
@ -12,6 +12,7 @@ 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 CONNECTION_ROOT_SCHEMA_TYPES = new Set<DatabaseType>(["oracle", "dameng", "oceanbase-oracle"]);
const JDBC_DIALECT_MATCHERS: Array<{ type: DatabaseType; patterns: RegExp[] }> = [
{ type: "databend", patterns: [/jdbc:databend:/i, /com\.databend\.jdbc\.DatabendDriver/i, /databend-jdbc/i] },
@ -67,6 +68,11 @@ export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConne
return inferJdbcDialect(connection) ?? "jdbc";
}
export function connectionUsesConnectionRootSchemaMode(connection?: JdbcDialectConnection): boolean {
const type = effectiveDatabaseTypeForConnection(connection);
return !!type && CONNECTION_ROOT_SCHEMA_TYPES.has(type);
}
export function connectionShouldLoadIdentifierQuote(connection: JdbcDialectConnection | undefined): boolean {
if (!connection) return false;
if (connection.db_type === "gbase" && isGbase8sProfile(connection.driver_profile)) return true;
@ -135,6 +141,7 @@ export function tableStructureDatabaseTypeForConnection(connection?: JdbcDialect
export function connectionUsesDatabaseObjectTreeMode(connection?: JdbcDialectConnection): boolean {
if (!connection) return false;
if (connection.db_type !== "jdbc") return usesDatabaseObjectTreeMode(effectiveDatabaseTypeForConnection(connection));
if (connectionUsesConnectionRootSchemaMode(connection)) return false;
const dialect = inferJdbcDialect(connection);
if (!dialect) return true;
if (dialect === "hive" || dialect === "trino") return false;
@ -149,7 +156,7 @@ export function connectionShouldDiscoverJdbcSchemas(connection?: JdbcDialectConn
return connection?.db_type === "jdbc" && !inferJdbcDialect(connection);
}
export function connectionUsesSchemaExecutionContext(connection?: Pick<ConnectionConfig, "db_type" | "connection_string" | "jdbc_driver_class" | "jdbc_driver_paths">): boolean {
export function connectionUsesSchemaExecutionContext(connection?: JdbcDialectConnection): boolean {
return connection?.db_type === "jdbc" && inferJdbcDialect(connection) === "databend";
}

View File

@ -1,4 +1,7 @@
import type { ConnectionConfig, DatabaseType } from "@/types/database";
import { connectionUsesConnectionRootSchemaMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
type VisibleDatabaseConnection = Partial<ConnectionConfig>;
type SystemNameRules = {
exact?: ReadonlySet<string>;
@ -171,7 +174,7 @@ export function isSystemSchemaName(databaseType: DatabaseType | undefined, schem
return rules.prefixes?.some((prefix) => normalized.startsWith(prefix)) ?? false;
}
export function filterDatabaseNamesForConnection(databaseNames: string[], connection: Pick<ConnectionConfig, "db_type" | "driver_profile" | "visible_databases"> | undefined): string[] {
export function filterDatabaseNamesForConnection(databaseNames: string[], connection: VisibleDatabaseConnection | undefined): string[] {
const visibleDatabases = connection?.visible_databases;
if (visibleDatabaseFilterIsEnabled(visibleDatabases)) {
return filterVisibleDatabaseNames(databaseNames, visibleDatabases);
@ -179,33 +182,29 @@ export function filterDatabaseNamesForConnection(databaseNames: string[], connec
return filterDatabaseNamesForVisiblePicker(databaseNames, connection);
}
export function filterDatabaseNamesForVisiblePicker(databaseNames: string[], connection: Pick<ConnectionConfig, "db_type" | "driver_profile"> | undefined): string[] {
export function filterDatabaseNamesForVisiblePicker(databaseNames: string[], connection: VisibleDatabaseConnection | undefined): string[] {
if (connection?.db_type === "gbase" && connection.driver_profile === "gbase8s") {
return databaseNames;
}
return databaseNames.filter((name) => !isSystemDatabaseName(connection?.db_type, name));
return databaseNames.filter((name) => !isSystemDatabaseName(effectiveDatabaseTypeForConnection(connection), name));
}
export function filterSchemaNamesForVisiblePicker(schemaNames: string[], connection: Partial<Pick<ConnectionConfig, "db_type" | "username" | "show_system_schemas">> | undefined, options?: SchemaFilterOptions): string[] {
export function filterSchemaNamesForVisiblePicker(schemaNames: string[], connection: VisibleDatabaseConnection | undefined, options?: SchemaFilterOptions): string[] {
if (schemaFilterShowSystemSchemas(connection, options)) return schemaNames;
const currentSchema = connection?.username?.trim().toLowerCase();
return schemaNames.filter((name) => name.toLowerCase() === currentSchema || !isSystemSchemaName(connection?.db_type, name));
const databaseType = effectiveDatabaseTypeForConnection(connection);
return schemaNames.filter((name) => name.toLowerCase() === currentSchema || !isSystemSchemaName(databaseType, name));
}
export function connectionUsesVisibleSchemaFilter(connection: Pick<ConnectionConfig, "db_type"> | undefined): boolean {
return connection?.db_type === "oracle" || connection?.db_type === "dameng" || connection?.db_type === "oceanbase-oracle";
export function connectionUsesVisibleSchemaFilter(connection: VisibleDatabaseConnection | undefined): boolean {
return connectionUsesConnectionRootSchemaMode(connection);
}
export function visibleSchemaFilterIsEnabled(visibleSchemas: Record<string, string[]> | undefined, database: string): boolean {
return Array.isArray(visibleSchemas?.[database]);
}
export function filterSchemaNamesForConnection(
schemaNames: string[],
connection: (Pick<ConnectionConfig, "db_type" | "visible_schemas" | "visible_databases" | "show_system_schemas"> & Partial<Pick<ConnectionConfig, "username">>) | undefined,
database: string,
options?: SchemaFilterOptions,
): string[] {
export function filterSchemaNamesForConnection(schemaNames: string[], connection: VisibleDatabaseConnection | undefined, database: string, options?: SchemaFilterOptions): string[] {
const visibleSchemas = connection?.visible_schemas;
if (!visibleSchemaFilterIsEnabled(visibleSchemas, database)) {
if (connectionUsesVisibleSchemaFilter(connection) && visibleDatabaseFilterIsEnabled(connection?.visible_databases)) {

View File

@ -75,6 +75,18 @@ function genericJdbcConnection(): ConnectionConfig {
} as ConnectionConfig;
}
function oracleJdbcConnection(): ConnectionConfig {
return {
...genericJdbcConnection(),
id: "jdbc-oracle-1",
name: "Oracle JDBC",
username: "DBX_TEST",
database: undefined,
connection_string: "jdbc:oracle:thin:@//127.0.0.1:1521/XE",
jdbc_driver_class: "oracle.jdbc.OracleDriver",
} as ConnectionConfig;
}
function informixConnection(): ConnectionConfig {
return {
id: "informix-1",
@ -247,6 +259,49 @@ describe("connectionStore metadata loading", () => {
]);
});
it("loads inferred Oracle JDBC schemas without requesting empty catalogs", async () => {
const listDatabases = vi.fn().mockResolvedValue([]);
const listSchemas = vi.fn().mockResolvedValue(["ANONYMOUS", "DBX_TEST", "SYS", "SYSTEM"]);
const listTables = vi.fn().mockResolvedValue([{ name: "sheet", 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),
listDatabases,
listObjects: vi.fn().mockResolvedValue([]),
listSchemas,
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 = oracleJdbcConnection();
const connectionNode: TreeNode = { id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, isExpanded: false, children: [] };
store.connections = [connection];
store.connectedIds.add(connection.id);
store.treeNodes = [connectionNode];
await store.loadDatabases(connection.id, { force: true });
expect(listDatabases).not.toHaveBeenCalled();
expect(listSchemas).toHaveBeenCalledWith(connection.id, "");
expect(connectionNode.children?.map((node) => [node.type, node.label, node.database, node.schema])).toEqual([["schema", "DBX_TEST", "DBX_TEST", "DBX_TEST"]]);
const schemaNode = connectionNode.children?.[0];
expect(schemaNode).toBeDefined();
await store.loadTreeNodeChildren(schemaNode!, { force: true });
expect(listTables.mock.calls[0]?.slice(0, 3)).toEqual([connection.id, "DBX_TEST", "DBX_TEST"]);
expect(schemaNode?.children?.map((node) => [node.type, node.label, node.schema])).toEqual([["table", "sheet", "DBX_TEST"]]);
});
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 }]);

View File

@ -100,6 +100,18 @@ test("OceanBase Oracle uses schema filtering for visible object selection", () =
assert.equal(connectionUsesVisibleSchemaFilter(config({ db_type: "mysql", driver_profile: "oceanbase" })), false);
});
test("Oracle JDBC uses schema filtering for visible object selection", () => {
const connection = config({
db_type: "jdbc",
driver_profile: "jdbc",
connection_string: "jdbc:oracle:thin:@//127.0.0.1:1521/XE",
username: "DBX_TEST",
});
assert.equal(connectionUsesVisibleSchemaFilter(connection), true);
assert.deepEqual(filterSchemaNamesForConnection(["ANONYMOUS", "DBX_TEST", "SYS", "SYSTEM"], connection, ""), ["DBX_TEST"]);
});
test("Vastbase schema filters preserve ordinary schemas and explicit empty selections", () => {
const schemas = ["public", "app"];
assert.deepEqual(filterSchemaNamesForConnection(schemas, config({ db_type: "vastbase", database: "vastbase" }), "vastbase"), schemas);

View File

@ -4,6 +4,7 @@ import {
codeMirrorSqlDialectForConnection,
connectionShouldDiscoverJdbcSchemas,
connectionShouldLoadIdentifierQuote,
connectionUsesConnectionRootSchemaMode,
connectionUsesDatabaseObjectTreeMode,
effectiveDatabaseTypeForConnection,
gaussdbIdentifierQuoteOverride,
@ -41,6 +42,16 @@ test("infers JDBC dialect from driver profile", () => {
);
});
test("Oracle JDBC uses connection-root schema navigation", () => {
const connection = {
db_type: "jdbc" as const,
connection_string: "jdbc:oracle:thin:@//127.0.0.1:1521/XE",
};
assert.equal(connectionUsesConnectionRootSchemaMode(connection), true);
assert.equal(connectionUsesDatabaseObjectTreeMode(connection), false);
});
test("uses dedicated ClickHouse editor syntax for inferred JDBC connections", () => {
const connection = {
db_type: "jdbc" as const,