fix(app): dedupe tree object counts
This commit is contained in:
parent
3ebd31f792
commit
735d5d86ba
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue