Skip to main content
Glama
YUChoe

SQLite MCP Server

by YUChoe
QueryUtils.ts6.88 kB
/** * SQL 쿼리 실행 유틸리티 * 매개변수화된 쿼리, 트랜잭션 처리, 오류 분류를 위한 헬퍼 함수들 */ import { QueryResult, Operation, SQLErrorType } from '../types/index'; /** * SQL 쿼리 타입 감지 */ export function detectQueryType(sql: string): 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE' | 'CREATE' | 'DROP' | 'ALTER' | 'PRAGMA' | 'UNKNOWN' { const trimmedSql = sql.trim().toUpperCase(); if (trimmedSql.startsWith('SELECT')) return 'SELECT'; if (trimmedSql.startsWith('INSERT')) return 'INSERT'; if (trimmedSql.startsWith('UPDATE')) return 'UPDATE'; if (trimmedSql.startsWith('DELETE')) return 'DELETE'; if (trimmedSql.startsWith('CREATE')) return 'CREATE'; if (trimmedSql.startsWith('DROP')) return 'DROP'; if (trimmedSql.startsWith('ALTER')) return 'ALTER'; if (trimmedSql.startsWith('PRAGMA')) return 'PRAGMA'; return 'UNKNOWN'; } /** * SQL 매개변수 검증 */ export function validateSQLParams(sql: string, params: any[]): boolean { // ? 플레이스홀더 개수 계산 const placeholderCount = (sql.match(/\?/g) || []).length; return placeholderCount === params.length; } /** * 안전한 테이블명 검증 */ export function validateTableName(tableName: string): boolean { // SQLite 테이블명 규칙: 문자, 숫자, 언더스코어만 허용, 숫자로 시작 불가 const tableNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/; return tableNameRegex.test(tableName) && tableName.length <= 64; } /** * 안전한 컬럼명 검증 */ export function validateColumnName(columnName: string): boolean { // SQLite 컬럼명 규칙: 테이블명과 동일 return validateTableName(columnName); } /** * SQL 인젝션 기본 검사 */ export function basicSQLInjectionCheck(sql: string): boolean { const dangerousPatterns = [ /;\s*(DROP|DELETE|UPDATE|INSERT|CREATE|ALTER)\s+/i, /UNION\s+SELECT/i, /--/, /\/\*/, /\*\//, /xp_/i, /sp_/i ]; return !dangerousPatterns.some(pattern => pattern.test(sql)); } /** * 트랜잭션 작업 빌더 */ export class TransactionBuilder { private operations: Operation[] = []; /** * 작업 추가 */ addOperation(sql: string, params?: any[]): TransactionBuilder { this.operations.push({ sql, params: params || [] }); return this; } /** * INSERT 작업 추가 */ insert(tableName: string, data: Record<string, any>): TransactionBuilder { if (!validateTableName(tableName)) { throw new Error(`Invalid table name: ${tableName}`); } const columns = Object.keys(data); const values = Object.values(data); const placeholders = columns.map(() => '?').join(', '); const sql = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`; return this.addOperation(sql, values); } /** * UPDATE 작업 추가 */ update(tableName: string, data: Record<string, any>, whereClause: string, whereParams?: any[]): TransactionBuilder { if (!validateTableName(tableName)) { throw new Error(`Invalid table name: ${tableName}`); } const columns = Object.keys(data); const values = Object.values(data); const setClause = columns.map(col => `${col} = ?`).join(', '); const sql = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`; const params = [...values, ...(whereParams || [])]; return this.addOperation(sql, params); } /** * DELETE 작업 추가 */ delete(tableName: string, whereClause: string, whereParams?: any[]): TransactionBuilder { if (!validateTableName(tableName)) { throw new Error(`Invalid table name: ${tableName}`); } const sql = `DELETE FROM ${tableName} WHERE ${whereClause}`; return this.addOperation(sql, whereParams || []); } /** * 작업 목록 반환 */ build(): Operation[] { return [...this.operations]; } /** * 작업 목록 초기화 */ clear(): TransactionBuilder { this.operations = []; return this; } } /** * 쿼리 결과 검증 */ export function validateQueryResult(result: QueryResult, expectedType: 'SELECT' | 'MODIFY'): boolean { if (!result.success) { return false; } if (expectedType === 'SELECT') { return Array.isArray(result.data); } else { return typeof result.rowsAffected === 'number'; } } /** * SQL 오류 분류 */ export function classifySQLError(error: Error): SQLErrorType { const message = error.message.toLowerCase(); if (message.includes('syntax error') || message.includes('near')) { return SQLErrorType.SYNTAX_ERROR; } if (message.includes('no such table')) { return SQLErrorType.TABLE_NOT_EXISTS; } if (message.includes('no such column')) { return SQLErrorType.COLUMN_NOT_EXISTS; } if (message.includes('constraint') || message.includes('unique') || message.includes('foreign key')) { return SQLErrorType.CONSTRAINT_VIOLATION; } if (message.includes('type') || message.includes('affinity')) { return SQLErrorType.TYPE_MISMATCH; } return SQLErrorType.SYNTAX_ERROR; // 기본값 } /** * 쿼리 성능 측정 래퍼 */ export function measureQueryPerformance<T>( operation: () => T, queryType: string, onComplete?: (duration: number, queryType: string) => void ): T { const startTime = process.hrtime.bigint(); try { const result = operation(); const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; // 밀리초로 변환 if (onComplete) { onComplete(duration, queryType); } return result; } catch (error) { const endTime = process.hrtime.bigint(); const duration = Number(endTime - startTime) / 1_000_000; if (onComplete) { onComplete(duration, `${queryType}_ERROR`); } throw error; } } /** * 배치 작업 처리 */ export function createBatchOperations( tableName: string, records: Record<string, any>[], batchSize: number = 100 ): Operation[][] { if (!validateTableName(tableName)) { throw new Error(`Invalid table name: ${tableName}`); } if (records.length === 0) { return []; } const batches: Operation[][] = []; const columns = Object.keys(records[0]); const placeholders = columns.map(() => '?').join(', '); const sql = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`; for (let i = 0; i < records.length; i += batchSize) { const batch = records.slice(i, i + batchSize); const operations: Operation[] = batch.map(record => ({ sql, params: columns.map(col => record[col]) })); batches.push(operations); } return batches; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/YUChoe/sqlite-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server