Compare commits
No commits in common. "c38e5ef4a683b8995ef1eff7dffbb57a298c557a" and "785e9a84eb4a5902cd51d070d5e6185d908b9072" have entirely different histories.
c38e5ef4a6
...
785e9a84eb
|
|
@ -1,21 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Core;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class MigrationException extends Exception
|
|
||||||
{
|
|
||||||
private string $migration;
|
|
||||||
|
|
||||||
public function __construct(string $migration, string $message, ?Exception $previous = null)
|
|
||||||
{
|
|
||||||
$this->migration = $migration;
|
|
||||||
parent::__construct($message, 0, $previous);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMigration(): string
|
|
||||||
{
|
|
||||||
return $this->migration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,9 +2,6 @@
|
||||||
|
|
||||||
namespace App\Core;
|
namespace App\Core;
|
||||||
|
|
||||||
require_once __DIR__ . '/NullLogger.php';
|
|
||||||
require_once __DIR__ . '/MigrationException.php';
|
|
||||||
|
|
||||||
use PDO;
|
use PDO;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
|
|
@ -12,10 +9,6 @@ class MigrationRunner
|
||||||
{
|
{
|
||||||
private PDO $pdo;
|
private PDO $pdo;
|
||||||
private string $migrationsDir;
|
private string $migrationsDir;
|
||||||
private string $driver;
|
|
||||||
private bool $isSqlite = false;
|
|
||||||
private $logger;
|
|
||||||
private array $lastResults = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed $db Either a PDO instance or the application's Database wrapper
|
* @param mixed $db Either a PDO instance or the application's Database wrapper
|
||||||
|
|
@ -40,77 +33,28 @@ class MigrationRunner
|
||||||
if (!is_dir($this->migrationsDir)) {
|
if (!is_dir($this->migrationsDir)) {
|
||||||
throw new Exception("Migrations directory not found: {$this->migrationsDir}");
|
throw new Exception("Migrations directory not found: {$this->migrationsDir}");
|
||||||
}
|
}
|
||||||
$this->driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
|
||||||
$this->isSqlite = ($this->driver === 'sqlite');
|
|
||||||
$this->ensureMigrationsTable();
|
$this->ensureMigrationsTable();
|
||||||
$this->ensureMigrationColumns();
|
|
||||||
$this->initializeLogger();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ensureMigrationsTable(): void
|
private function ensureMigrationsTable(): void
|
||||||
{
|
{
|
||||||
if ($this->isSqlite) {
|
$driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||||
|
if ($driver === 'sqlite') {
|
||||||
$sql = "CREATE TABLE IF NOT EXISTS migrations (
|
$sql = "CREATE TABLE IF NOT EXISTS migrations (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
migration TEXT NOT NULL UNIQUE,
|
migration TEXT NOT NULL UNIQUE,
|
||||||
applied_at TEXT NOT NULL,
|
applied_at TEXT NOT NULL
|
||||||
content_hash TEXT NULL,
|
|
||||||
content TEXT NULL
|
|
||||||
)";
|
)";
|
||||||
} else {
|
} else {
|
||||||
$sql = "CREATE TABLE IF NOT EXISTS migrations (
|
$sql = "CREATE TABLE IF NOT EXISTS migrations (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
migration VARCHAR(255) NOT NULL UNIQUE,
|
migration VARCHAR(255) NOT NULL UNIQUE,
|
||||||
applied_at DATETIME NOT NULL,
|
applied_at DATETIME NOT NULL
|
||||||
content_hash CHAR(64) NULL,
|
|
||||||
content LONGTEXT NULL
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
|
||||||
}
|
}
|
||||||
$this->pdo->exec($sql);
|
$this->pdo->exec($sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function ensureMigrationColumns(): void
|
|
||||||
{
|
|
||||||
$this->ensureColumnExists(
|
|
||||||
'content_hash',
|
|
||||||
$this->isSqlite ? "ALTER TABLE migrations ADD COLUMN content_hash TEXT NULL" : "ALTER TABLE migrations ADD COLUMN content_hash CHAR(64) NULL DEFAULT NULL AFTER applied_at"
|
|
||||||
);
|
|
||||||
$this->ensureColumnExists(
|
|
||||||
'content',
|
|
||||||
$this->isSqlite ? "ALTER TABLE migrations ADD COLUMN content TEXT NULL" : "ALTER TABLE migrations ADD COLUMN content LONGTEXT NULL DEFAULT NULL AFTER content_hash"
|
|
||||||
);
|
|
||||||
$this->ensureColumnExists(
|
|
||||||
'result',
|
|
||||||
$this->isSqlite ? "ALTER TABLE migrations ADD COLUMN result TEXT NULL" : "ALTER TABLE migrations ADD COLUMN result LONGTEXT NULL DEFAULT NULL AFTER content"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function ensureColumnExists(string $column, string $alterSql): void
|
|
||||||
{
|
|
||||||
if ($this->columnExists('migrations', $column)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$this->pdo->exec($alterSql);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function columnExists(string $table, string $column): bool
|
|
||||||
{
|
|
||||||
if ($this->isSqlite) {
|
|
||||||
$stmt = $this->pdo->query("PRAGMA table_info({$table})");
|
|
||||||
$columns = $stmt ? $stmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
|
||||||
foreach ($columns as $col) {
|
|
||||||
if (($col['name'] ?? '') === $column) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $this->pdo->prepare("SHOW COLUMNS FROM {$table} LIKE :column");
|
|
||||||
$stmt->execute([':column' => $column]);
|
|
||||||
return (bool)$stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function listAllMigrations(): array
|
public function listAllMigrations(): array
|
||||||
{
|
{
|
||||||
$files = glob($this->migrationsDir . '/*.sql');
|
$files = glob($this->migrationsDir . '/*.sql');
|
||||||
|
|
@ -128,8 +72,7 @@ class MigrationRunner
|
||||||
{
|
{
|
||||||
$all = $this->listAllMigrations();
|
$all = $this->listAllMigrations();
|
||||||
$applied = $this->listAppliedMigrations();
|
$applied = $this->listAppliedMigrations();
|
||||||
$pending = array_values(array_diff($all, $applied));
|
return array_values(array_diff($all, $applied));
|
||||||
return $this->sortMigrations($pending);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasPendingMigrations(): bool
|
public function hasPendingMigrations(): bool
|
||||||
|
|
@ -138,217 +81,39 @@ class MigrationRunner
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyPendingMigrations(): array
|
public function applyPendingMigrations(): array
|
||||||
{
|
|
||||||
return $this->runMigrations($this->listPendingMigrations());
|
|
||||||
}
|
|
||||||
|
|
||||||
public function applyNextMigration(): array
|
|
||||||
{
|
{
|
||||||
$pending = $this->listPendingMigrations();
|
$pending = $this->listPendingMigrations();
|
||||||
if (empty($pending)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return $this->runMigrations([reset($pending)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function applyMigrationByName(string $migration): array
|
|
||||||
{
|
|
||||||
$pending = $this->listPendingMigrations();
|
|
||||||
if (!in_array($migration, $pending, true)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return $this->runMigrations([$migration]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function runMigrations(array $migrations): array
|
|
||||||
{
|
|
||||||
$appliedNow = [];
|
$appliedNow = [];
|
||||||
if (empty($migrations)) {
|
if (empty($pending)) {
|
||||||
return $appliedNow;
|
return $appliedNow;
|
||||||
}
|
}
|
||||||
$this->lastResults = [];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->pdo->beginTransaction();
|
$this->pdo->beginTransaction();
|
||||||
foreach ($migrations as $migration) {
|
foreach ($pending as $migration) {
|
||||||
try {
|
$path = $this->migrationsDir . '/' . $migration;
|
||||||
$path = $this->migrationsDir . '/' . $migration;
|
$sql = file_get_contents($path);
|
||||||
$sql = file_get_contents($path);
|
if ($sql === false) {
|
||||||
if ($sql === false) {
|
throw new Exception("Unable to read migration file: {$migration}");
|
||||||
throw new Exception("Unable to read migration file: {$migration}");
|
|
||||||
}
|
|
||||||
$trimmedSql = trim($sql);
|
|
||||||
$hash = hash('sha256', $trimmedSql);
|
|
||||||
|
|
||||||
if ($this->contentHashExists($hash)) {
|
|
||||||
$this->recordMigration($migration, $trimmedSql, $hash);
|
|
||||||
$appliedNow[] = $migration;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$statements = $this->splitStatements($trimmedSql);
|
|
||||||
foreach ($statements as $stmtSql) {
|
|
||||||
if ($stmtSql === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$this->pdo->exec($stmtSql);
|
|
||||||
}
|
|
||||||
$statementCount = count($statements);
|
|
||||||
$resultMessage = sprintf('Migration "%s" applied successfully (%d statement%s).', $migration, $statementCount, $statementCount === 1 ? '' : 's');
|
|
||||||
$this->lastResults[$migration] = [
|
|
||||||
'content' => $trimmedSql,
|
|
||||||
'message' => $resultMessage,
|
|
||||||
'is_test' => $this->isTestMigration($migration)
|
|
||||||
];
|
|
||||||
if ($this->isTestMigration($migration)) {
|
|
||||||
$appliedNow[] = $migration;
|
|
||||||
$this->logger->log('info', $resultMessage . ' (test migration)', ['scope' => 'system', 'migration' => $migration]);
|
|
||||||
$this->cleanupTestMigrationFile($migration);
|
|
||||||
} else {
|
|
||||||
$this->recordMigration($migration, $trimmedSql, $hash, $resultMessage);
|
|
||||||
$appliedNow[] = $migration;
|
|
||||||
$this->logger->log('info', $resultMessage, ['scope' => 'system', 'migration' => $migration]);
|
|
||||||
}
|
|
||||||
} catch (Exception $migrationException) {
|
|
||||||
throw new MigrationException($migration, $migrationException->getMessage(), $migrationException);
|
|
||||||
}
|
}
|
||||||
|
// Split on ; at line ends, but allow inside procedures? Keep simple for our use-cases
|
||||||
|
$statements = array_filter(array_map('trim', preg_split('/;\s*\n/', $sql)));
|
||||||
|
foreach ($statements as $stmtSql) {
|
||||||
|
if ($stmtSql === '') continue;
|
||||||
|
$this->pdo->exec($stmtSql);
|
||||||
|
}
|
||||||
|
$ins = $this->pdo->prepare('INSERT INTO migrations (migration, applied_at) VALUES (:m, NOW())');
|
||||||
|
$ins->execute([':m' => $migration]);
|
||||||
|
$appliedNow[] = $migration;
|
||||||
}
|
}
|
||||||
$this->pdo->commit();
|
$this->pdo->commit();
|
||||||
} catch (MigrationException $e) {
|
|
||||||
if ($this->pdo->inTransaction()) {
|
|
||||||
$this->pdo->rollBack();
|
|
||||||
}
|
|
||||||
$this->logger->log('error', sprintf('Migration "%s" failed: %s', $e->getMigration(), $e->getMessage()), ['scope' => 'system', 'migration' => $e->getMigration()]);
|
|
||||||
throw $e;
|
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
if ($this->pdo->inTransaction()) {
|
if ($this->pdo->inTransaction()) {
|
||||||
$this->pdo->rollBack();
|
$this->pdo->rollBack();
|
||||||
}
|
}
|
||||||
$this->logger->log('error', 'Migration run failed: ' . $e->getMessage(), ['scope' => 'system']);
|
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $appliedNow;
|
return $appliedNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function splitStatements(string $sql): array
|
|
||||||
{
|
|
||||||
if ($sql === '') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return array_filter(array_map('trim', preg_split('/;\s*\n/', $sql)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function contentHashExists(string $hash): bool
|
|
||||||
{
|
|
||||||
if ($hash === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$stmt = $this->pdo->prepare('SELECT 1 FROM migrations WHERE content_hash = :hash LIMIT 1');
|
|
||||||
$stmt->execute([':hash' => $hash]);
|
|
||||||
return (bool)$stmt->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function recordMigration(string $name, string $content, string $hash, ?string $result = null): void
|
|
||||||
{
|
|
||||||
$timestampExpr = $this->isSqlite ? "datetime('now')" : 'NOW()';
|
|
||||||
$sql = "INSERT INTO migrations (migration, applied_at, content_hash, content, result) VALUES (:migration, {$timestampExpr}, :hash, :content, :result)";
|
|
||||||
$stmt = $this->pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':migration' => $name,
|
|
||||||
':hash' => $hash,
|
|
||||||
':content' => $content === '' ? null : $content,
|
|
||||||
':result' => $result,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function sortMigrations(array $items): array
|
|
||||||
{
|
|
||||||
usort($items, static function ($a, $b) {
|
|
||||||
$aTest = strpos($a, '_test_migration') !== false;
|
|
||||||
$bTest = strpos($b, '_test_migration') !== false;
|
|
||||||
if ($aTest === $bTest) {
|
|
||||||
return strcmp($a, $b);
|
|
||||||
}
|
|
||||||
return $aTest ? -1 : 1;
|
|
||||||
});
|
|
||||||
return $items;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isTestMigration(string $migration): bool
|
|
||||||
{
|
|
||||||
return strpos($migration, '_test_migration') !== false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function cleanupTestMigrationFile(string $migration): void
|
|
||||||
{
|
|
||||||
$path = $this->migrationsDir . '/' . $migration;
|
|
||||||
if (is_file($path)) {
|
|
||||||
@unlink($path);
|
|
||||||
}
|
|
||||||
$stmt = $this->pdo->prepare('DELETE FROM migrations WHERE migration = :migration');
|
|
||||||
$stmt->execute([':migration' => $migration]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function markMigrationApplied(string $migration, ?string $note = null): bool
|
|
||||||
{
|
|
||||||
$path = $this->migrationsDir . '/' . $migration;
|
|
||||||
$content = '';
|
|
||||||
if (is_file($path)) {
|
|
||||||
$fileContent = file_get_contents($path);
|
|
||||||
if ($fileContent !== false) {
|
|
||||||
$content = trim($fileContent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$hash = $content === '' ? '' : hash('sha256', $content);
|
|
||||||
if ($hash !== '' && $this->contentHashExists($hash)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $note ?? 'Marked as applied manually.';
|
|
||||||
$this->recordMigration($migration, $content, $hash, $result);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function skipMigration(string $migration): bool
|
|
||||||
{
|
|
||||||
$source = $this->migrationsDir . '/' . $migration;
|
|
||||||
if (!is_file($source)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$skippedDir = $this->migrationsDir . '/skipped';
|
|
||||||
if (!is_dir($skippedDir)) {
|
|
||||||
if (!mkdir($skippedDir, 0775, true) && !is_dir($skippedDir)) {
|
|
||||||
throw new Exception('Unable to create skipped migrations directory.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$destination = $skippedDir . '/' . $migration;
|
|
||||||
if (rename($source, $destination)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function initializeLogger(): void
|
|
||||||
{
|
|
||||||
$logger = $GLOBALS['logObject'] ?? null;
|
|
||||||
if (is_object($logger) && method_exists($logger, 'log')) {
|
|
||||||
$this->logger = $logger;
|
|
||||||
} else {
|
|
||||||
$this->logger = new NullLogger();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMigrationRecord(string $migration): ?array
|
|
||||||
{
|
|
||||||
$stmt = $this->pdo->prepare('SELECT migration, applied_at, content, result FROM migrations WHERE migration = :migration LIMIT 1');
|
|
||||||
$stmt->execute([':migration' => $migration]);
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
return $row ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getLastResults(): array
|
|
||||||
{
|
|
||||||
return $this->lastResults;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,32 +118,6 @@ if ($action !== '') {
|
||||||
} else {
|
} else {
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Applied migrations: ' . implode(', ', $applied), true);
|
Feedback::flash('NOTICE', 'DEFAULT', 'Applied migrations: ' . implode(', ', $applied), true);
|
||||||
}
|
}
|
||||||
} elseif ($action === 'migrate_apply_one') {
|
|
||||||
require_once __DIR__ . '/../core/MigrationRunner.php';
|
|
||||||
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
|
||||||
$runner = new \App\Core\MigrationRunner($db, $migrationsDir);
|
|
||||||
$migrationName = trim($_POST['migration_name'] ?? '');
|
|
||||||
$applied = $migrationName !== '' ? $runner->applyMigrationByName($migrationName) : [];
|
|
||||||
|
|
||||||
if (empty($applied)) {
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'No pending migrations.', true);
|
|
||||||
$_SESSION['migration_modal_result'] = [
|
|
||||||
'name' => $migrationName ?: null,
|
|
||||||
'status' => 'info',
|
|
||||||
'message' => 'No pending migrations to apply.'
|
|
||||||
];
|
|
||||||
if (!empty($migrationName)) {
|
|
||||||
$_SESSION['migration_modal_open'] = $migrationName;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Feedback::flash('NOTICE', 'DEFAULT', 'Applied migration: ' . implode(', ', $applied), true);
|
|
||||||
$_SESSION['migration_modal_result'] = [
|
|
||||||
'name' => $applied[0],
|
|
||||||
'status' => 'success',
|
|
||||||
'message' => 'Migration ' . $applied[0] . ' applied successfully.'
|
|
||||||
];
|
|
||||||
$_SESSION['migration_modal_open'] = $applied[0];
|
|
||||||
}
|
|
||||||
} elseif ($action === 'create_test_migration') {
|
} elseif ($action === 'create_test_migration') {
|
||||||
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
||||||
$timestamp = date('Ymd_His');
|
$timestamp = date('Ymd_His');
|
||||||
|
|
@ -202,39 +176,13 @@ require_once __DIR__ . '/../core/MigrationRunner.php';
|
||||||
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
$migrationsDir = __DIR__ . '/../../doc/database/migrations';
|
||||||
$pending = [];
|
$pending = [];
|
||||||
$applied = [];
|
$applied = [];
|
||||||
$next_pending = null;
|
|
||||||
$migration_contents = [];
|
$migration_contents = [];
|
||||||
$test_migrations_exist = false;
|
$test_migrations_exist = false;
|
||||||
$migration_modal_result = $_SESSION['migration_modal_result'] ?? null;
|
|
||||||
if (isset($_SESSION['migration_modal_result'])) {
|
|
||||||
unset($_SESSION['migration_modal_result']);
|
|
||||||
}
|
|
||||||
$modal_to_open = $_SESSION['migration_modal_open'] ?? null;
|
|
||||||
if (isset($_SESSION['migration_modal_open'])) {
|
|
||||||
unset($_SESSION['migration_modal_open']);
|
|
||||||
}
|
|
||||||
$migration_records = [];
|
|
||||||
try {
|
try {
|
||||||
$runner = new \App\Core\MigrationRunner($db, $migrationsDir);
|
$runner = new \App\Core\MigrationRunner($db, $migrationsDir);
|
||||||
$pending = $runner->listPendingMigrations();
|
$pending = $runner->listPendingMigrations();
|
||||||
$applied = $runner->listAppliedMigrations();
|
$applied = $runner->listAppliedMigrations();
|
||||||
|
|
||||||
$sortTestFirst = static function (array $items): array {
|
|
||||||
usort($items, static function ($a, $b) {
|
|
||||||
$aTest = strpos($a, '_test_migration') !== false;
|
|
||||||
$bTest = strpos($b, '_test_migration') !== false;
|
|
||||||
if ($aTest === $bTest) {
|
|
||||||
return strcmp($a, $b);
|
|
||||||
}
|
|
||||||
return $aTest ? -1 : 1;
|
|
||||||
});
|
|
||||||
return $items;
|
|
||||||
};
|
|
||||||
|
|
||||||
$pending = $sortTestFirst($pending);
|
|
||||||
$applied = $sortTestFirst($applied);
|
|
||||||
$next_pending = $pending[0] ?? null;
|
|
||||||
|
|
||||||
// Check if any test migrations exist
|
// Check if any test migrations exist
|
||||||
$test_migrations_exist = !empty(glob($migrationsDir . '/*_test_migration.sql'));
|
$test_migrations_exist = !empty(glob($migrationsDir . '/*_test_migration.sql'));
|
||||||
|
|
||||||
|
|
@ -248,10 +196,6 @@ try {
|
||||||
$migration_contents[$fname] = $content;
|
$migration_contents[$fname] = $content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$record = $runner->getMigrationRecord($fname);
|
|
||||||
if ($record) {
|
|
||||||
$migration_records[$fname] = $record;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
// show error in the page
|
// show error in the page
|
||||||
|
|
|
||||||
|
|
@ -73,12 +73,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div><strong>Pending</strong></div>
|
||||||
<strong>Pending</strong>
|
|
||||||
<?php if (!empty($next_pending)): ?>
|
|
||||||
<span class="badge bg-info text-dark ms-2">Next: <?= htmlspecialchars($next_pending) ?></span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<span class="badge <?= empty($pending) ? 'bg-success' : 'bg-warning text-dark' ?>"><?= count($pending) ?></span>
|
<span class="badge <?= empty($pending) ? 'bg-success' : 'bg-warning text-dark' ?>"><?= count($pending) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 small border rounded" style="max-height: 240px; overflow: auto;">
|
<div class="mt-2 small border rounded" style="max-height: 240px; overflow: auto;">
|
||||||
|
|
@ -89,13 +84,11 @@
|
||||||
<?php foreach ($pending as $fname): ?>
|
<?php foreach ($pending as $fname): ?>
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<span class="text-monospace small"><?= htmlspecialchars($fname) ?></span>
|
<span class="text-monospace small"><?= htmlspecialchars($fname) ?></span>
|
||||||
<div class="d-flex gap-2">
|
<button type="button"
|
||||||
<button type="button"
|
class="btn btn-outline-primary btn-sm"
|
||||||
class="btn btn-outline-primary btn-sm"
|
data-toggle="modal"
|
||||||
data-toggle="modal"
|
data-target="#migrationModal<?= md5($fname) ?>">View
|
||||||
data-target="#migrationModal<?= md5($fname) ?>">View
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -112,11 +105,7 @@
|
||||||
<div class="p-2"><span class="text-muted">none</span></div>
|
<div class="p-2"><span class="text-muted">none</span></div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<ul class="list-group list-group-flush">
|
<ul class="list-group list-group-flush">
|
||||||
<?php foreach ($applied as $fname):
|
<?php foreach ($applied as $fname): ?>
|
||||||
if (strpos($fname, '_test_migration') !== false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<span class="text-monospace small"><?= htmlspecialchars($fname) ?></span>
|
<span class="text-monospace small"><?= htmlspecialchars($fname) ?></span>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
|
@ -130,13 +119,11 @@
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2 mt-3">
|
<form method="post" class="mt-3">
|
||||||
<form method="post" class="tm-confirm" data-confirm="Apply all pending migrations?">
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
<input type="hidden" name="action" value="migrate_up">
|
||||||
<input type="hidden" name="action" value="migrate_up">
|
<button type="submit" class="btn btn-primary btn-sm" <?= empty($pending) ? 'disabled' : '' ?>>Apply pending migrations</button>
|
||||||
<button type="submit" class="btn btn-primary btn-sm" <?= empty($pending) ? 'disabled' : '' ?>>Apply all pending</button>
|
</form>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -158,24 +145,7 @@
|
||||||
<div class="modal-body p-0">
|
<div class="modal-body p-0">
|
||||||
<pre class="mb-0" style="max-height: 60vh; overflow: auto;"><code class="p-3 d-block"><?= htmlspecialchars($content) ?></code></pre>
|
<pre class="mb-0" style="max-height: 60vh; overflow: auto;"><code class="p-3 d-block"><?= htmlspecialchars($content) ?></code></pre>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
|
||||||
$isModalNext = (!empty($next_pending) && $next_pending === $name);
|
|
||||||
$modalResult = (!empty($migration_modal_result) && ($migration_modal_result['name'] ?? '') === $name) ? $migration_modal_result : null;
|
|
||||||
?>
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<?php if ($isModalNext): ?>
|
|
||||||
<form method="post" class="me-auto tm-confirm" data-confirm="Apply migration <?= htmlspecialchars($name) ?>?">
|
|
||||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>">
|
|
||||||
<input type="hidden" name="action" value="migrate_apply_one">
|
|
||||||
<input type="hidden" name="migration_name" value="<?= htmlspecialchars($name) ?>">
|
|
||||||
<button type="submit" class="btn btn-danger">Apply migration</button>
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($modalResult): ?>
|
|
||||||
<div class="alert alert-<?= $modalResult['status'] === 'success' ? 'success' : 'info' ?> mb-0 small">
|
|
||||||
<?= htmlspecialchars($modalResult['message']) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -183,16 +153,3 @@
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach;
|
<?php endforeach;
|
||||||
endif; ?>
|
endif; ?>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
document.querySelectorAll('form.tm-confirm').forEach(function (form) {
|
|
||||||
form.addEventListener('submit', function (event) {
|
|
||||||
const message = form.getAttribute('data-confirm') || 'Are you sure?';
|
|
||||||
if (!confirm(message)) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ function printUsage()
|
||||||
echo "Usage:\n";
|
echo "Usage:\n";
|
||||||
echo " php scripts/migrate.php status # Show pending and applied migrations\n";
|
echo " php scripts/migrate.php status # Show pending and applied migrations\n";
|
||||||
echo " php scripts/migrate.php up # Apply all pending migrations\n";
|
echo " php scripts/migrate.php up # Apply all pending migrations\n";
|
||||||
echo " php scripts/migrate.php next # Apply only the next pending migration\n";
|
|
||||||
echo "\n";
|
echo "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,21 +64,6 @@ try {
|
||||||
$applied = $runner->applyPendingMigrations();
|
$applied = $runner->applyPendingMigrations();
|
||||||
echo "\nApplied successfully: " . count($applied) . "\n";
|
echo "\nApplied successfully: " . count($applied) . "\n";
|
||||||
exit(0);
|
exit(0);
|
||||||
} elseif ($action === 'next') {
|
|
||||||
$pending = $runner->listPendingMigrations();
|
|
||||||
if (empty($pending)) {
|
|
||||||
echo "No pending migrations.\n";
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
$next = reset($pending);
|
|
||||||
echo "Applying next migration: {$next}\n";
|
|
||||||
$applied = $runner->applyNextMigration();
|
|
||||||
if (!empty($applied)) {
|
|
||||||
echo "Done.\n";
|
|
||||||
} else {
|
|
||||||
echo "Nothing applied.\n";
|
|
||||||
}
|
|
||||||
exit(0);
|
|
||||||
} else {
|
} else {
|
||||||
printUsage();
|
printUsage();
|
||||||
exit(1);
|
exit(1);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue