UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

390 lines 16.2 kB
/** * 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