fix(custom-fields) : prevent data loss on ModelType save + restoration scripts
Backend: match existing CustomField by name as fallback when ID is not provided, preventing deletion and recreation of field definitions (which cascade-deletes values). Includes restoration/migration scripts for prod: - restore-custom-field-values.php: restores piece values from audit logs - migrate-orphaned-custom-fields.php: migrates values from orphaned CFs - fix-prod-all.php: combined fix (migrate + restore + cleanup) - fix-prod-recreate-and-migrate.php: full fix (recreate missing CFs + migrate + restore) - check-prod-*.php: diagnostic scripts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
scripts/verify-prod-health.php
Normal file
95
scripts/verify-prod-health.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
|
||||
$conn = DriverManager::getConnection([
|
||||
'driver' => 'pdo_pgsql',
|
||||
'host' => 'localhost',
|
||||
'port' => 5432,
|
||||
'dbname' => 'inventory',
|
||||
'user' => 'ferme_user',
|
||||
'password' => 'fermerecette',
|
||||
]);
|
||||
|
||||
echo "=== CUSTOM FIELDS HEALTH CHECK ===\n\n";
|
||||
|
||||
// 1. Orphaned CFs (should be 0)
|
||||
$orphanedCfs = $conn->fetchOne('SELECT COUNT(*) FROM custom_fields WHERE typecomposantid IS NULL AND typepieceid IS NULL AND typeproductid IS NULL');
|
||||
echo sprintf("1. Orphaned CF definitions: %d %s\n", $orphanedCfs, 0 == $orphanedCfs ? '[OK]' : '[PROBLEM]');
|
||||
|
||||
// 2. Orphaned CFValues (pointing to orphaned CFs)
|
||||
$orphanedCfvs = $conn->fetchOne('
|
||||
SELECT COUNT(*) FROM custom_field_values cfv
|
||||
JOIN custom_fields cf ON cf.id = cfv.customfieldid
|
||||
WHERE cf.typecomposantid IS NULL AND cf.typepieceid IS NULL AND cf.typeproductid IS NULL
|
||||
');
|
||||
echo sprintf("2. Orphaned CF values: %d %s\n", $orphanedCfvs, 0 == $orphanedCfvs ? '[OK]' : '[PROBLEM]');
|
||||
|
||||
// 3. Duplicate CFValues (same entity + same field name)
|
||||
$duplicatePieces = $conn->fetchOne("
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT cfv.pieceid, cf.name, COUNT(*) as cnt
|
||||
FROM custom_field_values cfv
|
||||
JOIN custom_fields cf ON cf.id = cfv.customfieldid
|
||||
WHERE cfv.pieceid IS NOT NULL
|
||||
GROUP BY cfv.pieceid, cf.name
|
||||
HAVING COUNT(*) > 1
|
||||
) t
|
||||
");
|
||||
$duplicateComposants = $conn->fetchOne("
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT cfv.composantid, cf.name, COUNT(*) as cnt
|
||||
FROM custom_field_values cfv
|
||||
JOIN custom_fields cf ON cf.id = cfv.customfieldid
|
||||
WHERE cfv.composantid IS NOT NULL
|
||||
GROUP BY cfv.composantid, cf.name
|
||||
HAVING COUNT(*) > 1
|
||||
) t
|
||||
");
|
||||
$totalDuplicates = $duplicatePieces + $duplicateComposants;
|
||||
echo sprintf("3. Duplicate CF values: %d %s\n", $totalDuplicates, 0 == $totalDuplicates ? '[OK]' : '[PROBLEM]');
|
||||
|
||||
// 4. Spot check known pieces
|
||||
echo "\n--- Spot checks ---\n";
|
||||
|
||||
$checks = [
|
||||
['Arbre du palier pied E1', 'cl3d978dd4b071daff8fb185f7', 'pieceid', 'diamètre', '50'],
|
||||
['Arbre du palier tête E1', 'cmkr0qjw5004s1eq6pen63x7j', 'pieceid', 'diamètre', '70'],
|
||||
['Cage écureuil pied E1', 'clbe710810fd7ccd09811957b3', 'composantid', 'Diamètre', ''],
|
||||
];
|
||||
|
||||
foreach ($checks as [$name, $id, $fk, $fieldName, $expectedValue]) {
|
||||
$row = $conn->fetchAssociative(
|
||||
"SELECT cfv.value FROM custom_field_values cfv JOIN custom_fields cf ON cf.id = cfv.customfieldid WHERE cfv.{$fk} = ? AND cf.name = ?",
|
||||
[$id, $fieldName]
|
||||
);
|
||||
$value = $row ? $row['value'] : '(NOT FOUND)';
|
||||
$ok = '' === $expectedValue ? ('' !== $value && null !== $value) : ($value === $expectedValue);
|
||||
echo sprintf(" %s → %s = '%s' %s\n", $name, $fieldName, $value, $ok ? '[OK]' : '[CHECK]');
|
||||
}
|
||||
|
||||
// 5. Summary of empty vs filled values
|
||||
echo "\n--- Value fill rates ---\n";
|
||||
$stats = $conn->fetchAllAssociative("
|
||||
SELECT
|
||||
CASE WHEN cfv.pieceid IS NOT NULL THEN 'piece'
|
||||
WHEN cfv.composantid IS NOT NULL THEN 'composant'
|
||||
WHEN cfv.productid IS NOT NULL THEN 'product'
|
||||
ELSE 'unknown' END as entity_type,
|
||||
COUNT(*) as total,
|
||||
COUNT(*) FILTER (WHERE cfv.value != '' AND cfv.value IS NOT NULL) as filled,
|
||||
COUNT(*) FILTER (WHERE cfv.value = '' OR cfv.value IS NULL) as empty
|
||||
FROM custom_field_values cfv
|
||||
GROUP BY entity_type
|
||||
ORDER BY entity_type
|
||||
");
|
||||
foreach ($stats as $s) {
|
||||
$pct = $s['total'] > 0 ? round(100 * $s['filled'] / $s['total']) : 0;
|
||||
echo sprintf(" %s: %d/%d filled (%d%%)\n", $s['entity_type'], $s['filled'], $s['total'], $pct);
|
||||
}
|
||||
|
||||
echo "\n=== DONE ===\n";
|
||||
Reference in New Issue
Block a user