Files
michaelschiemer/src/Framework/Database/Commands/DatabaseOptimizeCommand.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

380 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Database\Commands;
use App\Framework\Console\ConsoleCommand;
use App\Framework\Console\ExitCode;
use App\Framework\Database\DatabaseManager;
use App\Framework\Database\Driver\Optimization\DatabaseOptimizer;
use App\Framework\Database\Driver\Optimization\MySQLOptimizer;
use App\Framework\Database\Driver\Optimization\PostgreSQLOptimizer;
use App\Framework\Database\Driver\Optimization\SQLiteOptimizer;
/**
* Command to optimize database tables and perform maintenance operations
*/
final readonly class DatabaseOptimizeCommand
{
public function __construct(
private DatabaseManager $databaseManager
) {
}
/**
* Optimize database tables
*
* @param string $connection The database connection to use
* @param string|null $table The table to optimize, or null for all tables
* @return ExitCode
*/
#[ConsoleCommand('db:optimize', 'Optimize database tables')]
public function optimize(string $connection = 'default', ?string $table = null): ExitCode
{
try {
$optimizer = $this->getOptimizer($connection);
echo "Optimizing tables...\n";
$results = $optimizer->optimizeTables($table);
$this->displayResults($results);
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
return ExitCode::SOFTWARE_ERROR;
}
}
/**
* Analyze database tables
*
* @param string $connection The database connection to use
* @param string|null $table The table to analyze, or null for all tables
* @return ExitCode
*/
#[ConsoleCommand('db:analyze', 'Analyze database tables')]
public function analyze(string $connection = 'default', ?string $table = null): ExitCode
{
try {
$optimizer = $this->getOptimizer($connection);
echo "Analyzing tables...\n";
$results = $optimizer->analyzeTables($table);
$this->displayResults($results);
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
return ExitCode::SOFTWARE_ERROR;
}
}
/**
* Check database tables
*
* @param string $connection The database connection to use
* @param string|null $table The table to check, or null for all tables
* @param bool $extended Whether to perform an extended check
* @return ExitCode
*/
#[ConsoleCommand('db:check', 'Check database tables for errors')]
public function check(string $connection = 'default', ?string $table = null, bool $extended = false): ExitCode
{
try {
$optimizer = $this->getOptimizer($connection);
echo "Checking tables...\n";
$results = $optimizer->checkTables($table, $extended);
$this->displayResults($results);
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
return ExitCode::SOFTWARE_ERROR;
}
}
/**
* Show table status
*
* @param string $connection The database connection to use
* @param string|null $table The table to show status for, or null for all tables
* @return ExitCode
*/
#[ConsoleCommand('db:status', 'Show database table status')]
public function status(string $connection = 'default', ?string $table = null): ExitCode
{
try {
$optimizer = $this->getOptimizer($connection);
echo "Table status:\n";
$results = $optimizer->getTableStatus($table);
foreach ($results as $tableName => $status) {
echo "\nTable: {$tableName}\n";
echo str_repeat('-', 80) . "\n";
// Format the status information
$this->displayTableStatus($status);
}
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
return ExitCode::SOFTWARE_ERROR;
}
}
/**
* Show index statistics
*
* @param string $connection The database connection to use
* @param string|null $table The table to show index statistics for, or null for all tables
* @return ExitCode
*/
#[ConsoleCommand('db:indexes', 'Show database index statistics')]
public function indexes(string $connection = 'default', ?string $table = null): ExitCode
{
try {
$optimizer = $this->getOptimizer($connection);
echo "Index statistics:\n";
$results = $optimizer->getIndexStatistics($table);
foreach ($results as $tableName => $indexes) {
echo "\nTable: {$tableName}\n";
echo str_repeat('-', 80) . "\n";
if (empty($indexes)) {
echo "No indexes found.\n";
continue;
}
foreach ($indexes as $indexName => $indexInfo) {
echo " Index: {$indexName}\n";
// Format the index information
$this->displayIndexInfo($indexInfo);
echo "\n";
}
}
return ExitCode::SUCCESS;
} catch (\Throwable $e) {
echo "Error: {$e->getMessage()}\n";
return ExitCode::SOFTWARE_ERROR;
}
}
/**
* Get the appropriate optimizer for the database connection
*
* @param string $connection The database connection name
* @return DatabaseOptimizer The database optimizer
* @throws \RuntimeException If the database type is not supported
*/
private function getOptimizer(string $connection): DatabaseOptimizer
{
$conn = $this->databaseManager->getConnection($connection);
$driver = $conn->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
return match ($driver) {
'mysql' => new MySQLOptimizer($conn),
'pgsql' => new PostgreSQLOptimizer($conn),
'sqlite' => new SQLiteOptimizer($conn),
default => throw new \RuntimeException("Unsupported database driver: {$driver}")
};
}
/**
* Display the results of an operation
*
* @param array<string, string> $results The results to display
*/
private function displayResults(array $results): void
{
foreach ($results as $name => $result) {
echo " {$name}: {$result}\n";
}
}
/**
* Display table status information
*
* @param array<string, mixed> $status The table status information
*/
private function displayTableStatus(array $status): void
{
// Handle error case
if (isset($status['error'])) {
echo " Error: {$status['error']}\n";
return;
}
// Display basic information
if (isset($status['row_count'])) {
echo " Rows: " . number_format($status['row_count']) . "\n";
}
if (isset($status['columns'])) {
echo " Columns: {$status['columns']}\n";
}
// Display size information
if (isset($status['total_size'])) {
echo " Total size: {$status['total_size']}\n";
} elseif (isset($status['Data_length']) && isset($status['Index_length'])) {
$totalSize = $status['Data_length'] + $status['Index_length'];
echo " Total size: " . $this->formatBytes($totalSize) . "\n";
echo " Data size: " . $this->formatBytes($status['Data_length']) . "\n";
echo " Index size: " . $this->formatBytes($status['Index_length']) . "\n";
}
// Display engine information for MySQL
if (isset($status['Engine'])) {
echo " Engine: {$status['Engine']}\n";
}
// Display row format for MySQL
if (isset($status['Row_format'])) {
echo " Row format: {$status['Row_format']}\n";
}
// Display collation for MySQL
if (isset($status['Collation'])) {
echo " Collation: {$status['Collation']}\n";
}
// Display index count
if (isset($status['indexes']) && is_array($status['indexes'])) {
echo " Indexes: " . count($status['indexes']) . "\n";
}
// Display foreign key count
if (isset($status['foreign_keys']) && is_array($status['foreign_keys'])) {
echo " Foreign keys: " . count($status['foreign_keys']) . "\n";
}
// Display last update time for MySQL
if (isset($status['Update_time']) && $status['Update_time']) {
echo " Last updated: {$status['Update_time']}\n";
}
// Display auto increment value for MySQL
if (isset($status['Auto_increment']) && $status['Auto_increment']) {
echo " Auto increment: " . number_format($status['Auto_increment']) . "\n";
}
// Display PostgreSQL-specific information
if (isset($status['dead_rows']) && isset($status['row_count']) && $status['row_count'] > 0) {
$deadRowPercentage = round(($status['dead_rows'] / $status['row_count']) * 100, 2);
echo " Dead rows: {$status['dead_rows']} ({$deadRowPercentage}%)\n";
}
if (isset($status['last_vacuum'])) {
echo " Last vacuum: {$status['last_vacuum']}\n";
}
if (isset($status['last_analyze'])) {
echo " Last analyze: {$status['last_analyze']}\n";
}
}
/**
* Display index information
*
* @param array<string, mixed> $indexInfo The index information
*/
private function displayIndexInfo(array $indexInfo): void
{
// Handle error case
if (isset($indexInfo['error'])) {
echo " Error: {$indexInfo['error']}\n";
return;
}
// Display index type
if (isset($indexInfo['type'])) {
echo " Type: {$indexInfo['type']}\n";
} elseif (isset($indexInfo['index_type'])) {
echo " Type: {$indexInfo['index_type']}\n";
}
// Display uniqueness
if (isset($indexInfo['unique'])) {
echo " Unique: " . ($indexInfo['unique'] ? 'Yes' : 'No') . "\n";
} elseif (isset($indexInfo['is_unique'])) {
echo " Unique: " . ($indexInfo['is_unique'] ? 'Yes' : 'No') . "\n";
} elseif (isset($indexInfo['Non_unique'])) {
echo " Unique: " . ($indexInfo['Non_unique'] == 0 ? 'Yes' : 'No') . "\n";
}
// Display primary key status
if (isset($indexInfo['is_primary'])) {
echo " Primary key: " . ($indexInfo['is_primary'] ? 'Yes' : 'No') . "\n";
}
// Display columns
if (isset($indexInfo['columns']) && is_array($indexInfo['columns'])) {
echo " Columns: " . implode(', ', $indexInfo['columns']) . "\n";
} elseif (isset($indexInfo['Column_name'])) {
echo " Column: {$indexInfo['Column_name']}\n";
}
// Display cardinality
if (isset($indexInfo['cardinality'])) {
echo " Cardinality: " . number_format($indexInfo['cardinality']) . "\n";
} elseif (isset($indexInfo['Cardinality'])) {
echo " Cardinality: " . number_format($indexInfo['Cardinality']) . "\n";
}
// Display index size
if (isset($indexInfo['index_size'])) {
echo " Size: {$indexInfo['index_size']}\n";
}
// Display PostgreSQL-specific information
if (isset($indexInfo['index_scans'])) {
echo " Scans: " . number_format($indexInfo['index_scans']) . "\n";
}
if (isset($indexInfo['tuples_read']) && isset($indexInfo['tuples_fetched'])) {
echo " Tuples read: " . number_format($indexInfo['tuples_read']) . "\n";
echo " Tuples fetched: " . number_format($indexInfo['tuples_fetched']) . "\n";
}
}
/**
* Format bytes to a human-readable string
*
* @param int $bytes The number of bytes
* @param int $precision The number of decimal places
* @return string The formatted string
*/
private function formatBytes(int $bytes, int $precision = 2): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
}
}