mysql.adapter.ts•9.5 kB
import { BaseDatabaseAdapter } from './base.adapter.js';
import {
  DatabaseConfig,
  QueryResult,
  TableInfo,
  QueryOptions,
  DatabaseType,
  SchemaInfo,
  ColumnInfo,
} from '../../types/database.types.js';
import { Logger } from '../../core/logger.js';
// Mock mysql2 module for testing purposes
interface MockConnection {
  query: (sql: string, values?: any[]) => Promise<[any, any]>;
  execute: (sql: string, values?: any[]) => Promise<[any, any]>;
  end: () => Promise<void>;
}
interface MockMysql {
  createConnection: (config: any) => Promise<MockConnection>;
}
// This will be mocked in tests
const defaultMysql: MockMysql = {
  createConnection: async () => {
    throw new Error('MySQL adapter requires mysql2 library to be installed');
  },
};
/**
 * MySQL Database Adapter
 */
export class MySQLAdapter extends BaseDatabaseAdapter {
  private connection: MockConnection | null = null;
  private mysql: MockMysql;
  constructor(config: DatabaseConfig, logger?: Logger, mysql?: MockMysql) {
    super(config, logger);
    this.mysql = mysql || defaultMysql;
  }
  /**
   * Connect to database
   */
  async connect(): Promise<void> {
    try {
      const config = {
        host: this.config.host,
        port: this.config.port || 3306,
        database: this.config.database,
        user: this.config.user,
        password: this.config.password,
        ssl: this.config.ssl || false,
        connectTimeout: this.config.options?.connectionTimeout || 60000,
        acquireTimeout: this.config.options?.requestTimeout || 60000,
        timeout: this.config.options?.requestTimeout || 60000,
      };
      this.connection = await this.mysql.createConnection(config);
      this.connected = true;
    } catch (error) {
      this.connected = false;
      throw new Error(
        `MySQL connection failed: ${error instanceof Error ? error.message : String(error)}`
      );
    }
  }
  /**
   * Disconnect from database
   */
  async disconnect(): Promise<void> {
    try {
      if (this.connection) {
        await this.connection.end();
        this.connection = null;
      }
      this.connected = false;
    } catch (error) {
      throw new Error(
        `MySQL disconnection failed: ${error instanceof Error ? error.message : String(error)}`
      );
    }
  }
  /**
   * Execute query
   */
  async executeQuery(query: string, options?: QueryOptions): Promise<QueryResult> {
    if (!this.connection) {
      throw new Error('Not connected to database.');
    }
    const startTime = Date.now();
    try {
      let result: any;
      if (options?.parameters && options.parameters.length > 0) {
        [result] = await this.connection.execute(query, options.parameters);
      } else {
        [result] = await this.connection.query(query);
      }
      const executionTime = Date.now() - startTime;
      // Handle different result types
      let rowsAffected = 0;
      let recordset: any[] | undefined = undefined;
      if (Array.isArray(result)) {
        recordset = result;
        rowsAffected = result.length;
      } else if (result && typeof result === 'object') {
        if ('affectedRows' in result) {
          rowsAffected = result.affectedRows || 0;
        }
        if ('insertId' in result) {
          recordset = [{ insertId: result.insertId }];
        }
      }
      return {
        success: true,
        recordset,
        data: recordset,
        rowsAffected,
        executionTime,
      };
    } catch (error) {
      const executionTime = Date.now() - startTime;
      return {
        success: false,
        error: error instanceof Error ? error.message : String(error),
        rowsAffected: 0,
        executionTime,
      };
    }
  }
  /**
   * Get table list (returns TableInfo array)
   */
  async getTables(): Promise<TableInfo[]> {
    const query = `
      SELECT 
        TABLE_NAME as name,
        TABLE_SCHEMA as tableSchema,
        TABLE_TYPE as type
      FROM INFORMATION_SCHEMA.TABLES
      WHERE TABLE_SCHEMA = DATABASE()
        AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
      ORDER BY TABLE_SCHEMA, TABLE_NAME
    `;
    const result = await this.executeQuery(query);
    if (!result.success || !result.recordset) {
      return [];
    }
    return result.recordset.map(row => ({
      name: row.name,
      schema: row.tableSchema,
      type: row.type === 'BASE TABLE' ? ('table' as const) : ('view' as const),
      columns: [],
      primaryKeys: [],
      foreignKeys: [],
      indexes: [],
    }));
  }
  /**
   * Get table name list (returns string array)
   */
  async getTableNames(): Promise<string[]> {
    const query = `
      SELECT 
        TABLE_NAME as name
      FROM INFORMATION_SCHEMA.TABLES
      WHERE TABLE_SCHEMA = DATABASE()
        AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
      ORDER BY TABLE_NAME
    `;
    const result = await this.executeQuery(query);
    if (!result.success || !result.recordset) {
      return [];
    }
    return result.recordset.map(row => row.name);
  }
  /**
   * Get table detailed information list (maintained for implementation compatibility)
   */
  async getTableInfos(): Promise<TableInfo[]> {
    return this.getTables();
  }
  /**
   * Get specific table information
   */
  async getTableInfo(tableName: string): Promise<TableInfo> {
    // Get column information
    const columnsQuery = `
      SELECT 
        COLUMN_NAME as name,
        DATA_TYPE as type,
        CHARACTER_MAXIMUM_LENGTH as maxLength,
        NUMERIC_PRECISION as precision,
        NUMERIC_SCALE as scale,
        IS_NULLABLE as isNullable,
        COLUMN_DEFAULT as defaultValue,
        ORDINAL_POSITION as ordinalPosition,
        EXTRA as extra
      FROM INFORMATION_SCHEMA.COLUMNS
      WHERE TABLE_SCHEMA = DATABASE()
        AND TABLE_NAME = ?
      ORDER BY ORDINAL_POSITION
    `;
    const columnsResult = await this.executeQuery(columnsQuery, {
      parameters: [tableName],
    });
    // Get primary key information
    const primaryKeysQuery = `
      SELECT 
        COLUMN_NAME as name
      FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
      WHERE TABLE_SCHEMA = DATABASE()
        AND TABLE_NAME = ?
        AND CONSTRAINT_NAME = 'PRIMARY'
    `;
    const primaryKeysResult = await this.executeQuery(primaryKeysQuery, {
      parameters: [tableName],
    });
    // Get foreign key information
    const foreignKeysQuery = `
      SELECT 
        COLUMN_NAME as name,
        REFERENCED_TABLE_NAME as referencedTable,
        REFERENCED_COLUMN_NAME as referencedColumn
      FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
      WHERE TABLE_SCHEMA = DATABASE()
        AND TABLE_NAME = ?
        AND REFERENCED_TABLE_NAME IS NOT NULL
    `;
    const foreignKeysResult = await this.executeQuery(foreignKeysQuery, {
      parameters: [tableName],
    });
    // Get index information
    const indexesQuery = `
      SELECT DISTINCT
        INDEX_NAME as name
      FROM INFORMATION_SCHEMA.STATISTICS
      WHERE TABLE_SCHEMA = DATABASE()
        AND TABLE_NAME = ?
        AND INDEX_NAME != 'PRIMARY'
    `;
    const indexesResult = await this.executeQuery(indexesQuery, {
      parameters: [tableName],
    });
    const primaryKeyNames = new Set(primaryKeysResult.recordset?.map(row => row.name) || []);
    const columns: ColumnInfo[] =
      columnsResult.recordset?.map(row => ({
        name: row.name,
        dataType: row.type,
        maxLength: row.maxLength,
        precision: row.precision,
        scale: row.scale,
        isNullable: row.isNullable === 'YES',
        isIdentity: row.extra?.includes('auto_increment') || false,
        isPrimaryKey: primaryKeyNames.has(row.name),
        defaultValue: row.defaultValue,
        ordinalPosition: row.ordinalPosition,
      })) || [];
    return {
      name: tableName,
      schema: this.config.database,
      type: 'table',
      columns,
      primaryKeys: primaryKeysResult.recordset?.map(row => row.name) || [],
      foreignKeys: foreignKeysResult.recordset || [],
      indexes: indexesResult.recordset || [],
    };
  }
  /**
   * Get schema information
   */
  async getSchema(includeDetailedInfo: boolean = true): Promise<SchemaInfo> {
    const tables = await this.getTables();
    // If detailed info is requested, populate each table's detailed information
    if (includeDetailedInfo && tables.length > 0) {
      for (let i = 0; i < tables.length; i++) {
        try {
          const tableInfo = await this.getTableInfo(tables[i].name);
          tables[i].columns = tableInfo.columns;
          tables[i].primaryKeys = tableInfo.primaryKeys;
          tables[i].foreignKeys = tableInfo.foreignKeys;
          tables[i].indexes = tableInfo.indexes;
        } catch (error) {
          // Keep empty arrays for failed tables
        }
      }
    }
    return {
      name: this.config.database,
      tables,
      views: tables.filter(t => t.type === 'view'),
      functions: [], // MySQL function query (separate implementation needed)
      procedures: [], // MySQL procedure query (separate implementation needed)
    };
  }
  /**
   * Test connection status
   */
  async testConnection(): Promise<boolean> {
    try {
      if (!this.connection) return false;
      const result = await this.executeQuery('SELECT 1 as test');
      return result.success && result.recordset?.[0]?.test === 1;
    } catch {
      return false;
    }
  }
  /**
   * Return database type
   */
  getType(): DatabaseType {
    return 'mysql';
  }
}