Baseline of the P1 conception work produced over sessions 5-7 (was uncommitted in the working tree). 11-entity model, French naming. Superseded next by the prod-like revision (English, ~16 entities) per the 2026-06-04 decision session - this commit preserves the baseline in history before that rewrite.
22 KiB
Modele Logique des Donnees (MLD) - Wakdo
Phase Merise : P1 - Conception, etape 5 (apres MCD, MCT, MLT)
Statut : v0.1
Date : 2026-05-28
Branche : feat/p1-conception
Auteur methodologie : BYAN
1. Objet du document
Le MLD transcrit le MCD en schema relationnel formel : 1 entite -> 1 table, chaque association traduite selon sa cardinalite, contraintes referentielles materialisees, index dimensionnes pour les acces frequents.
C'est l'etape qui transforme la modelisation conceptuelle en specification implementable. Le DDL SQL (db/migrations/0001_init_schema.sql) sera derive directement de ce document a P2.
Source : dictionary.md (types et contraintes par attribut), mcd.md (entites + cardinalites + decisions reportees), mct.md (operations + entites manipulees), mlt.md (regles de gestion + transitions + protection concurrence).
Cibles :
- MariaDB 11.4 LTS (cf.
docker-compose.ymlservicewakdo-db) - Engine InnoDB (ACID, FKs, row-level locking, CHECK depuis 10.2.1)
- Charset
utf8mb4collationutf8mb4_unicode_ci
2. Conventions de notation
Notation relationnelle
TABLE_NAME (col1, col2, #col_fk, [col_optionnelle])
PK : col1
UK : col2
FK : col_fk -> AUTRE_TABLE(id)
| Symbole | Signification |
|---|---|
col |
Colonne NOT NULL |
[col] |
Colonne nullable |
#col |
Colonne FK (sans le diese : non-FK) |
Cette notation reste proche de l'usage Merise francais (UNIRIS, ouvrages Nanci/Espinasse) : la cle primaire est soulignee dans les documents classiques, ici on prefixe par PK pour la portabilite ASCII.
Types
Les types SQL exacts sont definis dans dictionary.md section 2 (Conventions generales) et reprecises dans chaque section de cette MLD. Conventions retenues :
INT UNSIGNED AUTO_INCREMENTpour toutes les PK techniquesINT UNSIGNEDpour tous les montants en centimes (anti-FLOAT cf. dictionary note 1)VARCHAR(<n>)avec longueur calibree selon dictionary note 7ENUM(...)pour les valeurs metier stables (cf. dictionary note 2)DATETIMEpour les timestamps (pas TIMESTAMP qui ferait du fuseau auto-implicite)
3. Regles de traduction MCD -> MLD
Les regles classiques de passage MCD -> MLD appliquees :
3.1 Entite -> Table
Chaque entite du MCD devient une table. L'identifiant conceptuel id devient PK technique INT UNSIGNED AUTO_INCREMENT. Les attributs gardent leurs noms et types.
3.2 Association (1,1) - (1,N) -> FK simple
L'entite cote (1,1) porte la FK vers l'entite cote (0,N) ou (1,N). Exemple :
CATEGORIE (1,1) <--regroupe--> (0,N) PRODUIT
devient
CATEGORIE (id, libelle, ...) -- pas de FK
PRODUIT (id, #categorie_id, ...) -- FK vers CATEGORIE
3.3 Association (0,N) - (0,N) ou (1,N) - (1,N) -> Table de jointure
L'association devient sa propre table avec PK composite des deux FKs. Exemple :
MENU (1,N) <--compose--> (0,N) PRODUIT (via MENU_PRODUIT)
devient
MENU_PRODUIT (#menu_id, #produit_id, role, position)
PK composite : (menu_id, produit_id)
3.4 Association porteuse d'attributs -> Table associative
Si une association MCD porte des attributs propres (role, position sur compose), elle devient table meme si elle pourrait theoriquement etre une FK. Cas applique a MENU_PRODUIT et ROLE_PERMISSION.
3.5 Polymorphisme -> 2 FKs nullables + discriminateur
LIGNE_COMMANDE -> (PRODUIT ou MENU) traduit en 2 colonnes FK nullable + 1 colonne discriminateur :
LIGNE_COMMANDE (id, #commande_id, type_item, [#produit_id], [#menu_id], ...)
CHECK ((type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL)
OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL))
Cf. docs/notes/polymorphic-fk-snapshots.md pour la justification.
3.6 Audit (event sourcing) -> Table dediee
COMMANDE_EVENT est une table append-only, traduction directe de l'entite MCD 3.7. Aucune denormalisation user_id sur commande (cf. dictionary note 10).
4. Schema relationnel formel
Les 11 tables qui composent le schema Wakdo, ordonnees par dependance (les tables sans FK d'abord, puis les tables qui dependent d'elles).
4.1 categorie
categorie (id, libelle, slug, image_path, ordre, est_actif, created_at, updated_at)
PK : id
UK : libelle
UK : slug
Types :
id INT UNSIGNED AUTO_INCREMENTlibelle VARCHAR(80) NOT NULLslug VARCHAR(60) NOT NULLimage_path VARCHAR(255) NULL(cf. dictionary note 11)ordre SMALLINT UNSIGNED NOT NULL DEFAULT 0est_actif TINYINT(1) NOT NULL DEFAULT 1created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMPupdated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
Aucune FK. Table racine du sous-domaine Catalogue.
4.2 produit
produit (id, #categorie_id, libelle, [description], prix_ttc_cents, [image_path],
est_disponible, ordre, created_at, updated_at)
PK : id
FK : categorie_id -> categorie(id) ON DELETE RESTRICT
IDX : (categorie_id, est_disponible, ordre)
Types :
id INT UNSIGNED AUTO_INCREMENTcategorie_id INT UNSIGNED NOT NULLlibelle VARCHAR(120) NOT NULLdescription TEXT NULLprix_ttc_cents INT UNSIGNED NOT NULL CHECK (prix_ttc_cents > 0)image_path VARCHAR(255) NULLest_disponible TINYINT(1) NOT NULL DEFAULT 1ordre SMALLINT UNSIGNED NOT NULL DEFAULT 0created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMPupdated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
ON DELETE RESTRICT sur categorie_id : impossible de supprimer une categorie qui contient des produits (protection metier, evite les orphelins).
4.3 menu
menu (id, #categorie_id, libelle, [description], prix_ttc_cents, [image_path],
est_disponible, ordre, created_at, updated_at)
PK : id
FK : categorie_id -> categorie(id) ON DELETE RESTRICT
IDX : (categorie_id, est_disponible, ordre)
Types : identiques a produit (meme structure, semantique distincte cf. dictionary note 3).
4.4 menu_produit (table associative)
menu_produit (#menu_id, #produit_id, role, position)
PK : (menu_id, produit_id)
FK : menu_id -> menu(id) ON DELETE CASCADE
FK : produit_id -> produit(id) ON DELETE RESTRICT
IDX : (menu_id, position)
Types :
menu_id INT UNSIGNED NOT NULLproduit_id INT UNSIGNED NOT NULLrole ENUM('burger','accompagnement','boisson','sauce','dessert') NOT NULLposition SMALLINT UNSIGNED NOT NULL DEFAULT 0
ON DELETE CASCADE sur menu_id : si un menu est supprime, ses compositions le sont aussi.
ON DELETE RESTRICT sur produit_id : impossible de supprimer un produit utilise dans un menu (protection integrite menu).
Pas d'updated_at (table de jointure, cf. dictionary note 5 : les jointures sont supprimees+recreees, pas modifiees).
4.5 commande
commande (id, numero, source, mode_consommation, statut,
total_ht_cents, total_tva_cents, total_ttc_cents, tva_taux_pourmille,
[paye_a], created_at, updated_at)
PK : id
UK : numero
IDX : (source, created_at)
IDX : (statut, created_at)
IDX : created_at
CHECK : source != 'drive' OR mode_consommation = 'drive'
CHECK : total_ttc_cents = total_ht_cents + total_tva_cents
Types :
id INT UNSIGNED AUTO_INCREMENTnumero VARCHAR(20) NOT NULLsource ENUM('kiosk','comptoir','drive') NOT NULLmode_consommation ENUM('sur_place','a_emporter','drive') NOT NULLstatut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') NOT NULL DEFAULT 'pending_payment'total_ht_cents INT UNSIGNED NOT NULL CHECK (total_ht_cents >= 0)total_tva_cents INT UNSIGNED NOT NULL CHECK (total_tva_cents >= 0)total_ttc_cents INT UNSIGNED NOT NULL CHECK (total_ttc_cents > 0)tva_taux_pourmille SMALLINT UNSIGNED NOT NULLpaye_a DATETIME NULL(NULL avant paiement, timestamp du passage enpaid)created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMPupdated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
CHECK croise source/mode_consommation (cf. dictionary note 8) : empeche les combinaisons invalides au niveau SGBD plutot que de se reposer uniquement sur le code applicatif.
CHECK montants : invariant TTC = HT + TVA verifie en base (defense-in-depth contre les bugs de calcul applicatif).
Aucune FK directe vers user : la tracabilite passe par commande_event (cf. 4.7).
4.6 ligne_commande
ligne_commande (id, #commande_id, type_item, [#produit_id], [#menu_id],
libelle_snapshot, prix_unitaire_ttc_cents_snapshot, quantite, created_at)
PK : id
FK : commande_id -> commande(id) ON DELETE CASCADE
FK : produit_id -> produit(id) ON DELETE RESTRICT
FK : menu_id -> menu(id) ON DELETE RESTRICT
IDX : commande_id
CHECK : (type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL)
OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL)
Types :
id INT UNSIGNED AUTO_INCREMENTcommande_id INT UNSIGNED NOT NULLtype_item ENUM('produit','menu') NOT NULLproduit_id INT UNSIGNED NULLmenu_id INT UNSIGNED NULLlibelle_snapshot VARCHAR(120) NOT NULLprix_unitaire_ttc_cents_snapshot INT UNSIGNED NOT NULL CHECK (prix_unitaire_ttc_cents_snapshot > 0)quantite SMALLINT UNSIGNED NOT NULL DEFAULT 1 CHECK (quantite > 0)created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON DELETE CASCADE sur commande_id : si la commande disparait, ses lignes aussi.
ON DELETE RESTRICT sur produit_id et menu_id : impossible de supprimer un produit/menu reference par une commande historique (preserve les references meme si on snapshote).
CHECK polymorphisme : exclusivite mutuelle produit_id / menu_id selon type_item (cf. dictionary note 6).
4.7 commande_event
commande_event (id, #commande_id, event_type, [from_statut], to_statut,
[#user_id], [payload], created_at)
PK : id
FK : commande_id -> commande(id) ON DELETE CASCADE
FK : user_id -> user(id) ON DELETE SET NULL
IDX : (commande_id, created_at)
IDX : (user_id, created_at)
IDX : (event_type, created_at)
Types :
id INT UNSIGNED AUTO_INCREMENTcommande_id INT UNSIGNED NOT NULLevent_type ENUM('CREATED','PAID','PREPARING_STARTED','READY','DELIVERED','CANCELLED') NOT NULLfrom_statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') NULLto_statut ENUM('pending_payment','paid','preparing','ready','delivered','cancelled') NOT NULLuser_id INT UNSIGNED NULLpayload JSON NULLcreated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
ON DELETE CASCADE sur commande_id : si la commande est purgee, son journal disparait avec elle.
ON DELETE SET NULL sur user_id : si un equipier est supprime, les events restent (l'audit reste consultable, l'attribution individuelle est perdue).
Pas d'updated_at : table append-only. Aucun UPDATE applicatif autorise (cf. mlt.md RG-T10).
Pas de CHECK croise from_statut/to_statut : la verification de la machine a etats est applicative (mlt section 12), un CHECK SQL serait trop rigide (event_type peut prendre des valeurs non encore prevues).
4.8 user
user (id, email, password_hash, nom, prenom, #role_id, est_actif, [last_login_at],
created_at, updated_at)
PK : id
UK : email
FK : role_id -> role(id) ON DELETE RESTRICT
IDX : (est_actif, role_id)
Types :
id INT UNSIGNED AUTO_INCREMENTemail VARCHAR(254) NOT NULL(RFC 5321)password_hash VARCHAR(255) NOT NULL(argon2id, cf..envPASSWORD_ALGO)nom VARCHAR(60) NOT NULLprenom VARCHAR(60) NOT NULLrole_id INT UNSIGNED NOT NULLest_actif TINYINT(1) NOT NULL DEFAULT 1last_login_at DATETIME NULLcreated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMPupdated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
ON DELETE RESTRICT sur role_id : impossible de supprimer un role qui a encore des users (passer par est_actif = 0 sur le role avant de supprimer).
4.9 role
role (id, code, libelle, [description], est_actif, created_at, updated_at)
PK : id
UK : code
Types :
id INT UNSIGNED AUTO_INCREMENTcode VARCHAR(40) NOT NULLlibelle VARCHAR(80) NOT NULLdescription TEXT NULLest_actif TINYINT(1) NOT NULL DEFAULT 1created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMPupdated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
Aucune FK. Table racine du sous-domaine RBAC.
4.10 permission
permission (id, code, libelle, [description], created_at)
PK : id
UK : code
Types :
id INT UNSIGNED AUTO_INCREMENTcode VARCHAR(60) NOT NULL(format<resource>.<action>)libelle VARCHAR(120) NOT NULLdescription TEXT NULLcreated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
Pas d'updated_at : les permissions sont declarees en migration et ne sont pas modifiees via UI (cf. RBAC statique cote permissions, dictionary 3.10 et MCD 4.3).
4.11 role_permission (table associative)
role_permission (#role_id, #permission_id)
PK : (role_id, permission_id)
FK : role_id -> role(id) ON DELETE CASCADE
FK : permission_id -> permission(id) ON DELETE CASCADE
IDX : permission_id (acces inverse "quels roles ont cette permission ?")
Types :
role_id INT UNSIGNED NOT NULLpermission_id INT UNSIGNED NOT NULL
ON DELETE CASCADE des deux cotes : suppression d'un role ou d'une permission supprime ses mappings.
Pas de timestamps (table de jointure pure, cf. dictionary note 5).
5. Recapitulatif des contraintes referentielles
| FK | Reference | ON DELETE | Justification |
|---|---|---|---|
produit.categorie_id |
categorie(id) |
RESTRICT | Pas d'orphelin produit |
menu.categorie_id |
categorie(id) |
RESTRICT | Idem |
menu_produit.menu_id |
menu(id) |
CASCADE | Composition disparait avec le menu |
menu_produit.produit_id |
produit(id) |
RESTRICT | Pas de cascade : un produit reference dans un menu ne peut pas etre supprime sans amender la composition |
commande.-- |
(aucune FK vers user) | - | Tracabilite via commande_event |
ligne_commande.commande_id |
commande(id) |
CASCADE | Lignes disparaissent avec la commande |
ligne_commande.produit_id |
produit(id) |
RESTRICT | Preserve l'integrite historique |
ligne_commande.menu_id |
menu(id) |
RESTRICT | Idem |
commande_event.commande_id |
commande(id) |
CASCADE | Journal disparait avec la commande |
commande_event.user_id |
user(id) |
SET NULL | Audit conserve, attribution individuelle perdue |
user.role_id |
role(id) |
RESTRICT | Pas d'user sans role |
role_permission.role_id |
role(id) |
CASCADE | Mapping disparait avec le role |
role_permission.permission_id |
permission(id) |
CASCADE | Mapping disparait avec la permission |
Cles :
- CASCADE : la donnee dependante n'a pas de sens hors de son parent (lignes / events / mappings)
- RESTRICT : suppression du parent bloquee tant que des references existent (catalogue, role)
- SET NULL : preserve la donnee enfant en perdant le lien (audit event sans attribution)
6. Index complementaires
Au-dela des PK / UK / FK qui creent des index automatiquement, indexes ajoutes pour les requetes frequentes identifiees au MCT/MLT :
| Table | Index | Justification (operation MCT) |
|---|---|---|
produit |
(categorie_id, est_disponible, ordre) |
Chargement catalogue kiosk (op 1) : filtre par categorie + disponible + tri par ordre |
menu |
(categorie_id, est_disponible, ordre) |
Idem produit |
menu_produit |
(menu_id, position) |
Chargement composition d'un menu |
commande |
(source, created_at) |
Stats "par canal" + tri chronologique |
commande |
(statut, created_at) |
Files d'attente preparation/accueil (ops 6, 9) |
commande |
created_at |
Stats agregations live |
ligne_commande |
commande_id |
Recuperation des lignes d'une commande |
commande_event |
(commande_id, created_at) |
Historique d'une commande |
commande_event |
(user_id, created_at) |
Actions d'un equipier sur une periode |
commande_event |
(event_type, created_at) |
Stats "combien de cancellations cette semaine ?" |
user |
(est_actif, role_id) |
Login + permissions check (op 23) |
role_permission |
permission_id |
Acces inverse "quels roles ont cette permission ?" |
Index NON ajoutes (volontaire) :
commande.numero: UK suffit, pas de range query attendue dessuscommande.mode_consommation: faible cardinalite (3 valeurs), un index n'est pas rentable, full scan acceptablecommande.paye_a: NULL pour la majorite des lignes (commande encore en cours), index peu utile
7. Contraintes CHECK (MariaDB 10.2+)
Verification au niveau SGBD pour les invariants critiques. Defense-in-depth contre les bugs applicatifs.
| Table | CHECK | Pourquoi |
|---|---|---|
produit |
prix_ttc_cents > 0 |
Prix nul ou negatif = bug |
menu |
prix_ttc_cents > 0 |
Idem |
commande |
total_ht_cents >= 0 |
Plancher autorise (commande vide transitoire ?) |
commande |
total_tva_cents >= 0 |
Idem |
commande |
total_ttc_cents > 0 |
TTC nul = bug |
commande |
total_ttc_cents = total_ht_cents + total_tva_cents |
Invariant arithmetique |
commande |
source != 'drive' OR mode_consommation = 'drive' |
Contrainte croisee (dictionary note 8, mlt RG-T09) |
ligne_commande |
prix_unitaire_ttc_cents_snapshot > 0 |
Snapshot prix non nul |
ligne_commande |
quantite > 0 |
Quantite non nulle |
ligne_commande |
(type_item='produit' AND produit_id IS NOT NULL AND menu_id IS NULL) OR (type_item='menu' AND menu_id IS NOT NULL AND produit_id IS NULL) |
Polymorphisme exclusif (dictionary note 6) |
8. Cross-validation entites MCD -> tables MLD
| Entite MCD | Table MLD | Notes |
|---|---|---|
categorie (3.1) |
categorie (4.1) |
1:1 |
produit (3.2) |
produit (4.2) |
1:1 |
menu (3.3) |
menu (4.3) |
1:1 |
menu_produit (3.4) |
menu_produit (4.4) |
Entite associative -> table de jointure avec PK composite |
commande (3.5) |
commande (4.5) |
1:1, attribut source ajoute (decision 2026-05-28) |
ligne_commande (3.6) |
ligne_commande (4.6) |
1:1, polymorphisme materialise par 2 FKs nullables + CHECK |
commande_event (3.7) |
commande_event (4.7) |
1:1, table append-only, decision 2026-05-28 |
user (3.8) |
user (4.8) |
1:1 |
role (3.9) |
role (4.9) |
1:1 |
permission (3.10) |
permission (4.10) |
1:1 |
role_permission (3.11) |
role_permission (4.11) |
Entite associative -> table de jointure avec PK composite |
Conclusion : 11/11 entites tracees. Aucune entite MCD ne reste sans table, aucune table MLD ne sort du modele conceptuel.
9. Estimation volumes et taille
| Table | Volume 6 mois | Taille moyenne ligne | Taille totale |
|---|---|---|---|
categorie |
~10 | 200 octets | < 1 Ko |
produit |
~70 | 400 octets | ~30 Ko |
menu |
~15 | 400 octets | ~6 Ko |
menu_produit |
~80 | 30 octets | ~2 Ko |
commande |
~30k | 300 octets | ~9 Mo |
ligne_commande |
~150k | 200 octets | ~30 Mo |
commande_event |
~180k | 200 octets | ~36 Mo |
user |
~20 | 500 octets | ~10 Ko |
role |
~5 | 200 octets | ~1 Ko |
permission |
~40 | 300 octets | ~12 Ko |
role_permission |
~80 | 30 octets | ~2 Ko |
Total : ~75 Mo sur 6 mois. Largement gerable par MariaDB sur le conteneur Wakdo (volume wakdo_db_data named volume, cf. docker-compose.yml).
Les indexes ajoutent typiquement 30-50% du volume des tables, soit ~30 Mo supplementaires. Estimation totale : ~100-110 Mo / 6 mois.
10. Decisions reportees au DDL et a P2
Les decisions suivantes sont laissees au DDL (db/migrations/0001_init_schema.sql) ou aux phases ulterieures, parce qu'elles concernent l'implementation et pas la modelisation logique :
- Triggers ou colonnes generees :
service_day(cf. PROJECT_CONTEXT section 2) pourrait etre uneGENERATED ALWAYS AS (DATE_SUB(created_at, INTERVAL 4 HOUR + 30 MINUTE))virtuelle pour eviter le calcul applicatif. A evaluer en P3 si les stats sont penibles. - Partitionnement :
commande_eventpourrait etre partitionne par mois si le volume depasse les estimations. Pas pour MVP. - Foreign Key index : MariaDB cree automatiquement un index sur la FK lors de la declaration, sauf si un index utilisable existe deja. A verifier explicitement dans le DDL.
- Collation :
utf8mb4_unicode_ciretenu (sensible diacritiques et casse pour les recherches). Si besoin de tri locale francais strict, passer enutf8mb4_fr_0900_ai_ci(MySQL 8) ou rester enunicode_ci. - Engine :
InnoDBpar defaut (ACID + FKs). Pas de MEMORY ni Archive. - Charset emoji :
utf8mb4(4 octets / char max) couvre les emojis au cas ou ils apparaitraient dansdescriptionproduit oupayloadJSON.
11. A faire au prochain sprint (DDL + Seed)
-
DDL (
db/migrations/0001_init_schema.sql) : transcrire ce MLD en CREATE TABLE executables, dans l'ordre des dependances (categorie -> produit/menu -> menu_produit -> commande -> ligne_commande/commande_event ; permission -> role -> role_permission ; user en dernier). -
Seed (
db/seeds/0001_demo_data.sql) : INSERT pour :- 9 categories + 53 produits + 13 menus a partir des JSON sources (
docs/merise/_sources/), prix normalises en centimes - 1 admin par defaut + roles (admin, manager, equipier-comptoir, equipier-drive)
- Permissions declarees (CRUD produit/menu/categorie/user/role + commande operationnelles)
- Quelques commandes d'exemple pour les demos
- 9 categories + 53 produits + 13 menus a partir des JSON sources (
-
Export fallback JSON (
scripts/export-fallback.{sh|php}) : extrait des donnees seed verssrc/public/borne/data/*.jsonpour le mode "Bloc 1 isole" (kiosk sans BDD pour les tests). -
Tests de validation DDL : verifier que :
- Toutes les CHECK contraintes sont declenchees comme attendu (tests d'integration)
- Les ON DELETE CASCADE / RESTRICT se comportent comme specifie
- Les indexes accelerent reellement les requetes ciblees (EXPLAIN sur les requetes types du MCT)
-
Migration tooling : decider de l'outil (phinx, doctrine migrations, ou homemade PHP script). Cf. PROJECT_CONTEXT pour le choix retenu.