UNPKG

@aaswe/codebase-ai

Version:

AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs

1,225 lines 48.4 kB
"use strict"; /** * Neo4j Database Service * * Comprehensive Neo4j database integration service providing connection management, * schema operations, TTL file ingestion, query optimization, and health monitoring. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Neo4jDatabaseService = void 0; const neo4j_driver_1 = __importDefault(require("neo4j-driver")); const events_1 = require("events"); const fs = __importStar(require("fs/promises")); /** * Main Neo4j Database Service * Orchestrates all Neo4j operations including connection management, * schema operations, TTL ingestion, and query optimization. */ class Neo4jDatabaseService extends events_1.EventEmitter { driver = null; config = null; cacheStats = { totalQueries: 0, cacheHits: 0, cacheMisses: 0, hitRate: 0, totalMemoryUsage: 0, averageQueryTime: 0 }; eventListeners = []; constructor() { super(); this.setupEventHandlers(); } // Connection Management Implementation async connect(config) { try { this.config = config; const auth = neo4j_driver_1.default.auth.basic(config.username, config.password); this.driver = neo4j_driver_1.default.driver(config.uri, auth, { maxConnectionPoolSize: config.maxConnectionPoolSize || 50, connectionTimeout: config.connectionTimeout || 30000, maxTransactionRetryTime: config.maxTransactionRetryTime || 30000, encrypted: config.encrypted || false, // Default to false for local development trust: config.trust || 'TRUST_SYSTEM_CA_SIGNED_CERTIFICATES' }); await this.testConnection(); this.emitEvent({ type: 'CONNECTION', timestamp: new Date(), details: { action: 'CONNECT', database: config.database || 'neo4j', user: config.username }, severity: 'INFO' }); return this.driver; } catch (error) { this.emitError(error, 'Failed to connect to Neo4j database'); throw error; } } async disconnect() { if (this.driver) { await this.driver.close(); this.driver = null; this.emitEvent({ type: 'CONNECTION', timestamp: new Date(), details: { action: 'DISCONNECT', database: this.config?.database || 'neo4j', user: this.config?.username || 'unknown' }, severity: 'INFO' }); } } getDriver() { return this.driver; } isConnected() { return this.driver !== null; } async testConnection() { if (!this.driver) { return false; } try { const session = this.driver.session(); const result = await session.run('RETURN 1 as test'); await session.close(); return result.records.length > 0; } catch (error) { this.emitError(error, 'Connection test failed'); return false; } } getSession(database) { if (!this.driver) { throw new Error('Neo4j driver not initialized. Call connect() first.'); } return this.driver.session({ database: database || this.config?.database || 'neo4j' }); } async executeTransaction(work, database) { const session = this.getSession(database); try { const startTime = Date.now(); const result = await session.executeWrite(work); const executionTime = Date.now() - startTime; this.emitEvent({ type: 'TRANSACTION', timestamp: new Date(), details: { action: 'COMMIT', database: database || this.config?.database || 'neo4j', duration: executionTime }, severity: 'INFO' }); return result; } catch (error) { this.emitEvent({ type: 'TRANSACTION', timestamp: new Date(), details: { action: 'ROLLBACK', database: database || this.config?.database || 'neo4j' }, severity: 'ERROR' }); throw error; } finally { await session.close(); } } // TTL Ingestion Pipeline Implementation async ingestTTLFile(filePath) { const startTime = Date.now(); try { const content = await fs.readFile(filePath, 'utf-8'); return await this.ingestTTLContent(content, filePath); } catch (error) { return { success: false, sourceFile: filePath, nodesCreated: 0, relationshipsCreated: 0, propertiesSet: 0, errors: [{ type: 'PARSING_ERROR', message: `Failed to read TTL file: ${error.message}`, sourceFile: filePath }], warnings: [], processingTime: Date.now() - startTime, cypherQueries: [] }; } } async ingestTTLContent(content, sourceFile) { const startTime = Date.now(); const result = { success: false, sourceFile, nodesCreated: 0, relationshipsCreated: 0, propertiesSet: 0, errors: [], warnings: [], processingTime: 0, cypherQueries: [] }; try { // Validate TTL content before ingestion const validation = await this.validateTTLBeforeIngestion(content); if (!validation.valid) { result.errors = validation.errors.map(err => ({ type: 'PARSING_ERROR', message: err.message, sourceFile, lineNumber: err.lineNumber })); result.processingTime = Date.now() - startTime; return result; } // Parse TTL content into RDF triples const triples = this.parseTTLContent(content, sourceFile); // Create nodes from triples const nodeResult = await this.createNodesFromTriples(triples); result.nodesCreated = nodeResult.nodesCreated; result.cypherQueries.push(...nodeResult.cypherQueries); result.errors.push(...nodeResult.errors); // Create relationships from triples const relationshipResult = await this.createRelationshipsFromTriples(triples); result.relationshipsCreated = relationshipResult.relationshipsCreated; result.cypherQueries.push(...relationshipResult.cypherQueries); result.errors.push(...relationshipResult.errors); // Calculate properties set result.propertiesSet = triples.filter(t => t.objectType === 'literal').length; result.success = result.errors.length === 0; result.processingTime = Date.now() - startTime; return result; } catch (error) { result.errors.push({ type: 'PARSING_ERROR', message: `TTL ingestion failed: ${error.message}`, sourceFile }); result.processingTime = Date.now() - startTime; return result; } } async batchIngestTTLFiles(filePaths) { const startTime = Date.now(); const results = []; const errors = []; for (const filePath of filePaths) { try { const result = await this.ingestTTLFile(filePath); results.push(result); if (!result.success) { errors.push(...result.errors); } } catch (error) { errors.push({ type: 'PARSING_ERROR', message: `Batch ingestion failed for ${filePath}: ${error.message}`, sourceFile: filePath }); } } const successfulFiles = results.filter(r => r.success).length; const totalNodesCreated = results.reduce((sum, r) => sum + r.nodesCreated, 0); const totalRelationshipsCreated = results.reduce((sum, r) => sum + r.relationshipsCreated, 0); return { totalFiles: filePaths.length, successfulFiles, failedFiles: filePaths.length - successfulFiles, results, totalNodesCreated, totalRelationshipsCreated, totalProcessingTime: Date.now() - startTime, errors }; } async validateTTLBeforeIngestion(content) { const errors = []; const warnings = []; try { // Basic TTL syntax validation const lines = content.split('\n'); let tripleCount = 0; let nodeCount = 0; let relationshipCount = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Skip empty lines and comments if (!line || line.startsWith('#')) continue; // Basic triple pattern validation if (line.includes(' ') && (line.endsWith('.') || line.endsWith(';'))) { tripleCount++; // Count potential nodes and relationships if (line.includes('a ') || line.includes('rdf:type')) { nodeCount++; } else if (line.includes(':') && !line.includes('rdf:type')) { relationshipCount++; } } else if (line.length > 0) { errors.push({ type: 'SYNTAX_ERROR', message: `Invalid TTL syntax: ${line}`, lineNumber: i + 1 }); } } // Validation warnings if (tripleCount === 0) { warnings.push('No valid triples found in TTL content'); } if (nodeCount === 0) { warnings.push('No node definitions found in TTL content'); } return { valid: errors.length === 0, errors, warnings, tripleCount, nodeCount, relationshipCount }; } catch (error) { errors.push({ type: 'SYNTAX_ERROR', message: `TTL validation failed: ${error.message}` }); return { valid: false, errors, warnings, tripleCount: 0, nodeCount: 0, relationshipCount: 0 }; } } async createNodesFromTriples(triples) { const nodesByLabel = {}; const errors = []; const cypherQueries = []; let nodesCreated = 0; try { // Group triples by subject to create nodes const nodeTriples = triples.filter(t => t.predicate === 'rdf:type' || t.predicate === 'a'); for (const triple of nodeTriples) { try { const nodeId = this.extractNodeId(triple.subject); const nodeLabel = this.extractNodeLabel(triple.object); // Clean node label for Cypher (remove namespace prefixes and make valid identifier) const cleanLabel = nodeLabel.replace(/^[^:]*:/, '').replace(/[^a-zA-Z0-9_]/g, '_'); const cypherQuery = ` MERGE (n:\`${cleanLabel}\` {id: $nodeId}) SET n.sourceFile = $sourceFile, n.lastUpdated = datetime() RETURN n `; const session = this.getSession(); const result = await session.run(cypherQuery, { nodeId, sourceFile: triple.sourceFile }); await session.close(); if (result.records.length > 0) { nodesCreated++; nodesByLabel[nodeLabel] = (nodesByLabel[nodeLabel] || 0) + 1; cypherQueries.push(cypherQuery); } } catch (error) { errors.push({ type: 'CYPHER_ERROR', message: `Failed to create node: ${error.message}`, sourceFile: triple.sourceFile, triple, lineNumber: triple.lineNumber }); } } return { nodesCreated, nodesByLabel, errors, cypherQueries }; } catch (error) { errors.push({ type: 'CYPHER_ERROR', message: `Node creation failed: ${error.message}`, sourceFile: triples[0]?.sourceFile || 'unknown' }); return { nodesCreated: 0, nodesByLabel: {}, errors, cypherQueries: [] }; } } async createRelationshipsFromTriples(triples) { const relationshipsByType = {}; const errors = []; const cypherQueries = []; let relationshipsCreated = 0; try { // Filter relationship triples (not type declarations) const relationshipTriples = triples.filter(t => t.predicate !== 'rdf:type' && t.predicate !== 'a' && t.objectType === 'uri'); for (const triple of relationshipTriples) { try { const sourceId = this.extractNodeId(triple.subject); const targetId = this.extractNodeId(triple.object); const relationshipType = this.extractRelationshipType(triple.predicate); // Clean relationship type for Cypher (remove namespace prefixes and make valid identifier) let cleanRelType = relationshipType.replace(/^[^:]*:/, '').replace(/[^a-zA-Z0-9_]/g, '_'); // Additional validation to ensure non-empty relationship type if (!cleanRelType || cleanRelType.trim() === '') { cleanRelType = 'RELATED_TO'; } const cypherQuery = ` MATCH (source {id: $sourceId}) MATCH (target {id: $targetId}) MERGE (source)-[r:\`${cleanRelType}\`]->(target) SET r.sourceFile = $sourceFile, r.lastUpdated = datetime() RETURN r `; const session = this.getSession(); const result = await session.run(cypherQuery, { sourceId, targetId, sourceFile: triple.sourceFile }); await session.close(); if (result.records.length > 0) { relationshipsCreated++; relationshipsByType[relationshipType] = (relationshipsByType[relationshipType] || 0) + 1; cypherQueries.push(cypherQuery); } } catch (error) { errors.push({ type: 'CYPHER_ERROR', message: `Failed to create relationship: ${error.message}`, sourceFile: triple.sourceFile, triple, lineNumber: triple.lineNumber }); } } return { relationshipsCreated, relationshipsByType, errors, cypherQueries }; } catch (error) { errors.push({ type: 'CYPHER_ERROR', message: `Relationship creation failed: ${error.message}`, sourceFile: triples[0]?.sourceFile || 'unknown' }); return { relationshipsCreated: 0, relationshipsByType: {}, errors, cypherQueries: [] }; } } // Query Optimization Implementation async optimizeQuery(query) { const originalQuery = query; let optimizedQuery = query; const optimizations = []; let estimatedImprovement = 0; const warnings = []; try { // Basic query optimizations // 1. Add index hints for common patterns if (query.includes('WHERE') && query.includes('=')) { const indexHint = this.suggestIndexHints(query); if (indexHint) { optimizedQuery = indexHint.optimizedQuery; optimizations.push({ type: 'INDEX_HINT', description: 'Added index hint for property lookup', impact: 'HIGH', before: query, after: optimizedQuery }); estimatedImprovement += 30; } } // 2. Parameter binding optimization if (query.includes("'") || query.includes('"')) { const parameterized = this.parameterizeQuery(query); if (parameterized.hasChanges) { optimizedQuery = parameterized.query; optimizations.push({ type: 'PARAMETER_BINDING', description: 'Converted literals to parameters', impact: 'MEDIUM', before: query, after: optimizedQuery }); estimatedImprovement += 15; } } // 3. Query rewrite optimizations if (query.toLowerCase().includes('optional match')) { warnings.push('OPTIONAL MATCH can be expensive. Consider using WHERE EXISTS() pattern.'); } if (query.toLowerCase().includes('collect(')) { warnings.push('COLLECT() operations can consume significant memory for large result sets.'); } return { originalQuery, optimizedQuery, optimizations, estimatedImprovement, warnings }; } catch (error) { warnings.push(`Query optimization failed: ${error.message}`); return { originalQuery, optimizedQuery: originalQuery, optimizations: [], estimatedImprovement: 0, warnings }; } } async analyzeQueryPerformance(query) { try { const session = this.getSession(); const startTime = Date.now(); // Execute query with profiling const result = await session.run(`PROFILE ${query}`); const executionTime = Date.now() - startTime; await session.close(); // Extract performance metrics from profile const profile = result.summary.profile; const plan = result.summary.plan; return { executionTime, dbHits: profile?.dbHits || 0, rows: result.records.length, memoryUsage: 0, // Neo4j doesn't directly expose memory usage planningTime: 0, // Would need to be calculated separately profile: this.convertProfileToQueryProfile(profile), bottlenecks: this.identifyBottlenecks(profile, plan) }; } catch (error) { throw new Error(`Query performance analysis failed: ${error.message}`); } } async suggestIndexes(query) { const suggestions = []; try { // Analyze query for potential index opportunities const patterns = this.extractQueryPatterns(query); for (const pattern of patterns) { if (pattern.type === 'property_lookup') { suggestions.push({ type: 'BTREE', nodeLabel: pattern.label, properties: [pattern.property], estimatedImprovement: 50, cypherCommand: `CREATE INDEX ${pattern.label}_${pattern.property}_idx FOR (n:${pattern.label}) ON (n.${pattern.property})`, reasoning: `Property lookup on ${pattern.label}.${pattern.property} would benefit from an index` }); } if (pattern.type === 'text_search') { suggestions.push({ type: 'FULLTEXT', nodeLabel: pattern.label, properties: [pattern.property], estimatedImprovement: 70, cypherCommand: `CREATE FULLTEXT INDEX ${pattern.label}_${pattern.property}_fulltext FOR (n:${pattern.label}) ON EACH [n.${pattern.property}]`, reasoning: `Text search on ${pattern.label}.${pattern.property} would benefit from a fulltext index` }); } } return suggestions; } catch (error) { throw new Error(`Index suggestion failed: ${error.message}`); } } async validateQuery(query) { const errors = []; const warnings = []; try { // Basic syntax validation const syntaxValidation = this.validateQuerySyntax(query); // Semantic validation const semanticValidation = await this.validateQuerySemantics(query); return { valid: syntaxValidation.valid && semanticValidation.valid, errors: [...syntaxValidation.errors, ...semanticValidation.errors], warnings, syntax: syntaxValidation, semantics: semanticValidation }; } catch (error) { errors.push({ type: 'SYNTAX_ERROR', message: `Query validation failed: ${error.message}` }); return { valid: false, errors, warnings, syntax: { valid: false, errors: [error.message] }, semantics: { valid: false, errors: [], undefinedLabels: [], undefinedProperties: [], undefinedRelationships: [] } }; } } async explainQuery(query) { try { const session = this.getSession(); const result = await session.run(`EXPLAIN ${query}`); await session.close(); const plan = result.summary.plan; return { query, executionPlan: this.convertPlanToExecutionPlan(plan), estimatedCost: plan?.arguments?.EstimatedRows || 0, estimatedRows: plan?.arguments?.EstimatedRows || 0, indexUsage: this.extractIndexUsage(plan), recommendations: this.generateQueryRecommendations(plan) }; } catch (error) { throw new Error(`Query explanation failed: ${error.message}`); } } // Health Monitoring Implementation async checkHealth() { const checks = []; const startTime = Date.now(); try { // Connection health check const connectionCheck = await this.checkConnectionHealth(); checks.push(connectionCheck); // Database health check const databaseCheck = await this.checkDatabaseHealth(); checks.push(databaseCheck); // Schema health check const schemaCheck = await this.checkSchemaHealth(); checks.push(schemaCheck); // Performance health check const performanceCheck = await this.checkPerformanceHealth(); checks.push(performanceCheck); const failedChecks = checks.filter(c => c.status === 'FAIL'); const warnChecks = checks.filter(c => c.status === 'WARN'); let status; if (failedChecks.length > 0) { status = 'UNHEALTHY'; } else if (warnChecks.length > 0) { status = 'DEGRADED'; } else { status = 'HEALTHY'; } return { status, checks, lastChecked: new Date(), uptime: Date.now() - startTime }; } catch (error) { checks.push({ name: 'health_check_error', status: 'FAIL', message: `Health check failed: ${error.message}`, duration: Date.now() - startTime }); return { status: 'UNHEALTHY', checks, lastChecked: new Date(), uptime: 0 }; } } async getMetrics() { try { const session = this.getSession(); // Get node count const nodeCountResult = await session.run('MATCH (n) RETURN count(n) as nodeCount'); const nodeCount = nodeCountResult.records[0]?.get('nodeCount')?.toNumber() || 0; // Get relationship count const relCountResult = await session.run('MATCH ()-[r]->() RETURN count(r) as relCount'); const relationshipCount = relCountResult.records[0]?.get('relCount')?.toNumber() || 0; // Get label counts const labelCountsResult = await session.run('CALL db.labels() YIELD label MATCH (n) WHERE label IN labels(n) RETURN label, count(n) as count'); const labelCounts = {}; labelCountsResult.records.forEach(record => { labelCounts[record.get('label')] = record.get('count').toNumber(); }); // Get relationship type counts const relTypeCountsResult = await session.run('CALL db.relationshipTypes() YIELD relationshipType MATCH ()-[r]->() WHERE type(r) = relationshipType RETURN relationshipType, count(r) as count'); const relationshipTypeCounts = {}; relTypeCountsResult.records.forEach(record => { relationshipTypeCounts[record.get('relationshipType')] = record.get('count').toNumber(); }); await session.close(); return { nodeCount, relationshipCount, propertyCount: 0, // Would need additional query labelCounts, relationshipTypeCounts, storageSize: 0, // Would need APOC or system queries memoryUsage: 0, // Would need system queries queryCount: this.cacheStats.totalQueries, averageQueryTime: this.cacheStats.averageQueryTime, activeTransactions: 0 // Would need system queries }; } catch (error) { throw new Error(`Failed to get database metrics: ${error.message}`); } } async getConstraints() { try { const session = this.getSession(); const result = await session.run('SHOW CONSTRAINTS'); await session.close(); return result.records.map(record => ({ name: record.get('name'), type: record.get('type'), state: record.get('state'), nodeLabel: record.get('labelsOrTypes')?.[0], properties: record.get('properties') || [], failureMessage: record.get('failureMessage') })); } catch (error) { throw new Error(`Failed to get constraints: ${error.message}`); } } async getIndexes() { try { const session = this.getSession(); const result = await session.run('SHOW INDEXES'); await session.close(); return result.records.map(record => ({ name: record.get('name'), type: record.get('type'), state: record.get('state'), nodeLabel: record.get('labelsOrTypes')?.[0], properties: record.get('properties') || [], populationPercent: record.get('populationPercent'), failureMessage: record.get('failureMessage') })); } catch (error) { throw new Error(`Failed to get indexes: ${error.message}`); } } async getStorageInfo() { try { const session = this.getSession(); // Get database storage information using system procedures const storageResult = await session.run(` CALL dbms.queryJmx("org.neo4j:instance=kernel#0,name=Store file sizes") YIELD attributes RETURN attributes `); let storageInfo = { totalSize: 0, usedSize: 0, freeSize: 0, nodeStoreSize: 0, relationshipStoreSize: 0, propertyStoreSize: 0, stringStoreSize: 0, arrayStoreSize: 0 }; if (storageResult.records.length > 0) { const attributes = storageResult.records[0].get('attributes'); // Extract storage sizes from JMX attributes storageInfo.nodeStoreSize = this.extractStorageSize(attributes, 'NodeStoreSize'); storageInfo.relationshipStoreSize = this.extractStorageSize(attributes, 'RelationshipStoreSize'); storageInfo.propertyStoreSize = this.extractStorageSize(attributes, 'PropertyStoreSize'); storageInfo.stringStoreSize = this.extractStorageSize(attributes, 'StringStoreSize'); storageInfo.arrayStoreSize = this.extractStorageSize(attributes, 'ArrayStoreSize'); storageInfo.usedSize = storageInfo.nodeStoreSize + storageInfo.relationshipStoreSize + storageInfo.propertyStoreSize + storageInfo.stringStoreSize + storageInfo.arrayStoreSize; } // Get total database size using alternative approach if JMX fails if (storageInfo.usedSize === 0) { try { const sizeResult = await session.run(` CALL apoc.monitor.store() YIELD totalStoreSize, usedStoreSize RETURN totalStoreSize, usedStoreSize `); if (sizeResult.records.length > 0) { const record = sizeResult.records[0]; storageInfo.totalSize = record.get('totalStoreSize') || 0; storageInfo.usedSize = record.get('usedStoreSize') || 0; storageInfo.freeSize = storageInfo.totalSize - storageInfo.usedSize; } } catch (apocError) { // APOC not available, use basic estimation const countResult = await session.run(` MATCH (n) OPTIONAL MATCH (n)-[r]->() RETURN count(DISTINCT n) as nodeCount, count(r) as relCount `); if (countResult.records.length > 0) { const nodeCount = countResult.records[0].get('nodeCount').toNumber(); const relCount = countResult.records[0].get('relCount').toNumber(); // Rough estimation: 100 bytes per node, 50 bytes per relationship storageInfo.nodeStoreSize = nodeCount * 100; storageInfo.relationshipStoreSize = relCount * 50; storageInfo.usedSize = storageInfo.nodeStoreSize + storageInfo.relationshipStoreSize; storageInfo.totalSize = storageInfo.usedSize * 1.2; // Add 20% overhead storageInfo.freeSize = storageInfo.totalSize - storageInfo.usedSize; } } } await session.close(); return storageInfo; } catch (error) { throw new Error(`Failed to get storage info: ${error.message}`); } } extractStorageSize(attributes, sizeName) { try { if (attributes && attributes[sizeName]) { const sizeValue = attributes[sizeName].value || attributes[sizeName]; return typeof sizeValue === 'number' ? sizeValue : parseInt(sizeValue) || 0; } return 0; } catch (error) { return 0; } } // Private Helper Methods setupEventHandlers() { this.on('error', (error) => { console.error('Neo4j Database Service Error:', error); }); } emitEvent(event) { this.emit('database_event', event); this.eventListeners.forEach(listener => { switch (event.type) { case 'CONNECTION': listener.onConnection?.(event); break; case 'QUERY': listener.onQuery?.(event); break; case 'TRANSACTION': listener.onTransaction?.(event); break; case 'SCHEMA': listener.onSchema?.(event); break; case 'ERROR': listener.onError?.(event); break; } }); } emitError(error, context) { const errorEvent = { type: 'ERROR', timestamp: new Date(), details: { error: error.message, context: context || 'unknown' }, severity: 'ERROR' }; this.emitEvent(errorEvent); } parseTTLContent(content, sourceFile) { const triples = []; const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Skip empty lines and comments if (!line || line.startsWith('#')) continue; // Basic triple parsing (simplified) const parts = line.replace(/\.$/, '').split(/\s+/); if (parts.length >= 3) { const subject = parts[0]; const predicate = parts[1]; const object = parts.slice(2).join(' '); triples.push({ subject, predicate, object, objectType: this.determineObjectType(object), sourceFile, lineNumber: i + 1 }); } } return triples; } determineObjectType(object) { if (object.startsWith('<') && object.endsWith('>')) { return 'uri'; } else if (object.startsWith('_:')) { return 'blank'; } else { return 'literal'; } } extractNodeId(subject) { // Remove angle brackets and extract the last part after # const cleaned = subject.replace(/[<>]/g, ''); const parts = cleaned.split('#'); return parts[parts.length - 1] || cleaned; } extractNodeLabel(object) { // Extract label from RDF type declaration const cleaned = object.replace(/[<>]/g, ''); const parts = cleaned.split('#'); return parts[parts.length - 1] || 'Node'; } extractRelationshipType(predicate) { // Extract relationship type from predicate const cleaned = predicate.replace(/[<>]/g, ''); const parts = cleaned.split('#'); let relationshipType = parts[parts.length - 1] || 'RELATED_TO'; // Handle cases where the predicate might be a namespace prefix only if (!relationshipType || relationshipType.trim() === '') { // Try splitting by colon as well const colonParts = cleaned.split(':'); relationshipType = colonParts[colonParts.length - 1] || 'RELATED_TO'; } // Ensure we have a valid non-empty relationship type if (!relationshipType || relationshipType.trim() === '') { relationshipType = 'RELATED_TO'; } // Clean the relationship type for Cypher compatibility relationshipType = relationshipType.replace(/[^a-zA-Z0-9_]/g, '_'); // Ensure it doesn't start with a number if (/^[0-9]/.test(relationshipType)) { relationshipType = 'REL_' + relationshipType; } return relationshipType; } suggestIndexHints(query) { // Simple index hint suggestion if (query.includes('WHERE') && query.includes('=')) { const optimizedQuery = query.replace(/WHERE\s+(\w+)\.(\w+)\s*=/, 'USING INDEX $1($2) WHERE $1.$2 ='); return { optimizedQuery }; } return null; } parameterizeQuery(query) { let hasChanges = false; let parameterizedQuery = query; // Replace string literals with parameters const stringLiterals = query.match(/'[^']*'/g) || []; stringLiterals.forEach((literal, index) => { parameterizedQuery = parameterizedQuery.replace(literal, `$param${index}`); hasChanges = true; }); return { query: parameterizedQuery, hasChanges }; } convertProfileToQueryProfile(profile) { if (!profile) { return { operatorType: 'Unknown', identifiers: [], arguments: {}, children: [], dbHits: 0, rows: 0, time: 0 }; } return { operatorType: profile.operatorType || 'Unknown', identifiers: profile.identifiers || [], arguments: profile.arguments || {}, children: (profile.children || []).map((child) => this.convertProfileToQueryProfile(child)), dbHits: profile.dbHits || 0, rows: profile.rows || 0, time: profile.time || 0 }; } identifyBottlenecks(profile, _plan) { const bottlenecks = []; if (profile && profile.dbHits > 1000) { bottlenecks.push({ type: 'LARGE_SCAN', description: 'High database hits detected', suggestion: 'Consider adding indexes or optimizing query', impact: 'HIGH' }); } return bottlenecks; } extractQueryPatterns(query) { const patterns = []; // Look for property lookups const propertyMatches = query.match(/(\w+)\.(\w+)\s*=/g); if (propertyMatches) { propertyMatches.forEach(match => { const parts = match.split('.'); if (parts.length >= 2) { patterns.push({ type: 'property_lookup', label: parts[0], property: parts[1].replace(/\s*=.*/, '') }); } }); } // Look for text search patterns const textMatches = query.match(/CONTAINS\s+['"]([^'"]+)['"]/gi); if (textMatches) { textMatches.forEach(() => { patterns.push({ type: 'text_search', label: 'Node', property: 'text' }); }); } return patterns; } validateQuerySyntax(query) { const errors = []; // Basic syntax checks if (!query.trim()) { errors.push('Query cannot be empty'); } // Check for balanced parentheses const openParens = (query.match(/\(/g) || []).length; const closeParens = (query.match(/\)/g) || []).length; if (openParens !== closeParens) { errors.push('Unbalanced parentheses in query'); } // Check for basic Cypher keywords const hasValidKeyword = /\b(MATCH|CREATE|MERGE|RETURN|WHERE|WITH)\b/i.test(query); if (!hasValidKeyword) { errors.push('Query must contain at least one valid Cypher keyword'); } return { valid: errors.length === 0, errors }; } async validateQuerySemantics(_query) { // For now, return a basic semantic validation return { valid: true, errors: [], undefinedLabels: [], undefinedProperties: [], undefinedRelationships: [] }; } convertPlanToExecutionPlan(plan) { if (!plan) { return { operatorType: 'Unknown', cost: 0, rows: 0, arguments: {}, children: [] }; } return { operatorType: plan.operatorType || 'Unknown', cost: plan.cost || 0, rows: plan.rows || 0, arguments: plan.arguments || {}, children: (plan.children || []).map((child) => this.convertPlanToExecutionPlan(child)) }; } extractIndexUsage(plan) { const indexUsage = []; if (plan && plan.operatorType === 'NodeIndexSeek') { indexUsage.push({ indexName: plan.arguments?.index || 'unknown', indexType: 'BTREE', properties: plan.arguments?.properties || [], usage: 'SEEK', selectivity: plan.arguments?.selectivity || 0 }); } return indexUsage; } generateQueryRecommendations(plan) { const recommendations = []; if (plan && plan.operatorType === 'AllNodesScan') { recommendations.push('Consider adding a label to avoid scanning all nodes'); } if (plan && plan.operatorType === 'Filter') { recommendations.push('Consider adding an index for the filtered property'); } return recommendations; } async checkConnectionHealth() { const startTime = Date.now(); try { const isConnected = await this.testConnection(); return { name: 'connection_health', status: isConnected ? 'PASS' : 'FAIL', message: isConnected ? 'Database connection is healthy' : 'Database connection failed', duration: Date.now() - startTime }; } catch (error) { return { name: 'connection_health', status: 'FAIL', message: `Connection health check failed: ${error.message}`, duration: Date.now() - startTime }; } } async checkDatabaseHealth() { const startTime = Date.now(); try { const session = this.getSession(); await session.run('RETURN 1'); await session.close(); return { name: 'database_health', status: 'PASS', message: 'Database is responding to queries', duration: Date.now() - startTime }; } catch (error) { return { name: 'database_health', status: 'FAIL', message: `Database health check failed: ${error.message}`, duration: Date.now() - startTime }; } } async checkSchemaHealth() { const startTime = Date.now(); try { const constraints = await this.getConstraints(); const indexes = await this.getIndexes(); const failedConstraints = constraints.filter(c => c.state === 'FAILED'); const failedIndexes = indexes.filter(i => i.state === 'FAILED'); if (failedConstraints.length > 0 || failedIndexes.length > 0) { return { name: 'schema_health', status: 'WARN', message: `Found ${failedConstraints.length} failed constraints and ${failedIndexes.length} failed indexes`, duration: Date.now() - startTime }; } return { name: 'schema_health', status: 'PASS', message: 'All constraints and indexes are healthy', duration: Date.now() - startTime }; } catch (error) { return { name: 'schema_health', status: 'FAIL', message: `Schema health check failed: ${error.message}`, duration: Date.now() - startTime }; } } async checkPerformanceHealth() { const startTime = Date.now(); try { const metrics = await this.getMetrics(); // Check for performance issues if (metrics.averageQueryTime > 5000) { return { name: 'performance_health', status: 'WARN', message: `Average query time is high: ${metrics.averageQueryTime}ms`, duration: Date.now() - startTime }; } return { name: 'performance_health', status: 'PASS', message: 'Database performance is within acceptable limits', duration: Date.now() - startTime }; } catch (error) { return { name: 'performance_health', status: 'FAIL', message: `Performance health check failed: ${error.message}`, duration: Date.now() - startTime }; } } } exports.Neo4jDatabaseService = Neo4jDatabaseService; //# sourceMappingURL=Neo4jDatabaseService.js.map