mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-12 16:02:48 +08:00
🐛 fix(sidebar/kingbase): 修复函数列表重复叠加导致越刷越多
- 元数据 fallback:完整目录查询首次成功后停止,避免多条 SQL 结果叠加 - 函数去重:按 schema+name+type 大小写不敏感去重,折叠同名 overload - 树节点 key 区分 func/proc,避免同名函数/过程 key 冲突引发虚拟列表刷屏 - 补充 Kingbase/MySQL 函数元数据加载单测锁定回归
This commit is contained in:
@@ -6,9 +6,11 @@ vi.mock("../../../wailsjs/go/app/App", () => ({
|
||||
|
||||
import { DBQuery } from "../../../wailsjs/go/app/App";
|
||||
import {
|
||||
buildFunctionsMetadataQuerySpecs,
|
||||
buildPackagesMetadataQuerySpecs,
|
||||
buildSchemasMetadataQuerySpecs,
|
||||
buildSequencesMetadataQuerySpecs,
|
||||
loadFunctions,
|
||||
loadPackages,
|
||||
loadSequences,
|
||||
loadViews,
|
||||
@@ -132,3 +134,97 @@ describe("Oracle object metadata loaders", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kingbase/PG routine metadata loaders", () => {
|
||||
it("builds multi-step function fallback queries for kingbase", () => {
|
||||
const specs = buildFunctionsMetadataQuerySpecs("kingbase", "ldf_server_dbs");
|
||||
expect(specs.length).toBeGreaterThanOrEqual(2);
|
||||
expect(specs[0]?.sql).toContain("pg_proc");
|
||||
expect(specs.some((spec) => spec.sql.includes("information_schema.routines"))).toBe(true);
|
||||
});
|
||||
|
||||
it("does not stack the same kingbase function when multiple catalog fallbacks succeed", async () => {
|
||||
let queryCount = 0;
|
||||
mockedDBQuery.mockImplementation(async (_config: unknown, _dbName: string, sql: string) => {
|
||||
queryCount += 1;
|
||||
if (sql.includes("pg_proc") && sql.includes("prokind")) {
|
||||
return {
|
||||
success: true,
|
||||
message: "",
|
||||
data: [
|
||||
{ schema_name: "ldf_server", routine_name: "p1", routine_type: "FUNCTION" },
|
||||
{ schema_name: "ldf_server", routine_name: "p1", routine_type: "PROCEDURE" },
|
||||
{ schema_name: "ldf_server", routine_name: "pk_zero_fn", routine_type: "FUNCTION" },
|
||||
// overload rows with same name/type must collapse
|
||||
{ schema_name: "ldf_server", routine_name: "p1", routine_type: "FUNCTION" },
|
||||
{ schema_name: "LDF_SERVER", routine_name: "p1", routine_type: "FUNCTION" },
|
||||
],
|
||||
};
|
||||
}
|
||||
if (sql.includes("information_schema.routines")) {
|
||||
return {
|
||||
success: true,
|
||||
message: "",
|
||||
data: [
|
||||
{ schema_name: "ldf_server", routine_name: "p1", routine_type: "FUNCTION" },
|
||||
{ schema_name: "ldf_server", routine_name: "p1", routine_type: "PROCEDURE" },
|
||||
{ schema_name: "ldf_server", routine_name: "pk_zero_fn", routine_type: "FUNCTION" },
|
||||
],
|
||||
};
|
||||
}
|
||||
if (sql.includes("pg_proc")) {
|
||||
return {
|
||||
success: true,
|
||||
message: "",
|
||||
data: [
|
||||
{ schema_name: "ldf_server", routine_name: "p1", routine_type: "FUNCTION" },
|
||||
{ schema_name: "ldf_server", routine_name: "p1", routine_type: "FUNCTION" },
|
||||
{ schema_name: "ldf_server", routine_name: "pk_zero_fn", routine_type: "FUNCTION" },
|
||||
],
|
||||
};
|
||||
}
|
||||
return { success: false, message: "", data: [] };
|
||||
});
|
||||
|
||||
const result = await loadFunctions({ config: { type: "kingbase" } }, "ldf_server_dbs");
|
||||
|
||||
expect(result.supported).toBe(true);
|
||||
// First full catalog success must short-circuit fallback queries.
|
||||
expect(queryCount).toBe(1);
|
||||
const p1Funcs = result.routines.filter((item) => item.routineName.toLowerCase().endsWith(".p1") && item.routineType === "FUNCTION");
|
||||
const p1Procs = result.routines.filter((item) => item.routineName.toLowerCase().endsWith(".p1") && item.routineType === "PROCEDURE");
|
||||
expect(p1Funcs).toHaveLength(1);
|
||||
expect(p1Procs).toHaveLength(1);
|
||||
expect(result.routines.filter((item) => item.routineName.toLowerCase().includes("pk_zero_fn"))).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("still collects complementary SHOW FUNCTION/PROCEDURE fallbacks for MySQL", async () => {
|
||||
mockedDBQuery.mockImplementation(async (_config: unknown, _dbName: string, sql: string) => {
|
||||
if (sql.includes("information_schema.routines")) {
|
||||
return { success: false, message: "no routines view", data: [] };
|
||||
}
|
||||
if (sql.includes("SHOW FUNCTION STATUS")) {
|
||||
return {
|
||||
success: true,
|
||||
message: "",
|
||||
data: [{ Db: "app", Name: "fn_a", Type: "FUNCTION" }],
|
||||
};
|
||||
}
|
||||
if (sql.includes("SHOW PROCEDURE STATUS")) {
|
||||
return {
|
||||
success: true,
|
||||
message: "",
|
||||
data: [{ Db: "app", Name: "sp_b", Type: "PROCEDURE" }],
|
||||
};
|
||||
}
|
||||
return { success: false, message: "", data: [] };
|
||||
});
|
||||
|
||||
const result = await loadFunctions({ config: { type: "mysql" } }, "app");
|
||||
expect(result.supported).toBe(true);
|
||||
expect(result.routines.map((item) => `${item.routineType}:${item.routineName}`).sort()).toEqual([
|
||||
"FUNCTION:app.fn_a",
|
||||
"PROCEDURE:app.sp_b",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -733,8 +733,14 @@ const queryMetadataRowsBySpecs = async (
|
||||
const config = buildSidebarRuntimeConfig(conn, dbName);
|
||||
const results: MetadataQueryResult[] = [];
|
||||
let hasSuccessfulQuery = false;
|
||||
// Full queries (no inferredType) are mutually exclusive fallbacks: first success wins.
|
||||
// Partial queries (inferredType set) are complementary (e.g. SHOW FUNCTION + SHOW PROCEDURE).
|
||||
let hasFullSuccess = false;
|
||||
|
||||
for (const spec of normalizedSpecs) {
|
||||
if (hasFullSuccess) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const result = await DBQuery(
|
||||
buildRpcConnectionConfig(config) as any,
|
||||
@@ -749,6 +755,11 @@ const queryMetadataRowsBySpecs = async (
|
||||
rows: result.data as Record<string, any>[],
|
||||
inferredType: spec.inferredType,
|
||||
});
|
||||
if (!spec.inferredType) {
|
||||
// Primary/fallback full catalog query succeeded — do not merge later fallbacks
|
||||
// (Kingbase/PG 上多条成功会把同一函数叠加多次).
|
||||
hasFullSuccess = true;
|
||||
}
|
||||
} catch {
|
||||
// 忽略单条查询失败,继续尝试后续回退语句
|
||||
}
|
||||
@@ -1013,7 +1024,8 @@ const loadFunctions = async (
|
||||
? "PROCEDURE"
|
||||
: "FUNCTION";
|
||||
const fullName = buildQualifiedName(schemaName, routineName);
|
||||
const uniqueKey = `${fullName}@@${normalizedType}`;
|
||||
// Case-insensitive dedupe: Kingbase/PG 多条 catalog SQL 或 overload 行容易同名大小写不一致
|
||||
const uniqueKey = `${fullName.toLowerCase()}@@${normalizedType}`;
|
||||
if (!fullName || seen.has(uniqueKey)) return;
|
||||
seen.add(uniqueKey);
|
||||
const typeLabel = normalizedType === "PROCEDURE" ? "P" : "F";
|
||||
|
||||
@@ -705,15 +705,31 @@ export const useSidebarTreeLoaders = ({
|
||||
return deduped;
|
||||
})();
|
||||
|
||||
const routineEntries = routineRows.map((routine: any) => {
|
||||
const parsed = splitQualifiedName(routine.routineName);
|
||||
const typeLabel = routine.routineType === 'PROCEDURE' ? 'P' : 'F';
|
||||
return {
|
||||
...routine,
|
||||
schemaName: parsed.schemaName,
|
||||
displayName: `${parsed.objectName || routine.routineName} [${typeLabel}]`,
|
||||
};
|
||||
});
|
||||
const routineEntries = (() => {
|
||||
const deduped: Array<{ routineName: string; routineType: string; schemaName: string; displayName: string }> = [];
|
||||
const routineSeen = new Set<string>();
|
||||
routineRows.forEach((routine: any) => {
|
||||
const parsed = splitQualifiedName(routine.routineName);
|
||||
const routineType = String(routine.routineType || 'FUNCTION').toUpperCase().includes('PROC')
|
||||
? 'PROCEDURE'
|
||||
: 'FUNCTION';
|
||||
const schemaName = String(parsed.schemaName || routine.schemaName || '').trim();
|
||||
const objectName = String(parsed.objectName || routine.routineName || '').trim();
|
||||
if (!objectName) return;
|
||||
const routineName = String(routine.routineName || objectName).trim();
|
||||
const typeLabel = routineType === 'PROCEDURE' ? 'P' : 'F';
|
||||
const dedupeKey = `${schemaName.toLowerCase()}@@${objectName.toLowerCase()}@@${routineType}`;
|
||||
if (routineSeen.has(dedupeKey)) return;
|
||||
routineSeen.add(dedupeKey);
|
||||
deduped.push({
|
||||
routineName,
|
||||
routineType,
|
||||
schemaName,
|
||||
displayName: `${objectName} [${typeLabel}]`,
|
||||
});
|
||||
});
|
||||
return deduped;
|
||||
})();
|
||||
|
||||
const sequenceEntries = sequenceRows.map((sequence: any) => {
|
||||
const parsed = splitQualifiedName(sequence.sequenceName);
|
||||
@@ -858,14 +874,19 @@ export const useSidebarTreeLoaders = ({
|
||||
isLeaf: true,
|
||||
});
|
||||
|
||||
const buildRoutineNode = (entry: { routineName: string; routineType: string; schemaName: string; displayName: string }): TreeNode => ({
|
||||
title: entry.displayName,
|
||||
key: `${conn.id}-${conn.dbName}-routine-${entry.routineName}`,
|
||||
icon: <CodeOutlined />,
|
||||
type: 'routine',
|
||||
dataRef: { ...conn, routineName: entry.routineName, routineType: entry.routineType, schemaName: entry.schemaName },
|
||||
isLeaf: true,
|
||||
});
|
||||
const buildRoutineNode = (entry: { routineName: string; routineType: string; schemaName: string; displayName: string }): TreeNode => {
|
||||
const typeToken = entry.routineType === 'PROCEDURE' ? 'proc' : 'func';
|
||||
const keyName = buildSidebarObjectKeyName(conn.dbName, entry.schemaName, entry.routineName);
|
||||
return {
|
||||
title: entry.displayName,
|
||||
// 必须带 routineType:同名函数/过程否则 key 冲突,虚拟列表会叠成“同一函数无限重复”
|
||||
key: `${conn.id}-${conn.dbName}-routine-${typeToken}-${keyName}`,
|
||||
icon: <CodeOutlined />,
|
||||
type: 'routine',
|
||||
dataRef: { ...conn, routineName: entry.routineName, routineType: entry.routineType, schemaName: entry.schemaName },
|
||||
isLeaf: true,
|
||||
};
|
||||
};
|
||||
|
||||
const buildSequenceNode = (entry: { sequenceName: string; schemaName: string; displayName: string }): TreeNode => {
|
||||
const keyName = buildSidebarObjectKeyName(conn.dbName, entry.schemaName, entry.sequenceName);
|
||||
|
||||
Reference in New Issue
Block a user