ADR-083 : Suppression logique (options.soft_delete)¶
Statut¶
Acceptée.
Décision d'architecture ; relève du mainteneur.
Date¶
2026-07-14
Contexte¶
L'option d'entité options.soft_delete: true est documentée comme offrant une « suppression logique » : les enregistrements supprimés sont conservés en base avec une date de suppression.
En réalité, elle ne tenait pas ce contrat. Le normaliseur ajoutait une colonne deleted_at DATETIME NULL, mais le CRUD généré :
- exposait
deleted_atcomme un champ de saisie ordinaire (formulaire, liste, fiche détail), comme les horodatages avant ADR-081 ; - effectuait un
DELETEphysique, sans jamais poserdeleted_at; - ne filtrait aucune lecture sur
deleted_at IS NULL.
L'option promettait une suppression logique et en faisait une suppression physique : un contrat public rompu (charte principe 10, principe 3, refuser la magie cachée). Retour terrain, même famille que F56 (ADR-081).
Décision¶
options.soft_delete: true produit une suppression logique réelle. deleted_at est un champ géré par le framework, comme les horodatages, avec un comportement de suppression dédié.
Marqueur¶
Le normaliseur marque deleted_at avec managed = "soft_delete" (le marqueur générique d'ADR-081, valeur validée par ALLOWED_MANAGED_VALUES).
Comportement du CRUD généré¶
- Exclusion des vues et de l'écriture :
deleted_atest absent du formulaire, de la liste, de la fiche détail, de l'export CSV et du tri (comme tout champmanaged). Il est aussi absent de l'INSERTet de l'UPDATE: l'application ne le pose jamais. - Suppression :
delete_<entité>devientUPDATE <table> SET deleted_at = datetime.now(timezone.utc) WHERE <pk> = ?, au lieu d'unDELETE. La suppression groupée fait de même sur unIN (...). - Lectures filtrées : toutes les lectures du modèle filtrent
deleted_at IS NULL:SELECT_ALL,SELECT_BY_ID,count_*,find_*_paginated,find_*_for_export(qui délègue) et le lookup par slug. Un enregistrement supprimé logiquement n'apparaît plus.
Pas de valeur par défaut SQL¶
Le DDL reste deleted_at DATETIME NULL, sans DEFAULT. La valeur est posée par Python à la suppression (cohérent avec ADR-081 : une seule autorité).
Conséquences¶
options.soft_delete: truetient enfin son contrat : suppression réversible en base, invisible des lectures.- Surface : extension du marqueur
managed(valeursoft_delete), additive. Le modèle importedatetimedès qu'un champ géré ou une suppression logique pose une valeur temporelle. - Sans
soft_delete, la suppression reste physique : non-régression. - Limites (version minimale) : pas de commande de restauration ni de vue « corbeille » générées ; l'application les ajoute si besoin. Les listes de choix d'une relation (
many_to_one) vers une entité soft-deletable ne filtrent pas encoredeleted_at IS NULLde la cible ; à traiter si le besoin se confirme (règle B, révéler avant d'élargir).
Alternatives écartées¶
- Ne masquer que
deleted_atdes vues (comme les horodatages) sans changer la suppression.
Écartée : corrige l'UX mais laisse le contrat « suppression logique » mensonger (leDELETEreste physique). - Retirer l'option
soft_delete.
Envisagée (un contrat rompu est pire qu'une absence de feature) ; écartée car la suppression logique est un besoin réel et l'implémentation minimale correcte est bornée. - Défaut SQL / trigger pour poser
deleted_at.
Écartée : Python reste la seule autorité sur la valeur (cohérent ADR-081), et la portabilité entre backends (ADR-054) est plus simple.