Files
michaelschiemer/tests/Framework/Database/NPlusOneDetection/QueryExecutionContextTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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