Globinours/app/Services/Migrations.php

185 lines
8.5 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
declare(strict_types=1);
final class Migrations
{
public static function run(string $migrationsDir): void
{
$db = DB::pdo();
$db->exec('CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL);');
$files = glob(rtrim($migrationsDir, '/') . '/*.sql') ?: [];
sort($files);
foreach ($files as $file) {
$name = basename($file);
$stmt = $db->prepare('SELECT 1 FROM _migrations WHERE name = :n');
$stmt->execute([':n' => $name]);
if ($stmt->fetchColumn()) {
continue;
}
$sql = file_get_contents($file);
if ($sql === false) {
throw new RuntimeException("Cannot read migration: $name");
}
// Les toutes premières installations utilisaient déjà deleted_at avant
// que cette colonne ne soit correctement inscrite dans une migration.
if ($name === '025_animal_archive_and_trash.sql') {
$columns = array_column($db->query('PRAGMA table_info(animals)')->fetchAll(PDO::FETCH_ASSOC), 'name');
if (!in_array('deleted_at', $columns, true)) {
$db->exec('ALTER TABLE animals ADD COLUMN deleted_at TEXT');
}
}
// Répare les bases créées par une candidate rc.3 incomplète, dans
// laquelle les médias privés étaient utilisés avant d'être migrés.
if ($name === '089_media_schema_repair.sql') {
$columns = array_column(
$db->query('PRAGMA table_info(animal_photos)')->fetchAll(PDO::FETCH_ASSOC),
'name',
);
if (!in_array('is_public', $columns, true)) {
$db->exec('ALTER TABLE animal_photos ADD COLUMN is_public INTEGER NOT NULL DEFAULT 1');
}
}
// Les anciennes bases possédaient déjà le nouvel historique alors
// que les installations neuves recevaient encore son premier schéma.
if ($name === '090_medication_administration_routes.sql') {
$columns = array_column(
$db->query('PRAGMA table_info(animal_history)')->fetchAll(PDO::FETCH_ASSOC),
'name',
);
if (!in_array('type', $columns, true)) {
$db->exec("ALTER TABLE animal_history ADD COLUMN type TEXT NOT NULL DEFAULT 'history'");
}
if (!in_array('label', $columns, true)) {
$db->exec("ALTER TABLE animal_history ADD COLUMN label TEXT NOT NULL DEFAULT ''");
}
if (!in_array('details', $columns, true)) {
$db->exec('ALTER TABLE animal_history ADD COLUMN details TEXT');
}
}
// Deux anciens schémas de l’historique ont circulé : l’un avec
// action/field/values, l’autre avec type/label/details. Le code
// courant accepte les données des deux et a besoin de leur union.
if ($name === '096_animal_history_compatibility.sql') {
$columns = array_column(
$db->query('PRAGMA table_info(animal_history)')->fetchAll(PDO::FETCH_ASSOC),
'name',
);
$definitions = [
'action' => "TEXT NOT NULL DEFAULT 'history'",
'field' => 'TEXT',
'old_value' => 'TEXT',
'new_value' => 'TEXT',
'meta' => 'TEXT',
'type' => "TEXT NOT NULL DEFAULT 'history'",
'label' => "TEXT NOT NULL DEFAULT ''",
'details' => 'TEXT',
'user_id' => 'INTEGER',
];
foreach ($definitions as $column => $definition) {
if (!in_array($column, $columns, true)) {
$db->exec("ALTER TABLE animal_history ADD COLUMN $column $definition");
}
}
}
$relaxesForeignKeys = in_array(
$name,
['093_room_rules_and_care_periods.sql', '097_remove_legacy_room_constraint.sql'],
true,
);
if ($relaxesForeignKeys) {
$db->exec('PRAGMA foreign_keys = OFF');
}
$db->beginTransaction();
try {
if ($name === '097_remove_legacy_room_constraint.sql') {
self::modernizeAnimalConstraints($db);
} else {
$db->exec($sql);
}
$ins = $db->prepare("INSERT INTO _migrations(name, applied_at) VALUES(:n, datetime('now'))");
$ins->execute([':n' => $name]);
if ($relaxesForeignKeys && $db->query('PRAGMA foreign_key_check')->fetchColumn() !== false) {
throw new RuntimeException('Foreign key check failed after room migration');
}
$db->commit();
if ($relaxesForeignKeys) {
$db->exec('PRAGMA foreign_keys = ON');
}
} catch (Throwable $e) {
$db->rollBack();
if ($relaxesForeignKeys) {
$db->exec('PRAGMA foreign_keys = ON');
}
throw new RuntimeException('Migration ' . $name . ' : ' . $e->getMessage(), 0, $e);
}
}
}
private static function modernizeAnimalConstraints(PDO $db): void
{
$createSql = (string) $db
->query("SELECT sql FROM sqlite_master WHERE type='table' AND name='animals'")
->fetchColumn();
$constraint = "/refuge_room\\s+TEXT\\s+CHECK\\s*\\(\\s*refuge_room\\s+IN\\s*\\(\\s*'silver'\\s*,\\s*'twix'\\s*\\)\\s+OR\\s+refuge_room\\s+IS\\s+NULL\\s*\\)/i";
$registrationConstraint = "/identification_registration_status\\s+TEXT\\s+NOT\\s+NULL\\s+DEFAULT\\s+'unknown'\\s+CHECK\\s*\\(\\s*identification_registration_status\\s+IN\\s*\\(\\s*'unknown'\\s*,\\s*'pending'\\s*,\\s*'registered'\\s*\\)\\s*\\)/i";
if (
$createSql === ''
|| (!preg_match($constraint, $createSql) && !preg_match($registrationConstraint, $createSql))
) {
return;
}
$newSql = preg_replace(
'/^CREATE TABLE\\s+(?:IF NOT EXISTS\\s+)?["`\[]?animals["`\]]?/i',
'CREATE TABLE animals_room_unrestricted',
$createSql,
1,
);
$newSql = preg_replace($constraint, 'refuge_room TEXT', (string) $newSql, 1);
$newSql = preg_replace(
$registrationConstraint,
"identification_registration_status TEXT NOT NULL DEFAULT 'unknown' " .
"CHECK(identification_registration_status IN ('unknown','registered','change_pending','owner_changed','pending'))",
(string) $newSql,
1,
);
if (!$newSql || preg_match($constraint, $newSql)) {
throw new RuntimeException('Unable to remove the legacy shelter-room constraint.');
}
$objects = $db
->query("SELECT sql FROM sqlite_master WHERE tbl_name='animals' AND type IN ('index','trigger') AND sql IS NOT NULL")
->fetchAll(PDO::FETCH_COLUMN);
$views = $db
->query("SELECT name,sql FROM sqlite_master WHERE type='view' AND sql IS NOT NULL ORDER BY rowid")
->fetchAll(PDO::FETCH_ASSOC);
$columns = array_column($db->query('PRAGMA table_info(animals)')->fetchAll(PDO::FETCH_ASSOC), 'name');
$quotedColumns = implode(',', array_map(static fn(string $column): string => '"' . str_replace('"', '""', $column) . '"', $columns));
foreach ($views as $view) {
$viewName = '"' . str_replace('"', '""', (string) $view['name']) . '"';
$db->exec('DROP VIEW ' . $viewName);
}
$db->exec($newSql);
$db->exec("INSERT INTO animals_room_unrestricted ($quotedColumns) SELECT $quotedColumns FROM animals");
$db->exec("UPDATE animals_room_unrestricted SET identification_registration_status='change_pending' WHERE identification_registration_status='pending'");
$db->exec('DROP TABLE animals');
$db->exec('ALTER TABLE animals_room_unrestricted RENAME TO animals');
foreach ($objects as $objectSql) {
$db->exec((string) $objectSql);
}
foreach ($views as $view) {
$db->exec((string) $view['sql']);
}
}
}