@ahmedhegazee/nestjs-telescope
Version:
Advanced observability and monitoring solution for NestJS applications with ML-powered analytics, enterprise features, and production-ready scaling
372 lines • 15.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const testing_1 = require("@nestjs/testing");
const query_analyzer_service_1 = require("./query-analyzer.service");
const typeorm_1 = require("typeorm");
describe('QueryAnalyzerService', () => {
let service;
let dataSource;
beforeEach(async () => {
const mockDataSource = {
options: {
type: 'postgres'
},
query: jest.fn()
};
const module = await testing_1.Test.createTestingModule({
providers: [
query_analyzer_service_1.QueryAnalyzerService,
{
provide: typeorm_1.DataSource,
useValue: mockDataSource,
},
],
}).compile();
service = module.get(query_analyzer_service_1.QueryAnalyzerService);
dataSource = module.get(typeorm_1.DataSource);
});
afterEach(() => {
jest.clearAllMocks();
service.clearPatterns();
});
describe('query analysis', () => {
it('should analyze slow query and identify issues', async () => {
const context = {
id: 'query-1',
sql: 'SELECT * FROM users WHERE UPPER(name) = $1',
parameters: ['JOHN'],
startTime: Date.now(),
duration: 2000,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.queryId).toBe('query-1');
expect(analysis.duration).toBe(2000);
expect(analysis.severity).toBe('slow');
expect(analysis.issues).toHaveLength(2);
expect(analysis.issues).toContainEqual(expect.objectContaining({
type: 'full_table_scan',
severity: 'medium',
description: expect.stringContaining('SELECT *')
}));
expect(analysis.issues).toContainEqual(expect.objectContaining({
type: 'missing_index',
severity: 'medium',
description: expect.stringContaining('functions in WHERE clause')
}));
});
it('should detect missing WHERE clause', async () => {
const context = {
id: 'query-2',
sql: 'SELECT id, name FROM users',
parameters: [],
startTime: Date.now(),
duration: 3000,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.issues).toContainEqual(expect.objectContaining({
type: 'full_table_scan',
severity: 'high',
description: expect.stringContaining('lacks WHERE clause')
}));
});
it('should detect excessive JOINs', async () => {
const context = {
id: 'query-3',
sql: `SELECT u.name, p.title, c.content, t.name, cat.name, auth.name
FROM users u
JOIN posts p ON u.id = p.user_id
JOIN comments c ON p.id = c.post_id
JOIN tags t ON p.id = t.post_id
JOIN categories cat ON p.category_id = cat.id
JOIN authors auth ON p.author_id = auth.id
JOIN publishers pub ON auth.publisher_id = pub.id`,
parameters: [],
startTime: Date.now(),
duration: 4000,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.issues).toContainEqual(expect.objectContaining({
type: 'excessive_joins',
severity: 'high',
description: expect.stringContaining('6 joins')
}));
});
it('should detect IN subquery that could be optimized', async () => {
const context = {
id: 'query-4',
sql: 'SELECT * FROM users WHERE id IN (SELECT user_id FROM posts WHERE published = true)',
parameters: [],
startTime: Date.now(),
duration: 1500,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.issues).toContainEqual(expect.objectContaining({
type: 'subquery_performance',
severity: 'medium',
description: expect.stringContaining('IN with subquery')
}));
});
it('should detect LIKE with leading wildcard', async () => {
const context = {
id: 'query-5',
sql: 'SELECT * FROM users WHERE name LIKE \'%john%\'',
parameters: [],
startTime: Date.now(),
duration: 1200,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.issues).toContainEqual(expect.objectContaining({
type: 'missing_index',
severity: 'medium',
description: expect.stringContaining('LIKE with leading wildcard')
}));
});
it('should detect OR conditions in WHERE clause', async () => {
const context = {
id: 'query-6',
sql: 'SELECT * FROM users WHERE name = $1 OR email = $2',
parameters: ['john', 'john@example.com'],
startTime: Date.now(),
duration: 1800,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.issues).toContainEqual(expect.objectContaining({
type: 'missing_index',
severity: 'medium',
description: expect.stringContaining('OR conditions')
}));
});
});
describe('optimization hints', () => {
it('should generate index suggestion for slow queries', async () => {
const context = {
id: 'query-7',
sql: 'SELECT * FROM users WHERE created_at > $1 AND status = $2',
parameters: ['2023-01-01', 'active'],
startTime: Date.now(),
duration: 2000,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.optimizationHints).toContainEqual(expect.objectContaining({
type: 'index_suggestion',
priority: 'high',
description: expect.stringContaining('database indexes'),
estimatedImpact: expect.stringContaining('50-95%')
}));
});
it('should generate query rewrite suggestion for subqueries', async () => {
const context = {
id: 'query-8',
sql: 'SELECT * FROM users WHERE id IN (SELECT user_id FROM posts)',
parameters: [],
startTime: Date.now(),
duration: 1500,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.optimizationHints).toContainEqual(expect.objectContaining({
type: 'query_rewrite',
priority: 'medium',
description: expect.stringContaining('Rewrite subqueries as JOINs')
}));
});
it('should generate caching suggestion for frequent queries', async () => {
const sql = 'SELECT * FROM users WHERE status = $1';
for (let i = 0; i < 12; i++) {
const context = {
id: `query-${i}`,
sql,
parameters: ['active'],
startTime: Date.now(),
duration: 500,
operation: 'select',
tableName: 'users'
};
await service.analyzeQuery(context);
}
const context = {
id: 'query-final',
sql,
parameters: ['active'],
startTime: Date.now(),
duration: 500,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.optimizationHints).toContainEqual(expect.objectContaining({
type: 'caching_opportunity',
priority: 'medium',
description: expect.stringContaining('frequently executed query')
}));
});
});
describe('execution plan analysis', () => {
it('should analyze PostgreSQL execution plan', async () => {
const mockExecutionPlan = [
{
'QUERY PLAN': [
{
'Plan': {
'Node Type': 'Seq Scan',
'Relation Name': 'users',
'Total Cost': 15000,
'Actual Rows': 50000,
'Filter': 'name = $1'
}
}
]
}
];
dataSource.query.mockResolvedValue(mockExecutionPlan);
const context = {
id: 'query-plan',
sql: 'SELECT * FROM users WHERE name = $1',
parameters: ['john'],
startTime: Date.now(),
duration: 3000,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.executionPlan).toBeDefined();
expect(analysis.executionPlan.totalCost).toBe(15000);
expect(analysis.issues).toContainEqual(expect.objectContaining({
type: 'full_table_scan',
severity: 'high',
description: expect.stringContaining('high execution cost')
}));
});
it('should handle execution plan failures gracefully', async () => {
dataSource.query.mockRejectedValue(new Error('Permission denied'));
const context = {
id: 'query-plan-fail',
sql: 'SELECT * FROM users',
parameters: [],
startTime: Date.now(),
duration: 1000,
operation: 'select',
tableName: 'users'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.executionPlan).toBeUndefined();
expect(analysis.issues).toHaveLength(2);
});
});
describe('query patterns', () => {
it('should track query patterns', async () => {
const queries = [
'SELECT * FROM users WHERE id = $1',
'SELECT * FROM users WHERE id = $1',
'SELECT * FROM posts WHERE user_id = $1',
'SELECT * FROM users WHERE id = $1'
];
for (let i = 0; i < queries.length; i++) {
const context = {
id: `query-${i}`,
sql: queries[i],
parameters: [i + 1],
startTime: Date.now(),
duration: 100,
operation: 'select'
};
await service.analyzeQuery(context);
}
const patterns = service.getQueryPatterns();
expect(patterns).toHaveLength(2);
const userPattern = patterns.find(p => p.pattern.includes('users'));
expect(userPattern).toBeDefined();
expect(userPattern.count).toBe(3);
});
it('should identify slow query patterns', async () => {
const queries = [
{ sql: 'SELECT * FROM users WHERE name LIKE $1', duration: 2000 },
{ sql: 'SELECT * FROM users WHERE name LIKE $1', duration: 1800 },
{ sql: 'SELECT * FROM posts WHERE title LIKE $1', duration: 500 },
];
for (let i = 0; i < queries.length; i++) {
const context = {
id: `query-${i}`,
sql: queries[i].sql,
parameters: [`%test${i}%`],
startTime: Date.now(),
duration: queries[i].duration,
operation: 'select'
};
await service.analyzeQuery(context);
}
const slowPatterns = service.getSlowQueryPatterns();
expect(slowPatterns).toHaveLength(1);
expect(slowPatterns[0].averageDuration).toBe(1900);
});
});
describe('severity determination', () => {
it('should determine correct severity levels', async () => {
const testCases = [
{ duration: 800, expectedSeverity: 'slow' },
{ duration: 3000, expectedSeverity: 'slow' },
{ duration: 7000, expectedSeverity: 'very_slow' },
{ duration: 15000, expectedSeverity: 'critical' }
];
for (const testCase of testCases) {
const context = {
id: `query-${testCase.duration}`,
sql: 'SELECT * FROM users',
parameters: [],
startTime: Date.now(),
duration: testCase.duration,
operation: 'select'
};
const analysis = await service.analyzeQuery(context);
expect(analysis.severity).toBe(testCase.expectedSeverity);
}
});
});
describe('optimization suggestions', () => {
it('should return optimization suggestions', async () => {
const context = {
id: 'query-opt',
sql: 'SELECT * FROM users WHERE name = $1',
parameters: ['john'],
startTime: Date.now(),
duration: 2000,
operation: 'select',
tableName: 'users'
};
await service.analyzeQuery(context);
const suggestions = service.getOptimizationSuggestions();
expect(suggestions).toBeInstanceOf(Array);
});
it('should clear patterns and suggestions', async () => {
const context = {
id: 'query-clear',
sql: 'SELECT * FROM users',
parameters: [],
startTime: Date.now(),
duration: 1000,
operation: 'select'
};
await service.analyzeQuery(context);
expect(service.getQueryPatterns()).toHaveLength(1);
service.clearPatterns();
expect(service.getQueryPatterns()).toHaveLength(0);
expect(service.getOptimizationSuggestions()).toHaveLength(0);
});
});
});
//# sourceMappingURL=query-analyzer.service.spec.js.map