Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

前言

关于这本书

VSDB 是一个 VSCode 全功能数据库客户端扩展,为开发者提供在 VSCode 内直接管理 MySQL 和 PostgreSQL 数据库的能力。它支持自动扫描项目配置文件发现数据库连接、统一管理连接、浏览数据库结构、编辑数据、执行 SQL 查询以及导入导出数据。

本书是 VSDB 的技术实现文档,深入剖析系统架构、核心模块设计、关键技术实现,帮助开发者理解如何构建一个生产级的 VSCode 扩展。

核心定位

数据库管理一体化 | 开发效率不依赖工具切换

为什么需要 VSDB

传统数据库开发流程中,开发者面临以下痛点:

工具切换频繁

graph LR
    A[VSCode 编辑代码] --> B[切换到 DataGrip]
    B --> C[查看数据库结构]
    C --> D[切换回 VSCode]
    D --> E[修改代码]
    E --> F[再次切换...]
  • 开发者需要频繁切换 IDE 和数据库工具
  • 上下文丢失:切换导致思路中断
  • 效率损耗:每次切换需要重新定位

连接信息分散

graph TD
    subgraph "项目配置"
        E1[".env 文件"]
        E2["docker-compose.yml"]
        E3["框架配置文件"]
    end
    
    subgraph "数据库工具"
        D1["手动配置连接"]
        D2["密码存储问题"]
        D3["团队协作困难"]
    end
    
    E1 --> D1
    E2 --> D1
    E3 --> D1
    
    D1 --> D2
    D2 --> D3
  • 数据库连接信息散落在多处
  • 每个开发者需要手动配置
  • 团队协作时连接配置难以共享

稳定性问题

数据库操作直接在主进程执行时:

graph TD
    A[执行复杂查询] --> B[阻塞主线程]
    B --> C[VSCode 卡顿]
    C --> D[用户体验差]
    
    A --> E[数据库驱动异常]
    E --> F[扩展崩溃]
    F --> G[需要重启]

VSDB 通过双进程架构解决这些问题。

VSDB 的解决方案

双进程隔离架构

graph TD
    subgraph "VSCode Extension Host"
        A[UI层 - TreeView/Webview]
        B[管理层 - Connection/IPC]
    end
    
    subgraph "Worker Process"
        C[数据库驱动]
        D[查询队列]
        E[Schema Inspector]
    end
    
    A --> B
    B -->|"IPC (child_process)"| C
    C --> D
    D --> E

数据库操作隔离在独立进程

  • Worker 崩溃不影响 VSCode
  • 自动重启恢复机制
  • UI 响应不受阻塞

自动扫描连接

graph LR
    subgraph "扫描引擎"
        S1[".env 解析"]
        S2["docker-compose 解析"]
        S3["框架配置解析"]
    end
    
    S1 --> R["发现连接"]
    S2 --> R
    S3 --> R
    
    R --> M["自动添加到 VSDB"]

支持的扫描来源:

  • .env 文件:DATABASE_URL, DB_HOST
  • docker-compose.yml:MySQL/PostgreSQL 服务
  • 框架配置:TypeORM, Laravel, Django, Prisma

安全存储机制

graph TD
    A[连接配置] --> B["connections.json<br/>(项目级/全局)"]
    A --> C["SecretStorage<br/>(密码加密)"]
    
    B --> D["团队可共享"]
    C --> E["安全不可读"]
  • 连接元数据存储在 JSON 文件,团队可共享
  • 密码通过 VSCode SecretStorage API 加密存储

这本书解决什么问题

当你面对以下需求时,这本书会给你答案:

  • 理解系统架构? — 双进程架构设计原理
  • 掌握 IPC 通信? — 进程间消息协议实现
  • 如何实现驱动? — MySQL/PostgreSQL 驱动封装
  • 如何做扫描引擎? — 多源配置解析实现
  • Webview 如何设计? — React + Monaco 编辑器集成
  • 如何扩展功能? — 新数据库类型、新功能的开发方法

你将学到什么

概览篇:建立全局认知

  • VSDB 的定位与价值
  • 核心概念:连接、IPC、Worker、Schema Inspector

架构篇:理解系统设计

  • 双进程架构设计原理
  • IPC 通信机制完整实现
  • 数据库驱动抽象层设计
  • 扫描引擎架构

功能篇:掌握具体实现

  • UI 组件实现:TreeView、Webview
  • 数据操作:查询、导入导出
  • 辅助功能:历史记录、书签、搜索

实战篇:落地应用

  • 开发调试流程
  • 扩展开发指南
  • 最佳实践建议

核心技术要点

本书贯穿以下核心技术要点:

$$ \text{稳定性} = \text{进程隔离} + \text{自动恢复} + \text{超时控制} $$

graph LR
    A[数据库操作] --> B[Worker进程]
    B --> C[超时控制]
    C --> D[异常捕获]
    D --> E[自动恢复]

$$ \text{安全性} = \text{SecretStorage} + \text{无明文密码} $$

$$ \text{效率} = \text{自动扫描} + \text{流式传输} + \text{虚拟滚动} $$

适用读者

  • VSCode 扩展开发者:学习构建复杂扩展的最佳实践
  • 全栈工程师:理解数据库工具的设计思路
  • TypeScript 开发者:学习进程通信、异步编程模式
  • 技术架构师:理解双进程架构的设计决策

项目背景

VSDB 是一个开源项目,目标是为 VSCode 用户提供媲美 DataGrip 的数据库管理体验,同时保持:

  • 轻量:不离开 VSCode 即可完成数据库操作
  • 安全:密码加密存储,不泄露敏感信息
  • 稳定:Worker 崩溃自动恢复,不影响 VSCode
  • 智能:自动发现项目中的数据库连接

"好的工具应该融入工作流,而不是打断工作流。"

第一章:项目概述

本章介绍 VSDB 项目的整体定位、功能特性、技术选型和项目结构。

1.1 项目定位

VSDB 是一个为 Visual Studio Code 设计的全功能数据库客户端扩展。它的核心目标是:

graph TD
    A["VSDB 核心目标"] --> B["一体化数据库管理"]
    A --> C["开发效率提升"]
    A --> D["稳定性保障"]
    A --> E["安全性保证"]
    
    B --> B1["连接管理"]
    B --> B2["结构浏览"]
    B --> B3["数据编辑"]
    B --> B4["SQL执行"]
    
    C --> C1["自动扫描连接"]
    C --> C2["无需切换工具"]
    
    D --> D1["双进程架构"]
    D --> D2["自动恢复"]
    
    E --> E1["SecretStorage"]
    E --> E2["无明文存储"]

与竞品的定位差异

特性DataGripVSDBDBeaver
独立应用
VSCode 集成
自动扫描
轻量
功能完整度★★★★★★★★☆☆★★★★★
学习成本

VSDB 选择轻量集成路线,牺牲部分高级功能换取无缝的 VSCode 体验。

1.2 功能特性

连接管理

// 核心功能:连接 CRUD
interface ConnectionFeatures {
  add: '手动添加连接';
  scan: '自动扫描项目配置';
  edit: '编辑连接属性';
  delete: '删除连接';
  group: '连接分组';
  color: '颜色标记';
  scope: '项目级/全局';
}

支持的扫描来源:

来源文件解析内容
.env.env, .env.local, .env.developmentDATABASE_URL, DB_*, MYSQL_*, PG_*
docker-composedocker-compose.ymlMySQL/PostgreSQL 服务定义
TypeORMormconfig.json, data-source.ts连接配置对象
Laravelconfig/database.phpMySQL/PG 连接数组
Djangosettings.pyDATABASES 字典
Prismaschema.prisma数据库 URL

数据库浏览

graph TD
    A["连接节点"] --> B["数据库列表"]
    B --> C["表列表"]
    C --> D["列信息"]
    
    B --> E["视图列表"]
    B --> F["存储过程"]
    
    D --> G["类型"]
    D --> H["是否NULL"]
    D --> I["默认值"]
    D --> J["主键"]
    D --> K["自增"]
    
    C --> L["索引"]
    C --> M["约束"]
    C --> N["触发器"]

SQL 编辑器

  • Monaco 编辑器集成,语法高亮
  • SQL 关键字自动补全
  • Ctrl+Enter 快速执行
  • 多结果标签页
  • 执行状态指示

数据操作

graph LR
    subgraph "查询执行"
        Q1["普通查询"]
        Q2["流式查询<br/>(大数据集)"]
    end
    
    subgraph "结果处理"
        R1["分页显示"]
        R2["内联编辑"]
        R3["虚拟滚动"]
    end
    
    subgraph "导入导出"
        E1["CSV"]
        E2["JSON"]
        E3["SQL"]
    end
    
    Q1 --> R1
    Q2 --> R3
    R1 --> E1
    R1 --> E2
    R1 --> E3

辅助功能

  • 查询历史:记录执行的 SQL,支持固定防清理
  • 书签系统:收藏连接、表、查询,支持分组
  • 全局搜索:搜索表名、列名、数据内容

1.3 技术选型

语言与框架

语言: TypeScript
扩展框架: VSCode Extension API
前端框架: React + Vite
编辑器: Monaco Editor
数据库驱动: mysql2 / pg
构建工具: webpack (扩展) + Vite (Webview)
测试框架: Vitest

选型决策记录

决策点选择理由
双进程 vs 单进程双进程数据库操作隔离,崩溃不影响 VSCode
Worker 通信child_process IPC原生支持,无需额外依赖
前端框架ReactWebview 标准方案,生态丰富
编辑器MonacoVSCode 同款,语法支持完善
数据库驱动mysql2 / pg纯 JS 实现,支持 Promise
流式传输自定义 chunk避免大数据集阻塞 IPC

为什么选择 TypeScript

graph TD
    A["TypeScript"] --> B["类型安全"]
    A --> C["VSCode 原生"]
    A --> D["生态丰富"]
    
    B --> B1["IPC协议类型化"]
    B --> B2["配置验证"]
    
    C --> C1["调试支持"]
    C --> C2["开发体验"]
    
    D --> D1["数据库驱动"]
    D --> D2["UI 框架"]

1.4 项目结构

vsdb/
├── src/                    # 扩展源代码
│   ├── extension.ts        # 扩展入口,命令注册
│   ├── core/               # 核心管理模块
│   │   ├── connectionManager.ts  # 连接 CRUD
│   │   ├── ipcManager.ts        # Worker 通信
│   │   ├── storage.ts           # 配置存储
│   │   ├── scanner.ts           # 扫描协调器
│   │   ├── historyManager.ts    # 查询历史
│   │   ├── bookmarkManager.ts   # 书签管理
│   │   └── searchEngine.ts      # 全局搜索
│   ├── scanner/            # 配置解析器
│   │   ├── envParser.ts          # .env 解析
│   │   ├── dockerComposeParser.ts # docker-compose 解析
│   │   └eworkParser.ts           # 框架配置解析
│   ├── shared/             # 共享类型和常量
│   │   ├── types.ts              # 类型定义
│   │   ├── constants.ts          # 常量定义
│   ├── ui/                 # UI 层
│   │   ├── tree/                 # TreeView
│   │   │   ├── treeProvider.ts   # 树数据提供者
│   │   │   ├── treeItems.ts      # 树节点类型
│   │   │   └commands.ts          # 树命令
│   │   ├── webview/              # Webview Panel
│   │   │   ├── sqlEditor.ts      # SQL 编辑器
│   │   │   ├── dataGrid.ts       # 数据网格
│   │   │   ├── connectionForm.ts # 连接表单
│   │   │   ├── tableStructure.ts # 表结构面板
│   │   │   ├── importExport.ts   # 导入导出
│   ├── worker/             # Worker 进程
│   │   ├── worker.ts             # Worker 入口
│   │   ├── driver/               # 数据库驱动
│   │   │   ├── mysql.ts          # MySQL 驱动
│   │   │   ├── postgresql.ts     # PostgreSQL 驱动
│   │   │   ├── schema/           # Schema Inspector
│   │   │   ├── mysqlSchema.ts    # MySQL Schema
│   │   │   ├── pgSchema.ts       # PostgreSQL Schema
│   │   ├── queryQueue.ts         # 查询队列
├── webview-ui/             # Webview React 应用
│   ├── src/
│   │   ├── App.tsx              # 入口组件
│   │   ├── main.tsx             # React 入口
│   │   ├── components/
│   │   │   ├── SqlEditor.tsx    # SQL 编辑器组件
│   │   │   ├── DataGrid.tsx     # 数据网格组件
│   │   │   ├── GridToolbar.tsx  # 工具栏
│   │   │   ├── ResultPanel.tsx  # 结果面板
│   │   │   ├── ImportExport.tsx # 导入导出组件
│   ├── package.json
│   ├── vite.config.ts
├── __tests__/              # 测试目录
│   ├── core/
│   │   ├── ipcManager.test.ts
│   │   ├── connectionManager.test.ts
│   ├── scanner/
│   │   ├── envParser.test.ts
│   ├── fixtures/
│   │   ├── config/
│   │   │   ├── database.yml
│   │   │   ├── ormconfig.json
│   │   ├── docker-compose.yaml
├── dist/                   # 构建输出
├── resources/              # 资源文件 (图标等)
├── docs/                   # 设计文档
├── package.json            # 扩展配置
├── tsconfig.json           # TypeScript 配置
├── webpack.config.js       # webpack 配置
├── vite.config.ts          # Vite 配置
└── CLAUDE.md               # Claude Code 指导文件

1.5 关键文件说明

入口文件

文件作用
src/extension.ts扩展入口,注册命令、视图、初始化管理器
src/worker/worker.tsWorker 入口,处理 IPC 消息,路由到驱动

核心管理器

文件作用
src/core/connectionManager.ts连接 CRUD,项目级/全局存储协调
src/core/ipcManager.tsWorker 生命周期,请求/响应匹配,崩溃恢复
src/core/storage.tsJSON 文件存储 + SecretStorage
src/core/scanner.ts协调多个 Parser 执行扫描

类型定义

文件作用
src/shared/types.ts所有接口定义:DbConnection, WorkerRequest, QueryResult 等
src/shared/constants.ts常量:默认端口、命令 ID、视图 ID

1.6 构建与开发

构建命令

# 生产构建
npm run compile

# 开发监听
npm run watch

# 构建 Webview
npm run build:webview

# 运行测试
npm run test

# 测试监听
npm run test:watch

# Lint
npm run lint

# 打包扩展
vsce package

开发流程

graph TD
    A["克隆项目"] --> B["npm install"]
    B --> C["npm run compile"]
    C --> D["cd webview-ui && npm install"]
    D --> E["npm run build"]
    E --> F["F5 启动调试"]
    
    F --> G["修改代码"]
    G --> H{"Webview 变更?"}
    H -->|是| I["rebuild webview"]
    H -->|否| J["自动重编译"]
    I --> K["刷新 Webview"]
    J --> K
    K --> G

调试配置

.vscode/launch.json

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run Extension",
      "type": "extensionHost",
      "request": "launch",
      "runtimeExecutable": "${execPath}",
      "args": ["--extensionDevelopmentPath=${workspaceFolder}"]
    }
  ]
}

1.7 小结

本章介绍了 VSDB 的:

  • 定位:VSCode 内的轻量数据库客户端
  • 功能:连接管理、结构浏览、SQL 编辑、数据操作
  • 技术选型:TypeScript + 双进程架构 + React Webview
  • 项目结构:核心模块、扫描引擎、Worker、UI 层

下一章将深入介绍核心概念和数据模型。

第二章:核心概念

本章深入介绍 VSDB 的核心数据模型、类型定义和关键概念。

2.1 连接模型

DbConnection 接口

export interface DbConnection {
  id: string;                    // UUID,唯一标识
  name: string;                  // 用户自定义名称,如 "dev-mysql"
  type: 'mysql' | 'postgresql';  // 数据库类型
  host: string;                  // 主机地址
  port: number;                  // 端口号
  username: string;              // 用户名
  password: string;              // 密码(存储时加密)
  database?: string;             // 默认数据库
  options?: Record<string, any>; // 扩展选项(SSL、charset 等)
  group?: string;                // 分组名称
  color?: string;                // 颜色标记
  scope: 'project' | 'global';   // 存储范围
}

连接范围

graph TD
    subgraph "项目级 scope: project"
        P1[".vsdb/connections.json"]
        P2["仅当前项目可见"]
        P3["团队可共享(git)"]
    end
    
    subgraph "全局级 scope: global"
        G1["~/.vsdb/connections.json"]
        G2["所有项目可见"]
        G3["个人私有"]
    end
    
    P1 --> P2
    P2 --> P3
    
    G1 --> G2
    G2 --> G3
范围存储位置可见性用途
project.vsdb/connections.json当前项目团队共享开发数据库
global~/.vsdb/connections.json所有项目个人私有数据库

连接状态

export interface ConnectionState {
  connectionId: string;
  status: 'disconnected' | 'connecting' | 'connected' | 'error';
  error?: string;
  activeDatabase?: string;
  lastQuery?: string;
  lastQueryTime?: number;
}

状态流转:

stateDiagram-v2
    [*] --> disconnected
    disconnected --> connecting: 用户点击连接
    connecting --> connected: 连接成功
    connecting --> error: 连接失败
    connected --> disconnected: 用户断开
    error --> connecting: 重试
    connected --> error: 连接丢失

2.2 扫描模型

ScannedConnection 接口

export interface ScannedConnection {
  name: string;                  // 生成的连接名称
  type: 'mysql' | 'postgresql';
  host: string;
  port: number;
  username: string;
  password: string;
  database?: string;
  source: 'env' | 'docker-compose' | 'framework';
  sourceFile: string;            // 来源文件路径
  confidence: 'high' | 'medium' | 'low';
}

置信度分级

置信度来源特点
highDATABASE_URL, docker-compose信息完整,可直接使用
medium框架配置解析信息可能不完整
low部分 DB_* 变量缺少关键信息(如密码)

ScannerResult 接口

export interface ScannerResult {
  connections: ScannedConnection[];  // 发现的连接
  errors: Array<{ file: string; error: string }>;  // 解析错误
  scannedFiles: string[];            // 扫描过的文件列表
}

2.3 IPC 协议模型

WorkerRequest 接口

export interface WorkerRequest {
  id: string;           // 请求 UUID
  type: 'connect' | 'disconnect' | 'query' | 
        'streamQuery' | 'schema' | 'cancel' | 'ping' | 'shutdown';
  connectionId: string;
  payload: {
    config?: DbConnection;
    sql?: string;
    params?: unknown[];
    schemaType?: 'databases' | 'tables' | 'columns' | 
                 'views' | 'procedures' | 'indexes' | 
                 'constraints' | 'triggers' | 'ddl';
    database?: string;
    table?: string;
  };
}

WorkerResponse 接口

export interface WorkerResponse {
  id: string;           // 匹配请求 UUID
  type: 'result' | 'stream' | 'streamEnd' | 'error' | 'pong';
  data?: QueryResult | SchemaResult;
  error?: {
    code: string;
    message: string;
    errorClass?: 'connection' | 'syntax' | 'timeout' | 'permission' | 'unknown';
    retryable?: boolean;
  };
}

StreamChunk 接口(流式传输)

export interface StreamChunk {
  requestId: string;          // 匹配请求 UUID
  chunkIndex: number;         // 分块索引
  rows: Record<string, unknown>[];  // 数据行
  totalRows?: number;         // 总行数(仅最后一块)
}

流式传输示意:

sequenceDiagram
    participant Host as Extension Host
    participant Worker as Worker Process
    
    Host->>Worker: streamQuery (id=001)
    Worker-->>Host: StreamChunk (index=0, rows=[...1000])
    Worker-->>Host: StreamChunk (index=1, rows=[...1000])
    Worker-->>Host: StreamChunk (index=2, rows=[...500], totalRows=2500)
    Worker-->>Host: streamEnd (id=001)

2.4 查询结果模型

QueryResult 接口

export interface QueryResult {
  columns: string[];                // 列名数组
  rows: Record<string, unknown>[];  // 数据行数组
  rowCount: number;                 // 行数
  affectedRows?: number;            // 影响行数(INSERT/UPDATE/DELETE)
  executionTime: number;            // 执行时间(毫秒)
}

SchemaResult 接口

export interface SchemaResult {
  type: 'databases' | 'tables' | 'columns' | 'views' | 
        'procedures' | 'indexes' | 'constraints' | 'triggers' | 'ddl';
  data: unknown[];
}

2.5 Schema 信息模型

表信息

export interface TableInfo {
  name: string;
  schema?: string;            // PostgreSQL schema
  columns: ColumnInfo[];
  indexes: IndexInfo[];
  rowCount?: number;
}

列信息

export interface ColumnInfo {
  name: string;
  type: string;               // 如 "varchar(255)", "int"
  nullable: boolean;
  defaultValue?: string;
  isPrimaryKey: boolean;
  isAutoIncrement: boolean;
}

索引信息

export interface IndexInfo {
  name: string;
  columns: string[];          // 索引包含的列
  isUnique: boolean;
  isPrimary: boolean;
}

Schema 结构示例

graph TD
    A["数据库"] --> B["表: users"]
    B --> C["列"]
    C --> C1["id: int, PK, auto_increment"]
    C --> C2["name: varchar(255), NOT NULL"]
    C --> C3["email: varchar(255), NULL"]
    
    B --> D["索引"]
    D --> D1["PRIMARY (id)"]
    D --> D2["idx_email (email), UNIQUE"]
    
    B --> E["约束"]
    E --> E1["PRIMARY KEY (id)"]
    E --> E2["UNIQUE (email)"]

2.6 查询历史模型

export interface QueryHistoryItem {
  id: string;              // UUID
  sql: string;             // SQL 语句
  connectionId: string;    // 执行连接
  connectionName: string;  // 连接名称
  executedAt: Date;        // 执行时间
  rowCount?: number;       // 结果行数
  executionTime?: number;  // 执行时间(毫秒)
  pinned?: boolean;        // 固定标记(不被自动清理)
}

历史记录管理策略:

graph TD
    A["执行 SQL"] --> B["记录到历史"]
    B --> C["检查上限"]
    C -->|"超过 100 条"| D["清理未固定的旧记录"]
    C -->|"未超过"| E["保持"]
    D --> F["保留固定记录"]

2.7 书签模型

Bookmark 接口

export interface Bookmark {
  id: string;
  type: 'connection' | 'database' | 'table' | 'query';
  name: string;            // 显示名称
  target: {
    connectionId?: string;
    database?: string;
    table?: string;
    sql?: string;
  };
  createdAt: Date;
  groupId?: string;        // 所属分组 ID
}

BookmarkGroup 接口

export interface BookmarkGroup {
  id: string;
  name: string;
  color?: string;          // 颜色标识
}

书签类型结构:

graph TD
    A["Bookmark"] --> B["connection"]
    A --> C["database"]
    A --> D["table"]
    A --> E["query"]
    
    B --> B1["target: connectionId"]
    C --> C1["target: connectionId + database"]
    D --> D1["target: connectionId + database + table"]
    E --> E1["target: connectionId + sql"]

2.8 搜索模型

SearchResult 接口

export interface SearchResult {
  type: 'table' | 'column' | 'data';
  connectionId: string;
  connectionName: string;
  database: string;
  name: string;            // 表名/列名
  detail?: string;         // 列类型、表名等详细信息
}

SearchOptions 接口

export interface SearchOptions {
  searchTables?: boolean;      // 搜索表名(默认 true)
  searchColumns?: boolean;     // 搜索列名(默认 true)
  searchData?: boolean;        // 搜索数据内容(默认 false)
  connectionIds?: string[];    // 限制特定连接
  tables?: string[];           // 数据搜索范围
  caseSensitive?: boolean;     // 大小写敏感
}

Schema 缓存

export interface SchemaCacheEntry {
  tables: TableInfo[];
  columns: ColumnInfo[];
  cachedAt: Date;
}

缓存策略:

graph TD
    A["搜索请求"] --> B{"缓存存在?"}
    B -->|是| C["使用缓存"]
    B -->|否| D["查询 Worker"]
    D --> E["存入缓存"]
    E --> C
    C --> F["返回结果"]

2.9 小结

本章介绍了 VSDB 的核心概念:

