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']); } } }