feat(oracle): expand package members in schema tree

This commit is contained in:
t8y2 committed 2026-08-10 18:15:47 +08:00
1 parent ad9075e18f
commit 585e2f3c50
12 files changed
+508 -75

No files matched your search

@@ -91,6 +91,7 @@ import {
supportsTableTruncate,
supportsTableStructureEditing,
supportsTransfer,
supportsPackageMemberExpansion,
usesTreeSchemaMode,
isSingleDatabase,
} from "@/lib/database/databaseCapabilities";
@@ -622,8 +623,8 @@ async function toggle() {
return;
}
if (node.type === "package" && node.connectionId && connectionStore.getConfig(node.connectionId)?.db_type === "xugu") {
await connectionStore.loadXuguPackageMembers(node);
if (node.type === "package" && node.connectionId && supportsPackageMemberExpansion(currentDatabaseType())) {
await connectionStore.loadPackageMembers(node);
emitNodeToggled(node, wasExpanded);
return;
}
@@ -13,6 +13,9 @@ const connectionStore = {
canUseLoadedTreeNodeToggle: vi.fn(() => true),
releaseCollapsedTreeNodeChildren: vi.fn(),
getConfig: vi.fn(() => ({ db_type: "mysql" })),
loadPackageMembers: vi.fn(async (node: TreeNode) => {
node.isExpanded = true;
}),
};
vi.mock("@/stores/connectionStore", () => ({
@@ -41,6 +44,7 @@ afterEach(() => {
connectionStore.treeNodes = [];
connectionStore.sidebarSearchQuery = "";
vi.clearAllMocks();
connectionStore.getConfig.mockReturnValue({ db_type: "mysql" });
});
describe("SidebarTreeRuntimeHost expansion", () => {
@@ -78,4 +82,38 @@ describe("SidebarTreeRuntimeHost expansion", () => {
expect(toggled).toHaveBeenCalledWith(renderedGroup, false);
expect(liveGroup.isExpanded).toBe(false);
});
it("loads Oracle package members through the shared expansion path", async () => {
const packageNode: TreeNode = {
id: "oracle:XE:APP:package:BUSINESS_API",
label: "BUSINESS_API",
type: "package",
connectionId: "oracle",
database: "XE",
schema: "APP",
isExpanded: false,
children: [],
};
connectionStore.treeNodes = [packageNode];
connectionStore.getConfig.mockReturnValue({ db_type: "oracle" });
const host = ref<InstanceType<typeof SidebarTreeRuntimeHost> | null>(null);
const toggled = vi.fn();
const app = createApp(
defineComponent({
setup: () => () => h(SidebarTreeRuntimeHost, { ref: host, node: packageNode, depth: 0, onNodeToggled: toggled }),
}),
);
mountedApps.push(app);
const container = document.createElement("div");
document.body.append(container);
app.use(i18n);
app.mount(container);
host.value?.toggleNode(packageNode);
await vi.waitFor(() => expect(connectionStore.loadPackageMembers).toHaveBeenCalledWith(packageNode));
expect(packageNode.isExpanded).toBe(true);
expect(toggled).toHaveBeenCalledWith(packageNode, true);
});
});
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { normalizeSidebarObjectKind, sidebarObjectKindsForDatabase } from "@/lib/database/databaseObjectCapabilities";
import { normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, supportsPackageMemberExpansion } from "@/lib/database/databaseObjectCapabilities";
describe("databaseObjectCapabilities", () => {
it("exposes supported programmable objects for Dameng", () => {
@@ -11,6 +11,13 @@ describe("databaseObjectCapabilities", () => {
expect(sidebarObjectKindsForDatabase("postgres")).not.toContain("SYNONYM");
});
it("expands package members only for implemented database paths", () => {
expect(supportsPackageMemberExpansion("oracle")).toBe(true);
expect(supportsPackageMemberExpansion("xugu")).toBe(true);
expect(supportsPackageMemberExpansion("dameng")).toBe(false);
expect(supportsPackageMemberExpansion("opengauss")).toBe(false);
});
it("exposes only tables for HBase namespaces", () => {
expect(sidebarObjectKindsForDatabase("hbase")).toEqual(["TABLE"]);
});
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { CompletionAssistantCandidate, TreeNode } from "@/types/database";
import { buildXuguPackageMemberNodes, markXuguPackageNodesExpandable } from "@/lib/sidebar/xuguPackageMembers";
import { buildPackageMemberNodes, markPackageNodesExpandable } from "@/lib/sidebar/packageMembers";
function packageNode(): TreeNode {
return {
@@ -15,10 +15,10 @@ function packageNode(): TreeNode {
};
}
describe("Xugu package member tree", () => {
describe("package member tree", () => {
it("marks package specifications as expandable without changing other objects", () => {
const nodes: TreeNode[] = [packageNode(), { id: "body", label: "business_api", type: "package-body" }, { id: "proc", label: "standalone", type: "procedure" }];
const result = markXuguPackageNodesExpandable(nodes);
const result = markPackageNodesExpandable(nodes);
expect(result[0]?.children).toEqual([]);
expect(result[1]?.children).toBeUndefined();
@@ -32,7 +32,7 @@ describe("Xugu package member tree", () => {
{ name: "lookup", kind: "function", signature: null, data_type: "VARCHAR" },
{ name: "ignored_table", kind: "table" },
];
const result = buildXuguPackageMemberNodes(packageNode(), candidates);
const result = buildPackageMemberNodes(packageNode(), candidates);
expect(result.map((node) => [node.type, node.label])).toEqual([
["procedure", "calculate(p_value IN INT)"],
@@ -50,7 +50,7 @@ describe("Xugu package member tree", () => {
{ name: "MIXEDCASE", kind: "function", signature: "p INT" },
{ name: "", kind: "procedure" },
];
const result = buildXuguPackageMemberNodes(packageNode(), candidates);
const result = buildPackageMemberNodes(packageNode(), candidates);
expect(result.map((node) => node.objectName)).toEqual(["MixedCase", "MIXEDCASE"]);
});
@@ -18,6 +18,7 @@ const POSTGRES_LIKE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZ
const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"];
const DAMENG_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "MATERIALIZED_VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE", "PACKAGE", "PACKAGE_BODY"];
const XUGU_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "TRIGGER", "SEQUENCE", "SYNONYM", "PACKAGE", "PACKAGE_BODY", "TYPE", "TYPE_BODY"];
const PACKAGE_MEMBER_EXPANSION_DATABASES = new Set<DatabaseType>(["oracle", "xugu"]);
const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
// postgres
@@ -82,6 +83,10 @@ export function sidebarObjectKindsForDatabase(dbType?: DatabaseType): SidebarObj
return DATABASE_TYPE_OBJECTS.get(dbType) ?? [...ROUTINE_OBJECTS];
}
export function supportsPackageMemberExpansion(dbType?: DatabaseType): boolean {
return !!dbType && PACKAGE_MEMBER_EXPANSION_DATABASES.has(dbType);
}
export function normalizeSidebarObjectKind(type: string): SidebarObjectKind {
const value = type.toUpperCase();
const normalized = value.replace(/[\s-]+/g, "_");
@@ -1,10 +1,10 @@
import type { CompletionAssistantCandidate, TreeNode } from "@/types/database";
export function markXuguPackageNodesExpandable(nodes: TreeNode[]): TreeNode[] {
export function markPackageNodesExpandable(nodes: TreeNode[]): TreeNode[] {
return nodes.map((node) => (node.type === "package" ? { ...node, children: node.children ?? [] } : node));
}
export function buildXuguPackageMemberNodes(packageNode: TreeNode, candidates: readonly CompletionAssistantCandidate[]): TreeNode[] {
export function buildPackageMemberNodes(packageNode: TreeNode, candidates: readonly CompletionAssistantCandidate[]): TreeNode[] {
const parentName = packageNode.objectName || packageNode.label;
const parentSchema = packageNode.schema;
const seen = new Set<string>();
@@ -178,7 +178,7 @@ describe("connectionStore metadata loading", () => {
store.connectedIds = new Set([connection.id]);
store.treeNodes = [packageNode];
await store.loadXuguPackageMembers(packageNode);
await store.loadPackageMembers(packageNode);
expect(completionAssistantSearch).toHaveBeenCalledWith({
connection_id: connection.id,
@@ -200,6 +200,84 @@ describe("connectionStore metadata loading", () => {
expect(packageNode.children?.every((child) => child.parentName === "business_api")).toBe(true);
}, 15000);
it("loads Oracle package overloads through the shared package member path", async () => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [
{ name: "CALCULATE", kind: "procedure", signature: "P_VALUE IN NUMBER" },
{ name: "CALCULATE", kind: "procedure", signature: "P_VALUE IN VARCHAR2" },
{ name: "ITEM_COUNT", kind: "function", data_type: "NUMBER" },
],
incomplete: false,
fallback_used: false,
});
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({ checkConnectionHealth: vi.fn().mockResolvedValue(undefined), completionAssistantSearch }));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
const connection = oracleConnection();
const packageNode: TreeNode = {
id: "oracle-1:XE:APP:package:BUSINESS_API",
label: "BUSINESS_API",
type: "package",
connectionId: connection.id,
database: "XE",
schema: "APP",
children: [],
isExpanded: false,
};
store.connections = [connection];
store.connectedIds = new Set([connection.id]);
store.treeNodes = [packageNode];
await store.loadPackageMembers(packageNode);
expect(completionAssistantSearch).toHaveBeenCalledWith({
connection_id: connection.id,
database: "XE",
schema: "APP",
object_kinds: ["routine"],
mask: "",
case_sensitive: true,
global_search: false,
max_results: 1000,
search_in_comments: false,
search_in_definitions: false,
parent_schema: "APP",
parent_name: "BUSINESS_API",
match_mode: "prefix",
});
expect(packageNode.isExpanded).toBe(true);
expect(packageNode.children?.map((child) => child.label)).toEqual(["CALCULATE(P_VALUE IN NUMBER)", "CALCULATE(P_VALUE IN VARCHAR2)", "ITEM_COUNT"]);
}, 15000);
it("does not query package members for unsupported databases", async () => {
const completionAssistantSearch = vi.fn();
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({ checkConnectionHealth: vi.fn().mockResolvedValue(undefined), completionAssistantSearch }));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
const connection = { ...xuguConnection(), id: "dameng-1", db_type: "dameng" } as ConnectionConfig;
const packageNode: TreeNode = {
id: "dameng-1:APP:package:BUSINESS_API",
label: "BUSINESS_API",
type: "package",
connectionId: connection.id,
database: "APP",
schema: "APP",
children: [],
isExpanded: false,
};
store.connections = [connection];
store.connectedIds = new Set([connection.id]);
await store.loadPackageMembers(packageNode);
expect(completionAssistantSearch).not.toHaveBeenCalled();
expect(packageNode.isExpanded).toBe(false);
}, 15000);
it("loads missing database roots only for connected sidebar search targets", async () => {
const checkConnectionHealth = vi.fn().mockResolvedValue(undefined);
const listDatabases = vi.fn().mockResolvedValue([{ name: "dajia", comment: null }]);
+12 -10
View File
@@ -62,7 +62,7 @@ import { mergeSqlObjectNavigationType, sqlObjectNavigationTypeFromTableType } fr
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 { connectionIsDorisFamilyCatalogCapable, isInternalDorisCatalog, isSchemaAware, normalizeSidebarObjectKind, sidebarObjectKindsForDatabase, supportsPackageMemberExpansion, usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
import { connectionObjectTreeNodeSchema, connectionObjectTreeQuerySchema, connectionShouldDiscoverJdbcSchemas, connectionShouldLoadIdentifierQuote, connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection, gaussdbIdentifierQuoteOverride } from "@/lib/database/jdbcDialect";
import { buildDatabaseTreeNodes, buildDuckDbConnectionTreeNodes, compareSidebarNames, sortSidebarDatabases, sortSidebarNames, shouldIncludeDefaultDatabaseNode } from "@/lib/database/databaseTree";
import { buildSqlServerDatabaseTreeNodes } from "@/lib/database/sqlServerTree";
@@ -111,7 +111,7 @@ import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, hasInstalledAgentVer
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
import { appendVisibleDatabaseSelection } from "@/lib/connection/connectionVisibleDatabases";
import { filterNacosNamespacesForSidebar, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility";
import { buildXuguPackageMemberNodes, markXuguPackageNodesExpandable } from "@/lib/sidebar/xuguPackageMembers";
import { buildPackageMemberNodes, markPackageNodesExpandable } from "@/lib/sidebar/packageMembers";
import { configuredDatabaseProductName, connectionConfigFingerprint, normalizeDatabaseConnectionInfo } from "@/lib/connection/connectionDatabaseInfo";
import { createMetadataLoadTrace, logMetadataLoadTrace, MetadataLoadCoordinator, type MetadataLoadTraceLogger } from "@/lib/metadata/metadataLoadCoordinator";
import type { MetadataScopeInput } from "@/lib/metadata/metadataLoadScope";
@@ -1606,7 +1606,8 @@ export const useConnectionStore = defineStore("connection", () => {
});
const refreshedGroup = grouped.find((group) => group.type === options.node.type);
const children = refreshedGroup?.children ?? [];
return options.node.connectionId && getConfig(options.node.connectionId)?.db_type === "xugu" ? markXuguPackageNodesExpandable(children) : children;
const databaseType = options.node.connectionId ? effectiveDatabaseTypeForConnection(getConfig(options.node.connectionId)) : undefined;
return supportsPackageMemberExpansion(databaseType) ? markPackageNodesExpandable(children) : children;
}
function tableInfosToCompletionTables(tables: readonly TableInfo[], schema?: string): SqlCompletionTable[] {
@@ -1870,8 +1871,8 @@ export const useConnectionStore = defineStore("connection", () => {
schema: options.effectiveSchema,
objects: supplementalObjects,
});
if (getConfig(options.connectionId)?.db_type === "xugu") {
supplementalChildren = markXuguPackageNodesExpandable(supplementalChildren);
if (supportsPackageMemberExpansion(effectiveDatabaseTypeForConnection(getConfig(options.connectionId)))) {
supplementalChildren = markPackageNodesExpandable(supplementalChildren);
}
if (supplementalChildren.length === 0) return;
if (isTreeLoadSearchChanged(searchFilter, options.loadOptions)) return;
@@ -5618,8 +5619,9 @@ export const useConnectionStore = defineStore("connection", () => {
});
}
async function loadXuguPackageMembers(node: TreeNode, options?: LoadTreeOptions): Promise<void> {
if (node.type !== "package" || !node.connectionId || !node.database || getConfig(node.connectionId)?.db_type !== "xugu") return;
async function loadPackageMembers(node: TreeNode, options?: LoadTreeOptions): Promise<void> {
if (node.type !== "package" || !node.connectionId || !node.database) return;
if (!supportsPackageMemberExpansion(effectiveDatabaseTypeForConnection(getConfig(node.connectionId)))) return;
const connectionId = node.connectionId;
const database = node.database;
const schema = node.schema;
@@ -5629,7 +5631,7 @@ export const useConnectionStore = defineStore("connection", () => {
try {
await runTreeMetadataLoad(
{
kind: "xugu-package-members",
kind: "package-members",
connectionId,
database,
schema,
@@ -5656,7 +5658,7 @@ export const useConnectionStore = defineStore("connection", () => {
});
const targetNode = treeNodeLoadTarget(load);
if (!targetNode) return;
setChildren(targetNode, buildXuguPackageMemberNodes(targetNode, response.candidates));
setChildren(targetNode, buildPackageMemberNodes(targetNode, response.candidates));
targetNode.isExpanded = true;
},
options,
@@ -7280,7 +7282,7 @@ export const useConnectionStore = defineStore("connection", () => {
loadTables,
loadTableForLocate,
loadObjectGroupChildren,
loadXuguPackageMembers,
loadPackageMembers,
loadMoreObjectGroupChildren,
loadAllObjectGroupChildren,
loadTableGroups,