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
JavaScript
"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;