'value'] ); expect($context->queryCount)->toBe(10); expect($context->duration->toMilliseconds())->toBe(100); expect($context->uniqueQueryHashes)->toHaveCount(2); expect($context->queryTimings)->toHaveCount(3); expect($context->queryComplexityScores)->toHaveCount(3); expect($context->totalJoinCount)->toBe(5); expect($context->executedInLoop)->toBeTrue(); expect($context->loopDepth)->toBe(2); expect($context->metadata)->toHaveKey('key'); }); it('creates minimal context', function () { $context = QueryExecutionContext::minimal(); expect($context->queryCount)->toBe(1); expect($context->duration->toMilliseconds())->toBe(10); expect($context->uniqueQueryHashes)->toHaveCount(1); expect($context->executedInLoop)->toBeFalse(); expect($context->loopDepth)->toBeNull(); }); it('creates minimal context with custom parameters', function () { $context = QueryExecutionContext::minimal( queryCount: 5, durationMs: 50.0, uniqueQueries: 2 ); expect($context->queryCount)->toBe(5); expect($context->duration->toMilliseconds())->toBe(50); expect($context->uniqueQueryHashes)->toHaveCount(2); }); it('creates context from query array', function () { $queries = [ ['query' => 'SELECT * FROM users WHERE id = 1', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SELECT * FROM users WHERE id = 2', 'duration' => 12.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SELECT * FROM posts WHERE user_id = 1', 'duration' => 15.0, 'complexity' => 0.6, 'joins' => 1], ]; $context = QueryExecutionContext::fromQueries($queries); expect($context->queryCount)->toBe(3); expect($context->duration->toMilliseconds())->toBe(37); // 10 + 12 + 15 expect($context->queryTimings)->toBe([10.0, 12.0, 15.0]); expect($context->queryComplexityScores)->toBe([0.5, 0.5, 0.6]); expect($context->totalJoinCount)->toBe(1); }); it('normalizes and deduplicates similar queries', function () { $queries = [ ['query' => 'SELECT * FROM users WHERE id = 1', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SELECT * FROM users WHERE id = 2', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SELECT * FROM users WHERE id = 3', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SELECT * FROM posts WHERE user_id = 1', 'duration' => 15.0, 'complexity' => 0.6, 'joins' => 1], ]; $context = QueryExecutionContext::fromQueries($queries); // 4 total queries, but only 2 unique (users query normalized, posts query different) expect($context->queryCount)->toBe(4); expect($context->uniqueQueryHashes)->toHaveCount(2); }); it('detects N+1 pattern when high repetition + loop execution', function () { $queries = []; for ($i = 0; $i < 10; $i++) { $queries[] = [ 'query' => "SELECT * FROM users WHERE id = {$i}", 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0 ]; } $context = QueryExecutionContext::fromQueries($queries, executedInLoop: true); // 10 queries, 1 unique (all normalized to same hash) = 90% repetition expect($context->hasNPlusOnePattern())->toBeTrue(); }); it('does not detect N+1 pattern when not executed in loop', function () { $queries = []; for ($i = 0; $i < 10; $i++) { $queries[] = [ 'query' => "SELECT * FROM users WHERE id = {$i}", 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0 ]; } $context = QueryExecutionContext::fromQueries($queries, executedInLoop: false); // High repetition but no loop = no N+1 pattern expect($context->hasNPlusOnePattern())->toBeFalse(); }); it('does not detect N+1 pattern with low repetition', function () { $queries = [ ['query' => 'SELECT * FROM users', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SELECT * FROM posts', 'duration' => 15.0, 'complexity' => 0.6, 'joins' => 1], ['query' => 'SELECT * FROM comments', 'duration' => 12.0, 'complexity' => 0.7, 'joins' => 2], ]; $context = QueryExecutionContext::fromQueries($queries, executedInLoop: true); // 3 queries, 3 unique = 0% repetition expect($context->hasNPlusOnePattern())->toBeFalse(); }); it('does not detect N+1 pattern with less than 3 queries', function () { $queries = [ ['query' => 'SELECT * FROM users WHERE id = 1', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SELECT * FROM users WHERE id = 2', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ]; $context = QueryExecutionContext::fromQueries($queries, executedInLoop: true); // Less than 3 queries = no N+1 pattern expect($context->hasNPlusOnePattern())->toBeFalse(); }); it('calculates repetition rate correctly', function () { $queries = []; for ($i = 0; $i < 10; $i++) { $queries[] = [ 'query' => "SELECT * FROM users WHERE id = {$i}", 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0 ]; } $context = QueryExecutionContext::fromQueries($queries); // 10 total, 1 unique = 90% repetition $repetitionRate = $context->getRepetitionRate(); expect($repetitionRate)->toBe(90.0); }); it('calculates zero repetition rate for unique queries', function () { $queries = [ ['query' => 'SELECT * FROM users', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SELECT * FROM posts', 'duration' => 15.0, 'complexity' => 0.6, 'joins' => 1], ['query' => 'SELECT * FROM comments', 'duration' => 12.0, 'complexity' => 0.7, 'joins' => 2], ]; $context = QueryExecutionContext::fromQueries($queries); // 3 total, 3 unique = 0% repetition $repetitionRate = $context->getRepetitionRate(); expect($repetitionRate)->toBe(0.0); }); it('calculates average execution time', function () { $context = new QueryExecutionContext( queryCount: 5, duration: Duration::fromMilliseconds(100), uniqueQueryHashes: ['hash1'], queryTimings: [20.0, 20.0, 20.0, 20.0, 20.0], queryComplexityScores: [0.5, 0.5, 0.5, 0.5, 0.5], totalJoinCount: 0, executedInLoop: false ); // 100ms total / 5 queries = 20ms average $avgTime = $context->getAverageExecutionTime(); expect($avgTime)->toBe(20.0); }); it('handles zero queries gracefully', function () { $context = new QueryExecutionContext( queryCount: 0, duration: Duration::fromMilliseconds(0), uniqueQueryHashes: [], queryTimings: [], queryComplexityScores: [], totalJoinCount: 0, executedInLoop: false ); expect($context->hasNPlusOnePattern())->toBeFalse(); expect($context->getRepetitionRate())->toBe(0.0); expect($context->getAverageExecutionTime())->toBe(0.0); }); it('normalizes query with different parameter values to same hash', function () { $queries = [ ['query' => 'SELECT * FROM users WHERE id = 1', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SELECT * FROM users WHERE id = 999', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => "SELECT * FROM users WHERE id = '5'", 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ]; $context = QueryExecutionContext::fromQueries($queries); // All should normalize to same query pattern (different parameter values) expect($context->queryCount)->toBe(3); expect($context->uniqueQueryHashes)->toHaveCount(1); }); it('normalizes query with different whitespace to same hash', function () { $queries = [ ['query' => 'SELECT * FROM users WHERE id = 1', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SELECT * FROM users WHERE id = 1', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => "SELECT\n*\nFROM\nusers\nWHERE\nid\n=\n1", 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ]; $context = QueryExecutionContext::fromQueries($queries); // All should normalize to same query (different whitespace) expect($context->queryCount)->toBe(3); expect($context->uniqueQueryHashes)->toHaveCount(1); }); it('normalizes query case-insensitively', function () { $queries = [ ['query' => 'SELECT * FROM users WHERE id = 1', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'select * from users where id = 1', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ['query' => 'SeLeCt * FrOm UsErS wHeRe Id = 1', 'duration' => 10.0, 'complexity' => 0.5, 'joins' => 0], ]; $context = QueryExecutionContext::fromQueries($queries); // All should normalize to same query (different case) expect($context->queryCount)->toBe(3); expect($context->uniqueQueryHashes)->toHaveCount(1); }); it('tracks loop depth when provided', function () { $context = QueryExecutionContext::fromQueries( [], executedInLoop: true, loopDepth: 3 ); expect($context->executedInLoop)->toBeTrue(); expect($context->loopDepth)->toBe(3); }); });