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
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -8,48 +8,81 @@ use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Exception\DatabaseException;
use App\Framework\Database\Transaction;
final class MigrationRunner
final readonly class MigrationRunner
{
private ConnectionInterface $connection;
private string $migrationsTable;
/**
* @var MigrationDependencyGraph Dependency graph for migrations
*/
private MigrationDependencyGraph $dependencyGraph;
public function __construct(ConnectionInterface $connection, string $migrationsTable = 'migrations')
{
$this->connection = $connection;
$this->migrationsTable = $migrationsTable;
public function __construct(
private ConnectionInterface $connection,
private string $migrationsTable = 'migrations'
) {
$this->ensureMigrationsTable();
$this->dependencyGraph = new MigrationDependencyGraph();
}
/**
* @param Migration[] $migrations
* Run migrations
*
* @param MigrationCollection $migrations The migrations to run
* @return array<string> List of executed migration versions
*/
public function migrate(array $migrations): array
public function migrate(MigrationCollection $migrations): array
{
$executedMigrations = [];
$appliedVersions = $this->getAppliedVersions();
foreach ($migrations as $migration) {
if (in_array($migration->getVersion(), $appliedVersions, true)) {
// Build the dependency graph
$this->dependencyGraph->buildGraph($migrations);
// Get migrations in the correct execution order based on dependencies
$orderedMigrations = $this->dependencyGraph->getExecutionOrder();
foreach ($orderedMigrations as $migration) {
$version = $migration->getVersion()->toString();
if ($appliedVersions->containsString($version)) {
continue;
}
// Check if all dependencies are applied
if ($migration instanceof DependentMigration) {
$missingDependencies = [];
foreach ($migration->getDependencies() as $dependencyVersion) {
$dependencyVersionString = $dependencyVersion->toString();
if (! $appliedVersions->containsString($dependencyVersionString) &&
! in_array($dependencyVersionString, $executedMigrations)) {
$missingDependencies[] = $dependencyVersionString;
}
}
if (! empty($missingDependencies)) {
throw new DatabaseException(
"Cannot apply migration {$version} because it depends on migrations that have not been applied: " .
implode(', ', $missingDependencies)
);
}
}
try {
Transaction::run($this->connection, function() use ($migration) {
echo "Migrating: {$migration->getVersion()} - {$migration->getDescription()}\n";
Transaction::run($this->connection, function () use ($migration, $version) {
echo "Migrating: {$version} - {$migration->getDescription()}\n";
$migration->up($this->connection);
$this->connection->execute(
"INSERT INTO {$this->migrationsTable} (version, description, executed_at) VALUES (?, ?, ?)",
[$migration->getVersion(), $migration->getDescription(), date('Y-m-d H:i:s')]
[$version, $migration->getDescription(), date('Y-m-d H:i:s')]
);
});
$executedMigrations[] = $migration->getVersion();
echo "Migrated: {$migration->getVersion()}\n";
$executedMigrations[] = $version;
echo "Migrated: {$version}\n";
} catch (\Throwable $e) {
throw new DatabaseException(
"Migration {$migration->getVersion()} failed: {$e->getMessage()}",
"Migration {$version} failed: {$e->getMessage()}",
0,
$e
);
@@ -60,86 +93,164 @@ final class MigrationRunner
}
/**
* @param Migration[] $migrations
* Rollback migrations
*
* @param MigrationCollection $migrations The migrations to rollback from
* @param int $steps Number of migrations to rollback
* @return MigrationCollection List of rolled back migration versions
*/
public function rollback(array $migrations, int $steps = 1): array
public function rollback(MigrationCollection $migrations, int $steps = 1): MigrationCollection
{
$rolledBackMigrations = [];
$appliedVersions = $this->getAppliedVersions();
$sortedMigrations = $migrations;
usort($sortedMigrations, fn($a, $b) => $b->getVersion() <=> $a->getVersion());
// Build the dependency graph
$this->dependencyGraph->buildGraph($migrations);
// Use the collection's sortedDescending method to get migrations in reverse order
$sortedMigrations = $migrations->sortedDescending();
// Create a map of version strings to migrations for easy lookup
$migrationMap = [];
foreach ($migrations as $migration) {
$migrationMap[$migration->getVersion()->toString()] = $migration;
}
// Create a list of versions that can be rolled back
$versionsToRollback = [];
$count = 0;
foreach ($sortedMigrations as $migration) {
if ($count >= $steps) {
break;
}
if (!in_array($migration->getVersion(), $appliedVersions, true)) {
$version = $migration->getVersion()->toString();
if (! $appliedVersions->containsString($version)) {
continue;
}
// Check if any applied migrations depend on this one
$dependants = $this->dependencyGraph->getDependants($version);
$appliedDependants = [];
foreach ($dependants as $dependant) {
if ($appliedVersions->containsString($dependant) && ! in_array($dependant, $versionsToRollback)) {
$appliedDependants[] = $dependant;
}
}
// If there are applied dependants, we can't roll back this migration
if (! empty($appliedDependants)) {
echo "Cannot roll back {$version} because the following migrations depend on it: " . implode(', ', $appliedDependants) . "\n";
continue;
}
$versionsToRollback[] = $version;
$count++;
}
// Roll back the migrations in the correct order
foreach ($versionsToRollback as $version) {
$migration = $migrationMap[$version];
try {
Transaction::run($this->connection, function() use ($migration) {
echo "Rolling back: {$migration->getVersion()} - {$migration->getDescription()}\n";
Transaction::run($this->connection, function () use ($migration, $version) {
echo "Rolling back: {$version} - {$migration->getDescription()}\n";
$migration->down($this->connection);
$this->connection->execute(
"DELETE FROM {$this->migrationsTable} WHERE version = ?",
[$migration->getVersion()]
[$version]
);
});
$rolledBackMigrations[] = $migration->getVersion();
$count++;
echo "Rolled back: {$migration->getVersion()}\n";
$rolledBackMigrations[] = $migration;
echo "Rolled back: {$version}\n";
} catch (\Throwable $e) {
throw new DatabaseException(
"Rollback {$migration->getVersion()} failed: {$e->getMessage()}",
"Rollback {$version} failed: {$e->getMessage()}",
0,
$e
);
}
}
return $rolledBackMigrations;
return new MigrationCollection(...$rolledBackMigrations);
}
public function getStatus(array $migrations): array
/**
* Get status of all migrations
*
* @param MigrationCollection $migrations The migrations to check status for
* @return MigrationStatusCollection Collection of migration statuses
*/
public function getStatus(MigrationCollection $migrations): MigrationStatusCollection
{
$appliedVersions = $this->getAppliedVersions();
$status = [];
$statuses = [];
foreach ($migrations as $migration) {
$status[] = [
'version' => $migration->getVersion(),
'description' => $migration->getDescription(),
'applied' => in_array($migration->getVersion(), $appliedVersions, true),
];
$version = $migration->getVersion();
$applied = $appliedVersions->contains($version);
$statuses[] = $applied
? MigrationStatus::applied($version, $migration->getDescription())
: MigrationStatus::pending($version, $migration->getDescription());
}
return $status;
return new MigrationStatusCollection($statuses);
}
private function getAppliedVersions(): array
public function getAppliedVersions(): MigrationVersionCollection
{
return $this->connection->queryColumn(
$versionStrings = $this->connection->queryColumn(
"SELECT version FROM {$this->migrationsTable} ORDER BY executed_at"
);
return MigrationVersionCollection::fromStrings($versionStrings);
}
private function ensureMigrationsTable(): void
{
$sql = "CREATE TABLE IF NOT EXISTS {$this->migrationsTable} (
id INT PRIMARY KEY AUTO_INCREMENT,
version VARCHAR(255) NOT NULL UNIQUE,
description TEXT,
executed_at DATETIME NOT NULL,
INDEX idx_version (version)
)";
// Use database-agnostic approach
$driver = $this->connection->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME);
$sql = match($driver) {
'mysql' => "CREATE TABLE IF NOT EXISTS {$this->migrationsTable} (
id INT PRIMARY KEY AUTO_INCREMENT,
version VARCHAR(20) NOT NULL UNIQUE COMMENT 'Format: YYYY_MM_DD_HHMMSS',
description TEXT,
executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_version (version),
INDEX idx_executed_at (executed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
'pgsql' => "CREATE TABLE IF NOT EXISTS {$this->migrationsTable} (
id SERIAL PRIMARY KEY,
version VARCHAR(20) NOT NULL UNIQUE,
description TEXT,
executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)",
'sqlite' => "CREATE TABLE IF NOT EXISTS {$this->migrationsTable} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version TEXT NOT NULL UNIQUE,
description TEXT,
executed_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
)",
default => throw new \RuntimeException("Unsupported database driver: {$driver}")
};
$this->connection->execute($sql);
// Create indexes for PostgreSQL separately
if ($driver === 'pgsql') {
$this->connection->execute("CREATE INDEX IF NOT EXISTS idx_{$this->migrationsTable}_version ON {$this->migrationsTable} (version)");
$this->connection->execute("CREATE INDEX IF NOT EXISTS idx_{$this->migrationsTable}_executed_at ON {$this->migrationsTable} (executed_at)");
}
}
}