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,包括添加新数据库支持、自定义功能和插件机制。

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
  • 测试覆盖:新增模块需要完整测试

下一章将介绍最佳实践。