From ba2abbfae9d36ea01b5469d66c411fa93abafea9 Mon Sep 17 00:00:00 2001 From: Corentin JOGUET Date: Thu, 25 Jun 2026 10:58:39 +0200 Subject: [PATCH] fix(db): suivi seeds unifie + idempotence DDL + doc trou 0004 (#111) --- db/README.md | 48 +++++++++++++++++---- db/migrations/0001_init_schema.sql | 48 ++++++++++++--------- db/migrations/0002_pin_throttle.sql | 5 ++- db/migrations/0003_order_service_tag.sql | 20 ++++++++- db/migrations/0005_ingredient_nutrition.sql | 24 +++++++++-- db/seed.sh | 22 +++++++--- 6 files changed, 124 insertions(+), 43 deletions(-) diff --git a/db/README.md b/db/README.md index 843915e..067c478 100644 --- a/db/README.md +++ b/db/README.md @@ -6,23 +6,53 @@ Transcription executable du MLD (`docs/merise/mld.md`, 21 tables) vers MariaDB 1 ``` db/ - migrations/ migrations SQL versionnees, appliquees dans l'ordre lexicographique + migrations/ migrations SQL versionnees, appliquees dans l'ordre lexicographique 0001_init_schema.sql schema initial : 21 tables, FK, CHECK, index (InnoDB, utf8mb4) - seeds/ donnees de demonstration (a venir : roles/permissions, allergenes, catalogue) - migrate.sh runner de migrations (idempotent) + seeds/ donnees de reference (RBAC, allergenes, catalogue, variantes) + migrate-container.sh runner de boot IN-CONTAINER (canonique, service wakdo-migrate) + migrate.sh runner de migrations cote HOTE (manuel, via docker exec) + seed.sh runner de seeds cote HOTE (manuel, via docker exec) ``` -## Appliquer les migrations +## Numerotation des migrations (trou 0004 assume) + +Les migrations sautent `0004` : la sequence est `0001, 0002, 0003, 0005, 0006, +0007`. Ce n'est PAS un fichier manquant mais un desalignement historique assume : +le numero `0004` a ete consomme cote `seeds/` (`0004_menu_side_maxi.sql`) lors +d'un meme increment de travail, sans contrepartie cote `migrations/`. Le suivi se +fait par NOM DE FICHIER (`schema_migrations`), pas par numero contigu : le trou +est donc inoffensif (rien ne presuppose une sequence sans lacune). Convention +conservee : ne pas reattribuer `0004` cote migrations pour eviter toute confusion +avec le seed homonyme ; la prochaine migration prend le numero suivant disponible. + +## Appliquer les migrations + seeds + +Chemin canonique (boot de la stack) : le service one-shot `wakdo-migrate` +(`docker compose up`) execute `db/migrate-container.sh`, qui applique +`db/migrations/*.sql` (suivi : table `schema_migrations`) PUIS `db/seeds/*.sql` +(suivi : table `seeds_applied`), de maniere idempotente. Il se connecte a +`wakdo-db` par le reseau compose. + +Chemin manuel (hote, via `docker exec`) : ```bash bash db/migrate.sh # applique les migrations en attente -bash db/migrate.sh --status # liste l'etat sans rien appliquer +bash db/migrate.sh --status # liste l'etat des migrations sans rien appliquer +bash db/seed.sh # charge les seeds en attente +bash db/seed.sh --status # liste l'etat des seeds sans rien charger ``` -Le runner cible le conteneur `wakdo-db` et lit les identifiants dans `.env` -(`DB_NAME`, `DB_ROOT_PASSWORD`). Il maintient une table `schema_migrations` -(une ligne par fichier applique) : relancer ne rejoue que les nouvelles -migrations. La cible `bash db/migrate.sh` est destinee a appeler ce script. +Les runners hote ciblent le conteneur `wakdo-db` et lisent les identifiants dans +`.env` (`DB_NAME`, `DB_ROOT_PASSWORD`). + +### Suivi partage entre les deux chemins + +Les runners hote et conteneur partagent les MEMES tables de suivi (memes noms, +memes colonnes `filename` / `applied_at`) : `schema_migrations` pour les +migrations, `seeds_applied` pour les seeds. Consequence : rejouer un chemin apres +l'autre ne replaye RIEN. (Auparavant `db/seed.sh` suivait une table distincte +`seed_history`, ce qui pouvait lui faire re-jouer des seeds deja charges par +`wakdo-migrate` et echouer sur une contrainte UNIQUE — corrige.) ## Conventions diff --git a/db/migrations/0001_init_schema.sql b/db/migrations/0001_init_schema.sql index 9e564d8..59f5db8 100644 --- a/db/migrations/0001_init_schema.sql +++ b/db/migrations/0001_init_schema.sql @@ -18,6 +18,12 @@ -- the application layer. -- - No CREATE DATABASE / USE here: the target DB is chosen by the runner. -- - No seed / INSERT data here (see db/seeds/0001_demo_data.sql). +-- +-- Idempotence (defense en profondeur, suivi par schema_migrations) : chaque table +-- est creee en CREATE TABLE IF NOT EXISTS. Index, cles uniques et contraintes +-- (FK / CHECK) sont declares INLINE dans le CREATE TABLE : ils ne sont donc pas +-- re-joues quand la table preexiste. Re-jouer ce fichier sur une base deja +-- migree ne modifie pas le schema (aucun ALTER autonome). -- ============================================================================= SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -29,7 +35,7 @@ SET FOREIGN_KEY_CHECKS = 0; -- ----------------------------------------------------------------------------- -- 4.1 category — root table for the Catalogue sub-domain (no FK) -- ----------------------------------------------------------------------------- -CREATE TABLE category ( +CREATE TABLE IF NOT EXISTS category ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(60) NOT NULL, slug VARCHAR(60) NOT NULL, @@ -46,7 +52,7 @@ CREATE TABLE category ( -- ----------------------------------------------------------------------------- -- 4.6 ingredient — root table for Ingredients & Stock (no FK) -- ----------------------------------------------------------------------------- -CREATE TABLE ingredient ( +CREATE TABLE IF NOT EXISTS ingredient ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(120) NOT NULL, unit VARCHAR(40) NOT NULL, @@ -71,7 +77,7 @@ CREATE TABLE ingredient ( -- ----------------------------------------------------------------------------- -- 4.8 allergen — reference table (INCO EU 1169/2011), no FK -- ----------------------------------------------------------------------------- -CREATE TABLE allergen ( +CREATE TABLE IF NOT EXISTS allergen ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, code VARCHAR(30) NOT NULL, name VARCHAR(80) NOT NULL, @@ -83,7 +89,7 @@ CREATE TABLE allergen ( -- ----------------------------------------------------------------------------- -- 4.10 role — root table for RBAC (no FK) -- ----------------------------------------------------------------------------- -CREATE TABLE role ( +CREATE TABLE IF NOT EXISTS role ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, code VARCHAR(40) NOT NULL, label VARCHAR(80) NOT NULL, @@ -100,7 +106,7 @@ CREATE TABLE role ( -- ----------------------------------------------------------------------------- -- 4.13 permission — reference table, catalogue frozen at 23 codes (no FK) -- ----------------------------------------------------------------------------- -CREATE TABLE permission ( +CREATE TABLE IF NOT EXISTS permission ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, code VARCHAR(60) NOT NULL, label VARCHAR(120) NOT NULL, @@ -113,7 +119,7 @@ CREATE TABLE permission ( -- ----------------------------------------------------------------------------- -- 4.21 login_throttle — per-source-IP brute-force throttle (no FK) -- ----------------------------------------------------------------------------- -CREATE TABLE login_throttle ( +CREATE TABLE IF NOT EXISTS login_throttle ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, ip_address VARCHAR(45) NOT NULL, failed_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0, @@ -128,7 +134,7 @@ CREATE TABLE login_throttle ( -- ----------------------------------------------------------------------------- -- 4.2 product — depends on category -- ----------------------------------------------------------------------------- -CREATE TABLE product ( +CREATE TABLE IF NOT EXISTS product ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, category_id INT UNSIGNED NOT NULL, name VARCHAR(120) NOT NULL, @@ -151,7 +157,7 @@ CREATE TABLE product ( -- ----------------------------------------------------------------------------- -- 4.3 menu — depends on category, product -- ----------------------------------------------------------------------------- -CREATE TABLE menu ( +CREATE TABLE IF NOT EXISTS menu ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, category_id INT UNSIGNED NOT NULL, burger_product_id INT UNSIGNED NOT NULL, @@ -177,7 +183,7 @@ CREATE TABLE menu ( -- ----------------------------------------------------------------------------- -- 4.4 menu_slot — depends on menu (no audit fields) -- ----------------------------------------------------------------------------- -CREATE TABLE menu_slot ( +CREATE TABLE IF NOT EXISTS menu_slot ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, menu_id INT UNSIGNED NOT NULL, name VARCHAR(80) NOT NULL, @@ -194,7 +200,7 @@ CREATE TABLE menu_slot ( -- 4.5 menu_slot_option — pure join table, composite PK -- depends on menu_slot, product -- ----------------------------------------------------------------------------- -CREATE TABLE menu_slot_option ( +CREATE TABLE IF NOT EXISTS menu_slot_option ( menu_slot_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, PRIMARY KEY (menu_slot_id, product_id), @@ -209,7 +215,7 @@ CREATE TABLE menu_slot_option ( -- 4.7 product_ingredient — join table with attributes, composite PK -- depends on product, ingredient -- ----------------------------------------------------------------------------- -CREATE TABLE product_ingredient ( +CREATE TABLE IF NOT EXISTS product_ingredient ( product_id INT UNSIGNED NOT NULL, ingredient_id INT UNSIGNED NOT NULL, quantity_normal SMALLINT UNSIGNED NOT NULL DEFAULT 1, @@ -232,7 +238,7 @@ CREATE TABLE product_ingredient ( -- 4.9 ingredient_allergen — pure join table, composite PK -- depends on ingredient, allergen -- ----------------------------------------------------------------------------- -CREATE TABLE ingredient_allergen ( +CREATE TABLE IF NOT EXISTS ingredient_allergen ( ingredient_id INT UNSIGNED NOT NULL, allergen_id INT UNSIGNED NOT NULL, PRIMARY KEY (ingredient_id, allergen_id), @@ -246,7 +252,7 @@ CREATE TABLE ingredient_allergen ( -- ----------------------------------------------------------------------------- -- 4.11 user — depends on role -- ----------------------------------------------------------------------------- -CREATE TABLE user ( +CREATE TABLE IF NOT EXISTS user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, email VARCHAR(254) NOT NULL, password_hash VARCHAR(255) NOT NULL, @@ -275,7 +281,7 @@ CREATE TABLE user ( -- 4.12 role_visible_source — pure join table, composite PK -- depends on role -- ----------------------------------------------------------------------------- -CREATE TABLE role_visible_source ( +CREATE TABLE IF NOT EXISTS role_visible_source ( role_id INT UNSIGNED NOT NULL, source ENUM('kiosk','counter','drive') NOT NULL, PRIMARY KEY (role_id, source), @@ -287,7 +293,7 @@ CREATE TABLE role_visible_source ( -- 4.14 role_permission — pure join table, composite PK -- depends on role, permission -- ----------------------------------------------------------------------------- -CREATE TABLE role_permission ( +CREATE TABLE IF NOT EXISTS role_permission ( role_id INT UNSIGNED NOT NULL, permission_id INT UNSIGNED NOT NULL, PRIMARY KEY (role_id, permission_id), @@ -301,7 +307,7 @@ CREATE TABLE role_permission ( -- ----------------------------------------------------------------------------- -- 4.15 customer_order — depends on user (acting_user_id) -- ----------------------------------------------------------------------------- -CREATE TABLE customer_order ( +CREATE TABLE IF NOT EXISTS customer_order ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_number VARCHAR(20) NOT NULL, idempotency_key VARCHAR(36) NULL, @@ -336,7 +342,7 @@ CREATE TABLE customer_order ( -- 4.16 order_item — depends on customer_order, product, menu -- polymorphic line (product XOR menu) -- ----------------------------------------------------------------------------- -CREATE TABLE order_item ( +CREATE TABLE IF NOT EXISTS order_item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_id INT UNSIGNED NOT NULL, item_type ENUM('product','menu') NOT NULL, @@ -370,7 +376,7 @@ CREATE TABLE order_item ( -- ----------------------------------------------------------------------------- -- 4.17 order_item_selection — depends on order_item, menu_slot, product -- ----------------------------------------------------------------------------- -CREATE TABLE order_item_selection ( +CREATE TABLE IF NOT EXISTS order_item_selection ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_item_id INT UNSIGNED NOT NULL, menu_slot_id INT UNSIGNED NOT NULL, @@ -391,7 +397,7 @@ CREATE TABLE order_item_selection ( -- ----------------------------------------------------------------------------- -- 4.18 order_item_modifier — depends on order_item, ingredient -- ----------------------------------------------------------------------------- -CREATE TABLE order_item_modifier ( +CREATE TABLE IF NOT EXISTS order_item_modifier ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_item_id INT UNSIGNED NOT NULL, ingredient_id INT UNSIGNED NOT NULL, @@ -411,7 +417,7 @@ CREATE TABLE order_item_modifier ( -- 4.19 stock_movement — append-only audit log -- depends on ingredient, customer_order, user -- ----------------------------------------------------------------------------- -CREATE TABLE stock_movement ( +CREATE TABLE IF NOT EXISTS stock_movement ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, ingredient_id INT UNSIGNED NOT NULL, movement_type ENUM('sale','cancellation','restock','inventory_correction') NOT NULL, @@ -437,7 +443,7 @@ CREATE TABLE stock_movement ( -- 4.20 audit_log — append-only sensitive-action log -- depends on user, role -- ----------------------------------------------------------------------------- -CREATE TABLE audit_log ( +CREATE TABLE IF NOT EXISTS audit_log ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, actor_user_id INT UNSIGNED NULL, actor_role_id INT UNSIGNED NULL, diff --git a/db/migrations/0002_pin_throttle.sql b/db/migrations/0002_pin_throttle.sql index 3746fc2..6ab1a9c 100644 --- a/db/migrations/0002_pin_throttle.sql +++ b/db/migrations/0002_pin_throttle.sql @@ -16,7 +16,10 @@ SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; -CREATE TABLE pin_throttle ( +-- Idempotence (defense en profondeur) : CREATE TABLE IF NOT EXISTS. La cle +-- unique, l'index et la FK sont inline dans le CREATE TABLE, donc non re-joues +-- quand la table preexiste. Re-jouer ce fichier ne modifie pas le schema. +CREATE TABLE IF NOT EXISTS pin_throttle ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, actor_user_id INT UNSIGNED NOT NULL, failed_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0, diff --git a/db/migrations/0003_order_service_tag.sql b/db/migrations/0003_order_service_tag.sql index 13c5c98..790e4d0 100644 --- a/db/migrations/0003_order_service_tag.sql +++ b/db/migrations/0003_order_service_tag.sql @@ -13,5 +13,21 @@ SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE customer_order - ADD COLUMN service_tag VARCHAR(20) NULL AFTER service_mode; +-- Idempotence : meme garde information_schema que 0006/0007 (re-jouable sans +-- erreur). On verifie l'absence de la colonne `service_tag` avant l'ALTER ; +-- si elle existe deja, on execute un no-op (DO 0). Le schema resultant est +-- inchange : seul l'ajout de la colonne (si absente) est joue. +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'customer_order' AND column_name = 'service_tag' +); + +SET @ddl := IF( + @col_exists = 0, + 'ALTER TABLE customer_order + ADD COLUMN service_tag VARCHAR(20) NULL AFTER service_mode', + 'DO 0' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/db/migrations/0005_ingredient_nutrition.sql b/db/migrations/0005_ingredient_nutrition.sql index 838233a..46ee59a 100644 --- a/db/migrations/0005_ingredient_nutrition.sql +++ b/db/migrations/0005_ingredient_nutrition.sql @@ -14,10 +14,26 @@ SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; -ALTER TABLE ingredient - ADD COLUMN energy_kcal_100g SMALLINT UNSIGNED NULL AFTER pack_label, - ADD COLUMN nutrition_source VARCHAR(120) NULL AFTER energy_kcal_100g, - ADD COLUMN nutrition_fetched_at DATETIME NULL AFTER nutrition_source; +-- Idempotence : meme garde information_schema que 0007 (re-jouable sans erreur). +-- Les trois colonnes sont ajoutees ensemble ; l'existence de la premiere +-- (`energy_kcal_100g`) suffit donc a court-circuiter le groupe. Si elle existe +-- deja, on execute un no-op (DO 0). Le schema resultant est inchange. +SET @col_exists := ( + SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'ingredient' AND column_name = 'energy_kcal_100g' +); + +SET @ddl := IF( + @col_exists = 0, + 'ALTER TABLE ingredient + ADD COLUMN energy_kcal_100g SMALLINT UNSIGNED NULL AFTER pack_label, + ADD COLUMN nutrition_source VARCHAR(120) NULL AFTER energy_kcal_100g, + ADD COLUMN nutrition_fetched_at DATETIME NULL AFTER nutrition_source', + 'DO 0' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; -- energy_kcal_100g : apport energetique pour 100 g (SMALLINT UNSIGNED suffit ; les -- valeurs reelles restent < 1000). nutrition_source : provenance ("OpenFoodFacts"). diff --git a/db/seed.sh b/db/seed.sh index 864ac8f..a8f1ec1 100755 --- a/db/seed.sh +++ b/db/seed.sh @@ -1,11 +1,19 @@ #!/usr/bin/env bash # -# Wakdo - seed runner. +# Wakdo - seed runner (hote, via `docker exec`). # # Applique les fichiers db/seeds/*.sql dans l'ordre lexicographique, de maniere -# idempotente : une table seed_history enregistre les fichiers deja charges. +# idempotente : la table seeds_applied enregistre les fichiers deja charges. # Les seeds doivent etre joues APRES les migrations (les tables doivent exister). # +# Contrepartie hote de db/migrate.sh : meme role que la phase seed du service de +# boot wakdo-migrate (db/migrate-container.sh), mais lance manuellement depuis +# l'hote. Le suivi DOIT utiliser la MEME table que le runner conteneur +# (seeds_applied) pour que les deux interoperent : rejouer l'un apres l'autre ne +# replaye RIEN. Auparavant ce script suivait une table distincte (seed_history), +# ce qui lui faisait re-jouer des seeds deja charges par wakdo-migrate (INSERT +# bruts -> echec sur contrainte UNIQUE). +# # Cible : le service docker-compose `wakdo-db`. Identifiants lus dans .env. # # Usage : @@ -34,7 +42,9 @@ if [ ! -d "$SEEDS_DIR" ]; then exit 0 fi -db "$DB_NAME" -e "CREATE TABLE IF NOT EXISTS seed_history ( +# Meme schema de suivi que db/migrate-container.sh (seeds_applied) : nom de table +# et colonnes identiques, pour que les deux runners partagent le meme journal. +db "$DB_NAME" -e "CREATE TABLE IF NOT EXISTS seeds_applied ( filename VARCHAR(255) NOT NULL PRIMARY KEY, applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;" @@ -47,7 +57,7 @@ if [ "${1:-}" = "--status" ]; then echo "[seed] etat des seeds (base $DB_NAME) :" for f in "${files[@]}"; do base="$(basename "$f")" - n="$(db "$DB_NAME" -N -s -e "SELECT COUNT(*) FROM seed_history WHERE filename='$base';")" + n="$(db "$DB_NAME" -N -s -e "SELECT COUNT(*) FROM seeds_applied WHERE filename='$base';")" [ "$n" = "0" ] && echo " PENDING $base" || echo " loaded $base" done exit 0 @@ -56,11 +66,11 @@ fi loaded=0 for f in "${files[@]}"; do base="$(basename "$f")" - n="$(db "$DB_NAME" -N -s -e "SELECT COUNT(*) FROM seed_history WHERE filename='$base';")" + n="$(db "$DB_NAME" -N -s -e "SELECT COUNT(*) FROM seeds_applied WHERE filename='$base';")" if [ "$n" = "0" ]; then echo "[seed] chargement de $base ..." db "$DB_NAME" < "$f" - db "$DB_NAME" -e "INSERT INTO seed_history (filename) VALUES ('$base');" + db "$DB_NAME" -e "INSERT INTO seeds_applied (filename) VALUES ('$base');" loaded=$((loaded + 1)) else echo "[seed] $base deja charge, ignore"