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,250 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Align all FKs pointing to `pieces.id` with what entities declare
|
||||
* (ON DELETE CASCADE / SET NULL). Cleans up pre-existing orphan rows
|
||||
* inserted before the constraints existed, so the new FKs can be added.
|
||||
*
|
||||
* Mirror of Version20260506140000_FixComposantCascadeFKs for the Piece side.
|
||||
*/
|
||||
final class Version20260528090000_FixPieceCascadeFKs extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Align CASCADE/SET NULL FKs on pieces references (machine_piece_links, composant_piece_slots, piece_product_slots, documents, custom_field_values, piece_constructeur_links); cleanup pre-existing orphans';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// =========================================================================
|
||||
// 1. Audit log : snapshot des rows orphelines avant suppression.
|
||||
// =========================================================================
|
||||
$this->addSql(<<<'SQL'
|
||||
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 FK cascade fix migration (Version20260528090000) - 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)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
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 FK cascade fix migration (Version20260528090000) - referenced piece no longer existed'
|
||||
),
|
||||
NULL,
|
||||
NOW()
|
||||
FROM piece_product_slots s
|
||||
WHERE s.pieceid NOT IN (SELECT id FROM pieces)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
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 FK cascade fix migration (Version20260528090000) - 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)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
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 FK cascade fix migration (Version20260528090000) - 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)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
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 FK cascade fix migration (Version20260528090000) - referenced piece no longer existed'
|
||||
),
|
||||
NULL,
|
||||
NOW()
|
||||
FROM piece_constructeur_links l
|
||||
WHERE l.pieceid NOT IN (SELECT id FROM pieces)
|
||||
SQL);
|
||||
|
||||
// =========================================================================
|
||||
// 2. Nettoyage des orphelins (avant ADD CONSTRAINT, sinon PG rejette).
|
||||
// =========================================================================
|
||||
$this->addSql(<<<'SQL'
|
||||
DELETE FROM machine_piece_links
|
||||
WHERE pieceid IS NOT NULL
|
||||
AND pieceid NOT IN (SELECT id FROM pieces)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
UPDATE composant_piece_slots SET selectedpieceid = NULL
|
||||
WHERE selectedpieceid IS NOT NULL
|
||||
AND selectedpieceid NOT IN (SELECT id FROM pieces)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DELETE FROM piece_product_slots
|
||||
WHERE pieceid NOT IN (SELECT id FROM pieces)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DELETE FROM documents
|
||||
WHERE pieceid IS NOT NULL
|
||||
AND pieceid NOT IN (SELECT id FROM pieces)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DELETE FROM custom_field_values
|
||||
WHERE pieceid IS NOT NULL
|
||||
AND pieceid NOT IN (SELECT id FROM pieces)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DELETE FROM piece_constructeur_links
|
||||
WHERE pieceid NOT IN (SELECT id FROM pieces)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
DELETE FROM piece_products
|
||||
WHERE piece_id NOT IN (SELECT id FROM pieces)
|
||||
SQL);
|
||||
|
||||
// =========================================================================
|
||||
// 3. Drop des éventuelles FK existantes vers `pieces` (quel que soit leur
|
||||
// nom historique), puis ADD CONSTRAINT avec le bon ON DELETE.
|
||||
// =========================================================================
|
||||
$this->dropFksReferencingPieces('machine_piece_links', 'pieceid');
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE machine_piece_links ADD CONSTRAINT fk_mpl_piece
|
||||
FOREIGN KEY (pieceid) REFERENCES pieces(id) ON DELETE CASCADE
|
||||
SQL);
|
||||
|
||||
$this->dropFksReferencingPieces('composant_piece_slots', 'selectedpieceid');
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE composant_piece_slots ADD CONSTRAINT fk_cps_selected_piece
|
||||
FOREIGN KEY (selectedpieceid) REFERENCES pieces(id) ON DELETE SET NULL
|
||||
SQL);
|
||||
|
||||
$this->dropFksReferencingPieces('piece_product_slots', 'pieceid');
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE piece_product_slots ADD CONSTRAINT fk_pps_piece
|
||||
FOREIGN KEY (pieceid) REFERENCES pieces(id) ON DELETE CASCADE
|
||||
SQL);
|
||||
|
||||
$this->dropFksReferencingPieces('documents', 'pieceid');
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE documents ADD CONSTRAINT fk_documents_piece
|
||||
FOREIGN KEY (pieceid) REFERENCES pieces(id) ON DELETE CASCADE
|
||||
SQL);
|
||||
|
||||
$this->dropFksReferencingPieces('custom_field_values', 'pieceid');
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE custom_field_values ADD CONSTRAINT fk_cfv_piece
|
||||
FOREIGN KEY (pieceid) REFERENCES pieces(id) ON DELETE CASCADE
|
||||
SQL);
|
||||
|
||||
$this->dropFksReferencingPieces('piece_constructeur_links', 'pieceid');
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE piece_constructeur_links ADD CONSTRAINT fk_pcl_piece
|
||||
FOREIGN KEY (pieceid) REFERENCES pieces(id) ON DELETE CASCADE
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE machine_piece_links DROP CONSTRAINT IF EXISTS fk_mpl_piece');
|
||||
$this->addSql('ALTER TABLE composant_piece_slots DROP CONSTRAINT IF EXISTS fk_cps_selected_piece');
|
||||
$this->addSql('ALTER TABLE piece_product_slots DROP CONSTRAINT IF EXISTS fk_pps_piece');
|
||||
$this->addSql('ALTER TABLE documents DROP CONSTRAINT IF EXISTS fk_documents_piece');
|
||||
$this->addSql('ALTER TABLE custom_field_values DROP CONSTRAINT IF EXISTS fk_cfv_piece');
|
||||
$this->addSql('ALTER TABLE piece_constructeur_links DROP CONSTRAINT IF EXISTS fk_pcl_piece');
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop every FK on $table.$column that references the `pieces` table,
|
||||
* regardless of its historic name. Idempotent.
|
||||
*/
|
||||
private function dropFksReferencingPieces(string $table, string $column): void
|
||||
{
|
||||
$sql = <<<SQL
|
||||
DO \$\$
|
||||
DECLARE
|
||||
fk_name TEXT;
|
||||
BEGIN
|
||||
FOR fk_name IN
|
||||
SELECT tc.constraint_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON kcu.constraint_name = tc.constraint_name
|
||||
AND kcu.table_schema = tc.table_schema
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.table_schema
|
||||
WHERE tc.table_name = '{$table}'
|
||||
AND tc.constraint_type = 'FOREIGN KEY'
|
||||
AND kcu.column_name = '{$column}'
|
||||
AND ccu.table_name = 'pieces'
|
||||
LOOP
|
||||
EXECUTE format('ALTER TABLE {$table} DROP CONSTRAINT %I', fk_name);
|
||||
END LOOP;
|
||||
END \$\$;
|
||||
SQL;
|
||||
$this->addSql($sql);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user