@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
466 lines • 18.9 kB
JavaScript
/**
* Query Complexity Firewall - Final Protection Layer
*
* CRITICAL COMPONENT: Last line of defense against query explosions by:
* 1. Scoring query complexity across multiple dimensions
* 2. Implementing circuit breakers for dangerous queries
* 3. Providing intelligent fallback strategies
* 4. Learning from query execution patterns
*
* Created: July 5, 2025
* Purpose: Third line of defense against analytics system failure
*/
import { getLogger } from '../logging/Logger.js';
const logger = getLogger();
/**
* Query Complexity Firewall Implementation
*/
export class QueryComplexityFirewall {
config;
queryHistory = new Map(); // Pattern → executions
circuitBreaker = new Map();
constructor(config = {}) {
this.config = {
enabled: true,
thresholds: {
simple: { maxScore: 35, maxJoins: 2, maxRows: 10000 }, // Allow up to 2 JOINs for simple queries
moderate: { maxScore: 60, maxJoins: 3, maxRows: 50000 }, // Allow 3 JOINs for moderate
complex: { maxScore: 85, maxJoins: 4, maxRows: 200000 }, // Allow 4 JOINs for complex
dangerous: { maxScore: 100, maxJoins: 5, maxRows: 1000000 } // 5+ JOINs are dangerous
},
enableFallbacks: true,
enableLearning: true,
timeoutMs: 10000, // 10 second timeout
debugMode: true,
...config
};
logger.info(`QueryComplexityFirewall initialized with thresholds: ${JSON.stringify(this.config.thresholds)}`);
}
/**
* Analyze query complexity and determine if it should be allowed
*/
analyzeQuery(queryInfo) {
if (!this.config.enabled) {
return {
complexity: this.createSimpleComplexity(),
allowed: true,
recommendations: []
};
}
// Store query info for smart allowance logic
this.queryInfo = queryInfo;
// Calculate complexity score
const complexity = this.calculateComplexity(queryInfo);
// Check circuit breaker
const circuitOpen = this.checkCircuitBreaker(complexity.complexity);
if (circuitOpen) {
logger.warn(`Circuit breaker open for ${complexity.complexity} queries`);
return {
complexity,
allowed: false,
fallback: this.generateFallbackStrategy(queryInfo, 'circuit_breaker'),
recommendations: ['Circuit breaker is open due to repeated failures', 'Using simplified fallback query']
};
}
// Determine if query is allowed
const allowed = this.isQueryAllowed(complexity);
const recommendations = this.generateRecommendations(complexity);
// Generate fallback if needed
let fallback;
if (!allowed && this.config.enableFallbacks) {
fallback = this.generateFallbackStrategy(queryInfo, complexity.complexity);
}
if (this.config.debugMode) {
logger.info(`Query complexity analysis: ${complexity.complexity} (score: ${complexity.score}), allowed: ${allowed}`);
if (complexity.riskFactors.length > 0) {
logger.debug(`Risk factors: ${complexity.riskFactors.join(', ')}`);
}
}
return {
complexity,
allowed,
fallback,
recommendations
};
}
/**
* Calculate comprehensive complexity score
*/
calculateComplexity(queryInfo) {
let score = 0;
const riskFactors = [];
// JOIN complexity (major factor)
const joinCount = queryInfo.joins.length;
// L1-3 ENHANCEMENT: Smarter JOIN scoring
// First JOIN is often necessary (e.g., flags → flag_environments)
// Additional JOINs increase complexity more
if (joinCount === 1) {
score += 5; // Minimal penalty for single JOIN
}
else if (joinCount === 2) {
score += 15; // Moderate penalty for 2 JOINs
}
else if (joinCount >= 3) {
score += joinCount * 12; // Higher penalty for 3+ JOINs
riskFactors.push(`High JOIN count (${joinCount})`);
}
// Check for dangerous JOIN patterns
let dangerousJoinCount = 0;
for (const join of queryInfo.joins) {
if (this.isDangerousJoinPattern(join)) {
dangerousJoinCount++;
score += 25; // Reduced from 30
riskFactors.push(`Dangerous JOIN: ${join.fromTable} → ${join.toTable}`);
}
}
// If ALL JOINs are dangerous, that's a bigger problem
if (dangerousJoinCount === joinCount && joinCount > 0) {
score += 20;
riskFactors.push('All JOINs are dangerous patterns');
}
// Estimated result set size
const estimatedRows = queryInfo.estimatedRows;
if (estimatedRows > 100000) {
score += 25;
riskFactors.push(`Large result set (${estimatedRows} rows)`);
}
else if (estimatedRows > 10000) {
score += 15;
}
else if (estimatedRows > 1000) {
score += 5;
}
// Aggregation complexity
const aggregationCount = queryInfo.aggregations.length;
score += aggregationCount * 5; // 5 points per aggregation
// Special penalty for multiple aggregations with JOINs
if (aggregationCount > 1 && joinCount > 0) {
score += 20;
riskFactors.push('Multiple aggregations with JOINs');
}
// GROUP BY complexity
const groupByCount = queryInfo.groupByFields.length;
score += groupByCount * 8; // 8 points per GROUP BY field
if (groupByCount > 2) {
riskFactors.push(`Complex grouping (${groupByCount} fields)`);
}
// Cross-entity complexity
const crossEntityCount = queryInfo.crossEntities.length;
if (crossEntityCount > 2) {
score += (crossEntityCount - 2) * 10;
riskFactors.push(`Cross-entity query (${crossEntityCount} entities)`);
}
// JSON path complexity
const jsonPathCount = this.countJSONPaths(queryInfo.sql);
score += jsonPathCount * 8; // 8 points per JSON path
if (jsonPathCount > 2) {
riskFactors.push(`Complex JSON access (${jsonPathCount} paths)`);
}
// SQL structure complexity
const nestedQueryDepth = this.calculateNestedQueryDepth(queryInfo.sql);
score += nestedQueryDepth * 15; // 15 points per nesting level
if (nestedQueryDepth > 1) {
riskFactors.push(`Nested queries (depth: ${nestedQueryDepth})`);
}
// Determine complexity level
const complexity = this.scoreToComplexityLevel(score);
return {
joinCount,
estimatedRows,
aggregationFunctions: queryInfo.aggregations,
groupByFields: groupByCount,
crossEntityCount,
jsonPathCount,
nestedQueryDepth,
complexity,
score: Math.round(score),
riskFactors
};
}
/**
* Check if a JOIN pattern is known to be dangerous
*/
isDangerousJoinPattern(join) {
const dangerousPatterns = [
// The root cause patterns that created the 700x inflation
{ from: 'flag_environments', to: 'environments' },
{ from: 'experiment_pages', to: 'pages' },
{ from: 'experiment_audiences', to: 'audiences' },
// Other high-risk patterns
{ from: 'flags', to: 'projects' },
{ from: 'experiments', to: 'projects' },
{ from: 'audiences', to: 'attributes' }
];
return dangerousPatterns.some(pattern => join.fromTable === pattern.from && join.toTable === pattern.to);
}
/**
* Count JSON path operations in SQL
*/
countJSONPaths(sql) {
const jsonFunctions = ['JSON_EXTRACT', 'JSON_VALUE', 'JSON_QUERY', 'JSON_ARRAY', 'JSON_OBJECT'];
let count = 0;
for (const func of jsonFunctions) {
const regex = new RegExp(func, 'gi');
const matches = sql.match(regex);
count += matches ? matches.length : 0;
}
return count;
}
/**
* Calculate nested query depth
*/
calculateNestedQueryDepth(sql) {
let depth = 0;
let maxDepth = 0;
for (const char of sql) {
if (char === '(') {
depth++;
maxDepth = Math.max(maxDepth, depth);
}
else if (char === ')') {
depth--;
}
}
return maxDepth;
}
/**
* Convert score to complexity level
*/
scoreToComplexityLevel(score) {
if (score <= this.config.thresholds.simple.maxScore)
return 'simple';
if (score <= this.config.thresholds.moderate.maxScore)
return 'moderate';
if (score <= this.config.thresholds.complex.maxScore)
return 'complex';
return 'dangerous';
}
/**
* Determine if query should be allowed based on complexity
*/
isQueryAllowed(complexity) {
const threshold = this.config.thresholds[complexity.complexity];
// SMART ALLOWANCE: Special cases for necessary queries
// Allow queries with specific field selection (not SELECT *) more leniency
const hasSpecificFields = this.queryInfo?.sql &&
!this.queryInfo.sql.includes('SELECT *') &&
!this.queryInfo.sql.includes('SELECT COUNT(*)');
// If query has specific fields and reasonable JOIN count, be more lenient
if (hasSpecificFields && complexity.joinCount <= 3 && complexity.estimatedRows < 50000) {
// Allow up to score 70 for targeted queries
if (complexity.score <= 70) {
logger.info(`Allowing targeted query with score ${complexity.score} due to specific field selection`);
return true;
}
}
// Check multiple criteria
if (complexity.joinCount > threshold.maxJoins)
return false;
if (complexity.estimatedRows > threshold.maxRows)
return false;
if (complexity.score > threshold.maxScore)
return false;
// Special blocks for dangerous patterns
if (complexity.complexity === 'dangerous') {
logger.warn(`Blocking dangerous query (score: ${complexity.score})`);
return false;
}
return true;
}
// Store query info for use in isQueryAllowed
queryInfo;
/**
* Generate recommendations for query optimization
*/
generateRecommendations(complexity) {
const recommendations = [];
if (complexity.joinCount > 2) {
recommendations.push('Consider reducing the number of JOINs by using junction tables directly');
}
if (complexity.estimatedRows > 50000) {
recommendations.push('Add more selective WHERE conditions to reduce result set size');
}
if (complexity.groupByFields > 2) {
recommendations.push('Consider simplifying GROUP BY criteria or running separate queries');
}
if (complexity.crossEntityCount > 2) {
recommendations.push('Break down cross-entity query into simpler single-entity queries');
}
if (complexity.jsonPathCount > 3) {
recommendations.push('Consider materializing JSON fields into regular columns for better performance');
}
if (complexity.riskFactors.length > 3) {
recommendations.push('Query has multiple risk factors - consider using a simpler approach');
}
return recommendations;
}
/**
* Generate fallback strategy for complex queries
*/
generateFallbackStrategy(queryInfo, complexityLevel) {
// Determine primary entity
const primaryTable = queryInfo.joins[0]?.fromTable || 'flags';
// Build WHERE clause from conditions if available
let whereClause = '';
if (queryInfo.whereConditions && queryInfo.whereConditions.length > 0) {
const conditions = queryInfo.whereConditions.map(condition => {
// Extract field name without table prefix for simple fallback queries
let fieldName = condition.field;
if (fieldName.includes('.')) {
fieldName = fieldName.split('.').pop() || fieldName;
}
// Only include conditions that apply to the primary table
if (fieldName === 'project_id') {
if (typeof condition.value === 'string') {
return `${fieldName} ${condition.operator} '${condition.value}'`;
}
else {
return `${fieldName} ${condition.operator} ${condition.value}`;
}
}
return null;
}).filter(Boolean);
if (conditions.length > 0) {
whereClause = `WHERE ${conditions.join(' AND ')}`;
}
}
// Generate simple fallback based on query type
if (queryInfo.aggregations.includes('COUNT')) {
return {
name: 'Simple Count Fallback',
description: 'Basic COUNT query without JOINs',
sql: `SELECT COUNT(*) as count FROM ${primaryTable} ${whereClause}`.trim(),
estimatedAccuracy: 0.8,
executionTime: 500
};
}
if (queryInfo.groupByFields.length > 0) {
// L2-1 FIX: Handle GROUP BY fields that reference joined tables
let groupField = queryInfo.groupByFields[0] || 'status';
let selectField = groupField;
// If the GROUP BY field references another table, we can't use it in a simple query
if (groupField.includes('.')) {
logger.warn(`L2-1 DEBUG: GROUP BY field '${groupField}' references joined table, falling back to simple count`);
// Return a simple count instead of trying to GROUP BY a field we don't have
return {
name: 'Simple Count Fallback',
description: 'Cannot GROUP BY joined fields without JOINs',
sql: `SELECT COUNT(*) as count FROM ${primaryTable} ${whereClause}`.trim(),
estimatedAccuracy: 0.5,
executionTime: 500
};
}
return {
name: 'Simplified Group By',
description: 'Basic GROUP BY query without complex JOINs',
sql: `SELECT ${selectField}, COUNT(*) as count FROM ${primaryTable} ${whereClause} GROUP BY ${groupField}`.trim(),
estimatedAccuracy: 0.7,
executionTime: 1000
};
}
// Default list fallback
return {
name: 'Simple List Query',
description: 'Basic SELECT query without JOINs',
sql: `SELECT * FROM ${primaryTable} ${whereClause} LIMIT 100`.trim(),
estimatedAccuracy: 0.9,
executionTime: 200
};
}
/**
* Check circuit breaker for query type
*/
checkCircuitBreaker(complexityLevel) {
const breaker = this.circuitBreaker.get(complexityLevel);
if (!breaker)
return false;
// Circuit breaker logic: open if 3+ failures in last 5 minutes
const now = Date.now();
const fiveMinutesAgo = now - (5 * 60 * 1000);
if (breaker.failures >= 3 && breaker.lastFailure > fiveMinutesAgo) {
return true; // Circuit is open
}
// Reset circuit breaker if enough time has passed
if (breaker.lastFailure < fiveMinutesAgo) {
this.circuitBreaker.delete(complexityLevel);
}
return false;
}
/**
* Record query execution result for learning
*/
recordQueryExecution(complexity, success, executionTimeMs, resultCount) {
if (!this.config.enableLearning)
return;
// Update circuit breaker
if (!success) {
const breaker = this.circuitBreaker.get(complexity.complexity) || { failures: 0, lastFailure: 0 };
breaker.failures++;
breaker.lastFailure = Date.now();
this.circuitBreaker.set(complexity.complexity, breaker);
}
// Update query history for learning
const pattern = this.createQueryPattern(complexity);
const history = this.queryHistory.get(pattern) || [];
history.push({
...complexity,
score: complexity.score + (success ? 0 : 20) // Penalty for failures
});
// Keep only last 10 executions for each pattern
if (history.length > 10) {
history.shift();
}
this.queryHistory.set(pattern, history);
if (this.config.debugMode) {
logger.debug(`Recorded query execution: ${complexity.complexity}, success: ${success}, time: ${executionTimeMs}ms`);
}
}
/**
* Create a pattern key for query learning
*/
createQueryPattern(complexity) {
return `${complexity.complexity}_j${complexity.joinCount}_g${complexity.groupByFields}_a${complexity.aggregationFunctions.length}`;
}
/**
* Create simple complexity for disabled firewall
*/
createSimpleComplexity() {
return {
joinCount: 0,
estimatedRows: 100,
aggregationFunctions: [],
groupByFields: 0,
crossEntityCount: 1,
jsonPathCount: 0,
nestedQueryDepth: 0,
complexity: 'simple',
score: 10,
riskFactors: []
};
}
/**
* Get firewall statistics for monitoring
*/
getStatistics() {
const stats = {
totalQueries: 0,
blockedQueries: 0,
complexityDistribution: { simple: 0, moderate: 0, complex: 0, dangerous: 0 },
circuitBreakerStatus: {},
averageScore: 0
};
// Analyze circuit breaker status
for (const [level, breaker] of this.circuitBreaker) {
stats.circuitBreakerStatus[level] = this.checkCircuitBreaker(level);
}
return stats;
}
/**
* Update firewall configuration at runtime
*/
updateConfig(updates) {
this.config = { ...this.config, ...updates };
logger.info(`QueryComplexityFirewall configuration updated`);
}
}
//# sourceMappingURL=QueryComplexityFirewall.js.map