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'); }); });