@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
390 lines • 16.2 kB
JavaScript
/**
* Query Analysis Engine - Phase 3.2 of Dynamic Analytics Query Engine
*
* This engine analyzes SQL queries to determine:
* 1. Which fields are being accessed and how
* 2. Whether array flattening is required
* 3. Query complexity and optimization opportunities
* 4. Performance characteristics and bottlenecks
*
* The engine integrates with the UnifiedFieldResolver from Phase 3.1 to provide
* intelligent query planning that prevents the "variations.key does not exist" errors
* by understanding data structure requirements before execution.
*/
import { UnifiedFieldResolver } from './UnifiedFieldResolver.js';
import { getLogger } from '../logging/Logger.js';
const logger = getLogger();
/**
* Main Query Analysis Engine
*/
export class QueryAnalysisEngine {
fieldResolver;
version = '1.0.0';
constructor() {
this.fieldResolver = new UnifiedFieldResolver();
logger.info('QueryAnalysisEngine initialized');
}
/**
* Analyze a complete query and provide recommendations
*/
async analyzeQuery(query, context) {
const startTime = performance.now();
try {
logger.debug('Starting query analysis');
// Step 1: Parse the SQL query
const parsedQuery = this.parseQuery(query, context);
// Step 2: Resolve all referenced fields
const fieldResolutions = await this.resolveQueryFields(parsedQuery, context);
// Step 3: Analyze flattening requirements
const flatteningAnalysis = this.analyzeFlatteningRequirements(fieldResolutions, parsedQuery);
// Step 4: Perform optimization analysis
const optimization = this.analyzeOptimization(parsedQuery, fieldResolutions, flatteningAnalysis);
// Step 5: Generate overall recommendation
const recommendation = this.generateRecommendation(parsedQuery, flatteningAnalysis, optimization);
const analysisTime = performance.now() - startTime;
const result = {
parsedQuery,
fieldResolutions,
flatteningAnalysis,
optimization,
recommendation,
metadata: {
analysisTime,
analyzerVersion: this.version,
contextUsed: context
}
};
logger.info('Query analysis completed');
return result;
}
catch (error) {
logger.error('Query analysis failed');
throw new Error(`Query analysis failed: ${error.message}`);
}
}
/**
* Parse SQL query into structured representation
*
* Note: This is a simplified parser. In production, consider using
* a proper SQL parser like node-sql-parser or similar.
*/
parseQuery(query, context) {
// Implementation placeholder - this would be a comprehensive SQL parser
// For now, we'll create a basic implementation that handles common patterns
const normalizedQuery = query.trim().toUpperCase();
// Detect operation type
let operation = 'SELECT';
if (normalizedQuery.startsWith('INSERT'))
operation = 'INSERT';
else if (normalizedQuery.startsWith('UPDATE'))
operation = 'UPDATE';
else if (normalizedQuery.startsWith('DELETE'))
operation = 'DELETE';
// Extract referenced fields (simplified - real parser would be much more sophisticated)
const referencedFields = this.extractFieldReferences(query);
return {
originalQuery: query,
operation,
primaryEntity: context.primaryEntity,
referencedFields,
filterFields: this.extractFilterFields(query),
projectionFields: this.extractProjectionFields(query),
groupByFields: this.extractGroupByFields(query),
orderByFields: this.extractOrderByFields(query),
joins: this.extractJoins(query),
subqueries: [] // Simplified - would recursively parse subqueries
};
}
/**
* Resolve all fields referenced in the query using UnifiedFieldResolver
*/
async resolveQueryFields(parsedQuery, context) {
try {
const result = this.fieldResolver.resolveFields(parsedQuery.referencedFields, context);
return result.resolvedFields;
}
catch (error) {
logger.warn('Some fields could not be resolved');
// Return partial results rather than failing completely
return [];
}
}
/**
* Analyze which fields require flattening and estimate impact
*/
analyzeFlatteningRequirements(fieldResolutions, parsedQuery) {
const arrayFields = [];
const objectFields = [];
for (const field of fieldResolutions) {
if (field.requiresFlattening) {
const requirement = {
fieldName: field.originalInput,
resolvedField: field,
flatteningType: field.isArray ? 'array_to_rows' : 'object_to_columns',
nestingDepth: this.calculateNestingDepth(field.accessPath),
estimatedCardinality: this.estimateCardinality(field)
};
if (field.isArray) {
arrayFields.push(requirement);
}
else {
objectFields.push(requirement);
}
}
}
const requiresFlattening = arrayFields.length > 0 || objectFields.length > 0;
const estimatedImpact = this.estimateImpact(arrayFields, objectFields);
const recommendation = this.recommendFlatteningStrategy(arrayFields, objectFields, estimatedImpact);
return {
requiresFlattening,
arrayFields,
objectFields,
estimatedImpact,
recommendation
};
}
/**
* Analyze query performance and optimization opportunities
*/
analyzeOptimization(parsedQuery, fieldResolutions, flatteningAnalysis) {
const issues = [];
const optimizations = [];
// Analyze complexity
let complexityScore = 0;
// Add complexity for field count
complexityScore += Math.min(parsedQuery.referencedFields.length * 2, 20);
// Add complexity for flattening
if (flatteningAnalysis.requiresFlattening) {
complexityScore += 30;
if (flatteningAnalysis.estimatedImpact === 'explosive') {
complexityScore += 50;
}
}
// Add complexity for joins
complexityScore += parsedQuery.joins.length * 15;
// Add complexity for subqueries
complexityScore += parsedQuery.subqueries.length * 25;
// Determine complexity level
let complexityLevel;
if (complexityScore < 20)
complexityLevel = 'simple';
else if (complexityScore < 50)
complexityLevel = 'moderate';
else if (complexityScore < 80)
complexityLevel = 'complex';
else
complexityLevel = 'very_complex';
// Calculate performance score (inverse of complexity)
const performanceScore = Math.max(0, 100 - complexityScore);
// Identify specific issues
if (flatteningAnalysis.estimatedImpact === 'explosive') {
issues.push({
severity: 'critical',
category: 'cartesian_product',
description: 'Query may produce extremely large result set due to array flattening',
location: flatteningAnalysis.arrayFields.map(f => f.fieldName).join(', '),
suggestion: 'Consider adding more specific filters or using staged querying',
expectedImprovement: '90% reduction in execution time'
});
}
if (parsedQuery.joins.length > 3) {
issues.push({
severity: 'medium',
category: 'inefficient_join',
description: 'Multiple joins may impact performance',
location: 'JOIN clauses',
suggestion: 'Consider denormalizing frequently accessed data',
expectedImprovement: '30-50% reduction in execution time'
});
}
// Estimate execution time
const baseTime = 10; // 10ms base
const flatteningMultiplier = flatteningAnalysis.requiresFlattening ?
(flatteningAnalysis.estimatedImpact === 'explosive' ? 100 : 10) : 1;
const joinMultiplier = Math.pow(2, parsedQuery.joins.length);
const estimatedMin = baseTime * flatteningMultiplier;
const estimatedMax = baseTime * flatteningMultiplier * joinMultiplier * 5;
return {
complexityLevel,
performanceScore,
issues,
optimizations,
estimatedExecutionTime: {
min: estimatedMin,
max: estimatedMax,
confidence: issues.length === 0 ? 0.8 : 0.5
}
};
}
/**
* Generate overall recommendation for query execution
*/
generateRecommendation(parsedQuery, flatteningAnalysis, optimization) {
const modifications = [];
const alternatives = [];
let riskLevel = 'low';
let executeAsIs = true;
// Analyze risk factors
if (flatteningAnalysis.estimatedImpact === 'explosive') {
riskLevel = 'high';
executeAsIs = false;
modifications.push('Add more specific WHERE conditions to limit result set');
alternatives.push('Use staged querying to process data in chunks');
}
else if (flatteningAnalysis.estimatedImpact === 'significant') {
riskLevel = 'medium';
modifications.push('Consider adding LIMIT clause');
}
if (optimization.performanceScore < 30) {
executeAsIs = false;
modifications.push('Query requires optimization before execution');
}
// Calculate confidence
const confidence = Math.max(0.3, Math.min(1.0, optimization.performanceScore / 100));
return {
executeAsIs,
modifications,
alternatives,
riskLevel,
confidence
};
}
// Utility methods for parsing (simplified implementations)
extractFieldReferences(query) {
// Simplified field extraction - real implementation would use proper SQL parsing
const fields = [];
// Look for field patterns in SELECT, WHERE, GROUP BY, ORDER BY
const fieldPattern = /(?:SELECT|WHERE|GROUP BY|ORDER BY|HAVING)\s+.*?(?=(?:FROM|WHERE|GROUP BY|ORDER BY|HAVING|LIMIT|$))/gi;
const matches = query.match(fieldPattern);
if (matches) {
for (const match of matches) {
// Extract individual field names (very simplified)
const fieldMatches = match.match(/\b\w+\.\w+|\b\w+(?=\s*[,\s])/g);
if (fieldMatches) {
fields.push(...fieldMatches.filter(f => !['SELECT', 'WHERE', 'GROUP', 'BY', 'ORDER', 'HAVING'].includes(f.toUpperCase())));
}
}
}
return [...new Set(fields)]; // Remove duplicates
}
extractFilterFields(query) {
// Extract fields from WHERE clauses
const whereMatch = query.match(/WHERE\s+(.*?)(?=\s+(?:GROUP BY|ORDER BY|LIMIT|$))/i);
if (whereMatch) {
return this.extractFieldReferences(`WHERE ${whereMatch[1]}`);
}
return [];
}
extractProjectionFields(query) {
// Extract fields from SELECT clause
const selectMatch = query.match(/SELECT\s+(.*?)\s+FROM/i);
if (selectMatch) {
return this.extractFieldReferences(`SELECT ${selectMatch[1]}`);
}
return [];
}
extractGroupByFields(query) {
// Extract fields from GROUP BY clause
const groupByMatch = query.match(/GROUP BY\s+(.*?)(?=\s+(?:ORDER BY|HAVING|LIMIT|$))/i);
if (groupByMatch) {
return this.extractFieldReferences(`GROUP BY ${groupByMatch[1]}`);
}
return [];
}
extractOrderByFields(query) {
// Extract fields from ORDER BY clause
const orderByMatch = query.match(/ORDER BY\s+(.*?)(?=\s+(?:LIMIT|$))/i);
if (orderByMatch) {
return this.extractFieldReferences(`ORDER BY ${orderByMatch[1]}`);
}
return [];
}
extractJoins(query) {
// Simplified JOIN extraction
const joins = [];
const joinPattern = /(INNER|LEFT|RIGHT|FULL)?\s*JOIN\s+(\w+)\s+ON\s+(.*?)(?=\s+(?:INNER|LEFT|RIGHT|FULL|WHERE|GROUP|ORDER|$))/gi;
let match;
while ((match = joinPattern.exec(query)) !== null) {
joins.push({
type: (match[1] || 'INNER').toUpperCase(),
joinedEntity: match[2],
joinFields: this.extractFieldReferences(`ON ${match[3]}`)
});
}
return joins;
}
calculateNestingDepth(accessPath) {
// Count dots and array accessors to determine nesting depth
return (accessPath.match(/\./g) || []).length + (accessPath.match(/\[/g) || []).length;
}
estimateCardinality(field) {
// Estimate number of elements in arrays or properties in objects
if (field.isArray) {
// Default estimates based on common patterns
if (field.originalInput.includes('variations'))
return 5;
if (field.originalInput.includes('metrics'))
return 3;
if (field.originalInput.includes('audiences'))
return 10;
return 5; // Default
}
return 1;
}
estimateImpact(arrayFields, objectFields) {
if (arrayFields.length === 0 && objectFields.length === 0) {
return 'minimal';
}
const totalCardinality = arrayFields.reduce((sum, field) => sum * field.estimatedCardinality, 1);
if (totalCardinality < 10)
return 'minimal';
if (totalCardinality < 100)
return 'moderate';
if (totalCardinality < 1000)
return 'significant';
return 'explosive';
}
recommendFlatteningStrategy(arrayFields, objectFields, impact) {
if (impact === 'minimal') {
return {
approach: 'no_flattening',
steps: [],
performanceNotes: ['Query can execute without flattening'],
alternatives: []
};
}
if (impact === 'explosive') {
return {
approach: 'staged_flattening',
steps: [
{
order: 1,
description: 'Pre-filter data to reduce cardinality',
expression: 'WHERE conditions to limit scope',
sizeImpact: 0.1
},
{
order: 2,
description: 'Flatten in stages with intermediate caching',
expression: 'Step-by-step array expansion',
sizeImpact: 0.3
}
],
performanceNotes: ['High risk of cartesian product explosion', 'Consider pagination'],
alternatives: ['Use aggregation instead of detailed results', 'Process in smaller batches']
};
}
return {
approach: 'selective_flattening',
steps: arrayFields.map((field, index) => ({
order: index + 1,
description: `Flatten ${field.fieldName}`,
expression: field.resolvedField.sqlAccessor,
sizeImpact: field.estimatedCardinality
})),
performanceNotes: ['Monitor result set size', 'Consider adding limits'],
alternatives: ['Use summary queries instead of detail']
};
}
}
//# sourceMappingURL=QueryAnalysisEngine.js.map