UNPKG

hikma-engine

Version:

Code Knowledge Graph Indexer - A sophisticated TypeScript-based indexer that transforms Git repositories into multi-dimensional knowledge stores for AI agents

347 lines (346 loc) 14.4 kB
"use strict"; 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.SQLiteClient = void 0; const better_sqlite3_1 = __importDefault(require("better-sqlite3")); const logger_1 = require("../../utils/logger"); const error_handling_1 = require("../../utils/error-handling"); const unit_of_work_1 = require("../unit-of-work"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); /** * Modern SQLiteClient designed for phase-based indexing architecture */ class SQLiteClient { constructor(dbPath) { this.dbPath = dbPath; this.logger = (0, logger_1.getLogger)('SQLiteClient'); this.isConnected = false; this.vectorEnabled = false; this.circuitBreaker = new error_handling_1.CircuitBreaker(5, 60000); this.logger.info(`Initializing SQLite client`, { path: dbPath }); try { // Ensure the directory exists before creating the database const dbDir = path.dirname(path.resolve(this.dbPath)); if (!fs.existsSync(dbDir)) { this.logger.info(`Creating database directory`, { directory: dbDir }); fs.mkdirSync(dbDir, { recursive: true }); } this.db = new better_sqlite3_1.default(this.dbPath); this.logger.debug('SQLite database instance created successfully'); } catch (error) { this.logger.error('Failed to create SQLite database instance', { error: (0, error_handling_1.getErrorMessage)(error), path: this.dbPath, }); throw new error_handling_1.DatabaseConnectionError('SQLite', `Failed to create database instance: ${(0, error_handling_1.getErrorMessage)(error)}`, error); } } async connect() { if (this.isConnected) { this.logger.debug('Already connected to SQLite'); return; } try { await (0, error_handling_1.withRetry)(async () => { this.logger.info('Connecting to SQLite'); await this.testConnection(); this.loadVectorExtension(); this.isConnected = true; this.logger.info('Connected to SQLite successfully', { vectorEnabled: this.vectorEnabled, }); }, error_handling_1.DEFAULT_RETRY_CONFIG, this.logger, 'SQLite connection'); } catch (error) { this.logger.error('Failed to connect to SQLite after retries', { error: (0, error_handling_1.getErrorMessage)(error), circuitBreakerState: this.circuitBreaker.getState(), }); throw new error_handling_1.DatabaseConnectionError('SQLite', `Connection failed: ${(0, error_handling_1.getErrorMessage)(error)}`, error); } } disconnect() { if (!this.isConnected) { this.logger.debug('Already disconnected from SQLite'); return; } try { this.logger.info('Disconnecting from SQLite'); this.db.close(); this.isConnected = false; this.logger.info('Disconnected from SQLite successfully'); } catch (error) { this.logger.error('Failed to disconnect from SQLite', { error: (0, error_handling_1.getErrorMessage)(error), }); throw error; } } getDb() { if (!this.isConnected) { throw new error_handling_1.DatabaseConnectionError('SQLite', 'Not connected to SQLite. Call connect() first.'); } return this.db; } getUnitOfWork() { return new unit_of_work_1.UnitOfWork(this.getDb()); } // Convenience methods for common operations run(sql, params) { const db = this.getDb(); try { this.logger.debug(`Executing SQLite query`, { sql: sql.substring(0, 100), paramCount: params?.length || 0 }); const stmt = db.prepare(sql); const result = params ? stmt.run(...params) : stmt.run(); this.logger.debug('SQLite query executed successfully', { changes: result.changes, lastInsertRowid: result.lastInsertRowid }); return result; } catch (error) { this.logger.error('SQLite query execution failed', { error: (0, error_handling_1.getErrorMessage)(error), sql: sql.substring(0, 100), paramCount: params?.length || 0 }); throw new error_handling_1.DatabaseOperationError('SQLite', 'run', (0, error_handling_1.getErrorMessage)(error), error); } } prepare(sql) { const db = this.getDb(); try { this.logger.debug(`Preparing SQLite statement`, { sql: sql.substring(0, 100) }); const stmt = db.prepare(sql); this.logger.debug('SQLite statement prepared successfully'); return stmt; } catch (error) { this.logger.error('Failed to prepare SQLite statement', { error: (0, error_handling_1.getErrorMessage)(error), sql: sql.substring(0, 100) }); throw new error_handling_1.DatabaseOperationError('SQLite', 'prepare', (0, error_handling_1.getErrorMessage)(error), error); } } all(sql, params) { const db = this.getDb(); this.logger.debug(`Executing SQLite SELECT query`, { sql: sql.substring(0, 100) }); const stmt = db.prepare(sql); return params && params.length > 0 ? stmt.all(...params) : stmt.all(); } get(sql, params) { const db = this.getDb(); this.logger.debug(`Executing SQLite GET query`, { sql: sql.substring(0, 100) }); const stmt = db.prepare(sql); return params && params.length > 0 ? stmt.get(...params) : stmt.get(); } transaction(fn) { const db = this.getDb(); const transaction = db.transaction(fn); try { const result = transaction(); this.logger.debug('Transaction completed successfully'); return result; } catch (error) { this.logger.error('Transaction failed', { error: (0, error_handling_1.getErrorMessage)(error) }); throw error; } } // Vector operations async storeVector(table, column, recordId, embedding) { if (!this.vectorEnabled) { this.logger.warn('Vector operations not available, skipping embedding storage', { table, recordId }); return; } try { const embeddingBlob = Buffer.from(new Float32Array(embedding).buffer); const sql = `UPDATE ${table} SET ${column} = ? WHERE id = ?`; this.run(sql, [embeddingBlob, recordId]); this.logger.debug(`Stored embedding for ${table}.${recordId}`, { embeddingDimensions: embedding.length, column }); } catch (error) { this.logger.error(`Failed to store embedding for ${table}.${recordId}`, { error: (0, error_handling_1.getErrorMessage)(error), column }); throw new error_handling_1.DatabaseOperationError('SQLite', 'storeEmbedding', (0, error_handling_1.getErrorMessage)(error), error); } } async vectorSearch(table, column, queryEmbedding, limit = 10, threshold) { if (!this.vectorEnabled) { this.logger.warn('Vector operations not available, returning empty results', { table, column }); return []; } try { const queryBlob = Buffer.from(new Float32Array(queryEmbedding).buffer); let sql = ` SELECT id, vec_distance_cosine(${column}, ?) as similarity, * FROM ${table} WHERE ${column} IS NOT NULL `; const params = [queryBlob]; if (threshold !== undefined) { const distanceThreshold = 1 - threshold; sql += ` AND vec_distance_cosine(${column}, ?) <= ?`; params.push(queryBlob, distanceThreshold); } sql += ` ORDER BY similarity ASC LIMIT ?`; params.push(limit); const results = this.all(sql, params); return results.map(row => ({ id: row.id, similarity: 1 - row.similarity, // Convert distance to similarity data: row })); } catch (error) { this.logger.error(`Vector search failed for ${table}.${column}`, { error: (0, error_handling_1.getErrorMessage)(error), queryDimensions: queryEmbedding.length }); throw new error_handling_1.DatabaseOperationError('SQLite', 'vectorSearch', (0, error_handling_1.getErrorMessage)(error), error); } } // State management for phases getLastIndexedCommit() { try { const result = this.get('SELECT value FROM indexing_state WHERE key = ?', ['last_indexed_commit']); return result?.value || null; } catch (error) { this.logger.warn('Failed to get last indexed commit', { error: (0, error_handling_1.getErrorMessage)(error) }); return null; } } setLastIndexedCommit(commitHash) { try { this.run(`INSERT OR REPLACE INTO indexing_state (id, key, value, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, [`last_indexed_commit`, 'last_indexed_commit', commitHash]); this.logger.debug('Set last indexed commit', { commitHash }); } catch (error) { this.logger.error('Failed to set last indexed commit', { error: (0, error_handling_1.getErrorMessage)(error) }); throw error; } } // Connection status isConnectedToDatabase() { return this.isConnected; } get isVectorEnabled() { return this.vectorEnabled; } async isVectorSearchAvailable() { return this.vectorEnabled; } // Stats for monitoring async getIndexingStats() { try { const fileCount = this.get('SELECT COUNT(*) as count FROM files')?.count || 0; const commitCount = this.get('SELECT COUNT(*) as count FROM commits')?.count || 0; const lastIndexed = this.getLastIndexedCommit(); return { totalFiles: fileCount, totalCommits: commitCount, lastIndexed, }; } catch (error) { this.logger.error('Failed to get indexing stats', { error: (0, error_handling_1.getErrorMessage)(error) }); throw error; } } async testConnection() { try { this.db.prepare('SELECT 1').get(); this.logger.debug('SQLite connection test successful'); } catch (error) { this.logger.warn('SQLite connection test failed', { error: (0, error_handling_1.getErrorMessage)(error), }); throw error; } } loadVectorExtension() { try { let extensionPath = process.env.HIKMA_SQLITE_VEC_EXTENSION; if (!extensionPath) { // Try to resolve the extension path relative to the package const packageRoot = path.resolve(__dirname, '../../..'); const localExtensionPath = path.join(packageRoot, 'extensions', 'vec0'); // Check if the extension exists in the package directory if (fs.existsSync(localExtensionPath + '.dylib') || fs.existsSync(localExtensionPath + '.so') || fs.existsSync(localExtensionPath + '.dll')) { extensionPath = localExtensionPath; } else { // Fallback to relative path for development extensionPath = './extensions/vec0'; } } this.db.loadExtension(extensionPath); this.db.prepare('SELECT vec_version()').get(); this.vectorEnabled = true; this.logger.info('sqlite-vec extension loaded successfully', { extensionPath, }); } catch (error) { this.vectorEnabled = false; const errorMsg = (0, error_handling_1.getErrorMessage)(error); this.logger.warn('Failed to load sqlite-vec extension, vector operations will be disabled', { error: errorMsg, extensionPath: process.env.HIKMA_SQLITE_VEC_EXTENSION || 'auto-resolved', }); } } } exports.SQLiteClient = SQLiteClient;