fix(app): dedupe tree object counts

This commit is contained in:
t8y2 2026-05-18 23:58:38 +08:00
parent 3ebd31f792
commit 735d5d86ba
4 changed files with 51 additions and 6 deletions

View File

@ -1,7 +1,7 @@
export const SCHEMA_TREE_CACHE_TTL_MS = 15 * 60 * 1000;
export interface SchemaTreeCacheEnvelope<T> {
version: 1;
version: 2;
cachedAt: string;
children: T;
}
@ -13,7 +13,7 @@ export interface DecodedSchemaTreeCache<T> {
export function encodeSchemaTreeCache<T>(children: T, nowMs = Date.now()): SchemaTreeCacheEnvelope<T> {
return {
version: 1,
version: 2,
cachedAt: new Date(nowMs).toISOString(),
children,
};
@ -31,7 +31,7 @@ export function decodeSchemaTreeCache<T>(
if (!payload || typeof payload !== "object") return null;
const envelope = payload as Partial<SchemaTreeCacheEnvelope<unknown>>;
if (envelope.version !== 1 || !Array.isArray(envelope.children) || typeof envelope.cachedAt !== "string") {
if (envelope.version !== 2 || !Array.isArray(envelope.children) || typeof envelope.cachedAt !== "string") {
return null;
}

View File

@ -96,10 +96,15 @@ export function buildGroupedObjectTreeNodes({
objects: ObjectInfo[];
}): TreeNode[] {
const buckets = new Map<string, ObjectInfo[]>();
const seen = new Set<string>();
for (const obj of objects) {
const name = normalizeDatabaseObjectName(obj.name);
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()}`;
if (seen.has(key)) continue;
seen.add(key);
const arr = buckets.get(t) ?? [];
arr.push({ ...obj, name, schema: obj.schema ? normalizeDatabaseObjectName(obj.schema) : obj.schema });
buckets.set(t, arr);

View File

@ -11,7 +11,7 @@ const now = Date.parse("2026-05-17T10:00:00.000Z");
test("wraps tree children with a cache timestamp", () => {
assert.deepEqual(encodeSchemaTreeCache(children, now), {
version: 1,
version: 2,
cachedAt: "2026-05-17T10:00:00.000Z",
children,
});
@ -43,5 +43,6 @@ test("keeps legacy array cache readable but stale", () => {
});
test("rejects invalid schema tree cache payloads", () => {
assert.equal(decodeSchemaTreeCache({ version: 1, cachedAt: "bad", children: "nope" }, now), null);
assert.equal(decodeSchemaTreeCache({ version: 2, cachedAt: "bad", children: "nope" }, now), null);
assert.equal(decodeSchemaTreeCache({ version: 1, cachedAt: "2026-05-17T10:00:00.000Z", children }, now), null);
});

View File

@ -2,11 +2,12 @@ import assert from "node:assert/strict";
import test from "node:test";
import { readFileSync } from "node:fs";
import {
buildGroupedObjectTreeNodes,
buildTableTreeNodes,
expandCachedObjectBrowserNodes,
objectGroupRefreshParentId,
} from "../../apps/desktop/src/lib/tableTree.ts";
import type { TableInfo } from "../../apps/desktop/src/types/database.ts";
import type { ObjectInfo, TableInfo } from "../../apps/desktop/src/types/database.ts";
const treeItemSource = readFileSync("apps/desktop/src/components/sidebar/TreeItem.vue", "utf8");
const connectionStoreSource = readFileSync("apps/desktop/src/stores/connectionStore.ts", "utf8");
@ -15,6 +16,14 @@ function table(name: string, tableType: "TABLE" | "VIEW" = "TABLE"): TableInfo {
return { name, table_type: tableType };
}
function obj(name: string, objectType = "TABLE", schema = "public"): ObjectInfo {
return {
name,
object_type: objectType,
schema,
};
}
test("keeps every table as a sidebar node instead of truncating to object browser", () => {
const tables: TableInfo[] = Array.from({ length: 16 }, (_, index) => table(`table_${index + 1}`));
@ -69,6 +78,36 @@ test("normalizes padded table names from database drivers", () => {
);
});
test("object tree groups count unique objects when metadata returns duplicates", () => {
const nodes = buildGroupedObjectTreeNodes({
nodeId: "conn:app:public",
connectionId: "conn",
database: "app",
schema: "public",
objects: [
obj("orders"),
obj("orders"),
obj("customers"),
obj("active_orders", "VIEW"),
obj("active_orders", "VIEW"),
],
});
const tableGroup = nodes.find((node) => node.type === "group-tables");
assert.equal(tableGroup?.objectCount, 2);
assert.deepEqual(
tableGroup?.children?.map((child) => child.label),
["orders", "customers"],
);
const viewGroup = nodes.find((node) => node.type === "group-views");
assert.equal(viewGroup?.objectCount, 1);
assert.deepEqual(
viewGroup?.children?.map((child) => child.label),
["active_orders"],
);
});
test("expands cached object-browser nodes back into regular table nodes", () => {
const nodes = expandCachedObjectBrowserNodes([
{