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:
@@ -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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user