概念用途
DbConnection数据库连接配置
ConnectionState连接状态跟踪
ScannedConnection扫描发现的连接
WorkerRequest/ResponseIPC 通信协议
StreamChunk大数据流式传输
QueryResult查询结果封装
TableInfo/ColumnInfoSchema 元信息
QueryHistoryItem查询历史记录
Bookmark书签收藏
SearchResult搜索结果

下一章将深入系统架构设计。

第三章:系统架构

本章详细介绍 VSDB 的双进程架构设计、分层结构和各模块职责。

3.1 整体架构

VSDB 采用双进程架构,将数据库操作隔离在独立的 Worker 进程中。

graph TD
    subgraph "VSCode Extension Host (主进程)"
        A["extension.ts<br/>入口与命令注册"]
        B["核心管理层"]
        C["UI 层"]
        
        subgraph "核心管理层"
            B1["ConnectionManager"]
            B2["IpcManager"]
            B3["Storage"]
            B4["ScannerEngine"]
            B5["HistoryManager"]
            B6["BookmarkManager"]
            B7["SearchEngine"]
        end
        
        subgraph "UI 层"
            C1["TreeView<br/>(连接树)"]
            C2["Webview Panels<br/>(React 应用)"]
        end
    end
    
    subgraph "Worker Process (子进程)"
        D["worker.ts<br/>Worker 入口"]
        E["QueryQueue<br/>查询队列"]
        
        subgraph "数据库驱动层"
            F1["MySqlDriver"]
            F2["PostgreSqlDriver"]
        end
        
        subgraph "Schema Inspector"
            G1["MySqlSchema"]
            G2["PgSchema"]
        end
    end
    
    A --> B
    B --> C
    B2 -->|"IPC (child_process)"| D
    D --> E
    E --> F1
    E --> F2
    F1 --> G1
    F2 --> G2

3.2 双进程架构原理

为什么选择双进程

graph TD
    subgraph "单进程架构的问题"
        A1["数据库操作"] --> A2["阻塞 Extension Host"]
        A2 --> A3["VSCode 卡顿"]
        A4["驱动异常"] --> A5["扩展崩溃"]
        A5 --> A6["需要重启 VSCode"]
    end
    
    subgraph "双进程架构的优势"
        B1["数据库操作"] --> B2["Worker 进程"]
        B2 --> B3["Extension Host 不受影响"]
        B4["Worker 崩溃"] --> B5["自动重启恢复"]
        B5 --> B6["用户体验连续"]
    end
问题单进程双进程
复杂查询阻塞阻塞 VSCodeWorker 执行,UI 正常
驱动异常扩展崩溃Worker 崩溃自动恢复
内存泄漏影响 VSCodeWorker 可重启清理
长时间操作UI 无响应异步 IPC 通信

进程通信方式

VSDB 使用 Node.js child_process.fork() 创建 Worker:

// IpcManager.ts
this.worker = fork(this.workerPath, [], {
  stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
});

IPC 通信特点:

  • 原生支持:无需额外依赖
  • JSON 序列化:消息自动序列化/反序列化
  • 双向通信:主进程和 Worker 都可发送消息

3.3 分层架构

graph TB
    subgraph "UI 层"
        UI1["TreeView"]
        UI2["Webview Panel"]
    end
    
    subgraph "管理层"
        M1["ConnectionManager"]
        M2["IpcManager"]
        M3["ScannerEngine"]
        M4["HistoryManager"]
        M5["BookmarkManager"]
        M6["SearchEngine"]
    end
    
    subgraph "Worker 层"
        W1["QueryQueue"]
        W2["Driver"]
        W3["SchemaInspector"]
    end
    
    subgraph "存储层"
        S1["JSON Storage"]
        S2["SecretStorage"]
        S3["Schema Cache"]
    end
    
    UI1 --> M1
    UI1 --> M2
    UI2 --> M2
    
    M1 --> S1
    M1 --> S2
    M3 --> S1
    
    M2 -->|"IPC"| W1
    W1 --> W2
    W2 --> W3
    
    M6 --> S3

各层职责

层级模块职责
UI 层TreeView连接树展示、节点交互
WebviewSQL 编辑器、数据网格、表单
管理层ConnectionManager连接 CRUD、范围管理
IpcManagerWorker 生命周期、消息路由
ScannerEngine协调配置扫描
HistoryManager查询历史存储
BookmarkManager书签管理
SearchEngine全局搜索协调
Worker 层QueryQueue并发控制、超时管理
Driver数据库连接、查询执行
SchemaInspector元信息查询
存储层JSON Storage连接配置持久化
SecretStorage密码加密存储
Schema Cache搜索缓存

3.4 Extension Host(主进程)

入口模块

extension.ts 是扩展入口,负责:

// 主要职责
export function activate(context: vscode.ExtensionContext) {
  // 1. 初始化管理器
  const storage = new Storage(context.globalState, context.secrets);
  const connectionManager = new ConnectionManager(storage, projectRoot);
  const ipcManager = new IpcManager({ ... });
  
  // 2. 初始化扫描引擎
  const scannerEngine = new ScannerEngine();
  
  // 3. 初始化辅助管理器
  const historyManager = new HistoryManager(context.globalState);
  const bookmarkManager = new BookmarkManager(context.globalState);
  const searchEngine = new SearchEngine(ipcManager, connectionManager);
  
  // 4. 初始化 TreeView
  const treeProvider = new TreeProvider(connectionManager, ipcManager, ...);
  const treeView = vscode.window.createTreeView(VIEWS.CONNECTIONS, {
    treeDataProvider: treeProvider,
  });
  
  // 5. 注册命令
  registerCommands(context);
  
  // 6. 自动扫描(如果有工作区)
  autoScanProject(workspaceRoot);
}

命令注册

VSDB 注册两类命令:

graph TD
    subgraph "全局命令"
        C1["vsdb.addConnection"]
        C2["vsdb.scanProject"]
        C3["vsdb.newQuery"]
        C4["vsdb.search"]
    end
    
    subgraph "树节点命令"
        T1["vsdb.tree.connect"]
        T2["vsdb.tree.disconnect"]
        T3["vsdb.tree.newQuery"]
        T4["vsdb.viewTableStructure"]
        T5["vsdb.exportData"]
    end
    
    C1 --> F1["打开连接表单"]
    C2 --> F2["扫描项目配置"]
    C3 --> F3["打开 SQL 编辑器"]
    C4 --> F4["搜索表/列"]

3.5 Worker Process(子进程)

Worker 入口

worker.ts 是 Worker 进程入口:

// Worker 生命周期
process.on('message', (request: WorkerRequest) => {
  handleMessage(request);  // 处理请求
});

process.send?.({ type: 'ready' });  // 通知主进程就绪

process.on('disconnect', () => {
  shutdown();  // 父进程断开时清理
});

process.on('SIGTERM', () => {
  shutdown();  // 信号终止时清理
});

请求处理流程

sequenceDiagram
    participant Host as Extension Host
    participant Worker as Worker Process
    participant Queue as QueryQueue
    participant Driver as DatabaseDriver
    
    Host->>Worker: WorkerRequest (type=query)
    Worker->>Queue: enqueue(request)
    Queue->>Driver: execute(sql)
    Driver-->>Queue: QueryResult
    Queue-->>Worker: WorkerResponse
    Worker-->>Host: WorkerResponse

查询队列

QueryQueue 实现并发控制和超时管理:

class QueryQueue {
  private queue = new Map<string, PendingQuery>();
  private maxConcurrency = 5;
  private defaultTimeout = 30000;
  
  enqueue(request: WorkerRequest, handler: () => Promise<WorkerResponse>) {
    // 1. 检查并发限制
    // 2. 设置超时定时器
    // 3. 执行查询
    // 4. 返回结果或超时错误
  }
  
  cancel(requestId: string) {
    // 取消正在执行的查询
  }
}

3.6 数据流分析

连接建立流程

sequenceDiagram
    participant User as 用户
    participant Tree as TreeView
    participant CM as ConnectionManager
    participant IPC as IpcManager
    participant Worker as Worker Process
    participant Driver as MySqlDriver
    
    User->>Tree: 点击连接节点
    Tree->>IPC: sendRequest(connect)
    IPC->>Worker: WorkerRequest(type=connect)
    Worker->>CM: 检查连接配置
    Worker->>Driver: driver.connect(config)
    Driver-->>Worker: 连接成功
    Worker-->>IPC: WorkerResponse(type=result)
    IPC-->>Tree: 更新连接状态
    Tree-->>User: 显示连接图标

查询执行流程

sequenceDiagram
    participant User as 用户
    participant Webview as SQL Editor
    participant IPC as IpcManager
    participant Worker as Worker Process
    participant Queue as QueryQueue
    participant Driver as Driver
    
    User->>Webview: 输入 SQL,点击执行
    Webview->>IPC: postMessage(executeQuery)
    IPC->>Worker: WorkerRequest(type=query)
    Worker->>Queue: enqueue
    Queue->>Driver: driver.query(sql)
    Driver-->>Queue: QueryResult
    Queue-->>Worker: WorkerResponse
    Worker-->>IPC: WorkerResponse
    IPC-->>Webview: WorkerResponse
    Webview-->>User: 显示结果表格

流式查询流程(大数据)

sequenceDiagram
    participant User as 用户
    participant Webview as SQL Editor
    participant IPC as IpcManager
    participant Worker as Worker Process
    participant Driver as Driver
    
    User->>Webview: 执行大查询
    Webview->>IPC: sendRequest(streamQuery)
    IPC->>Worker: WorkerRequest(type=streamQuery)
    Worker->>Driver: driver.streamQuery(sql)
    
    loop 每 1000 行
        Driver-->>Worker: chunk
        Worker-->>IPC: StreamChunk
        IPC-->>Webview: StreamChunk
        Webview-->>User: 渲染数据块
    end
    
    Driver-->>Worker: end
    Worker-->>IPC: streamEnd
    IPC-->>Webview: streamEnd
    Webview-->>User: 完成

3.7 错误处理架构

Worker 崩溃恢复

stateDiagram-v2
    [*] --> Running
    Running --> Crashed: 进程退出
    Crashed --> Restarting: 重启尝试 < max
    Restarting --> Running: 重启成功
    Restarting --> Failed: 重试次数超限
    Failed --> [*]
    
    Running --> Shutdown: 正常关闭
    Shutdown --> [*]

恢复策略:

// IpcManager.ts
worker.on('exit', (code, signal) => {
  if (!isShuttingDown && restartOnCrash && restartAttempts < maxRestartAttempts) {
    restartAttempts++;
    const delay = Math.min(500 * Math.pow(2, restartAttempts - 1), 5000);
    
    setTimeout(() => {
      this.start();                  // 重启 Worker
      this.reconnectActiveConnections();  // 恢复连接
    }, delay);
  }
});

错误分类与处理

// Worker 错误分类
type QueryErrorClass = 'connection' | 'syntax' | 'timeout' | 'permission' | 'unknown';

function classifyQueryError(error: unknown): ClassifiedError {
  const message = error instanceof Error ? error.message : String(error);
  
  if (message.includes('ECONNREFUSED')) {
    return { errorClass: 'connection', retryable: true };
  }
  if (message.includes('syntax error')) {
    return { errorClass: 'syntax', retryable: false };
  }
  if (message.includes('timeout')) {
    return { errorClass: 'timeout', retryable: true };
  }
  // ...
}
错误类典型原因retryable用户提示
connection网络问题、服务未启动"连接失败,请检查网络"
syntaxSQL 语法错误"SQL 语法错误"
timeout查询超时"查询超时,可重试"
permission权限不足"权限不足"
unknown其他错误"未知错误"

3.8 性能考量

IPC 消息大小限制

IPC 消息通过 JSON 序列化传输,大消息会影响性能:

// 流式传输策略
const chunkSize = 1000;  // 每块最多 1000 行

for await (const chunk of driver.streamQuery(sql)) {
  send({
    requestId,
    chunkIndex,
    rows: chunk.rows,  // 限制在 chunkSize
  });
}

内存管理

// Worker 内存监控
const memoryLimitMb = 512;

setInterval(() => {
  const usage = process.memoryUsage();
  const heapUsedMb = usage.heapUsed / (1024 * 1024);
  
  if (heapUsedMb > memoryLimitMb) {
    send({ type: 'error', error: { code: 'WORKER_MEMORY_EXCEEDED' } });
  }
}, 10000);

心跳机制

// 防止僵尸进程
const heartbeatInterval = 15000;  // 15秒发送 ping
const maxMissedHeartbeats = 2;     // 2次未响应视为崩溃

setInterval(() => {
  worker.send({ type: 'ping' });
  missedHeartbeats++;
  
  if (missedHeartbeats >= maxMissedHeartbeats) {
    worker.kill('SIGKILL');  // 强制终止
  }
}, heartbeatInterval);

3.9 小结

本章介绍了 VSDB 的系统架构:

  • 双进程架构:Extension Host + Worker Process
  • 分层设计:UI 层 → 管理层 → Worker 层 → 存储层
  • IPC 通信:JSON 序列化,请求/响应匹配
  • 错误处理:崩溃恢复,错误分类
  • 性能优化:流式传输,内存监控,心跳机制

下一章将深入 IPC 通信机制的实现细节。

第四章:IPC通信机制

本章深入介绍 VSDB 的 IPC 通信实现,包括消息协议、请求匹配、流式传输和崩溃恢复。

4.1 IPC 概述

IPC(Inter-Process Communication)是双进程架构的核心,负责 Extension Host 和 Worker Process 之间的消息传递。

graph LR
    subgraph "Extension Host"
        A["IpcManager"]
        B["sendRequest()"]
        C["handleMessage()"]
    end
    
    subgraph "Worker Process"
        D["worker.ts"]
        E["handleMessage()"]
        F["send()"]
    end
    
    B -->|"JSON over IPC"| D
    D --> E
    E --> F
    F -->|"JSON over IPC"| A
    A --> C

Node.js child_process IPC

VSDB 使用 child_process.fork() 创建 Worker:

// fork 创建子进程,自动建立 IPC 通道
this.worker = fork(this.workerPath, [], {
  stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
});

IPC 特性:

  • 自动序列化process.send() 自动 JSON 序列化
  • 事件驱动process.on('message') 接收消息
  • 双向通信:主进程和子进程都可发送/接收

4.2 消息协议设计

请求类型

interface WorkerRequest {
  id: string;           // UUID,用于匹配响应
  type: RequestType;    // 请求类型
  connectionId: string; // 目标连接
  payload: RequestPayload;
}

type RequestType = 
  | 'connect'      // 建立连接
  | 'disconnect'   // 断开连接
  | 'query'        // 执行查询
  | 'streamQuery'  // 流式查询
  | 'schema'       // 查询 Schema
  | 'cancel'       // 取消查询
  | 'ping'         // 心跳检测
  | 'shutdown';    // 关闭 Worker

interface RequestPayload {
  config?: DbConnection;     // 连接配置
  sql?: string;              // SQL 语句
  params?: unknown[];        // 查询参数
  schemaType?: SchemaType;   // Schema 类型
  database?: string;         // 数据库名
  table?: string;            // 表名
}

响应类型

interface WorkerResponse {
  id: string;           // 匹配请求 ID
  type: ResponseType;   // 响应类型
  data?: QueryResult | SchemaResult;
  error?: WorkerError;
}

type ResponseType = 
  | 'result'       // 查询结果
  | 'streamEnd'    // 流式传输结束
  | 'error'        // 错误响应
  | 'pong';        // 心跳响应

interface WorkerError {
  code: string;
  message: string;
  errorClass?: ErrorClass;
  retryable?: boolean;
}

流式分块

interface StreamChunk {
  requestId: string;          // 匹配请求 ID
  chunkIndex: number;         // 分块索引
  rows: Record<string, unknown>[];  // 数据行
  totalRows?: number;         // 总行数(仅最后一块)
}

4.3 IpcManager 实现

核心结构

class IpcManager {
  private workerPath: string;
  private worker: ChildProcess | null = null;
  private pendingRequests = new Map<string, PendingRequest>();
  
  // 配置参数
  private restartOnCrash: boolean;
  private maxRestartAttempts: number;
  private requestTimeout: number;
  private heartbeatInterval: number;
  
  // 心跳监控
  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
  private missedHeartbeats = 0;
  
  // 连接跟踪(崩溃恢复用)
  private activeConnections = new Map<string, DbConnection>();
}

PendingRequest 结构

interface PendingRequest {
  resolve: (response: WorkerResponse) => void;
  reject: (error: Error) => void;
  streamChunks: StreamChunk[];  // 流式分块累积
  isStream: boolean;            // 是否流式请求
  timer: ReturnType<typeof setTimeout>;  // 超时定时器
}

启动 Worker

start(): void {
  if (this.worker && !this.worker.killed) {
    return;  // 已运行
  }
  
  this.isShuttingDown = false;
  
  // 创建 Worker 进程
  this.worker = fork(this.workerPath, [], {
    stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
  });
  
  // 消息处理
  this.worker.on('message', (message: WorkerResponse | StreamChunk) => {
    this.handleMessage(message);
  });
  
  // 进程退出处理
  this.worker.on('exit', (code, signal) => {
    this.handleWorkerExit(code, signal);
  });
  
  // 启动心跳
  this.startHeartbeat();
}

发送请求

sendRequest(request: Omit<WorkerRequest, 'id'>): Promise<WorkerResponse> {
  return new Promise((resolve, reject) => {
    if (!this.worker || this.worker.killed) {
      reject(new Error('Worker process is not running'));
      return;
    }
    
    // 生成完整请求
    const fullRequest: WorkerRequest = {
      ...request,
      id: randomUUID(),
    };
    
    // 创建 PendingRequest
    const pending: PendingRequest = {
      resolve,
      reject,
      streamChunks: [],
      isStream: request.type === 'streamQuery',
      timer: setTimeout(() => {
        this.pendingRequests.delete(fullRequest.id);
        reject(new Error(`Request timed out`));
      }, this.requestTimeout),
    };
    
    this.pendingRequests.set(fullRequest.id, pending);
    
    // 跟踪活动连接
    if (request.type === 'connect' && request.payload.config) {
      this.activeConnections.set(request.connectionId, request.payload.config);
    }
    
    // 发送请求
    this.worker.send(fullRequest);
  });
}

处理响应

private handleMessage(message: WorkerResponse | StreamChunk): void {
  // 重置心跳计数
  this.missedHeartbeats = 0;
  
  // 心跳响应
  if (message.type === 'pong') {
    return;
  }
  
  // 流式分块
  if ('requestId' in message) {
    this.handleStreamChunk(message as StreamChunk);
    return;
  }
  
  // 普通响应
  const response = message as WorkerResponse;
  const pending = this.pendingRequests.get(response.id);
  
  if (!pending) {
    return;  // 无匹配的请求(可能已超时)
  }
  
  clearTimeout(pending.timer);
  
  // 流式结束
  if (pending.isStream && response.type === 'streamEnd') {
    this.assembleStreamResponse(pending, response);
  } else {
    this.pendingRequests.delete(response.id);
    pending.resolve(response);
  }
}

流式响应组装

private assembleStreamResponse(
  pending: PendingRequest, 
  response: WorkerResponse
): void {
  // 合并所有分块
  const assembledRows: Record<string, unknown>[] = [];
  let columns: string[] = [];
  let totalRows = 0;
  
  for (const chunk of pending.streamChunks) {
    assembledRows.push(...chunk.rows);
    if (chunk.totalRows !== undefined) {
      totalRows = chunk.totalRows;
    }
  }
  
  // 提取列名
  if (assembledRows.length > 0) {
    columns = Object.keys(assembledRows[0]);
  }
  
  // 构造 QueryResult
  const queryResult: QueryResult = {
    columns,
    rows: assembledRows,
    rowCount: assembledRows.length,
    executionTime: 0,
    ...(totalRows > 0 ? { affectedRows: totalRows } : {}),
  };
  
  this.pendingRequests.delete(response.id);
  pending.resolve({
    id: response.id,
    type: 'result',
    data: queryResult,
  });
}

4.4 流式传输详解

为什么需要流式传输

graph TD
    A["查询返回 100 万行"] --> B{"传输方式"}
    
    B -->|普通查询| C["单次 IPC 消息"]
    C --> D["消息过大"]
    D --> E["IPC 阻塞"]
    E --> F["UI 卡顿"]
    
    B -->|流式查询| G["分块传输"]
    G --> H["每块 1000 行"]
    H --> I["渐进渲染"]
    I --> J["UI 流畅"]

Worker 端流式实现

async function handleStreamQuery(request: WorkerRequest): Promise<void> {
  const entry = connections.get(request.connectionId);
  const { driver } = entry;
  const sql = request.payload.sql;
  
  activeStreams.add(request.id);  // 注册活动流
  
  queryQueue.enqueue(request, async () => {
    try {
      const stream = driver.streamQuery(sql);
      const chunkSize = 1000;
      let chunkIndex = 0;
      
      for await (const chunk of stream) {
        // 检查是否被取消
        if (!activeStreams.has(request.id)) {
          return { id: request.id, type: 'streamEnd' };
        }
        
        // 发送分块
        send({
          requestId: request.id,
          chunkIndex,
          rows: chunk.rows,
          totalRows: chunk.totalRows,
        });
        
        chunkIndex++;
      }
      
      // 发送结束标记
      return { id: request.id, type: 'streamEnd' };
    } finally {
      activeStreams.delete(request.id);
    }
  });
}

Driver 端流式实现

// mysql.ts
async *streamQuery(
  sql: string, 
  params?: any[], 
  chunkSize = 1000
): AsyncGenerator<StreamChunkData> {
  const streamConn = await mysql.createConnection(config);
  
  try {
    const [result] = await streamConn.execute(sql, params);
    
    if (!Array.isArray(result)) {
      yield { chunkIndex: 0, rows: [], totalRows: 0 };
      return;
    }
    
    const allRows = result;
    const totalRows = allRows.length;
    
    // 分块 yield
    for (let offset = 0; offset < allRows.length; offset += chunkSize) {
      const chunk = allRows.slice(offset, offset + chunkSize);
      const isLast = offset + chunkSize >= allRows.length;
      
      yield {
        chunkIndex: Math.floor(offset / chunkSize),
        rows: chunk,
        ...(isLast ? { totalRows } : {}),
      };
    }
  } finally {
    await streamConn.end();
  }
}

分块超时控制

// IpcManager 设置分块超时
private setupStreamChunkTimeout(requestId: string): void {
  const timer = setTimeout(() => {
    const pending = this.pendingRequests.get(requestId);
    if (pending) {
      this.pendingRequests.delete(requestId);
      clearTimeout(pending.timer);
      pending.reject(new Error('STREAM_TIMEOUT'));
    }
  }, this.streamChunkTimeout);
  
  this.streamChunkTimers.set(requestId, timer);
}

// 收到分块时重置
private resetStreamChunkTimer(requestId: string): void {
  // 清除旧定时器
  const existing = this.streamChunkTimers.get(requestId);
  if (existing) clearTimeout(existing);
  
  // 设置新定时器
  this.setupStreamChunkTimeout(requestId);
}

4.5 心跳机制

心跳设计

sequenceDiagram
    participant Host as IpcManager
    participant Worker as Worker Process
    
    loop 每 15 秒
        Host->>Worker: ping
        Worker-->>Host: pong
        Host->>Host: missedHeartbeats = 0
    end
    
    Note over Host: Worker 未响应
    Host->>Host: missedHeartbeats++
    Host->>Host: missedHeartbeats++
    Host->>Worker: SIGKILL (强制终止)
    Host->>Host: handleWorkerExit()

实现

private startHeartbeat(): void {
  this.stopHeartbeat();
  this.missedHeartbeats = 0;
  
  this.heartbeatTimer = setInterval(() => {
    if (!this.worker || this.worker.killed) {
      this.stopHeartbeat();
      return;
    }
    
    // 发送 ping
    try {
      this.worker.send({ type: 'ping', id: 'heartbeat' });
    } catch {
      return;
    }
    
    // 增加计数
    this.missedHeartbeats++;
    if (this.missedHeartbeats >= this.maxMissedHeartbeats) {
      console.error('Worker heartbeat timeout');
      this.handleWorkerHang();
    }
  }, this.heartbeatInterval);
}

private handleWorkerHang(): void {
  if (this.worker && !this.worker.killed) {
    this.worker.kill('SIGKILL');  // 强制终止
  }
}

