Skip to content

Fix corruption de l'index FTS5 à la ré-importation (triggers _ad/_au + recursive_triggers)#8

Open
hermias1 wants to merge 1 commit into
Dahliyaal:mainfrom
hermias1:fix/fts5-triggers
Open

Fix corruption de l'index FTS5 à la ré-importation (triggers _ad/_au + recursive_triggers)#8
hermias1 wants to merge 1 commit into
Dahliyaal:mainfrom
hermias1:fix/fts5-triggers

Conversation

@hermias1

Copy link
Copy Markdown

Le bug (prouvé)

Toutes les bases FTS5 en external-content (content='…') n'avaient qu'un trigger _ai AFTER INSERT. Or tout le pipeline écrit en INSERT 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 :

sqlite3.DatabaseError: fts5: missing row 1 from content table 'main'.'cedh_decisions'

Reproduit sur SQLite en mémoire (2× INSERT OR REPLACE du même id → la recherche sur le texte périmé renvoie encore une ligne, et un SELECT itemid, text … MATCH plante). Deux chemins d'écriture minoritaires étaient aussi cassés autrement : le UPDATE … SET text de rescrape_cedh.py et le DELETE+INSERT de judilibre_sync.py ne réindexaient jamais rien (aucun trigger DELETE/UPDATE).

Le correctif — trois mécanismes, un par pattern

  1. 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 _ai existant. Corrige aussi les chemins DELETE explicite (judilibre) et UPDATE (rescrape) — qui déclenchent désormais _ad/_au directement.

  2. PRAGMA recursive_triggers=ON sur les 13 connexions d'écriture. Point subtil mais décisif : avec le défaut SQLite recursive_triggers=OFF, le DELETE implicite d'un INSERT OR REPLACE ne 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.

  3. download_opendata.py utilise un FTS contentless (content='') maintenu à la main, pas de triggers : purge explicite de l'ancienne entrée (commande 'delete' + valeurs exactes) avant le REPLACE qui 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, dans run_all.sh) :

  • cohérence statique : tout _ai d'un fichier a ses _ad/_au frères ; tout writer active recursive_triggers — verrouille la non-régression future ;
  • comportemental : rejoue la ré-importation sur les deux patterns (external-content et contentless) et prouve l'absence d'orphelin + l'integrity-check FTS5 natif ;
  • contre-preuve : sans le PRAGMA, l'orphelin réapparaît (justifie le volet 2).

py_compile OK sur les 12 fichiers ; test_source_url / test_citations toujours 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.

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
hermias1 force-pushed the fix/fts5-triggers branch from d1c5c6f to 119f9d6 Compare July 12, 2026 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant