Aller au contenu

Intégrité référentielle

Un flux GTFS est une petite base relationnelle avec une vingtaine de relations FK. L’éditer naïvement — retirer un arrêt, renommer une ligne — est une voie rapide pour produire un flux qui valide structurellement mais échoue sur Google Maps parce que stop_times référence plus rien. L’intégrité référentielle est ce qui vous sauve de ce piège.

gapline applique l’intégrité à chaque écriture. Aucune édition n’atteint le disque avant que le modèle d’intégrité ait confirmé que le résultat est cohérent.

En interne, gapline maintient un index inverse pour chaque clé primaire utilisée comme clé étrangère ailleurs. Au chargement du flux, l’index associe :

stops.stop_id → lignes de stop_times, transfers, pathways qui la référencent
routes.route_id → lignes de trips, fare_rules qui la référencent
trips.trip_id → lignes de stop_times, frequencies
calendar.service_id → lignes de trips, calendar_dates

Les requêtes sur l’index sont en O(1) par PK — construire le plan de cascade d’un delete sur un flux de taille moyenne se chiffre en millisecondes.

Délibérément, l’index n’est qu’une série de hash maps : pas de bibliothèque de graphes, pas de path-finding, pas de détection de cycles. Les chaînes de FK GTFS sont peu profondes (au plus 2–3 sauts), ce qui suffit amplement.

delete ne peut pas orpheliner de dépendants. Quand vous lancez :

Fenêtre de terminal
gapline delete stops --where "stop_id=S01"

gapline :

  1. Calcule l’ensemble des stop_id = S01 dans stops.txt.
  2. Pour chaque match, parcourt l’index inverse pour trouver toutes les lignes de tous les fichiers dépendants qui référencent le match transitivement.
  3. Affiche un aperçu :
    Records to delete from stops.txt:
    S01
    Deleting would also delete:
    - 83 records in stop_times.txt
    - 2 records in transfers.txt
    Proceed with cascade delete? [y/N]
  4. N’applique le plan qu’après votre confirmation (ou si vous avez passé --confirm).

Il n’y a pas de flag --cascade sur delete parce que la cascade est le seul comportement par défaut sûr. Si la cible n’a pas de dépendants (par exemple calendar_dates.txt est une feuille), le prompt liste simplement les lignes matchées.

Update : les réécritures de PK demandent --cascade

Section intitulée « Update : les réécritures de PK demandent --cascade »

Un update non-PK (par ex. changer un stop_name) ne touche que le fichier cible. Pas de cascade nécessaire, pas de cascade calculée.

Un update sur PK (par ex. renommer stop_id=S01 en stop_id=STOP_MAIN) est différent : toutes les lignes de tous les fichiers dépendants qui référencent l’ancienne PK doivent être réécrites pour pointer sur la nouvelle. --cascade active cette réécriture :

Fenêtre de terminal
gapline update stops \
--where "stop_id=S01" \
--set stop_id=STOP_MAIN \
--cascade --confirm

Sans --cascade, la réécriture de PK est refusée avant même de commencer — sinon la commande orphelinerait toutes les lignes stop_times référençant S01.

create refuse d’insérer un enregistrement dont les champs FK ne pointent pas sur des lignes existantes. Par exemple :

Fenêtre de terminal
gapline create stop-times --set trip_id=UNKNOWN stop_id=S01 ...

échoue immédiatement avec une erreur fk_violationtrip_id=UNKNOWN n’est pas dans trips.txt.

Un flux qui passe la validation structurelle mais a des références orphelines est la pire classe de bug : ça passe un check rapide mais ça casse silencieusement en prod. Les consommateurs gèrent les orphelins de façons incohérentes — certains sautent les lignes concernées, d’autres rejettent le flux entier, d’autres rendent des données partielles sans jamais signaler l’erreur.

En appliquant l’intégrité au moment de l’écriture, gapline rend cette classe de bug impossible à produire depuis le CLI. Le compromis est que delete et update --cascade doivent planifier la cascade complète avant application — généralement quelques millisecondes, occasionnellement quelques secondes sur de très gros flux.