import { describe, expect, it } from "vitest"; import { appendTableTreeLoadMoreNode, buildGroupedObjectTreeNodes, buildObjectGroupPlaceholderNodes, buildSimpleObjectTreeNodes, buildTableTreeNodes, mergeTableInfosIntoObjects, mergeTableTreePageChildren, tablePartitionGroups, withoutTableTreeLoadMoreNodes } from "@/lib/table/tableTree"; import type { ObjectInfo, TableInfo, TreeNode } from "@/types/database"; const context = { nodeId: "connection:db", connectionId: "connection", database: "db", }; describe("case-sensitive database objects", () => { const views: ObjectInfo[] = [ { name: "dbx_issue4529_case_V1", object_type: "VIEW", schema: "dbx_test" }, { name: "dbx_issue4529_case_v1", object_type: "VIEW", schema: "dbx_test" }, ]; it("keeps table metadata entries whose names differ only by case", () => { const tables: TableInfo[] = views.map((view) => ({ name: view.name, table_type: "VIEW", comment: null })); const merged = mergeTableInfosIntoObjects([], tables, "dbx_test"); expect(merged.map((object) => object.name)).toEqual(["dbx_issue4529_case_V1", "dbx_issue4529_case_v1"]); }); it("keeps simple tree nodes whose names differ only by case", () => { const nodes = buildSimpleObjectTreeNodes({ ...context, schema: "dbx_test", objects: views }); expect(nodes.map((node) => node.label)).toEqual(["dbx_issue4529_case_V1", "dbx_issue4529_case_v1"]); expect(new Set(nodes.map((node) => node.id)).size).toBe(2); }); it("keeps grouped tree nodes whose names differ only by case", () => { const groups = buildGroupedObjectTreeNodes({ ...context, schema: "dbx_test", objects: views }); const viewGroup = groups.find((node) => node.type === "group-views"); expect(viewGroup?.objectCount).toBe(2); expect(viewGroup?.children?.map((node) => node.label)).toEqual(["dbx_issue4529_case_V1", "dbx_issue4529_case_v1"]); expect(new Set(viewGroup?.children?.map((node) => node.id) ?? []).size).toBe(2); }); it("preserves view validity in simple and grouped trees", () => { const objects: ObjectInfo[] = [ { name: "VALID_VIEW", object_type: "VIEW", schema: "dbx_test", valid: true }, { name: "INVALID_VIEW", object_type: "VIEW", schema: "dbx_test", valid: false }, ]; const simple = buildSimpleObjectTreeNodes({ ...context, schema: "dbx_test", objects }); expect(simple.map((node) => node.valid)).toEqual([false, true]); const grouped = buildGroupedObjectTreeNodes({ ...context, schema: "dbx_test", objects }); expect(grouped.find((node) => node.type === "group-views")?.children?.map((node) => node.valid)).toEqual([false, true]); }); it("preserves view validity when building the table-like tree path", () => { const tables: TableInfo[] = [ { name: "VALID_VIEW", table_type: "VIEW", valid: true }, { name: "INVALID_VIEW", table_type: "VIEW", valid: false }, ]; const nodes = buildTableTreeNodes({ ...context, schema: "dbx_test", tables, }); expect(nodes.map((node) => node.valid)).toEqual([false, true]); const grouped = buildGroupedObjectTreeNodes({ ...context, schema: "dbx_test", objects: mergeTableInfosIntoObjects([], tables, "dbx_test"), }); expect(grouped.find((node) => node.type === "group-views")?.children?.map((node) => node.valid)).toEqual([false, true]); }); it("keeps table nodes whose names differ only by case across pages", () => { const firstPage = buildTableTreeNodes({ ...context, schema: "dbx_test", tables: [{ name: "dbx_issue4529_case_T1", table_type: "BASE TABLE", comment: null }], }); const secondPage = buildTableTreeNodes({ ...context, schema: "dbx_test", tables: [{ name: "dbx_issue4529_case_t1", table_type: "BASE TABLE", comment: null }], }); const merged = mergeTableTreePageChildren(firstPage, secondPage, context.connectionId, context.database); expect(merged.map((node) => node.label)).toEqual(["dbx_issue4529_case_T1", "dbx_issue4529_case_t1"]); }); }); describe("PostgreSQL overloaded routines", () => { it("keeps routines with the same name 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 nodes = buildSimpleObjectTreeNodes({ ...context, schema: "public", objects }); expect(nodes.map((node) => ({ label: node.label, objectName: node.objectName, signature: node.signature }))).toEqual( expect.arrayContaining([ { 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(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("PostgreSQL custom type metadata", () => { it("preserves type kind and member capability in simple and grouped trees", () => { const objects: ObjectInfo[] = [ { name: "address", object_type: "TYPE", schema: "public", custom_type_kind: "composite", has_members: true }, { name: "email", object_type: "TYPE", schema: "public", custom_type_kind: "domain", has_members: false }, ]; const simple = buildSimpleObjectTreeNodes({ ...context, schema: "public", objects }); expect(simple.map((node) => ({ name: node.label, kind: node.customTypeKind, hasMembers: node.hasMembers }))).toEqual([ { name: "address", kind: "composite", hasMembers: true }, { name: "email", kind: "domain", hasMembers: false }, ]); const grouped = buildGroupedObjectTreeNodes({ ...context, schema: "public", objects }); expect(grouped.find((node) => node.type === "group-types")?.children?.map((node) => ({ name: node.label, kind: node.customTypeKind, hasMembers: node.hasMembers }))).toEqual([ { name: "address", kind: "composite", hasMembers: true }, { name: "email", kind: "domain", hasMembers: false }, ]); }); }); describe("programmable database objects", () => { it("renders only the synonym group for the Xugu public-synonym scope", () => { const groups = buildObjectGroupPlaceholderNodes({ ...context, schema: "\u0000DBX_XUGU_PUBLIC_SYNONYMS", objectTypes: ["SYNONYM"], }); expect(groups).toEqual([expect.objectContaining({ type: "group-synonyms", label: "tree.synonyms" })]); }); it("coalesces Xugu package specification and body into one top-level node", () => { const objects: ObjectInfo[] = [ { name: "DBX_UI_PKG", object_type: "PACKAGE", schema: "APP", valid: true }, { name: "DBX_UI_PKG", object_type: "PACKAGE_BODY", schema: "APP", valid: false }, ]; const nodes = buildSimpleObjectTreeNodes({ ...context, schema: "APP", objects, databaseType: "xugu" }); expect(nodes).toHaveLength(1); expect(nodes[0]).toEqual( expect.objectContaining({ type: "package", objectName: "DBX_UI_PKG", valid: false, xuguPackageBodyAvailable: true, xuguPackageBodyValid: false, }), ); }); it("keeps the package body as metadata for the expandable Xugu package node", () => { const objects: ObjectInfo[] = [ { name: "DBX_UI_PKG", object_type: "PACKAGE", schema: "APP", valid: true }, { name: "DBX_UI_PKG", object_type: "PACKAGE_BODY", schema: "APP", valid: true }, ]; const groups = buildGroupedObjectTreeNodes({ ...context, schema: "APP", objects, databaseType: "xugu" }); const packageGroup = groups.find((node) => node.type === "group-packages"); expect(packageGroup?.objectCount).toBe(1); expect(packageGroup?.children).toEqual([expect.objectContaining({ type: "package", objectName: "DBX_UI_PKG" })]); }); it("keeps a body-only Xugu metadata response visible as a package", () => { const nodes = buildSimpleObjectTreeNodes({ ...context, schema: "APP", objects: [{ name: "BODY_ONLY_PKG", object_type: "PACKAGE_BODY", schema: "APP", valid: true }], databaseType: "xugu", }); expect(nodes).toEqual([ expect.objectContaining({ type: "package", objectName: "BODY_ONLY_PKG", xuguPackageBodyAvailable: true, xuguPackageBodyValid: true, }), ]); }); it("does not advertise a body child for a specification-only Xugu package", () => { const nodes = buildSimpleObjectTreeNodes({ ...context, schema: "APP", objects: [{ name: "SPEC_ONLY_PKG", object_type: "PACKAGE", schema: "APP", valid: true }], databaseType: "xugu", }); expect(nodes[0]).toEqual(expect.objectContaining({ type: "package", xuguPackageBodyAvailable: undefined })); }); it("does not merge package and package body for other databases", () => { const objects: ObjectInfo[] = [ { name: "DBX_UI_PKG", object_type: "PACKAGE", schema: "APP" }, { name: "DBX_UI_PKG", object_type: "PACKAGE_BODY", schema: "APP" }, ]; const nodes = buildSimpleObjectTreeNodes({ ...context, schema: "APP", objects, databaseType: "oracle" }); expect(nodes.map((node) => node.type)).toEqual(["package", "package-body"]); }); it("keeps Xugu trigger/type nodes distinct and preserves an invalid status", () => { const objects: ObjectInfo[] = [ { name: "TRG_AUDIT", object_type: "TRIGGER", schema: "APP", valid: false }, { name: "ADDRESS_T", object_type: "TYPE", schema: "APP", valid: true, xugu_type_members_expandable: true }, { name: "ADDRESS_LIST", object_type: "TYPE", schema: "APP", valid: true, xugu_type_members_expandable: false }, { name: "ADDRESS_T", object_type: "TYPE_BODY", schema: "APP", valid: true }, ]; const nodes = buildSimpleObjectTreeNodes({ ...context, schema: "APP", objects }); expect(nodes).toEqual( expect.arrayContaining([ expect.objectContaining({ type: "trigger", objectName: "TRG_AUDIT", valid: false }), expect.objectContaining({ type: "type", objectName: "ADDRESS_T", valid: true, xuguTypeMembersExpandable: true }), expect.objectContaining({ type: "type", objectName: "ADDRESS_LIST", valid: true, xuguTypeMembersExpandable: false }), expect.objectContaining({ type: "type-body", objectName: "ADDRESS_T", valid: true }), ]), ); }); it("preserves Xugu trigger details for the schema trigger group", () => { const trigger = { name: "TRG_AUDIT", event: "INSERT OR UPDATE", timing: "BEFORE", level: "FOR EACH ROW", enabled: false, valid: false, condition: "NEW_VALUE >= 0", language: "PL/SQL", comment: "row audit trigger", created_at: "2026-08-10 09:30:00", }; const groups = buildGroupedObjectTreeNodes({ ...context, schema: "APP", objects: [{ name: trigger.name, object_type: "TRIGGER", schema: "APP", valid: false, comment: trigger.comment, trigger }], }); const triggerGroup = groups.find((node) => node.type === "group-triggers"); expect(triggerGroup?.children).toEqual([ expect.objectContaining({ type: "trigger", objectName: trigger.name, valid: false, comment: trigger.comment, meta: trigger, }), ]); }); it("groups MySQL scheduled events under a dedicated Events group", () => { const objects: ObjectInfo[] = [{ name: "event_daily_middle_db_sync", object_type: "EVENT", schema: "shop" }]; const groups = buildGroupedObjectTreeNodes({ ...context, schema: "shop", objects, databaseType: "mysql" }); const eventGroup = groups.find((node) => node.type === "group-events"); expect(eventGroup).toEqual(expect.objectContaining({ objectCount: 1, label: "tree.events" })); expect(eventGroup?.children).toEqual([expect.objectContaining({ type: "event", objectName: "event_daily_middle_db_sync" })]); }); it("keeps table-scoped Kingbase triggers distinct and source-addressable", () => { const objects: ObjectInfo[] = [ { name: "audit_before", object_type: "TRIGGER", schema: "public", parent_schema: "public", parent_name: "items" }, { name: "audit_before", object_type: "TRIGGER", schema: "public", parent_schema: "public", parent_name: "orders" }, ]; const simple = buildSimpleObjectTreeNodes({ ...context, schema: "public", objects, databaseType: "kingbase" }); expect(simple.map((node) => ({ label: node.label, tableName: node.tableName }))).toEqual([ { label: "audit_before (items)", tableName: "items" }, { label: "audit_before (orders)", tableName: "orders" }, ]); expect(new Set(simple.map((node) => node.id)).size).toBe(2); const grouped = buildGroupedObjectTreeNodes({ ...context, schema: "public", objects, databaseType: "kingbase" }); const triggers = grouped.find((node) => node.type === "group-triggers")?.children ?? []; expect(triggers.map((node) => ({ label: node.label, objectName: node.objectName, tableName: node.tableName }))).toEqual([ { label: "audit_before (items)", objectName: "audit_before", tableName: "items" }, { label: "audit_before (orders)", objectName: "audit_before", tableName: "orders" }, ]); expect(new Set(triggers.map((node) => node.id)).size).toBe(2); }); it("groups Xugu private synonyms as source objects", () => { const objects: ObjectInfo[] = [{ name: "SYN_SHOP_USERS", object_type: "SYNONYM", schema: "SYSDBA", valid: true }]; const groups = buildGroupedObjectTreeNodes({ ...context, schema: "SYSDBA", objects }); const synonymGroup = groups.find((node) => node.type === "group-synonyms"); expect(synonymGroup).toEqual(expect.objectContaining({ objectCount: 1, label: "tree.synonyms" })); expect(synonymGroup?.children).toEqual([expect.objectContaining({ type: "synonym", objectName: "SYN_SHOP_USERS", valid: true })]); }); it("groups user-defined types with an object count", () => { const objects: ObjectInfo[] = [ { name: "status", object_type: "TYPE", schema: "app", comment: "order status" }, { name: "email", object_type: "TYPE", schema: "app" }, ]; const groups = buildGroupedObjectTreeNodes({ ...context, schema: "app", objects }); const typeGroup = groups.find((node) => node.type === "group-types"); expect(typeGroup).toEqual(expect.objectContaining({ objectCount: 2, label: "tree.types" })); expect(typeGroup?.children?.map((node) => node.type)).toEqual(["type", "type"]); expect(typeGroup?.children?.map((node) => node.objectName)).toEqual(["email", "status"]); expect(typeGroup?.children?.[0]?.comment).toBeUndefined(); expect(typeGroup?.children?.[1]?.comment).toBe("order status"); }); }); describe("PostgreSQL table hierarchy", () => { it("matches partition parents case-insensitively", () => { const nodes = buildTableTreeNodes({ ...context, schema: "public", tables: [ { name: "Orders", table_type: "PARTITIONED TABLE", comment: null }, { name: "orders_2026", table_type: "TABLE", comment: null, parent_schema: "PUBLIC", parent_name: "orders" }, ], }); expect(nodes.map((node) => node.label)).toEqual(["Orders"]); expect(tablePartitionGroups(nodes[0])[0].children?.map((node) => node.label)).toEqual(["orders_2026"]); }); it("prefers the exact partition parent when folded names are ambiguous", () => { const nodes = buildTableTreeNodes({ ...context, schema: "public", tables: [ { name: "Orders", table_type: "PARTITIONED TABLE", comment: null }, { name: "orders", table_type: "PARTITIONED TABLE", comment: null }, { name: "Orders_2026", table_type: "TABLE", comment: null, parent_schema: "public", parent_name: "Orders" }, ], }); const upperParent = nodes.find((node) => node.label === "Orders"); const lowerParent = nodes.find((node) => node.label === "orders"); expect(tablePartitionGroups(upperParent!)[0].children?.map((node) => node.label)).toEqual(["Orders_2026"]); expect(tablePartitionGroups(lowerParent!)).toHaveLength(0); }); it("prefers the exact partition parent when merging later pages", () => { const firstPage = buildTableTreeNodes({ ...context, schema: "public", tables: [ { name: "Orders", table_type: "PARTITIONED TABLE", comment: null }, { name: "orders", table_type: "PARTITIONED TABLE", comment: null }, ], }); const secondPage = buildTableTreeNodes({ ...context, schema: "public", tables: [{ name: "Orders_2026", table_type: "TABLE", comment: null, parent_schema: "public", parent_name: "Orders" }], }); const merged = mergeTableTreePageChildren(firstPage, secondPage, context.connectionId, context.database); const upperParent = merged.find((node) => node.label === "Orders"); const lowerParent = merged.find((node) => node.label === "orders"); expect(tablePartitionGroups(upperParent!)[0].children?.map((node) => node.label)).toEqual(["Orders_2026"]); expect(tablePartitionGroups(lowerParent!)).toHaveLength(0); }); it("keeps schema pagination visible at the table-group root when a page ends inside nested partitions", () => { const nodes = buildTableTreeNodes({ ...context, schema: "public", tables: [ { name: "orders", table_type: "BASE TABLE", comment: null }, { name: "orders_2026", table_type: "BASE TABLE", comment: null, parent_schema: "public", parent_name: "orders" }, { name: "orders_2026_01", table_type: "BASE TABLE", comment: null, parent_schema: "public", parent_name: "orders_2026" }, ], }); const loadMore: TreeNode = { id: "load-more:1000", label: "tree.loadMore", type: "load-more", connectionId: context.connectionId, database: context.database, loadMore: { parentId: "connection:db:__tables", offset: 1000, pageSize: 1000 }, }; const withLoadMore = appendTableTreeLoadMoreNode(nodes, loadMore, { schema: "public", name: "orders_2026" }); expect(withLoadMore.map((node) => node.label)).toEqual(["orders", "tree.loadMore"]); const yearPartition = tablePartitionGroups(withLoadMore[0])[0].children?.[0]; expect(yearPartition?.label).toBe("orders_2026"); expect(tablePartitionGroups(yearPartition!)[0].children?.map((node) => node.label)).toEqual(["orders_2026_01"]); }); }); describe("TDengine table hierarchy", () => { it("groups child tables under their supertable and keeps ordinary tables flat", () => { const tables: TableInfo[] = [ { name: "meters", table_type: "STABLE", comment: null }, { name: "device_b", table_type: "TABLE", comment: null, parent_name: "meters" }, { name: "standalone", table_type: "TABLE", comment: null }, { name: "device_a", table_type: "TABLE", comment: null, parent_name: "meters" }, ]; const nodes = buildTableTreeNodes({ ...context, tables }); expect(nodes.map((node) => node.label)).toEqual(["meters", "standalone"]); expect(nodes[0].children).toHaveLength(1); expect(nodes[0].children?.[0]).toMatchObject({ type: "group-partitions", label: "tree.childTables", objectCount: 2, isExpanded: true, }); expect(nodes[0].children?.[0].children?.map((node) => node.label)).toEqual(["device_a", "device_b"]); }); it("attaches child tables loaded on later pages to the existing supertable", () => { const firstPage = buildTableTreeNodes({ ...context, tables: [{ name: "meters", table_type: "STABLE", comment: null }], }); const secondPage = buildTableTreeNodes({ ...context, tables: [{ name: "device_a", table_type: "TABLE", comment: null, parent_name: "meters" }], }); const merged = mergeTableTreePageChildren(firstPage, secondPage, context.connectionId, context.database); expect(merged).toHaveLength(1); expect(merged[0].children?.[0]).toMatchObject({ type: "group-partitions", label: "tree.childTables", objectCount: 1, isExpanded: true, }); expect(merged[0].children?.[0].children?.[0].label).toBe("device_a"); }); it("keeps pagination inside the child table group across pages", () => { const firstPage = buildTableTreeNodes({ ...context, tables: [ { name: "meters", table_type: "STABLE", comment: null }, { name: "device_a", table_type: "TABLE", comment: null, parent_name: "meters" }, ], }); const firstLoadMore: TreeNode = { id: "load-more:1", label: "tree.loadMore", type: "load-more", connectionId: context.connectionId, database: context.database, loadMore: { parentId: "connection:db:__tables", offset: 2, pageSize: 2 }, }; appendTableTreeLoadMoreNode(firstPage, firstLoadMore, { name: "meters" }); expect(firstPage.map((node) => node.type)).toEqual(["table"]); expect(tablePartitionGroups(firstPage[0])[0].children?.map((node) => node.type)).toEqual(["table", "load-more"]); const secondPage = buildTableTreeNodes({ ...context, tables: [{ name: "device_b", table_type: "TABLE", comment: null, parent_name: "meters" }], }); const withoutLoadMore = withoutTableTreeLoadMoreNodes(firstPage); const merged = mergeTableTreePageChildren(withoutLoadMore, secondPage, context.connectionId, context.database); const secondLoadMore = { ...firstLoadMore, id: "load-more:2", loadMore: { ...firstLoadMore.loadMore!, offset: 4 } }; appendTableTreeLoadMoreNode(merged, secondLoadMore, { name: "meters" }); expect(merged).toHaveLength(1); expect(tablePartitionGroups(merged[0])[0]).toMatchObject({ objectCount: 2 }); expect(tablePartitionGroups(merged[0])[0].children?.map((node) => node.label)).toEqual(["device_a", "device_b", "tree.loadMore"]); }); it("keeps the partition label for non-TDengine parent tables", () => { const nodes = buildTableTreeNodes({ ...context, schema: "public", tables: [ { name: "orders", table_type: "PARTITIONED TABLE", comment: null }, { name: "orders_2026", table_type: "TABLE", comment: null, parent_schema: "public", parent_name: "orders" }, ], }); expect(nodes[0].children?.[0]).toMatchObject({ label: "tree.partitions", isExpanded: false }); }); it("nests second-level partitions under Kingbase-style lowercase parent names", () => { // KingbaseES returns lowercase relation names and a direct `parent_name`; // a partition that is itself a partitioned parent must nest recursively. const nodes = buildTableTreeNodes({ ...context, schema: "partition_demo", tables: [ { name: "catalog_nested", table_type: "BASE TABLE", comment: null }, { name: "catalog_nested_2024", table_type: "BASE TABLE", comment: null, parent_schema: "partition_demo", parent_name: "catalog_nested" }, { name: "catalog_nested_2024_asia", table_type: "BASE TABLE", comment: null, parent_schema: "partition_demo", parent_name: "catalog_nested_2024" }, { name: "catalog_nested_2024_eu", table_type: "BASE TABLE", comment: null, parent_schema: "partition_demo", parent_name: "catalog_nested_2024" }, { name: "catalog_nested_2025", table_type: "BASE TABLE", comment: null, parent_schema: "partition_demo", parent_name: "catalog_nested" }, ], }); expect(nodes.map((node) => node.label)).toEqual(["catalog_nested"]); const level1 = tablePartitionGroups(nodes[0])[0].children ?? []; expect(level1.map((node) => node.label)).toEqual(["catalog_nested_2024", "catalog_nested_2025"]); const yearPartition = level1.find((node) => node.label === "catalog_nested_2024"); expect(tablePartitionGroups(yearPartition!)[0].children?.map((node) => node.label)).toEqual(["catalog_nested_2024_asia", "catalog_nested_2024_eu"]); }); });