- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
229 lines
7.2 KiB
PHP
229 lines
7.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Database\QueryOptimization\Analysis\NPlusOneDetector;
|
|
use App\Framework\Database\QueryOptimization\ValueObjects\QueryLog;
|
|
|
|
describe('NPlusOneDetector', function () {
|
|
it('detects N+1 pattern with multiple executions', function () {
|
|
$detector = new NPlusOneDetector(minExecutionCount: 5, minSeverityScore: 4.0);
|
|
|
|
$queryLogs = [];
|
|
|
|
// 10 queries with same pattern
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$queryLogs[] = new QueryLog(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 8.0,
|
|
stackTrace: 'UserController::index',
|
|
callerClass: 'UserController',
|
|
callerMethod: 'index',
|
|
callerLine: 42
|
|
);
|
|
}
|
|
|
|
$detections = $detector->analyze($queryLogs);
|
|
|
|
expect($detections)->toHaveCount(1);
|
|
expect($detections[0]->pattern->getTableName())->toBe('posts');
|
|
expect($detections[0]->pattern->getExecutionCount())->toBe(10);
|
|
});
|
|
|
|
it('ignores patterns with too few executions', function () {
|
|
$detector = new NPlusOneDetector(minExecutionCount: 10);
|
|
|
|
$queryLogs = [];
|
|
|
|
// Only 5 queries
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$queryLogs[] = new QueryLog(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 5.0,
|
|
stackTrace: ''
|
|
);
|
|
}
|
|
|
|
$detections = $detector->analyze($queryLogs);
|
|
|
|
expect($detections)->toBeEmpty();
|
|
});
|
|
|
|
it('ignores patterns with low severity', function () {
|
|
$detector = new NPlusOneDetector(minExecutionCount: 5, minSeverityScore: 8.0);
|
|
|
|
$queryLogs = [];
|
|
|
|
// 6 queries with low severity
|
|
for ($i = 1; $i <= 6; $i++) {
|
|
$queryLogs[] = new QueryLog(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 1.0,
|
|
stackTrace: '',
|
|
callerClass: 'Different' . $i,
|
|
callerMethod: 'method' . $i
|
|
);
|
|
}
|
|
|
|
$detections = $detector->analyze($queryLogs);
|
|
|
|
expect($detections)->toBeEmpty();
|
|
});
|
|
|
|
it('generates recommendations for detected N+1', function () {
|
|
$detector = new NPlusOneDetector();
|
|
|
|
$queryLogs = [];
|
|
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$queryLogs[] = new QueryLog(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 5.0,
|
|
stackTrace: 'UserController::index',
|
|
callerClass: 'UserController',
|
|
callerMethod: 'index',
|
|
callerLine: 42
|
|
);
|
|
}
|
|
|
|
$detections = $detector->analyze($queryLogs);
|
|
|
|
expect($detections[0]->recommendation)->toContain('eager loading');
|
|
expect($detections[0]->recommendation)->toContain('batch loading');
|
|
expect($detections[0]->recommendation)->toContain('JOIN');
|
|
});
|
|
|
|
it('detects multiple N+1 patterns', function () {
|
|
$detector = new NPlusOneDetector();
|
|
|
|
$queryLogs = [];
|
|
|
|
// Pattern 1: Posts
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$queryLogs[] = new QueryLog(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 5.0,
|
|
stackTrace: '',
|
|
callerClass: 'UserController',
|
|
callerMethod: 'index'
|
|
);
|
|
}
|
|
|
|
// Pattern 2: Comments
|
|
for ($i = 1; $i <= 8; $i++) {
|
|
$queryLogs[] = new QueryLog(
|
|
sql: 'SELECT * FROM comments WHERE post_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 3.0,
|
|
stackTrace: '',
|
|
callerClass: 'PostController',
|
|
callerMethod: 'show'
|
|
);
|
|
}
|
|
|
|
$detections = $detector->analyze($queryLogs);
|
|
|
|
expect($detections)->toHaveCount(2);
|
|
|
|
$tables = array_map(fn($d) => $d->pattern->getTableName(), $detections);
|
|
expect($tables)->toContain('posts');
|
|
expect($tables)->toContain('comments');
|
|
});
|
|
|
|
it('calculates statistics correctly', function () {
|
|
$detector = new NPlusOneDetector();
|
|
|
|
$queryLogs = [];
|
|
|
|
// 10 N+1 queries
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$queryLogs[] = new QueryLog(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 10.0,
|
|
stackTrace: '',
|
|
callerClass: 'UserController',
|
|
callerMethod: 'index'
|
|
);
|
|
}
|
|
|
|
// 5 normal queries
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$queryLogs[] = new QueryLog(
|
|
sql: 'INSERT INTO logs (message) VALUES (?)',
|
|
bindings: ["log{$i}"],
|
|
executionTimeMs: 2.0,
|
|
stackTrace: ''
|
|
);
|
|
}
|
|
|
|
$stats = $detector->getStatistics($queryLogs);
|
|
|
|
expect($stats['total_queries'])->toBe(15);
|
|
expect($stats['n_plus_one_patterns'])->toBe(1);
|
|
expect($stats['n_plus_one_queries'])->toBe(10);
|
|
expect($stats['n_plus_one_time_ms'])->toBe(100.0);
|
|
expect($stats['n_plus_one_percentage'])->toBeGreaterThan(50.0);
|
|
});
|
|
|
|
it('quick check detects N+1 problems', function () {
|
|
$detector = new NPlusOneDetector();
|
|
|
|
$queryLogs = [];
|
|
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$queryLogs[] = new QueryLog(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 5.0,
|
|
stackTrace: '',
|
|
callerClass: 'UserController',
|
|
callerMethod: 'index'
|
|
);
|
|
}
|
|
|
|
expect($detector->hasNPlusOne($queryLogs))->toBeTrue();
|
|
});
|
|
|
|
it('quick check returns false for no N+1', function () {
|
|
$detector = new NPlusOneDetector();
|
|
|
|
$queryLogs = [
|
|
new QueryLog('SELECT * FROM users', [], 5.0, ''),
|
|
new QueryLog('SELECT * FROM posts WHERE id = ?', [1], 3.0, ''),
|
|
];
|
|
|
|
expect($detector->hasNPlusOne($queryLogs))->toBeFalse();
|
|
});
|
|
|
|
it('generates metadata with query details', function () {
|
|
$detector = new NPlusOneDetector();
|
|
|
|
$queryLogs = [];
|
|
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$queryLogs[] = new QueryLog(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 5.0,
|
|
stackTrace: '',
|
|
callerClass: 'UserController',
|
|
callerMethod: 'index'
|
|
);
|
|
}
|
|
|
|
$detections = $detector->analyze($queryLogs);
|
|
|
|
expect($detections[0]->metadata)->toHaveKey('execution_count');
|
|
expect($detections[0]->metadata)->toHaveKey('total_time_ms');
|
|
expect($detections[0]->metadata)->toHaveKey('average_time_ms');
|
|
expect($detections[0]->metadata)->toHaveKey('table_name');
|
|
expect($detections[0]->metadata)->toHaveKey('first_query_sql');
|
|
});
|
|
});
|