Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions __tests__/commands-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>): RelationRow {
/** 构造一条最小 StoredRelation(formatRelationsList 测试用)。 */
function row(over: Partial<StoredRelation>): 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,
};
}
Expand Down Expand Up @@ -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");
});
Expand Down
27 changes: 15 additions & 12 deletions __tests__/relation-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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);
});

Expand Down Expand Up @@ -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");
});
});
});
Expand Down
4 changes: 2 additions & 2 deletions __tests__/workspace-target.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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", () => {
Expand Down
41 changes: 20 additions & 21 deletions commands/relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -66,10 +65,10 @@
}

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);
Expand All @@ -79,19 +78,19 @@
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",
);
}
Expand Down Expand Up @@ -176,9 +175,9 @@
}

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);
Expand All @@ -189,7 +188,7 @@

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) {
Expand Down Expand Up @@ -242,7 +241,7 @@

for (const t of sampleTables) {
try {
const info = await ws.getTableSchema(t);

Check warning on line 244 in commands/relations.ts

View workflow job for this annotation

GitHub Actions / checks

eslint(no-await-in-loop)

Unexpected `await` inside a loop.
erLines.push(` "${t}" {`);
for (const col of info.columns) {
const colName = col.COLUMN_NAME as string;
Expand Down Expand Up @@ -327,8 +326,8 @@
const relations = ws.listRelations(table);
const relatedTableNames = new Set<string>();
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<string, SqlRow[]>();
Expand All @@ -336,7 +335,7 @@
for (const relatedTable of relatedTableNames) {
if (relatedTable === table) continue;
try {
const info = await ws.getTableSchema(relatedTable);

Check warning on line 338 in commands/relations.ts

View workflow job for this annotation

GitHub Actions / checks

eslint(no-await-in-loop)

Unexpected `await` inside a loop.
allColumns.set(relatedTable, info.columns);
} catch {
// 跳过
Expand All @@ -347,9 +346,9 @@

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<string>();
Expand Down
34 changes: 17 additions & 17 deletions relation-graph.ts
Original file line number Diff line number Diff line change
@@ -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 执行查询所用的接缝。由调用方提供
Expand Down Expand Up @@ -38,16 +38,16 @@
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);
}
}

Expand All @@ -72,7 +72,7 @@

// ── CRUD(经存储)──────────────────────────────────────

upsert(source: ColumnRef, target: ColumnRef, relationType = "MANY_TO_ONE"): RelationRow {
upsert(source: ColumnRef, target: ColumnRef, relationType = "MANY_TO_ONE"): StoredRelation {
const rel: Omit<ColumnRelation, "id"> = {
schema: source.schema,
table: source.table,
Expand Down Expand Up @@ -111,17 +111,17 @@
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<ColumnRef, ColumnRef[]> {
private getDirectRelations(schema: string, table: string): Map<ColumnRef, ColumnRef[]> {
const result = new Map<ColumnRef, ColumnRef[]>();
for (const entry of this.forward.values()) {
if (entry.source.schema !== schema || entry.source.table !== table) continue;
Expand Down Expand Up @@ -195,7 +195,7 @@
}

// 并行发起本层所有查询。
const settled = await Promise.allSettled(

Check warning on line 198 in relation-graph.ts

View workflow job for this annotation

GitHub Actions / checks

eslint(no-await-in-loop)

Unexpected `await` inside a loop.
batch.map((t) =>
(async () => {
let whereClause = `\`${t.targetCol.column}\` IN (?)`;
Expand Down Expand Up @@ -246,12 +246,12 @@
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({
Expand Down
42 changes: 13 additions & 29 deletions relation/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

// ====== 存储 ======

Expand Down Expand Up @@ -74,7 +58,7 @@ export class RelationStore {
// ── CRUD ──────────────────────────────────────────────────────

/** 幂等保存关系。冲突时创建或更新。返回该行。 */
upsert(rel: Omit<ColumnRelation, "id">): RelationRow {
upsert(rel: Omit<ColumnRelation, "id">): StoredRelation {
this.db
.prepare(`
INSERT INTO table_relations
Expand Down Expand Up @@ -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
Expand All @@ -133,7 +117,7 @@ export class RelationStore {
table?: string;
refSchema?: string;
refTable?: string;
}): RelationRow[] {
}): StoredRelation[] {
const conditions: string[] = [];
const params: any[] = [];

Expand Down Expand Up @@ -193,19 +177,19 @@ export class RelationStore {

// ── 辅助 ───────────────────────────────────────────────────

private rowToRelation(row: Record<string, any>): RelationRow {
private rowToRelation(row: Record<string, any>): 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,
};
}
}
Loading
Loading