Files
MyGoNavi/frontend/src/components/sidebar/sidebarPartitions.ts
Syngnat 0674216d53 🐛 fix(sidebar): 将 PostgreSQL 分区表归入主表
- 读取 PostgreSQL 分区父子关系并隐藏分区主表行数
- 在主表下增加 Partitions 分组并保留分区表操作能力
- 兼容多级分区、跨 schema 显隐及异常元数据
- 补充分区树接入测试与多语言文案

Refs #750
2026-07-28 13:58:56 +08:00

106 lines
3.7 KiB
TypeScript

export interface SidebarPartitionTableEntry {
tableName: string;
schemaName?: string;
displayName: string;
partitionParentTableName?: string;
rowCount?: number;
}
export type GroupedSidebarPartitionTableEntry<T extends SidebarPartitionTableEntry> = T & {
partitionTables?: GroupedSidebarPartitionTableEntry<T>[];
};
interface GroupSidebarPartitionTableEntriesOptions<T extends SidebarPartitionTableEntry> {
isEntryVisible?: (entry: T) => boolean;
}
const normalizePartitionTableKey = (value: unknown): string =>
String(value || '').trim().toLowerCase();
const extractUnqualifiedPartitionTableName = (value: string): string => {
const separatorIndex = value.indexOf('.');
return separatorIndex >= 0 ? value.slice(separatorIndex + 1) : value;
};
const buildPartitionEntryKeys = (
entry: Pick<SidebarPartitionTableEntry, 'tableName' | 'schemaName'>,
): string[] => {
const tableName = String(entry.tableName || '').trim();
const schemaName = String(entry.schemaName || '').trim();
if (!tableName) return [];
const keys = new Set<string>([normalizePartitionTableKey(tableName)]);
if (schemaName) {
const objectName = extractUnqualifiedPartitionTableName(tableName);
keys.add(normalizePartitionTableKey(`${schemaName}.${objectName}`));
}
return Array.from(keys).filter(Boolean);
};
const buildPartitionParentKeys = (
entry: Pick<SidebarPartitionTableEntry, 'partitionParentTableName' | 'schemaName'>,
): string[] => {
const parentTableName = String(entry.partitionParentTableName || '').trim();
const schemaName = String(entry.schemaName || '').trim();
if (!parentTableName) return [];
const keys = new Set<string>();
if (schemaName && !parentTableName.includes('.')) {
keys.add(normalizePartitionTableKey(`${schemaName}.${parentTableName}`));
}
keys.add(normalizePartitionTableKey(parentTableName));
return Array.from(keys).filter(Boolean);
};
export const groupSidebarPartitionTableEntries = <T extends SidebarPartitionTableEntry>(
entries: T[],
options: GroupSidebarPartitionTableEntriesOptions<T> = {},
): GroupedSidebarPartitionTableEntry<T>[] => {
const groupedEntries = entries
.filter((entry) => options.isEntryVisible?.(entry) ?? true)
.map((entry) => ({ ...entry })) as GroupedSidebarPartitionTableEntry<T>[];
const entryByKey = new Map<string, GroupedSidebarPartitionTableEntry<T>>();
groupedEntries.forEach((entry) => {
buildPartitionEntryKeys(entry).forEach((key) => {
if (!entryByKey.has(key)) entryByKey.set(key, entry);
});
});
const directParentByEntry = new Map<
GroupedSidebarPartitionTableEntry<T>,
GroupedSidebarPartitionTableEntry<T>
>();
groupedEntries.forEach((entry) => {
const parent = buildPartitionParentKeys(entry)
.map((key) => entryByKey.get(key))
.find((candidate) => candidate && candidate !== entry);
if (parent) directParentByEntry.set(entry, parent);
});
const createsCycle = (
child: GroupedSidebarPartitionTableEntry<T>,
parent: GroupedSidebarPartitionTableEntry<T>,
): boolean => {
const seen = new Set<GroupedSidebarPartitionTableEntry<T>>([child]);
let current: GroupedSidebarPartitionTableEntry<T> | undefined = parent;
while (current) {
if (seen.has(current)) return true;
seen.add(current);
current = directParentByEntry.get(current);
}
return false;
};
const nestedEntries = new Set<GroupedSidebarPartitionTableEntry<T>>();
groupedEntries.forEach((entry) => {
const parent = directParentByEntry.get(entry);
if (!parent || createsCycle(entry, parent)) return;
parent.partitionTables = [...(parent.partitionTables || []), entry];
delete parent.rowCount;
nestedEntries.add(entry);
});
return groupedEntries.filter((entry) => !nestedEntries.has(entry));
};