Files
Lizandro Guarnizo 33dfc0cf71 fix(migrate): detectar tablas faltantes aunque migración esté registrada
Si una migración está en la tabla migrations pero la tabla que crea ya
no existe en la BD (ej. Coolify recreó la DB), el sistema la re-ejecuta
automáticamente. Esto evita el error anterior donde 10 lab migrations
estaban registradas como ejecutadas pero las tablas no existían.
2026-03-12 09:46:07 -05:00

195 lines
7.2 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Sistema de migraciones automáticas
* Se ejecuta al iniciar el contenedor Docker
* Fecha: 3 de febrero de 2026
*/
// No mostrar errores en pantalla
error_reporting(E_ALL);
ini_set('display_errors', '0');
echo "🔍 Iniciando sistema de migraciones...\n";
// Cargar configuración
if (!file_exists(__DIR__ . '/../config/config.php')) {
echo "❌ Error: config.php no encontrado\n";
exit(1);
}
require_once __DIR__ . '/../config/config.php';
try {
$db = Database::getInstance();
echo "✅ Conexión a base de datos establecida\n";
// Crear tabla de control de migraciones si no existe
$db->execute("
CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
filename VARCHAR(255) NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
KEY idx_filename (filename)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
echo "✅ Tabla de control de migraciones verificada\n";
// Obtener lista de migraciones ejecutadas
$executed = $db->fetchAll("SELECT filename FROM migrations");
$executedFiles = array_column($executed, 'filename');
// Buscar archivos .sql en database/ Y en migrations/ (carpeta raíz del proyecto)
$dbFiles = glob(__DIR__ . '/*.sql') ?: [];
$migrationsFiles = glob(__DIR__ . '/../migrations/*.sql') ?: [];
// Unir y ordenar por nombre de archivo (orden cronológico por prefijo)
$allFiles = array_unique(array_merge($dbFiles, $migrationsFiles));
usort($allFiles, fn($a, $b) => strcmp(basename($a), basename($b)));
$migrationFiles = $allFiles;
if (empty($migrationFiles)) {
echo "️ No se encontraron archivos de migración\n";
exit(0);
}
echo "📋 Encontrados " . count($migrationFiles) . " archivos de migración\n";
echo " (database/: " . count($dbFiles) . ", migrations/: " . count($migrationsFiles) . ")\n\n";
$executed_count = 0;
$skipped_count = 0;
foreach ($migrationFiles as $file) {
$filename = basename($file);
// Saltar archivo de backup completo
if (strpos($filename, 'backup') !== false) {
continue;
}
// Verificar si ya fue ejecutada
if (in_array($filename, $executedFiles)) {
// Verificación extra: si el SQL crea tablas, comprobar que realmente existan.
// Si alguna tabla del archivo no existe, re-ejecutar la migración.
$sqlRaw = file_get_contents($file);
preg_match_all(
'/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?/i',
$sqlRaw,
$tableMatches
);
$missingTable = false;
foreach ($tableMatches[1] as $tableName) {
$rows = $db->fetchAll(
"SELECT COUNT(*) as cnt FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = ?",
[$tableName]
);
if (empty($rows) || ($rows[0]['cnt'] ?? 0) == 0) {
$missingTable = $tableName;
break;
}
}
if ($missingTable) {
echo "⚠️ $filename registrada pero tabla '$missingTable' no existe — re-ejecutando\n";
// Quitar del registro para re-ejecutar
$db->execute("DELETE FROM migrations WHERE filename = ?", [$filename]);
$executedFiles = array_diff($executedFiles, [$filename]);
// Continúa a la ejecución abajo
} else {
echo "⏭️ Saltando $filename (ya ejecutada)\n";
$skipped_count++;
continue;
}
}
echo "🔄 Ejecutando migración: $filename\n";
try {
$sql = file_get_contents($file);
// Eliminar comentarios de bloque /* ... */
$sql = preg_replace('/\/\*.*?\*\//s', '', $sql);
// Dividir en sentencias por ; y limpiar
$rawStatements = explode(';', $sql);
$statements = [];
foreach ($rawStatements as $stmt) {
// Eliminar líneas de comentario -- y líneas vacías
$lines = explode("\n", $stmt);
$cleaned = [];
foreach ($lines as $line) {
$trimmed = trim($line);
if ($trimmed !== '' && strpos($trimmed, '--') !== 0) {
$cleaned[] = $line;
}
}
$stmt = trim(implode("\n", $cleaned));
if ($stmt !== '') {
$statements[] = $stmt;
}
}
$stmt_count = 0;
$had_error = false;
foreach ($statements as $stmt) {
try {
$db->execute($stmt);
$stmt_count++;
} catch (Exception $e) {
$msg = $e->getMessage();
// Ignorar errores de "ya existe" — son idempotentes
$ignorable = [
'Duplicate column',
'already exists',
'Duplicate key name',
'Multiple primary key defined',
];
$ignore = false;
foreach ($ignorable as $pattern) {
if (strpos($msg, $pattern) !== false) {
$ignore = true;
break;
}
}
if ($ignore) {
echo " ⚠ Ya existe (ignorado): " . substr($msg, 0, 80) . "\n";
continue;
}
echo " ✗ Error en sentencia: $msg\n";
$had_error = true;
// Continuar con las demás sentencias del archivo
}
}
if (!$had_error) {
// Registrar migración como ejecutada solo si no hubo errores reales
$db->execute(
"INSERT INTO migrations (filename) VALUES (?)",
[$filename]
);
echo " ✓ Ejecutadas $stmt_count sentencias SQL — registrada\n";
} else {
echo " ⚠ Migración con errores — NO registrada (se reintentará en el próximo deploy)\n";
}
$executed_count++;
} catch (Exception $e) {
echo " ✗ Error fatal en $filename: " . $e->getMessage() . "\n";
// Continuar con la siguiente migración
}
}
echo "\n";
echo "═══════════════════════════════════════════\n";
echo "✅ Migraciones completadas\n";
echo " - Ejecutadas: $executed_count\n";
echo " - Saltadas: $skipped_count\n";
echo "═══════════════════════════════════════════\n";
} catch (Exception $e) {
echo "❌ Error fatal en sistema de migraciones: " . $e->getMessage() . "\n";
exit(1);
}
exit(0);