Worker 端心跳响应

// worker.ts
case 'ping':
  send({ id: 'heartbeat', type: 'pong' });
  break;

4.6 崩溃恢复

崩溃检测

worker.on('exit', (code, signal) => {
  console.error(`Worker exited: code=${code}, signal=${signal}`);
  this.stopHeartbeat();
  
  // 收集丢失的连接
  const lostConnectionIds = Array.from(this.activeConnections.keys());
  
  // 拒绝所有待处理请求
  this.rejectAllPending(`Worker exited unexpectedly`);
  
  this.worker = null;
  
  // 判断是否需要重启
  if (!this.isShuttingDown && this.restartOnCrash) {
    this.handleCrashRecovery(lostConnectionIds);
  }
});

重启策略

private handleCrashRecovery(lostConnectionIds: string[]): void {
  if (this.restartAttempts < this.maxRestartAttempts) {
    this.crashed = true;
    this.restartAttempts++;
    
    // 指数退避
    const delay = Math.min(
      500 * Math.pow(2, this.restartAttempts - 1), 
      5000
    );
    
    console.log(`Auto-restarting worker (attempt ${this.restartAttempts})`);
    
    // 通知回调
    this.callbacks.onWorkerCrash?.(this.restartAttempts);
    this.callbacks.onConnectionsLost?.(lostConnectionIds);
    
    setTimeout(() => {
      this.start();
      this.reconnectActiveConnections();
      this.callbacks.onWorkerRestarted?.();
    }, delay);
  }
}

连接恢复

private reconnectActiveConnections(): void {
  for (const [connectionId, config] of this.activeConnections) {
    console.log(`Reconnecting: ${connectionId}`);
    
    this.sendRequest({
      type: 'connect',
      connectionId,
      payload: { config },
    }).catch((err) => {
      console.error(`Failed to reconnect ${connectionId}: ${err.message}`);
    });
  }
}

重启时序

sequenceDiagram
    participant User as 用户
    participant IPC as IpcManager
    participant Worker1 as Worker (旧)
    participant Worker2 as Worker (新)
    
    Worker1-->>IPC: exit (crash)
    IPC->>User: onWorkerCrash callback
    IPC->>IPC: wait (指数退避)
    IPC->>Worker2: fork (新进程)
    Worker2-->>IPC: ready
    IPC->>Worker2: reconnect all activeConnections
    Worker2-->>IPC: connect results
    IPC->>User: onWorkerRestarted callback

4.7 优雅关闭

关闭流程

async shutdown(): Promise<void> {
  this.isShuttingDown = true;
  this.stopHeartbeat();
  
  // 等待待处理请求完成
  if (this.pendingRequests.size > 0 && this.worker) {
    // 发送关闭信号
    try {
      this.worker.send({ type: 'shutdown' });
    } catch {}
    
    // 等待请求排空
    const drainStart = Date.now();
    while (this.pendingRequests.size > 0 && 
           (Date.now() - drainStart) < this.gracefulShutdownTimeout) {
      await new Promise(resolve => setTimeout(resolve, 200));
    }
  }
  
  // 拒绝剩余请求
  this.rejectAllPending('IPCManager shutting down');
  
  // 终止进程
  if (this.worker && !this.worker.killed) {
    this.worker.kill('SIGTERM');
    
    // 等待退出或强制终止
    await new Promise(resolve => {
      const timeout = setTimeout(() => {
        if (!this.worker.killed) {
          this.worker.kill('SIGKILL');
        }
        resolve();
      }, this.gracefulShutdownTimeout);
      
      this.worker.on('exit', () => {
        clearTimeout(timeout);
        resolve();
      });
    });
  }
  
  this.worker = null;
  this.activeConnections.clear();
}

Worker 端关闭

// worker.ts
async function shutdown(): Promise<void> {
  stopMemoryMonitoring();
  queryQueue.dispose();
  
  // 断开所有连接
  for (const [id, entry] of connections) {
    try {
      await entry.driver.disconnect();
    } catch {}
  }
  
  connections.clear();
  activeStreams.clear();
}

process.on('disconnect', () => {
  shutdown().then(() => process.exit(0));
});

process.on('SIGTERM', () => {
  shutdown().then(() => process.exit(0));
});

4.8 Worker 就绪机制

就绪信号

// worker.ts - 启动时发送就绪信号
process.send?.({ type: 'ready' });

等待就绪

// IpcManager.ts
private workerReady: boolean = false;
private workerReadyResolve?: () => void;
private workerReadyPromise?: Promise<void>;

start(): void {
  this.workerReady = false;
  this.workerReadyPromise = new Promise<void>((resolve) => {
    this.workerReadyResolve = resolve;
  });
  
  this.worker = fork(this.workerPath, ...);
  
  this.worker.on('message', (message) => {
    if ((message as any).type === 'ready') {
      this.workerReady = true;
      this.workerReadyResolve?.();
      return;
    }
    this.handleMessage(message);
  });
}

async waitReady(timeoutMs = 5000): Promise<boolean> {
  if (this.workerReady) return true;
  
  const timeout = new Promise<false>((resolve) => {
    setTimeout(() => resolve(false), timeoutMs);
  });
  
  return Promise.race([
    this.workerReadyPromise!.then(() => true),
    timeout,
  ]);
}

4.9 配置参数

IpcManagerOptions

interface IpcManagerOptions {
  workerScriptPath: string;         // Worker 脚本路径
  
  restartOnCrash?: boolean;         // 崩溃时重启(默认 true)
  maxRestartAttempts?: number;      // 最大重启次数(默认 3)
  requestTimeout?: number;          // 请求超时(默认 60000ms)
  heartbeatInterval?: number;       // 心跳间隔(默认 15000ms)
  heartbeatTimeout?: number;        // 心跳超时(默认 30000ms)
  streamChunkTimeout?: number;      // 分块超时(默认 30000ms)
  gracefulShutdownTimeout?: number; // 关闭超时(默认 10000ms)
  
  callbacks?: IpcManagerCallbacks;  // 事件回调
}

默认值

const DEFAULT_REQUEST_TIMEOUT = 60000;      // 60 秒
const DEFAULT_MAX_RESTART_ATTEMPTS = 3;
const DEFAULT_HEARTBEAT_INTERVAL = 15000;   // 15 秒
const DEFAULT_HEARTBEAT_TIMEOUT = 30000;    // 30 秒
const DEFAULT_STREAM_CHUNK_TIMEOUT = 30000; // 30 秒
const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT = 10000; // 10 秒

4.10 小结

本章介绍了 VSDB 的 IPC 通信机制:

特性实现
消息协议WorkerRequest / WorkerResponse / StreamChunk
请求匹配UUID + pendingRequests Map
流式传输分块发送 + 组装响应
心跳机制ping/pong + missedHeartbeats 计数
崩溃恢复重启 + 重连 activeConnections
优雅关闭等待排空 + SIGTERM/SIGKILL
Worker 就绪ready 信号 + waitReady()

下一章将深入数据库驱动的实现细节。

第五章:数据库驱动实现

本章介绍 VSDB 的数据库驱动抽象层和 MySQL/PostgreSQL 驱动的具体实现。

5.1 驱动抽象层设计

驱动接口

VSDB 没有显式定义 Driver 接口,但所有驱动遵循相同的方法签名:

// 隐式驱动接口
interface DatabaseDriver {
  // 连接管理
  connect(config: DbConnection): Promise<void>;
  disconnect(): Promise<void>;
  isConnected(): boolean;
  
  // 查询执行
  query(sql: string, params?: unknown[]): Promise<QueryResult>;
  streamQuery(sql: string, params?: unknown[], chunkSize?: number): AsyncGenerator<StreamChunkData>;
  
  // 访问底层连接(用于 Schema 查询)
  getConnection(): Connection | Pool | null;
  getPool(): Pool | null;  // PostgreSQL
}

驱动选择策略

// worker.ts
function createDriver(config: DbConnection): DatabaseDriver {
  return config.type === 'mysql' 
    ? new MySqlDriver() 
    : new PostgreSqlDriver();
}
graph TD
    A["DbConnection.type"] --> B{"数据库类型"}
    B -->|"mysql"| C["MySqlDriver"]
    B -->|"postgresql"| D["PostgreSqlDriver"]
    
    C --> E["mysql2/promise"]
    D --> F["pg"]
    
    E --> G["MySQL 连接"]
    F --> H["PostgreSQL Pool"]

5.2 MySQL 驱动实现

MySqlDriver 类

import mysql from 'mysql2/promise';
import type { DbConnection, QueryResult } from '../../shared/types';

export class MySqlDriver {
  private connection: mysql.Connection | null = null;
  
  // 连接配置
  async connect(config: DbConnection): Promise<void> {
    if (this.connection) {
      await this.disconnect();  // 先断开旧连接
    }
    
    this.connection = await mysql.createConnection({
      host: config.host,
      port: config.port,
      user: config.username,
      password: config.password,
      database: config.database,
      connectTimeout: 10000,     // 10 秒连接超时
      ...config.options,         // 扩展选项(SSL、charset 等)
    });
  }
  
  // 断开连接
  async disconnect(): Promise<void> {
    if (this.connection) {
      try {
        await this.connection.end();
      } catch {
        // 连接可能已关闭,忽略错误
      }
      this.connection = null;
    }
  }
  
  // 检查连接状态
  isConnected(): boolean {
    return this.connection !== null;
  }
  
  // 获取底层连接
  getConnection(): mysql.Connection | null {
    return this.connection;
  }
}

查询执行

async query(sql: string, params?: any[]): Promise<QueryResult> {
  if (!this.connection) {
    throw new Error('MySQL: not connected');
  }
  
  const startTime = Date.now();
  const [result] = await this.connection.execute(sql, params);
  const executionTime = Date.now() - startTime;
  
  // 区分 SELECT 和 INSERT/UPDATE/DELETE
  if (result && typeof result === 'object' && 'affectedRows' in result) {
    // INSERT/UPDATE/DELETE 返回 ResultSetHeader
    return {
      columns: [],
      rows: [],
      rowCount: (result as any).affectedRows,
      affectedRows: (result as any).affectedRows,
      executionTime,
    };
  }
  
  // SELECT 返回 RowDataPacket[]
  const rows = Array.isArray(result) ? result as Record<string, unknown>[] : [];
  const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
  
  return {
    columns,
    rows,
    rowCount: rows.length,
    executionTime,
  };
}

流式查询

async *streamQuery(
  sql: string, 
  params?: any[], 
  chunkSize = 1000
): AsyncGenerator<{ chunkIndex: number; rows: Record<string, unknown>[]; totalRows?: number }> {
  if (!this.connection) {
    throw new Error('MySQL: not connected');
  }
  
  // 创建独立连接避免阻塞主连接
  const streamConn = await mysql.createConnection(
    (this.connection as any).connection?.config || {}
  );
  
  try {
    const [result] = await streamConn.execute(sql, params);
    
    if (!Array.isArray(result)) {
      yield { chunkIndex: 0, rows: [], totalRows: 0 };
      return;
    }
    
    const allRows = result;
    const totalRows = allRows.length;
    let chunkIndex = 0;
    
    // 分块 yield
    for (let offset = 0; offset < allRows.length; offset += chunkSize) {
      const chunk = allRows.slice(offset, offset + chunkSize);
      const isLast = offset + chunkSize >= allRows.length;
      
      yield {
        chunkIndex,
        rows: chunk,
        ...(isLast ? { totalRows } : {}),
      };
      chunkIndex++;
    }
  } finally {
    await streamConn.end();  // 确保关闭流连接
  }
}

mysql2 特性

VSDB 选择 mysql2 的原因:

特性mysql2mysql (旧版)
Promise 支持✓(原生)✗(需包装)
Prepare Statement
流式查询
性能更快较慢
维护状态活跃已停止

5.3 PostgreSQL 驱动实现

PostgreSqlDriver 类

import { Pool } from 'pg';
import type { DbConnection, QueryResult } from '../../shared/types';

export class PostgreSqlDriver {
  private pool: Pool | null = null;
  
  async connect(config: DbConnection): Promise<void> {
    if (this.pool) {
      await this.disconnect();
    }
    
    // PostgreSQL 使用连接池
    this.pool = new Pool({
      host: config.host,
      port: config.port,
      user: config.username,
      password: config.password,
      database: config.database || 'postgres',
      max: 5,                   // 最大连接数
      connectionTimeoutMillis: 10000,
      ...config.options,
    });
    
    // 测试连接
    try {
      await this.pool.query('SELECT 1');
    } catch (err) {
      this.pool.end();
      this.pool = null;
      throw err;
    }
  }
  
  async disconnect(): Promise<void> {
    if (this.pool) {
      await this.pool.end();
      this.pool = null;
    }
  }
  
  isConnected(): boolean {
    return this.pool !== null;
  }
  
  getPool(): Pool | null {
    return this.pool;
  }
}

查询执行

async query(sql: string, params?: any[]): Promise<QueryResult> {
  if (!this.pool) {
    throw new Error('PostgreSQL: not connected');
  }
  
  const startTime = Date.now();
  const result = await this.pool.query(sql, params);
  const executionTime = Date.now() - startTime;
  
  // PostgreSQL 结果格式不同于 MySQL
  if (result.command !== 'SELECT') {
    // INSERT/UPDATE/DELETE
    return {
      columns: [],
      rows: [],
      rowCount: result.rowCount || 0,
      affectedRows: result.rowCount || 0,
      executionTime,
    };
  }
  
  // SELECT
  const rows = result.rows as Record<string, unknown>[];
  const columns = result.fields?.map(f => f.name) || 
                  (rows.length > 0 ? Object.keys(rows[0]) : []);
  
  return {
    columns,
    rows,
    rowCount: rows.length,
    executionTime,
  };
}

流式查询

async *streamQuery(
  sql: string, 
  params?: any[], 
  chunkSize = 1000
): AsyncGenerator<StreamChunkData> {
  if (!this.pool) {
    throw new Error('PostgreSQL: not connected');
  }
  
  // 使用单一连接执行流式查询
  const client = await this.pool.connect();
  
  try {
    // 使用 cursor 实现真正的流式
    const cursor = client.query(new Cursor(sql, params));
    let chunkIndex = 0;
    
    while (true) {
      const rows = await cursor.read(chunkSize);
      if (rows.length === 0) break;
      
      yield {
        chunkIndex,
        rows: rows as Record<string, unknown>[],
        ...(rows.length < chunkSize ? { totalRows: chunkIndex * chunkSize + rows.length } : {}),
      };
      
      chunkIndex++;
    }
  } finally {
    client.release();  // 归还连接到池
  }
}

pg 库特性

VSDB 选择 pg 的原因:

特性pg其他库
纯 JavaScript部分 C++
连接池✓(内置)需额外配置
Promise 支持
Cursor 流式有限
TypeScript 类型✓(@types/pg)变化

5.4 连接池策略

MySQL vs PostgreSQL

graph TD
    subgraph "MySQL 连接策略"
        M1["单一连接"]
        M2["每次查询复用"]
        M3["流式查询使用独立连接"]
    end
    
    subgraph "PostgreSQL 连接策略"
        P1["连接池(max=5)"]
        P2["查询自动从池获取"]
        P3["流式查询使用独立客户端"]
    end
数据库连接模型原因
MySQL单一连接mysql2 默认不池化,简单够用
PostgreSQL连接池pg 内置池,PostgreSQL 连接开销大

连接池配置

// PostgreSQL 默认配置
const poolConfig = {
  max: 5,                        // 最大连接数
  connectionTimeoutMillis: 10000, // 连接超时
  idleTimeoutMillis: 10000,      // 空闲超时
};

5.5 Schema Inspector

MySqlSchema 实现

export class MySqlSchema {
  async getDatabases(conn: mysql.Connection): Promise<SchemaResult> {
    const [rows] = await conn.execute('SHOW DATABASES');
    return {
      type: 'databases',
      data: rows.map(r => ({ name: r.Database })),
    };
  }
  
  async getTables(conn: mysql.Connection, database: string): Promise<SchemaResult> {
    const [rows] = await conn.execute(
      `SHOW TABLES FROM ${mysql.escapeId(database)}`
    );
    const key = `Tables_in_${database}`;
    return {
      type: 'tables',
      data: rows.map(r => ({ name: r[key] })),
    };
  }
  
  async getColumns(conn: mysql.Connection, database: string, table: string): Promise<SchemaResult> {
    const [rows] = await conn.execute(
      `SHOW COLUMNS FROM ${mysql.escapeId(database)}.${mysql.escapeId(table)}`
    );
    return {
      type: 'columns',
      data: rows.map(r => ({
        name: r.Field,
        type: r.Type,
        nullable: r.Null === 'YES',
        defaultValue: r.Default,
        isPrimaryKey: r.Key === 'PRI',
        isAutoIncrement: r.Extra?.includes('auto_increment'),
      })),
    };
  }
  
  async getIndexes(conn: mysql.Connection, database: string, table: string): Promise<SchemaResult> {
    const [rows] = await conn.execute(
      `SHOW INDEX FROM ${mysql.escapeId(database)}.${mysql.escapeId(table)}`
    );
    // 合并同一索引的多行
    const indexMap = new Map();
    for (const r of rows) {
      if (!indexMap.has(r.Key_name)) {
        indexMap.set(r.Key_name, {
          name: r.Key_name,
          columns: [],
          isUnique: !r.Non_unique,
          isPrimary: r.Key_name === 'PRIMARY',
        });
      }
      indexMap.get(r.Key_name).columns.push(r.Column_name);
    }
    return {
      type: 'indexes',
      data: Array.from(indexMap.values()),
    };
  }
  
  async getConstraints(conn: mysql.Connection, database: string, table: string): Promise<SchemaResult> {
    const [rows] = await conn.execute(
      `SELECT * FROM information_schema.TABLE_CONSTRAINTS 
       WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?`,
      [database, table]
    );
    return {
      type: 'constraints',
      data: rows.map(r => ({
        name: r.CONSTRAINT_NAME,
        type: r.CONSTRAINT_TYPE,
      })),
    };
  }
  
  async getDDL(conn: mysql.Connection, database: string, table: string): Promise<SchemaResult> {
    const [rows] = await conn.execute(
      `SHOW CREATE TABLE ${mysql.escapeId(database)}.${mysql.escapeId(table)}`
    );
    return {
      type: 'ddl',
      data: [{ ddl: rows[0]['Create Table'] }],
    };
  }
}

PgSchema 实现

export class PgSchema {
  async getDatabases(pool: Pool): Promise<SchemaResult> {
    const result = await pool.query(
      `SELECT datname FROM pg_database WHERE datistemplate = false`
    );
    return {
      type: 'databases',
      data: result.rows.map(r => ({ name: r.datname })),
    };
  }
  
  async getTables(pool: Pool, schema: string): Promise<SchemaResult> {
    const result = await pool.query(
      `SELECT table_name FROM information_schema.tables 
       WHERE table_schema = $1 AND table_type = 'BASE TABLE'`,
      [schema]
    );
    return {
      type: 'tables',
      data: result.rows.map(r => ({ name: r.table_name, schema })),
    };
  }
  
  async getColumns(pool: Pool, schema: string, table: string): Promise<SchemaResult> {
    const result = await pool.query(
      `SELECT column_name, data_type, is_nullable, column_default,
              CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_pk
       FROM information_schema.columns c
       LEFT JOIN (
         SELECT kcu.column_name
         FROM information_schema.table_constraints tc
         JOIN information_schema.key_column_usage kcu 
           ON tc.constraint_name = kcu.constraint_name
         WHERE tc.constraint_type = 'PRIMARY KEY'
           AND tc.table_schema = $1 AND tc.table_name = $2
       ) pk ON c.column_name = pk.column_name
       WHERE c.table_schema = $1 AND c.table_name = $2`,
      [schema, table]
    );
    return {
      type: 'columns',
      data: result.rows.map(r => ({
        name: r.column_name,
        type: r.data_type,
        nullable: r.is_nullable === 'YES',
        defaultValue: r.column_default,
        isPrimaryKey: r.is_pk,
        isAutoIncrement: false,  // PostgreSQL 无原生 auto_increment
      })),
    };
  }
  
  async getDDL(pool: Pool, schema: string, table: string): Promise<SchemaResult> {
    // PostgreSQL DDL 需要拼接,无直接命令
    // 实际实现需要更复杂的逻辑
    const result = await pool.query(
      `SELECT 
        'CREATE TABLE ' || quote_ident($2) || ' (' ||
        string_agg(
          quote_ident(column_name) || ' ' || data_type ||
          CASE WHEN is_nullable = 'NO' THEN ' NOT NULL' ELSE '' END,
          ', '
        ) || ')' as ddl
       FROM information_schema.columns
       WHERE table_schema = $1 AND table_name = $2`,
      [schema, table]
    );
    return {
      type: 'ddl',
      data: [{ ddl: result.rows[0]?.ddl || '' }],
    };
  }
}

5.6 查询队列

QueryQueue 实现

class QueryQueue {
  private queue = new Map<string, PendingQuery>();
  private runningCount = 0;
  private maxConcurrency = 5;
  private defaultTimeout = 30000;
  
  enqueue(
    request: WorkerRequest, 
    handler: () => Promise<WorkerResponse>
  ): Promise<WorkerResponse> {
    return new Promise((resolve, reject) => {
      const pending: PendingQuery = {
        request,
        handler,
        resolve,
        reject,
        timer: setTimeout(() => {
          this.queue.delete(request.id);
          reject(new Error('Query timeout'));
        }, this.defaultTimeout),
      };
      
      this.queue.set(request.id, pending);
      this.processQueue();
    });
  }
  
  private processQueue(): void {
    while (this.runningCount < this.maxConcurrency && this.queue.size > 0) {
      const [id, pending] = Array.from(this.queue.entries())[0];
      this.queue.delete(id);
      this.runningCount++;
      
      pending.handler()
        .then(response => {
          clearTimeout(pending.timer);
          pending.resolve(response);
          this.runningCount--;
          this.processQueue();
        })
        .catch(error => {
          clearTimeout(pending.timer);
          pending.reject(error);
          this.runningCount--;
          this.processQueue();
        });
    }
  }
  
  cancel(requestId: string): void {
    const pending = this.queue.get(requestId);
    if (pending) {
      clearTimeout(pending.timer);
      pending.reject(new Error('Query cancelled'));
      this.queue.delete(requestId);
    }
  }
  
  dispose(): void {
    for (const [id, pending] of this.queue) {
      clearTimeout(pending.timer);
      pending.reject(new Error('QueryQueue disposed'));
    }
    this.queue.clear();
  }
}

并发控制示意

graph TD
    Q["请求队列"] --> C{"并发检查"}
    C -->|"runningCount < max"| E["执行查询"]
    C -->|"runningCount >= max"| W["等待"]
    
    E --> R["runningCount++"]
    R --> D["完成"]
    D --> Dec["runningCount--"]
    Dec --> C

5.7 错误处理

错误分类

type QueryErrorClass = 'connection' | 'syntax' | 'timeout' | 'permission' | 'unknown';

function classifyQueryError(error: unknown): ClassifiedError {
  const message = error instanceof Error ? error.message : String(error);
  
  // 连接错误(可重试)
  if (
    message.includes('ECONNREFUSED') ||
    message.includes('ETIMEDOUT') ||
    message.includes('PROTOCOL_CONNECTION_LOST')
  ) {
    return { errorClass: 'connection', retryable: true };
  }
  
  // 语法错误(不可重试)
  if (
    message.includes('syntax error') ||
    message.includes('ER_PARSE_ERROR') ||
    message.includes('42601')  // PostgreSQL syntax error code
  ) {
    return { errorClass: 'syntax', retryable: false };
  }
  
  // 超时错误(可重试)
  if (message.includes('timeout') || message.includes('timed out')) {
    return { errorClass: 'timeout', retryable: true };
  }
  
  // 权限错误(不可重试)
  if (
    message.includes('Access denied') ||
    message.includes('permission denied') ||
    message.includes('42501')  // PostgreSQL permission denied
  ) {
    return { errorClass: 'permission', retryable: false };
  }
  
  return { errorClass: 'unknown', retryable: false };
}

错误响应格式

function sendError(requestId: string, code: string, message: string, error?: unknown): void {
  const classification = error ? classifyQueryError(error) : undefined;
  
  send({
    id: requestId,
    type: 'error',
    error: {
      code,
      message,
      ...(classification ? {
        errorClass: classification.errorClass,
        retryable: classification.retryable,
      } : {}),
    },
  });
}

5.8 小结

本章介绍了 VSDB 的数据库驱动实现:

| 驱动 | 库 | 连接模型 | 特点 | |------|----|---------:----| | MySqlDriver | mysql2/promise | 单一连接 | Promise 原生,快速 | | PostgreSqlDriver | pg | 连接池 | 内置池,Cursor 流式 |

关键设计决策:

  • MySQL 单连接:简单够用,流式查询用独立连接
  • PostgreSQL 连接池:连接开销大,池化提升性能
  • Schema Inspector:SHOW 命令 vs information_schema
  • QueryQueue:并发控制 + 超时管理
  • 错误分类:区分可重试/不可重试错误

下一章将介绍扫描引擎的设计与实现。

第六章:扫描引擎设计

本章介绍 VSDB 的自动扫描引擎,它能从项目配置文件中自动发现数据库连接。

6.1 扫描引擎概述

功能定位

VSDB 扫描引擎解决的核心问题:

graph TD
    subgraph "传统方式"
        A1["连接信息分散"] --> A2["手动配置"]
        A2 --> A3["重复劳动"]
        A3 --> A4["团队协作困难"]
    end
    
    subgraph "VSDB 扫描引擎"
        B1["自动发现连接"] --> B2["一键添加"]
        B2 --> B3["团队共享配置"]
        B3 --> B4["效率提升"]
    end

支持的扫描来源

来源文件模式解析难度信息完整度
.env.env, .env.local, .env.*高(DATABASE_URL)
docker-composedocker-compose.yml高(服务定义)
TypeORMormconfig.json, data-source.ts
Laravelconfig/database.php
Djangosettings.py
Prismaschema.prisma

6.2 扫描引擎架构

ScannerEngine 协调器

export class ScannerEngine {
  private envParser = new EnvParser();
  private dockerComposeParser = new DockerComposeParser();
  private frameworkParser = new FrameworkParser();
  
  async scan(
    root: string, 
    existingConnections: DbConnection[]
  ): Promise<ScannerResult> {
    const results: ScannerResult = {
      connections: [],
      errors: [],
      scannedFiles: [],
    };
    
    // 执行各 Parser
    const envResult = await this.envParser.scan(root);
    const dockerResult = await this.dockerComposeParser.scan(root);
    const frameworkResult = await this.frameworkParser.scan(root);
    
    // 合并结果
    results.connections.push(...envResult.connections);
    results.connections.push(...dockerResult.connections);
    results.connections.push(...frameworkResult.connections);
    
    results.errors.push(...envResult.errors);
    results.errors.push(...dockerResult.errors);
    results.errors.push(...frameworkResult.errors);
    
    results.scannedFiles.push(...envResult.scannedFiles);
    results.scannedFiles.push(...dockerResult.scannedFiles);
    results.scannedFiles.push(...frameworkResult.scannedFiles);
    
    // 去重(与已存在连接比较)
    results.connections = this.deduplicate(results.connections, existingConnections);
    
    return results;
  }
  
  private deduplicate(
    scanned: ScannedConnection[], 
    existing: DbConnection[]
  ): ScannedConnection[] {
    return scanned.filter(conn => {
      // 检查 host+port 是否已存在
      return !existing.some(e => 
        e.host === conn.host && e.port === conn.port
      );
    });
  }
}

扫描流程

graph TD
    A["开始扫描"] --> B["EnvParser.scan()"]
    B --> C["DockerComposeParser.scan()"]
    C --> D["FrameworkParser.scan()"]
    
    D --> E["合并结果"]
    E --> F["去重处理"]
    F --> G["返回 ScannerResult"]
    
    B --> B1["查找 .env 文件"]
    B1 --> B2["解析 DATABASE_URL"]
    B2 --> B3["解析 DB_* 变量"]
    
    C --> C1["查找 docker-compose.yml"]
    C1 --> C2["识别 mysql/postgres 服务"]
    C2 --> C3["提取端口和环境变量"]
    
    D --> D1["查找框架配置文件"]
    D1 --> D2["解析连接数组"]

6.3 EnvParser 实现

.env 文件查找

const ENV_FILE_PATTERNS = [
  '.env',
  '.env.local',
  '.env.development',
  '.env.production',
  '.env.test',
  '.env.staging',
];

const MAX_DEPTH = 3;  // 最大搜索深度

private findEnvFiles(dir: string, depth: number): string[] {
  if (depth > MAX_DEPTH) return [];
  
  let entries: fs.Dirent[];
  try {
    entries = fs.readdirSync(dir, { withFileTypes: true });
  } catch {
    return [];
  }
  
  const files: string[] = [];
  
  for (const entry of entries) {
    if (entry.isDirectory()) {
      // 跳过 node_modules, .git 等
      if (entry.name === 'node_modules' || 
          entry.name === '.git' || 
          entry.name.startsWith('.')) {
        continue;
      }
      files.push(...this.findEnvFiles(path.join(dir, entry.name), depth + 1));
    } else if (ENV_FILE_PATTERNS.includes(entry.name)) {
      files.push(path.join(dir, entry.name));
    }
  }
  
  return files;
}

.env 内容解析

private parseEnvContent(content: string): Record<string, string> {
  const vars: Record<string, string> = {};
  
  for (const line of content.split('\n')) {
    const trimmed = line.trim();
    
    // 跳过空行和注释
    if (!trimmed || trimmed.startsWith('#')) continue;
    
    // 解析 KEY=VALUE
    const eqIdx = trimmed.indexOf('=');
    if (eqIdx < 0) continue;
    
    const key = trimmed.substring(0, eqIdx).trim();
    let value = trimmed.substring(eqIdx + 1).trim();
    
    // 去除引号
    if ((value.startsWith('"') && value.endsWith('"')) ||
        (value.startsWith("'") && value.endsWith("'"))) {
      value = value.substring(1, value.length - 1);
    }
    
    vars[key] = value;
  }
  
  return vars;
}

连接信息提取

async parseEnvFile(filePath: string): Promise<ScannedConnection[]> {
  const content = fs.readFileSync(filePath, 'utf-8');
  const vars = this.parseEnvContent(content);
  const connections: ScannedConnection[] = [];
  
  // 1) DATABASE_URL 格式
  const urlKeys = ['DATABASE_URL', 'DB_URL', 'DATABASE_URI'];
  for (const key of urlKeys) {
    if (vars[key]) {
      const conn = this.parseConnectionString(vars[key], filePath);
      if (conn) connections.push(conn);
    }
  }
  
  // 2) DB_* 变量组
  const dbHost = vars['DB_HOST'];
  if (dbHost) {
    const port = vars['DB_PORT'] 
      ? parseInt(vars['DB_PORT'], 10) 
      : DEFAULT_PORTS.postgresql;
    connections.push(this.buildFromVars(
      dbHost, port, vars['DB_USER'] || '', vars['DB_PASSWORD'] || '',
      vars['DB_NAME'], 'postgresql', filePath, 'DB_*'
    ));
  }
  
  // 3) MYSQL_* 变量组
  const mysqlHost = vars['MYSQL_HOST'];
  if (mysqlHost) {
    const port = vars['MYSQL_PORT'] 
      ? parseInt(vars['MYSQL_PORT'], 10) 
      : DEFAULT_PORTS.mysql;
    connections.push(this.buildFromVars(
      mysqlHost, port, vars['MYSQL_USER'] || '', vars['MYSQL_PASSWORD'] || '',
      vars['MYSQL_DATABASE'], 'mysql', filePath, 'MYSQL_*'
    ));
  }
  
  // 4) PG_* 变量组
  const pgHost = vars['PG_HOST'];
  if (pgHost) {
    const port = vars['PG_PORT'] 
      ? parseInt(vars['PG_PORT'], 10) 
      : DEFAULT_PORTS.postgresql;
    connections.push(this.buildFromVars(
      pgHost, port, vars['PG_USER'] || '', vars['PG_PASSWORD'] || '',
      vars['PG_DATABASE'], 'postgresql', filePath, 'PG_*'
    ));
  }
  
  return connections;
}

DATABASE_URL 解析

parseConnectionString(url: string, sourceFile: string): ScannedConnection | null {
  try {
    // 判断数据库类型
    let type: 'mysql' | 'postgresql' | null = null;
    let normalizedUrl = url;
    
    if (url.startsWith('postgresql://') || url.startsWith('postgres://')) {
      type = 'postgresql';
    } else if (url.startsWith('mysql://') || url.startsWith('mysql2://')) {
      type = 'mysql';
      if (url.startsWith('mysql2://')) {
        normalizedUrl = 'mysql://' + url.substring('mysql2://'.length);
      }
    } else {
      return null;  // 不支持的协议
    }
    
    const parsed = new URL(normalizedUrl);
    
    return {
      name: `${type}:${parsed.hostname}:${parsed.port}`,
      type,
      host: parsed.hostname || 'localhost',
      port: parsed.port 
        ? parseInt(parsed.port, 10) 
        : DEFAULT_PORTS[type],
      username: decodeURIComponent(parsed.username || ''),
      password: decodeURIComponent(parsed.password || ''),
      database: parsed.pathname.startsWith('/')
        ? parsed.pathname.substring(1)
        : parsed.pathname,
      source: 'env',
      sourceFile,
      confidence: 'high',
    };
  } catch {
    return null;  // URL 解析失败
  }
}

DATABASE_URL 格式示例

# PostgreSQL
DATABASE_URL=postgresql://user:password@localhost:5432/mydb

# MySQL
DATABASE_URL=mysql://root:secret@127.0.0.1:3306/testdb

# MySQL (mysql2 driver)
DATABASE_URL=mysql2://user:pass@host:3306/db

6.4 DockerComposeParser 实现

docker-compose.yml 解析

export class DockerComposeParser {
  async scan(root: string): Promise<ScannerResult> {
    const result: ScannerResult = {
      connections: [],
      errors: [],
      scannedFiles: [],
    };
    
    const composeFiles = this.findComposeFiles(root);
    
    for (const file of composeFiles) {
      result.scannedFiles.push(file);
      
      try {
        const content = fs.readFileSync(file, 'utf-8');
        const config = yaml.parse(content);
        const connections = this.parseComposeConfig(config, file);
        result.connections.push(...connections);
      } catch (err: any) {
        result.errors.push({ file, error: err.message });
      }
    }
    
    return result;
  }
  
  private parseComposeConfig(
    config: any, 
    sourceFile: string
  ): ScannedConnection[] {
    const connections: ScannedConnection[] =();
    
    if (!config.services) return connections;
    
    for (const [name, service] of Object.entries(config.services)) {
      const conn = this.parseService(name, service as any, sourceFile);
      if (conn) connections.push(conn);
    }
    
    return connections;
  }
}

服务定义解析

private parseService(
  name: string, 
  service: any, 
  sourceFile: string
): ScannedConnection | null {
  const image = service.image || '';
  
  // 识别 MySQL/PostgreSQL 服务
  let type: 'mysql' | 'postgresql' | null = null;
  
  if (image.includes('mysql') || image.includes('mariadb')) {
    type = 'mysql';
  } else if (image.includes('postgres') || image.includes('postgresql')) {
    type = 'postgresql';
  } else {
    return null;
  }
  
  // 提取端口
  const ports = service.ports || [];
  let hostPort = DEFAULT_PORTS[type];
  
  for (const portDef of ports) {
    // 格式: "3306:3306" 或 "3306"
    const parts = portDef.toString().split(':');
    if (parts.length === 2) {
      hostPort = parseInt(parts[0], 10);
    }
  }
  
  // 提取环境变量
  const envVars = this.parseEnvVars(service.environment || {});
  const username = envVars['MYSQL_USER'] || 
                   envVars['POSTGRES_USER'] || 
                   'root';
  const password = envVars['MYSQL_PASSWORD'] || 
                   envVars['POSTGRES_PASSWORD'] || 
                   '';
  const database = envVars['MYSQL_DATABASE'] || 
                   envVars['POSTGRES_DB'];
  
  return {
    name: `${type}:${name}:${hostPort}`,
    type,
    host: 'localhost',
    port: hostPort,
    username,
    password,
    database,
    source: 'docker-compose',
    sourceFile,
    confidence: 'high',
  };
}

docker-compose.yml 示例

version: '3'
services:
  mysql:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: myapp
      MYSQL_USER: appuser
      MYSQL_PASSWORD: apppass
  
  postgres:
    image: postgres:15
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: pguser
      POSTGRES_PASSWORD: pgpass
      POSTGRES_DB: myapp

6.5 FrameworkParser 实现

支持的框架配置

graph TD
    A["FrameworkParser"] --> B["TypeORM"]
    A --> C["Laravel"]
    A --> D["Django"]
    A --> E["Prisma"]
    
    B --> B1["ormconfig.json"]
    B --> B2["data-source.ts"]
    
    C --> C1["config/database.php"]
    
    D --> D1["settings.py"]
    
    E --> E1["schema.prisma"]

TypeORM 配置解析

parseTypeOrmConfig(filePath: string): ScannedConnection[] {
  const content = fs.readFileSync(filePath, 'utf-8');
  const config = JSON.parse(content);
  const connections: ScannedConnection[] = [];
  
  // TypeORM 支持多连接配置
  const connectionConfigs = Array.isArray(config) ? config : [config];
  
  for (const conn of connectionConfigs) {
    if (conn.type === 'mysql' || conn.type === 'postgres') {
      connections.push({
        name: `${conn.type}:${conn.host || 'localhost'}:${conn.port}`,
        type: conn.type === 'postgres' ? 'postgresql' : 'mysql',
        host: conn.host || 'localhost',
        port: conn.port || DEFAULT_PORTS[conn.type === 'postgres' ? 'postgresql' : 'mysql'],
        username: conn.username || '',
        password: conn.password || '',
        database: conn.database,
        source: 'framework',
        sourceFile: filePath,
        confidence: 'high',
      });
    }
  }
  
  return connections;
}

ormconfig.json 示例

[
  {
    "type": "mysql",
    "host": "localhost",
    "port": 3306,
    "username": "root",
    "password": "secret",
    "database": "myapp"
  },
  {
    "type": "postgres",
    "host": "localhost",
    "port": 5432,
    "username": "pguser",
    "password": "pgpass",
    "database": "myapp"
  }
]

Prisma schema 解析

parsePrismaSchema(filePath: string): ScannedConnection[] {
  const content = fs.readFileSync(filePath, 'utf-8');
  const connections: ScannedConnection[] = [];
  
  // 提取 datasource url
  const urlMatch = content.match(/url\s*=\s*"([^"]+)"/);
  if (urlMatch) {
    const url = urlMatch[1];
    const conn = this.parseConnectionString(url, filePath);
    if (conn) connections.push(conn);
  }
  
  // 也支持 env() 格式
  const envMatch = content.match(/url\s*=\s*env\("([^"]+)"\)/);
  if (envMatch) {
    // 标记为需要额外解析环境变量
    connections.push({
      name: `prisma:env:${envMatch[1]}`,
      type: 'postgresql',  // Prisma 默认 PostgreSQL
      host: '',
      port: 0,
      username: '',
      password: '',
      source: 'framework',
      sourceFile: filePath,
      confidence: 'low',
    });
  }
  
  return connections;
}

schema.prisma 示例

datasource db {
  provider = "postgresql"
  url      = "postgresql://user:pass@localhost:5432/mydb"
}

generator client {
  provider = "prisma-client-js"
}

6.6 去重与合并策略

去重逻辑

private deduplicate(
  scanned: ScannedConnection[], 
  existing: DbConnection[]
): ScannedConnection[] {
  return scanned.filter(conn => {
    // 按 host+port 去重
    const existsByHostPort = existing.some(e => 
      e.host === conn.host && e.port === conn.port
    );
    
    // 按数据库文件内的重复也去重
    // 同一文件可能定义多个指向同一数据库的连接
    
    return !existsByHostPort;
  });
}

多源合并示意

graph TD
    subgraph "扫描结果"
        A[".env: mysql:localhost:3306"]
        B["docker-compose: mysql:localhost:3306"]
        C["ormconfig: mysql:localhost:3306"]
        D["docker-compose: postgres:localhost:5432"]
    end
    
    A --> E["合并去重"]
    B --> E
    C --> E
    D --> E
    
    E --> F["最终结果"]
    F --> G["mysql:localhost:3306"]
    F --> H["postgres:localhost:5432"]

6.7 置信度评估

置信度分级

置信度条件示例
high信息完整,可直接使用DATABASE_URL, docker-compose 服务
medium信息基本完整,可能缺密码框架配置
low信息不完整,需用户补充部分 DB_* 变量,env() 引用

置信度使用

// 在 UI 中展示置信度
const items = result.connections.map(conn => ({
  label: conn.name,
  description: `${conn.type}://${conn.host}:${conn.port}`,
  detail: `Source: ${conn.source} (${conn.confidence} confidence)`,
  connection: conn,
}));

// 根据置信度决定是否需要用户输入
if (conn.confidence === 'low') {
  // 提示用户补充缺失信息
  const password = await vscode.window.showInputBox({
    prompt: `Enter password for ${conn.name}`,
    password: true,
  });
}

6.8 自动扫描时机

激活时扫描

// extension.ts
export function activate(context: vscode.ExtensionContext) {
  const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
  if (workspaceRoot) {
    autoScanProject(workspaceRoot);
  }
}

async function autoScanProject(workspaceRoot: string) {
  const existingConnections = await connectionManager!.listConnections();
  const result = await scannerEngine!.scan(workspaceRoot, existingConnections);
  
  if (result.connections.length > 0) {
    // 发现新连接,提示用户添加
    const selected = await vscode.window.showQuickPick(items, {
      placeHolder: `Found ${result.connections.length} connection(s). Select to add:`,
      canPickMany: true,
    });
    
    if (selected) {
      for (const item of selected) {
        await connectionManager!.addConnection({
          ...item.connection,
          scope: 'project',
        });
      }
      treeProvider?.refresh();
    }
  }
}

手动扫描命令

const scanProjectCmd = vscode.commands.registerCommand('vsdb.scanProject', async () => {
  const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
  if (!workspaceRoot) {
    vscode.window.showWarningMessage('No workspace folder open.');
    return;
  }
  
  vscode.window.withProgress({
    location: vscode.ProgressLocation.Notification,
    title: 'VSDB: Scanning project for database connections...',
  }, async () => {
    const existingConnections = await connectionManager!.listConnections();
    const result = await scannerEngine!.scan(workspaceRoot, existingConnections);
    
    // 处理结果...
  });
});

6.9 错误处理

解析错误收集

interface ScannerResult {
  connections: ScannedConnection[];
  errors: Array<{ file: string; error: string }>;  // 收集但不中断
  scannedFiles: string[];
}

错误示例处理

try {
  const config = yaml.parse(content);
  const connections = this.parseComposeConfig(config, file);
  result.connections.push(...connections);
} catch (err: any) {
  // 收集错误,继续扫描其他文件
  result.errors.push({ file, error: err.message });
}

错误报告

// 扫描完成后报告错误
if (result.errors.length > 0) {
  console.warn('VSDB scan errors:', result.errors);
  // 可选:向用户展示错误摘要
}

6.10 小结

本章介绍了 VSDB 的扫描引擎设计:

Parser目标解析方式置信度
EnvParser.env 文件DATABASE_URL + DB_* 变量
DockerComposeParserdocker-compose.yml服务 image + ports + env
FrameworkParser框架配置文件JSON/YAML/自定义解析中~高

关键设计:

  • 协调器模式:ScannerEngine 协调多个 Parser
  • 去重合并:host+port 去重避免重复添加
  • 置信度评估:区分可直接使用 vs 需补充信息
  • 错误收集:解析失败不中断,收集报告
  • 自动触发:激活时自动扫描,也可手动触发

下一章将介绍 UI 组件的实现。

第七章:UI组件实现

本章介绍 VSDB 的 UI 层实现,包括 TreeView 连接树和 Webview React 应用。

7.1 UI 层架构

graph TD
    subgraph "UI 层"
        A["TreeView"]
        B["Webview Panels"]
    end
    
    subgraph "TreeView"
        A1["treeProvider.ts"]
        A2["treeItems.ts"]
        A3["commands.ts"]
    end
    
    subgraph "Webview"
        B1["sqlEditor.ts"]
        B2["dataGrid.ts"]
        B3["connectionForm.ts"]
        B4["tableStructure.ts"]
    end
    
    A --> A1
    A1 --> A2
    A1 --> A3
    
    B --> B1
    B --> B2
    B --> B3
    B --> B4

7.2 TreeView 实现

TreeProvider 类

export class TreeProvider implements vscode.TreeDataProvider<TreeItem> {
  private _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined>();
  onDidChangeTreeData = this._onDidChangeTreeData.event;
  
  constructor(
    private connectionManager: ConnectionManager,
    private ipcManager: IpcManager,
    private historyManager?: HistoryManager,
    private bookmarkManager?: BookmarkManager,
  ) {}
  
  refresh(): void {
    this._onDidChangeTreeData.fire(undefined);
  }
  
  getTreeItem(element: TreeItem): vscode.TreeItem {
    return element;
  }
  
  async getChildren(element?: TreeItem): Promise<TreeItem[]> {
    if (!element) {
      // 根节点:连接列表
      return this.getConnectionNodes();
    }
    
    // 根据节点类型获取子节点
    if (element instanceof ConnectionNode) {
      return this.getDatabaseNodes(element);
    }
    if (element instanceof DatabaseNode) {
      return this.getTableNodes(element);
    }
    if (element instanceof TableNode) {
      return this.getColumnNodes(element);
    }
    
    return [];
  }
}

树节点类型

// 连接节点
class ConnectionNode extends vscode.TreeItem {
  constructor(
    public connection: DbConnection,
    public state: ConnectionState,
  ) {
    super(connection.name, vscode.TreeItemCollapsibleState.Collapsed);
    
    this.contextValue = `connection-${state.status}`;
    this.iconPath = this.getIconPath(state.status);
    this.tooltip = `${connection.type}://${connection.host}:${connection.port}`;
  }
  
  private getIconPath(status: string): vscode.ThemeIcon {
    switch (status) {
      case 'connected': return new vscode.ThemeIcon('database', new vscode.ThemeColor('charts.green'));
      case 'connecting': return new vscode.ThemeIcon('loading~spin');
      case 'error': return new vscode.ThemeIcon('error', new vscode.ThemeColor('charts.red'));
      default: return new vscode.ThemeIcon('database');
    }
  }
}

// 数据库节点
class DatabaseNode extends vscode.TreeItem {
  constructor(
    public connectionId: string,
    public database: string,
  ) {
    super(database, vscode.TreeItemCollapsibleState.Collapsed);
    this.contextValue = 'database';
    this.iconPath = new vscode.ThemeIcon('folder');
  }
}

// 表节点
class TableNode extends vscode.TreeItem {
  constructor(
    public connectionId: string,
    public database: string,
    public table: string,
  ) {
    super(table, vscode.TreeItemCollapsibleState.Collapsed);
    this.contextValue = 'table';
    this.iconPath = new vscode.ThemeIcon('table');
  }
}

// 列节点
class ColumnNode extends vscode.TreeItem {
  constructor(
    public column: ColumnInfo,
  ) {
    super(column.name, vscode.TreeItemCollapsibleState.None);
    this.contextValue = 'column';
    this.iconPath = new vscode.ThemeIcon('symbol-field');
    this.description = `${column.type}${column.isPrimaryKey ? ' (PK)' : ''}`;
  }
}

树节点展开逻辑

graph TD
    A["点击连接节点"] --> B{"连接状态"}
    B -->|"disconnected"| C["发送 connect 请求"]
    C --> D["更新为 connecting"]
    D --> E["收到响应"]
    E -->|"success"| F["更新为 connected"]
    E -->|"error"| G["更新为 error"]
    
    F --> H["查询 databases"]
    H --> I["返回 DatabaseNode[]"]
    
    B -->|"connected"| H

自动连接实现

