UNPKG

claude-flow-novice

Version:

Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.

544 lines (543 loc) 22.7 kB
/** * SQLite Database Adapter * * Implements IDatabaseAdapter for SQLite with prepared statements and connection pooling. * Part of Task 0.4: Database Query Abstraction Layer (MVP) * * UPDATED: Now uses ConnectionPoolManager for proper connection pool initialization, * health checks, automatic reconnection, and connection metrics. */ import { randomUUID } from 'crypto'; import { DatabaseErrorCode, createDatabaseError, createSuccessResult, createFailedResult, mapSQLiteError } from './errors.js'; import { ConnectionPoolManager } from './connection-pool-manager.js'; import { withDatabaseRetry } from '../retry-manager.js'; import { v4 as uuidv4 } from 'uuid'; export class SQLiteAdapter { poolManager = null; config; connected = false; transactions = new Map(); errorAggregator; correlationId; constructor(config, errorAggregator){ this.config = config; this.errorAggregator = errorAggregator; this.correlationId = uuidv4(); } getType() { return 'sqlite'; } /** * Track error with error aggregator * @private */ trackError(error, operation, context) { if (this.errorAggregator) { const dbError = error.code ? error : createDatabaseError(DatabaseErrorCode.QUERY_FAILED, `SQLite ${operation} failed`, error instanceof Error ? error : new Error(String(error)), context); this.errorAggregator.addError('sqlite', dbError, { ...context, operation, correlationId: this.correlationId }); } } /** * Record successful operation with error aggregator * @private */ recordSuccess() { if (this.errorAggregator) { this.errorAggregator.recordSuccess('sqlite'); } } async connect() { // Wrap connection with retry logic for transient failures await withDatabaseRetry(async ()=>{ try { // Initialize connection pool manager this.poolManager = new ConnectionPoolManager(this.config); await this.poolManager.initialize(); // Start health checks (ping every 30s) this.poolManager.startHealthChecks(); this.connected = true; this.recordSuccess(); } catch (err) { const error = createDatabaseError(DatabaseErrorCode.CONNECTION_FAILED, 'Failed to connect to SQLite', err instanceof Error ? err : new Error(String(err)), { config: this.config, correlationId: this.correlationId }); this.trackError(error, 'connect'); throw error; } }); } async disconnect() { if (this.poolManager) { await this.poolManager.shutdown(); this.poolManager = null; this.connected = false; } } isConnected() { return this.connected && this.poolManager !== null; } /** * Get connection pool statistics */ getPoolStats() { return this.poolManager?.getStats(); } async get(key) { this.ensureConnected(); // Wrap with retry logic for transient database failures return withDatabaseRetry(async ()=>{ const connection = await this.poolManager.acquire(); try { // Parse correlation key format: table:id or table:id:entity:subtype // For SQL adapters, we use only table:id for lookup const parts = key.split(':'); const table = parts[0]; const id = parts.slice(1).join(':'); // Rejoin remaining parts as ID if (!table || !id) { throw new Error('Invalid key format. Expected "table:id" or "table:id:entity:subtype"'); } const query = `SELECT * FROM ${this.sanitizeIdentifier(table)} WHERE id = ?`; const result = await connection.get(query, [ id ]); this.recordSuccess(); return result || null; } catch (err) { const errorCode = mapSQLiteError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to get record: ${key}`, err instanceof Error ? err : new Error(String(err)), { key, correlationId: this.correlationId }); this.trackError(error, 'get', { key }); throw error; } finally{ await this.poolManager.release(connection); } }); } async list(table, options) { this.ensureConnected(); // Wrap with retry logic for transient database failures return withDatabaseRetry(async ()=>{ const connection = await this.poolManager.acquire(); try { let query = `SELECT * FROM ${this.sanitizeIdentifier(table)}`; const params = []; // Apply filters if (options?.filters && options.filters.length > 0) { const whereClauses = options.filters.map((filter)=>{ return this.buildWhereClause(filter, params); }); query += ` WHERE ${whereClauses.join(' AND ')}`; } // Apply ordering if (options?.orderBy) { const order = options.order || 'asc'; query += ` ORDER BY ${this.sanitizeIdentifier(String(options.orderBy))} ${order.toUpperCase()}`; } // Apply limit and offset if (options?.limit) { query += ` LIMIT ?`; params.push(options.limit); } if (options?.offset) { query += ` OFFSET ?`; params.push(options.offset); } const results = await connection.all(query, params); this.recordSuccess(); return results; } catch (err) { const errorCode = mapSQLiteError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to list records from table: ${table}`, err instanceof Error ? err : new Error(String(err)), { table, options, correlationId: this.correlationId }); this.trackError(error, 'list', { table }); throw error; } finally{ await this.poolManager.release(connection); } }); } async query(table, filters) { return this.list(table, { filters }); } async insert(table, data) { this.ensureConnected(); // Wrap with retry logic for transient database failures return withDatabaseRetry(async ()=>{ const connection = await this.poolManager.acquire(); try { const keys = Object.keys(data); const values = Object.values(data); const placeholders = keys.map(()=>'?').join(', '); const columns = keys.map((k)=>this.sanitizeIdentifier(k)).join(', '); const query = `INSERT INTO ${this.sanitizeIdentifier(table)} (${columns}) VALUES (${placeholders})`; const result = await connection.run(query, values); this.recordSuccess(); return createSuccessResult(data, result.changes, result.lastID); } catch (err) { const errorCode = mapSQLiteError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to insert record into table: ${table}`, err instanceof Error ? err : new Error(String(err)), { table, data, correlationId: this.correlationId }); this.trackError(error, 'insert', { table }); return createFailedResult(error); } finally{ await this.poolManager.release(connection); } }); } async insertMany(table, data) { this.ensureConnected(); const connection = await this.poolManager.acquire(); // Check if we're already in an active transaction (SQLite doesn't support nested transactions) const hasActiveTransaction = this.transactions.size > 0; try { // Only begin transaction if not already in one if (!hasActiveTransaction) { await connection.run('BEGIN TRANSACTION'); } let totalChanges = 0; for (const item of data){ const keys = Object.keys(item); const values = Object.values(item); const placeholders = keys.map(()=>'?').join(', '); const columns = keys.map((k)=>this.sanitizeIdentifier(k)).join(', '); const query = `INSERT INTO ${this.sanitizeIdentifier(table)} (${columns}) VALUES (${placeholders})`; const result = await connection.run(query, values); totalChanges += result.changes || 0; } // Only commit if we started the transaction if (!hasActiveTransaction) { await connection.run('COMMIT'); } this.recordSuccess(); return createSuccessResult(data, totalChanges); } catch (err) { // Only rollback if we started the transaction if (!hasActiveTransaction) { await connection.run('ROLLBACK'); } const errorCode = mapSQLiteError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to insert multiple records into table: ${table}`, err instanceof Error ? err : new Error(String(err)), { table, count: data.length, correlationId: this.correlationId }); this.trackError(error, 'insertMany', { table, count: data.length }); return createFailedResult(error); } finally{ await this.poolManager.release(connection); } } async update(table, key, data) { this.ensureConnected(); const connection = await this.poolManager.acquire(); try { const keys = Object.keys(data); const values = Object.values(data); const setClauses = keys.map((k)=>`${this.sanitizeIdentifier(k)} = ?`).join(', '); const query = `UPDATE ${this.sanitizeIdentifier(table)} SET ${setClauses} WHERE id = ?`; const result = await connection.run(query, [ ...values, key ]); if (result.changes === 0) { const error = createDatabaseError(DatabaseErrorCode.NOT_FOUND, `Record not found in table: ${table}`, undefined, { table, key, correlationId: this.correlationId }); this.trackError(error, 'update', { table, key }); return createFailedResult(error); } // Get updated record const updated = await this.get(`${table}:${key}`); this.recordSuccess(); return createSuccessResult(updated, result.changes); } catch (err) { const errorCode = mapSQLiteError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to update record in table: ${table}`, err instanceof Error ? err : new Error(String(err)), { table, key, data, correlationId: this.correlationId }); this.trackError(error, 'update', { table, key }); return createFailedResult(error); } finally{ await this.poolManager.release(connection); } } async delete(table, key) { this.ensureConnected(); const connection = await this.poolManager.acquire(); try { const query = `DELETE FROM ${this.sanitizeIdentifier(table)} WHERE id = ?`; const result = await connection.run(query, [ key ]); if (result.changes === 0) { const error = createDatabaseError(DatabaseErrorCode.NOT_FOUND, `Record not found in table: ${table}`, undefined, { table, key, correlationId: this.correlationId }); this.trackError(error, 'delete', { table, key }); return createFailedResult(error); } this.recordSuccess(); return createSuccessResult(undefined, result.changes); } catch (err) { const errorCode = mapSQLiteError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to delete record from table: ${table}`, err instanceof Error ? err : new Error(String(err)), { table, key, correlationId: this.correlationId }); this.trackError(error, 'delete', { table, key }); return createFailedResult(error); } finally{ await this.poolManager.release(connection); } } /** * Detect query type with comprehensive pattern matching * Handles: SELECT, WITH/CTE, EXPLAIN, PRAGMA, comments */ detectQueryType(query) { // Remove multi-line comments first (/* */) let normalized = query.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove single-line comments (--) line by line normalized = normalized.split('\n').map((line)=>line.replace(/--.*$/, '')).join('\n').trim(); // Read operations: SELECT, WITH (CTEs), EXPLAIN, PRAGMA, SHOW const readPatterns = /^(SELECT|WITH|EXPLAIN|PRAGMA|SHOW)/i; return readPatterns.test(normalized) ? 'read' : 'write'; } async raw(query, params) { this.ensureConnected(); const connection = await this.poolManager.acquire(); try { // Determine if query is SELECT or modification using comprehensive detection const isSelect = this.detectQueryType(query) === 'read'; if (isSelect) { const results = await connection.all(query, params); this.recordSuccess(); return results; } else { const result = await connection.run(query, params); this.recordSuccess(); return result; } } catch (err) { const errorCode = mapSQLiteError(err instanceof Error ? err : new Error(String(err))); const error = createDatabaseError(errorCode, `Failed to execute raw query`, err instanceof Error ? err : new Error(String(err)), { query, params, correlationId: this.correlationId }); this.trackError(error, 'raw', { query }); throw error; } finally{ await this.poolManager.release(connection); } } async beginTransaction() { this.ensureConnected(); const connection = await this.poolManager.acquire(); const context = { id: `sqlite-tx-${randomUUID()}`, databases: [ 'sqlite' ], startTime: new Date(), status: 'pending' }; await connection.run('BEGIN TRANSACTION'); this.transactions.set(context.id, { ...context, connection }); return context; } async prepareTransaction(context) { this.ensureConnected(); if (!this.transactions.has(context.id)) { throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Transaction not found', undefined, { transactionId: context.id }); } try { // SQLite doesn't support native two-phase commit // We simulate PREPARE by validating the transaction can commit // This involves checking for constraint violations and lock conflicts const txData = this.transactions.get(context.id); const connection = txData.connection; // Check if database is locked (would prevent commit) await connection.get('PRAGMA lock_status'); // Validate foreign key constraints const violations = await connection.all('PRAGMA foreign_key_check'); if (violations && violations.length > 0) { throw new Error(`Foreign key constraint violations: ${JSON.stringify(violations)}`); } // If we get here, transaction can be committed context.status = 'prepared'; context.preparedAt = new Date(); // Update transaction in map this.transactions.set(context.id, { ...context, connection }); return true; } catch (err) { // Prepare failed - transaction can still be rolled back throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Failed to prepare transaction', err instanceof Error ? err : new Error(String(err)), { transactionId: context.id }); } } async commitTransaction(context) { this.ensureConnected(); if (!this.transactions.has(context.id)) { throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Transaction not found', undefined, { transactionId: context.id }); } const txData = this.transactions.get(context.id); const connection = txData.connection; try { await connection.run('COMMIT'); context.status = 'committed'; } finally{ // Always cleanup transaction from map and release connection this.transactions.delete(context.id); await this.poolManager.release(connection); } } async rollbackTransaction(context) { this.ensureConnected(); if (!this.transactions.has(context.id)) { throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Transaction not found', undefined, { transactionId: context.id }); } const txData = this.transactions.get(context.id); const connection = txData.connection; try { await connection.run('ROLLBACK'); context.status = 'rolled_back'; } finally{ // Always cleanup transaction from map and release connection this.transactions.delete(context.id); await this.poolManager.release(connection); } } ensureConnected() { if (!this.isConnected()) { throw createDatabaseError(DatabaseErrorCode.CONNECTION_FAILED, 'Not connected to SQLite', undefined, { config: this.config }); } } sanitizeIdentifier(identifier) { // Remove any characters that aren't alphanumeric or underscore return identifier.replace(/[^a-zA-Z0-9_]/g, ''); } buildWhereClause(filter, params) { const field = this.sanitizeIdentifier(String(filter.field)); switch(filter.operator){ case 'eq': { params.push(filter.value); return `${field} = ?`; } case 'ne': { params.push(filter.value); return `${field} != ?`; } case 'gt': { params.push(filter.value); return `${field} > ?`; } case 'gte': { params.push(filter.value); return `${field} >= ?`; } case 'lt': { params.push(filter.value); return `${field} < ?`; } case 'lte': { params.push(filter.value); return `${field} <= ?`; } case 'in': { if (!Array.isArray(filter.value)) { throw new TypeError(`Field '${String(filter.field)}' with operator 'in' requires an array value`); } if (filter.value.length === 0) { // Empty IN list - return false condition without invalid SQL return '1=0'; } const placeholders = filter.value.map(()=>'?').join(', '); params.push(...filter.value); return `${field} IN (${placeholders})`; } case 'like': { params.push(`%${filter.value}%`); return `${field} LIKE ?`; } case 'between': { if (!Array.isArray(filter.value)) { throw new TypeError(`Field '${String(filter.field)}' with operator 'between' requires an array value`); } if (filter.value.length !== 2) { throw new TypeError(`Field '${String(filter.field)}' with operator 'between' requires exactly 2 elements, got ${filter.value.length}`); } params.push(filter.value[0], filter.value[1]); return `${field} BETWEEN ? AND ?`; } default: return '1=1'; } } } //# sourceMappingURL=sqlite-adapter.js.map