UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

467 lines 17.4 kB
/** * Multi-Entity Join Path Planning - Phase 4A Implementation * * Enables complex analytics queries that join across multiple entities * like experiments+pages+events with automatic join path discovery. * * Features: * - Dijkstra-like algorithm for optimal join path discovery * - Cost-based join optimization * - Relationship type validation (one-to-one, one-to-many, many-to-many) * - Automatic join table detection for many-to-many relationships */ import { getLogger } from '../../logging/Logger.js'; const logger = getLogger(); export class JoinPathPlanner { relationships = new Map(); entityTableMap = new Map(); joinCostCache = new Map(); constructor() { this.initializeOptimizelyRelationships(); } /** * Initialize known relationships in Optimizely data model */ initializeOptimizelyRelationships() { const relationships = [ // Flags relationships { fromEntity: 'flags', toEntity: 'flag_environments', fromField: 'key', toField: 'flag_key', relationshipType: 'one-to-many', cost: 1, bidirectional: true }, { fromEntity: 'flags', toEntity: 'variations', fromField: 'key', toField: 'flag_key', relationshipType: 'one-to-many', cost: 2, bidirectional: true }, { fromEntity: 'flags', toEntity: 'rulesets', fromField: 'key', toField: 'flag_key', relationshipType: 'one-to-many', cost: 2, bidirectional: true }, // Experiment relationships { fromEntity: 'experiments', toEntity: 'experiment_pages', fromField: 'id', toField: 'experiment_id', relationshipType: 'one-to-many', cost: 1, bidirectional: true }, { fromEntity: 'experiments', toEntity: 'experiment_metrics', fromField: 'id', toField: 'experiment_id', relationshipType: 'one-to-many', cost: 1, bidirectional: true }, { fromEntity: 'experiments', toEntity: 'experiment_audiences', fromField: 'id', toField: 'experiment_id', relationshipType: 'one-to-many', cost: 1, bidirectional: true }, // Page relationships { fromEntity: 'pages', toEntity: 'experiment_pages', fromField: 'id', toField: 'page_id', relationshipType: 'one-to-many', cost: 1, bidirectional: true }, { fromEntity: 'pages', toEntity: 'page_events', fromField: 'id', toField: 'page_id', relationshipType: 'one-to-many', cost: 1, bidirectional: true }, // Event relationships { fromEntity: 'events', toEntity: 'experiment_metrics', fromField: 'id', toField: 'event_id', relationshipType: 'one-to-many', cost: 1, bidirectional: true }, { fromEntity: 'events', toEntity: 'page_events', fromField: 'id', toField: 'event_id', relationshipType: 'one-to-many', cost: 1, bidirectional: true }, // Audience relationships { fromEntity: 'audiences', toEntity: 'experiment_audiences', fromField: 'id', toField: 'audience_id', relationshipType: 'one-to-many', cost: 1, bidirectional: true }, // Many-to-many relationships through junction tables { fromEntity: 'experiments', toEntity: 'pages', fromField: 'id', toField: 'id', relationshipType: 'many-to-many', joinTable: 'experiment_pages', cost: 3, bidirectional: true }, { fromEntity: 'experiments', toEntity: 'events', fromField: 'id', toField: 'id', relationshipType: 'many-to-many', joinTable: 'experiment_metrics', cost: 3, bidirectional: true }, { fromEntity: 'experiments', toEntity: 'audiences', fromField: 'id', toField: 'id', relationshipType: 'many-to-many', joinTable: 'experiment_audiences', cost: 3, bidirectional: true }, { fromEntity: 'pages', toEntity: 'events', fromField: 'id', toField: 'id', relationshipType: 'many-to-many', joinTable: 'page_events', cost: 3, bidirectional: true } ]; // Build relationship map for (const rel of relationships) { if (!this.relationships.has(rel.fromEntity)) { this.relationships.set(rel.fromEntity, []); } this.relationships.get(rel.fromEntity).push(rel); // Add reverse relationships if bidirectional if (rel.bidirectional) { const reverseRel = { fromEntity: rel.toEntity, toEntity: rel.fromEntity, fromField: rel.toField, toField: rel.fromField, relationshipType: rel.relationshipType, joinTable: rel.joinTable, cost: rel.cost, bidirectional: true }; if (!this.relationships.has(rel.toEntity)) { this.relationships.set(rel.toEntity, []); } this.relationships.get(rel.toEntity).push(reverseRel); } } // Initialize entity to table mapping this.entityTableMap.set('flags', 'flags'); this.entityTableMap.set('flag_environments', 'flag_environments'); this.entityTableMap.set('variations', 'variations'); this.entityTableMap.set('rulesets', 'rulesets'); this.entityTableMap.set('rules', 'rules'); this.entityTableMap.set('experiments', 'experiments'); this.entityTableMap.set('pages', 'pages'); this.entityTableMap.set('events', 'events'); this.entityTableMap.set('audiences', 'audiences'); this.entityTableMap.set('experiment_pages', 'experiment_pages'); this.entityTableMap.set('experiment_metrics', 'experiment_metrics'); this.entityTableMap.set('experiment_audiences', 'experiment_audiences'); this.entityTableMap.set('page_events', 'page_events'); logger.info(`Initialized ${relationships.length} entity relationships for join planning`); } /** * Find optimal join path between multiple entities * Uses Dijkstra-like algorithm for shortest cost path */ async findOptimalJoinPath(entities, options = {}) { const opts = { preferLeftJoins: true, maxJoinDepth: 5, avoidCartesianProducts: true, optimizeForPerformance: true, ...options }; if (entities.length < 2) { throw new Error('Join planning requires at least 2 entities'); } logger.info(`Planning join path for entities: ${entities.join(' → ')}`); const primaryEntity = entities[0]; const joinPaths = []; // For each target entity, find the shortest path from primary for (let i = 1; i < entities.length; i++) { const targetEntity = entities[i]; const path = await this.findShortestPath(primaryEntity, targetEntity, opts); if (!path) { throw new Error(`No valid join path found from ${primaryEntity} to ${targetEntity}`); } joinPaths.push(...path); } // Optimize join order for performance const optimizedPaths = this.optimizeJoinOrder(joinPaths, opts); // Calculate total cost const totalCost = this.calculateJoinCost(optimizedPaths); logger.info(`Found join path with ${optimizedPaths.length} joins, total cost: ${totalCost}`); return optimizedPaths; } /** * Find shortest path between two entities using Dijkstra's algorithm */ async findShortestPath(fromEntity, toEntity, options) { const cacheKey = `${fromEntity}->${toEntity}`; // Check cache first if (this.joinCostCache.has(cacheKey)) { logger.debug(`Using cached join path for ${cacheKey}`); } const nodes = new Map(); const unvisited = new Set(); const visited = new Set(); // Initialize starting node nodes.set(fromEntity, { entity: fromEntity, table: this.getTableName(fromEntity), distance: 0 }); unvisited.add(fromEntity); // Add all reachable entities for (const entity of this.relationships.keys()) { if (entity !== fromEntity) { nodes.set(entity, { entity, table: this.getTableName(entity), distance: Infinity }); unvisited.add(entity); } } while (unvisited.size > 0) { // Find unvisited node with minimum distance let currentEntity = null; let minDistance = Infinity; for (const entity of unvisited) { const node = nodes.get(entity); if (node.distance < minDistance) { minDistance = node.distance; currentEntity = entity; } } if (!currentEntity || minDistance === Infinity) { break; // No path found } const currentNode = nodes.get(currentEntity); unvisited.delete(currentEntity); visited.add(currentEntity); // Found target if (currentEntity === toEntity) { return this.reconstructPath(nodes, toEntity); } // Update distances to neighbors const relationships = this.relationships.get(currentEntity) || []; for (const rel of relationships) { if (visited.has(rel.toEntity)) continue; const neighborNode = nodes.get(rel.toEntity); if (!neighborNode) continue; const newDistance = currentNode.distance + rel.cost; if (newDistance < neighborNode.distance) { neighborNode.distance = newDistance; neighborNode.previous = currentNode; neighborNode.relationship = rel; } } // Prevent infinite loops if (currentNode.distance > options.maxJoinDepth) { break; } } return null; // No path found } /** * Reconstruct join path from Dijkstra result */ reconstructPath(nodes, targetEntity) { const path = []; let currentNode = nodes.get(targetEntity); while (currentNode && currentNode.previous && currentNode.relationship) { const joinPath = { from: { entity: currentNode.previous.entity, field: currentNode.relationship.fromField, table: currentNode.previous.table }, to: { entity: currentNode.entity, field: currentNode.relationship.toField, table: currentNode.table }, joinType: 'LEFT', // Default to LEFT JOIN for optional relationships cost: currentNode.relationship.cost, required: false, // Will be updated based on query requirements relationshipType: currentNode.relationship.relationshipType, joinTable: currentNode.relationship.joinTable }; path.unshift(joinPath); currentNode = currentNode.previous; } return path; } /** * Optimize join order for better performance */ optimizeJoinOrder(joinPaths, options) { if (!options.optimizeForPerformance) { return joinPaths; } // Sort by cost (lower cost first) return joinPaths.sort((a, b) => { // Prefer one-to-one over one-to-many over many-to-many const typeWeight = { 'one-to-one': 1, 'one-to-many': 2, 'many-to-many': 3 }; const weightDiff = typeWeight[a.relationshipType] - typeWeight[b.relationshipType]; if (weightDiff !== 0) return weightDiff; // Then by cost return a.cost - b.cost; }); } /** * Calculate total cost of join path */ calculateJoinCost(joinPaths) { return joinPaths.reduce((total, path) => { let cost = path.cost; // Penalty for many-to-many joins if (path.relationshipType === 'many-to-many') { cost *= 2; } // Penalty for RIGHT JOINs (less efficient in most databases) if (path.joinType === 'RIGHT') { cost *= 1.5; } return total + cost; }, 0); } /** * Validate that join path is executable */ validateJoinPath(joinPaths) { const usedTables = new Set(); for (const path of joinPaths) { // Check for circular dependencies if (usedTables.has(path.to.table) && path.to.table !== path.from.table) { logger.warn(`Potential circular dependency detected: ${path.to.table}`); return false; } usedTables.add(path.from.table); usedTables.add(path.to.table); // Validate relationship exists const fromRelationships = this.relationships.get(path.from.entity) || []; const validRelationship = fromRelationships.some(rel => rel.toEntity === path.to.entity && rel.fromField === path.from.field && rel.toField === path.to.field); if (!validRelationship) { logger.warn(`Invalid relationship: ${path.from.entity}.${path.from.field} → ${path.to.entity}.${path.to.field}`); return false; } } return true; } /** * Get all available relationships for an entity */ getEntityRelationships(entity) { return this.relationships.get(entity) || []; } /** * Check if two entities can be joined directly */ canJoinDirectly(fromEntity, toEntity) { const relationships = this.relationships.get(fromEntity) || []; return relationships.some(rel => rel.toEntity === toEntity); } /** * Get table name for entity */ getTableName(entity) { return this.entityTableMap.get(entity) || entity; } /** * Add custom relationship (for extensibility) */ addRelationship(relationship) { if (!this.relationships.has(relationship.fromEntity)) { this.relationships.set(relationship.fromEntity, []); } this.relationships.get(relationship.fromEntity).push(relationship); logger.info(`Added custom relationship: ${relationship.fromEntity} → ${relationship.toEntity}`); } /** * Get statistics about relationship graph */ getStatistics() { const totalEntities = this.relationships.size; let totalRelationships = 0; const relationshipTypes = { 'one-to-one': 0, 'one-to-many': 0, 'many-to-many': 0 }; for (const relationships of this.relationships.values()) { totalRelationships += relationships.length; for (const rel of relationships) { relationshipTypes[rel.relationshipType]++; } } return { totalEntities, totalRelationships, avgRelationshipsPerEntity: totalRelationships / totalEntities, relationshipTypes }; } } //# sourceMappingURL=JoinPathPlanner.js.map