feat(oracle): expand package members in schema tree
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
+38
@@ -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"]);
|
||||
});
|
||||
|
||||
+5
-5
@@ -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, "_");
|
||||
|
||||
+2
-2
@@ -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 }]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user