fix(db): suivi seeds unifie + idempotence DDL + doc trou 0004 #111

Merged
Corentin merged 1 commit from fix/seed-runner-idempotence into dev 2026-06-25 10:58:43 +02:00
6 changed files with 124 additions and 43 deletions

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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;

View file

@ -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").

View file

@ -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"