DEV Community

Cover image for Pourquoi ton DELETE bulk sur Supabase est faux avant même de tourner (drift en 30 min)
Michel Faure
Michel Faure

Posted on • Originally published at dev.to

Pourquoi ton DELETE bulk sur Supabase est faux avant même de tourner (drift en 30 min)

Le comptage qui ne survit pas à un café

16 mai, console Supabase ouverte sur un lot de lignes orphelines. Catherine m'a remonté trois doublons d'émargement la veille avec sa formule habituelle, « hum, ça bug, mais c'est vite corrigé ». Une probe SQL plus tard, ce ne sont plus trois cas, c'est la classe entière. Je lance SELECT COUNT(*) FROM seances WHERE cours_id NOT IN (SELECT id FROM cours) à 10 h 47. La sortie brute dit 351. Je calibre le DELETE, je relis la clause WHERE, je vais chercher un café.

Dix minutes plus tard, la main sur le clavier, un réflexe résiduel me fait relancer le COUNT. Le chiffre est maintenant 381. Trente lignes apparues entre mes deux probes. Un cron a tourné une fois pendant que je commandais mon café et a ré-injecté ses lignes. Le DELETE que j'allais lancer aurait emporté un périmètre qui n'était plus celui que j'avais calibré dix minutes plus tôt. Aucune alarme, aucune erreur. Juste un écart de trente lignes que rien ne m'aurait montré si l'habitude ne m'avait pas fait relancer la probe.

Deux probes, deux fonctions distinctes

Le réflexe naturel est d'appeler comptage le geste qui retourne un entier. Cette confusion lexicale produit le piège. La première probe scope le travail — « voici l'ordre de grandeur, voici la classe d'incidents ». Elle informe une décision humaine de cadrage. Une demi-heure peut s'écouler entre cette probe et la décision finale, et c'est sain : un audit qui se précipite se trompe de classe.

La seconde probe a une fonction structurellement différente. Elle autorise. Elle dit « le périmètre que tu t'apprêtes à toucher est bien celui que tu as calibré ». Sa durée d'invalidation n'est pas comptée en heures de réflexion humaine mais en cycles de cron. Sur un Postgres en production traversé par des crons toutes les cinq minutes, des webhooks asynchrones, un sync nocturne, deux ou trois assistantes qui saisissent en parallèle, le périmètre a la durée de vie d'un intervalle entre deux exécutions de cron — minutes, pas heures.

Tout dev le sait intellectuellement. Aucun ne l'opère. Une bonne pratique sans dispositif matériel s'évapore exactement au moment où elle serait utile — j'en ai fait l'expérience trois fois en dix jours, et je ne suis pas un débutant.

R7 amendée, et le hook qui ferme la porte

Counterpart Toolkit v0.7, publié le 20 mai, ajoute un paragraphe à R7 :

Pour un bulk DELETE/UPDATE sur un système actif : relancer la requête de comptage immédiatement avant la mutation. Abort si delta > 5 % de la probe initiale. Un comptage de plus de 30 minutes est obsolète sur un système actif. La première probe scope le travail, la seconde est la porte.

Trente minutes est le plancher empirique en-dessous duquel une probe ne croise presque certainement aucun cycle de cron sur notre stack. Cinq pour cent est le seuil au-delà duquel la surface inattendue dépasse le coût d'un re-cadrage.

R7 vit dans CLAUDE.md. Lue en début de session, oubliée au moment utile. Le fix est un hook PreToolUse qui scanne tout payload mcp__supabase__execute_sql pour les patterns DELETE et UPDATE non ciblés par clé unique. Pour passer, le SQL doit porter un marqueur explicite :

-- count-fresh:20260516-1057
DELETE FROM seances
WHERE cours_id NOT IN (SELECT id FROM cours);
Enter fullscreen mode Exit fullscreen mode

Le hook parse l'horodatage et bloque s'il dépasse 30 minutes. Pas de marqueur, pas d'exécution. La bypass n'est pas une porte dérobée, c'est une déclaration nominative que l'opérateur a relancé la probe. La discipline n'est pas dans la mémoire — elle est dans la friction entre opérateur et mutation.

Trois fois en dix jours, j'allais détruire un périmètre que je croyais connaître. La quatrième fois, le hook m'a refusé l'exécution — mon marqueur datait de 47 minutes. J'ai relancé la probe : douze lignes avaient bougé. Vingt secondes de coût, un re-do que je n'aurais pas vu venir avant le lendemain matin.


Counterpart Toolkit v0.7, R7 amendée sur N=2 incidents. Hook ~/.claude/hooks/pre-bulk-mutation-count-staleness.sh. CC-BY-4.0 : github.com/michelfaure/doctrine-counterpart

Top comments (0)