UNPKG

@codai/cbd

Version:

Codai Better Database - High-Performance Vector Memory System with HPKV-inspired architecture and MCP server

506 lines 19.1 kB
/** * Graph Storage Engine - Neo4j-compatible graph database * Part of CBD Universal Database Phase 3 */ import { EventEmitter } from 'events'; export class GraphStorageEngine extends EventEmitter { nodes = new Map(); relationships = new Map(); nodeIndex = new Map(); // label -> node IDs relationshipIndex = new Map(); // type -> relationship IDs adjacencyList = new Map(); // node ID -> connected node IDs // Using separate latencies array for performance tracking latencies = []; // Query latencies for performance stats constructor() { super(); } async initialize() { // Initialize the graph storage engine this.emit('engine:initialized', { type: 'graph' }); } /** * Create a new node */ async createNode(id, labels = [], properties = {}) { try { if (this.nodes.has(id)) { throw new Error(`Node with ID ${id} already exists`); } const node = { id, labels, properties, createdAt: new Date(), updatedAt: new Date() }; this.nodes.set(id, node); // Update label index for (const label of labels) { if (!this.nodeIndex.has(label)) { this.nodeIndex.set(label, new Set()); } this.nodeIndex.get(label).add(id); } // Initialize adjacency list this.adjacencyList.set(id, new Set()); this.emit('node:created', { id, labels, properties }); return node; } catch (error) { this.emit('graph:error', { operation: 'createNode', id, error }); throw error; } } /** * Update a node */ async updateNode(id, labels, properties) { try { const node = this.nodes.get(id); if (!node) { throw new Error(`Node with ID ${id} not found`); } // Update labels if provided if (labels) { // Remove from old label indexes for (const oldLabel of node.labels) { const labelSet = this.nodeIndex.get(oldLabel); if (labelSet) { labelSet.delete(id); if (labelSet.size === 0) { this.nodeIndex.delete(oldLabel); } } } // Add to new label indexes for (const newLabel of labels) { if (!this.nodeIndex.has(newLabel)) { this.nodeIndex.set(newLabel, new Set()); } this.nodeIndex.get(newLabel).add(id); } node.labels = labels; } // Update properties if provided if (properties) { node.properties = { ...node.properties, ...properties }; } node.updatedAt = new Date(); this.emit('node:updated', { id, labels, properties }); return node; } catch (error) { this.emit('graph:error', { operation: 'updateNode', id, error }); throw error; } } /** * Delete a node and all its relationships */ async deleteNode(id) { try { const node = this.nodes.get(id); if (!node) { return false; } // Delete all relationships connected to this node const connectedRelationships = this.adjacencyList.get(id) || new Set(); for (const relId of connectedRelationships) { await this.deleteRelationship(relId); } // Remove from label indexes for (const label of node.labels) { const labelSet = this.nodeIndex.get(label); if (labelSet) { labelSet.delete(id); if (labelSet.size === 0) { this.nodeIndex.delete(label); } } } // Remove node this.nodes.delete(id); this.adjacencyList.delete(id); this.emit('node:deleted', { id }); return true; } catch (error) { this.emit('graph:error', { operation: 'deleteNode', id, error }); throw error; } } /** * Create a relationship between two nodes */ async createRelationship(id, type, fromNodeId, toNodeId, properties = {}) { try { if (this.relationships.has(id)) { throw new Error(`Relationship with ID ${id} already exists`); } if (!this.nodes.has(fromNodeId)) { throw new Error(`From node ${fromNodeId} does not exist`); } if (!this.nodes.has(toNodeId)) { throw new Error(`To node ${toNodeId} does not exist`); } const relationship = { id, type, fromNodeId, toNodeId, properties, createdAt: new Date(), updatedAt: new Date() }; this.relationships.set(id, relationship); // Update type index if (!this.relationshipIndex.has(type)) { this.relationshipIndex.set(type, new Set()); } this.relationshipIndex.get(type).add(id); // Update adjacency lists this.adjacencyList.get(fromNodeId).add(id); this.adjacencyList.get(toNodeId).add(id); this.emit('relationship:created', { id, type, fromNodeId, toNodeId, properties }); return relationship; } catch (error) { this.emit('graph:error', { operation: 'createRelationship', id, error }); throw error; } } /** * Update a relationship */ async updateRelationship(id, properties) { try { const relationship = this.relationships.get(id); if (!relationship) { throw new Error(`Relationship with ID ${id} not found`); } relationship.properties = { ...relationship.properties, ...properties }; relationship.updatedAt = new Date(); this.emit('relationship:updated', { id, properties }); return relationship; } catch (error) { this.emit('graph:error', { operation: 'updateRelationship', id, error }); throw error; } } /** * Delete a relationship */ async deleteRelationship(id) { try { const relationship = this.relationships.get(id); if (!relationship) { return false; } // Remove from type index const typeSet = this.relationshipIndex.get(relationship.type); if (typeSet) { typeSet.delete(id); if (typeSet.size === 0) { this.relationshipIndex.delete(relationship.type); } } // Remove from adjacency lists this.adjacencyList.get(relationship.fromNodeId)?.delete(id); this.adjacencyList.get(relationship.toNodeId)?.delete(id); // Remove relationship this.relationships.delete(id); this.emit('relationship:deleted', { id }); return true; } catch (error) { this.emit('graph:error', { operation: 'deleteRelationship', id, error }); throw error; } } /** * Find nodes by label and properties */ async findNodes(labels, properties, limit) { const startTime = Date.now(); try { let candidateIds; if (labels && labels.length > 0) { // Start with nodes matching first label candidateIds = new Set(this.nodeIndex.get(labels[0]) || []); // Intersect with other labels for (let i = 1; i < labels.length; i++) { const labelNodes = this.nodeIndex.get(labels[i]) || new Set(); candidateIds = new Set([...candidateIds].filter(x => labelNodes.has(x))); } } else { // All nodes candidateIds = new Set(this.nodes.keys()); } const results = []; for (const nodeId of candidateIds) { const node = this.nodes.get(nodeId); // Check property filters if (properties && !this.matchesProperties(node.properties, properties)) { continue; } results.push(node); if (limit && results.length >= limit) { break; } } const duration = Date.now() - startTime; this.trackLatency(duration); this.emit('nodes:found', { count: results.length, duration }); return results; } catch (error) { this.emit('graph:error', { operation: 'findNodes', error }); throw error; } } /** * Traverse the graph from a starting node */ async traverse(startNodeId, options = {}) { const startTime = Date.now(); try { const { maxDepth = 3, relationshipTypes, nodeLabels, direction = 'both', limit = 100, filters = {} } = options; if (!this.nodes.has(startNodeId)) { throw new Error(`Start node ${startNodeId} not found`); } const paths = []; const visited = new Set(); await this.traverseRecursive(startNodeId, [], [], 0, maxDepth, visited, paths, limit, relationshipTypes, nodeLabels, direction, filters); const duration = Date.now() - startTime; this.trackLatency(duration); this.emit('graph:traversed', { startNodeId, pathCount: paths.length, maxDepth, duration }); return paths; } catch (error) { this.emit('graph:error', { operation: 'traverse', startNodeId, error }); throw error; } } /** * Execute a simplified Cypher-like query */ async executeCypherQuery(cypherQuery) { const startTime = Date.now(); try { // This is a simplified implementation - real Cypher parsing would be much more complex const { query } = cypherQuery; // For now, support basic MATCH queries const matchPattern = /MATCH\s+\((\w+):?(\w*)\)(?:-\[(\w*):?(\w*)\]->\((\w+):?(\w*)\))?/i; const match = query.match(matchPattern); if (!match) { throw new Error('Unsupported Cypher query format'); } const [, , label1, , relType, nodeVar2, label2] = match; let nodes = []; let relationships = []; if (nodeVar2) { // Relationship pattern const startNodes = await this.findNodes(label1 ? [label1] : undefined); const paths = []; for (const startNode of startNodes) { const traversalOptions = { maxDepth: 1 }; if (relType) { traversalOptions.relationshipTypes = [relType]; } if (label2) { traversalOptions.nodeLabels = [label2]; } const nodePaths = await this.traverse(startNode.id, traversalOptions); paths.push(...nodePaths); } nodes = paths.flatMap(path => path.nodes); relationships = paths.flatMap(path => path.relationships); } else { // Node-only pattern nodes = await this.findNodes(label1 ? [label1] : undefined); } const duration = Date.now() - startTime; this.trackLatency(duration); return { nodes: this.deduplicateNodes(nodes), relationships: this.deduplicateRelationships(relationships), paths: [], executionTime: duration }; } catch (error) { this.emit('graph:error', { operation: 'executeCypherQuery', query: cypherQuery.query, error }); throw error; } } /** * Get graph statistics */ async getGraphStats() { const nodeLabels = new Map(); const relationshipTypes = new Map(); // Count node labels for (const node of this.nodes.values()) { for (const label of node.labels) { nodeLabels.set(label, (nodeLabels.get(label) || 0) + 1); } } // Count relationship types for (const relationship of this.relationships.values()) { const type = relationship.type; relationshipTypes.set(type, (relationshipTypes.get(type) || 0) + 1); } // Calculate average degree const totalDegree = Array.from(this.adjacencyList.values()) .reduce((sum, rels) => sum + rels.size, 0); const averageDegree = this.nodes.size > 0 ? totalDegree / this.nodes.size : 0; // Calculate max depth (simplified - actual implementation would use BFS) const maxDepth = Math.min(10, this.nodes.size); // Simplified const sortedLatencies = this.latencies.slice().sort((a, b) => a - b); return { totalNodes: this.nodes.size, totalRelationships: this.relationships.size, nodeLabels, relationshipTypes, averageDegree, maxDepth, queryLatency: { p50: this.percentile(sortedLatencies, 0.5), p95: this.percentile(sortedLatencies, 0.95), p99: this.percentile(sortedLatencies, 0.99) } }; } /** * Get node by ID */ async getNode(id) { return this.nodes.get(id) || null; } /** * Get relationship by ID */ async getRelationship(id) { return this.relationships.get(id) || null; } /** * Find relationships by type */ async findRelationships(type, properties, limit) { let candidates; if (type) { const relationshipIds = this.relationshipIndex.get(type) || new Set(); candidates = Array.from(relationshipIds).map(id => this.relationships.get(id)); } else { candidates = Array.from(this.relationships.values()); } const results = candidates.filter(rel => !properties || this.matchesProperties(rel.properties, properties)); return limit ? results.slice(0, limit) : results; } // Private helper methods async traverseRecursive(currentNodeId, currentPath, currentRelationships, depth, maxDepth, visited, paths, limit, relationshipTypes, nodeLabels, direction = 'both', filters = {}) { if (depth >= maxDepth || paths.length >= limit || visited.has(currentNodeId)) { return; } visited.add(currentNodeId); const currentNode = this.nodes.get(currentNodeId); const newPath = [...currentPath, currentNode]; // Add current path if we have traversed at least one relationship if (currentRelationships.length > 0) { paths.push({ nodes: newPath, relationships: [...currentRelationships], length: currentRelationships.length }); } // Get connected relationships const connectedRelIds = this.adjacencyList.get(currentNodeId) || new Set(); for (const relId of connectedRelIds) { const relationship = this.relationships.get(relId); // Check relationship type filter if (relationshipTypes && !relationshipTypes.includes(relationship.type)) { continue; } // Determine next node based on direction let nextNodeId = null; if (direction === 'outgoing' && relationship.fromNodeId === currentNodeId) { nextNodeId = relationship.toNodeId; } else if (direction === 'incoming' && relationship.toNodeId === currentNodeId) { nextNodeId = relationship.fromNodeId; } else if (direction === 'both') { nextNodeId = relationship.fromNodeId === currentNodeId ? relationship.toNodeId : relationship.fromNodeId; } if (!nextNodeId || visited.has(nextNodeId)) { continue; } const nextNode = this.nodes.get(nextNodeId); // Check node label filter if (nodeLabels && !nodeLabels.some(label => nextNode.labels.includes(label))) { continue; } // Check filters if (!this.matchesProperties(nextNode.properties, filters)) { continue; } await this.traverseRecursive(nextNodeId, newPath, [...currentRelationships, relationship], depth + 1, maxDepth, new Set(visited), // New visited set for each branch paths, limit, relationshipTypes, nodeLabels, direction, filters); } } matchesProperties(nodeProperties, filters) { for (const [key, value] of Object.entries(filters)) { if (nodeProperties[key] !== value) { return false; } } return true; } deduplicateNodes(nodes) { const seen = new Set(); return nodes.filter(node => { if (seen.has(node.id)) { return false; } seen.add(node.id); return true; }); } deduplicateRelationships(relationships) { const seen = new Set(); return relationships.filter(rel => { if (seen.has(rel.id)) { return false; } seen.add(rel.id); return true; }); } trackLatency(duration) { this.latencies.push(duration); // Keep only last 1000 measurements if (this.latencies.length > 1000) { this.latencies.splice(0, this.latencies.length - 1000); } } percentile(sortedArray, p) { if (sortedArray.length === 0) return 0; const index = Math.ceil(sortedArray.length * p) - 1; return sortedArray[Math.max(0, index)] || 0; } } //# sourceMappingURL=GraphStorageEngine.js.map