Initial commit: Inventory Barcode System
This commit is contained in:
468
__tests__/database-optimization.test.js
Normal file
468
__tests__/database-optimization.test.js
Normal file
@ -0,0 +1,468 @@
|
||||
const database = require('../models/database');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
describe('Database Optimization and Performance Tests', () => {
|
||||
let testDbPath;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Set up test database
|
||||
testDbPath = path.join(__dirname, '..', 'test_optimization.db');
|
||||
if (fs.existsSync(testDbPath)) {
|
||||
fs.unlinkSync(testDbPath);
|
||||
}
|
||||
database.dbPath = testDbPath;
|
||||
await database.initialize();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
database.close();
|
||||
if (fs.existsSync(testDbPath)) {
|
||||
fs.unlinkSync(testDbPath);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clean up database before each test
|
||||
const db = database.getDatabase();
|
||||
db.exec('DELETE FROM inventory_history');
|
||||
db.exec('DELETE FROM inventory');
|
||||
db.exec('DELETE FROM products');
|
||||
db.exec('DELETE FROM import_sessions');
|
||||
});
|
||||
|
||||
describe('Database Indexing and Query Optimization', () => {
|
||||
test('should have proper indexes created', async () => {
|
||||
const db = database.getDatabase();
|
||||
|
||||
// Check that indexes exist
|
||||
const indexes = db.prepare(`
|
||||
SELECT name, tbl_name, sql
|
||||
FROM sqlite_master
|
||||
WHERE type = 'index' AND name NOT LIKE 'sqlite_%'
|
||||
ORDER BY name
|
||||
`).all();
|
||||
|
||||
expect(indexes.length).toBeGreaterThan(10); // Should have many indexes
|
||||
|
||||
// Check for specific critical indexes
|
||||
const indexNames = indexes.map(idx => idx.name);
|
||||
expect(indexNames).toContain('idx_products_product_code');
|
||||
expect(indexNames).toContain('idx_inventory_product_id');
|
||||
expect(indexNames).toContain('idx_inventory_history_product_id');
|
||||
expect(indexNames).toContain('idx_inventory_low_stock');
|
||||
});
|
||||
|
||||
test('should perform fast queries with large dataset', async () => {
|
||||
const db = database.getDatabase();
|
||||
|
||||
// Create large dataset
|
||||
const insertProduct = db.prepare(`
|
||||
INSERT INTO products (product_code, description, category, unit_of_measure)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
const insertInventory = db.prepare(`
|
||||
INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
console.log('Creating large dataset for query optimization tests...');
|
||||
const transaction = db.transaction(() => {
|
||||
for (let i = 1; i <= 1000; i++) {
|
||||
const result = insertProduct.run(
|
||||
`OPT${i.toString().padStart(6, '0')}`,
|
||||
`Optimization Test Product ${i}`,
|
||||
`Category${i % 20}`,
|
||||
'pcs'
|
||||
);
|
||||
insertInventory.run(
|
||||
result.lastInsertRowid,
|
||||
Math.floor(Math.random() * 1000) + 1,
|
||||
10,
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
transaction();
|
||||
|
||||
// Test query performance
|
||||
const queries = [
|
||||
{
|
||||
name: 'Product lookup by code',
|
||||
query: 'SELECT * FROM products WHERE product_code = ?',
|
||||
params: ['OPT000500']
|
||||
},
|
||||
{
|
||||
name: 'Category filter',
|
||||
query: 'SELECT * FROM products WHERE category = ?',
|
||||
params: ['Category5']
|
||||
},
|
||||
{
|
||||
name: 'Low stock query',
|
||||
query: `
|
||||
SELECT p.*, i.current_level, i.minimum_level
|
||||
FROM products p
|
||||
INNER JOIN inventory i ON p.id = i.product_id
|
||||
WHERE i.current_level <= i.minimum_level
|
||||
`,
|
||||
params: []
|
||||
},
|
||||
{
|
||||
name: 'Inventory summary',
|
||||
query: `
|
||||
SELECT p.product_code, p.description, i.current_level
|
||||
FROM products p
|
||||
INNER JOIN inventory i ON p.id = i.product_id
|
||||
ORDER BY p.product_code
|
||||
LIMIT 100
|
||||
`,
|
||||
params: []
|
||||
}
|
||||
];
|
||||
|
||||
for (const queryTest of queries) {
|
||||
const startTime = Date.now();
|
||||
const stmt = db.prepare(queryTest.query);
|
||||
const results = queryTest.params.length > 0
|
||||
? stmt.all(...queryTest.params)
|
||||
: stmt.all();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(`${queryTest.name}: ${duration}ms (${results.length} results)`);
|
||||
expect(duration).toBeLessThan(100); // Should be very fast with proper indexes
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle concurrent reads efficiently', async () => {
|
||||
const db = database.getDatabase();
|
||||
|
||||
// Create test data
|
||||
const insertProduct = db.prepare(`
|
||||
INSERT INTO products (product_code, description, category)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
insertProduct.run(`CONCURRENT${i}`, `Product ${i}`, 'Test');
|
||||
}
|
||||
|
||||
// Perform concurrent reads
|
||||
const concurrentReads = 50;
|
||||
const promises = [];
|
||||
|
||||
const startTime = Date.now();
|
||||
for (let i = 0; i < concurrentReads; i++) {
|
||||
const promise = new Promise((resolve) => {
|
||||
const stmt = db.prepare('SELECT * FROM products WHERE category = ?');
|
||||
const results = stmt.all('Test');
|
||||
resolve(results.length);
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(`${concurrentReads} concurrent reads completed in ${duration}ms`);
|
||||
expect(duration).toBeLessThan(1000); // Should complete quickly
|
||||
expect(results.every(count => count === 100)).toBe(true); // All should return same count
|
||||
});
|
||||
});
|
||||
|
||||
describe('Database Statistics and Analysis', () => {
|
||||
test('should provide database statistics', () => {
|
||||
const stats = database.getStats();
|
||||
|
||||
expect(stats.isInitialized).toBe(true);
|
||||
expect(stats.dbPath).toBe(testDbPath);
|
||||
expect(stats.pragmas).toBeDefined();
|
||||
expect(stats.tables).toBeDefined();
|
||||
expect(stats.indexes).toBeDefined();
|
||||
|
||||
// Check pragma settings
|
||||
expect(stats.pragmas.journalMode).toBe('wal');
|
||||
expect(stats.pragmas.synchronous).toBe(1); // NORMAL
|
||||
expect(stats.pragmas.cacheSize).toBeGreaterThanOrEqual(1000);
|
||||
});
|
||||
|
||||
test('should analyze performance and provide recommendations', () => {
|
||||
const analysis = database.analyzePerformance();
|
||||
|
||||
expect(analysis.timestamp).toBeDefined();
|
||||
expect(Array.isArray(analysis.recommendations)).toBe(true);
|
||||
|
||||
console.log('Performance analysis:', analysis);
|
||||
});
|
||||
|
||||
test('should get table statistics', () => {
|
||||
const stats = database.getTableStats();
|
||||
|
||||
expect(stats.products).toBeDefined();
|
||||
expect(stats.inventory).toBeDefined();
|
||||
expect(stats.inventory_history).toBeDefined();
|
||||
expect(stats.import_sessions).toBeDefined();
|
||||
|
||||
// All tables should have row count
|
||||
Object.values(stats).forEach(tableStat => {
|
||||
if (!tableStat.error) {
|
||||
expect(typeof tableStat.rowCount).toBe('number');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should get index statistics', () => {
|
||||
const indexes = database.getIndexStats();
|
||||
|
||||
expect(Array.isArray(indexes)).toBe(true);
|
||||
expect(indexes.length).toBeGreaterThan(0);
|
||||
|
||||
indexes.forEach(index => {
|
||||
expect(index.name).toBeDefined();
|
||||
expect(index.table).toBeDefined();
|
||||
expect(index.definition).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Database Optimization Operations', () => {
|
||||
test('should optimize database successfully', async () => {
|
||||
// Add some data first
|
||||
const db = database.getDatabase();
|
||||
const insertProduct = db.prepare(`
|
||||
INSERT INTO products (product_code, description, category)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
insertProduct.run(`OPTIMIZE${i}`, `Product ${i}`, 'Test');
|
||||
}
|
||||
|
||||
const result = await database.optimize();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.duration).toBeDefined();
|
||||
expect(result.results).toBeDefined();
|
||||
expect(result.results.vacuum).toBe(true);
|
||||
expect(result.results.analyze).toBe(true);
|
||||
expect(result.results.reindex).toBe(true);
|
||||
|
||||
console.log('Database optimization completed:', result);
|
||||
});
|
||||
|
||||
test('should prepare optimized statements', () => {
|
||||
const statements = database.prepareOptimizedStatements();
|
||||
|
||||
expect(statements).toBeDefined();
|
||||
expect(statements.findProductByCode).toBeDefined();
|
||||
expect(statements.getInventorySummary).toBeDefined();
|
||||
expect(statements.getLowStockItems).toBeDefined();
|
||||
expect(statements.updateInventoryLevel).toBeDefined();
|
||||
|
||||
// Test using a prepared statement
|
||||
const db = database.getDatabase();
|
||||
const insertProduct = db.prepare(`
|
||||
INSERT INTO products (product_code, description, category)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
insertProduct.run('TEST001', 'Test Product', 'Test');
|
||||
|
||||
const result = statements.findProductByCode.get('TEST001');
|
||||
expect(result).toBeDefined();
|
||||
expect(result.product_code).toBe('TEST001');
|
||||
});
|
||||
|
||||
test('should get prepared statement by name', () => {
|
||||
const statement = database.getPreparedStatement('findProductByCode');
|
||||
expect(statement).toBeDefined();
|
||||
|
||||
// Test error for non-existent statement
|
||||
expect(() => {
|
||||
database.getPreparedStatement('nonExistentStatement');
|
||||
}).toThrow('Prepared statement \'nonExistentStatement\' not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Benchmarks', () => {
|
||||
test('should handle large batch inserts efficiently', async () => {
|
||||
const db = database.getDatabase();
|
||||
const batchSize = 1000;
|
||||
|
||||
const insertProduct = db.prepare(`
|
||||
INSERT INTO products (product_code, description, category, unit_of_measure)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const transaction = db.transaction(() => {
|
||||
for (let i = 1; i <= batchSize; i++) {
|
||||
insertProduct.run(
|
||||
`BATCH${i.toString().padStart(6, '0')}`,
|
||||
`Batch Product ${i}`,
|
||||
`Category${i % 10}`,
|
||||
'pcs'
|
||||
);
|
||||
}
|
||||
});
|
||||
transaction();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(`Batch insert of ${batchSize} products: ${duration}ms`);
|
||||
expect(duration).toBeLessThan(5000); // Should complete within 5 seconds
|
||||
expect(duration / batchSize).toBeLessThan(5); // Less than 5ms per insert
|
||||
});
|
||||
|
||||
test('should handle complex queries efficiently', async () => {
|
||||
const db = database.getDatabase();
|
||||
|
||||
// Create test data with relationships
|
||||
const insertProduct = db.prepare(`
|
||||
INSERT INTO products (product_code, description, category, unit_of_measure)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
const insertInventory = db.prepare(`
|
||||
INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`);
|
||||
const insertHistory = db.prepare(`
|
||||
INSERT INTO inventory_history (product_id, old_level, new_level, change_reason, updated_by)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
// Create complex dataset
|
||||
const transaction = db.transaction(() => {
|
||||
for (let i = 1; i <= 500; i++) {
|
||||
const result = insertProduct.run(
|
||||
`COMPLEX${i.toString().padStart(6, '0')}`,
|
||||
`Complex Product ${i}`,
|
||||
`Category${i % 15}`,
|
||||
'pcs'
|
||||
);
|
||||
|
||||
insertInventory.run(
|
||||
result.lastInsertRowid,
|
||||
Math.floor(Math.random() * 100) + 1,
|
||||
10,
|
||||
200
|
||||
);
|
||||
|
||||
// Add some history records
|
||||
for (let j = 0; j < 3; j++) {
|
||||
insertHistory.run(
|
||||
result.lastInsertRowid,
|
||||
Math.floor(Math.random() * 50),
|
||||
Math.floor(Math.random() * 100) + 1,
|
||||
`Test update ${j}`,
|
||||
'test-user'
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
transaction();
|
||||
|
||||
// Test complex queries
|
||||
const complexQueries = [
|
||||
{
|
||||
name: 'Multi-table join with aggregation',
|
||||
query: `
|
||||
SELECT
|
||||
p.category,
|
||||
COUNT(*) as product_count,
|
||||
AVG(i.current_level) as avg_level,
|
||||
SUM(i.current_level) as total_inventory
|
||||
FROM products p
|
||||
INNER JOIN inventory i ON p.id = i.product_id
|
||||
GROUP BY p.category
|
||||
ORDER BY total_inventory DESC
|
||||
`
|
||||
},
|
||||
{
|
||||
name: 'History analysis with window functions',
|
||||
query: `
|
||||
SELECT
|
||||
p.product_code,
|
||||
ih.new_level,
|
||||
ih.updated_at,
|
||||
ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY ih.updated_at DESC) as rn
|
||||
FROM products p
|
||||
INNER JOIN inventory_history ih ON p.id = ih.product_id
|
||||
WHERE p.category = 'Category5'
|
||||
ORDER BY ih.updated_at DESC
|
||||
LIMIT 50
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
for (const queryTest of complexQueries) {
|
||||
const startTime = Date.now();
|
||||
const stmt = db.prepare(queryTest.query);
|
||||
const results = stmt.all();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
console.log(`${queryTest.name}: ${duration}ms (${results.length} results)`);
|
||||
expect(duration).toBeLessThan(500); // Complex queries should still be fast
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Concurrent Access and Locking', () => {
|
||||
test('should handle concurrent writes with proper locking', async () => {
|
||||
const db = database.getDatabase();
|
||||
|
||||
// Create a test product
|
||||
const insertProduct = db.prepare(`
|
||||
INSERT INTO products (product_code, description, category)
|
||||
VALUES (?, ?, ?)
|
||||
`);
|
||||
const result = insertProduct.run('LOCK_TEST', 'Lock Test Product', 'Test');
|
||||
const productId = result.lastInsertRowid;
|
||||
|
||||
// Create inventory record
|
||||
const insertInventory = db.prepare(`
|
||||
INSERT INTO inventory (product_id, current_level, minimum_level, maximum_level, version)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
insertInventory.run(productId, 100, 10, 200, 1);
|
||||
|
||||
// Simulate concurrent updates
|
||||
const concurrentUpdates = 10;
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < concurrentUpdates; i++) {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Simulate optimistic locking
|
||||
const selectStmt = db.prepare('SELECT version FROM inventory WHERE product_id = ?');
|
||||
const currentVersion = selectStmt.get(productId)?.version || 1;
|
||||
|
||||
const updateStmt = db.prepare(`
|
||||
UPDATE inventory
|
||||
SET current_level = ?, version = version + 1
|
||||
WHERE product_id = ? AND version = ?
|
||||
`);
|
||||
|
||||
const updateResult = updateStmt.run(100 + i, productId, currentVersion);
|
||||
resolve(updateResult.changes > 0);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const successful = results.filter(r => r.status === 'fulfilled' && r.value === true).length;
|
||||
const failed = results.filter(r => r.status === 'fulfilled' && r.value === false).length;
|
||||
|
||||
console.log(`Concurrent updates: ${successful} successful, ${failed} failed`);
|
||||
|
||||
// At least one should succeed, others should fail due to version conflicts
|
||||
expect(successful).toBeGreaterThan(0);
|
||||
expect(successful + failed).toBe(concurrentUpdates);
|
||||
|
||||
// Verify final state is consistent
|
||||
const finalState = db.prepare('SELECT current_level, version FROM inventory WHERE product_id = ?').get(productId);
|
||||
expect(finalState.version).toBeGreaterThan(1); // Should be incremented
|
||||
expect(finalState.current_level).toBeGreaterThanOrEqual(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user