async getChildren(element?: TreeItem): Promise<TreeItem[]> {
  if (element instanceof ConnectionNode) {
    const conn = element.connection;
    
    // 自动连接
    if (element.state.status === 'disconnected') {
      await this.ipcManager.sendRequest({
        type: 'connect',
        connectionId: conn.id,
        payload: { config: conn },
      });
      
      // 刷新状态
      this.refresh();
      return [];  // 等待刷新后展开
    }
    
    // 已连接,查询数据库列表
    const response = await this.ipcManager.sendRequest({
      type: 'schema',
      connectionId: conn.id,
      payload: { schemaType: 'databases' },
    });
    
    return response.data?.data.map(db => 
      new DatabaseNode(conn.id, db.name)
    ) || [];
  }
}

7.3 Webview 架构

Webview Panel 管理

export class SqlEditorPanel {
  public static currentPanel: SqlEditorPanel | undefined;
  private panel: vscode.WebviewPanel;
  private extensionUri: vscode.Uri;
  
  public static createOrShow(
    extensionUri: vscode.Uri,
    ipcManager: IpcManager,
    connectionManager: ConnectionManager,
  ): SqlEditorPanel {
    if (SqlEditorPanel.currentPanel) {
      SqlEditorPanel.currentPanel.panel.reveal();
      return SqlEditorPanel.currentPanel;
    }
    
    const panel = vscode.window.createWebviewPanel(
      'vsdb.sqlEditor',
      'VSDB SQL Editor',
      vscode.ViewColumn.One,
      {
        enableScripts: true,
        retainContextWhenHidden: true,
      }
    );
    
    SqlEditorPanel.currentPanel = new SqlEditorPanel(
      panel, extensionUri, ipcManager, connectionManager
    );
    
    return SqlEditorPanel.currentPanel;
  }
}

Webview 与 React 通信

sequenceDiagram
    participant VSCode as VSCode Extension
    participant Webview as Webview Panel
    participant React as React App
    
    VSCode->>Webview: 初始化 HTML + 加载 React bundle
    Webview->>React: 启动 React 应用
    React->>VSCode: postMessage({ type: 'ready' })
    VSCode->>React: postMessage({ type: 'connections', payload: [...] })
    
    Note over React: 用户输入 SQL
    React->>VSCode: postMessage({ type: 'executeQuery', payload: { sql } })
    VSCode->>VSCode: ipcManager.sendRequest(query)
    VSCode->>React: postMessage({ type: 'queryResult', payload: result })
    React->>React: 渲染结果表格

VSCode 消息发送

private updateWebview(): void {
  const connections = await this.connectionManager.listConnections();
  
  this.panel.webview.postMessage({
    type: 'connections',
    payload: connections,
  });
}

private handleWebviewMessage(message: any): void {
  switch (message.type) {
    case 'executeQuery':
      this.executeQuery(message.payload);
      break;
    case 'saveQuery':
      this.saveQuery(message.payload);
      break;
    case 'exportData':
      this.exportData(message.payload);
      break;
  }
}

React 消息接收

// SqlEditor.tsx
useEffect(() => {
  const handler = (event: MessageEvent) => {
    const message = event.data;
    
    switch (message.type) {
      case 'connections':
        setConnections(message.payload);
        break;
      case 'queryResult':
        setQueryStatus('success');
        addResultTab(message.payload);
        break;
      case 'queryError':
        setQueryStatus('error');
        addErrorTab(message.payload.message);
        break;
    }
  };
  
  window.addEventListener('message', handler);
  return () => window.removeEventListener('message', handler);
}, []);

const executeQuery = () => {
  postMessage({
    type: 'executeQuery',
    payload: { sql, connectionId },
  });
};

7.4 SQL Editor 组件

Monaco Editor 配置

<Editor
  height="200px"
  defaultLanguage="sql"
  defaultValue={sql}
  theme={monacoTheme}
  options={{
    minimap: { enabled: false },
    fontSize: 13,
    lineNumbers: 'on',
    scrollBeyondLastLine: false,
    automaticLayout: true,
    wordWrap: 'on',
    tabSize: 2,
  }}
  onMount={handleEditorMount}
/>

SQL 自动补全

const handleEditorMount: OnMount = (editor, monaco) => {
  monaco.languages.registerCompletionItemProvider('sql', {
    provideCompletionItems: (model, position) => {
      const keywords = [
        'SELECT', 'FROM', 'WHERE', 'INSERT', 'UPDATE', 'DELETE',
        'JOIN', 'ON', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT',
        // ... 更多关键字
      ];
      
      const suggestions = keywords.map(kw => ({
        label: kw,
        kind: monaco.languages.CompletionItemKind.Keyword,
        insertText: kw,
        range,
      }));
      
      // 添加表名建议
      for (const conn of connections) {
        suggestions.push({
          label: conn.name,
          kind: monaco.languages.CompletionItemKind.Reference,
          insertText: conn.name,
          range,
        }));
      }
      
      return { suggestions };
    },
  });
  
  // Ctrl+Enter 执行快捷键
  editor.addAction({
    id: 'vsdb-execute-query',
    label: 'Execute Query',
    keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter],
    run: () => executeQuery(),
  });
};

7.5 DataGrid 组件

虚拟滚动

// 使用 react-window 处理大数据
import { FixedSizeList as List } from 'react-window';

const DataGrid: React.FC<{ rows: any[]; columns: string[] }> = ({ rows, columns }) => {
  const Row = ({ index, style }) => (
    <div style={style} className="grid-row">
      {columns.map(col => (
        <div key={col} className="grid-cell">
          {rows[index][col]}
        </div>
      ))}
    </div>
  );
  
  return (
    <List
      height={400}
      itemCount={rows.length}
      itemSize={35}
      width="100%"
    >
      {Row}
    </List>
  );
};

内联编辑

// EditManager 管理编辑状态
class EditManager {
  private edits = new Map<string, any>();  // rowIndex -> modifiedData
  private editMode: 'readonly' | 'inline' | 'form' = 'readonly';
  
  startEdit(rowIndex: number): void {
    this.editMode = 'inline';
  }
  
  setValue(rowIndex: number, column: string, value: any): void {
    // 记录修改
    this.edits.set(`${rowIndex}:${column}`, value);
  }
  
  getEdits(): Array<{ row: number; column: string; value: any }> {
    return Array.from(this.edits.entries()).map(([key, value]) => {
      const [row, col] = key.split(':');
      return { row: parseInt(row), column: col, value };
    });
  }
  
  commit(connectionId: string, database: string, table: string): Promise<void> {
    // 生成 UPDATE SQL 并发送
    for (const edit of this.getEdits()) {
      const sql = this.buildUpdateSql(table, edit);
      await postMessage({ type: 'executeUpdate', payload: { sql, connectionId } });
    }
  }
}

7.6 表结构面板

多 Tab 展示

interface TableStructureProps {
  connectionId: string;
  database: string;
  table: string;
}

const TableStructure: React.FC<TableStructureProps> = ({ connectionId, database, table }) => {
  const [activeTab, setActiveTab] = useState<'columns' | 'indexes' | 'constraints' | 'triggers' | 'ddl'>('columns');
  const [data, setData] = useState<any>(null);
  
  useEffect(() => {
    postMessage({
      type: 'getTableStructure',
      payload: { connectionId, database, table, schemaType: activeTab },
    });
    
    const handler = (event: MessageEvent) => {
      if (event.data.type === 'schemaResult') {
        setData(event.data.payload);
      }
    };
    
    window.addEventListener('message', handler);
    return () => window.removeEventListener('message', handler);
  }, [activeTab]);
  
  return (
    <div>
      <TabBar active={activeTab} onChange={setActiveTab}>
        <Tab id="columns">Columns</Tab>
        <Tab id="indexes">Indexes</Tab>
        <Tab id="constraints">Constraints</Tab>
        <Tab id="triggers">Triggers</Tab>
        <Tab id="ddl">DDL</Tab>
      </TabBar>
      
      <TabContent active={activeTab} data={data} />
    </div>
  );
};

7.7 连接表单

表单验证

interface ConnectionFormData {
  name: string;
  type: 'mysql' | 'postgresql';
  host: string;
  port: number;
  username: string;
  password: string;
  database?: string;
  scope: 'project' | 'global';
}

const ConnectionForm: React.FC = () => {
  const [formData, setFormData] = useState<ConnectionFormData>({
    name: '',
    type: 'mysql',
    host: 'localhost',
    port: 3306,
    username: '',
    password: '',
    database: '',
    scope: 'project',
  });
  
  const [errors, setErrors] = useState<Record<string, string>>({});
  
  const validate = (): boolean => {
    const errs: Record<string, string> = {};
    
    if (!formData.name) errs.name = 'Name is required';
    if (!formData.host) errs.host = 'Host is required';
    if (!formData.username) errs.username = 'Username is required';
    
    setErrors(errs);
    return Object.keys(errs).length === 0;
  };
  
  const handleSubmit = () => {
    if (!validate()) return;
    
    postMessage({
      type: 'saveConnection',
      payload: formData,
    });
  };
  
  // 自动填充默认端口
  const handleTypeChange = (type: 'mysql' | 'postgresql') => {
    setFormData({
      ...formData,
      type,
      port: type === 'mysql' ? 3306 : 5432,
    });
  };
  
  return (
    <form>
      <Input label="Name" value={formData.name} error={errors.name} onChange={...} />
      <Select label="Type" value={formData.type} onChange={handleTypeChange}>
        <option value="mysql">MySQL</option>
        <option value="postgresql">PostgreSQL</option>
      </Select>
      <Input label="Host" value={formData.host} error={errors.host} onChange={...} />
      <Input label="Port" type="number" value={formData.port} onChange={...} />
      <Input label="Username" value={formData.username} error={errors.username} onChange={...} />
      <Input label="Password" type="password" value={formData.password} onChange={...} />
      <Input label="Database" value={formData.database} onChange={...} />
      <RadioGroup label="Scope" value={formData.scope} onChange={...}>
        <Radio value="project">Project (shared)</Radio>
        <Radio value="global">Global (private)</Radio>
      </RadioGroup>
      <Button onClick={handleTestConnection}>Test Connection</Button>
      <Button onClick={handleSubmit}>Save</Button>
    </form>
  );
};

7.8 小结

本章介绍了 VSDB 的 UI 组件实现:

组件类型功能
TreeProviderVSCode TreeView连接树展示
ConnectionNodeTreeItem连接节点
SqlEditorPanelWebview PanelSQL 编辑器
SqlEditor.tsxReact ComponentMonaco 编辑器集成
DataGrid.tsxReact Component数据网格 + 虚拟滚动
ConnectionFormReact Component连接表单
TableStructureReact Component表结构多 Tab

关键设计:

  • TreeView 自动连接:展开时自动发送 connect 请求
  • Webview 双向通信:postMessage 实现消息传递
  • 虚拟滚动:react-window 处理大数据集
  • 内联编辑:EditManager 管理编辑状态
  • Monaco 集成:SQL 语法高亮 + 自动补全

下一章将介绍数据操作功能。

第八章:数据操作功能

本章介绍 VSDB 的数据操作功能,包括查询执行、导入导出、数据编辑等。

8.1 查询执行流程

执行流程概览

sequenceDiagram
    participant User as 用户
    participant Editor as SqlEditor
    participant Panel as SqlEditorPanel
    participant IPC as IpcManager
    participant Worker as Worker Process
    participant Driver as DatabaseDriver
    participant DB as Database
    
    User->>Editor: 输入 SQL + Ctrl+Enter
    Editor->>Panel: postMessage(executeQuery)
    Panel->>Panel: 记录查询历史
    Panel->>IPC: sendRequest(type=query)
    IPC->>Worker: WorkerRequest
    Worker->>Driver: driver.query(sql)
    Driver->>DB: 执行 SQL
    DB-->>Driver: 结果
    Driver-->>Worker: QueryResult
    Worker-->>IPC: WorkerResponse
    IPC-->>Panel: WorkerResponse
    Panel-->>Editor: postMessage(queryResult)
    Editor-->>User: 渲染结果表格

查询历史记录

// HistoryManager.ts
export class HistoryManager {
  private maxItems = 100;
  
  constructor(private globalState: vscode.Memento) {}
  
  add(item: QueryHistoryItem): void {
    const history = this.getList();
    
    // 添加新项
    history.unshift(item);
    
    // 清理超出上限的未固定项
    while (history.length > this.maxItems) {
      const unpinnedIndex = history.findIndex(h => !h.pinned);
      if (un pinnedIndex >= 0) {
        history.splice(unpinnedIndex, 1);
      } else {
        break;  // 全部固定,不再清理
      }
    }
    
    this.globalState.update('queryHistory', history);
  }
  
  getList(limit?: number): QueryHistoryItem[] {
    const history = this.globalState.get<QueryHistoryItem[]>('queryHistory', []);
    return limit ? history.slice(0, limit) : history;
  }
  
  pin(id: string): void {
    const history = this.getList();
    const item = history.find(h => h.id === id);
    if (item) {
      item.pinned = true;
      this.globalState.update('queryHistory', history);
    }
  }
  
  remove(id: string): void {
    const history = this.getList().filter(h => h.id !== id);
    this.globalState.update('queryHistory', history);
  }
  
  clear(): void {
    const history = this.getList().filter(h => h.pinned);
    this.globalState.update('queryHistory', history);
  }
}

执行时间统计

// Driver 端计时
async query(sql: string, params?: any[]): Promise<QueryResult> {
  const startTime = Date.now();
  const result = await this.connection.execute(sql, params);
  const executionTime = Date.now() - startTime;
  
  return {
    ...result,
    executionTime,
  };
}

// 历史记录包含执行时间
const historyItem: QueryHistoryItem = {
  id: uuid(),
  sql,
  connectionId,
  connectionName: conn.name,
  executedAt: new Date(),
  rowCount: result.rowCount,
  executionTime: result.executionTime,
};

8.2 导入导出功能

导出管理器

export class ExportManager {
  async exportToCsv(
    result: QueryResult,
    filePath: string
  ): Promise<void> {
    const lines: string[] = [];
    
    // 头行:列名
    lines.push(result.columns.join(','));
    
    // 数据行
    for (const row of result.rows) {
      const values = result.columns.map(col => {
        const val = row[col];
        // 处理特殊字符
        if (val === null || val === undefined) return '';
        if (typeof val === 'string' && (val.includes(',') || val.includes('"'))) {
          return `"${val.replace(/"/g, '""')}"`;
        }
        return String(val);
      });
      lines.push(values.join(','));
    }
    
    fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
  }
  
  async exportToJson(
    result: QueryResult,
    filePath: string
  ): Promise<void> {
    const data = {
      columns: result.columns,
      rows: result.rows,
      rowCount: result.rowCount,
      exportedAt: new Date().toISOString(),
    };
    
    fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
  }
  
  async exportToSql(
    result: QueryResult,
    filePath: string,
    tableName: string
  ): Promise<void> {
    const lines: string[] = [];
    
    for (const row of result.rows) {
      const columns = result.columns.map(c => `\`${c}\``).join(', ');
      const values = result.columns.map(c => {
        const val = row[c];
        if (val === null) return 'NULL';
        if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`;
        if (typeof val === 'number') return String(val);
        return `'${String(val)}'`;
      }).join(', ');
      
      lines.push(`INSERT INTO \`${tableName}\` (${columns}) VALUES (${values});`);
    }
    
    fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
  }
}

导入管理器

export class ImportManager {
  async importFromCsv(
    filePath: string,
    connectionId: string,
    tableName: string
  ): Promise<{ imported: number; errors: string[] }> {
    const content = fs.readFileSync(filePath, 'utf-8');
    const lines = content.split('\n');
    
    if (lines.length < 2) {
      throw new Error('CSV file is empty or has no data');
    }
    
    // 第一行是列名
    const columns = this.parseCsvLine(lines[0]);
    const errors: string[] = [];
    let imported = 0;
    
    for (let i = 1; i < lines.length; i++) {
      try {
        const values = this.parseCsvLine(lines[i]);
        const sql = this.buildInsertSql(tableName, columns, values);
        
        await ipcManager.sendRequest({
          type: 'query',
          connectionId,
          payload: { sql },
        });
        
        imported++;
      } catch (err: any) {
        errors.push(`Row ${i}: ${err.message}`);
      }
    }
    
    return { imported, errors };
  }
  
  async importFromJson(
    filePath: string,
    connectionId: string,
    tableName: string
  ): Promise<{ imported: number; errors: string[] }> {
    const content = fs.readFileSync(filePath, 'utf-8');
    const data = JSON.parse(content);
    
    const rows = Array.isArray(data.rows) ? data.rows : 
                 Array.isArray(data) ? data : [data];
    
    const errors: string[] = [];
    let imported = 0;
    
    for (const row of rows) {
      try {
        const columns = Object.keys(row);
        const values = columns.map(c => row[c]);
        const sql = this.buildInsertSql(tableName, columns, values);
        
        await ipcManager.sendRequest({
          type: 'query',
          connectionId,
          payload: { sql },
        });
        
        imported++;
      } catch (err: any) {
        errors.push(`Row ${imported + errors.length + 1}: ${err.message}`);
      }
    }
    
    return { imported, errors };
  }
}

CSV 解析

private parseCsvLine(line: string): string[] {
  const values: string[] = [];
  let current = '';
  let inQuotes = false;
  
  for (let i = 0; i < line.length; i++) {
    const char = line[i];
    
    if (inQuotes) {
      if (char === '"') {
        if (line[i + 1] === '"') {
          current += '"';
          i++;
        } else {
          inQuotes = false;
        }
      } else {
        current += char;
      }
    } else {
      if (char === '"') {
        inQuotes = true;
      } else if (char === ',') {
        values.push(current);
        current = '';
      } else {
        current += char;
      }
    }
  }
  
  values.push(current);
  return values;
}

8.3 数据编辑

内联编辑模式

graph TD
    A["查看数据"] --> B["启用编辑模式"]
    B --> C["点击单元格"]
    C --> D["输入新值"]
    D --> E{"确认修改"}
    E -->|"Enter"| F["标记为已修改"]
    E -->|"Esc"| G["取消修改"]
    
    F --> H["继续编辑或提交"]
    H --> I["提交更改"]
    I --> J["生成 UPDATE SQL"]
    J --> K["执行 SQL"]
    K --> L["刷新数据"]

EditManager 实现

class EditManager {
  private edits = new Map<string, { original: any; modified: any }>();
  private editMode: EditMode = 'readonly';
  
  startEdit(rowIndex: number, column: string, originalValue: any): void {
    const key = `${rowIndex}:${column}`;
    this.edits.set(key, { original: originalValue, modified: originalValue });
    this.editMode = 'inline';
  }
  
  updateValue(rowIndex: number, column: string, newValue: any): void {
    const key = `${rowIndex}:${column}`;
    const edit = this.edits.get(key);
    if (edit) {
      edit.modified = newValue;
    }
  }
  
  cancelEdit(rowIndex: number, column: string): void {
    const key = `${rowIndex}:${column}`;
    this.edits.delete(key);
  }
  
  hasEdits(): boolean {
    return this.edits.size > 0;
  }
  
  getEdits(): Array<{ rowIndex: number; column: string; original: any; modified: any }> {
    return Array.from(this.edits.entries())
      .filter(([_, edit]) => edit.original !== edit.modified)
      .map(([key, edit]) => {
        const [rowIndex, column] = key.split(':');
        return {
          rowIndex: parseInt(rowIndex),
          column,
          original: edit.original,
          modified: edit.modified,
        };
      });
  }
  
  buildUpdateSql(
    tableName: string,
    primaryKeyColumns: string[],
    rows: any[]
  ): string[] {
    const sqls: string[] = [];
    
    for (const edit of this.getEdits()) {
      const row = rows[edit.rowIndex];
      const whereClause = primaryKeyColumns
        .map(pk => `${pk} = ${this.formatValue(row[pk])}`)
        .join(' AND ');
      
      sqls.push(
        `UPDATE ${tableName} SET ${edit.column} = ${this.formatValue(edit.modified)} WHERE ${whereClause}`
      );
    }
    
    return sqls;
  }
  
  private formatValue(value: any): string {
    if (value === null) return 'NULL';
    if (typeof value === 'number') return String(value);
    return `'${String(value).replace(/'/g, "''")}'`;
  }
}

批量编辑提交

const commitEdits = async () => {
  if (!editManager.hasEdits()) return;
  
  const edits = editManager.getEdits();
  
  // 确认对话框
  const confirm = await vscode.window.showWarningMessage(
    `Apply ${edits.length} changes to ${tableName}?`,
    { modal: true },
    'Apply',
    'Cancel'
  );
  
  if (confirm !== 'Apply') return;
  
  // 生成并执行 SQL
  const sqls = editManager.buildUpdateSql(tableName, primaryKeyColumns, rows);
  
  for (const sql of sqls) {
    try {
      await ipcManager.sendRequest({
        type: 'query',
        connectionId,
        payload: { sql },
      });
    } catch (err) {
      // 错误处理
    }
  }
  
  // 刷新数据
  editManager.clearEdits();
  refreshData();
};

8.4 数据搜索

SearchEngine 实现

export class SearchEngine {
  private schemaCache = new Map<string, SchemaCacheEntry>();
  
  constructor(
    private ipcManager: IpcManager,
    private connectionManager: ConnectionManager,
  ) {}
  
  async searchAll(
    keyword: string,
    options: SearchOptions = {}
  ): Promise<SearchResult[]> {
    const results: SearchResult[] = [];
    
    const connectionIds = options.connectionIds || 
      await this.getAllConnectionIds();
    
    for (const connectionId of connectionIds) {
      if (options.searchTables) {
        const tables = await this.searchTables(connectionId, keyword);
        results.push(...tables);
      }
      
      if (options.searchColumns) {
        const columns = await this.searchColumns(connectionId, keyword);
        results.push(...columns);
      }
      
      if (options.searchData) {
        const data = await this.searchData(connectionId, keyword, options.tables);
        results.push(...data);
      }
    }
    
    return results;
  }
  
  async searchTables(connectionId: string, keyword: string): Promise<SearchResult[]> {
    const schema = await this.getSchema(connectionId);
    const results: SearchResult[] = [];
    
    for (const table of schema.tables) {
      if (this.matches(table.name, keyword)) {
        results.push({
          type: 'table',
          connectionId,
          connectionName: await this.getConnectionName(connectionId),
          database: table.schema || '',
          name: table.name,
          detail: `Table (${table.rowCount || '?'} rows)`,
        });
      }
    }
    
    return results;
  }
  
  async searchColumns(connectionId: string, keyword: string): Promise<SearchResult[]> {
    const schema = await this.getSchema(connectionId);
    const results: SearchResult[] = [];
    
    for (const column of schema.columns) {
      if (this.matches(column.name, keyword)) {
        results.push({
          type: 'column',
          connectionId,
          connectionName: await this.getConnectionName(connectionId),
          database: '',  // 需要关联表获取
          name: column.name,
          detail: `${column.type} in ${column.tableName}`,
        });
      }
    }
    
    return results;
  }
  
  private matches(value: string, keyword: string, caseSensitive = false): boolean {
    if (!caseSensitive) {
      return value.toLowerCase().includes(keyword.toLowerCase());
    }
    return value.includes(keyword);
  }
  
  private async getSchema(connectionId: string): Promise<SchemaCacheEntry> {
    const cached = this.schemaCache.get(connectionId);
    
    if (cached && Date.now() - cached.cachedAt.getTime() < 60000) {
      return cached;  // 1 分钟缓存
    }
    
    // 查询 Schema
    const tablesResponse = await this.ipcManager.sendRequest({
      type: 'schema',
      connectionId,
      payload: { schemaType: 'tables' },
    });
    
    const columnsResponse = await this.ipcManager.sendRequest({
      type: 'schema',
      connectionId,
      payload: { schemaType: 'columns' },
    });
    
    const entry: SchemaCacheEntry = {
      tables: tablesResponse.data?.data || [],
      columns: columnsResponse.data?.data || [],
      cachedAt: new Date(),
    };
    
    this.schemaCache.set(connectionId, entry);
    return entry;
  }
  
  clearCache(): void {
    this.schemaCache.clear();
  }
}

8.5 小结

本章介绍了 VSDB 的数据操作功能:

功能实现模块关键点
查询执行SqlEditorPanel + Worker历史记录、执行时间统计
导出ExportManagerCSV/JSON/SQL 格式
导入ImportManagerCSV/JSON 解析
数据编辑EditManager内联编辑、UPDATE SQL 生成
数据搜索SearchEngineSchema 缓存、表/列搜索

关键设计:

