feat(xugu): expose package members in schema tree
This commit is contained in:
1 parent
37f13018d3
commit
ad9075e18f
15 files changed
+922
-28
No files matched your search
@@ -12,7 +12,7 @@ import { matchSidebarLabel } from "@/lib/sidebar/sidebarSearch";
|
||||
import { buildTableTreeNodes } from "@/lib/table/tableTree";
|
||||
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut, isViewTableDdlShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { sidebarNodeSupportsDdlView } from "@/lib/sidebar/sidebarTreeDdlShortcut";
|
||||
import { copyNameForTreeNode, objectSourceKindForTreeNode } from "@/lib/sidebar/treeNodeClick";
|
||||
import { copyNameForTreeNode, objectSourceTargetForTreeNode } from "@/lib/sidebar/treeNodeClick";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { connectionPasteTargetGroupId, copySelectedConnectionsToClipboards, selectedConnectionEditTarget } from "@/lib/sidebar/sidebarConnectionSelection";
|
||||
import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch";
|
||||
@@ -106,7 +106,8 @@ const sidebarDdlDatabaseType = computed(() => {
|
||||
const connectionId = sidebarDdlTarget.value?.connectionId;
|
||||
return connectionId ? effectiveDatabaseTypeForConnection(store.getConfig(connectionId)) : undefined;
|
||||
});
|
||||
const sidebarObjectSourceType = computed(() => (sidebarObjectSourceTarget.value ? objectSourceKindForTreeNode(sidebarObjectSourceTarget.value.node.type) : null));
|
||||
const sidebarObjectSourceResolvedTarget = computed(() => (sidebarObjectSourceTarget.value ? objectSourceTargetForTreeNode(sidebarObjectSourceTarget.value.node) : null));
|
||||
const sidebarObjectSourceType = computed(() => sidebarObjectSourceResolvedTarget.value?.objectType ?? null);
|
||||
const sidebarObjectSourceDatabaseType = computed(() => {
|
||||
const connectionId = sidebarObjectSourceTarget.value?.node.connectionId;
|
||||
return connectionId ? effectiveDatabaseTypeForConnection(store.getConfig(connectionId)) : undefined;
|
||||
@@ -1336,7 +1337,7 @@ function openSidebarDdlForSelection(): boolean {
|
||||
}
|
||||
|
||||
function openSidebarObjectSource(node: TreeNode, initialEditing: boolean) {
|
||||
if (!node.connectionId || !node.database || !objectSourceKindForTreeNode(node.type)) return;
|
||||
if (!node.connectionId || !node.database || !objectSourceTargetForTreeNode(node)) return;
|
||||
const target = createSidebarActionTarget(node);
|
||||
const requestGeneration = beginSidebarAction();
|
||||
void store
|
||||
@@ -1980,10 +1981,10 @@ defineExpose({ focusSearch, createNewGroup, collapseAllTreeNodes });
|
||||
v-model:open="sidebarObjectSourceOpen"
|
||||
:connection-id="sidebarObjectSourceTarget.node.connectionId!"
|
||||
:database="sidebarObjectSourceTarget.node.database!"
|
||||
:schema="sidebarObjectSourceTarget.node.schema"
|
||||
:name="sidebarObjectSourceTarget.node.objectName || sidebarObjectSourceTarget.node.label"
|
||||
:schema="sidebarObjectSourceResolvedTarget?.schema"
|
||||
:name="sidebarObjectSourceResolvedTarget!.name"
|
||||
:relation-name="sidebarObjectSourceTarget.node.tableName"
|
||||
:signature="sidebarObjectSourceTarget.node.signature"
|
||||
:signature="sidebarObjectSourceResolvedTarget?.signature"
|
||||
:object-type="sidebarObjectSourceType"
|
||||
:database-type="sidebarObjectSourceDatabaseType"
|
||||
:dialect="sidebarObjectSourceDialect"
|
||||
|
||||
@@ -94,7 +94,7 @@ import {
|
||||
usesTreeSchemaMode,
|
||||
isSingleDatabase,
|
||||
} from "@/lib/database/databaseCapabilities";
|
||||
import { copyNameForTreeNode, isDirectNavigationTreeNode, isDocumentBrowserTreeNode, objectSourceKindForTreeNode, shouldRunTreeNodeRowAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/sidebar/treeNodeClick";
|
||||
import { copyNameForTreeNode, isDirectNavigationTreeNode, isDocumentBrowserTreeNode, objectSourceTargetForTreeNode, shouldRunTreeNodeRowAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "@/lib/sidebar/treeNodeClick";
|
||||
import { mongoCollectionTableTypeFromNode, mongoDropIndexFailureCount } from "@/lib/sidebar/mongoCollectionMutation";
|
||||
import { dataTabOpenModeFromTreeClick, type DataTabOpenMode } from "@/lib/sidebar/dataTabOpenPolicy";
|
||||
import { isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
@@ -622,6 +622,12 @@ async function toggle() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === "package" && node.connectionId && connectionStore.getConfig(node.connectionId)?.db_type === "xugu") {
|
||||
await connectionStore.loadXuguPackageMembers(node);
|
||||
emitNodeToggled(node, wasExpanded);
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === "connection" && node.connectionId) {
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
if (config?.db_type === "redis") {
|
||||
@@ -1674,18 +1680,18 @@ function openObjectSourceDialog(initialEditing: boolean) {
|
||||
if (!node.connectionId || !node.database) return;
|
||||
const connectionId = node.connectionId;
|
||||
const database = node.database;
|
||||
const objectType = objectSourceKindForTreeNode(node.type);
|
||||
if (!objectType) return;
|
||||
const sourceTarget = objectSourceTargetForTreeNode(node);
|
||||
if (!sourceTarget) return;
|
||||
const openMode = settingsStore.editorSettings.routineSourceOpenMode;
|
||||
if (openMode === "query-tab") {
|
||||
void connectionStore
|
||||
.ensureConnected(connectionId)
|
||||
.then(async () => {
|
||||
connectionStore.activeConnectionId = connectionId;
|
||||
const schema = node.schema || database;
|
||||
const schema = sourceTarget.schema || database;
|
||||
const databaseType = effectiveDatabaseTypeForConnection(connectionStore.getConfig(connectionId));
|
||||
if (!databaseType) throw new Error("Connection type is unavailable.");
|
||||
const objectName = node.objectName || node.label;
|
||||
const objectName = sourceTarget.name;
|
||||
const {
|
||||
raw,
|
||||
editableSource,
|
||||
@@ -1695,7 +1701,7 @@ function openObjectSourceDialog(initialEditing: boolean) {
|
||||
database,
|
||||
schema,
|
||||
name: objectName,
|
||||
objectType: objectType as any,
|
||||
objectType: sourceTarget.objectType as any,
|
||||
databaseType,
|
||||
signature: node.signature,
|
||||
});
|
||||
@@ -4649,14 +4655,15 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
|
||||
// 8. Procedure / Function / Package
|
||||
if (node.type === "procedure" || node.type === "function") {
|
||||
if (node.type === "procedure") {
|
||||
const isPackageMember = node.parentType === "package" && !!node.parentName;
|
||||
if (node.type === "procedure" && !isPackageMember) {
|
||||
items.push({ label: t("contextMenu.executeProcedure"), action: openProcedureExecution, icon: Play });
|
||||
}
|
||||
if (currentDatabaseType() === "xugu" && buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label })) {
|
||||
if (!isPackageMember && currentDatabaseType() === "xugu" && buildXuguCompileSql({ objectType: node.type, schema: node.schema, name: node.objectName || node.label })) {
|
||||
items.push({ label: t("contextMenu.compileObject"), action: compileXuguObject, icon: Wrench });
|
||||
}
|
||||
items.push({ label: t("contextMenu.viewSource"), action: () => openObjectSourceDialog(false), icon: Code2 });
|
||||
if (canRenameObject.value) {
|
||||
if (!isPackageMember && canRenameObject.value) {
|
||||
items.push({
|
||||
label: t("contextMenu.renameObject"),
|
||||
action: openRenameObjectDialog,
|
||||
@@ -4666,14 +4673,16 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
}
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
label: deleteMenuLabel(node.type === "procedure" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction")),
|
||||
action: deleteMenuAction(requestDropObject),
|
||||
icon: Trash2,
|
||||
shortcut: shortcutDelete,
|
||||
variant: "destructive" as const,
|
||||
});
|
||||
if (!isPackageMember) {
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
label: deleteMenuLabel(node.type === "procedure" ? t("contextMenu.dropProcedure") : t("contextMenu.dropFunction")),
|
||||
action: deleteMenuAction(requestDropObject),
|
||||
icon: Trash2,
|
||||
shortcut: shortcutDelete,
|
||||
variant: "destructive" as const,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -558,6 +558,7 @@ const canExpand = computed(() =>
|
||||
canTreeNodeShowExpander({
|
||||
type: activeNode.value.type,
|
||||
childCount: activeNode.value.children?.length ?? 0,
|
||||
explicitContainer: activeNode.value.children !== undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -27,6 +27,13 @@ describe("sidebar tree item layout", () => {
|
||||
expect(canTreeNodeShowExpander({ type: "consul-overview", childCount: 0 })).toBe(false);
|
||||
});
|
||||
|
||||
it("shows an expander only for package nodes explicitly marked as containers", () => {
|
||||
expect(canTreeNodeShowExpander({ type: "package", childCount: 0 })).toBe(false);
|
||||
expect(canTreeNodeShowExpander({ type: "package", childCount: 0, explicitContainer: true })).toBe(true);
|
||||
expect(canTreeNodeShowExpander({ type: "package-body", childCount: 0, explicitContainer: true })).toBe(false);
|
||||
expect(canTreeNodeShowExpander({ type: "procedure", childCount: 0, explicitContainer: true })).toBe(false);
|
||||
});
|
||||
|
||||
it("aligns comments to the longest sibling name without crossing parent groups", () => {
|
||||
const widths = alignedSidebarCommentLabelWidths([
|
||||
{ id: "tables", depth: 1, alignable: false, hasComment: false, labelWidth: 0 },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isDirectNavigationTreeNode, objectSourceKindForTreeNode, shouldActivateTreeNodeOnSingleClick, treeNodeRowAction } from "@/lib/sidebar/treeNodeClick";
|
||||
import { isDirectNavigationTreeNode, objectSourceKindForTreeNode, objectSourceTargetForTreeNode, shouldActivateTreeNodeOnSingleClick, treeNodeRowAction } from "@/lib/sidebar/treeNodeClick";
|
||||
|
||||
describe("treeNodeClick", () => {
|
||||
it("opens synonym nodes as synonym source", () => {
|
||||
@@ -17,4 +17,48 @@ describe("treeNodeClick", () => {
|
||||
expect(treeNodeRowAction("consul-root", false, "double")).toBe("toggle");
|
||||
expect(treeNodeRowAction("consul-overview", false, "double")).toBe("toggle");
|
||||
});
|
||||
|
||||
it("expands package containers while preserving source behavior for leaf packages", () => {
|
||||
expect(treeNodeRowAction("package", true)).toBe("toggle");
|
||||
expect(treeNodeRowAction("package", false)).toBe("open-source");
|
||||
});
|
||||
|
||||
it("routes package members to their owning package body", () => {
|
||||
expect(
|
||||
objectSourceTargetForTreeNode({
|
||||
id: "pkg:member",
|
||||
label: "calculate(p_value IN INT)",
|
||||
type: "function",
|
||||
objectName: "calculate",
|
||||
parentName: "business_api",
|
||||
parentSchema: "app_schema",
|
||||
parentType: "package",
|
||||
schema: "ignored_schema",
|
||||
signature: "p_value IN INT",
|
||||
}),
|
||||
).toEqual({
|
||||
name: "business_api",
|
||||
schema: "app_schema",
|
||||
objectType: "PACKAGE",
|
||||
signature: "p_value IN INT",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps standalone routine source routing unchanged", () => {
|
||||
expect(objectSourceTargetForTreeNode({ id: "proc", label: "standalone_proc", type: "procedure", schema: "app" })).toEqual({
|
||||
name: "standalone_proc",
|
||||
schema: "app",
|
||||
objectType: "PROCEDURE",
|
||||
signature: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not reinterpret unrelated parent metadata as a package member", () => {
|
||||
expect(objectSourceTargetForTreeNode({ id: "routine", label: "child_routine", type: "function", schema: "app", parentName: "other_parent" })).toEqual({
|
||||
name: "child_routine",
|
||||
schema: "app",
|
||||
objectType: "FUNCTION",
|
||||
signature: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CompletionAssistantCandidate, TreeNode } from "@/types/database";
|
||||
import { buildXuguPackageMemberNodes, markXuguPackageNodesExpandable } from "@/lib/sidebar/xuguPackageMembers";
|
||||
|
||||
function packageNode(): TreeNode {
|
||||
return {
|
||||
id: "conn:db:schema:package:business_api",
|
||||
label: "business_api",
|
||||
type: "package",
|
||||
objectName: "business_api",
|
||||
connectionId: "conn",
|
||||
database: "db",
|
||||
schema: "app_schema",
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Xugu 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);
|
||||
|
||||
expect(result[0]?.children).toEqual([]);
|
||||
expect(result[1]?.children).toBeUndefined();
|
||||
expect(result[2]?.children).toBeUndefined();
|
||||
});
|
||||
|
||||
it("builds public procedure and function children with overload identities", () => {
|
||||
const candidates: CompletionAssistantCandidate[] = [
|
||||
{ name: "calculate", kind: "procedure", signature: "p_value IN INT" },
|
||||
{ name: "calculate", kind: "procedure", signature: "p_value IN VARCHAR" },
|
||||
{ name: "lookup", kind: "function", signature: null, data_type: "VARCHAR" },
|
||||
{ name: "ignored_table", kind: "table" },
|
||||
];
|
||||
const result = buildXuguPackageMemberNodes(packageNode(), candidates);
|
||||
|
||||
expect(result.map((node) => [node.type, node.label])).toEqual([
|
||||
["procedure", "calculate(p_value IN INT)"],
|
||||
["procedure", "calculate(p_value IN VARCHAR)"],
|
||||
["function", "lookup"],
|
||||
]);
|
||||
expect(result.every((node) => node.parentName === "business_api" && node.parentSchema === "app_schema" && node.parentType === "package" && node.valid === false)).toBe(true);
|
||||
expect(new Set(result.map((node) => node.id)).size).toBe(3);
|
||||
});
|
||||
|
||||
it("deduplicates exact duplicate metadata without merging case-distinct members", () => {
|
||||
const candidates: CompletionAssistantCandidate[] = [
|
||||
{ name: "MixedCase", kind: "function", signature: "p INT" },
|
||||
{ name: "MixedCase", kind: "function", signature: "p INT" },
|
||||
{ name: "MIXEDCASE", kind: "function", signature: "p INT" },
|
||||
{ name: "", kind: "procedure" },
|
||||
];
|
||||
const result = buildXuguPackageMemberNodes(packageNode(), candidates);
|
||||
|
||||
expect(result.map((node) => node.objectName)).toEqual(["MixedCase", "MIXEDCASE"]);
|
||||
});
|
||||
});
|
||||
@@ -155,8 +155,8 @@ export function canTreeNodeExpand(type: TreeNodeType): boolean {
|
||||
return !leafTypes.has(type);
|
||||
}
|
||||
|
||||
export function canTreeNodeShowExpander({ type, childCount }: { type: TreeNodeType; childCount?: number }): boolean {
|
||||
if (!canTreeNodeExpand(type)) return false;
|
||||
export function canTreeNodeShowExpander({ type, childCount, explicitContainer = false }: { type: TreeNodeType; childCount?: number; explicitContainer?: boolean }): boolean {
|
||||
if (!canTreeNodeExpand(type) && !(type === "package" && explicitContainer)) return false;
|
||||
if (childCount === 0 && emptyContainerTypes.has(type)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,32 @@ export function objectSourceKindForTreeNode(type: TreeNodeType): ObjectSourceKin
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface TreeNodeObjectSourceTarget {
|
||||
name: string;
|
||||
schema?: string;
|
||||
objectType: ObjectSourceKind;
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export function objectSourceTargetForTreeNode(node: TreeNode): TreeNodeObjectSourceTarget | null {
|
||||
if ((node.type === "procedure" || node.type === "function") && node.parentType === "package" && node.parentName) {
|
||||
return {
|
||||
name: node.parentName,
|
||||
schema: node.parentSchema || node.schema,
|
||||
objectType: "PACKAGE",
|
||||
signature: node.signature,
|
||||
};
|
||||
}
|
||||
const objectType = objectSourceKindForTreeNode(node.type);
|
||||
if (!objectType) return null;
|
||||
return {
|
||||
name: node.objectName || node.label,
|
||||
schema: node.schema,
|
||||
objectType,
|
||||
signature: node.signature,
|
||||
};
|
||||
}
|
||||
|
||||
export function isDocumentBrowserTreeNode(type: TreeNodeType): boolean {
|
||||
return documentBrowserNodeTypes.has(type);
|
||||
}
|
||||
@@ -66,6 +92,7 @@ export function treeNodeRowAction(type: TreeNodeType, canExpand: boolean, activa
|
||||
if (!shouldActivateTreeNodeOnSingleClick(type, activation)) return "none";
|
||||
if (type === "extension") return "open-extension-details";
|
||||
if (dataNodeTypes.has(type)) return "open-data";
|
||||
if (type === "package" && canExpand) return "toggle";
|
||||
if (sourceNodeTypes.has(type)) return "open-source";
|
||||
if (toggleLeafNodeTypes.has(type)) return "toggle";
|
||||
if (canExpand) return "toggle";
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { CompletionAssistantCandidate, TreeNode } from "@/types/database";
|
||||
|
||||
export function markXuguPackageNodesExpandable(nodes: TreeNode[]): TreeNode[] {
|
||||
return nodes.map((node) => (node.type === "package" ? { ...node, children: node.children ?? [] } : node));
|
||||
}
|
||||
|
||||
export function buildXuguPackageMemberNodes(packageNode: TreeNode, candidates: readonly CompletionAssistantCandidate[]): TreeNode[] {
|
||||
const parentName = packageNode.objectName || packageNode.label;
|
||||
const parentSchema = packageNode.schema;
|
||||
const seen = new Set<string>();
|
||||
const children: TreeNode[] = [];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (candidate.kind !== "procedure" && candidate.kind !== "function") continue;
|
||||
const name = candidate.name.trim();
|
||||
if (!name) continue;
|
||||
const signature = candidate.signature?.trim() || "";
|
||||
const key = `${candidate.kind}\0${name}\0${signature}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
children.push({
|
||||
id: `${packageNode.id}:member:${candidate.kind}:${name}:${signature}`,
|
||||
label: signature ? `${name}(${signature})` : name,
|
||||
type: candidate.kind,
|
||||
objectName: name,
|
||||
signature: signature || undefined,
|
||||
parentName,
|
||||
parentSchema,
|
||||
parentType: "package",
|
||||
valid: packageNode.valid,
|
||||
connectionId: packageNode.connectionId,
|
||||
database: packageNode.database,
|
||||
schema: parentSchema,
|
||||
isExpanded: false,
|
||||
children: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
@@ -60,6 +60,19 @@ function oracleConnection(): ConnectionConfig {
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function xuguConnection(): ConnectionConfig {
|
||||
return {
|
||||
id: "xugu-1",
|
||||
name: "XuguDB",
|
||||
db_type: "xugu",
|
||||
host: "127.0.0.1",
|
||||
port: 5138,
|
||||
username: "app_user",
|
||||
password: "",
|
||||
database: "app_db",
|
||||
} as ConnectionConfig;
|
||||
}
|
||||
|
||||
function genericJdbcConnection(): ConnectionConfig {
|
||||
return {
|
||||
id: "jdbc-1",
|
||||
@@ -132,6 +145,61 @@ describe("connectionStore metadata loading", () => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
// Dynamic import of the merged connectionStore plus the scoped metadata
|
||||
// load can exceed Vitest's 5s default on slower CI runners; the assertion
|
||||
// is deterministic, so allow a wider timeout.
|
||||
it("loads Xugu package members through the scoped completion endpoint", async () => {
|
||||
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
||||
candidates: [
|
||||
{ name: "process_item", kind: "procedure", signature: "p_id IN INT" },
|
||||
{ name: "process_item", kind: "procedure", signature: "p_code IN VARCHAR" },
|
||||
{ name: "item_count", kind: "function", data_type: "INT" },
|
||||
],
|
||||
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 = xuguConnection();
|
||||
const packageNode: TreeNode = {
|
||||
id: "xugu-1:app_db:app_schema:package:business_api",
|
||||
label: "business_api",
|
||||
type: "package",
|
||||
connectionId: connection.id,
|
||||
database: "app_db",
|
||||
schema: "app_schema",
|
||||
children: [],
|
||||
isExpanded: false,
|
||||
};
|
||||
store.connections = [connection];
|
||||
store.connectedIds = new Set([connection.id]);
|
||||
store.treeNodes = [packageNode];
|
||||
|
||||
await store.loadXuguPackageMembers(packageNode);
|
||||
|
||||
expect(completionAssistantSearch).toHaveBeenCalledWith({
|
||||
connection_id: connection.id,
|
||||
database: "app_db",
|
||||
schema: "app_schema",
|
||||
object_kinds: ["routine"],
|
||||
mask: "",
|
||||
case_sensitive: true,
|
||||
global_search: false,
|
||||
max_results: 1000,
|
||||
search_in_comments: false,
|
||||
search_in_definitions: false,
|
||||
parent_schema: "app_schema",
|
||||
parent_name: "business_api",
|
||||
match_mode: "prefix",
|
||||
});
|
||||
expect(packageNode.isExpanded).toBe(true);
|
||||
expect(packageNode.children?.map((child) => child.label)).toEqual(["process_item(p_id IN INT)", "process_item(p_code IN VARCHAR)", "item_count"]);
|
||||
expect(packageNode.children?.every((child) => child.parentName === "business_api")).toBe(true);
|
||||
}, 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 }]);
|
||||
|
||||
@@ -111,6 +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 { configuredDatabaseProductName, connectionConfigFingerprint, normalizeDatabaseConnectionInfo } from "@/lib/connection/connectionDatabaseInfo";
|
||||
import { createMetadataLoadTrace, logMetadataLoadTrace, MetadataLoadCoordinator, type MetadataLoadTraceLogger } from "@/lib/metadata/metadataLoadCoordinator";
|
||||
import type { MetadataScopeInput } from "@/lib/metadata/metadataLoadScope";
|
||||
@@ -1604,7 +1605,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
objects: options.objects.filter((object) => options.objectTypes.includes(normalizedObjectTreeKind(object.object_type))),
|
||||
});
|
||||
const refreshedGroup = grouped.find((group) => group.type === options.node.type);
|
||||
return refreshedGroup?.children ?? [];
|
||||
const children = refreshedGroup?.children ?? [];
|
||||
return options.node.connectionId && getConfig(options.node.connectionId)?.db_type === "xugu" ? markXuguPackageNodesExpandable(children) : children;
|
||||
}
|
||||
|
||||
function tableInfosToCompletionTables(tables: readonly TableInfo[], schema?: string): SqlCompletionTable[] {
|
||||
@@ -1861,13 +1863,16 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
);
|
||||
const supplementalObjects = filterSimpleSidebarSupplementalObjects(objects);
|
||||
if (supplementalObjects.length === 0) return;
|
||||
const supplementalChildren = buildSimpleObjectTreeNodes({
|
||||
let supplementalChildren = buildSimpleObjectTreeNodes({
|
||||
nodeId: options.nodeId,
|
||||
connectionId: options.connectionId,
|
||||
database: options.database,
|
||||
schema: options.effectiveSchema,
|
||||
objects: supplementalObjects,
|
||||
});
|
||||
if (getConfig(options.connectionId)?.db_type === "xugu") {
|
||||
supplementalChildren = markXuguPackageNodesExpandable(supplementalChildren);
|
||||
}
|
||||
if (supplementalChildren.length === 0) return;
|
||||
if (isTreeLoadSearchChanged(searchFilter, options.loadOptions)) return;
|
||||
const targetNode = treeNodeLoadTarget(options.load);
|
||||
@@ -5613,6 +5618,57 @@ 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;
|
||||
const connectionId = node.connectionId;
|
||||
const database = node.database;
|
||||
const schema = node.schema;
|
||||
const packageName = node.objectName || node.label;
|
||||
let load = beginTreeNodeLoad(node);
|
||||
|
||||
try {
|
||||
await runTreeMetadataLoad(
|
||||
{
|
||||
kind: "xugu-package-members",
|
||||
connectionId,
|
||||
database,
|
||||
schema,
|
||||
nodeKind: node.type,
|
||||
extra: { packageName },
|
||||
},
|
||||
async () => {
|
||||
await ensureConnected(connectionId);
|
||||
load = reclaimTreeNodeLoad(load, node);
|
||||
const response = await completionAssistantSearch({
|
||||
connection_id: connectionId,
|
||||
database,
|
||||
schema: schema ?? null,
|
||||
object_kinds: ["routine"],
|
||||
mask: "",
|
||||
case_sensitive: true,
|
||||
global_search: false,
|
||||
max_results: 1000,
|
||||
search_in_comments: false,
|
||||
search_in_definitions: false,
|
||||
parent_schema: schema ?? null,
|
||||
parent_name: packageName,
|
||||
match_mode: "prefix",
|
||||
});
|
||||
const targetNode = treeNodeLoadTarget(load);
|
||||
if (!targetNode) return;
|
||||
setChildren(targetNode, buildXuguPackageMemberNodes(targetNode, response.candidates));
|
||||
targetNode.isExpanded = true;
|
||||
},
|
||||
options,
|
||||
);
|
||||
} catch (error) {
|
||||
recordMetadataLoadError(connectionId, error, load);
|
||||
throw error;
|
||||
} finally {
|
||||
finishTreeNodeLoad(load);
|
||||
}
|
||||
}
|
||||
|
||||
const ORACLE_SYSTEM_COMPLETION_SCHEMAS = new Set(["SYS", "SYSTEM", "SYSMAN", "DBSNMP", "OUTLN", "XDB", "MDSYS", "CTXSYS", "WMSYS"]);
|
||||
const FILTERED_ROUTINE_COMPLETION_DATABASES = new Set<DatabaseType>(["mysql", "postgres", "sqlserver", "oracle"]);
|
||||
|
||||
@@ -7224,6 +7280,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
loadTables,
|
||||
loadTableForLocate,
|
||||
loadObjectGroupChildren,
|
||||
loadXuguPackageMembers,
|
||||
loadMoreObjectGroupChildren,
|
||||
loadAllObjectGroupChildren,
|
||||
loadTableGroups,
|
||||
|
||||
@@ -890,6 +890,10 @@ export interface TreeNode {
|
||||
tableName?: string;
|
||||
objectName?: string;
|
||||
signature?: string;
|
||||
/** Owning package/type for nested routine members. */
|
||||
parentName?: string;
|
||||
parentSchema?: string;
|
||||
parentType?: TreeNodeType;
|
||||
tableType?: string;
|
||||
comment?: string | null;
|
||||
valid?: boolean | null;
|
||||
|
||||
Reference in new issue
Block a user