fix(pieces) : empêche EntityNotFoundException sur Piece orpheline + UX prévention delete
- Migration FK CASCADE/SET NULL pour toutes les FK vers pieces.id (miroir de la fix Composant) + cleanup des orphelins existants avec audit log - Helper ensurePieceExists() qui catch EntityNotFoundException dans MachineStructureController et CustomFieldValueController - Script SQL standalone scripts/cleanup_orphan_piece_refs.sql pour nettoyer la prod sans attendre la migration - Affiche les machines (avec leur site) utilisant la pièce avant la confirmation de suppression
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
-- =============================================================================
|
||||
-- cleanup_orphan_piece_refs.sql
|
||||
-- =============================================================================
|
||||
-- Contexte : la suppression directe de rows dans `pieces` (bypass Doctrine /
|
||||
-- FK DB sans ON DELETE CASCADE) laisse des références orphelines dans plusieurs
|
||||
-- tables, ce qui fait planter l'API au chargement d'une Machine :
|
||||
-- Doctrine\ORM\EntityNotFoundException: Entity of type 'App\Entity\Piece' ...
|
||||
--
|
||||
-- Ce script fait deux choses :
|
||||
-- 1. ÉTAPE 1 (toujours exécutée) : compte les références orphelines par table
|
||||
-- pour visualiser l'ampleur du problème.
|
||||
-- 2. ÉTAPE 2 (commentée par défaut) : insère un audit_log par row, puis
|
||||
-- DELETE / UPDATE SET NULL selon la sémantique attendue côté entité.
|
||||
-- Décommenter le bloc `BEGIN; ... COMMIT;` pour appliquer.
|
||||
--
|
||||
-- Usage :
|
||||
-- # Dry-run (compte seulement)
|
||||
-- psql -h <host> -U <user> -d inventory -f scripts/cleanup_orphan_piece_refs.sql
|
||||
--
|
||||
-- # Application : décommenter le bloc transactionnel en bas du fichier,
|
||||
-- # puis relancer la même commande. La transaction garantit l'atomicité.
|
||||
-- =============================================================================
|
||||
|
||||
|
||||
-- ============================== ÉTAPE 1 : DRY-RUN ============================
|
||||
\echo ''
|
||||
\echo '=== Orphelins par table (Pieces) ==='
|
||||
|
||||
SELECT 'machine_piece_links' AS table_name, count(*) AS orphans
|
||||
FROM machine_piece_links
|
||||
WHERE pieceid IS NOT NULL
|
||||
AND pieceid NOT IN (SELECT id FROM pieces)
|
||||
UNION ALL
|
||||
SELECT 'composant_piece_slots', count(*)
|
||||
FROM composant_piece_slots
|
||||
WHERE selectedpieceid IS NOT NULL
|
||||
AND selectedpieceid NOT IN (SELECT id FROM pieces)
|
||||
UNION ALL
|
||||
SELECT 'piece_product_slots', count(*)
|
||||
FROM piece_product_slots
|
||||
WHERE pieceid NOT IN (SELECT id FROM pieces)
|
||||
UNION ALL
|
||||
SELECT 'documents', count(*)
|
||||
FROM documents
|
||||
WHERE pieceid IS NOT NULL
|
||||
AND pieceid NOT IN (SELECT id FROM pieces)
|
||||
UNION ALL
|
||||
SELECT 'custom_field_values', count(*)
|
||||
FROM custom_field_values
|
||||
WHERE pieceid IS NOT NULL
|
||||
AND pieceid NOT IN (SELECT id FROM pieces)
|
||||
UNION ALL
|
||||
SELECT 'piece_constructeur_links', count(*)
|
||||
FROM piece_constructeur_links
|
||||
WHERE pieceid NOT IN (SELECT id FROM pieces)
|
||||
UNION ALL
|
||||
SELECT 'piece_products', count(*)
|
||||
FROM piece_products
|
||||
WHERE piece_id NOT IN (SELECT id FROM pieces)
|
||||
ORDER BY table_name;
|
||||
|
||||
\echo ''
|
||||
\echo '=> Pour appliquer le cleanup, décommenter le bloc BEGIN/COMMIT ci-dessous.'
|
||||
\echo ''
|
||||
|
||||
|
||||
-- ============================== ÉTAPE 2 : APPLY =============================
|
||||
-- Décommenter ce bloc pour exécuter le cleanup. La transaction garantit
|
||||
-- l'atomicité : tout passe, ou rien (en cas d'erreur, ROLLBACK auto).
|
||||
--
|
||||
-- BEGIN;
|
||||
--
|
||||
-- -- 1. Audit log : snapshot des rows qui vont être supprimées (traçabilité prod).
|
||||
--
|
||||
-- INSERT INTO audit_logs (id, entitytype, entityid, action, snapshot, actorprofileid, createdat)
|
||||
-- SELECT
|
||||
-- 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24),
|
||||
-- 'machine_piece_link',
|
||||
-- l.id,
|
||||
-- 'delete',
|
||||
-- json_build_object(
|
||||
-- 'id', l.id,
|
||||
-- 'machineId', l.machineid,
|
||||
-- 'pieceId', l.pieceid,
|
||||
-- 'note', 'Cleaned by cleanup_orphan_piece_refs.sql - referenced piece no longer existed'
|
||||
-- ),
|
||||
-- NULL,
|
||||
-- NOW()
|
||||
-- FROM machine_piece_links l
|
||||
-- WHERE l.pieceid IS NOT NULL
|
||||
-- AND l.pieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- INSERT INTO audit_logs (id, entitytype, entityid, action, snapshot, actorprofileid, createdat)
|
||||
-- SELECT
|
||||
-- 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24),
|
||||
-- 'piece_product_slot',
|
||||
-- s.id,
|
||||
-- 'delete',
|
||||
-- json_build_object(
|
||||
-- 'id', s.id,
|
||||
-- 'pieceId', s.pieceid,
|
||||
-- 'note', 'Cleaned by cleanup_orphan_piece_refs.sql - referenced piece no longer existed'
|
||||
-- ),
|
||||
-- NULL,
|
||||
-- NOW()
|
||||
-- FROM piece_product_slots s
|
||||
-- WHERE s.pieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- INSERT INTO audit_logs (id, entitytype, entityid, action, snapshot, actorprofileid, createdat)
|
||||
-- SELECT
|
||||
-- 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24),
|
||||
-- 'document',
|
||||
-- d.id,
|
||||
-- 'delete',
|
||||
-- json_build_object(
|
||||
-- 'id', d.id,
|
||||
-- 'name', d.name,
|
||||
-- 'filename', d.filename,
|
||||
-- 'pieceId', d.pieceid,
|
||||
-- 'note', 'Cleaned by cleanup_orphan_piece_refs.sql - referenced piece no longer existed'
|
||||
-- ),
|
||||
-- NULL,
|
||||
-- NOW()
|
||||
-- FROM documents d
|
||||
-- WHERE d.pieceid IS NOT NULL
|
||||
-- AND d.pieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- INSERT INTO audit_logs (id, entitytype, entityid, action, snapshot, actorprofileid, createdat)
|
||||
-- SELECT
|
||||
-- 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24),
|
||||
-- 'custom_field_value',
|
||||
-- v.id,
|
||||
-- 'delete',
|
||||
-- json_build_object(
|
||||
-- 'id', v.id,
|
||||
-- 'pieceId', v.pieceid,
|
||||
-- 'note', 'Cleaned by cleanup_orphan_piece_refs.sql - referenced piece no longer existed'
|
||||
-- ),
|
||||
-- NULL,
|
||||
-- NOW()
|
||||
-- FROM custom_field_values v
|
||||
-- WHERE v.pieceid IS NOT NULL
|
||||
-- AND v.pieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- INSERT INTO audit_logs (id, entitytype, entityid, action, snapshot, actorprofileid, createdat)
|
||||
-- SELECT
|
||||
-- 'cl' || substring(md5(random()::text || clock_timestamp()::text), 1, 24),
|
||||
-- 'piece_constructeur_link',
|
||||
-- l.id,
|
||||
-- 'delete',
|
||||
-- json_build_object(
|
||||
-- 'id', l.id,
|
||||
-- 'pieceId', l.pieceid,
|
||||
-- 'constructeurId', l.constructeurid,
|
||||
-- 'note', 'Cleaned by cleanup_orphan_piece_refs.sql - referenced piece no longer existed'
|
||||
-- ),
|
||||
-- NULL,
|
||||
-- NOW()
|
||||
-- FROM piece_constructeur_links l
|
||||
-- WHERE l.pieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- -- 2. Nettoyage des orphelins.
|
||||
--
|
||||
-- DELETE FROM machine_piece_links
|
||||
-- WHERE pieceid IS NOT NULL
|
||||
-- AND pieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- UPDATE composant_piece_slots SET selectedpieceid = NULL
|
||||
-- WHERE selectedpieceid IS NOT NULL
|
||||
-- AND selectedpieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- DELETE FROM piece_product_slots
|
||||
-- WHERE pieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- DELETE FROM documents
|
||||
-- WHERE pieceid IS NOT NULL
|
||||
-- AND pieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- DELETE FROM custom_field_values
|
||||
-- WHERE pieceid IS NOT NULL
|
||||
-- AND pieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- DELETE FROM piece_constructeur_links
|
||||
-- WHERE pieceid NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- DELETE FROM piece_products
|
||||
-- WHERE piece_id NOT IN (SELECT id FROM pieces);
|
||||
--
|
||||
-- -- 3. Vérification post-cleanup : tout doit être à 0.
|
||||
-- SELECT 'machine_piece_links' AS table_name, count(*) AS remaining_orphans
|
||||
-- FROM machine_piece_links
|
||||
-- WHERE pieceid IS NOT NULL
|
||||
-- AND pieceid NOT IN (SELECT id FROM pieces)
|
||||
-- UNION ALL
|
||||
-- SELECT 'composant_piece_slots', count(*)
|
||||
-- FROM composant_piece_slots
|
||||
-- WHERE selectedpieceid IS NOT NULL
|
||||
-- AND selectedpieceid NOT IN (SELECT id FROM pieces)
|
||||
-- UNION ALL
|
||||
-- SELECT 'piece_product_slots', count(*)
|
||||
-- FROM piece_product_slots
|
||||
-- WHERE pieceid NOT IN (SELECT id FROM pieces)
|
||||
-- UNION ALL
|
||||
-- SELECT 'documents', count(*)
|
||||
-- FROM documents
|
||||
-- WHERE pieceid IS NOT NULL
|
||||
-- AND pieceid NOT IN (SELECT id FROM pieces)
|
||||
-- UNION ALL
|
||||
-- SELECT 'custom_field_values', count(*)
|
||||
-- FROM custom_field_values
|
||||
-- WHERE pieceid IS NOT NULL
|
||||
-- AND pieceid NOT IN (SELECT id FROM pieces)
|
||||
-- UNION ALL
|
||||
-- SELECT 'piece_constructeur_links', count(*)
|
||||
-- FROM piece_constructeur_links
|
||||
-- WHERE pieceid NOT IN (SELECT id FROM pieces)
|
||||
-- UNION ALL
|
||||
-- SELECT 'piece_products', count(*)
|
||||
-- FROM piece_products
|
||||
-- WHERE piece_id NOT IN (SELECT id FROM pieces)
|
||||
-- ORDER BY table_name;
|
||||
--
|
||||
-- COMMIT;
|
||||
Reference in New Issue
Block a user