214 lines
7.4 KiB
PHP
214 lines
7.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Database\QueryOptimization\Analysis\EagerLoadingAnalyzer;
|
|
use App\Framework\Database\QueryOptimization\Analysis\NPlusOneDetector;
|
|
use App\Framework\Database\QueryOptimization\NPlusOneDetectionService;
|
|
use App\Framework\Database\QueryOptimization\QueryLogger;
|
|
use App\Framework\Database\QueryOptimization\ValueObjects\QueryLog;
|
|
use App\Framework\Logging\Logger;
|
|
|
|
describe('NPlusOneDetectionService', function () {
|
|
beforeEach(function () {
|
|
$this->queryLogger = new QueryLogger();
|
|
$this->detector = new NPlusOneDetector();
|
|
$this->analyzer = new EagerLoadingAnalyzer();
|
|
$this->logger = $this->createMock(Logger::class);
|
|
|
|
$this->service = new NPlusOneDetectionService(
|
|
$this->queryLogger,
|
|
$this->detector,
|
|
$this->analyzer,
|
|
$this->logger
|
|
);
|
|
});
|
|
|
|
it('starts and stops logging', function () {
|
|
expect($this->queryLogger->isEnabled())->toBeFalse();
|
|
|
|
$this->service->startLogging();
|
|
expect($this->queryLogger->isEnabled())->toBeTrue();
|
|
|
|
$this->service->stopLogging();
|
|
expect($this->queryLogger->isEnabled())->toBeFalse();
|
|
});
|
|
|
|
it('analyzes logged queries', function () {
|
|
// Enable logging
|
|
$this->service->startLogging();
|
|
|
|
// Simulate N+1 queries
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$this->queryLogger->logQuery(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 5.0,
|
|
rowCount: 1
|
|
);
|
|
}
|
|
|
|
// Analyze
|
|
$result = $this->service->analyze();
|
|
|
|
expect($result)->toHaveKeys(['detections', 'strategies', 'statistics']);
|
|
expect($result['detections'])->not->toBeEmpty();
|
|
expect($result['statistics']['total_queries'])->toBe(10);
|
|
});
|
|
|
|
it('generates formatted report', function () {
|
|
$this->service->startLogging();
|
|
|
|
// Simulate N+1 queries
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$this->queryLogger->logQuery(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 5.0,
|
|
rowCount: 1
|
|
);
|
|
}
|
|
|
|
$report = $this->service->analyzeAndReport();
|
|
|
|
expect($report)->toContain('N+1 Query Detection Report');
|
|
expect($report)->toContain('Query Statistics');
|
|
expect($report)->toContain('N+1 Problems Detected');
|
|
});
|
|
|
|
it('profiles code execution', function () {
|
|
$executedCallable = false;
|
|
|
|
$analysis = $this->service->profile(function () use (&$executedCallable) {
|
|
$executedCallable = true;
|
|
return 'result';
|
|
});
|
|
|
|
expect($executedCallable)->toBeTrue();
|
|
expect($analysis)->toHaveKey('execution_time_ms');
|
|
expect($analysis)->toHaveKey('callback_result');
|
|
expect($analysis['callback_result'])->toBe('result');
|
|
});
|
|
|
|
it('returns critical problems only', function () {
|
|
$this->service->startLogging();
|
|
|
|
// Add critical N+1 (high execution count + slow execution)
|
|
// Severity calculation: exec_count(50)=+3, avg_time(55ms)=+3, consistent_caller=+2, total_time(2750ms)=+1 → 9 points (CRITICAL)
|
|
for ($i = 1; $i <= 50; $i++) {
|
|
$this->queryLogger->logQuery(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 55.0, // Changed from 10.0 to 55.0 to reach CRITICAL severity
|
|
rowCount: 1
|
|
);
|
|
}
|
|
|
|
// Add low severity N+1
|
|
for ($i = 1; $i <= 6; $i++) {
|
|
$this->queryLogger->logQuery(
|
|
sql: 'SELECT * FROM comments WHERE post_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 1.0,
|
|
rowCount: 1
|
|
);
|
|
}
|
|
|
|
$critical = $this->service->getCriticalProblems();
|
|
|
|
// Should only contain the high-severity posts pattern
|
|
expect($critical)->toHaveCount(1);
|
|
expect($critical[0]->isCritical())->toBeTrue();
|
|
});
|
|
|
|
it('quick check detects N+1 problems', function () {
|
|
$this->service->startLogging();
|
|
|
|
for ($i = 1; $i <= 10; $i++) {
|
|
$this->queryLogger->logQuery(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 5.0,
|
|
rowCount: 1
|
|
);
|
|
}
|
|
|
|
expect($this->service->hasNPlusOneProblems())->toBeTrue();
|
|
});
|
|
|
|
it('returns query statistics', function () {
|
|
$this->service->startLogging();
|
|
|
|
$this->queryLogger->logQuery('SELECT * FROM users', [], 5.0, 10);
|
|
$this->queryLogger->logQuery('INSERT INTO logs (message) VALUES (?)', ['test'], 2.0, 1);
|
|
$this->queryLogger->logQuery('UPDATE users SET name = ? WHERE id = ?', ['John', 1], 3.0, 1);
|
|
|
|
$stats = $this->service->getQueryStatistics();
|
|
|
|
expect($stats['total_queries'])->toBe(3);
|
|
expect($stats['select_count'])->toBe(1);
|
|
expect($stats['insert_count'])->toBe(1);
|
|
expect($stats['update_count'])->toBe(1);
|
|
});
|
|
|
|
it('clears query logs', function () {
|
|
$this->service->startLogging();
|
|
|
|
$this->queryLogger->logQuery('SELECT * FROM users', [], 5.0);
|
|
expect($this->service->getQueryLogs())->toHaveCount(1);
|
|
|
|
$this->service->clearLogs();
|
|
expect($this->service->getQueryLogs())->toBeEmpty();
|
|
});
|
|
|
|
it('integrates detections with eager loading strategies', function () {
|
|
$this->service->startLogging();
|
|
|
|
// Create N+1 pattern (need >5 queries and severity >= 4)
|
|
// Severity: exec_count(15)=+2, avg_time(25ms)=+2, consistent_caller=+2, total_time=+0 → 6 points
|
|
for ($i = 1; $i <= 15; $i++) {
|
|
$this->queryLogger->logQuery(
|
|
sql: 'SELECT * FROM posts WHERE user_id = ?',
|
|
bindings: [$i],
|
|
executionTimeMs: 25.0, // Increased from 8.0 to reach severity >= 4
|
|
rowCount: 1
|
|
);
|
|
}
|
|
|
|
$result = $this->service->analyze();
|
|
|
|
expect($result['detections'])->toHaveCount(1);
|
|
expect($result['strategies'])->toHaveCount(1);
|
|
|
|
$strategy = $result['strategies'][0];
|
|
expect($strategy->tableName)->toBe('posts');
|
|
expect($strategy->codeExample)->toContain('Eager Loading'); // Updated to match actual case
|
|
});
|
|
|
|
it('handles empty query logs gracefully', function () {
|
|
$this->service->startLogging();
|
|
|
|
$result = $this->service->analyze();
|
|
|
|
expect($result['detections'])->toBeEmpty();
|
|
expect($result['strategies'])->toBeEmpty();
|
|
expect($result['statistics']['total_queries'])->toBe(0);
|
|
});
|
|
|
|
it('logs analysis completion', function () {
|
|
// Expect 2 log calls: "Starting analysis" and "Analysis completed"
|
|
$this->logger->expects($this->exactly(2))
|
|
->method('info')
|
|
->willReturnCallback(function ($message) {
|
|
// Only assert on the second call (Analysis completed)
|
|
if (str_contains($message, 'Analysis completed')) {
|
|
expect($message)->toContain('Analysis completed');
|
|
}
|
|
});
|
|
|
|
$this->service->startLogging();
|
|
$this->queryLogger->logQuery('SELECT * FROM users', [], 5.0);
|
|
$this->service->analyze();
|
|
});
|
|
});
|