- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
608 lines
18 KiB
PHP
608 lines
18 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Byte;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Database\ConnectionInterface;
|
|
use App\Framework\Database\Migration\Migration;
|
|
use App\Framework\Database\Migration\MigrationCollection;
|
|
use App\Framework\Database\Migration\MigrationRunner;
|
|
use App\Framework\Database\Migration\MigrationVersion;
|
|
use App\Framework\Database\Platform\MySQLPlatform;
|
|
use App\Framework\Database\QueryBuilder\QueryBuilder;
|
|
use App\Framework\Database\QueryBuilder\QueryBuilderFactory;
|
|
use App\Framework\Database\ResultInterface;
|
|
use App\Framework\Database\ValueObjects\SqlQuery;
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\Performance\Entity\PerformanceMetric;
|
|
use App\Framework\Performance\MemoryMonitor;
|
|
use App\Framework\Performance\OperationTracker;
|
|
use App\Framework\Performance\PerformanceCategory;
|
|
use App\Framework\Performance\Repository\PerformanceMetricsRepository;
|
|
|
|
describe('Performance Metrics Integration', function () {
|
|
beforeEach(function () {
|
|
$this->connection = new PerformanceMetricsMockConnection();
|
|
$this->platform = new MySQLPlatform();
|
|
$this->clock = new SystemClock();
|
|
$this->memoryMonitor = new MemoryMonitor();
|
|
$this->operationTracker = new OperationTracker($this->clock, $this->memoryMonitor);
|
|
$this->queryBuilderFactory = new PerformanceMetricsMockQueryBuilderFactory();
|
|
$this->performanceMetricsRepository = new PerformanceMetricsRepository(
|
|
$this->connection,
|
|
$this->queryBuilderFactory
|
|
);
|
|
|
|
$this->migrationRunner = new MigrationRunner(
|
|
$this->connection,
|
|
$this->platform,
|
|
$this->clock,
|
|
null, // tableConfig
|
|
null, // logger
|
|
$this->operationTracker,
|
|
$this->memoryMonitor,
|
|
null, // performanceReporter
|
|
null, // memoryThresholds
|
|
$this->performanceMetricsRepository
|
|
);
|
|
});
|
|
|
|
test('migration runner persists performance metrics for successful migrations', function () {
|
|
$migration = new PerformanceTestMigration();
|
|
$migrations = new MigrationCollection($migration);
|
|
|
|
// Set no applied migrations initially
|
|
$this->connection->setAppliedMigrations([]);
|
|
|
|
$result = $this->migrationRunner->migrate($migrations);
|
|
|
|
expect($result)->toContain('2024_01_01_000000');
|
|
expect($migration->wasExecuted())->toBeTrue();
|
|
|
|
// Verify performance metrics were persisted
|
|
$savedMetrics = $this->queryBuilderFactory->getInsertedData('performance_metrics');
|
|
expect($savedMetrics)->toHaveCount(1);
|
|
|
|
$metric = $savedMetrics[0];
|
|
expect($metric['operation_type'])->toBe('migration_execution');
|
|
expect($metric['category'])->toBe('DATABASE');
|
|
expect($metric['migration_version'])->toBe('2024_01_01_000000');
|
|
expect($metric['success'])->toBe(true);
|
|
expect($metric['error_message'])->toBeNull();
|
|
expect($metric['metadata'])->toContain('migration_description');
|
|
});
|
|
|
|
test('migration runner persists performance metrics for failed migrations', function () {
|
|
$migration = new PerformanceMetricsFailingTestMigration();
|
|
$migrations = new MigrationCollection($migration);
|
|
|
|
// Set no applied migrations initially
|
|
$this->connection->setAppliedMigrations([]);
|
|
|
|
expect(function () {
|
|
$this->migrationRunner->migrate($migrations);
|
|
})->toThrow(Exception::class);
|
|
|
|
// Verify failed migration performance metrics were persisted
|
|
$savedMetrics = $this->queryBuilderFactory->getInsertedData('performance_metrics');
|
|
expect($savedMetrics)->toHaveCount(1);
|
|
|
|
$metric = $savedMetrics[0];
|
|
expect($metric['operation_type'])->toBe('migration_execution');
|
|
expect($metric['category'])->toBe('DATABASE');
|
|
expect($metric['migration_version'])->toBe('2024_01_01_000001');
|
|
expect($metric['success'])->toBe(false);
|
|
expect($metric['error_message'])->toContain('Test migration failure');
|
|
expect($metric['metadata'])->toContain('error_type');
|
|
});
|
|
|
|
test('migration runner persists performance metrics for rollbacks', function () {
|
|
$migration = new PerformanceTestMigration();
|
|
$migrations = new MigrationCollection($migration);
|
|
|
|
// Set migration as already applied
|
|
$this->connection->setAppliedMigrations([
|
|
['version' => '2024_01_01_000000', 'description' => 'Performance Test Migration'],
|
|
]);
|
|
|
|
$result = $this->migrationRunner->rollback($migrations, 1);
|
|
|
|
expect($result)->toContain($migration);
|
|
|
|
// Verify rollback performance metrics were persisted
|
|
$savedMetrics = $this->queryBuilderFactory->getInsertedData('performance_metrics');
|
|
expect($savedMetrics)->toHaveCount(1);
|
|
|
|
$metric = $savedMetrics[0];
|
|
expect($metric['operation_type'])->toBe('rollback_execution');
|
|
expect($metric['category'])->toBe('DATABASE');
|
|
expect($metric['migration_version'])->toBe('2024_01_01_000000');
|
|
expect($metric['success'])->toBe(true);
|
|
expect($metric['metadata'])->toContain('rollback_steps');
|
|
});
|
|
|
|
test('performance metrics repository can query metrics by migration version', function () {
|
|
// Create test metrics
|
|
$version = MigrationVersion::fromTimestamp('2024_01_01_000000');
|
|
$metrics = [
|
|
PerformanceMetric::fromPerformanceSnapshot(
|
|
'migration_test_1',
|
|
'migration_execution',
|
|
PerformanceCategory::DATABASE,
|
|
Duration::fromMilliseconds(1500),
|
|
Byte::fromBytes(50 * 1024 * 1024),
|
|
Byte::fromBytes(55 * 1024 * 1024),
|
|
Byte::fromBytes(60 * 1024 * 1024),
|
|
Byte::fromBytes(5 * 1024 * 1024),
|
|
true,
|
|
null,
|
|
$version,
|
|
['test' => 'data']
|
|
),
|
|
PerformanceMetric::fromPerformanceSnapshot(
|
|
'rollback_test_1',
|
|
'rollback_execution',
|
|
PerformanceCategory::DATABASE,
|
|
Duration::fromMilliseconds(800),
|
|
Byte::fromBytes(55 * 1024 * 1024),
|
|
Byte::fromBytes(50 * 1024 * 1024),
|
|
Byte::fromBytes(55 * 1024 * 1024),
|
|
Byte::fromBytes(-5 * 1024 * 1024),
|
|
true,
|
|
null,
|
|
$version,
|
|
['rollback' => 'data']
|
|
),
|
|
];
|
|
|
|
// Save metrics
|
|
$this->performanceMetricsRepository->saveBatch($metrics);
|
|
|
|
// Query by migration version
|
|
$foundMetrics = $this->performanceMetricsRepository->findByMigrationVersion($version);
|
|
|
|
expect($foundMetrics)->toHaveCount(2);
|
|
expect($foundMetrics[0]->operationType)->toBeIn(['migration_execution', 'rollback_execution']);
|
|
expect($foundMetrics[1]->operationType)->toBeIn(['migration_execution', 'rollback_execution']);
|
|
});
|
|
|
|
test('performance metrics repository can get performance statistics', function () {
|
|
$since = new DateTimeImmutable('-1 hour');
|
|
|
|
// Create test metrics with different categories
|
|
$metrics = [
|
|
PerformanceMetric::fromPerformanceSnapshot(
|
|
'migration_1',
|
|
'migration_execution',
|
|
PerformanceCategory::DATABASE,
|
|
Duration::fromMilliseconds(1000),
|
|
Byte::fromBytes(50 * 1024 * 1024),
|
|
Byte::fromBytes(55 * 1024 * 1024),
|
|
Byte::fromBytes(60 * 1024 * 1024),
|
|
Byte::fromBytes(5 * 1024 * 1024),
|
|
true
|
|
),
|
|
PerformanceMetric::fromPerformanceSnapshot(
|
|
'migration_2',
|
|
'migration_execution',
|
|
PerformanceCategory::DATABASE,
|
|
Duration::fromMilliseconds(2000),
|
|
Byte::fromBytes(60 * 1024 * 1024),
|
|
Byte::fromBytes(65 * 1024 * 1024),
|
|
Byte::fromBytes(70 * 1024 * 1024),
|
|
Byte::fromBytes(5 * 1024 * 1024),
|
|
false,
|
|
'Test failure'
|
|
),
|
|
];
|
|
|
|
$this->performanceMetricsRepository->saveBatch($metrics);
|
|
|
|
$statistics = $this->performanceMetricsRepository->getPerformanceStatistics($since);
|
|
|
|
expect($statistics)->toHaveCount(1); // Only DATABASE category
|
|
expect($statistics[0]['category'])->toBe(PerformanceCategory::DATABASE);
|
|
expect($statistics[0]['total_operations'])->toBe(2);
|
|
expect($statistics[0]['successful_operations'])->toBe(1);
|
|
expect($statistics[0]['success_rate'])->toBe(0.5);
|
|
expect($statistics[0]['avg_execution_time'])->toBeInstanceOf(Duration::class);
|
|
});
|
|
|
|
test('performance metrics repository can find slow operations', function () {
|
|
$slowThreshold = Duration::fromMilliseconds(1500);
|
|
|
|
$metrics = [
|
|
PerformanceMetric::fromPerformanceSnapshot(
|
|
'fast_migration',
|
|
'migration_execution',
|
|
PerformanceCategory::DATABASE,
|
|
Duration::fromMilliseconds(500), // Fast
|
|
Byte::fromBytes(50 * 1024 * 1024),
|
|
Byte::fromBytes(55 * 1024 * 1024),
|
|
Byte::fromBytes(60 * 1024 * 1024),
|
|
Byte::fromBytes(5 * 1024 * 1024),
|
|
true
|
|
),
|
|
PerformanceMetric::fromPerformanceSnapshot(
|
|
'slow_migration',
|
|
'migration_execution',
|
|
PerformanceCategory::DATABASE,
|
|
Duration::fromMilliseconds(3000), // Slow
|
|
Byte::fromBytes(60 * 1024 * 1024),
|
|
Byte::fromBytes(70 * 1024 * 1024),
|
|
Byte::fromBytes(75 * 1024 * 1024),
|
|
Byte::fromBytes(10 * 1024 * 1024),
|
|
true
|
|
),
|
|
];
|
|
|
|
$this->performanceMetricsRepository->saveBatch($metrics);
|
|
|
|
$slowOperations = $this->performanceMetricsRepository->findSlowOperations($slowThreshold);
|
|
|
|
expect($slowOperations)->toHaveCount(1);
|
|
expect($slowOperations[0]->operationId)->toBe('slow_migration');
|
|
expect($slowOperations[0]->executionTime->toMilliseconds())->toBe(3000);
|
|
});
|
|
});
|
|
|
|
// Test fixtures
|
|
class PerformanceTestMigration implements Migration
|
|
{
|
|
private bool $executed = false;
|
|
|
|
public function up(ConnectionInterface $connection): void
|
|
{
|
|
$this->executed = true;
|
|
// Simulate migration execution
|
|
$connection->execute(SqlQuery::create('CREATE TABLE performance_test (id INT)'));
|
|
}
|
|
|
|
public function down(ConnectionInterface $connection): void
|
|
{
|
|
$connection->execute(SqlQuery::create('DROP TABLE performance_test'));
|
|
}
|
|
|
|
public function getDescription(): string
|
|
{
|
|
return 'Performance Test Migration';
|
|
}
|
|
|
|
public function getVersion(): MigrationVersion
|
|
{
|
|
return MigrationVersion::fromTimestamp('2024_01_01_000000');
|
|
}
|
|
|
|
public function wasExecuted(): bool
|
|
{
|
|
return $this->executed;
|
|
}
|
|
}
|
|
|
|
class PerformanceMetricsFailingTestMigration implements Migration
|
|
{
|
|
public function up(ConnectionInterface $connection): void
|
|
{
|
|
throw new \Exception('Test migration failure');
|
|
}
|
|
|
|
public function down(ConnectionInterface $connection): void
|
|
{
|
|
// Empty
|
|
}
|
|
|
|
public function getDescription(): string
|
|
{
|
|
return 'Failing Test Migration';
|
|
}
|
|
|
|
public function getVersion(): MigrationVersion
|
|
{
|
|
return MigrationVersion::fromTimestamp('2024_01_01_000001');
|
|
}
|
|
}
|
|
|
|
class PerformanceMetricsMockConnection implements ConnectionInterface
|
|
{
|
|
private array $queries = [];
|
|
|
|
private array $appliedMigrations = [];
|
|
|
|
private bool $inTransaction = false;
|
|
|
|
public function setAppliedMigrations(array $migrations): void
|
|
{
|
|
$this->appliedMigrations = $migrations;
|
|
}
|
|
|
|
public function execute(SqlQuery $query): int
|
|
{
|
|
$this->queries[] = ['type' => 'execute', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];
|
|
|
|
return 1;
|
|
}
|
|
|
|
public function query(SqlQuery $query): ResultInterface
|
|
{
|
|
$this->queries[] = ['type' => 'query', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];
|
|
|
|
return new PerformanceMetricsMockResult($this->appliedMigrations);
|
|
}
|
|
|
|
public function queryOne(SqlQuery $query): ?array
|
|
{
|
|
return null;
|
|
}
|
|
|
|
public function queryColumn(SqlQuery $query): array
|
|
{
|
|
$this->queries[] = ['type' => 'queryColumn', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];
|
|
|
|
return array_column($this->appliedMigrations, 'version');
|
|
}
|
|
|
|
public function queryScalar(SqlQuery $query): mixed
|
|
{
|
|
$this->queries[] = ['type' => 'queryScalar', 'sql' => $query->sql, 'params' => $query->parameters->toArray()];
|
|
|
|
// Return '1' for connectivity test (SELECT 1)
|
|
if (str_contains($query->sql, 'SELECT 1')) {
|
|
return '1';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function beginTransaction(): void
|
|
{
|
|
$this->inTransaction = true;
|
|
}
|
|
|
|
public function commit(): void
|
|
{
|
|
$this->inTransaction = false;
|
|
}
|
|
|
|
public function rollback(): void
|
|
{
|
|
$this->inTransaction = false;
|
|
}
|
|
|
|
public function inTransaction(): bool
|
|
{
|
|
return $this->inTransaction;
|
|
}
|
|
|
|
public function lastInsertId(): string
|
|
{
|
|
return '1';
|
|
}
|
|
|
|
public function getPdo(): \PDO
|
|
{
|
|
return new class () extends \PDO {
|
|
public function __construct()
|
|
{
|
|
// Skip parent constructor to avoid actual DB connection
|
|
}
|
|
|
|
public function getAttribute(int $attribute): mixed
|
|
{
|
|
return match($attribute) {
|
|
\PDO::ATTR_DRIVER_NAME => 'mysql',
|
|
default => null
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
public function getQueries(): array
|
|
{
|
|
return $this->queries;
|
|
}
|
|
}
|
|
|
|
class PerformanceMetricsMockResult implements ResultInterface
|
|
{
|
|
private array $data;
|
|
|
|
public function __construct(array $data)
|
|
{
|
|
$this->data = $data;
|
|
}
|
|
|
|
public function fetch(): ?array
|
|
{
|
|
return $this->data[0] ?? null;
|
|
}
|
|
|
|
public function fetchAll(): array
|
|
{
|
|
return $this->data;
|
|
}
|
|
|
|
public function fetchOne(): ?array
|
|
{
|
|
return $this->data[0] ?? null;
|
|
}
|
|
|
|
public function fetchColumn(int $column = 0): array
|
|
{
|
|
return array_column($this->data, $column);
|
|
}
|
|
|
|
public function fetchScalar(): mixed
|
|
{
|
|
$row = $this->fetchOne();
|
|
|
|
return $row ? array_values($row)[0] : null;
|
|
}
|
|
|
|
public function rowCount(): int
|
|
{
|
|
return count($this->data);
|
|
}
|
|
|
|
public function getIterator(): \Traversable
|
|
{
|
|
return new \ArrayIterator($this->data);
|
|
}
|
|
|
|
public function count(): int
|
|
{
|
|
return count($this->data);
|
|
}
|
|
}
|
|
|
|
class PerformanceMetricsMockQueryBuilderFactory
|
|
{
|
|
private array $insertedData = [];
|
|
|
|
public function create(ConnectionInterface $connection): QueryBuilder
|
|
{
|
|
return new PerformanceMetricsMockQueryBuilder($this);
|
|
}
|
|
|
|
public function recordInsert(string $table, array $data): void
|
|
{
|
|
if (! isset($this->insertedData[$table])) {
|
|
$this->insertedData[$table] = [];
|
|
}
|
|
$this->insertedData[$table][] = $data;
|
|
}
|
|
|
|
public function getInsertedData(string $table): array
|
|
{
|
|
return $this->insertedData[$table] ?? [];
|
|
}
|
|
}
|
|
|
|
class PerformanceMetricsMockQueryBuilder implements QueryBuilder
|
|
{
|
|
private string $tableName = '';
|
|
|
|
private array $wheres = [];
|
|
|
|
private string $orderBy = '';
|
|
|
|
private ?int $limitValue = null;
|
|
|
|
public function __construct(private PerformanceMetricsMockQueryBuilderFactory $factory)
|
|
{
|
|
}
|
|
|
|
public function table(string $table): self
|
|
{
|
|
$this->tableName = $table;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function select(array $columns): self
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
public function where(string $column, string $operator, mixed $value): self
|
|
{
|
|
$this->wheres[] = [$column, $operator, $value];
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function orderBy(string $column, string $direction = 'ASC'): self
|
|
{
|
|
$this->orderBy = "$column $direction";
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function limit(int $limit): self
|
|
{
|
|
$this->limitValue = $limit;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function groupBy(string $column): self
|
|
{
|
|
return $this;
|
|
}
|
|
|
|
public function get(): array
|
|
{
|
|
// Mock return based on table and conditions
|
|
if ($this->tableName === 'performance_metrics') {
|
|
return $this->factory->getInsertedData('performance_metrics');
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
public function first(): ?array
|
|
{
|
|
$results = $this->get();
|
|
|
|
return $results[0] ?? null;
|
|
}
|
|
|
|
public function count(): int
|
|
{
|
|
return count($this->get());
|
|
}
|
|
|
|
public function average(string $column): ?float
|
|
{
|
|
$results = $this->get();
|
|
if (empty($results)) {
|
|
return null;
|
|
}
|
|
|
|
$values = array_column($results, $column);
|
|
|
|
return array_sum($values) / count($values);
|
|
}
|
|
|
|
public function insert(array $data): int
|
|
{
|
|
$this->factory->recordInsert($this->tableName, $data);
|
|
|
|
return 1; // Mock insert ID
|
|
}
|
|
|
|
public function insertBatch(array $data): int
|
|
{
|
|
foreach ($data as $row) {
|
|
$this->factory->recordInsert($this->tableName, $row);
|
|
}
|
|
|
|
return count($data);
|
|
}
|
|
|
|
public function update(array $data): int
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
public function delete(): int
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
public function toSql(): string
|
|
{
|
|
return "SELECT * FROM {$this->tableName}";
|
|
}
|
|
|
|
public function getParameters(): array
|
|
{
|
|
return [];
|
|
}
|
|
|
|
public function execute(): mixed
|
|
{
|
|
return $this->get();
|
|
}
|
|
}
|