---
title: "Demo Merge — Scénarios de Validation SQL"
module: demo
type: functional
status: active
updated: 2026-04-24
---

# Demo Merge — Scénarios de Validation SQL

Scénarios de validation pour le mode `--merge` (Phase 15). Exécuter via tinker ou `database-query` sur un environnement de test avec un IDC de démo connu (ex: 985).

> **Prérequis :** avoir lancé `demo:hospital 985 "..." --reset` au moins une fois, puis `demo:hospital 985 "..." --merge` pour observer les comportements merge.

---

## Scénario 11 — NonIdempotent : lignes seed tracées dans seed_map

Après un merge, toutes les lignes NonIdempotent réinsérées doivent être tracées dans `demo_seed_map` avec `deleted_by_po=0`.

```sql
-- Vérifier que demo_seed_map contient des entrées pour les tables NonIdempotent
SELECT table_name, COUNT(*) AS nb_entries, SUM(deleted_by_po) AS nb_tombstones
FROM demo_seed_map
WHERE idc = 985
  AND table_name IN (
    'cyber_ev', 'customer_event', 'dsi_hw', 'dsi_lic', '_com_interv',
    '_com_contr', 'equipment_types', 'crm_aff', 'cust_suivi', 'dsi_hw_users',
    'project_step', 'project_news', 'project_meeting', 'project_backlog',
    'equipments', 'project_kpival', '_com_task', '_com_livrable', 'equipment_counters'
  )
GROUP BY table_name
ORDER BY table_name;
```

**Résultat attendu :** chaque table NonIdempotent présente dans le template apparaît avec `nb_entries > 0` et `nb_tombstones = 0`.

---

## Scénario 12 — Tombstone hard delete : ligne supprimée détectée

Simuler un hard delete d'une ligne seed, puis relancer le merge.

```sql
-- 1. Repérer une entrée seed dans dsi_hw pour idc=985
SELECT dsm.seed_key, dsm.real_id, dh.idhw, dh.lbhw
FROM demo_seed_map dsm
JOIN dsi_hw dh ON dh.idhw = dsm.real_id
WHERE dsm.idc = 985
  AND dsm.table_name = 'dsi_hw'
  AND dsm.deleted_by_po = 0
LIMIT 1;

-- 2. Supprimer physiquement la ligne (simule un hard delete PO)
-- DELETE FROM dsi_hw WHERE idhw = {real_id_trouvé};

-- 3. Relancer : php artisan demo:hospital 985 "..." --merge

-- 4. Vérifier que le tombstone est enregistré
SELECT seed_key, real_id, deleted_by_po, deleted_at
FROM demo_seed_map
WHERE idc = 985
  AND table_name = 'dsi_hw'
  AND deleted_by_po = 1;
```

**Résultat attendu :** la ligne supprimée apparaît dans `demo_seed_map` avec `deleted_by_po=1` et `deleted_at` renseigné. Elle n'est pas réinsérée lors du merge suivant.

---

## Scénario 13 — Tombstone soft delete : ligne dtdel détectée

Simuler un soft delete (mise à jour `dtdel`) sur une ligne seed.

```sql
-- 1. Repérer une entrée seed dans _com_interv pour idc=985
SELECT dsm.seed_key, dsm.real_id, ci.idinterv, ci.lbinterv, ci.dtdel
FROM demo_seed_map dsm
JOIN _com_interv ci ON ci.idinterv = dsm.real_id
WHERE dsm.idc = 985
  AND dsm.table_name = '_com_interv'
  AND dsm.deleted_by_po = 0
LIMIT 1;

-- 2. Soft-supprimer la ligne (simule une suppression PO)
-- UPDATE _com_interv SET dtdel = NOW() WHERE idinterv = {real_id_trouvé};

-- 3. Relancer : php artisan demo:hospital 985 "..." --merge

-- 4. Vérifier que le tombstone est enregistré avec deleted_at
SELECT seed_key, real_id, deleted_by_po, deleted_at
FROM demo_seed_map
WHERE idc = 985
  AND table_name = '_com_interv'
  AND deleted_by_po = 1;
```

**Résultat attendu :** `deleted_by_po=1` avec `deleted_at` correspondant à la valeur `dtdel` de la ligne.

---

## Scénario 14 — force-restore : tombstone ignoré et ligne recréée

Après la création d'un tombstone (scénario 12 ou 13), vérifier que `--force-restore` recrée la ligne.

```sql
-- Avant force-restore : vérifier l'état du tombstone
SELECT seed_key, real_id, deleted_by_po
FROM demo_seed_map
WHERE idc = 985
  AND table_name = 'dsi_hw'
  AND deleted_by_po = 1;

-- Lancer : php artisan demo:hospital 985 "..." --merge --force-restore

-- Après force-restore : le tombstone doit avoir disparu (deleted_by_po remis à 0 ou nouvelle entrée)
SELECT dsm.seed_key, dsm.real_id, dsm.deleted_by_po, dh.lbhw
FROM demo_seed_map dsm
JOIN dsi_hw dh ON dh.idhw = dsm.real_id
WHERE dsm.idc = 985
  AND dsm.table_name = 'dsi_hw'
ORDER BY dsm.seed_key;

-- La ligne doit exister en DB
SELECT idhw, lbhw, dtdel
FROM dsi_hw
WHERE idc = 985;
```

