347 lines
12 KiB
TypeScript
Vendored
347 lines
12 KiB
TypeScript
Vendored
import { test } from "vitest";
|
|
import assert from "node:assert/strict";
|
|
import { buildGroupedObjectTreeNodes, buildObjectGroupPlaceholderNodes, buildSimpleObjectTreeNodes, buildTableTreeNodes, filterSimpleSidebarSupplementalObjects, mergeTableInfosIntoObjects, mergeTableTreePageChildren } from "../../apps/desktop/src/lib/table/tableTree.ts";
|
|
import type { ObjectInfo, TableInfo, TreeNode } from "../../apps/desktop/src/types/database.ts";
|
|
|
|
function table(name: string, parent?: string): TableInfo {
|
|
return {
|
|
name,
|
|
table_type: "BASE TABLE",
|
|
comment: null,
|
|
parent_schema: parent ? "public" : null,
|
|
parent_name: parent ?? null,
|
|
};
|
|
}
|
|
|
|
function object(name: string, parent?: string): ObjectInfo {
|
|
return {
|
|
name,
|
|
object_type: "TABLE",
|
|
schema: "public",
|
|
comment: null,
|
|
created_at: null,
|
|
updated_at: null,
|
|
parent_schema: parent ? "public" : null,
|
|
parent_name: parent ?? null,
|
|
};
|
|
}
|
|
|
|
function partitionGroup(node: TreeNode): TreeNode | undefined {
|
|
return node.children?.find((child) => child.type === "group-partitions");
|
|
}
|
|
|
|
test("buildTableTreeNodes nests multi-level table partitions", () => {
|
|
const nodes = buildTableTreeNodes({
|
|
nodeId: "conn:app:public",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "public",
|
|
tables: [table("events"), table("events_2026", "events"), table("events_2026_05", "events_2026"), table("users")],
|
|
});
|
|
|
|
assert.deepEqual(
|
|
nodes.map((node) => node.label),
|
|
["events", "users"],
|
|
);
|
|
assert.equal(nodes[0].id, "conn:app:public:events");
|
|
|
|
const events = nodes[0];
|
|
const firstLevel = partitionGroup(events);
|
|
assert.equal(firstLevel?.label, "tree.partitions");
|
|
assert.deepEqual(
|
|
firstLevel?.children?.map((node) => node.label),
|
|
["events_2026"],
|
|
);
|
|
|
|
const secondLevel = partitionGroup(firstLevel!.children![0]);
|
|
assert.deepEqual(
|
|
secondLevel?.children?.map((node) => node.label),
|
|
["events_2026_05"],
|
|
);
|
|
});
|
|
|
|
test("buildTableTreeNodes keeps partitions visible when their parent is not loaded", () => {
|
|
const nodes = buildTableTreeNodes({
|
|
nodeId: "conn:app:public",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "public",
|
|
tables: [table("events_2026", "events")],
|
|
});
|
|
|
|
assert.deepEqual(
|
|
nodes.map((node) => node.label),
|
|
["events_2026"],
|
|
);
|
|
});
|
|
|
|
test("mergeTableTreePageChildren attaches later page partitions to loaded parents", () => {
|
|
const firstPage = buildTableTreeNodes({
|
|
nodeId: "conn:app:public",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "public",
|
|
tables: [table("events"), table("events_region_0", "events")],
|
|
});
|
|
const secondPage = buildTableTreeNodes({
|
|
nodeId: "conn:app:public",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "public",
|
|
tables: [table("events_region_0_2026_01", "events_region_0")],
|
|
});
|
|
|
|
const merged = mergeTableTreePageChildren(firstPage, secondPage, "conn", "app");
|
|
assert.deepEqual(
|
|
merged.map((node) => node.label),
|
|
["events"],
|
|
);
|
|
|
|
const regionPartition = partitionGroup(merged[0])?.children?.[0];
|
|
assert.equal(regionPartition?.label, "events_region_0");
|
|
assert.deepEqual(
|
|
partitionGroup(regionPartition!)?.children?.map((node) => node.label),
|
|
["events_region_0_2026_01"],
|
|
);
|
|
});
|
|
|
|
test("buildTableTreeNodes keeps sidebar tables in natural name order", () => {
|
|
const nodes = buildTableTreeNodes({
|
|
nodeId: "conn:app:public",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "public",
|
|
tables: [table("chat_staff"), table("chat_staff_his"), table("staff"), table("staff_his")],
|
|
});
|
|
|
|
assert.deepEqual(
|
|
nodes.map((node) => node.label),
|
|
["chat_staff", "chat_staff_his", "staff", "staff_his"],
|
|
);
|
|
});
|
|
|
|
test("buildTableTreeNodes does not relocate prefixed business tables by suffix", () => {
|
|
const nodes = buildTableTreeNodes({
|
|
nodeId: "conn:app",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
tables: [table("CurrentStock"), table("YonSuite_CurrentStock"), table("YonSuite_LocationStock")],
|
|
});
|
|
|
|
assert.deepEqual(
|
|
nodes.map((node) => node.label),
|
|
["CurrentStock", "YonSuite_CurrentStock", "YonSuite_LocationStock"],
|
|
);
|
|
});
|
|
|
|
test("buildGroupedObjectTreeNodes nests partitions inside the tables group", () => {
|
|
const groups = buildGroupedObjectTreeNodes({
|
|
nodeId: "conn:app:public",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "public",
|
|
objects: [object("events"), object("events_2026", "events"), object("events_2026_05", "events_2026")],
|
|
});
|
|
|
|
const tableGroup = groups.find((node) => node.type === "group-tables");
|
|
assert.equal(tableGroup?.objectCount, 3);
|
|
assert.deepEqual(
|
|
tableGroup?.children?.map((node) => node.label),
|
|
["events"],
|
|
);
|
|
assert.equal(tableGroup?.children?.[0]?.id, "conn:app:public:__tables:public:events");
|
|
assert.deepEqual(
|
|
partitionGroup(tableGroup!.children![0])?.children?.map((node) => node.label),
|
|
["events_2026"],
|
|
);
|
|
});
|
|
|
|
test("buildGroupedObjectTreeNodes applies natural name sorting inside object groups", () => {
|
|
const groups = buildGroupedObjectTreeNodes({
|
|
nodeId: "conn:app:public",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "public",
|
|
objects: [object("chat_staff"), object("chat_staff_his"), object("staff"), object("staff_his")],
|
|
});
|
|
|
|
const tableGroup = groups.find((node) => node.type === "group-tables");
|
|
assert.deepEqual(
|
|
tableGroup?.children?.map((node) => node.label),
|
|
["chat_staff", "chat_staff_his", "staff", "staff_his"],
|
|
);
|
|
});
|
|
|
|
test("buildGroupedObjectTreeNodes groups Oracle packages and package bodies", () => {
|
|
const groups = buildGroupedObjectTreeNodes({
|
|
nodeId: "conn:app:HR",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "HR",
|
|
objects: [
|
|
{ name: "PAYROLL", object_type: "PACKAGE", schema: "HR" },
|
|
{ name: "PAYROLL", object_type: "PACKAGE_BODY", schema: "HR" },
|
|
],
|
|
});
|
|
|
|
const packageGroup = groups.find((node) => node.type === "group-packages");
|
|
assert.equal(packageGroup?.label, "tree.packages");
|
|
assert.deepEqual(
|
|
packageGroup?.children?.map((node) => ({ label: node.label, type: node.type, id: node.id })),
|
|
[
|
|
{ label: "PAYROLL", type: "package", id: "conn:app:HR:__packages:HR:PAYROLL:PACKAGE" },
|
|
{ label: "PAYROLL", type: "package-body", id: "conn:app:HR:__packages:HR:PAYROLL:PACKAGE_BODY" },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("buildGroupedObjectTreeNodes groups materialized views separately from views", () => {
|
|
const groups = buildGroupedObjectTreeNodes({
|
|
nodeId: "conn:app:APP",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "APP",
|
|
objects: [
|
|
{ name: "ACTIVE_USERS", object_type: "VIEW", schema: "APP" },
|
|
{ name: "USER_SUMMARY_MV", object_type: "MATERIALIZED_VIEW", schema: "APP" },
|
|
],
|
|
});
|
|
|
|
const viewGroup = groups.find((node) => node.type === "group-views");
|
|
const materializedViewGroup = groups.find((node) => node.type === "group-materialized-views");
|
|
assert.deepEqual(
|
|
viewGroup?.children?.map((node) => ({ label: node.label, type: node.type })),
|
|
[{ label: "ACTIVE_USERS", type: "view" }],
|
|
);
|
|
assert.deepEqual(
|
|
materializedViewGroup?.children?.map((node) => ({ label: node.label, type: node.type })),
|
|
[{ label: "USER_SUMMARY_MV", type: "materialized_view" }],
|
|
);
|
|
});
|
|
|
|
test("buildObjectGroupPlaceholderNodes creates capability-driven lazy sidebar groups", () => {
|
|
const groups = buildObjectGroupPlaceholderNodes({
|
|
nodeId: "conn:app:HR",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "HR",
|
|
objectTypes: ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"],
|
|
});
|
|
|
|
assert.deepEqual(
|
|
groups.map((node) => ({ label: node.label, type: node.type, count: node.objectCount, children: node.children })),
|
|
[
|
|
{ label: "tree.tables", type: "group-tables", count: undefined, children: [] },
|
|
{ label: "tree.views", type: "group-views", count: undefined, children: [] },
|
|
{ label: "tree.procedures", type: "group-procedures", count: undefined, children: [] },
|
|
{ label: "tree.functions", type: "group-functions", count: undefined, children: [] },
|
|
{ label: "tree.sequences", type: "group-sequences", count: undefined, children: [] },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("buildSimpleObjectTreeNodes keeps routines, sequences, and packages visible in flat sidebar mode", () => {
|
|
const nodes = buildSimpleObjectTreeNodes({
|
|
nodeId: "conn:app:HR",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "HR",
|
|
objects: [
|
|
{ name: "ORDERS", object_type: "TABLE", schema: "HR" },
|
|
{ name: "ACTIVE_ORDERS", object_type: "VIEW", schema: "HR" },
|
|
{ name: "REFRESH_STATS", object_type: "PROCEDURE", schema: "HR" },
|
|
{ name: "TOTAL_DUE", object_type: "FUNCTION", schema: "HR" },
|
|
{ name: "ORDER_ID_SEQ", object_type: "SEQUENCE", schema: "HR" },
|
|
{ name: "PAYROLL", object_type: "PACKAGE", schema: "HR" },
|
|
{ name: "PAYROLL", object_type: "PACKAGE_BODY", schema: "HR" },
|
|
],
|
|
});
|
|
|
|
assert.deepEqual(
|
|
nodes.map((node) => ({ label: node.label, type: node.type })),
|
|
[
|
|
{ label: "ORDERS", type: "table" },
|
|
{ label: "ACTIVE_ORDERS", type: "view" },
|
|
{ label: "ORDER_ID_SEQ", type: "sequence" },
|
|
{ label: "PAYROLL", type: "package" },
|
|
{ label: "PAYROLL", type: "package-body" },
|
|
{ label: "REFRESH_STATS", type: "procedure" },
|
|
{ label: "TOTAL_DUE", type: "function" },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("mergeTableInfosIntoObjects restores views missing from object metadata", () => {
|
|
const merged = mergeTableInfosIntoObjects(
|
|
[object("orders")],
|
|
[
|
|
table("orders"),
|
|
{
|
|
name: "active_orders",
|
|
table_type: "VIEW",
|
|
comment: "current orders",
|
|
parent_schema: null,
|
|
parent_name: null,
|
|
},
|
|
],
|
|
"public",
|
|
);
|
|
|
|
assert.deepEqual(
|
|
merged.map((item) => ({ name: item.name, type: item.object_type, schema: item.schema, comment: item.comment })),
|
|
[
|
|
{ name: "orders", type: "TABLE", schema: "public", comment: null },
|
|
{ name: "active_orders", type: "VIEW", schema: "public", comment: "current orders" },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("filterSimpleSidebarSupplementalObjects leaves paged tables and views to listTables", () => {
|
|
const supplemental = filterSimpleSidebarSupplementalObjects([
|
|
{ name: "orders", object_type: "TABLE", schema: "public" },
|
|
{ name: "active_orders", object_type: "VIEW", schema: "public" },
|
|
{ name: "order_summary", object_type: "MATERIALIZED VIEW", schema: "public" },
|
|
{ name: "refresh_stats", object_type: "PROCEDURE", schema: "public" },
|
|
{ name: "total_due", object_type: "FUNCTION", schema: "public" },
|
|
]);
|
|
const nodes = buildSimpleObjectTreeNodes({
|
|
nodeId: "conn:app:public",
|
|
connectionId: "conn",
|
|
database: "app",
|
|
schema: "public",
|
|
objects: mergeTableInfosIntoObjects(supplemental, [table("orders")], "public"),
|
|
});
|
|
|
|
assert.deepEqual(
|
|
nodes.map((node) => ({ label: node.label, type: node.type })),
|
|
[
|
|
{ label: "orders", type: "table" },
|
|
{ label: "refresh_stats", type: "procedure" },
|
|
{ label: "total_due", type: "function" },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("mergeTableInfosIntoObjects dedupes MySQL tables when object metadata carries database as schema", () => {
|
|
const merged = mergeTableInfosIntoObjects(
|
|
[
|
|
{
|
|
name: "orders",
|
|
object_type: "TABLE",
|
|
schema: "app",
|
|
comment: null,
|
|
created_at: null,
|
|
updated_at: null,
|
|
parent_schema: null,
|
|
parent_name: null,
|
|
},
|
|
],
|
|
[table("orders")],
|
|
);
|
|
|
|
assert.deepEqual(
|
|
merged.map((item) => ({ name: item.name, type: item.object_type, schema: item.schema })),
|
|
[{ name: "orders", type: "TABLE", schema: "app" }],
|
|
);
|
|
});
|