fetchAll('SELECT id, managed_path FROM files ORDER BY id') as $file) { try { ManagedPath::validate($file['managed_path']); } catch (InvalidArgumentException) { $problems[] = 'noncanonical managed path on file ID ' . $file['id']; } } $duplicates = $this->fetchAll('SELECT f.id FROM files f INNER JOIN (SELECT managed_path FROM files GROUP BY managed_path HAVING COUNT(*) > 1) d ON f.managed_path = d.managed_path ORDER BY f.id'); if ($duplicates !== []) { $problems[] = 'duplicate managed paths on file IDs ' . implode(', ', array_column($duplicates, 'id')); } $this->reject( $problems, 'Resolve paths and ownership explicitly; do not merge or delete records automatically.' ); $this->table('files') ->dropForeignKey('model_id') ->changeColumn('model_id', 'integer', ['null' => true, 'signed' => false]) ->addForeignKey('model_id', 'models', 'id', ['delete' => 'RESTRICT', 'update' => 'NO_ACTION']) ->removeIndexByName('idx_files_model_path') ->addIndex(['managed_path'], ['unique' => true, 'name' => 'idx_files_managed_path']) ->addIndex(['model_id'], ['name' => 'idx_files_model_id']) ->update(); $this->table('models') ->removeIndexByName('idx_models_folder_name') ->addIndex(['folder_id', 'name'], ['name' => 'idx_models_folder_name']) ->update(); } public function down(): void { $problems = []; $unassigned = $this->fetchAll('SELECT id FROM files WHERE model_id IS NULL ORDER BY id'); if ($unassigned !== []) { $problems[] = 'unassigned file IDs ' . implode(', ', array_column($unassigned, 'id')); } $duplicates = $this->fetchAll('SELECT m.id FROM models m INNER JOIN (SELECT folder_id, name FROM models GROUP BY folder_id, name HAVING COUNT(*) > 1) d ON m.folder_id = d.folder_id AND m.name = d.name ORDER BY m.id'); if ($duplicates !== []) { $problems[] = 'duplicate same-folder titles on model IDs ' . implode(', ', array_column($duplicates, 'id')); } $this->reject( $problems, 'Resolve by explicitly assigning files and disambiguating model titles before retrying.' ); $this->table('files') ->dropForeignKey('model_id') ->changeColumn('model_id', 'integer', ['null' => false, 'signed' => false]) ->addForeignKey('model_id', 'models', 'id', ['delete' => 'CASCADE', 'update' => 'NO_ACTION']) ->removeIndexByName('idx_files_managed_path') ->removeIndexByName('idx_files_model_id') ->addIndex(['model_id', 'managed_path'], ['unique' => true, 'name' => 'idx_files_model_path']) ->update(); $this->table('models') ->removeIndexByName('idx_models_folder_name') ->addIndex(['folder_id', 'name'], ['unique' => true, 'name' => 'idx_models_folder_name']) ->update(); } private function reject(array $problems, string $guidance): void { if ($problems !== []) { throw new RuntimeException('Catalog identity preflight: ' . implode('; ', $problems) . '. ' . $guidance); } } }