Fix corruption de l'index FTS5 à la ré-importation (triggers _ad/_au + recursive_triggers)#8
Open
hermias1 wants to merge 1 commit into
Open
Fix corruption de l'index FTS5 à la ré-importation (triggers _ad/_au + recursive_triggers)#8hermias1 wants to merge 1 commit into
hermias1 wants to merge 1 commit into
Conversation
Bug systémique sur toutes les bases : les tables FTS5 en external-content
n'avaient qu'un trigger _ai AFTER INSERT. Un INSERT OR REPLACE (chemin
d'écriture dominant de tout le pipeline) réassigne un nouveau rowid et
laissait l'ancienne entrée FTS orpheline → recherche dédupliquée/fausse,
et crash "fts5: missing row from content table" sur une recherche qui
lit les colonnes de contenu. Prouvé par reproduction SQLite.
Correctif en trois volets, un par mécanisme :
- Triggers _ad (AFTER DELETE) + _au (AFTER UPDATE) sur les 10 tables
external-content (parse_dila_bulk : legi/jorf/juris/kali/cnil ;
index_dila ; scrape_legifrance ; scrape_cedh/cjue/ariane), colonnes
rigoureusement alignées sur le _ai. Corrige aussi les chemins DELETE
explicite (judilibre_sync) et UPDATE (rescrape_cedh) jamais réindexés.
- PRAGMA recursive_triggers=ON sur les 13 connexions d'écriture : sans
lui, le DELETE implicite d'un INSERT OR REPLACE ne déclenche pas _ad
(défaut SQLite recursive_triggers=OFF). C'est le volet sans lequel les
triggers seuls ne corrigent pas le chemin REPLACE.
- download_opendata : FTS contentless maintenu à la main (pas de
triggers) — purge explicite de l'ancienne entrée ('delete' + valeurs)
avant le REPLACE qui change le rowid.
scripts/rebuild_fts.py : reconstruit les index une fois après déploiement
pour purger les orphelins déjà accumulés (idempotent, --dry-run).
tests/test_fts5_triggers.py (offline) : cohérence statique (tout _ai a
ses _ad/_au ; tout writer active recursive_triggers), non-régression
comportementale sur les deux patterns (external-content et contentless),
et contre-preuve que sans le PRAGMA l'orphelin réapparaît.
hermias1
force-pushed
the
fix/fts5-triggers
branch
from
July 12, 2026 14:46
d1c5c6f to
119f9d6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Le bug (prouvé)
Toutes les bases FTS5 en external-content (
content='…') n'avaient qu'un trigger_ai AFTER INSERT. Or tout le pipeline écrit enINSERT OR REPLACE— qui réassigne un nouveau rowid à chaque ré-importation. L'ancienne entrée FTS, pointant l'ancien rowid, n'était jamais supprimée → index désynchronisé : résultats de recherche dupliqués/faux, et surtout crash sur une recherche qui lit les colonnes de contenu :Reproduit sur SQLite en mémoire (2×
INSERT OR REPLACEdu même id → la recherche sur le texte périmé renvoie encore une ligne, et unSELECT itemid, text … MATCHplante). Deux chemins d'écriture minoritaires étaient aussi cassés autrement : leUPDATE … SET textderescrape_cedh.pyet leDELETE+INSERTdejudilibre_sync.pyne réindexaient jamais rien (aucun trigger DELETE/UPDATE).Le correctif — trois mécanismes, un par pattern
Triggers
_ad(AFTER DELETE) +_au(AFTER UPDATE) sur les 10 tables external-content (parse_dila_bulk.py: legi/jorf/juris/kali/cnil ;index_dila.py;scrape_legifrance.py;scrape_cedh/cjue/ariane.py), colonnes rigoureusement alignées sur le_aiexistant. Corrige aussi les chemins DELETE explicite (judilibre) et UPDATE (rescrape) — qui déclenchent désormais_ad/_audirectement.PRAGMA recursive_triggers=ONsur les 13 connexions d'écriture. Point subtil mais décisif : avec le défaut SQLiterecursive_triggers=OFF, le DELETE implicite d'unINSERT OR REPLACEne déclenche pas_ad. Sans ce PRAGMA, les triggers seuls ne corrigent pas le chemin REPLACE (le chemin dominant). Le test inclut une contre-preuve qui le démontre.download_opendata.pyutilise un FTS contentless (content='') maintenu à la main, pas de triggers : purge explicite de l'ancienne entrée (commande'delete'+ valeurs exactes) avant leREPLACEqui change le rowid. Même symptôme, mécanisme différent — reproduit et corrigé aussi.Migration des bases existantes
Les triggers étant en
CREATE … IF NOT EXISTS, ils s'installent au prochain run et rendent les écritures futures correctes. Pour purger les orphelins déjà accumulés,scripts/rebuild_fts.py(idempotent,--dry-run) reconstruit chaque index une fois après déploiement — à lancer hors des heures de parse (coûteux sur legi/jade).Vérification
tests/test_fts5_triggers.py(offline, dansrun_all.sh) :_aid'un fichier a ses_ad/_aufrères ; tout writer activerecursive_triggers— verrouille la non-régression future ;integrity-checkFTS5 natif ;py_compileOK sur les 12 fichiers ;test_source_url/test_citationstoujours verts.Portée
Autoportante (basée sur
main). Ne touche que le schéma FTS + le PRAGMA de connexion + la purge opendata — aucune logique de parsing/scraping applicative modifiée.