UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

510 lines 18 kB
/** * Hybrid Executor - Multi-Phase Query Execution Engine * * The Hybrid Executor executes query plans using the optimal combination * of SQL, JSONata, and post-processing phases. It handles data flow * between phases and manages parallel execution when possible. */ import jsonata from 'jsonata'; import { getLogger } from '../../logging/Logger.js'; import { SQLBuilder } from './SQLBuilder.js'; import { QueryExecutionError } from './types.js'; const logger = getLogger(); /** * Hybrid Executor implementation */ export class HybridExecutor { adapters; fieldCatalog; config; resultCache; sqlBuilder; constructor(adapters, fieldCatalog, config = {}) { this.adapters = adapters; this.fieldCatalog = fieldCatalog; this.config = config; this.resultCache = new Map(); this.sqlBuilder = new SQLBuilder(fieldCatalog); logger.info('HybridExecutor initialized'); } /** * Execute a query plan */ async execute(plan) { logger.info(`Executing plan ${plan.id} with strategy: ${plan.strategy}`); // L7-11 FIX: Handle error queries early if (plan.query.error || plan.query.find === 'error') { const errorMessage = plan.query.error || 'Invalid query'; logger.warn(`Error query detected: ${errorMessage}`); return { data: [], metadata: { rowCount: 0, executionTime: 0, cacheHit: false, plan, error: errorMessage }, plan }; } // Check cache if enabled if (this.config.cacheResults) { const cached = this.getCachedResult(plan); if (cached) { logger.debug(`Cache hit for plan ${plan.id}`); return cached; } } // Initialize execution context const context = { plan, intermediateResults: new Map(), startTime: Date.now(), memoryUsage: 0, warnings: [] }; try { // Execute phases let finalResult; if (this.shouldExecuteParallel(plan)) { finalResult = await this.executeParallel(context); } else { finalResult = await this.executeSequential(context); } // Create result const result = this.createResult(finalResult, context); // Cache if enabled if (this.config.cacheResults) { this.cacheResult(plan, result); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Execution failed for plan ${plan.id}: ${error}`); throw new QueryExecutionError(`Failed to execute query: ${errorMessage}`, { plan, error }); } } /** * Execute phases sequentially */ async executeSequential(context) { let currentData = []; for (const phase of context.plan.phases) { logger.debug(`Executing phase: ${phase.name}`); // Get input data const inputData = phase.inputFrom ? context.intermediateResults.get(phase.inputFrom) || [] : currentData; // Execute phase const phaseResult = await this.executePhase(phase, inputData, context); // Store output if (phase.outputTo) { context.intermediateResults.set(phase.outputTo, phaseResult); } currentData = phaseResult; // Check execution time if (this.config.maxExecutionTime) { const elapsed = Date.now() - context.startTime; if (elapsed > this.config.maxExecutionTime) { throw new Error(`Execution timeout after ${elapsed}ms`); } } } return currentData; } /** * Execute phases in parallel where possible */ async executeParallel(context) { // Group phases by dependencies const phaseGroups = this.groupPhasesByDependencies(context.plan.phases); let finalResult = []; for (const group of phaseGroups) { // Execute phases in group in parallel const groupPromises = group.map(phase => { const inputData = phase.inputFrom ? context.intermediateResults.get(phase.inputFrom) || [] : finalResult; return this.executePhase(phase, inputData, context) .then(result => ({ phase, result })); }); const groupResults = await Promise.all(groupPromises); // Store results for (const { phase, result } of groupResults) { if (phase.outputTo) { context.intermediateResults.set(phase.outputTo, result); } finalResult = result; // Keep last result } } return finalResult; } /** * Execute a single phase */ async executePhase(phase, inputData, context) { const phaseStart = Date.now(); try { let result; switch (phase.type) { case 'sql': result = await this.executeSQLPhase(phase, inputData); break; case 'jsonata': result = await this.executeJSONataPhase(phase, inputData); break; case 'post-process': result = await this.executePostProcessPhase(phase, inputData); break; default: throw new Error(`Unknown phase type: ${phase.type}`); } const phaseTime = Date.now() - phaseStart; logger.debug(`Phase ${phase.name} completed in ${phaseTime}ms`); // Update memory usage estimate context.memoryUsage += this.estimateMemoryUsage(result); return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Phase ${phase.name} failed: ${error}`); context.warnings.push(`Phase ${phase.name} failed: ${errorMessage}`); throw error; } } /** * Execute SQL phase */ async executeSQLPhase(phase, inputData) { // Get appropriate adapter const adapter = this.getAdapterForEntity(phase.query.find || 'default'); if (!adapter) { throw new Error('No adapter available for SQL execution'); } // Build SQL query using SQLBuilder const sqlQuery = await this.sqlBuilder.buildSQL(phase.query); // Debug SQL generation logger.debug('Generated SQL query:', sqlQuery); logger.debug('Query entity:', phase.query.find); logger.debug('Query conditions:', JSON.stringify(phase.query.where, null, 2)); // Execute via adapter const results = await adapter.executeNativeQuery(sqlQuery); // Enhanced GROUP BY result tracking logger.debug('SQL execution complete'); logger.debug(`Raw results count: ${results.length}`); logger.debug(`Has GROUP BY: ${phase.query.groupBy && phase.query.groupBy.length > 0}`); if (phase.query.groupBy && phase.query.groupBy.length > 0) { logger.debug(`GROUP BY fields: ${JSON.stringify(phase.query.groupBy)}`); logger.debug(`First 3 GROUP BY results: ${JSON.stringify(results.slice(0, 3))}`); // Check if results are empty when they shouldn't be if (results.length === 0) { logger.warn('GROUP BY query returned 0 results!'); logger.warn(`SQL was: ${sqlQuery}`); } } // Debug aggregation results if (phase.query.aggregations && phase.query.aggregations.length > 0) { logger.debug(`SQL results count = ${results.length}`); if (results.length > 0) { logger.debug('First result =', JSON.stringify(results[0])); } } return results; } /** * Execute JSONata phase */ async executeJSONataPhase(phase, inputData) { const expression = phase.query.expression; if (!expression) { throw new Error('JSONata phase missing expression'); } try { // Compile expression const compiled = jsonata(expression); // Apply to input data const result = await compiled.evaluate(inputData); // Ensure result is array return Array.isArray(result) ? result : [result]; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`JSONata evaluation failed: ${errorMessage}`); } } /** * Execute post-processing phase */ async executePostProcessPhase(phase, inputData) { let result = [...inputData]; // Apply GROUP BY if (phase.query.groupBy) { result = this.applyGroupBy(result, phase.query.groupBy); } // Apply HAVING if (phase.query.having) { result = this.applyHaving(result, phase.query.having); } // Apply ORDER BY if (phase.query.orderBy) { result = this.applyOrderBy(result, phase.query.orderBy); } return result; } /** * Build SQL query from phase query object */ buildSQLQuery(query, inputData) { // Simplified SQL building - real implementation would be more complex let sql = 'SELECT '; // SELECT clause if (query.select && query.select.length > 0) { sql += query.select.join(', '); } else { sql += '*'; } // FROM clause sql += ` FROM ${query.find}`; // JOINs if (query.joins) { for (const join of query.joins) { sql += ` ${join.type} JOIN ${join.entity}`; sql += ` ON ${join.on.leftField} = ${join.on.rightField}`; } } // WHERE clause if (query.where && query.where.length > 0) { const conditions = query.where.map((c) => `${c.field} ${c.operator} '${c.value}'`).join(' AND '); sql += ` WHERE ${conditions}`; } // GROUP BY if (query.groupBy) { sql += ` GROUP BY ${query.groupBy.join(', ')}`; } // ORDER BY if (query.orderBy) { const orderClauses = query.orderBy.map((o) => `${o.field} ${o.direction}`).join(', '); sql += ` ORDER BY ${orderClauses}`; } // LIMIT/OFFSET if (query.limit) { sql += ` LIMIT ${query.limit}`; } if (query.offset) { sql += ` OFFSET ${query.offset}`; } return sql; } /** * Apply GROUP BY to data */ applyGroupBy(data, fields) { const groups = new Map(); for (const row of data) { const key = fields.map(f => row[f]).join('|'); if (!groups.has(key)) { groups.set(key, []); } groups.get(key).push(row); } // Create grouped results const results = []; for (const [key, group] of groups) { const groupedRow = {}; // Add group fields fields.forEach((field, i) => { groupedRow[field] = key.split('|')[i]; }); // Add count groupedRow._count = group.length; results.push(groupedRow); } return results; } /** * Apply HAVING conditions */ applyHaving(data, conditions) { return data.filter(row => { for (const condition of conditions) { if (!this.evaluateCondition(row, condition)) { return false; } } return true; }); } /** * Apply ORDER BY to data */ applyOrderBy(data, orderBy) { return data.sort((a, b) => { for (const order of orderBy) { const aVal = a[order.field]; const bVal = b[order.field]; if (aVal < bVal) return order.direction === 'ASC' ? -1 : 1; if (aVal > bVal) return order.direction === 'ASC' ? 1 : -1; } return 0; }); } /** * Evaluate a condition against a row */ evaluateCondition(row, condition) { const value = row[condition.field]; const compareValue = condition.value; switch (condition.operator) { case '=': return value === compareValue; case '!=': return value !== compareValue; case '<': return value < compareValue; case '>': return value > compareValue; case '<=': return value <= compareValue; case '>=': return value >= compareValue; case 'IN': return Array.isArray(compareValue) && compareValue.includes(value); case 'NOT IN': return Array.isArray(compareValue) && !compareValue.includes(value); case 'LIKE': return String(value).includes(String(compareValue)); case 'IS NULL': return value == null; case 'IS NOT NULL': return value != null; default: return true; } } /** * Create final result object */ createResult(data, context) { const executionTime = Date.now() - context.startTime; // Track result transformation logger.debug(`createResult called with ${data.length} rows`); if (context.plan.query.groupBy && context.plan.query.groupBy.length > 0) { logger.debug('GROUP BY query result transformation'); logger.debug(`First result before transformation: ${JSON.stringify(data[0])}`); } const metadata = { rowCount: data.length, executionTime, cacheHit: false, warnings: context.warnings.length > 0 ? context.warnings : undefined }; // Add pagination metadata if applicable if (context.plan.query.limit) { const page = Math.floor((context.plan.query.offset || 0) / context.plan.query.limit) + 1; metadata.pagination = { page, pageSize: context.plan.query.limit, totalPages: Math.ceil(context.plan.estimatedRows / context.plan.query.limit), totalRows: context.plan.estimatedRows }; } const result = { data, metadata, plan: context.plan }; // L2-1 DEBUG: Final result structure logger.info(`L2-1 DEBUG: Final QueryResult structure: data.length=${result.data.length}, has metadata=${!!result.metadata}`); return result; } /** * Group phases by dependencies for parallel execution */ groupPhasesByDependencies(phases) { const groups = []; const processed = new Set(); while (processed.size < phases.length) { const group = []; for (const phase of phases) { if (processed.has(phase)) continue; // Check if dependencies are satisfied const canExecute = !phase.inputFrom || phases.filter(p => p.outputTo === phase.inputFrom) .every(p => processed.has(p)); if (canExecute) { group.push(phase); } } // Add group to processed group.forEach(p => processed.add(p)); groups.push(group); } return groups; } /** * Check if plan should be executed in parallel */ shouldExecuteParallel(plan) { return this.config.enableParallel !== false && plan.strategy === 'parallel' && plan.phases.some(p => p.parallel); } /** * Get adapter for entity */ getAdapterForEntity(entity) { // For now, return first adapter const [, adapter] = this.adapters.entries().next().value || [null, null]; return adapter; } /** * Estimate memory usage of result */ estimateMemoryUsage(data) { // Rough estimate - 1KB per row return data.length * 1024; } /** * Get cached result */ getCachedResult(plan) { const cacheKey = this.getCacheKey(plan); const cached = this.resultCache.get(cacheKey); if (cached) { cached.metadata.cacheHit = true; return cached; } return null; } /** * Cache result */ cacheResult(plan, result) { const cacheKey = this.getCacheKey(plan); this.resultCache.set(cacheKey, result); // Simple cache eviction - keep only last 100 results if (this.resultCache.size > 100) { const firstKey = this.resultCache.keys().next().value; if (firstKey) { this.resultCache.delete(firstKey); } } } /** * Generate cache key for plan */ getCacheKey(plan) { return JSON.stringify({ query: plan.query, strategy: plan.strategy }); } } //# sourceMappingURL=HybridExecutor.js.map