voyage-assistant / backend /migrate_to_postgres.py
optimalako
Déploiement Voyage Assistant : FastAPI + RAG + APIs temps réel + front (Docker, Neon)
c8f0d9b
Raw
History Blame Contribute Delete
3.16 kB
"""Migration des données SQLite (voyage.db) -> Postgres (Neon, etc.).
La table `routes` (~182 000 lignes) n'est PAS régénérable par seed.py : elle
n'existe que dans voyage.db. Ce script copie donc *les données existantes*
table par table, dans l'ordre des dépendances de clés étrangères.
Usage :
# 1) la cible (Neon) est lue depuis DATABASE_URL
export DATABASE_URL="postgresql+psycopg://user:pass@host/db?sslmode=require"
# 2) la source SQLite (défaut : ./voyage.db)
.venv/bin/python migrate_to_postgres.py
# source personnalisée :
.venv/bin/python migrate_to_postgres.py sqlite:///./voyage.db
Idempotent-ish : on vide chaque table cible avant d'insérer (TRUNCATE/DELETE),
donc relançable sans doublons.
"""
from __future__ import annotations
import os
import sys
from sqlalchemy import create_engine, delete, select
from sqlalchemy.orm import Session
# Importe les modèles pour que Base.metadata connaisse toutes les tables.
from app.database import Base
from app.models import (
Admin,
Billet,
ChatMessage,
ChatSession,
Reclamation,
Route,
Trajet,
User,
)
# Ordre d'insertion : parents avant enfants (FK).
# Ordre de purge : l'inverse.
ORDER = [Route, User, Trajet, Billet, Reclamation, ChatSession, ChatMessage, Admin]
BATCH = 5000
def _columns(model) -> list[str]:
return [c.key for c in model.__mapper__.column_attrs]
def main() -> None:
source_url = sys.argv[1] if len(sys.argv) > 1 else "sqlite:///./voyage.db"
target_url = os.getenv("DATABASE_URL", "")
if not target_url:
sys.exit("DATABASE_URL (cible Postgres) est vide. Exporte-le d'abord.")
if target_url.startswith("sqlite"):
sys.exit("DATABASE_URL pointe sur SQLite : ce script migre VERS Postgres.")
print(f"Source : {source_url}")
print(f"Cible : {target_url.split('@')[-1]}") # masque les creds
src_engine = create_engine(source_url, future=True)
tgt_engine = create_engine(target_url, future=True)
# Crée le schéma sur la cible.
Base.metadata.create_all(tgt_engine)
print("Schéma créé sur la cible.")
# Purge (enfants -> parents) pour pouvoir relancer.
with Session(tgt_engine) as tgt:
for model in reversed(ORDER):
tgt.execute(delete(model))
tgt.commit()
print("Tables cibles vidées.")
with Session(src_engine) as src, Session(tgt_engine) as tgt:
for model in ORDER:
cols = _columns(model)
rows = src.execute(select(model)).scalars().all()
total = len(rows)
if total == 0:
print(f" {model.__tablename__:16} : 0 ligne")
continue
mappings = [{c: getattr(r, c) for c in cols} for r in rows]
for i in range(0, total, BATCH):
tgt.bulk_insert_mappings(model, mappings[i : i + BATCH])
tgt.commit()
print(f" {model.__tablename__:16} : {min(i + BATCH, total)}/{total}", end="\r")
print(f" {model.__tablename__:16} : {total}/{total} ✓ ")
print("\nMigration terminée.")
if __name__ == "__main__":
main()