@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
510 lines • 18 kB
JavaScript
/**
* 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