fix(balance): v16 abort guard too broad — scope to convertible accounts (asset_type NOT NULL) #228

Open
opened 2026-06-06 20:36:10 +00:00 by maximus · 0 comments
Owner

Trouvé par la review adversariale de PR #220 (#211). Fix-forward décidé : à appliquer sur main après le merge de la chaîne Étape 2, avant le tag de release (v16 n'a jamais été appliquée à un profil persistant — modifier sa SQL avant la première application respecte la règle d'immutabilité des migrations).

Bug

La garde d'abort de la migration v16 (src-tauri/src/lib.rs, sous-requête _v16_guard) clé sur a.symbol IS NOT NULL au lieu de la convertibilité (c.asset_type IS NOT NULL).

Un compte sous catégorie simple portant un symbole résiduel (l'AccountForm rend le champ symbole inconditionnellement ; updateBalanceAccount préserve le symbole lors d'une recatégorisation priced→simple) a des lignes à quantity NULL par construction. Elles satisfont le prédicat de la garde (symbol NOT NULL AND qty NULL AND aucun holding) → la garde insère 0CHECK(ok=1) échoue → toute la migration v16 abort → l'app ne démarre plus pour ce profil (pas de perte de données, blocage de lancement non récupérable).

La logique de conversion (étapes 1-3) est correcte ; seule la garde belt-and-suspenders est trop large.

Fix

Joindre balance_categories dans la sous-requête de garde et exiger AND c.asset_type IS NOT NULL, dans les deux copies (le SQL de Migration { version: 16 } ET la constante de test V16_SQL, statement-equivalentes) :

-- garde : ne flaguer que les lignes des comptes CONVERTIBLES (symbol + asset_type)
INSERT INTO _v16_guard(ok) SELECT CASE WHEN EXISTS (
  SELECT 1 FROM balance_snapshot_lines sl
  JOIN balance_accounts a ON a.id = sl.account_id
  JOIN balance_categories c ON c.id = a.balance_category_id
  WHERE a.symbol IS NOT NULL AND c.asset_type IS NOT NULL AND sl.quantity IS NULL
    AND NOT EXISTS (SELECT 1 FROM balance_snapshot_holdings h WHERE h.snapshot_line_id = sl.id)
) THEN 0 ELSE 1 END;

Test de régression

Ajouter un cas (style db_pre_v16) seedant un compte sous catégorie simple avec un symbol résiduel + une ligne snapshot à quantity NULL, puis asserter que v16 s'applique proprement (pas d'abort) et laisse ce compte intact (non converti, qty/price préservés). Le test échoue avant le fix, passe après.

Critères d'acceptation

  • Garde v16 scopée sur c.asset_type IS NOT NULL (Migration v16 + const V16_SQL)
  • Test : compte simple + symbole résiduel + ligne qty-NULL → v16 applique sans abort, compte intact
  • cargo test vert
Trouvé par la review adversariale de PR #220 (#211). **Fix-forward décidé** : à appliquer sur `main` après le merge de la chaîne Étape 2, avant le tag de release (v16 n'a jamais été appliquée à un profil persistant — modifier sa SQL avant la première application respecte la règle d'immutabilité des migrations). ## Bug La garde d'abort de la migration v16 (`src-tauri/src/lib.rs`, sous-requête `_v16_guard`) clé sur `a.symbol IS NOT NULL` au lieu de la **convertibilité** (`c.asset_type IS NOT NULL`). Un compte sous catégorie **simple** portant un symbole résiduel (l'AccountForm rend le champ symbole inconditionnellement ; `updateBalanceAccount` préserve le symbole lors d'une recatégorisation priced→simple) a des lignes à `quantity NULL` par construction. Elles satisfont le prédicat de la garde (`symbol NOT NULL AND qty NULL AND aucun holding`) → la garde insère `0` → `CHECK(ok=1)` échoue → **toute la migration v16 abort → l'app ne démarre plus** pour ce profil (pas de perte de données, blocage de lancement non récupérable). La logique de conversion (étapes 1-3) est correcte ; seule la garde belt-and-suspenders est trop large. ## Fix Joindre `balance_categories` dans la sous-requête de garde et exiger `AND c.asset_type IS NOT NULL`, dans **les deux** copies (le SQL de `Migration { version: 16 }` ET la constante de test `V16_SQL`, statement-equivalentes) : ```sql -- garde : ne flaguer que les lignes des comptes CONVERTIBLES (symbol + asset_type) INSERT INTO _v16_guard(ok) SELECT CASE WHEN EXISTS ( SELECT 1 FROM balance_snapshot_lines sl JOIN balance_accounts a ON a.id = sl.account_id JOIN balance_categories c ON c.id = a.balance_category_id WHERE a.symbol IS NOT NULL AND c.asset_type IS NOT NULL AND sl.quantity IS NULL AND NOT EXISTS (SELECT 1 FROM balance_snapshot_holdings h WHERE h.snapshot_line_id = sl.id) ) THEN 0 ELSE 1 END; ``` ## Test de régression Ajouter un cas (style `db_pre_v16`) seedant un compte sous catégorie **simple** avec un `symbol` résiduel + une ligne snapshot à `quantity NULL`, puis asserter que v16 s'applique **proprement** (pas d'abort) et laisse ce compte intact (non converti, qty/price préservés). Le test échoue avant le fix, passe après. ## Critères d'acceptation - [ ] Garde v16 scopée sur `c.asset_type IS NOT NULL` (Migration v16 + const `V16_SQL`) - [ ] Test : compte simple + symbole résiduel + ligne qty-NULL → v16 applique sans abort, compte intact - [ ] `cargo test` vert
maximus added this to the overnight-2026-06-05-bilan-detail-titres milestone 2026-06-06 20:36:10 +00:00
maximus added the
status:ready
type:bug
source:human
labels 2026-06-06 20:36:10 +00:00
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#228
No description provided.