Files
michaelschiemer/src/Framework/Database/Indexing/IndexMigrationGenerator.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

278 lines
8.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Database\Indexing;
use App\Framework\Database\Indexing\ValueObjects\IndexRecommendation;
use App\Framework\Database\PdoConnection;
/**
* Generates database migration files for index optimizations
*
* Creates both UP and DOWN migrations for safe index management
*/
final readonly class IndexMigrationGenerator
{
private const MIGRATION_TEMPLATE = <<<'PHP'
<?php
declare(strict_types=1);
use App\Framework\Database\Migration\Migration;
use App\Framework\Database\Schema\Schema;
final class {CLASS_NAME} extends Migration
{
public function up(Schema $schema): void
{
// {DESCRIPTION}
$schema->table('{TABLE_NAME}', function ($table) {
{UP_STATEMENTS}
});
}
public function down(Schema $schema): void
{
$schema->table('{TABLE_NAME}', function ($table) {
{DOWN_STATEMENTS}
});
}
}
PHP;
public function __construct(
private PdoConnection $connection
) {}
/**
* Generate migration file for adding recommended indexes
*
* @param array<IndexRecommendation> $recommendations
*/
public function generateAddIndexMigration(array $recommendations, string $tableName): string
{
if (empty($recommendations)) {
throw new \InvalidArgumentException('No index recommendations provided');
}
$className = $this->generateClassName('AddIndexesTo' . ucfirst($tableName));
$upStatements = [];
$downStatements = [];
foreach ($recommendations as $recommendation) {
$indexName = $recommendation->getIndexName()->toString();
$columns = $recommendation->columns;
$upStatements[] = $this->generateAddIndexStatement($indexName, $columns, $recommendation->indexType->value);
$downStatements[] = $this->generateDropIndexStatement($indexName);
}
$description = 'Add optimized indexes based on query analysis';
return $this->fillTemplate(
className: $className,
tableName: $tableName,
description: $description,
upStatements: $upStatements,
downStatements: $downStatements
);
}
/**
* Generate migration file for removing unused indexes
*
* @param array<array{index_name: string, columns: array<string>}> $unusedIndexes
*/
public function generateRemoveIndexMigration(array $unusedIndexes, string $tableName): string
{
if (empty($unusedIndexes)) {
throw new \InvalidArgumentException('No unused indexes provided');
}
$className = $this->generateClassName('RemoveUnusedIndexesFrom' . ucfirst($tableName));
$upStatements = [];
$downStatements = [];
foreach ($unusedIndexes as $index) {
$indexName = $index['index_name'];
$columns = $index['columns'];
$upStatements[] = $this->generateDropIndexStatement($indexName);
$downStatements[] = $this->generateAddIndexStatement($indexName, $columns, 'BTREE');
}
$description = 'Remove unused indexes identified by analysis';
return $this->fillTemplate(
className: $className,
tableName: $tableName,
description: $description,
upStatements: $upStatements,
downStatements: $downStatements
);
}
/**
* Generate comprehensive optimization migration (add + remove)
*
* @param array<IndexRecommendation> $toAdd
* @param array<array{index_name: string, columns: array<string>}> $toRemove
*/
public function generateOptimizationMigration(array $toAdd, array $toRemove, string $tableName): string
{
$className = $this->generateClassName('OptimizeIndexesFor' . ucfirst($tableName));
$upStatements = [];
$downStatements = [];
// First, drop unused indexes
foreach ($toRemove as $index) {
$indexName = $index['index_name'];
$columns = $index['columns'];
$upStatements[] = $this->generateDropIndexStatement($indexName);
$downStatements[] = $this->generateAddIndexStatement($indexName, $columns, 'BTREE');
}
// Then, add recommended indexes
foreach ($toAdd as $recommendation) {
$indexName = $recommendation->getIndexName()->toString();
$columns = $recommendation->columns;
$upStatements[] = $this->generateAddIndexStatement($indexName, $columns, $recommendation->indexType->value);
$downStatements[] = $this->generateDropIndexStatement($indexName);
}
// Reverse down statements to maintain proper order
$downStatements = array_reverse($downStatements);
$description = 'Optimize indexes: remove unused, add recommended';
return $this->fillTemplate(
className: $className,
tableName: $tableName,
description: $description,
upStatements: $upStatements,
downStatements: $downStatements
);
}
/**
* Save migration file to disk
*/
public function saveMigration(string $migrationContent, ?string $customPath = null): string
{
$timestamp = date('YmdHis');
$className = $this->extractClassName($migrationContent);
$snakeCaseName = $this->camelToSnake($className);
$filename = "{$timestamp}_{$snakeCaseName}.php";
$path = $customPath ?? __DIR__ . '/../../../migrations/' . $filename;
// Ensure directory exists
$directory = dirname($path);
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
file_put_contents($path, $migrationContent);
return $path;
}
/**
* Generate migration class name with timestamp
*/
private function generateClassName(string $baseName): string
{
return 'Migration' . date('YmdHis') . $baseName;
}
/**
* Generate ADD INDEX statement
*/
private function generateAddIndexStatement(string $indexName, array $columns, string $type = 'BTREE'): string
{
$columnsStr = implode(', ', array_map(fn($col) => "'{$col}'", $columns));
return "\$table->index([{$columnsStr}], '{$indexName}', '{$type}');";
}
/**
* Generate DROP INDEX statement
*/
private function generateDropIndexStatement(string $indexName): string
{
return "\$table->dropIndex('{$indexName}');";
}
/**
* Fill migration template with actual data
*/
private function fillTemplate(
string $className,
string $tableName,
string $description,
array $upStatements,
array $downStatements
): string {
$upStatementsStr = implode("\n", array_map(
fn($stmt) => " {$stmt}",
$upStatements
));
$downStatementsStr = implode("\n", array_map(
fn($stmt) => " {$stmt}",
$downStatements
));
return str_replace(
['{CLASS_NAME}', '{TABLE_NAME}', '{DESCRIPTION}', '{UP_STATEMENTS}', '{DOWN_STATEMENTS}'],
[$className, $tableName, $description, $upStatementsStr, $downStatementsStr],
self::MIGRATION_TEMPLATE
);
}
/**
* Extract class name from migration content
*/
private function extractClassName(string $content): string
{
if (preg_match('/final class (\w+) extends Migration/', $content, $matches)) {
return $matches[1];
}
throw new \RuntimeException('Could not extract class name from migration');
}
/**
* Convert camelCase to snake_case
*/
private function camelToSnake(string $input): string
{
return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $input));
}
/**
* Validate migration content before saving
*/
private function validateMigration(string $content): bool
{
// Basic validation
if (!str_contains($content, 'extends Migration')) {
throw new \RuntimeException('Migration must extend Migration class');
}
if (!str_contains($content, 'public function up(')) {
throw new \RuntimeException('Migration must have up() method');
}
if (!str_contains($content, 'public function down(')) {
throw new \RuntimeException('Migration must have down() method');
}
return true;
}
}