[ '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"; // ============================================================ // PART 1: Restore piece custom field values from audit logs // ============================================================ echo "--- PART 1: Restoring piece custom field values ---\n\n"; // Find all deletion audit entries (where values went from X to null on 2026-03-13) $deletionLogs = $conn->fetchAllAssociative(" SELECT al.entityid, al.diff::text as diff, p.name as piece_name FROM audit_logs al JOIN pieces p ON p.id = al.entityid WHERE al.entitytype = 'piece' AND al.action = 'update' AND al.diff::text LIKE '%\"to\":null%' AND al.diff::text LIKE '%customField%' AND al.createdat >= '2026-03-13' ORDER BY p.name "); echo sprintf("Found %d pieces with deleted custom field values.\n\n", count($deletionLogs)); $restoredCount = 0; $errorCount = 0; foreach ($deletionLogs as $log) { $pieceId = $log['entityid']; $pieceName = $log['piece_name']; $diff = json_decode($log['diff'], true); foreach ($diff as $key => $change) { if (!str_starts_with($key, 'customField:')) { continue; } if (null !== $change['to']) { continue; // Not a deletion } $oldValue = $change['from']; if (null === $oldValue || '' === $oldValue) { continue; // Nothing to restore } $fieldName = substr($key, strlen('customField:')); // Find the current CustomFieldValue for this piece + field name $cfv = $conn->fetchAssociative(' SELECT cfv.id, cfv.value, cf.name as field_name FROM custom_field_values cfv JOIN custom_fields cf ON cf.id = cfv.customfieldid WHERE cfv.pieceid = ? AND cf.name = ? ', [$pieceId, $fieldName]); if (!$cfv) { echo sprintf(" WARNING: No CustomFieldValue found for piece '%s' field '%s' — skipping\n", $pieceName, $fieldName); ++$errorCount; continue; } if ('' !== $cfv['value'] && null !== $cfv['value']) { echo sprintf(" SKIP: Piece '%s' field '%s' already has value '%s' (would restore '%s')\n", $pieceName, $fieldName, $cfv['value'], $oldValue); continue; } echo sprintf(" RESTORE: Piece '%s' field '%s' = '%s'\n", $pieceName, $fieldName, $oldValue); if (!$dryRun) { $conn->executeStatement( 'UPDATE custom_field_values SET value = ? WHERE id = ?', [$oldValue, $cfv['id']] ); } ++$restoredCount; } } echo sprintf("\nPieces: %d values restored, %d errors.\n\n", $restoredCount, $errorCount); // ============================================================ // PART 2: Remove duplicate empty composant CustomFieldValues // ============================================================ echo "--- PART 2: Cleaning duplicate composant custom field values ---\n\n"; // Find composants that have duplicate CFVs (same composantid + same field name, one with value and one empty) $duplicates = $conn->fetchAllAssociative(" SELECT cfv_empty.id as empty_cfv_id, c.name as composant_name, cf_empty.name as field_name, cfv_filled.value as existing_value FROM custom_field_values cfv_empty JOIN custom_fields cf_empty ON cf_empty.id = cfv_empty.customfieldid JOIN composants c ON c.id = cfv_empty.composantid JOIN custom_field_values cfv_filled ON cfv_filled.composantid = cfv_empty.composantid JOIN custom_fields cf_filled ON cf_filled.id = cfv_filled.customfieldid WHERE cfv_empty.composantid IS NOT NULL AND cfv_empty.value = '' AND cf_empty.name = cf_filled.name AND cfv_filled.value != '' AND cfv_filled.id != cfv_empty.id ORDER BY c.name, cf_empty.name "); echo sprintf("Found %d duplicate empty custom field values on composants.\n\n", count($duplicates)); $deletedDuplicates = 0; foreach ($duplicates as $dup) { echo sprintf( " DELETE empty duplicate: Composant '%s' field '%s' (has value '%s' in other record)\n", $dup['composant_name'], $dup['field_name'], $dup['existing_value'] ); if (!$dryRun) { $conn->executeStatement('DELETE FROM custom_field_values WHERE id = ?', [$dup['empty_cfv_id']]); } ++$deletedDuplicates; } // Also find composants with duplicate empty CFVs (both empty, same field name - keep one, delete the other) $emptyDuplicates = $conn->fetchAllAssociative(" SELECT cfv2.id as duplicate_id, c.name as composant_name, cf2.name as field_name FROM custom_field_values cfv1 JOIN custom_fields cf1 ON cf1.id = cfv1.customfieldid JOIN custom_field_values cfv2 ON cfv2.composantid = cfv1.composantid AND cfv2.id > cfv1.id JOIN custom_fields cf2 ON cf2.id = cfv2.customfieldid JOIN composants c ON c.id = cfv1.composantid WHERE cfv1.composantid IS NOT NULL AND cfv1.value = '' AND cfv2.value = '' AND cf1.name = cf2.name ORDER BY c.name, cf2.name "); echo sprintf("\nFound %d duplicate empty-empty custom field values on composants.\n\n", count($emptyDuplicates)); foreach ($emptyDuplicates as $dup) { echo sprintf( " DELETE empty-empty duplicate: Composant '%s' field '%s'\n", $dup['composant_name'], $dup['field_name'] ); if (!$dryRun) { $conn->executeStatement('DELETE FROM custom_field_values WHERE id = ?', [$dup['duplicate_id']]); } ++$deletedDuplicates; } echo sprintf("\nComposants: %d duplicate values removed.\n", $deletedDuplicates); echo "\n=== DONE ===\n";