Initial commit: Inventory Barcode System
This commit is contained in:
696
models/database.js
Normal file
696
models/database.js
Normal file
@ -0,0 +1,696 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user