  • 查询历史:上限 100 条,支持固定防清理
  • 导出格式:CSV(逗号分隔)、JSON(结构化)、SQL(INSERT 语句)
  • 内联编辑:标记修改、批量提交、主键定位
  • 搜索缓存:Schema 缓存 1 分钟,提升搜索性能

下一章将介绍辅助功能系统。

第九章:辅助功能系统

本章介绍 VSDB 的辅助功能,包括查询历史、书签系统、全局搜索和错误处理。

9.1 查询历史系统

HistoryManager 设计

graph TD
    A["执行查询"] --> B["HistoryManager.add()"]
    B --> C["添加到历史列表"]
    C --> D["检查上限"]
    D -->|"超过 100 条"| E["清理未固定项"]
    D -->|"未超过"| F["保存到 globalState"]
    
    G["查看历史"] --> H["TreeProvider"]
    H --> I["显示历史节点"]
    I --> J["点击重用"]
    J --> K["填充到 SQL Editor"]

实现细节

export class HistoryManager {
  private maxItems = 100;
  
  constructor(private globalState: vscode.Memento) {}
  
  // 添加历史项
  add(item: QueryHistoryItem): void {
    const history = this.getList();
    history.unshift(item);  // 新项在前
    
    // 清理策略:先清理未固定,超过上限时清理最旧的
    while (history.length > this.maxItems) {
      // 从末尾找未固定项
      for (let i = history.length - 1; i >= 0; i--) {
        if (!history[i].pinned) {
          history.splice(i, 1);
          break;
        }
      }
      
      // 全部固定时,清理最旧的固定项
      if (history.length > this.maxItems) {
        history.pop();
      }
    }
    
    this.globalState.update('queryHistory', history);
  }
  
  // 固定历史项
  pin(id: string): void {
    const history = this.getList();
    const item = history.find(h => h.id === id);
    if (item) {
      item.pinned = true;
      this.globalState.update('queryHistory', history);
    }
  }
  
  // 取消固定
  unpin(id: string): void {
    const history = this.getList();
    const item = history.find(h => h.id === id);
    if (item) {
      item.pinned = false;
      this.globalState.update('queryHistory', history);
    }
  }
  
  // 删除单项
  remove(id: string): void {
    const history = this.getList().filter(h => h.id !== id);
    this.globalState.update('queryHistory', history);
  }
  
  // 清空未固定
  clear(): void {
    const pinned = this.getList().filter(h => h.pinned);
    this.globalState.update('queryHistory', pinned);
  }
  
  // 获取列表
  getList(limit?: number): QueryHistoryItem[] {
    const history = this.globalState.get<QueryHistoryItem[]>('queryHistory', []);
    return limit ? history.slice(0, limit) : history;
  }
}

TreeView 历史节点

class HistoryNode extends vscode.TreeItem {
  constructor(public item: QueryHistoryItem) {
    super(item.sql.substring(0, 50) + '...', vscode.TreeItemCollapsibleState.None);
    
    this.contextValue = item.pinned ? 'history-pinned' : 'history';
    this.iconPath = item.pinned 
      ? new vscode.ThemeIcon('pin') 
      : new vscode.ThemeIcon('history');
    this.tooltip = `${item.connectionName}\n${item.sql}\nExecuted: ${item.executedAt}`;
    this.description = `${item.executionTime}ms, ${item.rowCount || 0} rows`;
  }
}

9.2 书签系统

BookmarkManager 设计

graph TD
    A["添加书签"] --> B["BookmarkManager.add()"]
    B --> C["保存到 globalState"]
    
    D["查看书签"] --> E["TreeProvider"]
    E --> F["显示书签节点"]
    F --> G["展开分组"]
    
    H["管理书签"] --> I["移动到分组"]
    H --> J["删除书签"]
    H --> K["创建分组"]

实现细节

export class BookmarkManager {
  constructor(private globalState: vscode.Memento) {}
  
  add(bookmark: Bookmark): void {
    const bookmarks = this.getList();
    bookmark.id = uuid();
    bookmark.createdAt = new Date();
    bookmarks.push(bookmark);
    this.globalState.update('bookmarks', bookmarks);
  }
  
  remove(id: string): void {
    const bookmarks = this.getList().filter(b => b.id !== id);
    this.globalState.update('bookmarks', bookmarks);
  }
  
  getList(): Bookmark[] {
    return this.globalState.get<Bookmark[]>('bookmarks', []);
  }
  
  // 分组管理
  getGroups(): BookmarkGroup[] {
    return this.globalState.get<BookmarkGroup[]>('bookmarkGroups', []);
  }
  
  createGroup(name: string, color?: string): BookmarkGroup {
    const groups = this.getGroups();
    const group: BookmarkGroup = {
      id: uuid(),
      name,
      color,
    };
    groups.push(group);
    this.globalState.update('bookmarkGroups', groups);
    return group;
  }
  
  moveToGroup(bookmarkId: string, groupId?: string): void {
    const bookmarks = this.getList();
    const bookmark = bookmarks.find(b => b.id === bookmarkId);
    if (bookmark) {
      bookmark.groupId = groupId;
      this.globalState.update('bookmarks', bookmarks);
    }
  }
  
  // 按分组获取书签
  getByGroup(groupId?: string): Bookmark[] {
    return this.getList().filter(b => b.groupId === groupId);
  }
}

书签节点类型

// 书签分组节点
class BookmarkGroupNode extends vscode.TreeItem {
  constructor(public group: BookmarkGroup) {
    super(group.name, vscode.TreeItemCollapsibleState.Collapsed);
    this.contextValue = 'bookmark-group';
    this.iconPath = new vscode.ThemeIcon('folder', new vscode.ThemeColor(group.color));
  }
}

// 书签项节点
class BookmarkNode extends vscode.TreeItem {
  constructor(public bookmark: Bookmark) {
    super(bookmark.name, vscode.TreeItemCollapsibleState.None);
    this.contextValue = `bookmark-${bookmark.type}`;
    
    // 根据类型设置图标
    switch (bookmark.type) {
      case 'connection':
        this.iconPath = new vscode.ThemeIcon('plug');
        break;
      case 'table':
        this.iconPath = new vscode.ThemeIcon('table');
        break;
      case 'query':
        this.iconPath = new vscode.ThemeIcon('file-code');
        break;
    }
    
    this.tooltip = `Type: ${bookmark.type}`;
  }
}

9.3 全局搜索

SearchEngine 设计

graph TD
    A["用户输入关键词"] --> B["SearchEngine.searchAll()"]
    B --> C["遍历活动连接"]
    
    C --> D["searchTables()"]
    C --> E["searchColumns()"]
    C --> F["searchData()"]
    
    D --> G["查询 Schema Cache"]
    E --> G
    F --> H["生成 SELECT LIKE SQL"]
    
    G --> I["匹配过滤"]
    H --> I
    
    I --> J["合并结果"]
    J --> K["返回 SearchResult[]"]

实现细节

export class SearchEngine {
  private schemaCache = new Map<string, SchemaCacheEntry>();
  private cacheTimeout = 60000;  // 1 分钟缓存
  
  async searchAll(keyword: string, options: SearchOptions = {}): Promise<SearchResult[]> {
    const results: SearchResult[] = [];
    const connectionIds = options.connectionIds || await this.getActiveConnectionIds();
    
    for (const connectionId of connectionIds) {
      if (options.searchTables !== false) {
        results.push(...await this.searchTables(connectionId, keyword, options));
      }
      
      if (options.searchColumns !== false) {
        results.push(...await this.searchColumns(connectionId, keyword, options));
      }
      
      if (options.searchData) {
        results.push(...await this.searchData(connectionId, keyword, options));
      }
    }
    
    return results;
  }
  
  async searchTables(
    connectionId: string, 
    keyword: string, 
    options: SearchOptions
  ): Promise<SearchResult[]> {
    const schema = await this.getSchemaWithCache(connectionId);
    const matcher = this.createMatcher(keyword, options.caseSensitive);
    
    return schema.tables
      .filter(t => matcher(t.name))
      .map(t => ({
        type: 'table',
        connectionId,
        connectionName: await this.getConnectionName(connectionId),
        database: t.schema || 'public',
        name: t.name,
        detail: `${t.rowCount || '?'} rows`,
      }));
  }
  
  async searchData(
    connectionId: string,
    keyword: string,
    options: SearchOptions
  ): Promise<SearchResult[]> {
    const results: SearchResult[] = [];
    const tables = options.tables || await this.getAllTables(connectionId);
    
    for (const table of tables) {
      // 生成搜索 SQL
      const columns = await this.getTableColumns(connectionId, table);
      const whereClause = columns
        .map(c => `${c.name} LIKE '%${keyword}%'`)
        .join(' OR ');
      
      const sql = `SELECT * FROM ${table} WHERE ${whereClause} LIMIT 100`;
      
      try {
        const response = await this.ipcManager.sendRequest({
          type: 'query',
          connectionId,
          payload: { sql },
        });
        
        if (response.data?.rowCount > 0) {
          results.push({
            type: 'data',
            connectionId,
            connectionName: await this.getConnectionName(connectionId),
            database: '',
            name: keyword,
            detail: `Found in ${table}: ${response.data.rowCount} rows`,
          });
        }
      } catch {
        // 搜索失败跳过
      }
    }
    
    return results;
  }
  
  private createMatcher(keyword: string, caseSensitive?: boolean): (value: string) => boolean {
    const k = caseSensitive ? keyword : keyword.toLowerCase();
    return (value: string) => {
      const v = caseSensitive ? value : value.toLowerCase();
      return v.includes(k);
    };
  }
  
  private async getSchemaWithCache(connectionId: string): Promise<SchemaCacheEntry> {
    const cached = this.schemaCache.get(connectionId);
    
    if (cached && Date.now() - cached.cachedAt.getTime() < this.cacheTimeout) {
      return cached;
    }
    
    // 刷新缓存
    const tables = await this.fetchTables(connectionId);
    const columns = await this.fetchColumns(connectionId);
    
    const entry: SchemaCacheEntry = {
      tables,
      columns,
      cachedAt: new Date(),
    };
    
    this.schemaCache.set(connectionId, entry);
    return entry;
  }
  
  clearCache(): void {
    this.schemaCache.clear();
  }
}

搜索命令

const searchCmd = vscode.commands.registerCommand(COMMANDS.SEARCH, async () => {
  const keyword = await vscode.window.showInputBox({
    prompt: 'Search tables, columns, or data',
    placeHolder: 'Enter search keyword...',
  });
  
  if (!keyword) return;
  
  vscode.window.withProgress({
    location: vscode.ProgressLocation.Notification,
    title: `VSDB: Searching for "${keyword}"...`,
  }, async () => {
    const results = await searchEngine!.searchAll(keyword, {
      searchTables: true,
      searchColumns: true,
      searchData: false,  // 数据搜索较慢,默认关闭
    });
    
    if (results.length === 0) {
      vscode.window.showInformationMessage(`No results found for "${keyword}"`);
      return;
    }
    
    const items = results.map(r => ({
      label: r.name,
      description: r.detail || '',
      detail: `${r.connectionName} / ${r.database}`,
      result: r,
    }));
    
    const selected = await vscode.window.showQuickPick(items, {
      placeHolder: `Found ${results.length} results`,
      matchOnDescription: true,
      matchOnDetail: true,
    });
    
    if (selected) {
      // 导航到结果
      navigateToResult(selected.result);
    }
  });
});

9.4 错误处理系统

ErrorHandler 设计

export class ErrorHandler {
  private outputChannel: vscode.OutputChannel;
  
  constructor(outputChannel: vscode.OutputChannel) {
    this.outputChannel = outputChannel;
  }
  
  handleError(error: Error | unknown, context?: string): void {
    const message = this.extractMessage(error);
    const code = this.extractCode(error);
    
    // 记录到输出通道
    this.outputChannel.appendLine(
      `[${new Date().toISOString()}] ERROR: ${context || ''} ${message}`
    );
    
    // 根据错误类型决定是否显示用户通知
    if (this.shouldNotifyUser(error)) {
      vscode.window.showErrorMessage(this.toUserMessage(error));
    }
  }
  
  extractMessage(error: unknown): string {
    if (error instanceof Error) return error.message;
    if (typeof error === 'string') return error;
    return String(error);
  }
  
  extractCode(error: unknown): string {
    if (error && typeof error === 'object' && 'code' in error) {
      return String((error as any).code);
    }
    return 'UNKNOWN';
  }
  
  shouldNotifyUser(error: unknown): boolean {
    const message = this.extractMessage(error);
    
    // 连接相关错误显示通知
    if (message.includes('connect') || 
        message.includes('ECONNREFUSED') ||
        message.includes('timeout')) {
      return true;
    }
    
    // Worker 崩溃显示通知
    if (message.includes('Worker') || message.includes('crash')) {
      return true;
    }
    
    return false;
  }
  
  toUserMessage(error: unknown): string {
    const message = this.extractMessage(error);
    
    // 简化技术细节
    if (message.includes('ECONNREFUSED')) {
      return 'VSDB: Connection refused. Check if database is running.';
    }
    if (message.includes('ETIMEDOUT')) {
      return 'VSDB: Connection timed out. Check network connectivity.';
    }
    if (message.includes('syntax error')) {
      return 'VSDB: SQL syntax error.';
    }
    if (message.includes('Access denied')) {
      return 'VSDB: Access denied. Check username/password.';
    }
    
    return `VSDB: ${message}`;
  }
}

Worker 崩溃回调

// extension.ts
const ipcManager = new IpcManager({
  workerScriptPath: workerPath,
  restartOnCrash: true,
  callbacks: {
    onWorkerCrash: (attempt: number) => {
      errorHandler?.handleError(new Error(`Worker crashed, restart attempt ${attempt}`));
      vscode.window.showWarningMessage(
        `VSDB: Worker process crashed. Restarting (attempt ${attempt})...`
      );
    },
    onWorkerRestarted: () => {
      vscode.window.showInformationMessage('VSDB: Worker process restarted successfully');
    },
    onConnectionsLost: (connectionIds: string[]) => {
      errorHandler?.handleError(new Error(`Connections lost: ${connectionIds.join(', ')}`));
      treeProvider?.refresh();  // 更新连接状态显示
    },
  },
});

9.5 小结

本章介绍了 VSDB 的辅助功能系统:

功能实现模块关键特性
查询历史HistoryManager上限 100 条,固定防清理
书签系统BookmarkManager分组管理,多种书签类型
全局搜索SearchEngineSchema 缓存,表/列/数据搜索
错误处理ErrorHandler输出通道记录,用户友好提示

关键设计:

  • 历史清理策略:优先清理未固定项
  • 书签分组:支持创建分组、移动书签
  • 搜索缓存:Schema 缓存 1 分钟提升性能
  • 错误友好化:技术错误转换为用户友好提示

下一章将介绍开发与调试流程。

第十章:开发与调试

本章介绍 VSDB 的开发环境配置、调试流程和测试策略。

10.1 开发环境配置

依赖安装

# 克隆项目
git clone https://github.com/yourname/vsdb.git
cd vsdb

# 安装扩展依赖
npm install --legacy-peer-deps

# 安装 Webview 依赖
cd webview-ui
npm install
cd ..

项目配置

// package.json 关键配置
{
  "engines": { "vscode": "^1.85.0" },
  "main": "./dist/extension.js",
  "scripts": {
    "vscode:prepublish": "npm run compile",
    "compile": "webpack --mode production",
    "watch": "webpack --mode development --watch",
    "build:webview": "cd webview-ui && npm run build",
    "test": "vitest run",
    "lint": "eslint src --ext ts"
  }
}

TypeScript 配置

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES2020",
    "outDir": "./dist",
    "rootDir": "./src",
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "webview-ui"]
}

10.2 调试配置

VSCode Launch 配置

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run Extension",
      "type": "extensionHost",
      "request": "launch",
      "runtimeExecutable": "${execPath}",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}"
      ],
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "preLaunchTask": "npm: watch"
    },
    {
      "name": "Extension Tests",
      "type": "extensionHost",
      "request": "launch",
      "runtimeExecutable": "${execPath}",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}",
        "--extensionTestsPath=${workspaceFolder}/dist/test"
      ],
      "preLaunchTask": "npm: test-compile"
    }
  ]
}

Tasks 配置

// .vscode/tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "id": "npm: watch",
      "type": "npm",
      "script": "watch",
      "problemMatcher": "$tsc-watch",
      "isBackground": true
    },
    {
      "id": "npm: compile",
      "type": "npm",
      "script": "compile"
    }
  ]
}

调试流程

graph TD
    A["F5 启动调试"] --> B["启动 Extension Development Host"]
    B --> C["加载扩展代码"]
    C --> D["激活扩展"]
    D --> E["测试功能"]
    
    E --> F["发现问题"]
    F --> G["修改代码"]
    G --> H{"代码类型"}
    
    H -->|"扩展代码"| I["自动重编译"]
    I --> J["重启扩展 Host"]
    
    H -->|"Webview 代码"| K["手动 rebuild"]
    K --> L["刷新 Webview"]
    
    J --> E
    L --> E

10.3 Webview 调试

开发模式构建

# Webview 开发模式(支持热重载)
cd webview-ui
npm run dev

Webview DevTools

// 在 Webview Panel 创建时启用 DevTools
const panel = vscode.window.createWebviewPanel(
  'vsdb.sqlEditor',
  'VSDB SQL Editor',
  vscode.ViewColumn.One,
  {
    enableScripts: true,
    retainContextWhenHidden: true,
    // 启用 DevTools(仅开发时)
    enableFindWidget: true,
  }
);

// 开发时可以打开 DevTools 检查 React 组件

消息调试

// 在 Webview 端添加调试日志
useEffect(() => {
  const handler = (event: MessageEvent) => {
    console.log('[Webview] Received:', event.data);
    handleExtensionMessage(event.data);
  };
  
  window.addEventListener('message', handler);
  return () => window.removeEventListener('message', handler);
}, []);

// 在扩展端添加调试日志
panel.webview.onDidReceiveMessage(message => {
  console.log('[Extension] Received:', message);
  handleWebviewMessage(message);
});

10.4 Worker 调试

Worker 日志

// worker.ts 启用详细日志
console.log('[Worker] Started');

process.on('message', (request: WorkerRequest) => {
  console.log('[Worker] Request:', request.type, request.id);
  handleMessage(request);
});

function send(message: WorkerResponse) {
  console.log('[Worker] Response:', message.type, message.id);
  process.send?.(message);
}

IPC 调试

// IpcManager.ts 启用详细日志
sendRequest(request): Promise<WorkerResponse> {
  console.log('[IPC] Sending:', request.type, request.id);
  
  return new Promise((resolve, reject) => {
    // ...
    
    this.worker.on('message', (message) => {
      console.log('[IPC] Received:', message.type, message.id);
      this.handleMessage(message);
    });
  });
}

模拟 Worker 崩溃

// 测试崩溃恢复
// 在 Worker 中手动触发崩溃
process.exit(1);

// 或发送无效消息
send({ type: 'invalid' } as any);

10.5 测试策略

测试架构

graph TD
    A["测试层级"] --> B["单元测试"]
    A --> C["集成测试"]
    A --> D["E2E 测试"]
    
    B --> B1["Parser 测试"]
    B --> B2["Manager 测试"]
    B --> B3["Driver 测试"]
    
    C --> C1["真实数据库连接"]
    C --> C2["IPC 通信测试"]
    
    D --> D1["VSCode Extension Tester"]
    D --> D2["UI 交互测试"]

单元测试示例

// __tests__/core/ipcManager.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { IpcManager } from '../../src/core/ipcManager';

describe('IpcManager', () => {
  let ipcManager: IpcManager;
  
  beforeEach(() => {
    ipcManager = new IpcManager({
      workerScriptPath: './dist/worker.js',
      requestTimeout: 5000,
    });
  });
  
  afterEach(async () => {
    await ipcManager.shutdown();
  });
  
  it('should start worker successfully', async () => {
    ipcManager.start();
    
    const ready = await ipcManager.waitReady(5000);
    expect(ready).toBe(true);
    expect(ipcManager.isRunning()).toBe(true);
  });
  
  it('should handle connect request', async () => {
    ipcManager.start();
    await ipcManager.waitReady();
    
    const response = await ipcManager.sendRequest({
      type: 'connect',
      connectionId: 'test-conn',
      payload: {
        config: {
          id: 'test-conn',
          name: 'test',
          type: 'mysql',
          host: 'localhost',
          port: 3306,
          username: 'root',
          password: '',
          scope: 'project',
        },
      },
    });
    
    expect(response.type).toBe('result');
  });
  
  it('should restart on crash', async () => {
    ipcManager.start();
    await ipcManager.waitReady();
    
    // 模拟崩溃
    const worker = ipcManager.getWorker();
    worker?.kill('SIGKILL');
    
    // 等待重启
    await new Promise(resolve => setTimeout(resolve, 2000));
    
    expect(ipcManager.isRunning()).toBe(true);
  });
});

Parser 测试示例

// __tests__/scanner/envParser.test.ts
import { describe, it, expect } from 'vitest';
import { EnvParser } from '../../src/scanner/envParser';
import * as fs from 'fs';
import * as path from 'path';

describe('EnvParser', () => {
  const parser = new EnvParser();
  
  it('should parse DATABASE_URL', async () => {
    // 创建临时测试文件
    const tempDir = './temp-test-env';
    const envFile = path.join(tempDir, '.env');
    
    fs.mkdirSync(tempDir, { recursive: true });
    fs.writeFileSync(envFile, 'DATABASE_URL=postgresql://user:pass@localhost:5432/testdb');
    
    const result = await parser.scan(tempDir);
    
    expect(result.connections.length).toBe(1);
    expect(result.connections[0].type).toBe('postgresql');
    expect(result.connections[0].host).toBe('localhost');
    expect(result.connections[0].port).toBe(5432);
    
    // 清理
    fs.rmSync(tempDir, { recursive: true });
  });
  
  it('should parse MYSQL_* variables', async () => {
    const tempDir = './temp-test-mysql';
    const envFile = path.join(tempDir, '.env');
    
    fs.mkdirSync(tempDir, { recursive: true });
    fs.writeFileSync(envFile, `
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=secret
MYSQL_DATABASE=myapp
    `.trim());
    
    const result = await parser.scan(tempDir);
    
    expect(result.connections.length).toBe(1);
    expect(result.connections[0].type).toBe('mysql');
    expect(result.connections[0].database).toBe('myapp');
    
    fs.rmSync(tempDir, { recursive: true });
  });
});

Vitest 配置

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      threshold: {
        lines: 80,
        functions: 80,
        branches: 80,
      },
    },
    testTimeout: 10000,
    hookTimeout: 10000,
  },
});

10.6 常用调试技巧

日志级别控制

// 根据环境变量控制日志
const DEBUG = process.env.VSDB_DEBUG === 'true';

function log(...args: any[]) {
  if (DEBUG) {
    console.log('[VSDB]', ...args);
  }
}

断点调试

// 在关键位置设置断点
debugger;  // VSCode 会在此暂停

// 或使用条件断点
if (request.type === 'query') {
  debugger;
}

状态检查

// 定期检查内部状态
setInterval(() => {
  console.log('Pending requests:', ipcManager.pendingRequestCount);
  console.log('Active connections:', ipcManager.getActiveConnectionIds());
}, 5000);

10.7 小结

本章介绍了 VSDB 的开发与调试:

主题内容
开发环境依赖安装、项目配置
调试配置launch.json、tasks.json
Webview 调试DevTools、消息日志
Worker 调试IPC 日志、崩溃模拟
测试策略单元测试、集成测试
测试工具Vitest、覆盖率要求

关键配置:

  • TypeScript strict:严格类型检查
  • 覆盖率阈值:80% lines/functions/branches
  • 热重载:扩展代码自动重编译
  • Webview 调试:React DevTools 可用

下一章将介绍扩展开发指南。

第十一章:扩展开发

本章介绍如何扩展 VSDB,包括添加新数据库支持、自定义功能和插件机制。

11.1 添加新数据库支持

扩展架构

