-- ============================================================================ -- MIGRATION : OPTIMISATION DE LA TABLE UNIQUE "horaires" -- Date : 2026-10-01 -- Objectif : Consolider 100% des données relatives aux horaires (sillons, dessertes, -- quais, arrêts intermédiaires, substitutions, calendrier, matériel, -- temps réel) dans une seule et unique table "horaires" ultra-performante. -- ============================================================================ -- 1. Ajout de toutes les colonnes nécessaires à la table horaires DO $$ BEGIN -- Identifiants et informations train IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'nom_train') THEN ALTER TABLE "public"."horaires" ADD COLUMN "nom_train" varchar(255); END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'code_mission') THEN ALTER TABLE "public"."horaires" ADD COLUMN "code_mission" varchar(50); END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'sens') THEN ALTER TABLE "public"."horaires" ADD COLUMN "sens" varchar(20) DEFAULT 'aller'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'transporteur') THEN ALTER TABLE "public"."horaires" ADD COLUMN "transporteur" varchar(100) DEFAULT 'SNCF Voyageurs'; END IF; -- Dessertes et attribution des quais IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'gares_desservies') THEN ALTER TABLE "public"."horaires" ADD COLUMN "gares_desservies" jsonb NOT NULL DEFAULT '[]'::jsonb; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'attributions_quais') THEN ALTER TABLE "public"."horaires" ADD COLUMN "attributions_quais" jsonb NOT NULL DEFAULT '{}'::jsonb; END IF; -- Calendrier étendu et dates de validité IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'date_debut') THEN ALTER TABLE "public"."horaires" ADD COLUMN "date_debut" timestamptz; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'date_fin') THEN ALTER TABLE "public"."horaires" ADD COLUMN "date_fin" timestamptz; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'calendrier_circulation') THEN ALTER TABLE "public"."horaires" ADD COLUMN "calendrier_circulation" jsonb NOT NULL DEFAULT '[]'::jsonb; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'periode_validite') THEN ALTER TABLE "public"."horaires" ADD COLUMN "periode_validite" jsonb NOT NULL DEFAULT '{}'::jsonb; END IF; -- Matériel et composition IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'composition_train') THEN ALTER TABLE "public"."horaires" ADD COLUMN "composition_train" jsonb NOT NULL DEFAULT '[]'::jsonb; END IF; -- Région IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'region') THEN ALTER TABLE "public"."horaires" ADD COLUMN "region" varchar(255); END IF; -- Service annuel et type de service IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'service_type') THEN ALTER TABLE "public"."horaires" ADD COLUMN "service_type" varchar(50) DEFAULT 'TER'; END IF; -- Substitutions et autocars IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'est_substitution') THEN ALTER TABLE "public"."horaires" ADD COLUMN "est_substitution" boolean NOT NULL DEFAULT false; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'motif_substitution') THEN ALTER TABLE "public"."horaires" ADD COLUMN "motif_substitution" varchar(255); END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'substitution_disponible') THEN ALTER TABLE "public"."horaires" ADD COLUMN "substitution_disponible" boolean NOT NULL DEFAULT false; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'horaire_id') THEN ALTER TABLE "public"."horaires" ADD COLUMN "horaire_id" uuid; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'substitutions_associees') THEN ALTER TABLE "public"."horaires" ADD COLUMN "substitutions_associees" jsonb NOT NULL DEFAULT '[]'::jsonb; END IF; -- Sillons & infrastructure IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'sillon_id') THEN ALTER TABLE "public"."horaires" ADD COLUMN "sillon_id" text; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'tranche') THEN ALTER TABLE "public"."horaires" ADD COLUMN "tranche" varchar(50); END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'gestionnaire_infrastructure') THEN ALTER TABLE "public"."horaires" ADD COLUMN "gestionnaire_infrastructure" varchar(100) DEFAULT 'SNCF Réseau'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'vitesse_max_kmh') THEN ALTER TABLE "public"."horaires" ADD COLUMN "vitesse_max_kmh" integer; END IF; -- Exploitation temps réel et retards IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'statut') THEN ALTER TABLE "public"."horaires" ADD COLUMN "statut" varchar(50) NOT NULL DEFAULT 'a_l_heure'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'retard_minutes') THEN ALTER TABLE "public"."horaires" ADD COLUMN "retard_minutes" integer NOT NULL DEFAULT 0; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'retard_motif') THEN ALTER TABLE "public"."horaires" ADD COLUMN "retard_motif" text; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'informations_voyageurs') THEN ALTER TABLE "public"."horaires" ADD COLUMN "informations_voyageurs" text; END IF; -- Métadonnées et notes IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'metadata') THEN ALTER TABLE "public"."horaires" ADD COLUMN "metadata" jsonb NOT NULL DEFAULT '{}'::jsonb; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'horaires' AND column_name = 'notes') THEN ALTER TABLE "public"."horaires" ADD COLUMN "notes" text; END IF; END $$; -- 2. Migration des données relationnelles de horaires_gares_desservies vers horaires.gares_desservies (si la table existe) DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'horaires_gares_desservies') THEN -- Mettre à jour horaires.gares_desservies avec les lignes de la table relationnelle si gares_desservies est vide UPDATE "public"."horaires" h SET gares_desservies = sub.json_dessertes, updated_at = NOW() FROM ( SELECT horaire_id, jsonb_agg( jsonb_build_object( 'gare_id', gare_id, 'gare_nom', gare_nom, 'heure_arrivee', heure_arrivee, 'heure_depart', heure_depart, 'ordre', ordre, 'quai', quai ) ORDER BY ordre ASC ) AS json_dessertes FROM "public"."horaires_gares_desservies" GROUP BY horaire_id ) sub WHERE h.id = sub.horaire_id AND (h.gares_desservies IS NULL OR h.gares_desservies = '[]'::jsonb); END IF; END $$; -- 3. Migration des substitutions de horaires_substitutions vers horaires (si la table existe) DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'horaires_substitutions') THEN INSERT INTO "public"."horaires" ( "id", "numero_train", "type_train", "type_train_id", "gare_depart_id", "gare_depart_nom", "heure_depart", "gare_arrivee_id", "gare_arrivee_nom", "heure_arrivee", "date_debut", "date_fin", "circule_lundi", "circule_mardi", "circule_mercredi", "circule_jeudi", "circule_vendredi", "circule_samedi", "circule_dimanche", "circule_jours_feries", "jours_personnalises", "jours_non_circulation", "materiel_roulant_id", "ligne_id", "ligne_nom", "service_annuel_id", "service_annuel_nom", "est_substitution", "motif_substitution", "gares_desservies", "composition_train", "substitution_disponible", "actif", "region_id", "region", "horaire_id", "created_at", "updated_at", "deleted_at" ) SELECT s."id", COALESCE(s."numero_train", 'SUBST-' || substring(s."id"::text, 1, 6)), s."type_train", s."type_train_id", s."gare_depart_id", s."gare_depart_nom", s."heure_depart", s."gare_arrivee_id", s."gare_arrivee_nom", s."heure_arrivee", s."date_debut", s."date_fin", COALESCE(s."circule_lundi", true), COALESCE(s."circule_mardi", true), COALESCE(s."circule_mercredi", true), COALESCE(s."circule_jeudi", true), COALESCE(s."circule_vendredi", true), COALESCE(s."circule_samedi", true), COALESCE(s."circule_dimanche", true), COALESCE(s."circule_jours_feries", false), COALESCE(s."jours_personnalises", '[]'::jsonb), COALESCE(s."jours_non_circulation", '[]'::jsonb), s."materiel_roulant_id", s."ligne_id", s."ligne_nom", s."service_annuel_id", s."service_annuel_nom", true AS est_substitution, s."motif_substitution", COALESCE(s."gares_desservies", '[]'::jsonb), COALESCE(s."composition_train", '[]'::jsonb), false, COALESCE(s."actif", true), s."region_id", s."region", s."horaire_id", COALESCE(s."created_at", NOW()), COALESCE(s."updated_at", NOW()), s."deleted_at" FROM "public"."horaires_substitutions" s ON CONFLICT ("id") DO UPDATE SET est_substitution = true, motif_substitution = EXCLUDED.motif_substitution, date_debut = EXCLUDED.date_debut, date_fin = EXCLUDED.date_fin, updated_at = NOW(); END IF; END $$; -- 4. Initialisation du dictionnaire attributions_quais depuis gares_desservies UPDATE "public"."horaires" SET attributions_quais = ( SELECT COALESCE( jsonb_object_agg( COALESCE(elem->>'gare_id', elem->>'gare_nom'), jsonb_build_object( 'quai', elem->>'quai', 'quai_reel', elem->>'quai_reel', 'ordre', elem->'ordre' ) ), '{}'::jsonb ) FROM jsonb_array_elements(gares_desservies) elem WHERE elem->>'quai' IS NOT NULL OR elem->>'quai_reel' IS NOT NULL ) WHERE attributions_quais = '{}'::jsonb AND gares_desservies IS NOT NULL AND jsonb_array_length(gares_desservies) > 0; -- 5. Création des index haute performance (B-Tree et GIN) CREATE INDEX IF NOT EXISTS "idx_horaires_numero_train" ON "public"."horaires" ("numero_train"); CREATE INDEX IF NOT EXISTS "idx_horaires_ligne_id" ON "public"."horaires" ("ligne_id"); CREATE INDEX IF NOT EXISTS "idx_horaires_region_id" ON "public"."horaires" ("region_id"); CREATE INDEX IF NOT EXISTS "idx_horaires_departs" ON "public"."horaires" ("gare_depart_id", "heure_depart"); CREATE INDEX IF NOT EXISTS "idx_horaires_arrivees" ON "public"."horaires" ("gare_arrivee_id", "heure_arrivee"); CREATE INDEX IF NOT EXISTS "idx_horaires_actif" ON "public"."horaires" ("actif") WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS "idx_horaires_substitution" ON "public"."horaires" ("est_substitution"); CREATE INDEX IF NOT EXISTS "idx_horaires_dates" ON "public"."horaires" ("date_debut", "date_fin"); CREATE INDEX IF NOT EXISTS "idx_horaires_statut" ON "public"."horaires" ("statut"); -- Index GIN JSONB pour recherche et filtrage ultra-rapide sans jointure CREATE INDEX IF NOT EXISTS "idx_horaires_gares_desservies_gin" ON "public"."horaires" USING gin ("gares_desservies" jsonb_path_ops); CREATE INDEX IF NOT EXISTS "idx_horaires_attributions_quais_gin" ON "public"."horaires" USING gin ("attributions_quais"); CREATE INDEX IF NOT EXISTS "idx_horaires_calendrier_gin" ON "public"."horaires" USING gin ("calendrier_circulation" jsonb_path_ops); CREATE INDEX IF NOT EXISTS "idx_horaires_metadata_gin" ON "public"."horaires" USING gin ("metadata"); -- 6. Trigger de mise à jour automatique de updated_at CREATE OR REPLACE TRIGGER "tr_horaires_set_updated_at" BEFORE UPDATE ON "public"."horaires" FOR EACH ROW EXECUTE FUNCTION "public"."set_updated_at"(); -- 7. Politiques de sécurité RLS ALTER TABLE "public"."horaires" ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Public read access" ON "public"."horaires"; CREATE POLICY "Public read access" ON "public"."horaires" FOR SELECT USING (true); DROP POLICY IF EXISTS "Public insert access" ON "public"."horaires"; CREATE POLICY "Public insert access" ON "public"."horaires" FOR INSERT WITH CHECK (true); DROP POLICY IF EXISTS "Public update access" ON "public"."horaires"; CREATE POLICY "Public update access" ON "public"."horaires" FOR UPDATE USING (true); DROP POLICY IF EXISTS "Public delete access" ON "public"."horaires"; CREATE POLICY "Public delete access" ON "public"."horaires" FOR DELETE USING (true); -- Commentaires d'optimisation COMMENT ON TABLE "public"."horaires" IS 'Table UNIQUE et centrale pour stocker l''intégralité des données horaires, dessertes, quais, substitutions, calendrier et temps réel.'; COMMENT ON COLUMN "public"."horaires"."gares_desservies" IS 'Liste JSONB complète et ordonnée de tous les arrêts, gares, horaires et quais.'; COMMENT ON COLUMN "public"."horaires"."attributions_quais" IS 'Dictionnaire JSONB rapide O(1) { [gare_id]: { quai, quai_reel } } sans jointure.'; COMMENT ON COLUMN "public"."horaires"."est_substitution" IS 'Vrai si ce sillon représente un autocar ou train de substitution.';