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