graph TD
    A["新数据库支持"] --> B["驱动实现"]
    A --> C["Schema Inspector"]
    A --> D["配置解析器"]
    
    B --> B1["实现 Driver 接口"]
    B --> B2["connect/disconnect"]
    B --> B3["query/streamQuery"]
    
    C --> C1["getDatabases"]
    C --> C2["getTables"]
    C --> C3["getColumns"]
    
    D --> D1["更新 ScannerEngine"]
    D --> D2["添加 Parser"]

Driver 接口实现

以 SQLite 为例:

// src/worker/driver/sqlite.ts
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import type { DbConnection, QueryResult } from '../../shared/types';

export class SqliteDriver {
  private db: sqlite.Database | null = null;
  
  async connect(config: DbConnection): Promise<void> {
    // SQLite 连接配置使用文件路径
    const dbPath = config.host;  // SQLite 使用 host 字段存储路径
    
    this.db = await open({
      filename: dbPath,
      driver: sqlite3.Database,
    });
  }
  
  async disconnect(): Promise<void> {
    if (this.db) {
      await this.db.close();
      this.db = null;
    }
  }
  
  async query(sql: string, params?: any[]): Promise<QueryResult> {
    if (!this.db) {
      throw new Error('SQLite: not connected');
    }
    
    const startTime = Date.now();
    const result = await this.db.all(sql, params || []);
    const executionTime = Date.now() - startTime;
    
    const rows = result as Record<string, unknown>[];
    const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
    
    return {
      columns,
      rows,
      rowCount: rows.length,
      executionTime,
    };
  }
  
  async *streamQuery(
    sql: string, 
    params?: any[], 
    chunkSize = 1000
  ): AsyncGenerator<StreamChunkData> {
    if (!this.db) {
      throw new Error('SQLite: not connected');
    }
    
    const result = await this.db.all(sql, params || []);
    const rows = result;
    const totalRows = rows.length;
    
    for (let offset = 0; offset < rows.length; offset += chunkSize) {
      const chunk = rows.slice(offset, offset + chunkSize);
      const isLast = offset + chunkSize >= rows.length;
      
      yield {
        chunkIndex: Math.floor(offset / chunkSize),
        rows: chunk as Record<string, unknown>[],
        ...(isLast ? { totalRows } : {}),
      };
    }
  }
  
  getDb(): sqlite.Database | null {
    return this.db;
  }
}

Schema Inspector 实现

// src/worker/schema/sqliteSchema.ts
export class SqliteSchema {
  async getTables(db: sqlite.Database): Promise<SchemaResult> {
    const rows = await db.all(
      `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`
    );
    
    return {
      type: 'tables',
      data: rows.map(r => ({ name: r.name })),
    };
  }
  
  async getColumns(db: sqlite.Database, table: string): Promise<SchemaResult> {
    const rows = await db.all(`PRAGMA table_info(${table})`);
    
    return {
      type: 'columns',
      data: rows.map(r => ({
        name: r.name,
        type: r.type,
        nullable: r.notnull === 0,
        defaultValue: r.dflt_value,
        isPrimaryKey: r.pk === 1,
        isAutoIncrement: false,  // SQLite 无原生 auto_increment 标记
      })),
    };
  }
  
  async getIndexes(db: sqlite.Database, table: string): Promise<SchemaResult> {
    const rows = await db.all(`PRAGMA index_list(${table})`);
    const indexes: IndexInfo[] = [];
    
    for (const row of rows) {
      const indexInfo = await db.all(`PRAGMA index_info(${row.name})`);
      indexes.push({
        name: row.name,
        columns: indexInfo.map(i => i.name),
        isUnique: row.unique === 1,
        isPrimary: row.origin === 'pk',
      });
    }
    
    return {
      type: 'indexes',
      data: indexes,
    };
  }
  
  async getDDL(db: sqlite.Database, table: string): Promise<SchemaResult> {
    const row = await db.get(
      `SELECT sql FROM sqlite_master WHERE type='table' AND name=?`,
      table
    );
    
    return {
      type: 'ddl',
      data: [{ ddl: row?.sql || '' }],
    };
  }
}

Worker 路由更新

// worker.ts
import { SqliteDriver } from './driver/sqlite';
import { SqliteSchema } from './schema/sqliteSchema';

async function handleConnect(request: WorkerRequest): Promise<void> {
  const config = request.payload.config;
  
  let driver;
  switch (config.type) {
    case 'mysql':
      driver = new MySqlDriver();
      break;
    case 'postgresql':
      driver = new PostgreSqlDriver();
      break;
    case 'sqlite':  // 新增
      driver = new SqliteDriver();
      break;
    default:
      sendError(request.id, 'UNSUPPORTED_DATABASE', `Unsupported database type: ${config.type}`);
      return;
  }
  
  // ... 连接逻辑
}

async function executeSchema(schemaType: string, driver: any, ...) {
  // 新增 SQLite 路由
  if (config.type === 'sqlite') {
    const db = (driver as SqliteDriver).getDb();
    return sqliteSchema[schemaType](db, ...);
  }
  // ...
}

类型定义更新

// types.ts
export interface DbConnection {
  // ...
  type: 'mysql' | 'postgresql' | 'sqlite' | 'redis' | 'mongodb';
  // ...
}

11.2 自定义功能扩展

添加新命令

// extension.ts
const customCommand = vscode.commands.registerCommand('vsdb.customAction', async () => {
  // 实现自定义功能
  const selected = await vscode.window.showQuickPick([...]);
  
  if (selected) {
    // 处理逻辑
  }
});

context.subscriptions.push(customCommand);

扩展 TreeView

// treeItems.ts - 添加自定义节点类型
class CustomNode extends vscode.TreeItem {
  constructor(public data: any) {
    super('Custom Node', vscode.TreeItemCollapsibleState.None);
    this.contextValue = 'custom';
    this.iconPath = new vscode.ThemeIcon('star');
    this.command = {
      command: 'vsdb.customAction',
      title: 'Custom Action',
    };
  }
}

// treeProvider.ts - 返回自定义节点
async getChildren(element?: TreeItem): Promise<TreeItem[]> {
  if (!element) {
    const nodes: TreeItem[] = [];
    
    // 添加标准连接节点
    nodes.push(...await this.getConnectionNodes());
    
    // 添加自定义节点
    nodes.push(new CustomNode({ ... }));
    
    return nodes;
  }
}

扩展 Webview

// 创建新的 Webview Panel
export class CustomPanel {
  public static createOrShow(extensionUri: vscode.Uri): CustomPanel {
    const panel = vscode.window.createWebviewPanel(
      'vsdb.customPanel',
      'VSDB Custom Panel',
      vscode.ViewColumn.One,
      { enableScripts: true }
    );
    
    return new CustomPanel(panel, extensionUri);
  }
  
  private getHtml(): string {
    return `
      <!DOCTYPE html>
      <html>
        <head>
          <script src="${this.panel.webview.asWebviewUri(...)}"></script>
        </head>
        <body>
          <div id="root"></div>
          <script>
            // React 应用入口
          </script>
        </body>
      </html>
    `;
  }
}

11.3 配置解析器扩展

添加新 Parser

// src/scanner/customParser.ts
export class CustomParser {
  async scan(root: string): Promise<ScannerResult> {
    const result: ScannerResult = {
      connections: [],
      errors: [],
      scannedFiles: [],
    };
    
    // 查找特定配置文件
    const configFiles = this.findConfigFiles(root);
    
    for (const file of configFiles) {
      try {
        const content = fs.readFileSync(file, 'utf-8');
        const connections = this.parseContent(content, file);
        result.connections.push(...connections);
        result.scannedFiles.push(file);
      } catch (err: any) {
        result.errors.push({ file, error: err.message });
      }
    }
    
    return result;
  }
  
  private parseContent(content: string, sourceFile: string): ScannedConnection[] {
    // 自定义解析逻辑
    // ...
    
    return [{
      name: 'custom-connection',
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'user',
      password: 'pass',
      database: 'db',
      source: 'custom',
      sourceFile,
      confidence: 'high',
    }];
  }
}

注册到 ScannerEngine

// scanner.ts
export class ScannerEngine {
  private envParser = new EnvParser();
  private dockerComposeParser = new DockerComposeParser();
  private frameworkParser = new FrameworkParser();
  private customParser = new CustomParser();  // 新增
  
  async scan(root: string, existing: DbConnection[]): Promise<ScannerResult> {
    // 执行所有 Parser
    const results = [
      await this.envParser.scan(root),
      await this.dockerComposeParser.scan(root),
      await this.frameworkParser.scan(root),
      await this.customParser.scan(root),  // 新增
    ];
    
    // 合并结果
    // ...
  }
}

11.4 扩展最佳实践

代码组织

src/
├── worker/
│   ├── driver/
│   │   ├── mysql.ts      # 已有
│   │   ├── postgresql.ts # 已有
│   │   ├── sqlite.ts     # 新增
│   │   └── redis.ts      # 未来扩展
│   └── schema/
│       ├── mysqlSchema.ts
│       ├── pgSchema.ts
│       ├── sqliteSchema.ts  # 新增
├── scanner/
│   ├── envParser.ts
│   ├── dockerComposeParser.ts
│   ├── customParser.ts      # 新增

测试覆盖

// __tests__/worker/driver/sqlite.test.ts
describe('SqliteDriver', () => {
  it('should connect to sqlite file', async () => {
    const driver = new SqliteDriver();
    await driver.connect({
      type: 'sqlite',
      host: './test.db',
      // ...
    });
    
    expect(driver.isConnected()).toBe(true);
  });
  
  it('should execute query', async () => {
    // ...
  });
});

文档更新

// README.md
## Supported Databases

| Database | Status |
|----------|--------|
| MySQL | ✅ Full support |
| PostgreSQL | ✅ Full support |
| SQLite | ✅ Added in v0.2.0 |
| Redis | 🔜 Planned |

11.5 小结

本章介绍了 VSDB 的扩展开发:

扩展类型步骤
新数据库支持Driver + Schema Inspector + Worker 路由
自定义命令registerCommand + 实现
TreeView 扩展自定义节点 + TreeProvider
Webview 扩展新 Panel + React 组件
配置解析器Parser + ScannerEngine 注册

关键要点:

  • Driver 接口:遵循 connect/disconnect/query/streamQuery
  • Schema Inspector:实现 getTables/getColumns/getIndexes
  • Worker 路由:在 worker.ts 添加新类型判断
  • 类型更新:扩展 DbConnection.type
  • 测试覆盖:新增模块需要完整测试

下一章将介绍最佳实践。

第十二章:最佳实践

本章总结 VSDB 开发中的最佳实践,包括架构设计、性能优化、安全考虑和代码规范。

12.1 架构设计原则

进程隔离原则

graph TD
    A["架构原则"] --> B["进程隔离"]
    A --> C["异步优先"]
    A --> D["错误边界"]
    A --> E["状态持久化"]
    
    B --> B1["数据库操作在 Worker"]
    B --> B2["Worker 崩溃不影响 VSCode"]
    
    C --> C1["IPC 消息异步"]
    C --> C2["UI 不阻塞"]
    
    D --> D1["错误分类"]
    D --> D2["自动恢复"]
    
    E --> E1["连接配置 JSON"]
    E --> E2["密码 SecretStorage"]

关键原则总结

原则实现原因
进程隔离Worker 进程数据库操作异常不影响 VSCode
异步优先Promise + AsyncGeneratorUI 保持响应
错误边界try-catch + 错误分类友好错误提示
状态持久化JSON + SecretStorage配置不丢失

12.2 性能优化

IPC 优化

// 1. 流式传输避免大消息
async *streamQuery(sql: string): AsyncGenerator<StreamChunk> {
  for (let offset = 0; offset < rows.length; offset += 1000) {
    yield { rows: rows.slice(offset, offset + 1000) };
  }
}

// 2. 批量请求合并
async getFullSchema(connectionId: string): Promise<FullSchema> {
  // 并行发送多个 schema 请求
  const [databases, tables, columns] = await Promise.all([
    this.sendRequest({ type: 'schema', payload: { schemaType: 'databases' } }),
    this.sendRequest({ type: 'schema', payload: { schemaType: 'tables' } }),
    this.sendRequest({ type: 'schema', payload: { schemaType: 'columns' } }),
  ]);
  
  return { databases, tables, columns };
}

UI 渲染优化

// 1. 虚拟滚动
import { FixedSizeList } from 'react-window';

<FixedSizeList
  height={400}
  itemCount={rows.length}
  itemSize={35}
>
  {Row}
</FixedSizeList>

// 2. 惰性加载
async getChildren(element: TreeItem): Promise<TreeItem[]> {
  // 仅在展开时加载子节点
  if (element.collapsibleState === vscode.TreeItemCollapsibleState.Collapsed) {
    return [];  // 空数组,等待展开时加载
  }
  
  return await this.loadChildren(element);
}

// 3. 缓存策略
class SchemaCache {
  private cache = new Map<string, { data: any; expireAt: number }>();
  private ttl = 60000;  // 1 分钟
  
  get(key: string): any | null {
    const entry = this.cache.get(key);
    if (entry && entry.expireAt > Date.now()) {
      return entry.data;
    }
    this.cache.delete(key);
    return null;
  }
  
  set(key: string, data: any): void {
    this.cache.set(key, {
      data,
      expireAt: Date.now() + this.ttl,
    });
  }
}

数据库查询优化

// 1. 连接池配置(PostgreSQL)
const pool = new Pool({
  max: 5,                    // 限制连接数
  connectionTimeoutMillis: 10000,
  idleTimeoutMillis: 10000,
});

// 2. 查询超时
const queryQueue = new QueryQueue({
  maxConcurrency: 5,
  defaultTimeout: 30000,    // 30 秒超时
});

// 3. 结果限制
const sql = `${baseSql} LIMIT 10000`;  // 防止返回过多数据

12.3 安全考虑

密码存储

// 使用 VSCode SecretStorage API
export class Storage {
  constructor(
    private globalState: vscode.Memento,
    private secrets: vscode.SecretStorage,
  ) {}
  
  async savePassword(connectionId: string, password: string): Promise<void> {
    await this.secrets.store(`vsdb.password.${connectionId}`, password);
  }
  
  async getPassword(connectionId: string): Promise<string | undefined> {
    return await this.secrets.get(`vsdb.password.${connectionId}`);
  }
}

SQL 注入防护

// 使用参数化查询
async query(sql: string, params?: any[]): Promise<QueryResult> {
  // mysql2 和 pg 都支持参数化查询
  await this.connection.execute(sql, params);
}

// 不要拼接 SQL
// 错误示例:
const sql = `SELECT * FROM users WHERE name = '${name}'`;  // 危险!

// 正确示例:
const sql = `SELECT * FROM users WHERE name = ?`;
await this.connection.execute(sql, [name]);

连接字符串处理

// 安全解析 DATABASE_URL
parseConnectionString(url: string): ScannedConnection | null {
  try {
    const parsed = new URL(url);
    
    // 解码用户名和密码
    const username = decodeURIComponent(parsed.username || '');
    const password = decodeURIComponent(parsed.password || '');
    
    // 不暴露原始 URL
    return {
      username,
      password,
      // 不存储原始 URL
    };
  } catch {
    return null;
  }
}

12.4 错误处理规范

错误分类

type ErrorClass = 'connection' | 'syntax' | 'timeout' | 'permission' | 'unknown';

interface ClassifiedError {
  errorClass: ErrorClass;
  retryable: boolean;
  userMessage: string;
}

const ERROR_MESSAGES: Record<ErrorClass, string> = {
  connection: 'Connection failed. Check if database is running.',
  syntax: 'SQL syntax error.',
  timeout: 'Query timed out. Try again or simplify query.',
  permission: 'Access denied. Check credentials.',
  unknown: 'An unexpected error occurred.',
};

错误恢复策略

错误类型策略
connection重试 3 次,提示检查服务
syntax不重试,显示错误位置
timeout提示重试或简化查询
permission不重试,提示检查权限
Worker crash自动重启,恢复连接

用户友好提示

toUserMessage(error: unknown): string {
  const message = this.extractMessage(error);
  
  // 技术错误转换为用户友好提示
  const mappings: Array<{ pattern: RegExp; message: string }> = [
    { pattern: /ECONNREFUSED/, message: 'Cannot connect to database. Is it running?' },
    { pattern: /ETIMEDOUT/, message: 'Connection timed out. Check network.' },
    { pattern: /syntax error/, message: 'SQL syntax error. Check your query.' },
    { pattern: /Access denied/, message: 'Permission denied. Check username/password.' },
  ];
  
  for (const { pattern, message } of mappings) {
    if (pattern.test(message)) {
      return `VSDB: ${message}`;
    }
  }
  
  return `VSDB: ${message}`;
}

12.5 代码规范

TypeScript 规范

// 1. 严格类型
// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
  }
}

// 2. 接口优先
interface DbConnection {
  id: string;
  name: string;
  // ...
}

// 3. 枚举替代字符串常量
type DatabaseType = 'mysql' | 'postgresql' | 'sqlite';

// 4. 明确的返回类型
async query(sql: string): Promise<QueryResult> {
  // ...
}

命名规范

// 文件命名
connectionManager.ts    // Manager 类
envParser.ts            // Parser 类
mysqlDriver.ts          // Driver 类

// 类命名
class ConnectionManager {}
class EnvParser {}
class MySqlDriver {}

// 接口命名
interface DbConnection {}
interface WorkerRequest {}

// 函数命名
async getConnection() {}      // 获取
async addConnection() {}      // 添加
async deleteConnection() {}   // 删除
async executeQuery() {}       // 执行

// 变量命名
const connectionId = '...';   // camelCase
const MAX_CONNECTIONS = 5;    // 常量 UPPER_CASE

文档规范

/**
 * IPC Manager for communicating with the DB Worker process.
 * 
 * Responsibilities:
 * - Spawn and manage Worker process lifecycle
 * - Send WorkerRequest messages and match responses
 * - Handle Worker crashes and auto-restart
 * 
 * @example
 * const ipc = new IpcManager({ workerScriptPath: './worker.js' });
 * ipc.start();
 * const response = await ipc.sendRequest({ type: 'query', ... });
 */
export class IpcManager {
  // ...
}

12.6 测试规范

测试覆盖率要求

// vitest.config.ts
coverage: {
  threshold: {
    lines: 80,
    functions: 80,
    branches: 80,
  },
}

测试命名

describe('IpcManager', () => {
  describe('start', () => {
    it('should start worker process', async () => {
      // ...
    });
    
    it('should throw if worker script not found', async () => {
      // ...
    });
  });
  
  describe('sendRequest', () => {
    it('should return response for valid request', async () => {
      // ...
    });
    
    it('should timeout for long request', async () => {
      // ...
    });
  });
});

测试结构

describe('Module', () => {
  let instance: Module;
  
  beforeEach(() => {
    instance = new Module();
  });
  
  afterEach(() => {
    instance.dispose();
  });
  
  describe('feature', () => {
    it('should do something', async () => {
      // 准备
      const input = '...';
      
      // 执行
      const result = await instance.doSomething(input);
      
      // 验证
      expect(result).toBe('...');
    });
  });
});

12.7 小结

本章总结了 VSDB 开发的最佳实践:

主题关键点
架构设计进程隔离、异步优先、错误边界、状态持久化
性能优化流式传输、虚拟滚动、缓存、并行请求
安全考虑SecretStorage、参数化查询、不暴露原始 URL
错误处理分类、友好提示、恢复策略
代码规范严格类型、接口优先、命名规范
测试规范覆盖率 80%、describe/it 结构

这些最佳实践确保 VSDB 的:

  • 稳定性:Worker 崩溃不影响 VSCode
  • 性能:大数据集流畅渲染
  • 安全性:密码加密、防注入
  • 可维护性:清晰代码结构、完整测试

附录 A:类型定义速查

本附录汇总 VSDB 所有核心类型定义。

A.1 连接相关类型

// DbConnection - 数据库连接配置
export interface DbConnection {
  id: string;                    // UUID
  name: string;                  // 用户自定义名称
  type: 'mysql' | 'postgresql';  // 数据库类型
  host: string;                  // 主机地址
  port: number;                  // 端口号
  username: string;              // 用户名
  password: string;              // 密码
  database?: string;             // 默认数据库
  options?: Record<string, any>; // 扩展选项
  group?: string;                // 分组名称
  color?: string;                // 颜色标记
  scope: 'project' | 'global';   // 存储范围
}

// ConnectionState - 连接状态
export interface ConnectionState {
  connectionId: string;
  status: 'disconnected' | 'connecting' | 'connected' | 'error';
  error?: string;
  activeDatabase?: string;
  lastQuery?: string;
  lastQueryTime?: number;
}

// ScannedConnection - 扫描发现的连接
export interface ScannedConnection {
  name: string;
  type: 'mysql' | 'postgresql';
  host: string;
  port: number;
  username: string;
  password: string;
  database?: string;
  source: 'env' | 'docker-compose' | 'framework';
  sourceFile: string;
  confidence: 'high' | 'medium' | 'low';
}

// ScannerResult - 扫描结果
export interface ScannerResult {
  connections: ScannedConnection[];
  errors: Array<{ file: string; error: string }>;
  scannedFiles: string[];
}

A.2 IPC 协议类型

// WorkerRequest - Worker 请求
export interface WorkerRequest {
  id: string;
  type: 'connect' | 'disconnect' | 'query' | 'streamQuery' | 
        'schema' | 'cancel' | 'ping' | 'shutdown';
  connectionId: string;
  payload: {
    config?: DbConnection;
    sql?: string;
    params?: unknown[];
    schemaType?: SchemaType;
    database?: string;
    table?: string;
  };
}

// WorkerResponse - Worker 响应
export interface WorkerResponse {
  id: string;
  type: 'result' | 'stream' | 'streamEnd' | 'error' | 'pong';
  data?: QueryResult | SchemaResult;
  error?: WorkerError;
}

// StreamChunk - 流式分块
export interface StreamChunk {
  requestId: string;
  chunkIndex: number;
  rows: Record<string, unknown>[];
  totalRows?: number;
}

// WorkerError - Worker 错误
export interface WorkerError {
  code: string;
  message: string;
  errorClass?: 'connection' | 'syntax' | 'timeout' | 'permission' | 'unknown';
  retryable?: boolean;
}

type SchemaType = 'databases' | 'tables' | 'columns' | 'views' | 
                  'procedures' | 'indexes' | 'constraints' | 'triggers' | 'ddl';

A.3 查询结果类型

// QueryResult - 查询结果
export interface QueryResult {
  columns: string[];
  rows: Record<string, unknown>[];
  rowCount: number;
  affectedRows?: number;
  executionTime: number;
}

// SchemaResult - Schema 查询结果
export interface SchemaResult {
  type: SchemaType;
  data: unknown[];
}

A.4 Schema 信息类型

// TableInfo - 表信息
export interface TableInfo {
  name: string;
  schema?: string;
  columns: ColumnInfo[];
  indexes: IndexInfo[];
  rowCount?: number;
}

// ColumnInfo - 列信息
export interface ColumnInfo {
  name: string;
  type: string;
  nullable: boolean;
  defaultValue?: string;
  isPrimaryKey: boolean;
  isAutoIncrement: boolean;
}

// IndexInfo - 索引信息
export interface IndexInfo {
  name: string;
  columns: string[];
  isUnique: boolean;
  isPrimary: boolean;
}

// ViewInfo - 视图信息
export interface ViewInfo {
  name: string;
  schema?: string;
  definition?: string;
}

// ProcedureInfo - 存储过程信息
export interface ProcedureInfo {
  name: string;
  schema?: string;
  parameters?: any[];
}

// SchemaInfo - 完整 Schema 信息
export interface SchemaInfo {
  tables: TableInfo[];
  views: ViewInfo[];
  procedures: ProcedureInfo[];
}

A.5 辅助功能类型

// QueryHistoryItem - 查询历史项
export interface QueryHistoryItem {
  id: string;
  sql: string;
  connectionId: string;
  connectionName: string;
  executedAt: Date;
  rowCount?: number;
  executionTime?: number;
  pinned?: boolean;
}

// Bookmark - 书签
export interface Bookmark {
  id: string;
  type: 'connection' | 'database' | 'table' | 'query';
  name: string;
  target: {
    connectionId?: string;
    database?: string;
    table?: string;
    sql?: string;
  };
  createdAt: Date;
  groupId?: string;
}

