[ 'driver' => 'pdo_pgsql', 'host' => 'localhost', 'port' => 5432, 'dbname' => getenv('DB_NAME') ?: 'inventory', 'user' => 'ferme_user', 'password' => 'fermerecette', ], default => [ 'driver' => 'pdo_pgsql', 'host' => 'db', 'port' => 5432, 'dbname' => 'inventory', 'user' => 'root', 'password' => 'root', ], }); echo $dryRun ? "=== DRY RUN MODE ===\n\n" : "=== LIVE MODE ===\n\n"; $migratedCount = 0; $deletedCfCount = 0; $skippedCount = 0; $conflictCount = 0; // Process each entity type $entityTypes = [ [ 'label' => 'composant', 'entityTable' => 'composants', 'cfvFk' => 'composantid', 'modelTypeFk' => 'typecomposantid', 'cfModelTypeFk' => 'typecomposantid', ], [ 'label' => 'piece', 'entityTable' => 'pieces', 'cfvFk' => 'pieceid', 'modelTypeFk' => 'typepieceid', 'cfModelTypeFk' => 'typepieceid', ], [ 'label' => 'product', 'entityTable' => 'products', 'cfvFk' => 'productid', 'modelTypeFk' => 'typeproductid', 'cfModelTypeFk' => 'typeproductid', ], ]; foreach ($entityTypes as $et) { echo sprintf("--- Processing %ss ---\n\n", $et['label']); // Find all CFValues pointing to orphaned CFs for this entity type $orphanedValues = $conn->fetchAllAssociative(" SELECT cfv.id as cfv_id, cfv.value, cfv.{$et['cfvFk']} as entity_id, cf_old.id as old_cf_id, cf_old.name as field_name, e.name as entity_name, e.{$et['modelTypeFk']} as model_type_id FROM custom_field_values cfv JOIN custom_fields cf_old ON cf_old.id = cfv.customfieldid JOIN {$et['entityTable']} e ON e.id = cfv.{$et['cfvFk']} WHERE cfv.{$et['cfvFk']} IS NOT NULL AND cf_old.{$et['cfModelTypeFk']} IS NULL AND cf_old.typecomposantid IS NULL AND cf_old.typepieceid IS NULL AND cf_old.typeproductid IS NULL ORDER BY e.name, cf_old.name "); echo sprintf(" Found %d orphaned custom field values.\n", count($orphanedValues)); foreach ($orphanedValues as $ov) { if (!$ov['model_type_id']) { echo sprintf(" SKIP: %s '%s' has no ModelType\n", $et['label'], $ov['entity_name']); ++$skippedCount; continue; } // Find the current CF definition on the ModelType with the same name $currentCf = $conn->fetchAssociative(" SELECT id FROM custom_fields WHERE {$et['cfModelTypeFk']} = ? AND name = ? LIMIT 1 ", [$ov['model_type_id'], $ov['field_name']]); if (!$currentCf) { echo sprintf( " WARNING: No current CF '%s' on ModelType %s for %s '%s'\n", $ov['field_name'], $ov['model_type_id'], $et['label'], $ov['entity_name'] ); ++$skippedCount; continue; } // Check if entity already has a CFValue for this current CF $existingValue = $conn->fetchAssociative(" SELECT id, value FROM custom_field_values WHERE {$et['cfvFk']} = ? AND customfieldid = ? ", [$ov['entity_id'], $currentCf['id']]); if ($existingValue) { // Current CF already has a value for this entity if ('' !== $existingValue['value'] && null !== $existingValue['value']) { // Both have values — conflict, skip if ('' !== $ov['value'] && null !== $ov['value'] && $ov['value'] !== $existingValue['value']) { echo sprintf( " CONFLICT: %s '%s' field '%s': old='%s' vs current='%s' — keeping current\n", $et['label'], $ov['entity_name'], $ov['field_name'], $ov['value'], $existingValue['value'] ); ++$conflictCount; } // Delete the orphaned value (current one is kept) if (!$dryRun) { $conn->executeStatement('DELETE FROM custom_field_values WHERE id = ?', [$ov['cfv_id']]); } echo sprintf( " DELETE orphaned: %s '%s' field '%s' (current has value '%s')\n", $et['label'], $ov['entity_name'], $ov['field_name'], $existingValue['value'] ); } else { // Current value is empty, orphaned has data — update the current one and delete orphaned if ('' !== $ov['value'] && null !== $ov['value']) { echo sprintf( " MIGRATE: %s '%s' field '%s' = '%s'\n", $et['label'], $ov['entity_name'], $ov['field_name'], $ov['value'] ); if (!$dryRun) { $conn->executeStatement( 'UPDATE custom_field_values SET value = ? WHERE id = ?', [$ov['value'], $existingValue['id']] ); } ++$migratedCount; } // Delete the orphaned CFV if (!$dryRun) { $conn->executeStatement('DELETE FROM custom_field_values WHERE id = ?', [$ov['cfv_id']]); } } } else { // No current CFV exists — reassign the orphaned one to the current CF echo sprintf( " REASSIGN: %s '%s' field '%s' = '%s'\n", $et['label'], $ov['entity_name'], $ov['field_name'], $ov['value'] ); if (!$dryRun) { $conn->executeStatement( 'UPDATE custom_field_values SET customfieldid = ? WHERE id = ?', [$currentCf['id'], $ov['cfv_id']] ); } ++$migratedCount; } } echo "\n"; } // Clean up orphaned CF definitions with no remaining values echo "--- Cleaning up orphaned CustomField definitions ---\n\n"; $orphanedCfs = $conn->fetchAllAssociative(' SELECT cf.id, cf.name FROM custom_fields cf WHERE cf.typecomposantid IS NULL AND cf.typepieceid IS NULL AND cf.typeproductid IS NULL AND NOT EXISTS (SELECT 1 FROM custom_field_values cfv WHERE cfv.customfieldid = cf.id) ORDER BY cf.name '); echo sprintf("Found %d orphaned CustomField definitions with no values.\n", count($orphanedCfs)); foreach ($orphanedCfs as $cf) { if (!$dryRun) { $conn->executeStatement('DELETE FROM custom_fields WHERE id = ?', [$cf['id']]); } ++$deletedCfCount; } echo sprintf("\n=== SUMMARY ===\n"); echo sprintf("Values migrated/reassigned: %d\n", $migratedCount); echo sprintf("Orphaned CF definitions deleted: %d\n", $deletedCfCount); echo sprintf("Skipped: %d\n", $skippedCount); echo sprintf("Conflicts (kept current): %d\n", $conflictCount); echo "=== DONE ===\n";