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