// BookmarkGroup - 书签分组
export interface BookmarkGroup {
  id: string;
  name: string;
  color?: string;
}

// SearchResult - 搜索结果
export interface SearchResult {
  type: 'table' | 'column' | 'data';
  connectionId: string;
  connectionName: string;
  database: string;
  name: string;
  detail?: string;
}

// SearchOptions - 搜索选项
export interface SearchOptions {
  searchTables?: boolean;
  searchColumns?: boolean;
  searchData?: boolean;
  connectionIds?: string[];
  tables?: string[];
  caseSensitive?: boolean;
}

// SchemaCacheEntry - Schema 缓存条目
export interface SchemaCacheEntry {
  tables: TableInfo[];
  columns: ColumnInfo[];
  cachedAt: Date;
}

A.6 默认端口常量

// constants.ts
export const DEFAULT_PORTS = {
  mysql: 3306,
  postgresql: 5432,
  sqlite: 0,  // SQLite 无端口概念
  redis: 6379,
  mongodb: 27017,
};

A.7 命令和视图常量

// constants.ts
export const VIEWS = {
  CONNECTIONS: 'vsdb.connections',
};

export const COMMANDS = {
  ADD_CONNECTION: 'vsdb.addConnection',
  SCAN_PROJECT: 'vsdb.scanProject',
  REMOVE_CONNECTION: 'vsdb.removeConnection',
  CONNECT: 'vsdb.connect',
  DISCONNECT: 'vsdb.disconnect',
  NEW_QUERY: 'vsdb.newQuery',
  SEARCH: 'vsdb.search',
  EXPORT_DATA: 'vsdb.exportData',
  IMPORT_DATA: 'vsdb.importData',
};

export const TREE_COMMANDS = {
  CONNECT: 'vsdb.tree.connect',
  DISCONNECT: 'vsdb.tree.disconnect',
  NEW_QUERY: 'vsdb.tree.newQuery',
  COPY_CONNECTION_STRING: 'vsdb.tree.copyConnectionString',
  EDIT_CONNECTION: 'vsdb.tree.editConnection',
  DELETE_CONNECTION: 'vsdb.tree.deleteConnection',
  VIEW_TABLE_STRUCTURE: 'vsdb.viewTableStructure',
  REFRESH: 'vsdb.tree.refresh',
};

附录 B:IPC协议参考

本附录详细描述 VSDB IPC 通信协议。

B.1 协议概述

VSDB 使用 JSON 序列化的消息通过 child_process.send() 进行 IPC 通信。

graph LR
    A["Extension Host"] -->|"JSON over IPC"| B["Worker Process"]
    B -->|"JSON over IPC"| A

B.2 请求类型

connect

建立数据库连接。

{
  "id": "uuid-request",
  "type": "connect",
  "connectionId": "uuid-connection",
  "payload": {
    "config": {
      "id": "uuid-connection",
      "name": "dev-mysql",
      "type": "mysql",
      "host": "localhost",
      "port": 3306,
      "username": "root",
      "password": "secret",
      "database": "myapp",
      "scope": "project"
    }
  }
}

disconnect

断开数据库连接。

{
  "id": "uuid-request",
  "type": "disconnect",
  "connectionId": "uuid-connection",
  "payload": {}
}

query

执行 SQL 查询。

{
  "id": "uuid-request",
  "type": "query",
  "connectionId": "uuid-connection",
  "payload": {
    "sql": "SELECT * FROM users LIMIT 10",
    "params": []
  }
}

streamQuery

执行流式查询(大数据集)。

{
  "id": "uuid-request",
  "type": "streamQuery",
  "connectionId": "uuid-connection",
  "payload": {
    "sql": "SELECT * FROM large_table",
    "params": []
  }
}

schema

查询数据库元信息。

{
  "id": "uuid-request",
  "type": "schema",
  "connectionId": "uuid-connection",
  "payload": {
    "schemaType": "tables",
    "database": "myapp"
  }
}

schemaType 可选值:

  • databases - 数据库列表
  • tables - 表列表
  • columns - 列信息(需指定 table)
  • views - 视图列表
  • procedures - 存储过程列表
  • indexes - 索引信息(需指定 table)
  • constraints - 约束信息(需指定 table)
  • triggers - 触发器信息(需指定 table)
  • ddl - DDL 语句(需指定 table)

cancel

取消流式查询。

{
  "id": "uuid-stream-request",
  "type": "cancel",
  "connectionId": "",
  "payload": {}
}

ping

心跳检测。

{
  "id": "heartbeat",
  "type": "ping",
  "connectionId": "",
  "payload": {}
}

shutdown

关闭 Worker。

{
  "id": "",
  "type": "shutdown",
  "connectionId": "",
  "payload": {}
}

B.3 响应类型

result

成功响应。

{
  "id": "uuid-request",
  "type": "result",
  "data": {
    "columns": ["id", "name", "email"],
    "rows": [
      { "id": 1, "name": "Alice", "email": "alice@example.com" },
      { "id": 2, "name": "Bob", "email": "bob@example.com" }
    ],
    "rowCount": 2,
    "executionTime": 15
  }
}

streamEnd

流式查询结束标记。

{
  "id": "uuid-request",
  "type": "streamEnd"
}

error

错误响应。

{
  "id": "uuid-request",
  "type": "error",
  "error": {
    "code": "QUERY_ERROR",
    "message": "Syntax error near 'SELCT'",
    "errorClass": "syntax",
    "retryable": false
  }
}

错误码列表:

  • CONNECT_FAILED - 连接失败
  • NOT_CONNECTED - 未连接
  • MISSING_SQL - 缺少 SQL
  • MISSING_CONFIG - 缺少配置
  • QUERY_ERROR - 查询错误
  • STREAM_ERROR - 流式查询错误
  • SCHEMA_ERROR - Schema 查询错误
  • UNKNOWN_REQUEST_TYPE - 未知的请求类型
  • WORKER_MEMORY_EXCEEDED - 内存超限

pong

心跳响应。

{
  "id": "heartbeat",
  "type": "pong"
}

B.4 流式分块

StreamChunk 格式:

{
  "requestId": "uuid-request",
  "chunkIndex": 0,
  "rows": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" },
    // ... 最多 1000 行
  ],
  "totalRows": 2500  // 仅最后一块包含
}

B.5 时序图示例

普通查询

sequenceDiagram
    participant Host as Extension Host
    participant Worker as Worker
    
    Host->>Worker: { type: "query", sql: "SELECT ..." }
    Worker-->>Host: { type: "result", data: { rows: [...] } }

流式查询

sequenceDiagram
    participant Host as Extension Host
    participant Worker as Worker
    
    Host->>Worker: { type: "streamQuery", sql: "SELECT ..." }
    Worker-->>Host: { requestId, chunkIndex: 0, rows: [...] }
    Worker-->>Host: { requestId, chunkIndex: 1, rows: [...] }
    Worker-->>Host: { requestId, chunkIndex: 2, rows: [...], totalRows: 2500 }
    Worker-->>Host: { type: "streamEnd" }

连接失败

sequenceDiagram
    participant Host as Extension Host
    participant Worker as Worker
    
    Host->>Worker: { type: "connect", config: {...} }
    Worker-->>Host: { type: "error", error: { code: "CONNECT_FAILED" } }

B.6 超时配置

操作类型默认超时说明
connect60s连接建立
query60s普通查询
streamQuery120s流式查询(较长)
streamChunk30s分块间隔超时
heartbeat30s心跳超时

B.7 错误分类

errorClass典型错误retryable
connectionECONNREFUSED, CONNECTION_LOST
syntaxsyntax error, ER_PARSE_ERROR
timeoutETIMEDOUT, query timeout
permissionAccess denied, permission denied
unknown其他

附录 C:命令与菜单

本附录列出 VSDB 所有命令、菜单和快捷键。

C.1 全局命令

命令 ID标题功能
vsdb.addConnectionVSDB: Add Connection打开连接表单添加新连接
vsdb.scanProjectVSDB: Scan Project扫描项目配置发现连接
vsdb.removeConnectionVSDB: Remove Connection选择并删除连接
vsdb.connectVSDB: Connect连接选中的数据库
vsdb.disconnectVSDB: Disconnect断开选中连接
vsdb.newQueryVSDB: New Query打开 SQL 编辑器
vsdb.searchVSDB: Search打开搜索对话框
vsdb.exportDataVSDB: Export Data导出当前结果
vsdb.importDataVSDB: Import Data导入数据

C.2 树节点命令

命令 ID标题适用节点功能
vsdb.tree.connectConnectconnection连接数据库
vsdb.tree.disconnectDisconnectconnection-connected断开连接
vsdb.tree.newQueryNew Queryconnection-connected, database, table打开 SQL 编辑器
vsdb.tree.copyConnectionStringCopy Connection Stringconnection, database复制连接字符串
vsdb.tree.editConnectionEdit Connectionconnection编辑连接配置
vsdb.tree.deleteConnectionDelete Connectionconnection删除连接
vsdb.tree.refreshRefreshconnection, database刷新节点
vsdb.viewTableStructureView Table Structuretable查看表结构

C.3 视图工具栏

命令图标位置功能
vsdb.addConnection$(add)navigation@1添加连接
vsdb.tree.refresh$(refresh)navigation@2刷新树
vsdb.scanProject$(search)navigation@3扫描项目

C.4 右键菜单

view/title(视图标题栏)

{
  "view/title": [
    {
      "command": "vsdb.addConnection",
      "when": "view == vsdb.connections",
      "group": "navigation@1"
    },
    {
      "command": "vsdb.tree.refresh",
      "when": "view == vsdb.connections",
      "group": "navigation@2"
    },
    {
      "command": "vsdb.scanProject",
      "when": "view == vsdb.connections",
      "group": "navigation@3"
    }
  ]
}

view/item/context(节点右键)

{
  "view/item/context": [
    // 连接节点内联按钮
    {
      "command": "vsdb.tree.connect",
      "when": "view == vsdb.connections && viewItem == connection",
      "group": "inline@1"
    },
    {
      "command": "vsdb.tree.disconnect",
      "when": "view == vsdb.connections && viewItem == connection-connected",
      "group": "inline@1"
    },
    
    // 查询相关
    {
      "command": "vsdb.tree.newQuery",
      "when": "view == vsdb.connections && viewItem == connection-connected",
      "group": "1_query@1"
    },
    
    // 表结构
    {
      "command": "vsdb.viewTableStructure",
      "when": "view == vsdb.connections && viewItem == table",
      "group": "1_actions@1"
    },
    
    // 管理操作
    {
      "command": "vsdb.tree.copyConnectionString",
      "when": "view == vsdb.connections && viewItem =~ /^connection/",
      "group": "2_manage@1"
    },
    {
      "command": "vsdb.tree.editConnection",
      "when": "view == vsdb.connections && viewItem =~ /^connection/",
      "group": "3_edit@1"
    },
    {
      "command": "vsdb.tree.deleteConnection",
      "when": "view == vsdb.connections && viewItem =~ /^connection/",
      "group": "3_edit@2"
    },
    
    // 刷新
    {
      "command": "vsdb.tree.refresh",
      "when": "view == vsdb.connections && viewItem =~ /^(connection|database)/",
      "group": "4_refresh@1"
    }
  ]
}

C.5 viewItem 类型

viewItem 值节点类型说明
connectionConnectionNode (disconnected)未连接的连接节点
connection-connectedConnectionNode (connected)已连接的连接节点
connection-connectingConnectionNode (connecting)正在连接
connection-errorConnectionNode (error)连接错误
databaseDatabaseNode数据库节点
tableTableNode表节点
columnColumnNode列节点
historyHistoryNode历史记录节点
history-pinnedHistoryNode (pinned)固定的历史记录
bookmarkBookmarkNode书签节点

C.6 SQL Editor 快捷键

快捷键功能
Ctrl+Enter / Cmd+Enter执行当前 SQL
Ctrl+S / Cmd+S保存查询

C.7 树节点图标

状态图标颜色
connected$(database)charts.green
connecting$(loading~spin)-
disconnected$(database)-
error$(error)charts.red
database$(folder)-
table$(table)-
column$(symbol-field)-
primary key$(key)-
index$(list-unordered)-

C.8 命令注册示例

// extension.ts
const addConnectionCmd = vscode.commands.registerCommand(
  'vsdb.addConnection',
  () => {
    ConnectionFormPanel.show(connectionManager, ipcManager, undefined, () => {
      treeProvider?.refresh();
    });
  }
);

const scanProjectCmd = vscode.commands.registerCommand(
  'vsdb.scanProject',
  async () => {
    const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath;
    if (!workspaceRoot) {
      vscode.window.showWarningMessage('No workspace folder open.');
      return;
    }
    
    vscode.window.withProgress({
      location: vscode.ProgressLocation.Notification,
      title: 'VSDB: Scanning project...',
    }, async () => {
      const result = await scannerEngine!.scan(workspaceRoot, existingConnections);
      // 处理结果...
    });
  }
);

context.subscriptions.push(
  addConnectionCmd,
  scanProjectCmd,
  // ...
);

附录 D:配置参考

本附录列出 VSDB 的配置文件和选项。

D.1 连接存储位置

项目级连接

.vsdb/connections.json

示例内容:

{
  "connections": [
    {
      "id": "uuid-1",
      "name": "dev-mysql",
      "type": "mysql",
      "host": "localhost",
      "port": 3306,
      "username": "root",
      "database": "myapp",
      "scope": "project",
      "group": "development",
      "color": "blue"
    }
  ]
}

全局连接

~/.vsdb/connections.json

格式与项目级相同,scope"global"

密码存储

密码通过 VSCode SecretStorage API 加密存储,键名格式:

vsdb.password.{connectionId}

D.2 IpcManager 配置

interface IpcManagerOptions {
  workerScriptPath: string;         // Worker 脚本路径(必填)
  restartOnCrash?: boolean;         // 崩溃时重启(默认 true)
  maxRestartAttempts?: number;      // 最大重启次数(默认 3)
  requestTimeout?: number;          // 请求超时(默认 60000ms)
  heartbeatInterval?: number;       // 心跳间隔(默认 15000ms)
  heartbeatTimeout?: number;        // 心跳超时(默认 30000ms)
  streamChunkTimeout?: number;      // 分块超时(默认 30000ms)
  gracefulShutdownTimeout?: number; // 关闭超时(默认 10000ms)
  callbacks?: IpcManagerCallbacks;  // 事件回调
}

默认值:

const DEFAULT_REQUEST_TIMEOUT = 60000;      // 60 秒
const DEFAULT_MAX_RESTART_ATTEMPTS = 3;
const DEFAULT_HEARTBEAT_INTERVAL = 15000;   // 15 秒
const DEFAULT_HEARTBEAT_TIMEOUT = 30000;    // 30 秒
const DEFAULT_STREAM_CHUNK_TIMEOUT = 30000; // 30 秒
const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT = 10000; // 10 秒

D.3 QueryQueue 配置

interface QueryQueueOptions {
  maxConcurrency?: number;      // 最大并发(默认 5)
  defaultTimeout?: number;      // 默认超时(默认 30000ms)
}

D.4 连接选项

DbConnection 的 options 字段支持:

MySQL 选项

{
  charset?: string;             // 字符集(如 'utf8mb4')
  timezone?: string;            // 时区(如 '+08:00'')
  ssl?: {                       // SSL 配置
    rejectUnauthorized?: boolean;
    ca?: string;
    cert?: string;
    key?: string;
  };
  multipleStatements?: boolean; // 多语句支持(默认 false)
}

PostgreSQL 选项

{
  ssl?: boolean | {             // SSL 配置
    rejectUnauthorized?: boolean;
    ca?: string;
    cert?: string;
    key?: string;
  };
  applicationName?: string;     // 应用名
  connectionTimeoutMillis?: number; // 连接超时
  idleTimeoutMillis?: number;   // 空闲超时
}

D.5 扫描配置

EnvParser

const ENV_FILE_PATTERNS = [
  '.env',
  '.env.local',
  '.env.development',
  '.env.production',
  '.env.test',
  '.env.staging',
];

const MAX_DEPTH = 3;  // 最大搜索深度

解析的环境变量:

变量类型说明
DATABASE_URLPostgreSQL/MySQL URL完整连接字符串
DB_URLPostgreSQL/MySQL URL同 DATABASE_URL
DB_HOSTHost需配合 DB_PORT 等
DB_PORTPort默认 PostgreSQL
DB_USERUsername-
DB_PASSWORDPassword-
DB_NAMEDatabase-
MYSQL_HOSTHost (MySQL)-
MYSQL_PORTPort (MySQL)默认 3306
MYSQL_USERUsername-
MYSQL_PASSWORDPassword-
MYSQL_DATABASEDatabase-
PG_HOSTHost (PostgreSQL)-
PG_PORTPort (PostgreSQL)默认 5432
PG_USERUsername-
PG_PASSWORDPassword-
PG_DATABASEDatabase-

DockerComposeParser

识别的镜像:

  • 包含 mysqlmariadb → MySQL
  • 包含 postgrespostgresql → PostgreSQL

解析的环境变量:

变量类型说明
MYSQL_ROOT_PASSWORDPasswordMySQL root 密码
MYSQL_USERUsernameMySQL 用户
MYSQL_PASSWORDPasswordMySQL 用户密码
MYSQL_DATABASEDatabaseMySQL 数据库
POSTGRES_USERUsernamePostgreSQL 用户
POSTGRES_PASSWORDPasswordPostgreSQL 密码
POSTGRES_DBDatabasePostgreSQL 数据库

D.6 HistoryManager 配置

const maxItems = 100;  // 最大历史记录数

清理策略:

  • 超过上限时优先清理未固定项
  • 全部固定时清理最旧的固定项

D.7 SearchEngine 配置

const cacheTimeout = 60000;  // Schema 缓存时间(1 分钟)

搜索选项默认值:

选项默认值
searchTablestrue
searchColumnstrue
searchDatafalse(较慢)
caseSensitivefalse

D.8 扩展 manifest 配置

// package.json
{
  "name": "vsdb",
  "displayName": "VSDB - Database Client",
  "version": "0.1.0",
  "engines": {
    "vscode": "^1.85.0"
  },
  "categories": ["Database", "Other"],
  "activationEvents": ["onView:vsdb.connections"],
  "main": "./dist/extension.js"
}

D.9 TypeScript 配置

// tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "ES2020",
    "outDir": "./dist",
    "rootDir": "./src",
    "sourceMap": true,
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

D.10 Vitest 配置

// vitest.config.ts
export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    coverage: {
      provider: 'v8',
      threshold: {
        lines: 80,
        functions: 80,
        branches: 80,
      },
    },
  },
});

附录 E:常见问题

本附录解答 VSDB 开发和使用中的常见问题。

E.1 开发问题

Q: Worker 启动后立即退出

原因:Worker 脚本路径错误或未正确编译。

解决方案

  1. 确保 npm run compile 已执行
  2. 检查 worker.js 是否存在于 dist/ 目录
  3. 检查 IpcManager 的 workerScriptPath 配置
const workerPath = vscode.extensions.getExtension('vsdb.vsdb')?.extensionPath
  ? path.join(extensionPath, 'dist', 'worker.js')
  : path.join(__dirname, 'worker.js');

Q: IPC 消息无响应

原因:Worker 未就绪或请求超时。

解决方案

  1. 使用 waitReady() 等待 Worker 就绪
  2. 检查超时配置是否合理
  3. 检查 Worker 日志是否有错误
ipcManager.start();
await ipcManager.waitReady(5000);
const response = await ipcManager.sendRequest({ ... });

Q: Webview 无法接收消息

原因:Webview 未正确初始化或消息监听未设置。

解决方案

  1. 确保 Webview 启用了 enableScripts
  2. 确保 React 组件正确监听 window.message
// extension 端
panel.webview.postMessage({ type: 'connections', payload: [...] });

// React 端
window.addEventListener('message', (event) => {
  handleExtensionMessage(event.data);
});

Q: Monaco Editor 加载缓慢

原因:Monaco 从 CDN 加载需要网络。

解决方案

  1. 使用本地 Monaco 资源
  2. 或配置 CDN 加载路径
loader.config({
  paths: {
    vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.50.0/min/vs'
  }
});

E.2 连接问题

Q: 连接数据库失败

常见原因

  1. 数据库服务未启动
  2. 网络不通或防火墙阻止
  3. 用户名/密码错误
  4. 端口配置错误

排查步骤

  1. 检查数据库服务是否运行
  2. 使用 telnet host port 测试网络
  3. 检查连接配置是否正确
  4. 查看 VSDB 输出通道日志

Q: MySQL 连接超时

解决方案

  1. 增加 connectTimeout 配置
  2. 检查 MySQL 是否允许远程连接
  3. 检查防火墙规则
await mysql.createConnection({
  host: config.host,
  connectTimeout: 20000,  // 20 秒
});

Q: PostgreSQL SSL 连接失败

解决方案

  1. 配置 SSL 选项
  2. 或禁用 SSL(仅开发环境)
const pool = new Pool({
  host: config.host,
  ssl: config.ssl || false,  // 开发环境可禁用
});

E.3 性能问题

Q: 大数据查询卡顿

解决方案

  1. 使用流式查询 streamQuery
  2. 前端使用虚拟滚动
  3. 添加 LIMIT 限制
// 使用流式查询
await ipcManager.sendRequest({
  type: 'streamQuery',
  payload: { sql: 'SELECT * FROM large_table' },
});

// 前端虚拟滚动
<FixedSizeList height={400} itemCount={rows.length} itemSize={35}>
  {Row}
</FixedSizeList>

Q: TreeView 展开缓慢

原因:每次展开都查询 Schema。

解决方案

  1. 使用 Schema 缓存
  2. 预加载常用节点
// 缓存 Schema
const cached = schemaCache.get(connectionId);
if (cached) {
  return cached.tables;
}

E.4 扩展问题

Q: 扩展激活失败

常见原因

  1. VSCode 版本不满足
  2. 依赖未安装
  3. 入口文件路径错误

排查步骤

  1. 检查 engines.vscode 配置
  2. 执行 npm install
  3. 检查 main 配置路径
{
  "engines": { "vscode": "^1.85.0" },
  "main": "./dist/extension.js"
}

Q: 打包扩展体积过大

解决方案

  1. 使用 .vscodeignore 排除不必要文件
  2. 避免打包测试文件
  3. 使用 webpack 优化
.vscodeignore:
__tests__/**
node_modules/@types/**
*.test.ts

E.5 测试问题

Q: 测试数据库连接失败

解决方案

  1. 使用 Docker 启动测试数据库
  2. 或使用 mock 驱动
# docker-compose.yaml
services:
  mysql-test:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: test

Q: Vitest 测试超时

解决方案

  1. 增加测试超时配置
  2. 或优化测试代码
// vitest.config.ts
test: {
  testTimeout: 30000,  // 30 秒
}

E.6 使用问题

Q: 扫描未发现连接

原因

  1. 配置文件格式不正确
  2. 扫描深度限制
  3. 文件被跳过

解决方案

  1. 检查配置文件语法
  2. 确保配置文件在项目根目录附近
  3. 检查 .env 文件命名
有效的文件名:
.env
.env.local
.env.development

无效的文件名:
.env.prod(不在标准列表)
.env.backup(扩展名不匹配)

Q: 密码无法保存

原因:SecretStorage API 问题。

解决方案

  1. 确保 VSCode 版本 >= 1.85.0
  2. 检查 SecretStorage 权限
await context.secrets.store(`vsdb.password.${id}`, password);

Q: 查询历史丢失

原因

  1. 全局状态未持久化
  2. 清理策略删除了历史

解决方案

  1. 固定重要查询
  2. 检查 globalState 存储
// 固定查询防止被清理
historyManager.pin(historyId);

E.7 其他问题

Q: 如何调试 Worker

解决方案

  1. 在 Worker 中添加日志
  2. 使用 VSCode Attach to Process
// worker.ts
console.log('[Worker] Request:', request);

Q: 如何添加新数据库支持

参考第十一章扩展开发。

Q: 如何贡献代码

  1. Fork 项目
  2. 创建功能分支
  3. 提交 PR
git checkout -b feature/new-database
npm run test
npm run lint
git push origin feature/new-database