diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 2d54582d4..1c8601304 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -1653,7 +1653,7 @@ async function handleQuickOpenSelect(item: any) { const schema = item.schema || item.database; try { - const result = await api.getObjectSource(item.connectionId, item.database, schema, item.objectName || item.tableName, objectType); + const result = await api.getObjectSource(item.connectionId, item.database, schema, item.objectName || item.tableName, objectType, item.signature); const tabId = queryStore.createTab(item.connectionId, item.database, `Source - ${item.objectName || item.tableName}`); queryStore.updateSql(tabId, result.source); if (item.type !== "sequence" && item.type !== "trigger" && item.type !== "type" && item.type !== "type-body") { @@ -1661,6 +1661,7 @@ async function handleQuickOpenSelect(item: any) { schema, name: item.objectName || item.tableName, objectType, + signature: item.signature, }); } queryStore.markTabClean(queryStore.tabs.find((tab) => tab.id === tabId)); diff --git a/apps/desktop/src/components/diff/SchemaDiffDialog.vue b/apps/desktop/src/components/diff/SchemaDiffDialog.vue index 045362e2d..94e7a273a 100644 --- a/apps/desktop/src/components/diff/SchemaDiffDialog.vue +++ b/apps/desktop/src/components/diff/SchemaDiffDialog.vue @@ -527,24 +527,24 @@ async function handleSelectObject(obj: SchemaDiffObject) { try { // For "create" objects: source has it, target doesn't → fetch source DDL if (obj.operationType === "create" && !obj.sourceDdl) { - const result = await api.getObjectSource(sourceConnectionId.value, sourceDatabase.value, sourceSchema.value, obj.name, objectType); + const result = await api.getObjectSource(sourceConnectionId.value, sourceDatabase.value, sourceSchema.value, obj.name, objectType, obj.arguments); if (result?.source) obj.sourceDdl = result.source; } // For "delete" objects: target has it, source doesn't → fetch target DDL if (obj.operationType === "delete" && !obj.targetDdl) { - const result = await api.getObjectSource(targetConnectionId.value, targetDatabase.value, targetSchema.value, obj.name, objectType); + const result = await api.getObjectSource(targetConnectionId.value, targetDatabase.value, targetSchema.value, obj.name, objectType, obj.arguments); if (result?.source) obj.targetDdl = result.source; } // For "modify" objects: fetch whichever side is missing if (obj.operationType === "modify") { if (!obj.sourceDdl) { - const result = await api.getObjectSource(sourceConnectionId.value, sourceDatabase.value, sourceSchema.value, obj.name, objectType); + const result = await api.getObjectSource(sourceConnectionId.value, sourceDatabase.value, sourceSchema.value, obj.name, objectType, obj.arguments); if (result?.source) obj.sourceDdl = result.source; } if (!obj.targetDdl) { - const result = await api.getObjectSource(targetConnectionId.value, targetDatabase.value, targetSchema.value, obj.name, objectType); + const result = await api.getObjectSource(targetConnectionId.value, targetDatabase.value, targetSchema.value, obj.name, objectType, obj.arguments); if (result?.source) obj.targetDdl = result.source; } } diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 6df2a7e69..9ecbaa813 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -1031,7 +1031,7 @@ async function openSource(row: ObjectBrowserRow) { const database = props.database; const schema = row.schema || selectedSchema.value || database; try { - const result = await api.getObjectSource(connectionId, database, schema, row.name, row.type as ObjectSourceKind); + const result = await api.getObjectSource(connectionId, database, schema, row.name, row.type as ObjectSourceKind, row.signature ?? undefined); if (sidePanelGuard.isStale(epoch)) return; sourceCanEdit.value = result.editable !== false && !["SEQUENCE", "TRIGGER", "TYPE", "TYPE_BODY"].includes(row.type); const editable = sourceCanEdit.value @@ -1154,7 +1154,7 @@ async function confirmRename() { try { const schema = row.schema || selectedSchema.value || props.database; if (supportsSourceBackedRoutineRename(effectiveDatabaseType.value, row.type as ObjectSourceKind)) { - const source = await api.getObjectSource(props.connection.id, props.database, schema, row.name, row.type as ObjectSourceKind); + const source = await api.getObjectSource(props.connection.id, props.database, schema, row.name, row.type as ObjectSourceKind, row.signature ?? undefined); const statements = await buildRoutineRenameObjectSourceStatements({ databaseType: effectiveDatabaseType.value, objectType: row.type as ObjectSourceKind, diff --git a/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts b/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts index 24f4db7c5..72d1346c0 100644 --- a/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/tableTree.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { appendTableTreeLoadMoreNode, buildSimpleObjectTreeNodes, buildTableTreeNodes, mergeTableTreePageChildren, tablePartitionGroups, withoutTableTreeLoadMoreNodes } from "@/lib/table/tableTree"; +import { appendTableTreeLoadMoreNode, buildGroupedObjectTreeNodes, buildSimpleObjectTreeNodes, buildTableTreeNodes, mergeTableTreePageChildren, tablePartitionGroups, withoutTableTreeLoadMoreNodes } from "@/lib/table/tableTree"; import type { ObjectInfo, TableInfo, TreeNode } from "@/types/database"; const context = { @@ -27,6 +27,24 @@ describe("PostgreSQL overloaded routines", () => { ); expect(new Set(nodes.map((node) => node.id)).size).toBe(3); }); + + it("keeps grouped routine nodes distinct by identity arguments", () => { + const objects: ObjectInfo[] = [ + { name: "calc", object_type: "FUNCTION", schema: "public", signature: "integer" }, + { name: "calc", object_type: "FUNCTION", schema: "public", signature: "integer, integer" }, + { name: "calc", object_type: "FUNCTION", schema: "public", signature: "numeric" }, + ]; + + const groups = buildGroupedObjectTreeNodes({ ...context, schema: "public", objects }); + const functionGroup = groups.find((node) => node.type === "group-functions"); + + expect(functionGroup?.children?.map((node) => ({ label: node.label, objectName: node.objectName, signature: node.signature }))).toEqual([ + { label: "calc(integer)", objectName: "calc", signature: "integer" }, + { label: "calc(integer, integer)", objectName: "calc", signature: "integer, integer" }, + { label: "calc(numeric)", objectName: "calc", signature: "numeric" }, + ]); + expect(new Set(functionGroup?.children?.map((node) => node.id) ?? []).size).toBe(3); + }); }); describe("programmable database objects", () => { diff --git a/apps/desktop/src/lib/table/tableTree.ts b/apps/desktop/src/lib/table/tableTree.ts index d45de7699..6ff3eba6c 100644 --- a/apps/desktop/src/lib/table/tableTree.ts +++ b/apps/desktop/src/lib/table/tableTree.ts @@ -668,7 +668,8 @@ export function buildGroupedObjectTreeNodes({ nodeId, connectionId, database, sc if (!name) continue; const t = normalizeObjectType(obj.object_type); const objectSchema = obj.schema ? normalizeDatabaseObjectName(obj.schema) : schema || ""; - const key = `${t}\0${objectSchema.toLowerCase()}\0${name.toLowerCase()}`; + const signature = (obj.signature ?? "").trim(); + const key = `${t}\0${objectSchema.toLowerCase()}\0${name.toLowerCase()}\0${signature.toLowerCase()}`; if (seen.has(key)) continue; seen.add(key); const arr = buckets.get(t) ?? []; @@ -695,10 +696,14 @@ export function buildGroupedObjectTreeNodes({ nodeId, connectionId, database, sc const objectType = normalizeObjectType(obj.object_type); const childType = typeof def.childType === "function" ? def.childType(objectType) : def.childType; const objectTypeSuffix = objectType === "PACKAGE" || objectType === "PACKAGE_BODY" || objectType === "TYPE" || objectType === "TYPE_BODY" ? `:${objectType}` : ""; + const signature = obj.signature?.trim() || ""; + const signatureIdPart = signature && (objectType === "FUNCTION" || objectType === "PROCEDURE") ? `:${signature}` : ""; return { - id: `${nodeId}:${def.key}:${childSchema ? `${childSchema}:` : ""}${obj.name}${objectTypeSuffix}`, - label: obj.name, + id: `${nodeId}:${def.key}:${childSchema ? `${childSchema}:` : ""}${obj.name}${signatureIdPart}${objectTypeSuffix}`, + label: signature && (objectType === "FUNCTION" || objectType === "PROCEDURE") ? `${obj.name}(${signature})` : obj.name, type: childType, + objectName: obj.name, + signature: signature || undefined, comment: obj.comment, valid: obj.valid ?? undefined, connectionId, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 7e1ddd2af..7ff217b4d 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -847,6 +847,7 @@ export interface QueryTab { schema?: string; name: string; objectType: ObjectSourceKind; + signature?: string; }; tableMeta?: { schema?: string;