**Résultat attendu :** la ligne est réinsérée en DB, `demo_seed_map` n'a plus d'entrée `deleted_by_po=1` pour cette `seed_key`, et `dtdel` est NULL.

---

## Scénario 15 — SkipIfExists : données PO protégées

Vérifier que les tickets créés par le PO ne sont pas écrasés lors d'un merge.

```sql
-- 1. Compter les tickets actifs pour idc=985 avant merge
SELECT COUNT(*) AS nb_tickets_before
FROM customer_ask
WHERE idc = 985
  AND deleted_at IS NULL;

-- 2. Vérifier l'absence d'entrées seed_map pour 'tickets' (SkipIfExists ne trace pas)
SELECT COUNT(*) AS nb_seed_tickets
FROM demo_seed_map
WHERE idc = 985
  AND table_name = 'tickets';

-- 3. Lancer : php artisan demo:hospital 985 "..." --merge

-- 4. Recompter — le count doit être identique si le PO avait des tickets
SELECT COUNT(*) AS nb_tickets_after
FROM customer_ask
WHERE idc = 985
  AND deleted_at IS NULL;
```

**Résultat attendu :** si `nb_tickets_before > 0`, alors `nb_tickets_after = nb_tickets_before`. Les tickets PO sont intacts. Si l'instance est fraîche (tickets créés uniquement par le template), le premier merge les insère et les suivants les skippent.

---

## Scénario 16 — NonIdempotent : collision PK non-AI skippée

Pour les tables à PK non auto-increment, une collision avec une ligne PO doit être ignorée sans erreur.

```sql
-- Vérifier que equipment_types utilise une PK non-AI (vérification schéma)
SHOW CREATE TABLE equipment_types\G

-- Après merge, vérifier les warnings de collision dans les logs
-- (les collisions sont comptées dans le résultat de executeTable)

-- Vérifier les lignes equipment_types pour idc=985
SELECT idequipment_type, idc, lbequipment_type
FROM equipment_types
WHERE idc = 985
ORDER BY idequipment_type;

-- Comparer avec les entrées seed_map : toutes les lignes seed doivent être tracées
SELECT seed_key, real_id, deleted_by_po
FROM demo_seed_map
WHERE idc = 985
  AND table_name = 'equipment_types';
```

**Résultat attendu :** les lignes seed sont tracées. Si une collision PK a été skippée, le `real_id` n'est pas dans `demo_seed_map` pour ce `seed_key`.

---

## Scénario 17 — Cohérence globale seed_map après merge complet

Validation finale : intégrité du `demo_seed_map` après un merge complet.

```sql
-- 1. Toutes les real_id actives doivent exister en DB (pas de ghost entries)
-- Exemple sur dsi_hw
SELECT dsm.seed_key, dsm.real_id, dh.idhw
FROM demo_seed_map dsm
LEFT JOIN dsi_hw dh ON dh.idhw = dsm.real_id
WHERE dsm.idc = 985
  AND dsm.table_name = 'dsi_hw'
  AND dsm.deleted_by_po = 0
  AND dh.idhw IS NULL;
-- Résultat attendu : 0 lignes (pas de ghost)

-- 2. Pas de doublons seed_key par table
SELECT table_name, seed_key, COUNT(*) AS cnt
FROM demo_seed_map
WHERE idc = 985
GROUP BY table_name, seed_key
HAVING cnt > 1;
-- Résultat attendu : 0 lignes

-- 3. Résumé complet du seed_map pour idc=985
SELECT
  table_name,
  COUNT(*) AS total,
  SUM(CASE WHEN deleted_by_po = 0 THEN 1 ELSE 0 END) AS actives,
  SUM(CASE WHEN deleted_by_po = 1 THEN 1 ELSE 0 END) AS tombstones
FROM demo_seed_map
WHERE idc = 985
GROUP BY table_name
ORDER BY table_name;
```

**Résultat attendu :**
- Scénario 17a : 0 ghost entries
- Scénario 17b : 0 doublons de seed_key
- Scénario 17c : toutes les tables NonIdempotent du template apparaissent avec `actives > 0` et un nombre de tombstones cohérent avec les suppressions PO effectuées

---

## Phase 15 — Statut

**Phase 15 est complète.** Toutes les stratégies de merge sont implémentées et testées :

| Livrable | Statut |
|----------|--------|
| L1 — Infrastructure merge (SeedMapRepository, MergeStrategy, SemiIdempotentMergeStrategy, MergeRegistry) | ✅ Complet |
| L2 — StaticMergeStrategy, DatePolicy, DemoImporter merge mode | ✅ Complet |
| L3 — NonIdempotentMergeStrategy, SkipIfExistsMergeStrategy, TombstoneDetector, --force-restore | ✅ Complet |

**Tests automatisés :** 29 tests PHPUnit passent (540 assertions) — voir `tests/Feature/DemoAccount/`.
