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
2 changes: 1 addition & 1 deletion commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export async function handleAdd(

// --- 写入 ---
try {
ws.createConnection(env, connName, {
ws.createConnection(connName, {
environment: env,
type: "mysql",
host,
Expand Down
2 changes: 1 addition & 1 deletion commands/favorites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export async function handleFavorite(
async function handleFavoriteAdd(
ctx: ExtensionCommandContext,
ws: DatabaseWorkspaceService,
pi: ExtensionAPI,
_pi: ExtensionAPI,
args: string[],
): Promise<void> {
let name: string | undefined;
Expand Down
17 changes: 1 addition & 16 deletions commands/llm-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function sendActiveDb(pi: ExtensionAPI, ws: DatabaseWorkspaceService): vo
}

/** 已配置但未选择数据库:引导用户运行 /db switch。 */
export function sendConfiguredHint(pi: ExtensionAPI, ws: DatabaseWorkspaceService): void {
function sendConfiguredHint(pi: ExtensionAPI, ws: DatabaseWorkspaceService): void {
pi.sendMessage(
{
customType: "db-hint",
Expand All @@ -49,18 +49,3 @@ export function sendDbStatus(pi: ExtensionAPI, ws: DatabaseWorkspaceService): vo
sendConfiguredHint(pi, ws);
}
}

/** 未配置任何连接:triggerTurn 让 AI 主动提议建立第一个连接。 */
export function sendUnconfiguredHint(pi: ExtensionAPI, configPath: string): void {
pi.sendMessage(
{
customType: "db-hint",
content:
`No database connections are configured yet. Help the user create their first ` +
`connection in ${configPath}. Ask for host, port, username, password, and default ` +
`database name. After the config file is written, tell the user to run /db switch to connect.`,
display: false,
},
{ deliverAs: "followUp", triggerTurn: true },
);
}
2 changes: 1 addition & 1 deletion commands/mutate-confirm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function showMutationConfirm(
const warningText = params.warning ?? DEFAULT_WARNING;

const result = await ctx.ui.custom<boolean>(
(tui, theme, _kb, done) => {
(_tui, theme, _kb, done) => {
const container = new Container();

// ── 上边框 ──
Expand Down
4 changes: 2 additions & 2 deletions commands/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function sanitizeRows(rows: SqlRow[]): Record<string, string | null>[] {
// ── 关联结果转换 ──────────────────────────────────

/** 将 RelatedResult 转为 TUI entry 可用的 RelatedTuiData(行值清洗为 string|null)。 */
export function toRelatedTuiData(related: RelatedResult[]): RelatedTuiData[] {
function toRelatedTuiData(related: RelatedResult[]): RelatedTuiData[] {
return related.map((r) => ({
schema: r.schema,
table: r.table,
Expand All @@ -62,7 +62,7 @@ export function toRelatedTuiData(related: RelatedResult[]): RelatedTuiData[] {
// ── 展示(双受众:TUI 条目 + LLM 上下文)──────────

async function displayQueryResult(
ctx: ExtensionCommandContext,
_ctx: ExtensionCommandContext,
ws: DatabaseWorkspaceService,
pi: ExtensionAPI,
result: ExecutedResult,
Expand Down
2 changes: 1 addition & 1 deletion commands/related-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export interface BrowserTab {
}

/** 浏览器内容的分段结构——组件渲染时对每段套不同样式。 */
export interface BrowserContent {
interface BrowserContent {
/** 副标题(数据库名 + 关联表数量),如 `shop(2 个关联表)`。 */
title: string;
/** 表切换行(当前表 active=true)。 */
Expand Down
11 changes: 0 additions & 11 deletions commands/renderers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,11 @@
* 文档文本由 formatting/result-document.ts 统一装配(纯函数),
* 本模块只做两件事:把 DocLine 的 style 映射为 theme 颜色、
* 追加依赖 pi keyHint 的交互提示行。
*
* 面板(`db-workspace-panel`)保持为消息渲染器——其内容是
* 对 LLM 也有用的短预格式化文本。
*/

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { keyHint } from "@earendil-works/pi-coding-agent";
import type { Component } from "@earendil-works/pi-tui";
import { Text } from "@earendil-works/pi-tui";
import type { QueryResultDoc } from "../formatting/result-document";
import { renderQueryDocument } from "../formatting/result-document";

Expand Down Expand Up @@ -65,13 +61,6 @@ export function registerRenderers(pi: ExtensionAPI): void {
invalidate() {},
} satisfies Component;
});

// ── db-workspace-panel:短预格式化文本——保持为消息渲染器 ──

pi.registerMessageRenderer("db-workspace-panel", (message, _options, _theme) => {
if (typeof message.content !== "string") return undefined;
return new Text(message.content, 1, 0);
});
}

// ====== 内部辅助 ======
Expand Down
79 changes: 1 addition & 78 deletions commands/switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,87 +5,10 @@
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
import type { DatabaseWorkspaceService } from "../state/workspace";
import { withLoader } from "./utils";
import { sendActiveDb, sendUnconfiguredHint } from "./llm-context";
import { sendActiveDb } from "./llm-context";

export const STATUS_KEY = "db-workspace";

export async function showWorkspacePanel(
ctx: ExtensionCommandContext,
ws: DatabaseWorkspaceService,
pi?: ExtensionAPI,
): Promise<void> {
const lines: string[] = [];

lines.push("═══ 数据库工作区 ═══");
lines.push("");

if (ws.current) {
const conn = ws.getCurrentConnection();
lines.push(`环境 :${ws.current.environment}`);
lines.push(`连接 :${ws.current.connectionId}(${conn?.host ?? "?"})`);
lines.push(`数据库 :${ws.current.database}`);
} else {
lines.push("未选择数据库,使用 /db switch 连接");
lines.push("");
if (ws.isConfigured) {
lines.push(`可用环境:${ws.getEnvironments().join(", ")}`);
} else {
lines.push("⚠️ 尚未配置数据库连接");
lines.push("");
lines.push(`配置文件:${ws.configPath}`);
lines.push("");
lines.push("配置示例:");
lines.push(" connections:");
lines.push(" my-db:");
lines.push(" environment: prod");
lines.push(" type: mysql");
lines.push(" host: 127.0.0.1");
lines.push(" port: 3306");
lines.push(" username: root");
lines.push(" password: ${DB_PASSWORD}");
}
}

// 显示配置警告(如未解析的 env 变量)
const warnings = ws.getConfigWarnings();
if (warnings.length > 0) {
lines.push("");
for (const w of warnings) {
lines.push(`⚠ ${w}`);
}
}

lines.push("");
lines.push("命令:");
lines.push(" /db switch 选择环境和数据库");
lines.push(" /db add 添加新连接");
lines.push(" /db tables 列出所有表");
lines.push(" /db schema <表名> 查看表结构");
lines.push(" /db query [表名] 选表 → WHERE → 查询");
lines.push(" /db history 查询历史");
lines.push(" /db favorite 收藏的查询模板");
lines.push(" /db relations 表关联关系管理");

// 用户可见的面板(纯文本渲染器)。
if (pi) {
pi.sendMessage(
{
customType: "db-workspace-panel",
content: lines.join("\n"),
display: true,
},
{ deliverAs: "followUp", triggerTurn: false },
);
}

// 未配置连接时给 LLM 的静默提示。
// display: false 让它不进聊天;triggerTurn 让 AI
// 主动提议帮助建立第一个连接。
if (!ws.isConfigured && pi) {
sendUnconfiguredHint(pi, ws.configPath);
}
}

export async function handleSwitch(
ctx: ExtensionCommandContext,
ws: DatabaseWorkspaceService,
Expand Down
6 changes: 3 additions & 3 deletions connection/db-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { homedir } from "node:os";

// ====== 配置文件类型 ======

export interface ConnectionConfig {
interface ConnectionConfig {
environment: string;
type: "mysql";
host: string;
Expand All @@ -24,7 +24,7 @@ export interface ConnectionConfig {
queryLimit?: number; // row cap for SELECTs without LIMIT (default: DEFAULT_QUERY_LIMIT)
}

export interface ConnectionsFile {
interface ConnectionsFile {
connections: Record<string, ConnectionConfig>;
}

Expand Down Expand Up @@ -59,7 +59,7 @@ function resolveEnvVars(value: string): { resolved: string; unresolved: string[]
return { resolved, unresolved };
}

export interface ConfigLoadResult {
interface ConfigLoadResult {
connections: ResolvedConnectionConfig[];
/** 被跳过的连接或未解析的 env 变量的警告。 */
warnings: string[];
Expand Down
6 changes: 3 additions & 3 deletions connection/db-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,18 @@

// ====== 选项与输出 ======

export interface QueryOptions {
interface QueryOptions {
params?: unknown[]; // bound values for ? placeholders
}

export interface QueryOutput {
interface QueryOutput {
columns: string[];
rows: SqlRow[];
elapsed: string;
sql: string; // final SQL after policy (LIMIT may have been appended)
}

export interface MutationOutput {
interface MutationOutput {
affectedRows: number;
elapsed: string;
sql: string;
Expand Down Expand Up @@ -118,7 +118,7 @@
async getDatabases(connectionId: string): Promise<string[]> {
const pool = this.getPool(connectionId);
const [rows] = await pool.query<RowDataPacket[]>("SHOW DATABASES");
return rows.map((r) => r.Database as string).sort();

Check warning on line 121 in connection/db-manager.ts

View workflow job for this annotation

GitHub Actions / checks

unicorn(no-array-sort)

Use `Array#toSorted()` instead of `Array#sort()`.
}

/**
Expand Down
2 changes: 1 addition & 1 deletion connection/sql-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const MUTATION_SQL_RE = /^(INSERT|UPDATE|DELETE|REPLACE)\b/i;

const WHERE_RE = /\bWHERE\b/i;

export interface MutationValidation {
interface MutationValidation {
sql: string;
operation: "INSERT" | "UPDATE" | "DELETE" | "REPLACE";
hasWhere: boolean;
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class DatabaseWorkspaceService {
saveFavorite(name, sql, desc): FavoriteEntry;

// ── 关系图 ──
upsertRelation(srcTable, srcCol, refTable, refCol, opts?): RelationRow;
upsertRelation(srcTable, srcCol, refTable, refCol, opts?): StoredRelation;
removeRelationByColumns(database, srcTable, srcCol, refTable, refCol): boolean;
async discoverForeignKeys(): Promise<number>;
}
Expand All @@ -114,7 +114,7 @@ executeQuery(connId, db, sql)

**为什么需要专用连接**:MySQL 的 `USE database` 是连接级状态。如果使用连接池的 `pool.query()`,两次调用可能落在不同连接上,导致 `USE` 和 `query` 分离。检出专用连接并在 finally 中归还,保证了原子性。

**延迟连接**:MySQL 连接池在第一次使用时才创建(`getPool()` 懒初始化),按 connection ID 缓存。`destroy()` 遍历关闭所有池。
**延迟连接**:MySQL 连接池在第一次使用时才懒创建,按 connection ID 缓存。`destroy()` 遍历关闭所有池。

### 3.3 SQL 安全策略 — sql-policy.ts

Expand Down
3 changes: 1 addition & 2 deletions docs/mutation-tool-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,14 @@ async executeMutation(
connectionId: string,
database: string,
sql: string,
opts: { timeout?: number } = {},
): Promise<MutationOutput> {
const pool = this.getPool(connectionId);
const conn = await pool.getConnection();
try {
await conn.query(`USE \`${database}\``);
const start = Date.now();
const [result] = await conn.query(
{ sql, timeout: opts.timeout ?? 30000 },
{ sql, timeout: 30000 },
) as [mysql.ResultSetHeader, any];
const elapsed = `${((Date.now() - start) / 1000).toFixed(3)}s`;
return {
Expand Down
10 changes: 5 additions & 5 deletions docs/relation-tool-redesign.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_relations_unique
`RelationStore.insert()` 改为 `RelationStore.upsert()`:

```typescript
upsert(rel: Omit<ColumnRelation, "id">): RelationRow {
upsert(rel: Omit<ColumnRelation, "id">): StoredRelation {
const stmt = this.db.prepare(`
INSERT INTO table_relations
(schema, table_name, column_name, condition, ref_schema, ref_table, ref_column, relation_type)
Expand Down Expand Up @@ -219,7 +219,7 @@ upsert(rel: Omit<ColumnRelation, "id">): RelationRow {
findByColumns(
schema: string, table: string, column: string, condition: string,
refSchema: string, refTable: string, refColumn: string,
): RelationRow | undefined {
): StoredRelation | undefined {
const row = this.db.prepare(`
SELECT * FROM table_relations
WHERE schema = ? AND table_name = ? AND column_name = ? AND condition = ?
Expand Down Expand Up @@ -254,7 +254,7 @@ deleteByColumns(
### 5.1 `register` → `upsert`

```typescript
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 @@ -307,7 +307,7 @@ upsertRelation(
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(
Expand Down Expand Up @@ -359,7 +359,7 @@ removeRelationByColumns(
### 7.3 其余子命令不变

`/db relations remove` 仍通过 id 选择 → `ws.removeRelation(id)`。
`/db relations er-diagram` 不变
`/db relations er-diagram` 已移除(候选 5:pi 不渲染 mermaid 图,命令删除;discover 的 AI 消息改为表结构文本)

---

Expand Down
1 change: 0 additions & 1 deletion docs/testing/ai-runner.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ Step 4 收尾(人验证完成后): H3 重建或 init-env.mjs --cleanup 销毁,
| G8 | G7 中选择"📎 一起查询关联表" | 主表 + 关联 customers/coupons/order_items/products 区块, 含关联路径 | 关联区块出现 |
| G9 | /db query t_customers → WHERE id=1 → 关联表 | 关联 profiles(1)/orders(3)/coupons(1) | 各关联行数 |
| G10 | /db relations discover | 0 条(无系统外键) | 提示 0 |
| G11 | /db relations er-diagram | 已注册关系图 | 图示渲染 |
| G12 | /db favorite add(任一 SQL) → /db favorite | 收藏成功并按库分组列出 | 列表出现 |
| G13 | /db history / /db history 订单 | 最近 20 条 / 关键词过滤 | 列表过滤 |
| G14 | /db off → /db on | 状态栏移除/恢复; 工具报错提示未启用 | 状态栏变化 |
Expand Down
5 changes: 2 additions & 3 deletions docs/testing/test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,8 @@ node docs/testing/init-env.mjs --cleanup
| G7 | 选表查询 | `/db query t_orders` → 输入 `status='paid'` | 2 行; **有注册关系时询问"查询关联表?"** |
| G8 | **BFS 关联查询** | G7 选择"📎 一起查询关联表" | 主表结果 + 摘要行"关联表(N 个)— 详情见 LLM 上下文"(实测: TUI 不渲染完整关联表格, 完整内容在 LLM 上下文可见); 关联区块含 customers/coupons/order_items/products 等 |
| G9 | BFS 多跳 | `/db query t_customers` → WHERE `id=1` → 关联表 | 修复后(见 §7.2)提示关联查询; 关联 profiles(1)/orders(3)/coupons(1)/archive(1)等 8 个区块 |
| G10 | 关系发现 | `/db relations discover` | 同步外键 0 条(本环境无系统外键) → 之后**总会询问**"是否使用 AI 分析表关系?"(选 ⏭ 跳过即可; 选 🤖 会生成 ER 图交 AI 分析并自动注册) |
| G11 | ER 图 | `/db relations er-diagram` | 展示已注册关系图 |
| G12 | 收藏 | `/db favorite add` + `/db favorite` | 收藏成功并按库分组列出 |
| G10 | 关系发现 | `/db relations discover` | 同步外键 0 条(本环境无系统外键) → 之后**总会询问**"是否使用 AI 分析表关系?"(选 ⏭ 跳过即可; 选 🤖 会发送表结构文本交 AI 分析并自动注册) |
| G11 | 收藏 | `/db favorite add` + `/db favorite` | 收藏成功并按库分组列出 |
| G13 | 历史 | `/db history` / `/db history t_orders` | 最近 20 条 / 关键词过滤(匹配 **SQL 文本**, 中文表名搜不到; 无匹配时提示"未找到") |
| G14 | 扩展开关 | `/db off` → `/db on` | 状态栏移除/恢复; 工具调用报错提示未启用 |

Expand Down
4 changes: 2 additions & 2 deletions formatting/result-document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ export interface QueryResultDoc {
related?: RelatedResult[];
}

export type DocumentAudience = "tui-zh" | "llm-zh";
type DocumentAudience = "tui-zh" | "llm-zh";

export interface RenderOptions {
interface RenderOptions {
audience: DocumentAudience;
/** 仅 tui-zh 使用:自适应终端宽度。 */
width?: number;
Expand Down
2 changes: 1 addition & 1 deletion history/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export interface FavoriteEntry {
updatedTime: string;
}

export interface FavoriteFilter {
interface FavoriteFilter {
database?: string; // also includes global (database = '')
keyword?: string;
limit?: number;
Expand Down
Loading
Loading