UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

800 lines 32.6 kB
/** * Query Planner - Intelligent Query Execution Planning * * The Query Planner analyzes queries and creates optimal execution plans. * It determines the best strategy (pure SQL, pure JSONata, or hybrid) * based on query complexity, data locations, and adapter capabilities. */ import { randomUUID } from 'crypto'; import { getLogger } from '../../logging/Logger.js'; import { JoinPathPlanner } from './JoinPathPlanner.js'; import { MultiTableSQLBuilder } from './MultiTableSQLBuilder.js'; const logger = getLogger(); /** * Cost factors for different operations */ const COST_FACTORS = { sqlColumn: 1, jsonExtract: 5, jsonataTransform: 10, join: 20, aggregation: 15, postProcess: 5, networkRoundtrip: 50 }; /** * Query Planner implementation */ export class QueryPlanner { fieldCatalog; adapters; config; joinPathPlanner; multiTableSQLBuilder = null; constructor(fieldCatalog, adapters, config = {}) { this.fieldCatalog = fieldCatalog; this.adapters = adapters; this.config = config; this.joinPathPlanner = new JoinPathPlanner(); logger.info('QueryPlanner initialized with multi-entity support'); } /** * Create an execution plan for a query */ async createPlan(query) { logger.debug('Creating execution plan for query'); try { // 🚨 CRITICAL: Check for enhanced parser hints for COUNT inflation prevention if (query.hints?.enhancedHints) { logger.info('Enhanced parser hints detected - applying COUNT inflation prevention'); if (query.hints.enhancedHints.queryIntent?.type === 'count') { logger.info({ queryType: query.hints.enhancedHints.queryIntent.type, confidence: query.hints.enhancedHints.queryIntent.confidence, aggregationStrategy: query.hints.enhancedHints.aggregationContext?.preferredStrategy, message: 'COUNT query detected - using optimized planning' }); } if (query.hints.enhancedHints.joinHints) { logger.info({ unnecessaryJoins: query.hints.enhancedHints.joinHints.unnecessaryJoins, prohibitedJoins: query.hints.enhancedHints.joinHints.prohibitedJoins, message: 'JOIN hints detected - optimizing JOIN strategy' }); } } // Analyze query complexity (enhanced with parser hints) const complexity = await this.analyzeQueryComplexity(query); // Determine optimal strategy (enhanced with parser hints) const strategy = await this.determineStrategy(query, complexity); // Build execution phases (enhanced with parser hints) const phases = await this.buildExecutionPhases(query, strategy, complexity); // Estimate costs const estimatedCost = this.estimatePlanCost(phases, complexity); const estimatedRows = await this.estimateRowCount(query); const plan = { id: randomUUID(), query, strategy, phases, estimatedCost, estimatedRows, requiresPostProcessing: this.requiresPostProcessing(phases) }; logger.info('Created execution plan'); return plan; } catch (error) { logger.error(`Failed to create execution plan: ${error}`); throw error; } } /** * Analyze query complexity */ async analyzeQueryComplexity(query) { const complexity = { fieldCount: 0, sqlFields: 0, jsonFields: 0, computedFields: 0, relatedFields: 0, hasAggregations: false, hasGroupBy: false, hasJoins: false, hasComplexConditions: false, requiresJSONata: false, totalCost: 0 }; // Analyze SELECT fields const fieldsToAnalyze = query.select || ['*']; if (fieldsToAnalyze[0] !== '*') { for (const field of fieldsToAnalyze) { // Skip aggregate functions - they don't need field resolution if (this.isAggregateFunction(field)) { complexity.fieldCount++; complexity.sqlFields++; complexity.totalCost += COST_FACTORS.aggregation; continue; } try { const location = await this.fieldCatalog.resolveField(query.find, field); complexity.fieldCount++; switch (location.physicalLocation.type) { case 'column': complexity.sqlFields++; break; case 'json_path': complexity.jsonFields++; complexity.requiresJSONata = true; break; case 'computed': complexity.computedFields++; complexity.requiresJSONata = true; break; case 'related': complexity.relatedFields++; complexity.hasJoins = true; break; } complexity.totalCost += location.estimatedCost || 1; } catch (error) { logger.warn(`Failed to resolve field ${field}: ${error}`); complexity.fieldCount++; complexity.totalCost += COST_FACTORS.jsonataTransform; // Assume unresolved fields are SQL columns complexity.sqlFields++; } } } // Analyze WHERE conditions if (query.where && query.where.length > 0) { complexity.hasComplexConditions = this.hasComplexConditions(query.where); } // Analyze GROUP BY if (query.groupBy && query.groupBy.length > 0) { complexity.hasGroupBy = true; for (const field of query.groupBy) { // Skip aggregate functions in GROUP BY (shouldn't happen but be safe) if (this.isAggregateFunction(field)) { continue; } try { const location = await this.fieldCatalog.resolveField(query.find, field); if (location.physicalLocation.type === 'related') { complexity.hasJoins = true; } } catch (error) { logger.warn(`Failed to resolve group by field ${field}: ${error}`); } } } // Analyze aggregations if (query.aggregations && query.aggregations.length > 0) { complexity.hasAggregations = true; complexity.totalCost += query.aggregations.length * COST_FACTORS.aggregation; } // Analyze explicit joins if (query.joins && query.joins.length > 0) { complexity.hasJoins = true; complexity.totalCost += query.joins.length * COST_FACTORS.join; } return complexity; } /** * Determine optimal execution strategy */ async determineStrategy(query, complexity) { // Check adapter capabilities const adapter = this.getPrimaryAdapter(query.find); if (!adapter) { throw new Error(`No adapter found for entity: ${query.find}`); } const capabilities = adapter.getCapabilities(); // 🚨 CRITICAL: Check enhanced parser hints for COUNT inflation prevention if (query.hints?.enhancedHints?.aggregationContext?.preferredStrategy) { const preferredStrategy = query.hints.enhancedHints.aggregationContext.preferredStrategy; logger.info({ preferredStrategy, queryType: query.hints.enhancedHints.queryIntent?.type, message: 'Using enhanced parser preferred strategy for COUNT inflation prevention' }); // For COUNT queries, prefer strategies that avoid JOINs if (query.hints.enhancedHints.queryIntent?.type === 'count') { if (preferredStrategy === 'LocalCountStrategy') { // Use pure-sql with optimized COUNT strategy if (capabilities.supportsSQL && capabilities.supportsAggregations) { return 'pure-sql'; } } } } // If query explicitly requests a strategy, use it if possible if (query.hints?.preferredStrategy) { const preferred = query.hints.preferredStrategy; // Map simplified strategy names to full names let mappedStrategy = null; if (preferred === 'sql') { mappedStrategy = 'pure-sql'; } else if (preferred === 'jsonata') { mappedStrategy = 'pure-jsonata'; } else if (preferred === 'hybrid') { mappedStrategy = 'hybrid-sql-first'; } else if (this.isExecutionStrategy(preferred)) { mappedStrategy = preferred; } if (mappedStrategy && this.canUseStrategy(mappedStrategy, capabilities, complexity)) { return mappedStrategy; } } // Debug log for analysis logger.debug(`Query complexity analysis: sqlFields=${complexity.sqlFields}, fieldCount=${complexity.fieldCount}, requiresJSONata=${complexity.requiresJSONata}, hasJoins=${complexity.hasJoins}, hasAggregations=${complexity.hasAggregations}, supportsSQL=${capabilities.supportsSQL}, supportsJoins=${capabilities.supportsJoins}, supportsAggregations=${capabilities.supportsAggregations}`); // Pure SQL strategy - fastest for simple queries if (complexity.sqlFields === complexity.fieldCount && !complexity.requiresJSONata && capabilities.supportsSQL && (!complexity.hasJoins || capabilities.supportsJoins) && (!complexity.hasAggregations || capabilities.supportsAggregations)) { return 'pure-sql'; } // Pure JSONata - when everything is JSON-based if (complexity.jsonFields > complexity.sqlFields && !complexity.hasJoins && capabilities.supportsJSONata) { return 'pure-jsonata'; } // Hybrid SQL-first - best for most mixed queries if (capabilities.supportsSQL && capabilities.supportsJSONata) { return 'hybrid-sql-first'; } // Fallback to JSONata if available if (capabilities.supportsJSONata) { return 'pure-jsonata'; } throw new Error('No suitable execution strategy found'); } /** * Build execution phases based on strategy */ async buildExecutionPhases(query, strategy, complexity) { switch (strategy) { case 'pure-sql': return this.buildPureSQLPhases(query); case 'pure-jsonata': return this.buildPureJSONataPhases(query); case 'hybrid-sql-first': return this.buildHybridSQLFirstPhases(query, complexity); case 'hybrid-jsonata-first': return this.buildHybridJSONataFirstPhases(query, complexity); case 'parallel': return this.buildParallelPhases(query, complexity); default: throw new Error(`Unknown strategy: ${strategy}`); } } /** * Build phases for pure SQL execution */ buildPureSQLPhases(query) { // 🚨 CRITICAL: Apply enhanced parser hints for COUNT inflation prevention const optimizedQuery = { ...query }; if (query.hints?.enhancedHints) { logger.info('Applying enhanced parser hints to SQL phase'); // For COUNT queries, optimize JOIN strategy if (query.hints.enhancedHints.queryIntent?.type === 'count') { logger.info('Optimizing SQL for COUNT query to prevent inflation'); // Remove unnecessary JOINs based on field locality hints if (query.hints.enhancedHints.joinHints?.unnecessaryJoins?.length && query.hints.enhancedHints.joinHints.unnecessaryJoins.length > 0) { const unnecessaryJoins = query.hints.enhancedHints.joinHints.unnecessaryJoins; logger.info({ originalJoins: query.joins?.length || 0, unnecessaryJoins, message: 'Removing unnecessary JOINs to prevent COUNT inflation' }); // Filter out unnecessary JOINs if (optimizedQuery.joins) { optimizedQuery.joins = optimizedQuery.joins.filter(join => !unnecessaryJoins.includes(join.entity)); } } // Use field locality to optimize SELECT fields if (query.hints.enhancedHints.fieldLocality) { logger.info('Optimizing SELECT fields using field locality information'); } // Ensure proper aggregation for COUNT queries if (query.hints.enhancedHints.aggregationContext?.isAggregation && (!optimizedQuery.aggregations || optimizedQuery.aggregations.length === 0)) { logger.info('Adding COUNT aggregation for COUNT query'); optimizedQuery.aggregations = [{ field: '*', function: 'count', alias: 'count' }]; // Check if we need GROUP BY for "count by/in each" patterns const normalizedQuery = query.hints.enhancedHints.decomposedQuery?.normalizedQuery || ''; const needsGroupBy = /count.*(by|in each|per|for each|across)/.test(normalizedQuery); if (needsGroupBy && optimizedQuery.select && optimizedQuery.select.length > 0) { // Find environment-related field in SELECT const envField = optimizedQuery.select.find(field => field.includes('environment') || field.includes('env')); if (envField && !optimizedQuery.groupBy) { logger.info(`Adding GROUP BY ${envField} for COUNT query`); optimizedQuery.groupBy = [envField]; } } } } } return [{ name: 'sql-execution', type: 'sql', query: { type: 'SELECT', ...optimizedQuery }, parallel: false }]; } /** * Build phases for pure JSONata execution */ buildPureJSONataPhases(query) { return [{ name: 'jsonata-execution', type: 'jsonata', query: { expression: this.buildJSONataExpression(query), ...query }, parallel: false }]; } /** * Build phases for hybrid SQL-first execution */ async buildHybridSQLFirstPhases(query, complexity) { const phases = []; // Phase 1: SQL for filtering and basic operations phases.push({ name: 'sql-filter-and-join', type: 'sql', query: { type: 'SELECT', find: query.find, where: query.where, joins: query.joins, limit: query.limit, offset: query.offset }, outputTo: 'filtered-data' }); // Phase 2: JSONata for complex transformations if (complexity.jsonFields > 0 || complexity.computedFields > 0) { phases.push({ name: 'jsonata-transform', type: 'jsonata', query: { expression: this.buildJSONataExpression(query), select: query.select, aggregations: query.aggregations }, inputFrom: 'filtered-data', outputTo: 'transformed-data' }); } // Phase 3: Post-processing if needed if (query.groupBy || query.having || query.orderBy) { phases.push({ name: 'post-process', type: 'post-process', query: { groupBy: query.groupBy, having: query.having, orderBy: query.orderBy }, inputFrom: phases[phases.length - 1].outputTo || 'filtered-data' }); } return phases; } /** * Build phases for hybrid JSONata-first execution */ async buildHybridJSONataFirstPhases(query, complexity) { // Similar to SQL-first but reversed order return this.buildHybridSQLFirstPhases(query, complexity); } /** * Build phases for parallel execution */ async buildParallelPhases(query, complexity) { // For now, same as hybrid but with parallel flag const phases = await this.buildHybridSQLFirstPhases(query, complexity); phases.forEach(phase => { if (phase.type !== 'post-process') { phase.parallel = true; } }); return phases; } /** * Check if conditions are complex (nested, JSON paths, etc.) */ hasComplexConditions(conditions) { for (const condition of conditions) { if (condition.nested && condition.nested.length > 0) { return true; } if (condition.field.includes('.') || condition.field.includes('[')) { return true; } if (condition.operator === 'STARTS WITH' || condition.operator === 'ENDS WITH') { return true; } // CONTAINS can be handled in SQL with LIKE or INSTR // Don't mark as complex just for CONTAINS } return false; } /** * Build JSONata expression from query */ buildJSONataExpression(query) { // Simplified - would be much more complex in real implementation let expression = `$`; if (query.where && query.where.length > 0) { const conditions = query.where.map(c => `${c.field} ${c.operator} "${c.value}"`).join(' and '); expression += `[${conditions}]`; } if (query.select && query.select[0] !== '*') { expression += `.{${query.select.join(', ')}}`; } return expression; } /** * Estimate plan execution cost */ estimatePlanCost(phases, complexity) { let cost = complexity.totalCost; for (const phase of phases) { switch (phase.type) { case 'sql': cost += COST_FACTORS.sqlColumn; break; case 'jsonata': cost += COST_FACTORS.jsonataTransform; break; case 'post-process': cost += COST_FACTORS.postProcess; break; } } // Reduce cost for parallel execution if (phases.some(p => p.parallel)) { cost *= 0.7; } return Math.round(cost); } /** * Estimate row count for query */ async estimateRowCount(query) { // Simplified estimation - would use statistics in real implementation let estimate = 1000; // Default estimate if (query.where && query.where.length > 0) { // Reduce estimate based on filters estimate *= Math.pow(0.5, query.where.length); } if (query.limit) { estimate = Math.min(estimate, query.limit); } return Math.round(estimate); } /** * Check if plan requires post-processing */ requiresPostProcessing(phases) { return phases.some(p => p.type === 'post-process'); } /** * Get primary adapter for entity */ getPrimaryAdapter(entity) { // Check if we have any adapters if (this.adapters.size === 0) { logger.warn('No adapters registered'); return null; } // For now, return the first adapter (optimizely) // In a real implementation, we would check which adapter handles this entity const firstAdapter = this.adapters.values().next().value; if (!firstAdapter) { logger.warn('Failed to retrieve adapter'); return null; } logger.debug(`Using adapter '${firstAdapter.name}' for entity '${entity}'`); return firstAdapter; } /** * Check if a field is an aggregate function */ isAggregateFunction(field) { // Check for common aggregate functions const aggregatePattern = /^(.*\.)?(COUNT|SUM|AVG|MIN|MAX|GROUP_CONCAT)\s*\(/i; return aggregatePattern.test(field); } /** * Check if a value is a valid ExecutionStrategy */ isExecutionStrategy(value) { return ['pure-sql', 'pure-jsonata', 'hybrid-sql-first', 'hybrid-jsonata-first', 'parallel'].includes(value); } /** * Check if strategy can be used with given capabilities */ canUseStrategy(strategy, capabilities, complexity) { switch (strategy) { case 'pure-sql': return capabilities.supportsSQL && !complexity.requiresJSONata; case 'pure-jsonata': return capabilities.supportsJSONata; case 'hybrid-sql-first': case 'hybrid-jsonata-first': return capabilities.supportsSQL && capabilities.supportsJSONata; case 'parallel': return this.config.enableParallel !== false; default: return false; } } /** * Create execution plan for multi-entity queries */ async createMultiEntityPlan(query) { logger.info(`Creating multi-entity execution plan for primary entity: ${query.find}`); try { // Initialize multi-table SQL builder if needed if (!this.multiTableSQLBuilder) { this.multiTableSQLBuilder = new MultiTableSQLBuilder(this.fieldCatalog); } // Discover required entities from query const requiredEntities = await this.discoverRequiredEntities(query); logger.info(`Discovered required entities: ${requiredEntities.join(', ')}`); // Plan join paths if multiple entities are involved let joinPaths = []; if (requiredEntities.length > 1) { joinPaths = await this.joinPathPlanner.findOptimalJoinPath(requiredEntities); logger.info(`Planned ${joinPaths.length} join paths`); } // Analyze complexity including join complexity const complexity = await this.analyzeMultiEntityComplexity(query, joinPaths); // Determine optimal strategy for multi-entity query const strategy = await this.determineMultiEntityStrategy(query, complexity, joinPaths); // Build execution phases const phases = await this.buildMultiEntityExecutionPhases(query, strategy, complexity, joinPaths); // Estimate costs including join costs const estimatedCost = this.estimateMultiEntityPlanCost(phases, complexity, joinPaths); const estimatedRows = await this.estimateRowCount(query); const plan = { id: randomUUID(), query, strategy, phases, estimatedCost, estimatedRows, requiresPostProcessing: this.requiresPostProcessing(phases) }; logger.info(`Created multi-entity execution plan with strategy: ${strategy}`); return plan; } catch (error) { logger.error(`Failed to create multi-entity execution plan: ${error}`); throw error; } } /** * Discover required entities from multi-entity query */ async discoverRequiredEntities(query) { const entities = new Set(); entities.add(query.find); // Primary entity // Add explicitly requested entities if (query.entities) { query.entities.forEach(e => entities.add(e)); } // Extract entities from qualified field names (e.g., "experiments.name") const extractEntityFromField = (field) => { const parts = field.split('.'); if (parts.length >= 2) { const entity = parts[0]; // Validate against known entities const knownEntities = ['experiments', 'pages', 'events', 'audiences', 'flags', 'variations', 'rules', 'rulesets']; if (knownEntities.includes(entity)) { return entity; } } return null; }; // Check SELECT fields if (query.select) { for (const field of query.select) { const entity = extractEntityFromField(field); if (entity && entity !== query.find) { entities.add(entity); } } } // Check WHERE conditions if (query.where) { for (const condition of query.where) { const entity = extractEntityFromField(condition.field); if (entity && entity !== query.find) { entities.add(entity); } } } // Check GROUP BY fields if (query.groupBy) { for (const field of query.groupBy) { const entity = extractEntityFromField(field); if (entity && entity !== query.find) { entities.add(entity); } } } // Check ORDER BY fields if (query.orderBy) { for (const order of query.orderBy) { const entity = extractEntityFromField(order.field); if (entity && entity !== query.find) { entities.add(entity); } } } return Array.from(entities); } /** * Analyze complexity for multi-entity queries */ async analyzeMultiEntityComplexity(query, joinPaths) { // Start with base complexity const baseComplexity = await this.analyzeQueryComplexity(query); const multiComplexity = { ...baseComplexity, entityCount: 0, joinPathCount: joinPaths.length, totalJoinCost: 0, hasManyToManyJoins: false, hasJunctionTables: false, requiresMultiTableSQL: joinPaths.length > 0 }; // Calculate join-specific complexity const entitiesInvolved = new Set(); for (const path of joinPaths) { entitiesInvolved.add(path.from.entity); entitiesInvolved.add(path.to.entity); multiComplexity.totalJoinCost += path.cost; if (path.relationshipType === 'many-to-many') { multiComplexity.hasManyToManyJoins = true; } if (path.joinTable) { multiComplexity.hasJunctionTables = true; } } multiComplexity.entityCount = entitiesInvolved.size; // Update total cost with join costs multiComplexity.totalCost += multiComplexity.totalJoinCost * COST_FACTORS.join; return multiComplexity; } /** * Determine strategy for multi-entity queries */ async determineMultiEntityStrategy(query, complexity, joinPaths) { const adapter = this.getPrimaryAdapter(query.find); if (!adapter) { throw new Error(`No adapter found for entity: ${query.find}`); } const capabilities = adapter.getCapabilities(); // For multi-entity queries, prefer SQL-based strategies if adapter supports complex joins if (complexity.requiresMultiTableSQL && capabilities.supportsSQL && capabilities.supportsJoins) { // Use pure SQL if everything can be handled in SQL if (!complexity.requiresJSONata && !complexity.hasManyToManyJoins) { return 'pure-sql'; } // Use hybrid for complex cases return 'hybrid-sql-first'; } // Fallback to base strategy determination return await this.determineStrategy(query, complexity); } /** * Build execution phases for multi-entity queries */ async buildMultiEntityExecutionPhases(query, strategy, complexity, joinPaths) { const phases = []; switch (strategy) { case 'pure-sql': if (complexity.requiresMultiTableSQL) { phases.push({ name: 'Execute multi-entity SQL query with joins', type: 'sql', query: { query, joinPaths }, parallel: false }); } else { // Fallback to regular SQL phases.push({ name: 'Execute SQL query', type: 'sql', query: query, parallel: false }); } break; case 'hybrid-sql-first': // First phase: Multi-table SQL phases.push({ name: 'Execute multi-entity SQL query', type: 'sql', query: { query, joinPaths }, parallel: false }); // Second phase: JSONata post-processing if needed if (complexity.requiresJSONata) { phases.push({ name: 'JSONata transformation of joined results', type: 'jsonata', query: { expression: this.buildJSONataExpression(query) }, parallel: false }); } break; default: // Fallback to single-entity phases return await this.buildExecutionPhases(query, strategy, complexity); } return phases; } /** * Estimate cost for multi-entity plans */ estimateMultiEntityPlanCost(phases, complexity, joinPaths) { let cost = complexity.totalCost; // Add join-specific costs cost += this.joinPathPlanner.calculateJoinCost(joinPaths); // Add execution phase costs for (const phase of phases) { switch (phase.type) { case 'sql': // Check if this is a multi-table SQL phase based on query content if (phase.query && typeof phase.query === 'object' && 'joinPaths' in phase.query) { cost += COST_FACTORS.join * joinPaths.length; } else { cost += COST_FACTORS.sqlColumn; } break; case 'jsonata': cost += COST_FACTORS.jsonataTransform; break; case 'post-process': cost += COST_FACTORS.postProcess; break; } } return Math.round(cost); } /** * Get join path planner statistics */ getJoinPlannerStatistics() { return this.joinPathPlanner.getStatistics(); } } //# sourceMappingURL=QueryPlanner.js.map