claude-flow-multilang
Version:
Revolutionary multilingual AI orchestration framework with cultural awareness and DDD architecture
349 lines (290 loc) • 9.35 kB
text/typescript
/**
* SQLite backend implementation for memory storage
*/
import { promises as fs } from 'fs';
import path from 'path';
import type { IMemoryBackend } from './base.js';
import type { MemoryEntry, MemoryQuery } from '../../utils/types.js';
import type { ILogger } from '../../core/logger.js';
import { MemoryBackendError } from '../../utils/errors.js';
// Dynamic imports for SQLite
let createDatabase: any;
let isSQLiteAvailable: any;
/**
* SQLite-based memory backend
*/
export class SQLiteBackend implements IMemoryBackend {
private db?: any;
private sqliteLoaded: boolean = false;
constructor(
private dbPath: string,
private logger: ILogger,
) {}
async initialize(): Promise<void> {
this.logger.info('Initializing SQLite backend', { dbPath: this.dbPath });
try {
// Load SQLite wrapper if not loaded
if (!this.sqliteLoaded) {
const module = await import('../sqlite-wrapper.js');
createDatabase = module.createDatabase;
isSQLiteAvailable = module.isSQLiteAvailable;
this.sqliteLoaded = true;
}
// Check if SQLite is available
const sqliteAvailable = await isSQLiteAvailable();
if (!sqliteAvailable) {
throw new Error('SQLite module not available');
}
// Ensure directory exists
const dir = path.dirname(this.dbPath);
await fs.mkdir(dir, { recursive: true });
// Open SQLite connection
this.db = await createDatabase(this.dbPath);
// Enable WAL mode for better performance
this.db.pragma('journal_mode = WAL');
this.db.pragma('synchronous = NORMAL');
this.db.pragma('cache_size = 1000');
this.db.pragma('temp_store = memory');
// Create tables
this.createTables();
// Create indexes
this.createIndexes();
this.logger.info('SQLite backend initialized');
} catch (error) {
throw new MemoryBackendError('Failed to initialize SQLite backend', { error });
}
}
async shutdown(): Promise<void> {
this.logger.info('Shutting down SQLite backend');
if (this.db) {
this.db.close();
delete this.db;
}
}
async store(entry: MemoryEntry): Promise<void> {
if (!this.db) {
throw new MemoryBackendError('Database not initialized');
}
const sql = `
INSERT OR REPLACE INTO memory_entries (
id, agent_id, session_id, type, content,
context, timestamp, tags, version, parent_id, metadata
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`;
const params = [
entry.id,
entry.agentId,
entry.sessionId,
entry.type,
entry.content,
JSON.stringify(entry.context),
entry.timestamp.toISOString(),
JSON.stringify(entry.tags),
entry.version,
entry.parentId || null,
entry.metadata ? JSON.stringify(entry.metadata) : null,
];
try {
const stmt = this.db.prepare(sql);
stmt.run(...params);
} catch (error) {
throw new MemoryBackendError('Failed to store entry', { error });
}
}
async retrieve(id: string): Promise<MemoryEntry | undefined> {
if (!this.db) {
throw new MemoryBackendError('Database not initialized');
}
const sql = 'SELECT * FROM memory_entries WHERE id = ?';
try {
const stmt = this.db.prepare(sql);
const row = stmt.get(id);
if (!row) {
return undefined;
}
return this.rowToEntry(row as Record<string, unknown>);
} catch (error) {
throw new MemoryBackendError('Failed to retrieve entry', { error });
}
}
async update(id: string, entry: MemoryEntry): Promise<void> {
// SQLite INSERT OR REPLACE handles updates
await this.store(entry);
}
async delete(id: string): Promise<void> {
if (!this.db) {
throw new MemoryBackendError('Database not initialized');
}
const sql = 'DELETE FROM memory_entries WHERE id = ?';
try {
const stmt = this.db.prepare(sql);
stmt.run(id);
} catch (error) {
throw new MemoryBackendError('Failed to delete entry', { error });
}
}
async query(query: MemoryQuery): Promise<MemoryEntry[]> {
if (!this.db) {
throw new MemoryBackendError('Database not initialized');
}
const conditions: string[] = [];
const params: unknown[] = [];
if (query.agentId) {
conditions.push('agent_id = ?');
params.push(query.agentId);
}
if (query.sessionId) {
conditions.push('session_id = ?');
params.push(query.sessionId);
}
if (query.type) {
conditions.push('type = ?');
params.push(query.type);
}
if (query.startTime) {
conditions.push('timestamp >= ?');
params.push(query.startTime.toISOString());
}
if (query.endTime) {
conditions.push('timestamp <= ?');
params.push(query.endTime.toISOString());
}
if (query.search) {
conditions.push('(content LIKE ? OR tags LIKE ?)');
params.push(`%${query.search}%`, `%${query.search}%`);
}
if (query.tags && query.tags.length > 0) {
const tagConditions = query.tags.map(() => 'tags LIKE ?');
conditions.push(`(${tagConditions.join(' OR ')})`);
query.tags.forEach((tag: string) => params.push(`%"${tag}"%`));
}
let sql = 'SELECT * FROM memory_entries';
if (conditions.length > 0) {
sql += ' WHERE ' + conditions.join(' AND ');
}
sql += ' ORDER BY timestamp DESC';
if (query.limit) {
sql += ' LIMIT ?';
params.push(query.limit);
}
if (query.offset) {
// SQLite requires LIMIT when using OFFSET
if (!query.limit) {
sql += ' LIMIT -1'; // -1 means no limit in SQLite
}
sql += ' OFFSET ?';
params.push(query.offset);
}
try {
const stmt = this.db.prepare(sql);
const rows = stmt.all(...params);
return rows.map((row: any) => this.rowToEntry(row as Record<string, unknown>));
} catch (error) {
throw new MemoryBackendError('Failed to query entries', { error });
}
}
async getAllEntries(): Promise<MemoryEntry[]> {
if (!this.db) {
throw new MemoryBackendError('Database not initialized');
}
const sql = 'SELECT * FROM memory_entries ORDER BY timestamp DESC';
try {
const stmt = this.db.prepare(sql);
const rows = stmt.all();
return rows.map((row: any) => this.rowToEntry(row as Record<string, unknown>));
} catch (error) {
throw new MemoryBackendError('Failed to get all entries', { error });
}
}
async getHealthStatus(): Promise<{
healthy: boolean;
error?: string;
metrics?: Record<string, number>;
}> {
if (!this.db) {
return {
healthy: false,
error: 'Database not initialized',
};
}
try {
// Check database connectivity
this.db.prepare('SELECT 1').get();
// Get metrics
const countResult = this.db
.prepare('SELECT COUNT(*) as count FROM memory_entries')
.get() as any;
const entryCount = countResult.count;
const sizeResult = this.db
.prepare(
'SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()',
)
.get() as any;
const dbSize = sizeResult.size;
return {
healthy: true,
metrics: {
entryCount,
dbSizeBytes: dbSize,
},
};
} catch (error) {
return {
healthy: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
private createTables(): void {
const sql = `
CREATE TABLE IF NOT EXISTS memory_entries (
id TEXT PRIMARY KEY,
agent_id TEXT NOT NULL,
session_id TEXT NOT NULL,
type TEXT NOT NULL,
content TEXT NOT NULL,
context TEXT NOT NULL,
timestamp TEXT NOT NULL,
tags TEXT NOT NULL,
version INTEGER NOT NULL,
parent_id TEXT,
metadata TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`;
this.db!.exec(sql);
}
private createIndexes(): void {
const indexes = [
'CREATE INDEX IF NOT EXISTS idx_agent_id ON memory_entries(agent_id)',
'CREATE INDEX IF NOT EXISTS idx_session_id ON memory_entries(session_id)',
'CREATE INDEX IF NOT EXISTS idx_type ON memory_entries(type)',
'CREATE INDEX IF NOT EXISTS idx_timestamp ON memory_entries(timestamp)',
'CREATE INDEX IF NOT EXISTS idx_parent_id ON memory_entries(parent_id)',
];
for (const sql of indexes) {
this.db!.exec(sql);
}
}
private rowToEntry(row: Record<string, unknown>): MemoryEntry {
const entry: MemoryEntry = {
id: row.id as string,
agentId: row.agent_id as string,
sessionId: row.session_id as string,
type: row.type as MemoryEntry['type'],
content: row.content as string,
context: JSON.parse(row.context as string),
timestamp: new Date(row.timestamp as string),
tags: JSON.parse(row.tags as string),
version: row.version as number,
};
if (row.parent_id) {
entry.parentId = row.parent_id as string;
}
if (row.metadata) {
entry.metadata = JSON.parse(row.metadata as string);
}
return entry;
}
}