const Database = require('better-sqlite3'); const path = require('path'); const logger = require('../utils/logger'); const { withDatabaseRetry, CircuitBreaker } = require('../utils/retry'); const { DatabaseError } = require('../middleware/errorHandler'); class DatabaseManager { constructor() { this.db = null; this.dbPath = path.join(__dirname, '..', 'inventory.db'); this.isInitialized = false; this.circuitBreaker = new CircuitBreaker({ failureThreshold: 5, resetTimeout: 30000 }); } /** * Initialize database connection and create tables */ async initialize() { if (this.isInitialized) { logger.debug('Database already initialized'); return; } try { await withDatabaseRetry(async () => { logger.info('Initializing database connection', { dbPath: this.dbPath }); this.db = new Database(this.dbPath); // Configure database settings this.db.pragma('journal_mode = WAL'); this.db.pragma('synchronous = NORMAL'); this.db.pragma('cache_size = 1000'); this.db.pragma('temp_store = memory'); this.db.pragma('mmap_size = 268435456'); // 256MB // Set busy timeout this.db.pragma('busy_timeout = 5000'); await this.createTables(); this.isInitialized = true; logger.info('Database initialized successfully', { dbPath: this.dbPath, journalMode: this.db.pragma('journal_mode', { simple: true }), cacheSize: this.db.pragma('cache_size', { simple: true }) }); }); } catch (error) { logger.error('Database initialization failed', { error: error.message, dbPath: this.dbPath, stack: error.stack }); throw new DatabaseError('Failed to initialize database', { originalError: error.message, dbPath: this.dbPath }); } } /** * Create all required tables */ async createTables() { // Products table as per design document const createProductsTable = ` CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_code VARCHAR(50) UNIQUE NOT NULL, description TEXT, category VARCHAR(100), unit_of_measure VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `; // Inventory table as per design document const createInventoryTable = ` CREATE TABLE IF NOT EXISTS inventory ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, current_level INTEGER NOT NULL DEFAULT 0, minimum_level INTEGER DEFAULT 0, maximum_level INTEGER, last_updated DATETIME DEFAULT CURRENT_TIMESTAMP, updated_by VARCHAR(100), version INTEGER DEFAULT 1, FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE ) `; // Inventory history table as per design document const createInventoryHistoryTable = ` CREATE TABLE IF NOT EXISTS inventory_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, old_level INTEGER, new_level INTEGER NOT NULL, change_reason VARCHAR(200), updated_by VARCHAR(100), updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE ) `; // Import sessions table as per design document const createImportSessionsTable = ` CREATE TABLE IF NOT EXISTS import_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename VARCHAR(255), total_records INTEGER, successful_imports INTEGER, failed_imports INTEGER, import_date DATETIME DEFAULT CURRENT_TIMESTAMP, status VARCHAR(20) DEFAULT 'completed' ) `; // Legacy tables for backward compatibility (will be migrated) const createItemsTable = ` CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, category TEXT, quantity INTEGER NOT NULL DEFAULT 0, unit TEXT, barcode TEXT UNIQUE, qr_code TEXT, location TEXT, min_stock_level INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `; const createTransactionsTable = ` CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id INTEGER NOT NULL, type TEXT NOT NULL CHECK (type IN ('in', 'out', 'adjustment')), quantity INTEGER NOT NULL, reason TEXT, user_name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (item_id) REFERENCES items (id) ON DELETE CASCADE ) `; const createIndexes = [ // New schema indexes - optimized for performance 'CREATE UNIQUE INDEX IF NOT EXISTS idx_products_product_code ON products(product_code)', 'CREATE INDEX IF NOT EXISTS idx_products_category ON products(category)', 'CREATE INDEX IF NOT EXISTS idx_products_description ON products(description)', 'CREATE INDEX IF NOT EXISTS idx_products_created_at ON products(created_at)', // Inventory indexes - optimized for common queries 'CREATE UNIQUE INDEX IF NOT EXISTS idx_inventory_product_id ON inventory(product_id)', 'CREATE INDEX IF NOT EXISTS idx_inventory_current_level ON inventory(current_level)', 'CREATE INDEX IF NOT EXISTS idx_inventory_low_stock ON inventory(current_level, minimum_level) WHERE current_level <= minimum_level', 'CREATE INDEX IF NOT EXISTS idx_inventory_last_updated ON inventory(last_updated)', 'CREATE INDEX IF NOT EXISTS idx_inventory_updated_by ON inventory(updated_by)', 'CREATE INDEX IF NOT EXISTS idx_inventory_version ON inventory(version)', // For optimistic locking // Inventory history indexes - optimized for audit queries 'CREATE INDEX IF NOT EXISTS idx_inventory_history_product_id ON inventory_history(product_id)', 'CREATE INDEX IF NOT EXISTS idx_inventory_history_updated_at ON inventory_history(updated_at)', 'CREATE INDEX IF NOT EXISTS idx_inventory_history_updated_by ON inventory_history(updated_by)', 'CREATE INDEX IF NOT EXISTS idx_inventory_history_product_date ON inventory_history(product_id, updated_at)', // Import sessions indexes 'CREATE INDEX IF NOT EXISTS idx_import_sessions_date ON import_sessions(import_date)', 'CREATE INDEX IF NOT EXISTS idx_import_sessions_status ON import_sessions(status)', // Composite indexes for complex queries 'CREATE INDEX IF NOT EXISTS idx_products_category_code ON products(category, product_code)', 'CREATE INDEX IF NOT EXISTS idx_inventory_product_level ON inventory(product_id, current_level)', 'CREATE INDEX IF NOT EXISTS idx_inventory_levels_range ON inventory(current_level, minimum_level, maximum_level)', // Legacy schema indexes 'CREATE INDEX IF NOT EXISTS idx_items_barcode ON items(barcode)', 'CREATE INDEX IF NOT EXISTS idx_items_name ON items(name)', 'CREATE INDEX IF NOT EXISTS idx_items_category ON items(category)', 'CREATE INDEX IF NOT EXISTS idx_items_quantity ON items(quantity)', 'CREATE INDEX IF NOT EXISTS idx_transactions_item_id ON transactions(item_id)', 'CREATE INDEX IF NOT EXISTS idx_transactions_created_at ON transactions(created_at)', 'CREATE INDEX IF NOT EXISTS idx_transactions_type ON transactions(type)', 'CREATE INDEX IF NOT EXISTS idx_transactions_item_date ON transactions(item_id, created_at)' ]; try { logger.info('Creating database tables and indexes'); const startTime = Date.now(); // Create new schema tables this.db.exec(createProductsTable); this.db.exec(createInventoryTable); this.db.exec(createInventoryHistoryTable); this.db.exec(createImportSessionsTable); // Create legacy tables for backward compatibility this.db.exec(createItemsTable); this.db.exec(createTransactionsTable); // Create indexes createIndexes.forEach((indexQuery, index) => { try { this.db.exec(indexQuery); logger.debug('Index created', { index: index + 1, total: createIndexes.length }); } catch (indexError) { logger.warn('Index creation failed', { query: indexQuery, error: indexError.message }); } }); const duration = Date.now() - startTime; logger.info('Database tables created successfully', { duration: `${duration}ms` }); } catch (error) { logger.error('Error creating database tables', { error: error.message, stack: error.stack }); throw new DatabaseError('Failed to create database tables', { originalError: error.message }); } } /** * Get database instance with circuit breaker protection */ getDatabase() { if (!this.db || !this.isInitialized) { throw new DatabaseError('Database not initialized. Call initialize() first.'); } return this.db; } /** * Execute database operation with circuit breaker and retry logic */ async executeWithProtection(operation, operationName = 'database_operation') { return await this.circuitBreaker.execute(async () => { return await withDatabaseRetry(async () => { const startTime = Date.now(); try { const result = await operation(); const duration = Date.now() - startTime; logger.logDbOperation(operationName, 'unknown', {}, duration); return result; } catch (error) { const duration = Date.now() - startTime; logger.error('Database operation failed', { operation: operationName, duration: `${duration}ms`, error: error.message, code: error.code }); throw error; } }); }); } /** * Close database connection */ close() { if (this.db) { try { logger.info('Closing database connection'); this.db.close(); this.db = null; this.isInitialized = false; logger.info('Database connection closed successfully'); } catch (error) { logger.error('Error closing database connection', { error: error.message }); } } } /** * Execute a transaction with rollback support and error handling */ async executeTransaction(callback, transactionName = 'transaction') { return await this.executeWithProtection(async () => { const startTime = Date.now(); try { logger.debug('Starting database transaction', { name: transactionName }); const transaction = this.db.transaction(callback); const result = transaction(); const duration = Date.now() - startTime; logger.debug('Database transaction completed', { name: transactionName, duration: `${duration}ms` }); return result; } catch (error) { const duration = Date.now() - startTime; logger.error('Database transaction failed', { name: transactionName, duration: `${duration}ms`, error: error.message }); throw new DatabaseError(`Transaction failed: ${transactionName}`, { originalError: error.message, transactionName }); } }, `transaction_${transactionName}`); } /** * Health check for database connection */ async healthCheck() { try { const result = this.db.prepare('SELECT 1 as health').get(); return { status: 'healthy', connected: true, result: result.health === 1 }; } catch (error) { logger.error('Database health check failed', { error: error.message }); return { status: 'unhealthy', connected: false, error: error.message }; } } /** * Get database statistics */ getStats() { try { const stats = { isInitialized: this.isInitialized, dbPath: this.dbPath, circuitBreakerState: this.circuitBreaker.getState() }; if (this.db) { stats.pragmas = { journalMode: this.db.pragma('journal_mode', { simple: true }), synchronous: this.db.pragma('synchronous', { simple: true }), cacheSize: this.db.pragma('cache_size', { simple: true }), busyTimeout: this.db.pragma('busy_timeout', { simple: true }) }; // Get table statistics stats.tables = this.getTableStats(); // Get index usage statistics stats.indexes = this.getIndexStats(); } return stats; } catch (error) { logger.error('Error getting database stats', { error: error.message }); return { isInitialized: this.isInitialized, error: error.message }; } } /** * Get table statistics for performance monitoring */ getTableStats() { try { const tables = ['products', 'inventory', 'inventory_history', 'import_sessions']; const stats = {}; tables.forEach(table => { try { const countResult = this.db.prepare(`SELECT COUNT(*) as count FROM ${table}`).get(); stats[table] = { rowCount: countResult.count }; } catch (error) { stats[table] = { error: error.message }; } }); return stats; } catch (error) { logger.error('Error getting table stats', { error: error.message }); return {}; } } /** * Get index statistics for query optimization */ getIndexStats() { try { const indexes = this.db.prepare(` SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'index' AND name NOT LIKE 'sqlite_%' ORDER BY tbl_name, name `).all(); return indexes.map(index => ({ name: index.name, table: index.tbl_name, definition: index.sql })); } catch (error) { logger.error('Error getting index stats', { error: error.message }); return []; } } /** * Analyze database performance and suggest optimizations */ analyzePerformance() { try { const analysis = { timestamp: new Date().toISOString(), recommendations: [] }; // Check for missing indexes on frequently queried columns const tableStats = this.getTableStats(); // Analyze query patterns (this would be enhanced with actual query logging) if (tableStats.products && tableStats.products.rowCount > 1000) { analysis.recommendations.push({ type: 'INDEX_OPTIMIZATION', message: 'Consider adding composite indexes for frequently filtered product queries', priority: 'MEDIUM' }); } if (tableStats.inventory_history && tableStats.inventory_history.rowCount > 10000) { analysis.recommendations.push({ type: 'ARCHIVAL', message: 'Consider archiving old inventory history records to improve query performance', priority: 'LOW' }); } // Check cache hit ratio (simplified) const cacheSize = this.db.pragma('cache_size', { simple: true }); if (cacheSize < 2000) { analysis.recommendations.push({ type: 'CACHE_OPTIMIZATION', message: 'Consider increasing cache size for better performance with large datasets', priority: 'HIGH' }); } return analysis; } catch (error) { logger.error('Error analyzing database performance', { error: error.message }); return { timestamp: new Date().toISOString(), error: error.message, recommendations: [] }; } } /** * Optimize database for better performance */ async optimize() { return await this.executeWithProtection(async () => { logger.info('Starting database optimization'); const startTime = Date.now(); const results = { vacuum: false, analyze: false, reindex: false, pragmaUpdates: [] }; try { // Run VACUUM to reclaim space and defragment this.db.exec('VACUUM'); results.vacuum = true; logger.debug('Database VACUUM completed'); // Run ANALYZE to update query planner statistics this.db.exec('ANALYZE'); results.analyze = true; logger.debug('Database ANALYZE completed'); // Reindex all indexes this.db.exec('REINDEX'); results.reindex = true; logger.debug('Database REINDEX completed'); // Optimize pragma settings for performance const optimizations = [ { pragma: 'optimize', value: null }, // SQLite auto-optimization { pragma: 'cache_size', value: 2000 }, { pragma: 'temp_store', value: 'memory' }, { pragma: 'mmap_size', value: 268435456 } // 256MB ]; optimizations.forEach(opt => { try { if (opt.value !== null) { this.db.pragma(`${opt.pragma} = ${opt.value}`); } else { this.db.pragma(opt.pragma); } results.pragmaUpdates.push(opt.pragma); } catch (error) { logger.warn(`Failed to update pragma ${opt.pragma}`, { error: error.message }); } }); const duration = Date.now() - startTime; logger.info('Database optimization completed', { duration: `${duration}ms`, results }); return { success: true, duration, results }; } catch (error) { const duration = Date.now() - startTime; logger.error('Database optimization failed', { duration: `${duration}ms`, error: error.message, partialResults: results }); throw new DatabaseError('Database optimization failed', { originalError: error.message, partialResults: results }); } }, 'database_optimization'); } /** * Prepare optimized statements for common queries */ prepareOptimizedStatements() { if (!this.db) { throw new DatabaseError('Database not initialized'); } try { // Cache frequently used prepared statements this.preparedStatements = { // Product queries findProductByCode: this.db.prepare(` SELECT p.*, i.current_level, i.minimum_level, i.maximum_level FROM products p LEFT JOIN inventory i ON p.id = i.product_id WHERE p.product_code = ? `), findProductsByCategory: this.db.prepare(` SELECT p.*, i.current_level, i.minimum_level, i.maximum_level FROM products p LEFT JOIN inventory i ON p.id = i.product_id WHERE p.category = ? ORDER BY p.product_code LIMIT ? OFFSET ? `), // Inventory queries getInventorySummary: this.db.prepare(` SELECT p.id, p.product_code, p.description, p.category, i.current_level, i.minimum_level, i.maximum_level, i.last_updated, i.updated_by, CASE WHEN i.current_level <= i.minimum_level THEN 'low' WHEN i.current_level >= i.maximum_level THEN 'high' ELSE 'normal' END as stock_status FROM products p INNER JOIN inventory i ON p.id = i.product_id ORDER BY p.product_code LIMIT ? OFFSET ? `), getLowStockItems: this.db.prepare(` SELECT p.id, p.product_code, p.description, p.category, i.current_level, i.minimum_level, i.last_updated FROM products p INNER JOIN inventory i ON p.id = i.product_id WHERE i.current_level <= i.minimum_level ORDER BY (i.current_level - i.minimum_level), p.product_code `), // History queries getInventoryHistory: this.db.prepare(` SELECT ih.*, p.product_code, p.description FROM inventory_history ih INNER JOIN products p ON ih.product_id = p.id WHERE ih.product_id = ? ORDER BY ih.updated_at DESC LIMIT ? OFFSET ? `), // Update queries with optimistic locking updateInventoryLevel: this.db.prepare(` UPDATE inventory SET current_level = ?, last_updated = datetime('now'), updated_by = ?, version = version + 1 WHERE product_id = ? AND version = ? `), insertInventoryHistory: this.db.prepare(` INSERT INTO inventory_history (product_id, old_level, new_level, change_reason, updated_by) VALUES (?, ?, ?, ?, ?) `) }; logger.info('Optimized prepared statements created'); return this.preparedStatements; } catch (error) { logger.error('Failed to prepare optimized statements', { error: error.message }); throw new DatabaseError('Failed to prepare optimized statements', { originalError: error.message }); } } /** * Get prepared statement by name */ getPreparedStatement(name) { if (!this.preparedStatements) { this.prepareOptimizedStatements(); } const statement = this.preparedStatements[name]; if (!statement) { throw new DatabaseError(`Prepared statement '${name}' not found`); } return statement; } } module.exports = new DatabaseManager();