- 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.
264 lines
11 KiB
PHP
264 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Framework\Database\NPlusOneDetection;
|
|
|
|
use App\Framework\Database\NPlusOneDetection\QueryExecutionContext;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
|
|
describe('QueryExecutionContext', function () {
|
|
it('can be constructed with all parameters', function () {
|
|
$context = new QueryExecutionContext(
|
|
queryCount: 10,
|
|
duration: Duration::fromMilliseconds(100),
|
|
uniqueQueryHashes: ['hash1', 'hash2'],
|
|
queryTimings: [10.0, 20.0, 30.0],
|
|
queryComplexityScores: [0.5, 0.6, 0.7],
|
|
totalJoinCount: 5,
|
|
executedInLoop: true,
|
|
loopDepth: 2,
|
|
metadata: ['key' => '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);
|
|
});
|
|
});
|