From 299801959b73d032fed2f8fda243b36dffe284f7 Mon Sep 17 00:00:00 2001 From: zavier <765324639@qq.com> Date: Sat, 1 Aug 2026 21:00:45 +0800 Subject: [PATCH] =?UTF-8?q?refactor(relations):=20=E5=85=B3=E7=B3=BB?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=BB=9F=E4=B8=80=E4=B8=BA=20camelCase=20?= =?UTF-8?q?=E5=8D=95=E4=B8=80=E5=BD=A2=E7=8A=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 候选 4:同一概念(表间列关联)此前有两种形状——存储返回的 snake_case RelationRow 穿过 graph/facade 漏到 8+ 消费点,转换逻辑 手写三处,调用方必须同时懂两种命名。改为 store 边界一次映射。 - types.ts 新增 StoredRelation(camelCase 全字段含 id/时间戳), ColumnRelation 派生为 Omit 掉持久化字段的子集——字段名完全一致 - relation/store.ts 删除 RelationRow,rowToRelation 在 SQLite 边界 一次映射;graph/facade/commands/db-tools 只认一种形状 - RelationGraph.getDirectRelations 收为 private(仅内部 BFS 使用), 测试改走公共接口 - 行格式化四变体保持现状(受众差异是需求,不是重复) --- __tests__/commands-format.test.ts | 25 +++++++++--------- __tests__/relation-graph.test.ts | 27 ++++++++++--------- __tests__/workspace-target.test.ts | 4 +-- commands/relations.ts | 41 ++++++++++++++--------------- relation-graph.ts | 34 ++++++++++++------------ relation/store.ts | 42 +++++++++--------------------- state/workspace.ts | 7 +++-- tools/db-tools.ts | 6 ++--- types.ts | 9 ++++++- 9 files changed, 93 insertions(+), 102 deletions(-) diff --git a/__tests__/commands-format.test.ts b/__tests__/commands-format.test.ts index 8203150..c73c927 100644 --- a/__tests__/commands-format.test.ts +++ b/__tests__/commands-format.test.ts @@ -3,24 +3,23 @@ import { sanitizeRows } from "../commands/query"; import { formatRelationsList } from "../commands/relations"; import { formatFavoriteList } from "../commands/favorites"; import { formatEntry, entryToItem } from "../commands/history"; -import type { RelationRow } from "../relation/store"; import type { FavoriteEntry, HistoryEntry } from "../history/store"; -import type { SqlRow } from "../types"; +import type { SqlRow, StoredRelation } from "../types"; -/** 构造一条最小 RelationRow(formatRelationsList 测试用)。 */ -function row(over: Partial): RelationRow { +/** 构造一条最小 StoredRelation(formatRelationsList 测试用)。 */ +function row(over: Partial): StoredRelation { return { id: 1, schema: "db", - table_name: "t_orders", - column_name: "customer_id", + table: "t_orders", + column: "customer_id", condition: "", - ref_schema: "db", - ref_table: "t_customers", - ref_column: "id", - relation_type: "MANY_TO_ONE", - created_time: "2025-01-01 00:00:00", - updated_time: "2025-01-01 00:00:00", + refSchema: "db", + refTable: "t_customers", + refColumn: "id", + relationType: "MANY_TO_ONE", + createdTime: "2025-01-01 00:00:00", + updatedTime: "2025-01-01 00:00:00", ...over, }; } @@ -84,7 +83,7 @@ describe("formatRelationsList", () => { }); it("renders multiple rows", () => { - const out = formatRelationsList([row({ id: 1 }), row({ id: 2, table_name: "t_items" })]); + const out = formatRelationsList([row({ id: 1 }), row({ id: 2, table: "t_items" })]); expect(out).toContain("— 2 条"); expect(out).toContain("# 2"); }); diff --git a/__tests__/relation-graph.test.ts b/__tests__/relation-graph.test.ts index 7b12a76..26915ae 100644 --- a/__tests__/relation-graph.test.ts +++ b/__tests__/relation-graph.test.ts @@ -37,13 +37,16 @@ describe("RelationGraph", () => { graph.upsert(src, tgt, "MANY_TO_ONE"); - // 前向 - const forward = graph.getDirectRelations("db1", "t_order"); - expect(forward.size).toBe(1); - - // 反向(双向) - const reverse = graph.getDirectRelations("db2", "t_user"); - expect(reverse.size).toBe(1); + // 前向(源 schema 内检索) + const forward = graph.list("db1", "t_order"); + expect(forward.length).toBe(1); + + // 反向——list 的 involving 过滤按源 schema 匹配,跨 schema 反向 + // 查不到(row.schema 是源表 db1);反向图遍历由 bfsQuery 测试覆盖, + // 这里用 listAll 验证目标方向已记录。 + const all = graph.listAll(); + expect(all.length).toBe(1); + expect(all[0].refSchema).toBe("db2"); }); it("removes relations by column match", () => { @@ -56,8 +59,8 @@ describe("RelationGraph", () => { const removed = graph.remove(src, tgt); expect(removed).toBe(true); - const forward = graph.getDirectRelations("db1", "t_order"); - expect(forward.size).toBe(0); + const forward = graph.list("db1", "t_order"); + expect(forward.length).toBe(0); }); it("remove returns false for non-existent relation", () => { @@ -94,7 +97,7 @@ describe("RelationGraph", () => { // 只有一条关系,更新为 MANY_TO_ONE const relations = graph.list("a", "t1"); expect(relations.length).toBe(1); - expect(relations[0].relation_type).toBe("MANY_TO_ONE"); + expect(relations[0].relationType).toBe("MANY_TO_ONE"); }); it("upsert with different conditions creates separate rows", () => { @@ -137,7 +140,7 @@ describe("RelationGraph", () => { // 注册时不校验目标表/列是否存在——错误引用要到 BFS 时才暴露。 const row = graph.upsert(src, tgt, "MANY_TO_ONE"); - expect(row.ref_table).toBe("t_nope"); + expect(row.refTable).toBe("t_nope"); expect(graph.list("a").length).toBe(1); }); @@ -172,7 +175,7 @@ describe("RelationGraph", () => { void _rt; // 运行时信息可能不带 relationType(如 FK 发现路径)——类型断言模拟 graph.mergeForeignKeys([withoutType as ColumnRelation]); - expect(graph.list("a")[0].relation_type).toBe("MANY_TO_ONE"); + expect(graph.list("a")[0].relationType).toBe("MANY_TO_ONE"); }); }); }); diff --git a/__tests__/workspace-target.test.ts b/__tests__/workspace-target.test.ts index b73b0e5..1cc2cb1 100644 --- a/__tests__/workspace-target.test.ts +++ b/__tests__/workspace-target.test.ts @@ -217,7 +217,7 @@ describe("DatabaseWorkspaceService target resolution", () => { const inLogs = ws.listRelations(undefined, "logs"); expect(inLogs).toHaveLength(1); expect(inLogs[0].schema).toBe("logs"); - expect(inLogs[0].table_name).toBe("orders"); + expect(inLogs[0].table).toBe("orders"); // ……而工作空间默认仍停留在当前数据库。 expect(ws.listRelations()).toHaveLength(0); @@ -240,7 +240,7 @@ describe("DatabaseWorkspaceService target resolution", () => { ws.upsertRelation("a", "x", "b", "y", { database: "somedb", relationType: "ONE_TO_ONE" }); const all = ws.listRelations(); expect(all).toHaveLength(1); - expect(all[0].relation_type).toBe("ONE_TO_ONE"); + expect(all[0].relationType).toBe("ONE_TO_ONE"); }); it("removeRelationByColumns deletes by column match", () => { diff --git a/commands/relations.ts b/commands/relations.ts index c634a2d..75f1f99 100644 --- a/commands/relations.ts +++ b/commands/relations.ts @@ -6,22 +6,21 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent"; import type { DatabaseWorkspaceService } from "../state/workspace"; -import type { SqlRow } from "../types"; -import type { RelationRow } from "../relation/store"; +import type { SqlRow, StoredRelation } from "../types"; import { pickTableFuzzy, withLoader } from "./utils"; // ── 列表格式化 ───────────────────────────────────────────── -export function formatRelationsList(rows: RelationRow[]): string { +export function formatRelationsList(rows: StoredRelation[]): string { if (rows.length === 0) return "暂无表关联关系。"; const lines = [`═══ 表关联关系 — ${rows.length} 条 ═══`, ""]; for (const r of rows) { - const src = `${r.schema}.${r.table_name}.${r.column_name}`; - const ref = `${r.ref_schema}.${r.ref_table}.${r.ref_column}`; + const src = `${r.schema}.${r.table}.${r.column}`; + const ref = `${r.refSchema}.${r.refTable}.${r.refColumn}`; const cond = r.condition ? ` [${r.condition}]` : ""; - lines.push(` #${String(r.id).padStart(3)} ${src} → ${ref} (${r.relation_type})${cond}`); + lines.push(` #${String(r.id).padStart(3)} ${src} → ${ref} (${r.relationType})${cond}`); } return lines.join("\n"); @@ -66,10 +65,10 @@ async function handleRelationsList( } const labels = rows.map((r) => { - const src = `${r.table_name}.${r.column_name}`; - const ref = `${r.ref_table}.${r.ref_column}`; + const src = `${r.table}.${r.column}`; + const ref = `${r.refTable}.${r.refColumn}`; const cond = r.condition ? ` [${r.condition}]` : ""; - return `#${String(r.id).padStart(3)} ${src.padEnd(24)} → ${ref} (${r.relation_type})${cond}`; + return `#${String(r.id).padStart(3)} ${src.padEnd(24)} → ${ref} (${r.relationType})${cond}`; }); const choice = await ctx.ui.select("选择一个关系", labels); @@ -79,19 +78,19 @@ async function handleRelationsList( const entry = rows[idx]; const action = await ctx.ui.select( - `#${entry.id} ${entry.table_name}.${entry.column_name} → ${entry.ref_table}.${entry.ref_column}`, + `#${entry.id} ${entry.table}.${entry.column} → ${entry.refTable}.${entry.refColumn}`, ["🗑 删除", "取消"], ); if (action === "🗑 删除") { const ok = await ctx.ui.confirm( "确认删除", - `#${entry.id} ${entry.table_name}.${entry.column_name} → ${entry.ref_table}.${entry.ref_column}`, + `#${entry.id} ${entry.table}.${entry.column} → ${entry.refTable}.${entry.refColumn}`, ); if (ok) { ws.removeRelation(entry.id); ctx.ui.notify( - `已删除关系 #${entry.id} ${entry.table_name}.${entry.column_name} → ${entry.ref_table}.${entry.ref_column}`, + `已删除关系 #${entry.id} ${entry.table}.${entry.column} → ${entry.refTable}.${entry.refColumn}`, "info", ); } @@ -176,9 +175,9 @@ async function handleRelationsRemove( } const labels = rows.map((r) => { - const src = `${r.table_name}.${r.column_name}`; - const ref = `${r.ref_table}.${r.ref_column}`; - return `#${String(r.id).padStart(3)} ${src.padEnd(24)} → ${ref} (${r.relation_type})`; + const src = `${r.table}.${r.column}`; + const ref = `${r.refTable}.${r.refColumn}`; + return `#${String(r.id).padStart(3)} ${src.padEnd(24)} → ${ref} (${r.relationType})`; }); const choice = await ctx.ui.select("选择要删除的关系", labels); @@ -189,7 +188,7 @@ async function handleRelationsRemove( const ok = await ctx.ui.confirm( "确认删除", - `"${entry.table_name}.${entry.column_name} → ${entry.ref_table}.${entry.ref_column}"?`, + `"${entry.table}.${entry.column} → ${entry.refTable}.${entry.refColumn}"?`, ); if (ok) { @@ -327,8 +326,8 @@ async function handleRelationsERDiagram( const relations = ws.listRelations(table); const relatedTableNames = new Set(); for (const r of relations) { - relatedTableNames.add(r.ref_table); - relatedTableNames.add(r.table_name); + relatedTableNames.add(r.refTable); + relatedTableNames.add(r.table); } const allColumns = new Map(); @@ -347,9 +346,9 @@ async function handleRelationsERDiagram( for (const r of relations) { const label = r.condition - ? `${r.column_name} → ${r.ref_column} [${r.condition}]` - : `${r.column_name} → ${r.ref_column}`; - lines.push(` "${r.table_name}" ||--o{ "${r.ref_table}" : "${label}"`); + ? `${r.column} → ${r.refColumn} [${r.condition}]` + : `${r.column} → ${r.refColumn}`; + lines.push(` "${r.table}" ||--o{ "${r.refTable}" : "${label}"`); } const drawn = new Set(); diff --git a/relation-graph.ts b/relation-graph.ts index d469cd9..6c312d1 100644 --- a/relation-graph.ts +++ b/relation-graph.ts @@ -1,6 +1,6 @@ import type { Database } from "better-sqlite3"; -import type { ColumnRef, ColumnRelation, RelatedResult } from "./types"; -import { RelationStore, type RelationRow } from "./relation/store"; +import type { ColumnRef, ColumnRelation, RelatedResult, StoredRelation } from "./types"; +import { RelationStore } from "./relation/store"; /** * BFS 执行查询所用的接缝。由调用方提供 @@ -38,16 +38,16 @@ export class RelationGraph { for (const row of all) { const source: ColumnRef = { schema: row.schema, - table: row.table_name, - column: row.column_name, + table: row.table, + column: row.column, condition: row.condition || undefined, }; const target: ColumnRef = { - schema: row.ref_schema, - table: row.ref_table, - column: row.ref_column, + schema: row.refSchema, + table: row.refTable, + column: row.refColumn, }; - this.addToForward(source, target, row.relation_type); + this.addToForward(source, target, row.relationType); } } @@ -72,7 +72,7 @@ export class RelationGraph { // ── CRUD(经存储)────────────────────────────────────── - upsert(source: ColumnRef, target: ColumnRef, relationType = "MANY_TO_ONE"): RelationRow { + upsert(source: ColumnRef, target: ColumnRef, relationType = "MANY_TO_ONE"): StoredRelation { const rel: Omit = { schema: source.schema, table: source.table, @@ -111,17 +111,17 @@ export class RelationGraph { return deleted; } - list(schema?: string, table?: string): RelationRow[] { + list(schema?: string, table?: string): StoredRelation[] { return this.store.list({ schema, table }); } - listAll(): RelationRow[] { + listAll(): StoredRelation[] { return this.store.list(); } // ── BFS 遍历 ───────────────────────────────────────────── - getDirectRelations(schema: string, table: string): Map { + private getDirectRelations(schema: string, table: string): Map { const result = new Map(); for (const entry of this.forward.values()) { if (entry.source.schema !== schema || entry.source.table !== table) continue; @@ -246,12 +246,12 @@ export class RelationGraph { const exists = allExisting.some( (ex) => ex.schema === r.schema && - ex.table_name === r.table && - ex.column_name === r.column && + ex.table === r.table && + ex.column === r.column && ex.condition === (r.condition ?? "") && - ex.ref_schema === r.refSchema && - ex.ref_table === r.refTable && - ex.ref_column === r.refColumn, + ex.refSchema === r.refSchema && + ex.refTable === r.refTable && + ex.refColumn === r.refColumn, ); if (!exists) { this.store.upsert({ diff --git a/relation/store.ts b/relation/store.ts index 3cc4439..b43d198 100644 --- a/relation/store.ts +++ b/relation/store.ts @@ -5,23 +5,7 @@ */ import Database from "better-sqlite3"; -import type { ColumnRelation } from "../types"; - -// ====== 类型 ====== - -export interface RelationRow { - id: number; - schema: string; - table_name: string; - column_name: string; - condition: string; - ref_schema: string; - ref_table: string; - ref_column: string; - relation_type: string; - created_time: string; - updated_time: string; -} +import type { ColumnRelation, StoredRelation } from "../types"; // ====== 存储 ====== @@ -74,7 +58,7 @@ export class RelationStore { // ── CRUD ────────────────────────────────────────────────────── /** 幂等保存关系。冲突时创建或更新。返回该行。 */ - upsert(rel: Omit): RelationRow { + upsert(rel: Omit): StoredRelation { this.db .prepare(` INSERT INTO table_relations @@ -114,7 +98,7 @@ export class RelationStore { refSchema: string, refTable: string, refColumn: string, - ): RelationRow | undefined { + ): StoredRelation | undefined { const row = this.db .prepare(` SELECT * FROM table_relations @@ -133,7 +117,7 @@ export class RelationStore { table?: string; refSchema?: string; refTable?: string; - }): RelationRow[] { + }): StoredRelation[] { const conditions: string[] = []; const params: any[] = []; @@ -193,19 +177,19 @@ export class RelationStore { // ── 辅助 ─────────────────────────────────────────────────── - private rowToRelation(row: Record): RelationRow { + private rowToRelation(row: Record): StoredRelation { return { id: row.id, schema: row.schema, - table_name: row.table_name, - column_name: row.column_name, + table: row.table_name, + column: row.column_name, condition: row.condition, - ref_schema: row.ref_schema, - ref_table: row.ref_table, - ref_column: row.ref_column, - relation_type: row.relation_type, - created_time: row.created_time, - updated_time: row.updated_time, + refSchema: row.ref_schema, + refTable: row.ref_table, + refColumn: row.ref_column, + relationType: row.relation_type, + createdTime: row.created_time, + updatedTime: row.updated_time, }; } } diff --git a/state/workspace.ts b/state/workspace.ts index 263fcfd..bd0fc09 100644 --- a/state/workspace.ts +++ b/state/workspace.ts @@ -24,8 +24,7 @@ import { type HistoryFilter, } from "../history/store"; import { RelationGraph } from "../relation-graph"; -import type { RelationRow } from "../relation/store"; -import type { RelatedResult, SqlRow } from "../types"; +import type { RelatedResult, SqlRow, StoredRelation } from "../types"; import { StateStore } from "./state-store"; // ====== 内部类型 ====== @@ -470,7 +469,7 @@ export class DatabaseWorkspaceService { // ── 关系 ────────────────────────────────────────────────── - listRelations(table?: string, database?: string): RelationRow[] { + listRelations(table?: string, database?: string): StoredRelation[] { const schema = database ?? this.current?.database; if (!schema) return this.relationGraph.listAll(); return this.relationGraph.list(schema, table); @@ -482,7 +481,7 @@ export class DatabaseWorkspaceService { refTable: string, refColumn: string, opts?: { condition?: string; relationType?: string; database?: string }, - ): RelationRow { + ): StoredRelation { const schema = opts?.database ?? this.current?.database; if (!schema) throw new Error("No database selected"); return this.relationGraph.upsert( diff --git a/tools/db-tools.ts b/tools/db-tools.ts index 3d5a7f5..db1589c 100644 --- a/tools/db-tools.ts +++ b/tools/db-tools.ts @@ -300,8 +300,8 @@ export function registerDbTools( const rows = ws.listRelations(params.table, params.database); const lines = rows.map( (r) => - `#${r.id} ${r.table_name}.${r.column_name} → ${r.ref_table}.${r.ref_column}` + - `(${r.relation_type}${r.condition ? `,条件:${r.condition}` : ""})`, + `#${r.id} ${r.table}.${r.column} → ${r.refTable}.${r.refColumn}` + + `(${r.relationType}${r.condition ? `,条件:${r.condition}` : ""})`, ); const scope = params.database ?? ws.current?.database ?? "全部数据库"; return { @@ -451,7 +451,7 @@ export function registerDbTools( type: "text", text: `关联 #${row.id}:${params.table}.${params.column} → ` + - `${params.refTable}.${params.refColumn}(${row.relation_type})`, + `${params.refTable}.${params.refColumn}(${row.relationType})`, }, ], details: { relationId: row.id }, diff --git a/types.ts b/types.ts index b53d607..62d4b36 100644 --- a/types.ts +++ b/types.ts @@ -15,7 +15,9 @@ export interface ColumnRef { condition?: string; } -export interface ColumnRelation { +/** 存储中的一条表关系——camelCase 唯一形状(snake_case 只在 SQLite 边界映射)。 */ +export interface StoredRelation { + id: number; schema: string; table: string; column: string; @@ -24,8 +26,13 @@ export interface ColumnRelation { refTable: string; refColumn: string; relationType: string; + createdTime: string; + updatedTime: string; } +/** 关系输入/领域形状——StoredRelation 去掉持久化字段。 */ +export type ColumnRelation = Omit; + // ====== 查询结果类型 ====== export interface RelatedResult {