# ============================================================================
# [DOCS] SQLALCHEMY - INDEX ET TABLE DES MATIÈRES
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR NAVIGUER DANS LA DOCUMENTATION
#
# Ce fichier vous aide à trouver rapidement l'information que vous cherchez
# dans les 8 fichiers du guide SQLAlchemy.
#
# [TEMPS] Temps total d'apprentissage : ~20-30 heures
# [DOCS] Niveau : Débutant -> Expert
# ============================================================================


# ============================================================================
# [GUIDE] STRUCTURE COMPLÈTE DU GUIDE
# ============================================================================

"""
┌─────────────────────────────────────────────────────────────────────┐
│                     GUIDE SQLALCHEMY COMPLET                        │
│                         8 FICHIERS                                  │
│                        ~200KB de code                               │
└─────────────────────────────────────────────────────────────────────┘

[DOSSIER] PARTIE 1 : FONDAMENTAUX
   ├── sqlalchemy_partie1.txt (~40KB)
   │   ├── Chapitre 0 : Introduction à SQLAlchemy
   │   ├── Chapitre 1 : Installation et Configuration
   │   └── Chapitre 2 : Créer des Modèles (Tables)
   │
   └── sqlalchemy_partie1_suite.txt (~31KB)
       ├── Chapitre 3 : Types de Colonnes
       └── Chapitre 4 : Contraintes et Options

[DOSSIER] PARTIE 2 : RELATIONS ET REQUÊTES
   ├── sqlalchemy_partie2.txt (~16KB)
   │   └── Chapitre 5 : Relations entre Tables
   │
   └── sqlalchemy_partie2_suite.txt (~20KB)
       ├── Chapitre 6 : Requêtes CRUD
       ├── Chapitre 7 : Requêtes Avancées
       └── Chapitre 8 : Jointures et Agrégations

[DOSSIER] PARTIE 3 : SESSIONS ET PERFORMANCE
   ├── sqlalchemy_partie3.txt (~23KB)
   │   ├── Chapitre 9 : Comprendre les Sessions
   │   └── Chapitre 10 : Transactions
   │
   └── sqlalchemy_partie3_suite.txt (~17KB)
       ├── Chapitre 11 : Performance et Optimisation
       └── Chapitre 12 : Eager Loading et N+1

[DOSSIER] PARTIE 4 : PRODUCTION
   └── sqlalchemy_partie4.txt (~22KB)
       ├── Chapitre 13 : Migrations avec Alembic
       ├── Chapitre 14 : Testing
       ├── Chapitre 15 : Déploiement Production
       └── Chapitre 16 : Best Practices

[FICHIER] BONUS
   └── sqlalchemy_quickstart.txt (~20KB)
       └── Guide de démarrage rapide (30 min)
"""


# ============================================================================
# [WORLD_MAP] TABLE DES MATIÈRES DÉTAILLÉE
# ============================================================================

"""
═══════════════════════════════════════════════════════════════════════════
PARTIE 1 : FONDAMENTAUX
═══════════════════════════════════════════════════════════════════════════

[FICHIER] FICHIER : sqlalchemy_partie1.txt
──────────────────────────────────────────────────────────────────────────

CHAPITRE 0 : INTRODUCTION À SQLALCHEMY (~1h)
├── [REFLEXION] Qu'est-ce que SQLAlchemy ?
│   ├── Définition simple
│   ├── ORM vs SQL brut (comparaison)
│   ├── Avantages SQLAlchemy
│   └── Quand utiliser SQLAlchemy
├── [CONSTRUCTION] Architecture de SQLAlchemy
│   ├── Engine (Moteur)
│   ├── Base déclarative
│   ├── Session
│   └── Modèles
└── [GRAPHIQUE] Comparaison avec alternatives
    ├── Peewee
    ├── Tortoise ORM
    └── Django ORM

CHAPITRE 1 : INSTALLATION ET CONFIGURATION (~1-2h)
├── [OUTILS] Installation
│   ├── pip install sqlalchemy
│   ├── Drivers par SGBD (PostgreSQL, MySQL, SQLite)
│   └── Vérification installation
├── [OUTIL] Configuration Engine
│   ├── SQLite (fichier et mémoire)
│   ├── PostgreSQL
│   ├── MySQL
│   ├── Options create_engine (echo, pool_size, etc.)
│   └── Tester la connexion
├── [DOSSIER] Structure de Projet
│   ├── Petit projet (1 fichier)
│   ├── Projet moyen (Flask/FastAPI)
│   ├── Grand projet (Enterprise)
│   └── Séparation des responsabilités
└── [SECURISE] Configuration par Environnement
    ├── Variables d'environnement
    ├── Fichier .env
    └── Config par environnement (dev/prod/test)

CHAPITRE 2 : CRÉER DES MODÈLES (~1-2h)
├── [DEMARRAGE] Premier Modèle
│   ├── Modèle minimal
│   ├── Décorticage ligne par ligne
│   ├── __tablename__
│   └── Base.metadata.create_all()
├── [NOTE] Modèle Complet
│   ├── Options de colonnes (unique, nullable, index, default)
│   ├── autoincrement, onupdate
│   └── Toutes les options expliquées
├── [DESIGN] Méthodes Spéciales
│   ├── __repr__() (représentation technique)
│   ├── __str__() (représentation humaine)
│   └── Méthodes personnalisées (to_dict, activate, etc.)
└── [CONSTRUCTION] Créer les Tables
    ├── create_all() vs drop_all()
    ├── Créer table spécifique
    └── Vérifier tables créées (SQLite, PostgreSQL, Inspector)


[FICHIER] FICHIER : sqlalchemy_partie1_suite.txt
──────────────────────────────────────────────────────────────────────────

CHAPITRE 3 : TYPES DE COLONNES (~2-3h)
├── [NOMBRE] Types Numériques
│   ├── Integer, SmallInteger, BigInteger
│   ├── Float (approximatif)
│   └── Numeric/Decimal (exact, pour argent)
├── [NOTE] Types Texte
│   ├── String (VARCHAR)
│   ├── Text (texte long)
│   └── Différences et longueurs recommandées
├── [CALENDRIER] Types Date et Heure
│   ├── DateTime (date + heure)
│   ├── Date (date seule)
│   ├── Time (heure seule)
│   └── Gestion timezones (UTC recommandé)
├── [OK] Types Booléens
│   ├── Boolean
│   ├── nullable vs non-nullable
│   └── 2 états vs 3 états
└── [DOSSIER] Types Avancés
    ├── JSON (objets Python)
    ├── Enum (valeurs limitées)
    ├── ARRAY (PostgreSQL)
    ├── Interval (durées)
    └── UUID (identifiants uniques)

CHAPITRE 4 : CONTRAINTES ET OPTIONS (~2h)
├── [VERROUILLE] Contraintes de Colonnes
│   ├── Primary Key (simple et composite)
│   ├── Unique (simple et composite)
│   ├── Nullable
│   └── CheckConstraint
├── [BOOKMARK_TABS] Index
│   ├── Index simple
│   ├── Index composite
│   ├── Index unique
│   └── Quand créer un index
├── [OBJECTIF] Default et OnUpdate
│   ├── default (valeur par défaut)
│   ├── server_default
│   └── onupdate (mise à jour auto)
└── [CALCUL] Colonnes Calculées
    ├── hybrid_property (calculé Python)
    └── Computed (calculé BD)


═══════════════════════════════════════════════════════════════════════════
PARTIE 2 : RELATIONS ET REQUÊTES
═══════════════════════════════════════════════════════════════════════════

[FICHIER] FICHIER : sqlalchemy_partie2.txt
──────────────────────────────────────────────────────────────────────────

CHAPITRE 5 : RELATIONS ENTRE TABLES (~3-4h)
├── [LIEN] Relation One-to-Many (1-N)
│   ├── Concept (User -> Posts)
│   ├── ForeignKey (côté enfant)
│   ├── relationship() (navigation)
│   ├── back_populates
│   ├── lazy loading (select, joined, subquery, dynamic)
│   └── cascade (delete, delete-orphan)
├── [LIEN] Relation Many-to-Many (N-N)
│   ├── Concept (Posts <-> Tags)
│   ├── Table d'association
│   ├── secondary parameter
│   ├── Utilisation (append, remove)
│   └── Table association avec attributs supplémentaires
└── [LIEN] Relation One-to-One (1-1)
    ├── Concept (User <-> Profile)
    ├── uselist=False
    ├── Foreign Key unique
    └── One-to-One optionnel


[FICHIER] FICHIER : sqlalchemy_partie2_suite.txt
──────────────────────────────────────────────────────────────────────────

CHAPITRE 6 : REQUÊTES CRUD (~2-3h)
├── + CREATE - Créer
│   ├── Créer un enregistrement
│   ├── Créer plusieurs (add_all)
│   ├── Gestion d'erreurs (IntegrityError)
│   └── try/except/rollback
├── [RECHERCHE] READ - Lire
│   ├── all(), first(), get()
│   ├── filter() vs filter_by()
│   ├── Opérateurs (==, !=, >, <, in_, like)
│   ├── Opérateurs logiques (and_, or_, not_)
│   ├── Tri (order_by)
│   ├── Limite et offset
│   ├── Compter (count)
│   └── Sélectionner colonnes spécifiques
├── [EDIT] UPDATE - Modifier
│   ├── Modifier un objet
│   ├── Modifier plusieurs attributs
│   └── Bulk update
└── [X] DELETE - Supprimer
    ├── Supprimer un objet
    ├── Supprimer plusieurs
    ├── Bulk delete
    └── Cascade delete

CHAPITRE 7 : REQUÊTES AVANCÉES (~2h)
├── [NOMBRE] Fonctions SQL
│   ├── Agrégations (COUNT, SUM, AVG, MIN, MAX)
│   ├── Fonctions de chaîne (UPPER, LOWER, LENGTH, CONCAT)
│   └── Fonctions de date (NOW, DATE, YEAR, MONTH)
├── [GRAPHIQUE] GROUP BY et Agrégations
│   ├── GROUP BY
│   ├── HAVING
│   └── WHERE vs HAVING
├── [SYNC] Sous-requêtes
│   ├── Sous-requête scalaire
│   ├── Sous-requête dans FROM
│   └── EXISTS
└── [NOTE] SQL Brut
    ├── Exécuter SQL brut
    └── Sécurité (paramètres)

CHAPITRE 8 : JOINTURES ET AGRÉGATIONS (~1-2h)
├── [LIEN] Jointures
│   ├── INNER JOIN (join)
│   ├── LEFT JOIN (outerjoin)
│   ├── Choisir type de JOIN
│   ├── Jointure explicite
│   └── Jointure Many-to-Many
└── [GRAPHIQUE] Comparaison stratégies


═══════════════════════════════════════════════════════════════════════════
PARTIE 3 : SESSIONS, TRANSACTIONS ET PERFORMANCE
═══════════════════════════════════════════════════════════════════════════

[FICHIER] FICHIER : sqlalchemy_partie3.txt
──────────────────────────────────────────────────────────────────────────

CHAPITRE 9 : COMPRENDRE LES SESSIONS (~2-3h)
├── [REFLEXION] Qu'est-ce qu'une Session ?
│   ├── Définition (panier d'achats)
│   ├── Analogie
│   ├── Créer une session
│   └── SessionMaker
├── [SYNC] Cycle de Vie
│   ├── Context manager (recommandé)
│   ├── try/except/finally
│   ├── Helper function
│   └── Scoped session
├── [SCENARIO] États des Objets
│   ├── TRANSIENT (nouveau)
│   ├── PENDING (ajouté, pas commit)
│   ├── PERSISTENT (en BD et session)
│   ├── DELETED (marqué suppression)
│   ├── DETACHED (hors session)
│   └── Diagramme des états
├── [WORLD_MAP] Identity Map
│   ├── Concept (1 ligne = 1 objet)
│   ├── Avantages (cohérence, performance)
│   ├── Limites (scope session)
│   ├── Expiration d'objets
│   └── Refresh
├── [CONFIG] Unit of Work
│   ├── Suivre changements
│   ├── Tracker automatique
│   ├── Inspecter changements (new, dirty, deleted)
│   ├── flush() vs commit()
│   └── autoflush
└── [OBJECTIF] Bonnes Pratiques
    ├── Une session par requête web
    ├── Pas de partage entre threads
    ├── Toujours fermer
    ├── Gérer erreurs
    └── Ne pas utiliser objets détachés

CHAPITRE 10 : TRANSACTIONS (~2h)
├── [GEM_STONE] ACID
│   ├── Atomicity (tout ou rien)
│   ├── Consistency (cohérence)
│   ├── Isolation
│   └── Durability (permanence)
├── [SYNC] Gestion Manuelle
│   ├── begin, commit, rollback
│   └── Exemple transfert bancaire
├── [IMPORTANT] Savepoints
│   ├── Concept (point de sauvegarde)
│   ├── Rollback partiel
│   └── Exemple batch avec erreurs
└── [VERROUILLE] Niveaux d'Isolation
    ├── READ UNCOMMITTED
    ├── READ COMMITTED (défaut)
    ├── REPEATABLE READ
    ├── SERIALIZABLE
    └── Choisir niveau


[FICHIER] FICHIER : sqlalchemy_partie3_suite.txt
──────────────────────────────────────────────────────────────────────────

CHAPITRE 11 : PERFORMANCE ET OPTIMISATION (~2-3h)
├── [GRAPHIQUE] Mesurer Performance
│   ├── echo SQL
│   ├── Compter requêtes
│   └── Explain query
├── [RAPIDE] Optimisations Requêtes
│   ├── Sélectionner colonnes nécessaires
│   ├── Utiliser LIMIT
│   ├── Pagination efficace (keyset)
│   ├── EXISTS vs COUNT
│   └── Bulk operations
├── [CARD_INDEX] Index
│   ├── Créer index
│   ├── Quand créer
│   ├── Index composite
│   └── Vérifier index utilisé
└── [PLUGIN] Connection Pooling
    ├── Concept (réutiliser connexions)
    ├── Configuration (pool_size, max_overflow)
    ├── Types de pools (QueuePool, NullPool, StaticPool)
    └── Dimensionner pool

CHAPITRE 12 : EAGER LOADING ET N+1 (~2h)
├── [LENT] Problème N+1
│   ├── Qu'est-ce que c'est ?
│   ├── Exemple (1 + 100 queries)
│   └── Détecter N+1
├── [OK] Solution 1 : joinedload
│   ├── LEFT OUTER JOIN
│   ├── 1 seule requête
│   ├── Duplication lignes
│   └── Quand utiliser
├── [OK] Solution 2 : subqueryload
│   ├── 2 requêtes (sous-requête)
│   ├── Pas de duplication
│   └── Quand utiliser
├── [OK] Solution 3 : selectinload
│   ├── 2 requêtes (IN clause)
│   ├── Recommandé SQLAlchemy 2.0+
│   └── Quand utiliser
├── [GRAPHIQUE] Comparaison Stratégies
│   └── Tableau comparatif
└── [OBJECTIF] Eager Loading par Défaut
    └── lazy dans relationship


═══════════════════════════════════════════════════════════════════════════
PARTIE 4 : PRODUCTION ET BEST PRACTICES
═══════════════════════════════════════════════════════════════════════════

[FICHIER] FICHIER : sqlalchemy_partie4.txt
──────────────────────────────────────────────────────────────────────────

CHAPITRE 13 : MIGRATIONS AVEC ALEMBIC (~3-4h)
├── [REFLEXION] Pourquoi Alembic ?
│   ├── Problème évolution schéma
│   ├── Solutions sans Alembic (mauvaises)
│   └── Alembic = Git pour BD
├── [OUTILS] Installation et Initialisation
│   ├── pip install alembic
│   ├── alembic init
│   ├── Configuration alembic.ini
│   └── Configuration env.py
├── [NOTE] Créer Migration
│   ├── Migration automatique (--autogenerate)
│   ├── Fichier généré (upgrade/downgrade)
│   ├── Appliquer migration (upgrade head)
│   └── Vérifier état
├── [OUTIL] Migrations Manuelles
│   ├── Quand écrire manuellement
│   ├── Migration de données
│   ├── Renommer colonne
│   ├── Ajouter index, foreign key
│   └── Créer table
├── [SYNC] Gestion Migrations
│   ├── upgrade, downgrade
│   ├── Historique
│   ├── stamp
│   └── merge (branches)
└── [OBJECTIF] Bonnes Pratiques
    ├── Vérifier avant commit
    ├── Messages descriptifs
    ├── Migrations petites
    ├── Tester downgrade
    ├── Backup avant production
    └── Utiliser transactions

CHAPITRE 14 : TESTING (~1-2h)
├── [TEST] Configuration Tests
│   ├── pytest installation
│   ├── Structure projet
│   ├── Base de données test (mémoire)
│   └── Fixtures pytest
├── [TEST] Tester Modèles
│   ├── Test création
│   ├── Test contraintes
│   └── Test relations
├── [TEST] Tester Requêtes
│   └── Test filtrage
└── [USINE] Factories
    ├── factory-boy
    └── Générer données test

CHAPITRE 15 : PRODUCTION (~1-2h)
├── [CONFIG] Configuration Production
│   ├── Variables d'environnement
│   ├── Config par environnement
│   └── database.py production
├── [NOTE] Logging
│   └── Configuration logging
└── [GRAPHIQUE] Monitoring
    └── Logger requêtes lentes

CHAPITRE 16 : BEST PRACTICES (~1h)
├── [OK] Architecture
│   ├── Séparer concerns
│   └── Repositories pattern
├── [OK] Performance
│   ├── Toujours eager load
│   ├── Limiter résultats
│   └── Index colonnes filtrées
├── [OK] Sécurité
│   ├── Paramétrer requêtes
│   └── Valider entrées
└── [OK] Migrations
    ├── Tester avant production
    └── Sauvegarder BD


═══════════════════════════════════════════════════════════════════════════
BONUS : DÉMARRAGE RAPIDE
═══════════════════════════════════════════════════════════════════════════

[FICHIER] FICHIER : sqlalchemy_quickstart.txt (~30 min)
──────────────────────────────────────────────────────────────────────────

[RAPIDE] Installation (5 min)
[CONSTRUCTION] Modèles de Base (10 min)
   ├── User simple
   ├── One-to-Many (User -> Posts)
   ├── Many-to-Many (Posts <-> Tags)
   └── One-to-One (User <-> Profile)
[RAPIDE] CRUD (10 min)
   ├── CREATE
   ├── READ
   ├── UPDATE
   └── DELETE
[HOT] Patterns Courants (5 min)
   ├── Context manager
   ├── Repository
   ├── Eager loading
   └── Bulk operations
[TEST] Testing
[TRANSPORT] Alembic
[ATTENTION] Erreurs Courantes
[LISTE] Checklist
[OBJECTIF] Exemple Complet
"""


# ============================================================================
# [RECHERCHE] INDEX PAR SUJET
# ============================================================================

"""
CHERCHER UN SUJET SPÉCIFIQUE ? TROUVEZ-LE ICI !


──────────────────────────────────────────────────────────────────────────
A
──────────────────────────────────────────────────────────────────────────
ACID                          -> Partie 3, Chapitre 10
add()                         -> Partie 2, Chapitre 6
add_all()                     -> Partie 2, Chapitre 6
Agrégations                   -> Partie 2, Chapitre 7
Alembic                       -> Partie 4, Chapitre 13
and_()                        -> Partie 2, Chapitre 6
ARRAY                         -> Partie 1, Chapitre 3
Association Table             -> Partie 2, Chapitre 5
Atomicité                     -> Partie 3, Chapitre 10
autoflush                     -> Partie 3, Chapitre 9
autoincrement                 -> Partie 1, Chapitre 2


──────────────────────────────────────────────────────────────────────────
B
──────────────────────────────────────────────────────────────────────────
back_populates                -> Partie 2, Chapitre 5
Base                          -> Partie 1, Chapitre 1
Base.metadata.create_all()    -> Partie 1, Chapitre 2
Best Practices                -> Partie 4, Chapitre 16
BigInteger                    -> Partie 1, Chapitre 3
Boolean                       -> Partie 1, Chapitre 3
Bulk operations               -> Partie 3, Chapitre 11


──────────────────────────────────────────────────────────────────────────
C
──────────────────────────────────────────────────────────────────────────
CASCADE                       -> Partie 2, Chapitre 5
CASE                          -> Partie 2, Chapitre 7
CheckConstraint               -> Partie 1, Chapitre 4
close()                       -> Partie 3, Chapitre 9
Column                        -> Partie 1, Chapitre 2
commit()                      -> Partie 3, Chapitre 10
Computed                      -> Partie 1, Chapitre 4
Connection Pooling            -> Partie 3, Chapitre 11
Context Manager               -> Partie 3, Chapitre 9
COUNT()                       -> Partie 2, Chapitre 7
create_engine()               -> Partie 1, Chapitre 1
CRUD                          -> Partie 2, Chapitre 6


──────────────────────────────────────────────────────────────────────────
D
──────────────────────────────────────────────────────────────────────────
Database URL                  -> Partie 1, Chapitre 1
Date                          -> Partie 1, Chapitre 3
DateTime                      -> Partie 1, Chapitre 3
Decimal                       -> Partie 1, Chapitre 3
declarative_base              -> Partie 1, Chapitre 1
default                       -> Partie 1, Chapitre 4
DELETE                        -> Partie 2, Chapitre 6
delete()                      -> Partie 2, Chapitre 6
delete-orphan                 -> Partie 2, Chapitre 5
DETACHED                      -> Partie 3, Chapitre 9
DISTINCT                      -> Partie 2, Chapitre 7
downgrade                     -> Partie 4, Chapitre 13


──────────────────────────────────────────────────────────────────────────
E
──────────────────────────────────────────────────────────────────────────
Eager Loading                 -> Partie 3, Chapitre 12
echo                          -> Partie 1, Chapitre 1
Engine                        -> Partie 1, Chapitre 1
Enum                          -> Partie 1, Chapitre 3
Erreurs courantes             -> Quickstart
EXISTS                        -> Partie 2, Chapitre 7
expire()                      -> Partie 3, Chapitre 9
EXPLAIN                       -> Partie 3, Chapitre 11


──────────────────────────────────────────────────────────────────────────
F
──────────────────────────────────────────────────────────────────────────
factory-boy                   -> Partie 4, Chapitre 14
filter()                      -> Partie 2, Chapitre 6
filter_by()                   -> Partie 2, Chapitre 6
first()                       -> Partie 2, Chapitre 6
Float                         -> Partie 1, Chapitre 3
flush()                       -> Partie 3, Chapitre 9
ForeignKey                    -> Partie 2, Chapitre 5
Fonctions SQL                 -> Partie 2, Chapitre 7


──────────────────────────────────────────────────────────────────────────
G
──────────────────────────────────────────────────────────────────────────
get()                         -> Partie 2, Chapitre 6
GROUP BY                      -> Partie 2, Chapitre 7


──────────────────────────────────────────────────────────────────────────
H
──────────────────────────────────────────────────────────────────────────
HAVING                        -> Partie 2, Chapitre 7
hybrid_property               -> Partie 1, Chapitre 4


──────────────────────────────────────────────────────────────────────────
I
──────────────────────────────────────────────────────────────────────────
Identity Map                  -> Partie 3, Chapitre 9
IN                            -> Partie 2, Chapitre 6
Index                         -> Partie 1, Chapitre 4
INNER JOIN                    -> Partie 2, Chapitre 8
Installation                  -> Partie 1, Chapitre 1
Integer                       -> Partie 1, Chapitre 3
IntegrityError                -> Partie 2, Chapitre 6
Interval                      -> Partie 1, Chapitre 3
Isolation                     -> Partie 3, Chapitre 10


──────────────────────────────────────────────────────────────────────────
J
──────────────────────────────────────────────────────────────────────────
JOIN                          -> Partie 2, Chapitre 8
joinedload                    -> Partie 3, Chapitre 12
JSON                          -> Partie 1, Chapitre 3


──────────────────────────────────────────────────────────────────────────
L
──────────────────────────────────────────────────────────────────────────
lazy                          -> Partie 2, Chapitre 5
LEFT JOIN                     -> Partie 2, Chapitre 8
LIKE                          -> Partie 2, Chapitre 6
limit()                       -> Partie 2, Chapitre 6
Logging                       -> Partie 4, Chapitre 15


──────────────────────────────────────────────────────────────────────────
M
──────────────────────────────────────────────────────────────────────────
Many-to-Many                  -> Partie 2, Chapitre 5
max_overflow                  -> Partie 3, Chapitre 11
Migrations                    -> Partie 4, Chapitre 13
Monitoring                    -> Partie 4, Chapitre 15
MySQL                         -> Partie 1, Chapitre 1


──────────────────────────────────────────────────────────────────────────
N
──────────────────────────────────────────────────────────────────────────
N+1 Problem                   -> Partie 3, Chapitre 12
NOT NULL                      -> Partie 1, Chapitre 4
nullable                      -> Partie 1, Chapitre 4
Numeric                       -> Partie 1, Chapitre 3


──────────────────────────────────────────────────────────────────────────
O
──────────────────────────────────────────────────────────────────────────
offset()                      -> Partie 2, Chapitre 6
One-to-Many                   -> Partie 2, Chapitre 5
One-to-One                    -> Partie 2, Chapitre 5
onupdate                      -> Partie 1, Chapitre 4
or_()                         -> Partie 2, Chapitre 6
order_by()                    -> Partie 2, Chapitre 6
ORM                           -> Partie 1, Chapitre 0
OUTER JOIN                    -> Partie 2, Chapitre 8


──────────────────────────────────────────────────────────────────────────
P
──────────────────────────────────────────────────────────────────────────
Pagination                    -> Partie 2, Chapitre 6
PENDING                       -> Partie 3, Chapitre 9
Performance                   -> Partie 3, Chapitre 11
PERSISTENT                    -> Partie 3, Chapitre 9
pool_size                     -> Partie 3, Chapitre 11
PostgreSQL                    -> Partie 1, Chapitre 1
Primary Key                   -> Partie 1, Chapitre 4
Production                    -> Partie 4, Chapitre 15
pytest                        -> Partie 4, Chapitre 14


──────────────────────────────────────────────────────────────────────────
Q
──────────────────────────────────────────────────────────────────────────
query()                       -> Partie 2, Chapitre 6


──────────────────────────────────────────────────────────────────────────
R
──────────────────────────────────────────────────────────────────────────
READ                          -> Partie 2, Chapitre 6
refresh()                     -> Partie 3, Chapitre 9
relationship()                -> Partie 2, Chapitre 5
Repository                    -> Quickstart
REPEATABLE READ               -> Partie 3, Chapitre 10
__repr__()                    -> Partie 1, Chapitre 2
rollback()                    -> Partie 3, Chapitre 10


──────────────────────────────────────────────────────────────────────────
S
──────────────────────────────────────────────────────────────────────────
Savepoints                    -> Partie 3, Chapitre 10
Scoped Session                -> Partie 3, Chapitre 9
secondary                     -> Partie 2, Chapitre 5
SELECT                        -> Partie 2, Chapitre 6
selectinload                  -> Partie 3, Chapitre 12
SERIALIZABLE                  -> Partie 3, Chapitre 10
server_default                -> Partie 1, Chapitre 4
Session                       -> Partie 3, Chapitre 9
sessionmaker                  -> Partie 3, Chapitre 9
SmallInteger                  -> Partie 1, Chapitre 3
SQLite                        -> Partie 1, Chapitre 1
SQL brut                      -> Partie 2, Chapitre 7
__str__()                     -> Partie 1, Chapitre 2
String                        -> Partie 1, Chapitre 3
Structure projet              -> Partie 1, Chapitre 1
subqueryload                  -> Partie 3, Chapitre 12
Sous-requêtes                 -> Partie 2, Chapitre 7


──────────────────────────────────────────────────────────────────────────
T
──────────────────────────────────────────────────────────────────────────
__tablename__                 -> Partie 1, Chapitre 2
Testing                       -> Partie 4, Chapitre 14
Text                          -> Partie 1, Chapitre 3
Time                          -> Partie 1, Chapitre 3
Timezones                     -> Partie 1, Chapitre 3
Transactions                  -> Partie 3, Chapitre 10
TRANSIENT                     -> Partie 3, Chapitre 9


──────────────────────────────────────────────────────────────────────────
U
──────────────────────────────────────────────────────────────────────────
Unit of Work                  -> Partie 3, Chapitre 9
UNIQUE                        -> Partie 1, Chapitre 4
UPDATE                        -> Partie 2, Chapitre 6
update()                      -> Partie 2, Chapitre 6
upgrade                       -> Partie 4, Chapitre 13
uselist                       -> Partie 2, Chapitre 5
UUID                          -> Partie 1, Chapitre 3


──────────────────────────────────────────────────────────────────────────
V
──────────────────────────────────────────────────────────────────────────
Variables environnement       -> Partie 1, Chapitre 1


──────────────────────────────────────────────────────────────────────────
W
──────────────────────────────────────────────────────────────────────────
WHERE                         -> Partie 2, Chapitre 6
"""


# ============================================================================
# [GUIDE] PARCOURS D'APPRENTISSAGE RECOMMANDÉS
# ============================================================================

"""
PARCOURS 1 : DÉBUTANT ABSOLU (10-15h)
═════════════════════════════════════════════════════════════════════════

Jour 1 (3h)
├── sqlalchemy_quickstart.txt (30 min)
├── Partie 1, Chapitre 0 (1h)
└── Partie 1, Chapitre 1 (1h30)

Jour 2 (3h)
├── Partie 1, Chapitre 2 (1h30)
└── Partie 1, Chapitre 3 (1h30)

Jour 3 (2h)
└── Partie 1, Chapitre 4 (2h)

Jour 4 (3h)
└── Partie 2, Chapitre 5 (3h)

Jour 5 (2h)
└── Partie 2, Chapitre 6 (2h)

EXERCICE : Créer un blog simple (User, Post, Comment)


PARCOURS 2 : DÉVELOPPEUR INTERMÉDIAIRE (15-20h)
═════════════════════════════════════════════════════════════════════════

Semaine 1
├── Parcours débutant complet
└── Partie 2, Chapitres 7-8 (3h)

Semaine 2
├── Partie 3, Chapitre 9 (3h)
├── Partie 3, Chapitre 10 (2h)
└── Partie 3, Chapitre 11 (2h)

Semaine 3
├── Partie 3, Chapitre 12 (2h)
└── Partie 4, Chapitre 13 (3h)

EXERCICE : E-commerce (Product, Order, User, Payment, Review)


PARCOURS 3 : EXPERT / PRODUCTION (20-30h)
═════════════════════════════════════════════════════════════════════════

Tout lire + Pratiquer sur projet réel

Focus :
├── Performance (Partie 3, Chapitre 11-12)
├── Transactions complexes (Partie 3, Chapitre 10)
├── Migrations (Partie 4, Chapitre 13)
├── Testing (Partie 4, Chapitre 14)
└── Production (Partie 4, Chapitre 15-16)

PROJET : Application enterprise complète


PARCOURS 4 : RÉFÉRENCE RAPIDE (Ad-hoc)
═════════════════════════════════════════════════════════════════════════

Utilisez cet index pour trouver rapidement :
├── Un type de colonne
├── Une syntaxe de requête
├── Un pattern spécifique
└── Une solution à un problème
"""


# ============================================================================
# [?] FAQ - QUESTIONS FRÉQUENTES
# ============================================================================

"""
Q: Par où commencer ?
A: sqlalchemy_quickstart.txt puis Partie 1


Q: Je connais déjà les bases SQL, puis-je sauter des parties ?
A: Oui, commencez par Partie 2 (Relations et Requêtes)


Q: Quelle est la différence entre filter() et filter_by() ?
A: Partie 2, Chapitre 6 - filter_by() pour égalité simple,
   filter() pour conditions complexes


Q: Comment éviter le problème N+1 ?
A: Partie 3, Chapitre 12 - Utiliser selectinload, joinedload


Q: Comment faire des migrations de schéma ?
A: Partie 4, Chapitre 13 - Alembic


Q: Quelle est la différence entre commit() et flush() ?
A: Partie 3, Chapitre 9 - flush() envoie à BD sans valider,
   commit() valide définitivement


Q: Comment configurer pour production ?
A: Partie 4, Chapitres 15-16


Q: Comment tester mon code SQLAlchemy ?
A: Partie 4, Chapitre 14 - pytest avec base en mémoire


Q: Quelle stratégie lazy loading choisir ?
A: Partie 2, Chapitre 5 et Partie 3, Chapitre 12
   Défaut : selectinload


Q: Dois-je utiliser SQLAlchemy Core ou ORM ?
A: ORM pour 95% des cas (ce guide)
   Core pour requêtes ultra-complexes
"""


# ============================================================================
# [OBJECTIF] OBJECTIFS PÉDAGOGIQUES PAR PARTIE
# ============================================================================

"""
APRÈS LA PARTIE 1, VOUS SAUREZ :
═════════════════════════════════════════════════════════════════════════
[OK] Installer et configurer SQLAlchemy
[OK] Créer des modèles (tables)
[OK] Utiliser tous les types de colonnes
[OK] Ajouter contraintes et index
[OK] Structurer un projet SQLAlchemy


APRÈS LA PARTIE 2, VOUS SAUREZ :
═════════════════════════════════════════════════════════════════════════
[OK] Créer relations entre tables (1-N, N-N, 1-1)
[OK] Effectuer opérations CRUD
[OK] Écrire requêtes avancées
[OK] Utiliser jointures et agrégations
[OK] Filtrer, trier, paginer


APRÈS LA PARTIE 3, VOUS SAUREZ :
═════════════════════════════════════════════════════════════════════════
[OK] Gérer sessions correctement
[OK] Utiliser transactions
[OK] Optimiser performance
[OK] Résoudre problème N+1
[OK] Configurer pooling


APRÈS LA PARTIE 4, VOUS SAUREZ :
═════════════════════════════════════════════════════════════════════════
[OK] Gérer migrations avec Alembic
[OK] Tester code SQLAlchemy
[OK] Déployer en production
[OK] Suivre best practices
[OK] Monitoring et logging
"""


# ============================================================================
# [RAPIDE] POUR ALLER PLUS LOIN
# ============================================================================

"""
APRÈS CE GUIDE, EXPLOREZ :

1. SQLAlchemy Async
   - Pour applications asynchrones (FastAPI async, asyncio)
   - Documentation officielle SQLAlchemy 2.0

2. Events et Listeners
   - Hooks avant/après INSERT, UPDATE, DELETE
   - Validation avancée
   - Audit automatique

3. Hybrid Properties et Methods
   - Propriétés calculées utilisables dans queries
   - Encapsulation logique métier

4. Association Proxies
   - Simplifier navigation Many-to-Many
   - Accès direct aux relations

5. Custom Types
   - Créer vos propres types de colonnes
   - Serialization/deserialization custom

6. Extensions
   - SQLAlchemy-Utils (types utilitaires)
   - GeoAlchemy2 (données géographiques)
   - SQLAlchemy-Continuum (audit/versioning)

7. Contribuer
   - Open source projects utilisant SQLAlchemy
   - Améliorer ce guide !
"""


# ============================================================================
# [TEL] RESSOURCES SUPPLÉMENTAIRES
# ============================================================================

"""
DOCUMENTATION OFFICIELLE
────────────────────────────────────────────────────────────────────────
https://docs.sqlalchemy.org/

TUTORIELS COMPLÉMENTAIRES
────────────────────────────────────────────────────────────────────────
- SQLAlchemy 2.0 Tutorial (officiel)
- Real Python SQLAlchemy Tutorials
- Full Stack Python - SQLAlchemy

VIDÉOS
────────────────────────────────────────────────────────────────────────
- PyCon talks sur SQLAlchemy
- SQLAlchemy Core vs ORM

LIVRES
────────────────────────────────────────────────────────────────────────
- "Essential SQLAlchemy" by Jason Myers & Rick Copeland
- "SQLAlchemy Documentation" (PDF official)

COMMUNAUTÉ
────────────────────────────────────────────────────────────────────────
- Stack Overflow (tag: sqlalchemy)
- Reddit r/SQLAlchemy
- GitHub Issues SQLAlchemy
"""


# ============================================================================
# [OK] RÉCAPITULATIF
# ============================================================================

"""
VOUS AVEZ MAINTENANT :
══════════════════════════════════════════════════════════════════════════

[DOCS] 8 fichiers de documentation (~200KB)
[OBJECTIF] 16 chapitres progressifs
[IDEE] Des centaines d'exemples pratiques
[RAPIDE] Un guide de démarrage rapide
[GUIDE] Cet index complet

PROCHAINE ÉTAPE :
══════════════════════════════════════════════════════════════════════════

1. Commencez par sqlalchemy_quickstart.txt (30 min)
2. Suivez un parcours d'apprentissage adapté
3. Pratiquez avec un projet réel
4. Consultez l'index quand besoin

BON APPRENTISSAGE ! [RAPIDE]
"""
# ============================================================================
# [LIVRE] SQLALCHEMY - GUIDE DE DÉMARRAGE RAPIDE
# ============================================================================
#
# [OBJECTIF] CE GUIDE EST UN RÉSUMÉ ULTRA-CONDENSÉ
#
# Pour apprendre en profondeur, consultez les 4 parties complètes :
# - sqlalchemy_partie1.txt et partie1_suite.txt
# - sqlalchemy_partie2.txt et partie2_suite.txt
# - sqlalchemy_partie3.txt et partie3_suite.txt
# - sqlalchemy_partie4.txt
#
# [TEMPS] TEMPS : 30 minutes
# [DOCS] OBJECTIF : Être opérationnel rapidement
# ============================================================================


# ============================================================================
# [RAPIDE] INSTALLATION ET CONFIGURATION (5 min)
# ============================================================================

"""
INSTALLATION
"""

# Installation basique
pip install sqlalchemy

# Avec PostgreSQL
pip install sqlalchemy psycopg2-binary

# Avec MySQL
pip install sqlalchemy pymysql

# Pour migrations
pip install alembic

# Pour tests
pip install pytest


"""
STRUCTURE PROJET RECOMMANDÉE
"""

"""
mon_projet/
├── venv/
├── models.py          # Vos modèles
├── database.py        # Configuration engine/session
├── main.py            # Code principal
└── requirements.txt
"""


"""
database.py - Configuration de base
"""

from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

# Engine (connexion à la BD)
engine = create_engine(
    'sqlite:///app.db',  # SQLite local
    echo=True  # Afficher SQL (dev seulement)
)

# Base pour tous les modèles
Base = declarative_base()

# Session factory
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)

# Helper pour obtenir une session
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


"""
DATABASE URLs COURANTES
"""

# SQLite (fichier local)
'sqlite:///app.db'

# PostgreSQL
'postgresql://user:password@localhost:5432/mydatabase'

# MySQL
'mysql+pymysql://user:password@localhost:3306/mydatabase'

# PostgreSQL avec variables d'environnement (production)
import os
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///app.db')


# ============================================================================
# [NOTE] MODÈLES DE BASE (10 min)
# ============================================================================

"""
models.py - Exemples de modèles courants
"""

from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, ForeignKey, Table
from sqlalchemy.orm import relationship
from datetime import datetime
from database import Base


# ──────────────────────────────────────────────────────────────────────────
# MODÈLE SIMPLE : USER
# ──────────────────────────────────────────────────────────────────────────

class User(Base):
    """Modèle User de base"""
    __tablename__ = 'users'
    
    # Primary Key
    id = Column(Integer, primary_key=True)
    
    # Colonnes obligatoires
    username = Column(String(50), unique=True, nullable=False, index=True)
    email = Column(String(120), unique=True, nullable=False)
    password_hash = Column(String(128), nullable=False)
    
    # Colonnes optionnelles
    bio = Column(Text)
    
    # Boolean
    is_active = Column(Boolean, default=True, nullable=False)
    is_admin = Column(Boolean, default=False, nullable=False)
    
    # Timestamps
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def __repr__(self):
        return f"<User(id={self.id}, username='{self.username}')>"


# ──────────────────────────────────────────────────────────────────────────
# RELATION ONE-TO-MANY : USER -> POSTS
# ──────────────────────────────────────────────────────────────────────────

class Post(Base):
    """Modèle Post avec relation vers User"""
    __tablename__ = 'posts'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    content = Column(Text)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # Foreign Key vers users
    user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    
    # Relationship (navigation Python)
    author = relationship('User', back_populates='posts')
    
    def __repr__(self):
        return f"<Post(id={self.id}, title='{self.title}')>"

# Ajouter la relation inverse dans User
User.posts = relationship('Post', back_populates='author', lazy='selectin')


# ──────────────────────────────────────────────────────────────────────────
# RELATION MANY-TO-MANY : POSTS <-> TAGS
# ──────────────────────────────────────────────────────────────────────────

# Table d'association
post_tags = Table(
    'post_tags',
    Base.metadata,
    Column('post_id', Integer, ForeignKey('posts.id'), primary_key=True),
    Column('tag_id', Integer, ForeignKey('tags.id'), primary_key=True)
)

class Tag(Base):
    """Modèle Tag"""
    __tablename__ = 'tags'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True, nullable=False)
    
    # Relationship Many-to-Many
    posts = relationship('Post', secondary=post_tags, back_populates='tags')

# Ajouter relation inverse dans Post
Post.tags = relationship('Tag', secondary=post_tags, back_populates='posts')


# ──────────────────────────────────────────────────────────────────────────
# RELATION ONE-TO-ONE : USER <-> PROFILE
# ──────────────────────────────────────────────────────────────────────────

class Profile(Base):
    """Modèle Profile (One-to-One avec User)"""
    __tablename__ = 'profiles'
    
    id = Column(Integer, primary_key=True)
    avatar_url = Column(String(200))
    website = Column(String(200))
    location = Column(String(100))
    
    # Foreign Key UNIQUE (One-to-One)
    user_id = Column(Integer, ForeignKey('users.id'), unique=True)
    
    # Relationship
    user = relationship('User', back_populates='profile')

# Ajouter relation dans User
User.profile = relationship('Profile', back_populates='user', uselist=False)


# ============================================================================
# [RAPIDE] OPÉRATIONS CRUD (10 min)
# ============================================================================

"""
main.py - Exemples d'utilisation
"""

from sqlalchemy.orm import Session
from database import engine, Base, SessionLocal
from models import User, Post, Tag, Profile


# ──────────────────────────────────────────────────────────────────────────
# CRÉER LES TABLES
# ──────────────────────────────────────────────────────────────────────────

def init_db():
    """Créer toutes les tables"""
    Base.metadata.create_all(bind=engine)
    print("[OK] Tables créées")


# ──────────────────────────────────────────────────────────────────────────
# CREATE - CRÉER
# ──────────────────────────────────────────────────────────────────────────

def create_user_example():
    """Créer un utilisateur"""
    session = SessionLocal()
    
    try:
        # Créer user
        user = User(
            username='alice',
            email='alice@example.com',
            password_hash='hashed_password_here',
            bio='Python developer'
        )
        
        session.add(user)
        session.commit()
        
        print(f"[OK] User créé : {user.id}")
        return user.id
        
    except Exception as e:
        session.rollback()
        print(f"[X] Erreur : {e}")
    finally:
        session.close()


def create_post_with_tags_example(user_id):
    """Créer un post avec des tags"""
    session = SessionLocal()
    
    try:
        # Récupérer user
        user = session.query(User).get(user_id)
        
        # Créer ou récupérer tags
        python_tag = session.query(Tag).filter_by(name='python').first()
        if not python_tag:
            python_tag = Tag(name='python')
            session.add(python_tag)
        
        web_tag = session.query(Tag).filter_by(name='web').first()
        if not web_tag:
            web_tag = Tag(name='web')
            session.add(web_tag)
        
        # Créer post
        post = Post(
            title='Introduction à Python',
            content='Python est un langage...',
            author=user
        )
        
        # Ajouter tags
        post.tags.append(python_tag)
        post.tags.append(web_tag)
        
        session.add(post)
        session.commit()
        
        print(f"[OK] Post créé : {post.id}")
        return post.id
        
    except Exception as e:
        session.rollback()
        print(f"[X] Erreur : {e}")
    finally:
        session.close()


# ──────────────────────────────────────────────────────────────────────────
# READ - LIRE
# ──────────────────────────────────────────────────────────────────────────

def read_examples():
    """Exemples de lecture"""
    session = SessionLocal()
    
    try:
        # Tous les users
        users = session.query(User).all()
        print(f"Total users : {len(users)}")
        
        # Premier user
        first_user = session.query(User).first()
        
        # User par ID
        user = session.query(User).get(1)
        
        # Filtrer
        alice = session.query(User).filter_by(username='alice').first()
        
        # Filtrer avec conditions
        active_users = session.query(User).filter(
            User.is_active == True
        ).all()
        
        # Plusieurs conditions (AND)
        from sqlalchemy import and_
        results = session.query(User).filter(
            and_(
                User.is_active == True,
                User.username.like('a%')
            )
        ).all()
        
        # Trier
        users_sorted = session.query(User).order_by(User.username).all()
        
        # Limiter
        first_10 = session.query(User).limit(10).all()
        
        # Pagination
        page = 1
        per_page = 20
        users_page = session.query(User).offset(
            (page - 1) * per_page
        ).limit(per_page).all()
        
        # Compter
        count = session.query(User).count()
        
        # Sélectionner colonnes spécifiques
        usernames = session.query(User.username).all()
        
        # Accéder aux relations
        user_with_posts = session.query(User).first()
        for post in user_with_posts.posts:
            print(f"  - {post.title}")
        
        print("[OK] Lectures effectuées")
        
    finally:
        session.close()


# ──────────────────────────────────────────────────────────────────────────
# UPDATE - MODIFIER
# ──────────────────────────────────────────────────────────────────────────

def update_examples():
    """Exemples de modification"""
    session = SessionLocal()
    
    try:
        # Modifier un objet
        user = session.query(User).filter_by(username='alice').first()
        user.bio = 'Senior Python Developer'
        user.email = 'alice.new@example.com'
        
        session.commit()
        print("[OK] User modifié")
        
        # Bulk update (plusieurs enregistrements)
        session.query(User).filter(
            User.is_active == False
        ).update({'is_active': True})
        
        session.commit()
        print("[OK] Bulk update effectué")
        
    except Exception as e:
        session.rollback()
        print(f"[X] Erreur : {e}")
    finally:
        session.close()


# ──────────────────────────────────────────────────────────────────────────
# DELETE - SUPPRIMER
# ──────────────────────────────────────────────────────────────────────────

def delete_examples():
    """Exemples de suppression"""
    session = SessionLocal()
    
    try:
        # Supprimer un objet
        user = session.query(User).filter_by(username='test').first()
        if user:
            session.delete(user)
            session.commit()
            print("[OK] User supprimé")
        
        # Bulk delete
        session.query(User).filter(
            User.is_active == False
        ).delete()
        
        session.commit()
        print("[OK] Bulk delete effectué")
        
    except Exception as e:
        session.rollback()
        print(f"[X] Erreur : {e}")
    finally:
        session.close()


# ============================================================================
# [HOT] PATTERNS COURANTS (5 min)
# ============================================================================

# ──────────────────────────────────────────────────────────────────────────
# PATTERN 1 : CONTEXT MANAGER (Recommandé)
# ──────────────────────────────────────────────────────────────────────────

def pattern_context_manager():
    """Pattern avec context manager"""
    with SessionLocal() as session:
        user = User(username='bob', email='bob@example.com', password_hash='hash')
        session.add(user)
        session.commit()
        return user.id
    # session.close() automatique


# ──────────────────────────────────────────────────────────────────────────
# PATTERN 2 : REPOSITORY (Clean Architecture)
# ──────────────────────────────────────────────────────────────────────────

class UserRepository:
    """Repository pour User"""
    
    def __init__(self, session: Session):
        self.session = session
    
    def create(self, **kwargs):
        """Créer user"""
        user = User(**kwargs)
        self.session.add(user)
        self.session.flush()
        return user
    
    def get_by_id(self, user_id: int):
        """Récupérer par ID"""
        return self.session.query(User).get(user_id)
    
    def get_by_username(self, username: str):
        """Récupérer par username"""
        return self.session.query(User).filter_by(username=username).first()
    
    def get_all(self, skip: int = 0, limit: int = 100):
        """Liste paginée"""
        return self.session.query(User).offset(skip).limit(limit).all()
    
    def update(self, user_id: int, **kwargs):
        """Modifier user"""
        user = self.get_by_id(user_id)
        if user:
            for key, value in kwargs.items():
                setattr(user, key, value)
            self.session.flush()
        return user
    
    def delete(self, user_id: int):
        """Supprimer user"""
        user = self.get_by_id(user_id)
        if user:
            self.session.delete(user)
            self.session.flush()

# Utilisation
def use_repository():
    with SessionLocal() as session:
        repo = UserRepository(session)
        
        user = repo.create(username='charlie', email='charlie@example.com', password_hash='hash')
        session.commit()
        
        found = repo.get_by_username('charlie')
        print(f"Found: {found.username}")


# ──────────────────────────────────────────────────────────────────────────
# PATTERN 3 : EAGER LOADING (Performance)
# ──────────────────────────────────────────────────────────────────────────

from sqlalchemy.orm import selectinload, joinedload

def pattern_eager_loading():
    """Éviter N+1 queries"""
    session = SessionLocal()
    
    # [X] MAUVAIS (N+1 queries)
    users = session.query(User).all()
    for user in users:
        print(user.posts)  # Nouvelle query par user !
    
    # [OK] BON (Eager loading)
    users = session.query(User).options(
        selectinload(User.posts)  # Charge posts en avance
    ).all()
    
    for user in users:
        print(user.posts)  # Pas de query supplémentaire
    
    session.close()


# ──────────────────────────────────────────────────────────────────────────
# PATTERN 4 : BULK OPERATIONS (Performance)
# ──────────────────────────────────────────────────────────────────────────

def pattern_bulk_insert():
    """Insert en masse"""
    session = SessionLocal()
    
    # [X] LENT
    for i in range(1000):
        user = User(username=f'user{i}', email=f'user{i}@example.com', password_hash='hash')
        session.add(user)
        session.commit()  # 1000 commits !
    
    # [OK] RAPIDE
    users = [
        User(username=f'user{i}', email=f'user{i}@example.com', password_hash='hash')
        for i in range(1000)
    ]
    session.add_all(users)
    session.commit()  # 1 commit
    
    # [OK] ENCORE PLUS RAPIDE (bypass ORM)
    session.bulk_insert_mappings(
        User,
        [{'username': f'user{i}', 'email': f'user{i}@example.com', 'password_hash': 'hash'}
         for i in range(1000)]
    )
    session.commit()
    
    session.close()


# ============================================================================
# [TEST] TESTING
# ============================================================================

"""
conftest.py - Configuration pytest
"""

"""
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from database import Base

@pytest.fixture(scope='function')
def db_session():
    # Base de données en mémoire pour tests
    engine = create_engine('sqlite:///:memory:')
    Base.metadata.create_all(engine)
    
    Session = sessionmaker(bind=engine)
    session = Session()
    
    yield session
    
    session.close()
    Base.metadata.drop_all(engine)
"""

"""
test_models.py - Tests
"""

"""
from models import User

def test_create_user(db_session):
    user = User(username='test', email='test@example.com', password_hash='hash')
    db_session.add(user)
    db_session.commit()
    
    assert user.id is not None
    assert user.username == 'test'

def test_user_relationship(db_session):
    user = User(username='test', email='test@example.com', password_hash='hash')
    post = Post(title='Test', content='Content', author=user)
    
    db_session.add_all([user, post])
    db_session.commit()
    
    assert len(user.posts) == 1
    assert post.author.username == 'test'
"""


# ============================================================================
# [TRANSPORT] ALEMBIC - MIGRATIONS
# ============================================================================

"""
INITIALISER ALEMBIC
"""

# Terminal
"""
alembic init alembic
"""

"""
CONFIGURER alembic/env.py
"""

"""
# Dans alembic/env.py, ajouter :

from models import Base
target_metadata = Base.metadata
"""

"""
CRÉER MIGRATION
"""

# Après modification des modèles
"""
alembic revision --autogenerate -m "Add bio column to users"
"""

"""
APPLIQUER MIGRATION
"""

"""
alembic upgrade head
"""

"""
ANNULER MIGRATION
"""

"""
alembic downgrade -1
"""


# ============================================================================
# [ATTENTION] ERREURS COURANTES À ÉVITER
# ============================================================================

"""
[X] ERREUR 1 : Oublier de fermer la session
"""

# MAUVAIS
session = SessionLocal()
users = session.query(User).all()
# Oubli de session.close() !

# BON
with SessionLocal() as session:
    users = session.query(User).all()
# Fermeture automatique


"""
[X] ERREUR 2 : Problème N+1
"""

# MAUVAIS
users = session.query(User).all()
for user in users:
    print(user.posts)  # N+1 queries

# BON
users = session.query(User).options(selectinload(User.posts)).all()


"""
[X] ERREUR 3 : Modifier objet détaché
"""

# MAUVAIS
session = SessionLocal()
user = session.query(User).first()
session.close()

user.username = 'new'  # Objet détaché, changement perdu !

# BON
with SessionLocal() as session:
    user = session.query(User).first()
    user.username = 'new'
    session.commit()  # Changement sauvegardé


"""
[X] ERREUR 4 : Ne pas gérer les erreurs
"""

# MAUVAIS
session.add(user)
session.commit()  # Peut crasher !

# BON
try:
    session.add(user)
    session.commit()
except Exception as e:
    session.rollback()
    print(f"Erreur : {e}")


"""
[X] ERREUR 5 : Utiliser datetime.utcnow() avec parenthèses
"""

# MAUVAIS
created_at = Column(DateTime, default=datetime.utcnow())
# Évalue une seule fois au démarrage !

# BON
created_at = Column(DateTime, default=datetime.utcnow)
# Fonction appelée à chaque INSERT


# ============================================================================
# [LISTE] CHECKLIST DE DÉMARRAGE
# ============================================================================

"""
[OK] AVANT DE CODER

1. [ ] Installer SQLAlchemy
2. [ ] Créer database.py avec engine et Base
3. [ ] Créer models.py avec vos modèles
4. [ ] Définir relations (ForeignKey, relationship)
5. [ ] Créer tables avec Base.metadata.create_all()


[OK] EN DÉVELOPPEMENT

1. [ ] Utiliser echo=True pour voir SQL
2. [ ] Utiliser context manager (with)
3. [ ] Eager load avec selectinload
4. [ ] Tester avec pytest
5. [ ] Gérer erreurs avec try/except


[OK] AVANT PRODUCTION

1. [ ] Initialiser Alembic
2. [ ] Créer migrations
3. [ ] Tester migrations (upgrade + downgrade)
4. [ ] Configurer pool de connexions
5. [ ] Désactiver echo
6. [ ] Sauvegarder BD avant migration
7. [ ] Configurer logging
8. [ ] Ajouter index sur colonnes filtrées
"""


# ============================================================================
# [OBJECTIF] EXEMPLE COMPLET MINIMAL
# ============================================================================

def exemple_complet():
    """Exemple complet fonctionnel"""
    
    # 1. Créer tables
    Base.metadata.create_all(engine)
    
    # 2. Créer données
    with SessionLocal() as session:
        # User
        user = User(
            username='alice',
            email='alice@example.com',
            password_hash='hashed_password'
        )
        session.add(user)
        session.flush()  # Obtenir user.id
        
        # Post
        post = Post(
            title='Mon premier post',
            content='Bonjour le monde !',
            author=user
        )
        session.add(post)
        
        # Tags
        tag1 = Tag(name='python')
        tag2 = Tag(name='tutorial')
        post.tags.extend([tag1, tag2])
        
        session.commit()
        
        print(f"[OK] User créé : {user.id}")
        print(f"[OK] Post créé : {post.id}")
    
    # 3. Lire données
    with SessionLocal() as session:
        # Eager load pour éviter N+1
        users = session.query(User).options(
            selectinload(User.posts).selectinload(Post.tags)
        ).all()
        
        for user in users:
            print(f"\nUser: {user.username}")
            for post in user.posts:
                print(f"  Post: {post.title}")
                print(f"  Tags: {', '.join(tag.name for tag in post.tags)}")


# ============================================================================
# [RAPIDE] LANCER L'EXEMPLE
# ============================================================================

if __name__ == '__main__':
    print("[RAPIDE] Démarrage exemple SQLAlchemy...\n")
    
    # Initialiser BD
    init_db()
    
    # Exemple complet
    exemple_complet()
    
    print("\n[OK] Terminé !")


"""
[COURS] PROCHAINES ÉTAPES

Maintenant que vous maîtrisez les bases :

1. Lisez les parties complètes pour approfondir
2. Pratiquez avec un vrai projet
3. Explorez features avancées :
   - Hybrid properties
   - Association proxies
   - Events et listeners
   - Async SQLAlchemy

Bon courage ! [FORCE]
"""
# ============================================================================
# [LIVRE] SQLALCHEMY - GUIDE ULTRA-DÉTAILLÉ POUR DÉBUTANTS
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR MAÎTRISER SQLALCHEMY DE ZÉRO À EXPERT
#
# Ce guide est organisé en 4 parties progressives :
#
# PARTIE 1 : INTRODUCTION ET BASES (sqlalchemy_partie1.txt)
# - Chapitre 0 : Introduction à SQLAlchemy
# - Chapitre 1 : Installation et Configuration
# - Chapitre 2 : Créer des Modèles (Tables)
# - Chapitre 3 : Types de Colonnes
# - Chapitre 4 : Contraintes et Options
#
# PARTIE 2 : RELATIONS ET REQUÊTES (sqlalchemy_partie2.txt)
# - Chapitre 5 : Relations entre Tables
# - Chapitre 6 : Requêtes de Base (SELECT)
# - Chapitre 7 : Requêtes Avancées
# - Chapitre 8 : Jointures
#
# PARTIE 3 : SESSIONS ET TRANSACTIONS (sqlalchemy_partie3.txt)
# - Chapitre 9 : Comprendre les Sessions
# - Chapitre 10 : Transactions
# - Chapitre 11 : Performance et Optimisation
# - Chapitre 12 : Eager Loading
#
# PARTIE 4 : PRODUCTION (sqlalchemy_partie4.txt)
# - Chapitre 13 : Migrations avec Alembic
# - Chapitre 14 : Pooling de Connexions
# - Chapitre 15 : Testing
# - Chapitre 16 : Best Practices Production
#
# [TEMPS] TEMPS DE LECTURE TOTAL : ~15-20 heures
# [DOCS] PRÉREQUIS : Python de base, SQL de base
#
# [IDEE] COMMENT UTILISER CE GUIDE :
# 1. Lisez les parties dans l'ordre
# 2. Testez TOUS les exemples dans un REPL Python
# 3. Faites les exercices pratiques
# 4. Créez vos propres projets
#
# ============================================================================

"""
[OBJECTIF] PHILOSOPHIE DE CE GUIDE

COMMENT ? -> Explications pas à pas
POURQUOI ? -> Raisons et contexte
QUAND ? -> Cas d'usage concrets
PRATIQUE -> Exemples réels et exercices

Ce guide vise à être VOTRE SEULE RÉFÉRENCE SQLAlchemy !
"""

# ============================================================================
# [NOTE] CONVENTIONS UTILISÉES DANS CE GUIDE
# ============================================================================

"""
[IDEE] Information importante
[REFLEXION] Question / Réflexion
[OK] Bonne pratique
[X] Mauvaise pratique
[ATTENTION] Attention / Avertissement
[CLE] Point clé à retenir
[COURS] Exercice pratique
[DOCS] Résumé
[OBJECTIF] Objectif
[TEMPS] Temps estimé
[RAPIDE] Prêt pour la suite
"""

# ============================================================================
# [GUIDE] CHAPITRE 0 : INTRODUCTION À SQLALCHEMY
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est SQLAlchemy et son rôle
[OK] Différence entre ORM et SQL brut
[OK] Architecture de SQLAlchemy
[OK] Quand utiliser SQLAlchemy
[OK] Alternatives à SQLAlchemy
"""

# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QUE SQLALCHEMY ?
# ----------------------------------------------------------------------------

"""
DÉFINITION SIMPLE

SQLAlchemy est une bibliothèque Python qui facilite l'interaction avec
les bases de données SQL en permettant de manipuler des tables comme
des objets Python.

[IDEE] ORM = Object-Relational Mapping
Mapping = Correspondance entre objets Python et tables SQL


ANALOGIE [CONSTRUCTION]

BASE DE DONNÉES :
Table SQL = Feuille Excel structurée
Ligne = Enregistrement
Colonne = Champ

SQLALCHEMY :
Table = Classe Python
Ligne = Instance de classe
Colonne = Attribut de classe


SANS SQLALCHEMY (SQL brut)
---------------------------
"""

import sqlite3

# Connexion
conn = sqlite3.connect('database.db')
cursor = conn.cursor()

# Créer table
cursor.execute('''
    CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        email TEXT UNIQUE
    )
''')

# Insérer données
cursor.execute(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    ('Alice', 'alice@example.com')
)
conn.commit()

# Récupérer données
cursor.execute("SELECT * FROM users WHERE name = ?", ('Alice',))
row = cursor.fetchone()
print(row)  # (1, 'Alice', 'alice@example.com')

# Accéder aux données par index
user_id = row[0]
user_name = row[1]
user_email = row[2]

conn.close()

"""
[X] PROBLÈMES SQL BRUT

1. CODE VERBEUX
   Beaucoup de boilerplate pour chaque opération
   
2. STRINGS SQL
   Pas d'autocomplétion IDE
   Erreurs de syntaxe SQL difficiles à déboguer
   Typos non détectées jusqu'à l'exécution
   
3. GESTION MANUELLE CONNEXIONS
   Ouvrir/fermer connexions manuellement
   Risque de fuites de connexions
   
4. PAS DE VALIDATION
   Aucune validation des types Python
   Aucune contrainte côté code
   
5. RÉSULTATS BRUTS
   Tuples difficiles à manipuler
   Pas d'objets Python structurés
   
6. INJECTION SQL
   Risque si pas bien fait
   
7. PORTABILITÉ
   SQL spécifique à chaque SGBD
   MySQL ≠ PostgreSQL ≠ SQLite


AVEC SQLALCHEMY (ORM)
---------------------
"""

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker

# Configuration
Base = declarative_base()
engine = create_engine('sqlite:///database.db')

# Définir modèle
class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    name = Column(String, nullable=False)
    email = Column(String, unique=True)

# Créer tables
Base.metadata.create_all(engine)

# Session
Session = sessionmaker(bind=engine)
session = Session()

# Insérer données
user = User(name='Alice', email='alice@example.com')
session.add(user)
session.commit()

# Récupérer données
user = session.query(User).filter_by(name='Alice').first()

# Accéder aux données comme attributs
print(user.id)     # 1
print(user.name)   # 'Alice'
print(user.email)  # 'alice@example.com'

session.close()

"""
[OK] AVANTAGES SQLALCHEMY

1. CODE PYTHONIQUE
   Manipuler des objets Python, pas des strings SQL
   
2. AUTOCOMPLÉTION
   IDE aide avec attributs et méthodes
   user.name -> autocomplétion !
   
3. VALIDATION AUTOMATIQUE
   Types Python vérifiés
   Contraintes appliquées
   
4. SÉCURITÉ
   Protection contre injection SQL automatique
   Paramètres échappés automatiquement
   
5. ABSTRACTION BASE DE DONNÉES
   Code identique pour SQLite, PostgreSQL, MySQL, etc.
   Changer de SGBD = changer une ligne de config
   
6. RELATIONS
   user.posts automatique (foreign keys gérées)
   
7. MIGRATION FACILITÉE
   Avec Alembic, évolution du schéma tracée
   
8. OPTIMISATION
   Lazy loading, eager loading
   Requêtes optimisées automatiquement


[IDEE] SQLALCHEMY = 2 COMPOSANTS

┌─────────────────────────────────────┐
│         SQLALCHEMY                  │
├─────────────────────────────────────┤
│                                     │
│  ┌──────────────────────────────┐  │
│  │         ORM (HAUT NIVEAU)    │  │
│  │  Classes Python = Tables     │  │
│  │  user.name, session.add()    │  │
│  └──────────────────────────────┘  │
│              v utilise              │
│  ┌──────────────────────────────┐  │
│  │      CORE (BAS NIVEAU)       │  │
│  │  SQL Expression Language     │  │
│  │  select(), insert(), etc.    │  │
│  └──────────────────────────────┘  │
│              v utilise              │
│  ┌──────────────────────────────┐  │
│  │         ENGINE               │  │
│  │  Connection Pool             │  │
│  │  Dialect (MySQL, Postgres)   │  │
│  └──────────────────────────────┘  │
└─────────────────────────────────────┘

SQLAlchemy ORM : Haut niveau, orienté objet
SQLAlchemy Core : Bas niveau, SQL fonctionnel
Engine : Gestion des connexions

[IDEE] DANS CE GUIDE : Focus sur ORM (le plus utilisé)
"""


# ----------------------------------------------------------------------------
# [RECHERCHE] QUAND UTILISER SQLALCHEMY ?
# ----------------------------------------------------------------------------

"""
CAS D'USAGE IDÉAUX


1. APPLICATIONS WEB [RAPIDE]
   POURQUOI : Gestion propre des données
   FRAMEWORKS : Flask, Django (via extension), FastAPI
   EXEMPLE : Blog, E-commerce, SaaS
   

2. SCRIPTS D'ANALYSE DE DONNÉES [GRAPHIQUE]
   POURQUOI : Lecture/écriture facilitée
   AVEC : Pandas, NumPy
   EXEMPLE : ETL, Data pipelines
   

3. APIS REST/GRAPHQL [PLUGIN]
   POURQUOI : CRUD automatique
   AVEC : Flask-RESTful, FastAPI
   EXEMPLE : Backend mobile, Microservices
   

4. APPLICATIONS DESKTOP [ECRAN]
   POURQUOI : Base de données locale
   AVEC : Tkinter, PyQt, Kivy
   EXEMPLE : Logiciel de gestion, CRM local
   

5. BOTS ET AUTOMATION [BOT]
   POURQUOI : Persistance des données
   EXEMPLE : Bot Telegram, Web scraper
   

6. JEUX [VIDEO_GAME]
   POURQUOI : Sauvegardes, scores
   EXEMPLE : RPG, jeu multijoueur


QUAND NE PAS UTILISER SQLALCHEMY [X]

1. REQUÊTES TRÈS COMPLEXES
   [X] Requêtes SQL ultra-optimisées
   [X] Procédures stockées
   -> Utiliser SQL brut ou Core
   
2. BASES DE DONNÉES NoSQL
   [X] MongoDB, Redis, Cassandra
   -> Utiliser leurs drivers natifs
   -> Ou ODM spécialisés (MongoEngine)
   
3. PERFORMANCE EXTRÊME
   [X] Millions de requêtes/seconde
   [X] Latence < 1ms critique
   -> Utiliser Core ou SQL brut
   
4. SCRIPTS ULTRA-SIMPLES
   [X] 1-2 requêtes SELECT simples
   -> sqlite3 module suffit


COMPARAISON AVEC ALTERNATIVES
------------------------------

┌─────────────┬────────────┬────────────┬─────────────┐
│             │ SQLALCHEMY │   PEEWEE   │  TORTOISE   │
├─────────────┼────────────┼────────────┼─────────────┤
│ Complexité  │   Moyenne  │   Faible   │   Faible    │
│ Puissance   │   Haute    │   Moyenne  │   Moyenne   │
│ Async       │   Oui      │   Non      │   Natif     │
│ Popularité  │   Très     │   Moyenne  │   Croissante│
│             │   haute    │            │             │
│ Docs        │ Excellentes│   Bonnes   │   Bonnes    │
│ Courbe      │   Raide    │   Douce    │   Douce     │
│ Community   │   Huge     │   Petite   │   Moyenne   │
└─────────────┴────────────┴────────────┴─────────────┘

SQLALCHEMY : Standard de facto, le plus puissant
PEEWEE : Simple, léger, bon pour petits projets
TORTOISE : Moderne, async natif, inspiré de Django ORM


[IDEE] POURQUOI CHOISIR SQLALCHEMY ?

[OK] Standard de l'industrie (employabilité)
[OK] Documentation exhaustive
[OK] Communauté massive
[OK] Supporte tous les SGBD
[OK] Évolue avec vos besoins
[OK] Frameworks l'utilisent (Flask-SQLAlchemy)
"""


# ----------------------------------------------------------------------------
# [CONSTRUCTION] ARCHITECTURE DE SQLALCHEMY
# ----------------------------------------------------------------------------

"""
COMPOSANTS PRINCIPAUX


1. ENGINE (Moteur)
------------------
Gère les connexions à la base de données
"""

from sqlalchemy import create_engine

# Créer engine
engine = create_engine('sqlite:///mydb.db')
# Format : dialect+driver://user:password@host:port/database

"""
[IDEE] QU'EST-CE QU'UN ENGINE ?

- Point d'entrée vers la base de données
- Gère le pool de connexions
- Choisit le dialecte SQL (SQLite, PostgreSQL, etc.)
- NE se connecte PAS immédiatement (lazy)
- Thread-safe


2. DECLARATIVE_BASE (Base déclarative)
--------------------------------------
Classe de base pour tous vos modèles
"""

from sqlalchemy.orm import declarative_base

Base = declarative_base()

# Tous vos modèles héritent de Base
class User(Base):
    __tablename__ = 'users'
    # ...

"""
[IDEE] QU'EST-CE QUE BASE ?

- Factory pour créer des modèles
- Registre de tous vos modèles
- Contient metadata (infos sur tables)
- Base.metadata.create_all(engine) crée toutes les tables


3. SESSION (Session)
--------------------
"Panier d'achats" pour vos opérations de base de données
"""

from sqlalchemy.orm import sessionmaker

Session = sessionmaker(bind=engine)
session = Session()

# Opérations
user = User(name='Alice')
session.add(user)      # Ajouter au panier
session.commit()       # Valider tout le panier
session.close()        # Fermer

"""
[IDEE] QU'EST-CE QU'UNE SESSION ?

- Gère une "conversation" avec la BD
- Track les objets (modifiés, créés, supprimés)
- Transaction : tout ou rien
- Identity map : 1 objet Python = 1 ligne BD


4. MODÈLES (Models)
-------------------
Classes Python représentant des tables
"""

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    name = Column(String)

"""
[IDEE] QU'EST-CE QU'UN MODÈLE ?

- Classe Python héritant de Base
- Attributs = colonnes de table
- Instance = ligne de table
- Méthodes = logique métier


FLUX COMPLET
------------

┌──────────────────────────────────────────────┐
│  1. CONFIGURATION                            │
│     engine = create_engine('sqlite:///db')   │
│     Base = declarative_base()                │
│     Session = sessionmaker(bind=engine)      │
└──────────────────────────────────────────────┘
                    v
┌──────────────────────────────────────────────┐
│  2. DÉFINIR MODÈLES                          │
│     class User(Base):                        │
│         __tablename__ = 'users'              │
│         id = Column(Integer, primary_key=...) │
└──────────────────────────────────────────────┘
                    v
┌──────────────────────────────────────────────┐
│  3. CRÉER TABLES                             │
│     Base.metadata.create_all(engine)         │
└──────────────────────────────────────────────┘
                    v
┌──────────────────────────────────────────────┐
│  4. UTILISER SESSION                         │
│     session = Session()                      │
│     user = User(name='Alice')                │
│     session.add(user)                        │
│     session.commit()                         │
│     session.close()                          │
└──────────────────────────────────────────────┘
"""


# ============================================================================
# [GUIDE] CHAPITRE 1 : INSTALLATION ET CONFIGURATION
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Installer SQLAlchemy
[OK] Choisir un SGBD
[OK] Configurer l'engine
[OK] Structure de projet recommandée
[OK] Connexion à différentes bases de données
"""

# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION
# ----------------------------------------------------------------------------

"""
INSTALLATION DE BASE
"""

# SQLAlchemy seul
pip install sqlalchemy

# Avec support async (optionnel)
pip install sqlalchemy[asyncio]

# Avec PostgreSQL
pip install sqlalchemy psycopg2-binary

# Avec MySQL
pip install sqlalchemy pymysql

# Vérifier installation
python -c "import sqlalchemy; print(sqlalchemy.__version__)"
# Devrait afficher : 2.0.x

"""
[IDEE] VERSIONS SQLALCHEMY

SQLAlchemy 1.4 : Version stable (ancienne)
SQLAlchemy 2.0 : Version moderne (actuelle)

Ce guide utilise SQLAlchemy 2.0+
Syntaxe différente de 1.4 !


DRIVERS PAR SGBD
----------------

SQLite (fichier local) :
    pip install sqlalchemy
    # Pas de driver supplémentaire (inclus dans Python)

PostgreSQL :
    pip install psycopg2-binary
    # ou psycopg2 (nécessite compilation)
    # ou asyncpg pour async

MySQL :
    pip install pymysql
    # ou mysqlclient
    # ou mysql-connector-python

SQL Server :
    pip install pyodbc
    # ou pymssql

Oracle :
    pip install cx_Oracle
"""


# ----------------------------------------------------------------------------
# [OUTIL] CONFIGURATION ENGINE
# ----------------------------------------------------------------------------

"""
CRÉER UN ENGINE
"""

from sqlalchemy import create_engine

# SQLite (fichier)
engine = create_engine('sqlite:///mydb.db')

# SQLite (en mémoire, pour tests)
engine = create_engine('sqlite:///:memory:')

# PostgreSQL
engine = create_engine(
    'postgresql://user:password@localhost:5432/mydatabase'
)

# MySQL
engine = create_engine(
    'mysql+pymysql://user:password@localhost:3306/mydatabase'
)

# SQL Server
engine = create_engine(
    'mssql+pyodbc://user:password@localhost/mydatabase?driver=ODBC+Driver+17+for+SQL+Server'
)

"""
[IDEE] FORMAT DATABASE URL

dialect+driver://username:password@host:port/database

Composants :
- dialect : Type de BD (sqlite, postgresql, mysql)
- driver : Driver Python (psycopg2, pymysql)
- username : Nom d'utilisateur
- password : Mot de passe
- host : Adresse serveur (localhost, IP)
- port : Port (5432 pour PostgreSQL, 3306 pour MySQL)
- database : Nom de la base de données


EXEMPLES DÉTAILLÉS
------------------

SQLite local :
"""
engine = create_engine('sqlite:///app.db')
# Crée fichier app.db dans le dossier courant

engine = create_engine('sqlite:////absolute/path/to/app.db')
# Chemin absolu (note les 4 slashes ////)

"""
PostgreSQL local :
"""
engine = create_engine('postgresql://postgres:secret@localhost/mydb')
# user=postgres, password=secret, db=mydb

"""
PostgreSQL distant :
"""
engine = create_engine('postgresql://user:pass@192.168.1.100:5432/mydb')

"""
MySQL avec options :
"""
engine = create_engine(
    'mysql+pymysql://root:password@localhost/mydb?charset=utf8mb4'
)


"""
OPTIONS DE CREATE_ENGINE
-------------------------
"""

engine = create_engine(
    'sqlite:///app.db',
    
    # Echo SQL (debug)
    echo=True,              # Affiche toutes les requêtes SQL
    
    # Pool de connexions
    pool_size=5,            # Nombre de connexions dans le pool
    max_overflow=10,        # Connexions supplémentaires si besoin
    pool_pre_ping=True,     # Vérifier connexion avant utilisation
    pool_recycle=3600,      # Recycler connexions après 1h
    
    # Options de connexion
    connect_args={
        'timeout': 15,       # Timeout connexion (SQLite)
        'check_same_thread': False  # SQLite multi-thread
    },
    
    # Isolation niveau (PostgreSQL/MySQL)
    # isolation_level='REPEATABLE READ',
    
    # Encodage
    # encoding='utf-8',
)

"""
[IDEE] OPTIONS EXPLIQUÉES

echo=True :
    Affiche SQL généré dans console
    Utile pour debug et apprentissage
    [ATTENTION] Ne pas utiliser en production !

pool_size :
    Nombre de connexions permanentes
    Défaut : 5
    Ajuster selon charge

max_overflow :
    Connexions temporaires supplémentaires
    Total max = pool_size + max_overflow
    
pool_pre_ping :
    Teste connexion avant chaque utilisation
    Évite erreurs "connection lost"
    Léger overhead mais sécurise

pool_recycle :
    Renouvelle connexions après N secondes
    Évite timeouts serveur (MySQL 8h par défaut)
    
connect_args :
    Arguments spécifiques au driver
    SQLite : timeout, check_same_thread
    PostgreSQL : sslmode, connect_timeout
    

TESTER LA CONNEXION
--------------------
"""

from sqlalchemy import create_engine, text

engine = create_engine('sqlite:///test.db', echo=True)

# Tester connexion
try:
    with engine.connect() as connection:
        result = connection.execute(text("SELECT 1"))
        print("Connexion réussie !")
        print(result.fetchone())  # (1,)
except Exception as e:
    print(f"Erreur de connexion : {e}")

"""
[IDEE] CONTEXT MANAGER

with engine.connect() as connection:
    ...

- Ouvre connexion
- Ferme automatiquement après le bloc
- Gère les erreurs proprement
"""


# ----------------------------------------------------------------------------
# [DOSSIER] STRUCTURE DE PROJET
# ----------------------------------------------------------------------------

"""
STRUCTURE SIMPLE (Petit projet)
--------------------------------
"""

my_project/
├── venv/                   # Environnement virtuel
├── models.py               # Modèles SQLAlchemy
├── database.py             # Configuration engine/session
├── main.py                 # Point d'entrée
├── mydb.db                 # Base de données SQLite
└── requirements.txt        # Dépendances

"""
CONTENU DES FICHIERS

database.py
-----------
"""

# database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

# Engine
engine = create_engine('sqlite:///mydb.db', echo=True)

# Base
Base = declarative_base()

# Session factory
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)

# Helper pour obtenir session
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

"""
models.py
---------
"""

# models.py
from sqlalchemy import Column, Integer, String
from database import Base

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    name = Column(String)
    email = Column(String, unique=True)

"""
main.py
-------
"""

# main.py
from database import engine, SessionLocal, Base
from models import User

# Créer tables
Base.metadata.create_all(bind=engine)

# Utiliser
session = SessionLocal()

# Créer user
user = User(name='Alice', email='alice@example.com')
session.add(user)
session.commit()

# Récupérer users
users = session.query(User).all()
for user in users:
    print(user.name)

session.close()

"""
STRUCTURE MOYENNE (Projet Flask/FastAPI)
-----------------------------------------
"""

my_app/
├── venv/
├── app/
│   ├── __init__.py
│   ├── database.py         # Engine, Base, Session
│   ├── models/             # Modèles
│   │   ├── __init__.py
│   │   ├── user.py
│   │   ├── post.py
│   │   └── comment.py
│   ├── crud/               # Opérations DB
│   │   ├── __init__.py
│   │   ├── user.py
│   │   └── post.py
│   └── schemas/            # Pydantic schemas (FastAPI)
│       └── user.py
├── alembic/                # Migrations
│   ├── versions/
│   └── env.py
├── tests/
│   └── test_models.py
├── alembic.ini
├── config.py               # Configuration
└── requirements.txt

"""
STRUCTURE GRANDE (Application complexe)
----------------------------------------
"""

enterprise_app/
├── venv/
├── src/
│   ├── core/
│   │   ├── database.py
│   │   ├── config.py
│   │   └── security.py
│   ├── models/
│   │   ├── base.py         # Base class commune
│   │   ├── user.py
│   │   ├── product.py
│   │   └── order.py
│   ├── repositories/       # Data access layer
│   │   ├── base.py
│   │   ├── user.py
│   │   └── product.py
│   ├── services/           # Business logic
│   │   ├── auth.py
│   │   └── order.py
│   └── api/
│       └── endpoints/
├── migrations/             # Alembic
├── tests/
│   ├── unit/
│   └── integration/
├── scripts/
│   └── init_db.py
└── docker-compose.yml


"""
[IDEE] PRINCIPES D'ORGANISATION

SÉPARATION DES RESPONSABILITÉS :
- database.py : Configuration technique
- models/ : Structure des données
- crud/ ou repositories/ : Accès aux données
- services/ : Logique métier
- api/ : Endpoints HTTP

ÉVOLUTIVITÉ :
- Commencez simple (1 fichier)
- Séparez quand ça grossit
- Gardez une structure cohérente
"""


# ----------------------------------------------------------------------------
# [SECURISE] CONFIGURATION PAR ENVIRONNEMENT
# ----------------------------------------------------------------------------

"""
UTILISER VARIABLES D'ENVIRONNEMENT
"""

import os
from sqlalchemy import create_engine

# Lire depuis environnement
DATABASE_URL = os.getenv(
    'DATABASE_URL',
    'sqlite:///default.db'  # Valeur par défaut
)

engine = create_engine(DATABASE_URL)

"""
FICHIER .env
"""

# .env
DATABASE_URL=postgresql://user:pass@localhost/mydb
DEBUG=True
POOL_SIZE=10

"""
CHARGER AVEC PYTHON-DOTENV
"""

pip install python-dotenv

# config.py
from dotenv import load_dotenv
import os

load_dotenv()  # Charge .env

class Config:
    DATABASE_URL = os.getenv('DATABASE_URL')
    DEBUG = os.getenv('DEBUG', 'False') == 'True'
    POOL_SIZE = int(os.getenv('POOL_SIZE', '5'))

# database.py
from config import Config
from sqlalchemy import create_engine

engine = create_engine(
    Config.DATABASE_URL,
    pool_size=Config.POOL_SIZE,
    echo=Config.DEBUG
)

"""
CONFIGURATION PAR ENVIRONNEMENT
"""

# config.py
import os

class Config:
    """Configuration de base"""
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
class DevelopmentConfig(Config):
    """Configuration développement"""
    DATABASE_URL = 'sqlite:///dev.db'
    DEBUG = True
    ECHO = True

class ProductionConfig(Config):
    """Configuration production"""
    DATABASE_URL = os.getenv('DATABASE_URL')
    DEBUG = False
    ECHO = False
    POOL_SIZE = 20
    POOL_PRE_PING = True

class TestingConfig(Config):
    """Configuration tests"""
    DATABASE_URL = 'sqlite:///:memory:'
    DEBUG = True

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'testing': TestingConfig,
    'default': DevelopmentConfig
}

# Utilisation
ENV = os.getenv('FLASK_ENV', 'development')
current_config = config[ENV]

engine = create_engine(
    current_config.DATABASE_URL,
    echo=current_config.ECHO
)


"""
[ATTENTION] SÉCURITÉ

[X] NE JAMAIS commiter .env dans Git !
[X] NE JAMAIS hardcoder credentials dans le code !

[OK] Utiliser .env pour développement local
[OK] Utiliser variables d'environnement en production
[OK] Ajouter .env à .gitignore
"""

# .gitignore
"""
.env
*.db
__pycache__/
venv/
"""


# ============================================================================
# [GUIDE] CHAPITRE 2 : CRÉER DES MODÈLES (TABLES)
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer une classe modèle
[OK] Définir colonnes
[OK] Comprendre __tablename__
[OK] Méthode __repr__()
[OK] Créer tables en base de données
[OK] Vérifier tables créées
"""

# ----------------------------------------------------------------------------
# [DEMARRAGE] PREMIER MODÈLE
# ----------------------------------------------------------------------------

"""
MODÈLE MINIMAL
"""

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base

# Configuration
Base = declarative_base()

class User(Base):
    """
    [IDEE] MODÈLE USER
    
    Hérite de Base
    Représente table 'user' en base de données
    """
    
    __tablename__ = 'user'
    
    # Colonnes
    id = Column(Integer, primary_key=True)
    name = Column(String)

# Créer engine
engine = create_engine('sqlite:///test.db', echo=True)

# Créer table
Base.metadata.create_all(engine)

"""
[IDEE] DÉCORTIQUONS


LIGNE 1 : from sqlalchemy.orm import declarative_base
---------------------------------------------------------

Importe la fonction pour créer la classe Base


LIGNE 2 : Base = declarative_base()
------------------------------------

Crée la classe Base dont héritent tous vos modèles

[IDEE] QU'EST-CE QUE Base ?

- Factory pour modèles
- Registre de tous vos modèles
- Contient metadata (infos sur tables)
- Une seule Base par application


LIGNE 3 : class User(Base):
----------------------------

Crée classe User héritant de Base

[IDEE] CONVENTIONS :
- Nom de classe : Singulier, PascalCase (User, BlogPost)
- Nom de table : Pluriel, snake_case (users, blog_posts)


LIGNE 4 : __tablename__ = 'user'
---------------------------------

Définit le nom de la table SQL

[IDEE] SI OMIS :
SQLAlchemy utilise nom de classe en minuscules
class User -> table 'user'
class BlogPost -> table 'blogpost' (pas idéal !)

[OK] BONNE PRATIQUE : Toujours spécifier __tablename__


LIGNE 5 : id = Column(Integer, primary_key=True)
-------------------------------------------------

Définit colonne 'id' de type Integer, clé primaire

[IDEE] QU'EST-CE QU'UNE PRIMARY KEY ?

- Identifiant unique de chaque ligne
- Auto-incrémenté (1, 2, 3, ...)
- Ne peut être NULL
- Index automatique (recherche rapide)

[OK] Chaque table doit avoir une primary key


LIGNE 6 : name = Column(String)
--------------------------------

Définit colonne 'name' de type String

[IDEE] String sans longueur :
SQLite : OK (pas de limite)
PostgreSQL/MySQL : Défaut (généralement 255 ou illimité)

[OK] BONNE PRATIQUE : Spécifier longueur
name = Column(String(50))


LIGNE 7 : Base.metadata.create_all(engine)
-------------------------------------------

Crée TOUTES les tables définies dans Base

[IDEE] QUE FAIT-ELLE ?

1. Parcourt tous les modèles héritant de Base
2. Génère SQL CREATE TABLE
3. Exécute sur l'engine
4. Si table existe déjà -> Ne fait rien (pas d'erreur)

[ATTENTION] NE MODIFIE PAS les tables existantes
Pour ça : Utiliser Alembic (Partie 4)
"""


# ----------------------------------------------------------------------------
# [NOTE] MODÈLE COMPLET
# ----------------------------------------------------------------------------

"""
MODÈLE AVEC TOUTES LES OPTIONS
"""

from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text
from sqlalchemy.orm import declarative_base
from datetime import datetime

Base = declarative_base()

class User(Base):
    """
    Modèle User complet avec toutes les bonnes pratiques
    """
    
    # Nom de la table
    __tablename__ = 'users'
    
    # Colonnes
    id = Column(Integer, primary_key=True, autoincrement=True)
    username = Column(String(50), unique=True, nullable=False, index=True)
    email = Column(String(120), unique=True, nullable=False)
    password_hash = Column(String(128), nullable=False)
    is_active = Column(Boolean, default=True, nullable=False)
    is_admin = Column(Boolean, default=False, nullable=False)
    bio = Column(Text, nullable=True)
    created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def __repr__(self):
        """
        [IDEE] REPRÉSENTATION STRING
        
        Appelée par print(user) ou repr(user)
        Utile pour debug
        """
        return f"<User(id={self.id}, username='{self.username}')>"
    
    def __str__(self):
        """
        Représentation humaine
        Appelée par str(user)
        """
        return self.username

"""
[IDEE] OPTIONS DE COLONNES EXPLIQUÉES


autoincrement=True (défaut pour Integer primary_key)
----------------------------------------------------
Auto-incrémente l'ID : 1, 2, 3, ...

user1 = User(username='alice')  # id sera 1
user2 = User(username='bob')    # id sera 2


unique=True
-----------
Valeur doit être unique dans toute la table

username = Column(String(50), unique=True)

user1 = User(username='alice')  # OK
user2 = User(username='alice')  # [X] Erreur IntegrityError


nullable=False
--------------
Colonne obligatoire (NOT NULL en SQL)

email = Column(String(120), nullable=False)

user = User(username='alice')  # [X] Erreur : email manquant


default=value
-------------
Valeur par défaut si non fournie

is_active = Column(Boolean, default=True)

user = User(username='alice')
print(user.is_active)  # True (défaut appliqué)


default=fonction
----------------
Fonction appelée pour chaque nouvelle ligne

created_at = Column(DateTime, default=datetime.utcnow)

[ATTENTION] datetime.utcnow (SANS parenthèses !)
[OK] datetime.utcnow   -> Fonction (appelée à chaque insertion)
[X] datetime.utcnow() -> Valeur fixe (date de définition du modèle)


onupdate=fonction
-----------------
Fonction appelée à chaque UPDATE

updated_at = Column(DateTime, onupdate=datetime.utcnow)

user.username = 'alice2'
session.commit()
# updated_at mis à jour automatiquement


index=True
----------
Crée un index sur la colonne
Accélère les recherches

username = Column(String(50), index=True)

# Recherche rapide
User.query.filter_by(username='alice').first()


server_default
--------------
Valeur par défaut côté serveur SQL (pas Python)

created_at = Column(DateTime, server_default='CURRENT_TIMESTAMP')

Différence avec default :
- default : Valeur calculée en Python avant INSERT
- server_default : Valeur calculée par le serveur SQL
"""


# ----------------------------------------------------------------------------
# [DESIGN] MÉTHODES SPÉCIALES
# ----------------------------------------------------------------------------

"""
__repr__() : REPRÉSENTATION TECHNIQUE
"""

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    
    def __repr__(self):
        return f"<User(id={self.id}, username='{self.username}')>"

# Utilisation
user = User(id=1, username='alice')
print(repr(user))  # <User(id=1, username='alice')>
print(user)        # <User(id=1, username='alice')> (appelle __repr__)

"""
[IDEE] BONNES PRATIQUES __repr__()

[OK] Concis mais informatif
[OK] Format : <ClassName(attr=value, ...)>
[OK] Inclure primary key et 1-2 attributs importants
[X] Ne pas inclure tous les attributs (trop long)


__str__() : REPRÉSENTATION HUMAINE
"""

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    
    def __repr__(self):
        return f"<User(id={self.id}, username='{self.username}')>"
    
    def __str__(self):
        return self.username

# Utilisation
user = User(id=1, username='alice')
print(str(user))   # alice
print(f"User: {user}")  # User: alice

"""
[IDEE] DIFFÉRENCE __repr__() vs __str__()

__repr__() :
- Représentation technique
- Pour développeurs
- Utilisée par print() si __str__() absent
- Devrait être "évaluable" : eval(repr(obj)) == obj

__str__() :
- Représentation humaine
- Pour utilisateurs finaux
- Utilisée par str() et f-strings
- Peut être n'importe quoi


MÉTHODES PERSONNALISÉES
"""

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    email = Column(String(120))
    is_active = Column(Boolean, default=True)
    
    def activate(self):
        """Activer l'utilisateur"""
        self.is_active = True
    
    def deactivate(self):
        """Désactiver l'utilisateur"""
        self.is_active = False
    
    def to_dict(self):
        """Convertir en dictionnaire (pour JSON API)"""
        return {
            'id': self.id,
            'username': self.username,
            'email': self.email,
            'is_active': self.is_active
        }

# Utilisation
user = User(username='alice', email='alice@example.com')
user.deactivate()
print(user.is_active)  # False

user_dict = user.to_dict()
# {'id': 1, 'username': 'alice', ...}


# ----------------------------------------------------------------------------
# [CONSTRUCTION] CRÉER LES TABLES
# ----------------------------------------------------------------------------

"""
MÉTHODE 1 : create_all() (Simple)
"""

from sqlalchemy import create_engine
from models import Base, User

engine = create_engine('sqlite:///mydb.db')

# Créer TOUTES les tables
Base.metadata.create_all(engine)

"""
[IDEE] QUE FAIT create_all() ?

1. Parcourt tous les modèles (User, Post, Comment, ...)
2. Génère SQL CREATE TABLE pour chacun
3. Exécute sur l'engine
4. Si table existe -> Ne fait rien
5. Si table n'existe pas -> Crée

[ATTENTION] LIMITATIONS :
- Ne modifie PAS tables existantes
- Ne détecte PAS les changements de schéma
- Ne supprime PAS colonnes devenues inutiles

-> Pour évolution du schéma : Alembic (Partie 4)


MÉTHODE 2 : drop_all() puis create_all() (Tests)
"""

# [ATTENTION] SUPPRIME TOUTES LES DONNÉES !
Base.metadata.drop_all(engine)   # Supprime toutes les tables
Base.metadata.create_all(engine) # Recrée toutes les tables

"""
[ATTENTION] DANGEREUX EN PRODUCTION !
Utiliser seulement pour :
- Tests automatisés
- Réinitialisation environnement développement


MÉTHODE 3 : Créer table spécifique
"""

# Créer seulement table User
User.__table__.create(engine, checkfirst=True)

# checkfirst=True : Ne pas créer si existe déjà


"""
VÉRIFIER LES TABLES CRÉÉES
---------------------------

SQLite :
"""

import sqlite3

conn = sqlite3.connect('mydb.db')
cursor = conn.cursor()

# Lister tables
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = cursor.fetchall()
print(tables)  # [('users',), ('posts',), ...]

# Schéma d'une table
cursor.execute("PRAGMA table_info(users);")
columns = cursor.fetchall()
for col in columns:
    print(col)
# (0, 'id', 'INTEGER', 1, None, 1)  # cid, name, type, notnull, dflt_value, pk
# (1, 'username', 'VARCHAR(50)', 1, None, 0)
# ...

conn.close()

"""
PostgreSQL :
"""

from sqlalchemy import create_engine, text

engine = create_engine('postgresql://user:pass@localhost/mydb')

with engine.connect() as conn:
    # Lister tables
    result = conn.execute(text("""
        SELECT table_name 
        FROM information_schema.tables 
        WHERE table_schema='public'
    """))
    for row in result:
        print(row[0])

"""
Avec Inspector (tous SGBD) :
"""

from sqlalchemy import create_engine, inspect

engine = create_engine('sqlite:///mydb.db')
inspector = inspect(engine)

# Lister tables
tables = inspector.get_table_names()
print(tables)  # ['users', 'posts', 'comments']

# Colonnes d'une table
columns = inspector.get_columns('users')
for col in columns:
    print(f"{col['name']} - {col['type']} - Nullable: {col['nullable']}")

# Index
indexes = inspector.get_indexes('users')
for idx in indexes:
    print(f"Index: {idx['name']} on {idx['column_names']}")


# ============================================================================
# CE FICHIER CONTINUE...
# POUR NE PAS DÉPASSER LA LIMITE, LA SUITE DANS LE PROCHAIN FICHIER
# ============================================================================

"""
[DOCS] RÉCAPITULATIF CHAPITRE 2

[OK] Modèle = Classe Python héritant de Base
[OK] __tablename__ définit nom de table
[OK] Column() définit colonnes
[OK] Options : unique, nullable, default, index
[OK] __repr__() pour représentation debug
[OK] Base.metadata.create_all() crée tables


-> PROCHAINE ÉTAPE : Chapitre 3 - Types de Colonnes

Vous allez apprendre :
- Tous les types de colonnes (Integer, String, DateTime, etc.)
- Quand utiliser chaque type
- Options avancées
- Types personnalisés

[RAPIDE] Continuez !
"""
# ============================================================================
# [LIVRE] SQLALCHEMY - PARTIE 1 (SUITE) : TYPES ET CONTRAINTES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 3 : Types de Colonnes (Integer, String, DateTime, etc.)
# - Chapitre 4 : Contraintes et Options Avancées
#
# [TEMPS] TEMPS : ~3-4 heures
# [DOCS] PRÉREQUIS : Chapitres 0-2 complétés
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 3 : TYPES DE COLONNES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Tous les types de colonnes SQLAlchemy
[OK] Quand utiliser chaque type
[OK] Différences entre SGBD
[OK] Types personnalisés
[OK] Validation des données
"""

# ----------------------------------------------------------------------------
# [NOMBRE] TYPES NUMÉRIQUES
# ----------------------------------------------------------------------------

"""
INTEGER - ENTIERS
"""

from sqlalchemy import Column, Integer, SmallInteger, BigInteger

class Product(Base):
    __tablename__ = 'products'
    
    # Integer standard (32-bit signé : -2,147,483,648 à 2,147,483,647)
    id = Column(Integer, primary_key=True)
    stock = Column(Integer, default=0)
    
    # SmallInteger (16-bit : -32,768 à 32,767)
    priority = Column(SmallInteger, default=1)
    
    # BigInteger (64-bit : énorme)
    views = Column(BigInteger, default=0)

"""
[IDEE] QUAND UTILISER CHAQUE TYPE ?

Integer (défaut) :
    Utilisation : IDs, compteurs, âge, quantités
    Plage : -2 milliards à +2 milliards
    Stockage : 4 bytes
    Exemple : id, stock, age, quantity

SmallInteger :
    Utilisation : Petits nombres (notes, priorités)
    Plage : -32,768 à 32,767
    Stockage : 2 bytes (économie espace)
    Exemple : priority (1-5), rating (1-10)

BigInteger :
    Utilisation : Très grands nombres
    Plage : -9 quintillions à +9 quintillions
    Stockage : 8 bytes
    Exemple : views, likes, timestamps (millisecondes)


[ATTENTION] DIFFÉRENCES SGBD

SQLite :
    Integer, SmallInteger, BigInteger -> Tous stockés comme INTEGER
    Pas de limite stricte (dynamique)

PostgreSQL :
    INTEGER (4 bytes), SMALLINT (2 bytes), BIGINT (8 bytes)
    Limites strictes

MySQL :
    INT, SMALLINT, BIGINT
    Support UNSIGNED (seulement positifs)
"""


"""
FLOAT - NOMBRES DÉCIMAUX APPROXIMATIFS
"""

from sqlalchemy import Float

class Measurement(Base):
    __tablename__ = 'measurements'
    
    id = Column(Integer, primary_key=True)
    
    # Float standard (approximatif)
    latitude = Column(Float)
    longitude = Column(Float)
    temperature = Column(Float)

"""
[IDEE] FLOAT EXPLIQUÉ

Caractéristiques :
- Stocke nombres décimaux
- APPROXIMATIF (pas exact !)
- Utilise virgule flottante IEEE 754

Utilisation :
[OK] Coordonnées GPS
[OK] Mesures scientifiques
[OK] Valeurs où précision absolue pas critique

[X] NE PAS utiliser pour :
- Argent (utiliser Numeric/Decimal)
- Calculs financiers précis

Exemple problème :
"""
# Python
price = 0.1 + 0.2
print(price)  # 0.30000000000000004 (pas 0.3 !)

"""
NUMERIC / DECIMAL - NOMBRES DÉCIMAUX EXACTS
"""

from sqlalchemy import Numeric

class Product(Base):
    __tablename__ = 'products'
    
    id = Column(Integer, primary_key=True)
    
    # Numeric(precision, scale)
    # precision = nombre total de chiffres
    # scale = chiffres après la virgule
    
    price = Column(Numeric(10, 2))  # 99999999.99 max
    discount = Column(Numeric(5, 2))  # 999.99 max
    tax_rate = Column(Numeric(5, 4))  # 9.9999 max

"""
[IDEE] NUMERIC EXPLIQUÉ

Numeric(10, 2) :
    10 = Total de chiffres (precision)
    2 = Chiffres après virgule (scale)
    
    Plage : -99,999,999.99 à 99,999,999.99
    Exemples valides : 1234.56, 0.99, 99999999.99
    Exemples invalides : 123456789.99 (trop de chiffres)

Utilisation :
[OK] Prix, argent
[OK] Pourcentages précis
[OK] Taux (taxes, intérêts)
[OK] Tout calcul financier

Stockage :
- Exact (pas d'approximation)
- Calculs précis garantis

Exemple :
"""
product = Product(price=Decimal('19.99'))
tax = product.price * Decimal('0.2')
total = product.price + tax
# Résultat exact : 23.99 (pas 23.989999999)

"""
[IDEE] FLOAT vs NUMERIC

┌────────────┬────────────┬──────────────┐
│            │   FLOAT    │   NUMERIC    │
├────────────┼────────────┼──────────────┤
│ Précision  │ Approximatif│    Exact     │
│ Vitesse    │   Rapide   │   Lent       │
│ Stockage   │  Compact   │   Plus gros  │
│ Utilisation│  Science   │   Finance    │
│ Argent     │     [X]      │      [OK]       │
│ GPS        │     [OK]      │      [X]       │
└────────────┴────────────┴──────────────┘
"""


# ----------------------------------------------------------------------------
# [NOTE] TYPES TEXTE
# ----------------------------------------------------------------------------

"""
STRING - CHAÎNES DE CARACTÈRES
"""

from sqlalchemy import String

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    
    # String avec longueur (VARCHAR en SQL)
    username = Column(String(50))
    email = Column(String(120))
    
    # String sans longueur
    # [ATTENTION] Comportement dépend du SGBD
    code = Column(String)

"""
[IDEE] STRING EXPLIQUÉ

String(length) :
    Génère : VARCHAR(length) en SQL
    Stockage : Variable (seulement ce qui est utilisé)
    
    String(50) :
        'alice' -> 5 caractères stockés
        'bob' -> 3 caractères stockés
        Max : 50 caractères

String() sans longueur :
    SQLite : TEXT (illimité)
    PostgreSQL : TEXT (illimité)
    MySQL : VARCHAR(255) par défaut
    
[OK] BONNE PRATIQUE : Toujours spécifier longueur
Pourquoi ?
- Documentation (limite claire)
- Validation automatique
- Portable entre SGBD
- Index plus efficaces


LONGUEURS RECOMMANDÉES :

username : String(50)        # 3-50 caractères
email : String(120)          # Standard email
phone : String(20)           # Numéros internationaux
zipcode : String(10)         # Codes postaux
country_code : String(2)     # ISO codes (FR, US)
title : String(200)          # Titres, sujets
slug : String(100)           # URL-friendly
"""


"""
TEXT - TEXTE LONG
"""

from sqlalchemy import Text

class BlogPost(Base):
    __tablename__ = 'blog_posts'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    
    # Text pour contenu long
    content = Column(Text)
    
    # Text avec sous-types (PostgreSQL)
    # short_text = Column(Text)  # Jusqu'à 65,535 caractères
    # long_text = Column(Text)   # Illimité

"""
[IDEE] TEXT EXPLIQUÉ

Caractéristiques :
- Texte illimité (ou très long)
- Pas de limite de caractères
- Stockage efficace pour long texte

Utilisation :
[OK] Articles de blog
[OK] Descriptions longues
[OK] Commentaires
[OK] Contenu HTML
[OK] JSON (si pas de type JSON natif)

[X] NE PAS utiliser pour :
- Texte court (utiliser String)
- Données structurées (utiliser JSON)

Différences SGBD :

SQLite :
    TEXT (illimité)

PostgreSQL :
    TEXT (illimité)
    Variantes : VARCHAR, CHAR (compatibilité)

MySQL :
    TEXT (65,535 caractères)
    MEDIUMTEXT (16 millions)
    LONGTEXT (4 GB)


[IDEE] STRING vs TEXT

┌───────────┬──────────────┬──────────────┐
│           │    STRING    │     TEXT     │
├───────────┼──────────────┼──────────────┤
│ Longueur  │   Limitée    │   Illimité   │
│ Index     │   Rapide     │   Lent/Non   │
│ Recherche │   Rapide     │   Lent       │
│ Usage     │  Court texte │  Long texte  │
│ Exemple   │  Username    │  Article     │
└───────────┴──────────────┴──────────────┘

Règle simple :
- < 255 caractères -> String(length)
- > 255 caractères -> Text
"""


# ----------------------------------------------------------------------------
# [CALENDRIER] TYPES DATE ET HEURE
# ----------------------------------------------------------------------------

"""
DATETIME - DATE + HEURE
"""

from sqlalchemy import DateTime
from datetime import datetime

class Event(Base):
    __tablename__ = 'events'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    
    # DateTime standard
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, onupdate=datetime.utcnow)
    
    # DateTime avec timezone (PostgreSQL)
    # scheduled_at = Column(DateTime(timezone=True))

"""
[IDEE] DATETIME EXPLIQUÉ

Stocke : Date + Heure
Format : YYYY-MM-DD HH:MM:SS
Exemple : 2024-01-18 14:30:00

Utilisation :
[OK] created_at, updated_at
[OK] Timestamps d'événements
[OK] Logs
[OK] Rendez-vous

Options :

timezone=False (défaut) :
    Stocke SANS timezone
    DateTime "naïf"
    Format : 2024-01-18 14:30:00

timezone=True (PostgreSQL) :
    Stocke AVEC timezone
    DateTime "aware"
    Format : 2024-01-18 14:30:00+00:00


[ATTENTION] TIMEZONES - IMPORTANT !

Problème :
"""
# Créer événement
event = Event(
    name='Meeting',
    scheduled_at=datetime.now()  # [X] Timezone locale !
)

# Utilisateur en France : 14:00
# Utilisateur aux USA : 08:00 (même heure stockée !)

"""
[OK] SOLUTION : Toujours utiliser UTC
"""
from datetime import datetime, timezone

# Stocker en UTC
event = Event(
    name='Meeting',
    scheduled_at=datetime.now(timezone.utc)
)

# Ou
event.scheduled_at = datetime.utcnow()

# Afficher dans timezone utilisateur (frontend)
# user_time = event.scheduled_at.replace(tzinfo=timezone.utc)
# local_time = user_time.astimezone(user_timezone)

"""
[IDEE] BEST PRACTICES DATETIME

1. TOUJOURS stocker en UTC
2. Convertir timezone côté client/frontend
3. Utiliser datetime.utcnow() pour default
4. onupdate pour updated_at automatique


DEFAULT ET ONUPDATE
"""

class Post(Base):
    __tablename__ = 'posts'
    id = Column(Integer, primary_key=True)
    
    # Défini à la création
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # Mis à jour automatiquement
    updated_at = Column(DateTime, 
                       default=datetime.utcnow,
                       onupdate=datetime.utcnow)

# Utilisation
post = Post(title='Hello')
session.add(post)
session.commit()
print(post.created_at)  # 2024-01-18 14:30:00

# Plus tard...
post.title = 'Hello World'
session.commit()
print(post.updated_at)  # 2024-01-18 15:45:00 (mis à jour !)

"""
[ATTENTION] PIÈGE COMMUN : Parenthèses !
"""
# [X] MAUVAIS
created_at = Column(DateTime, default=datetime.utcnow())
# Évalue datetime.utcnow() UNE FOIS au démarrage
# Toutes les lignes auront la même date !

# [OK] BON
created_at = Column(DateTime, default=datetime.utcnow)
# Passe la FONCTION (sans ())
# Appelée à chaque INSERT


"""
DATE - DATE SEULEMENT
"""

from sqlalchemy import Date
from datetime import date

class Person(Base):
    __tablename__ = 'persons'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    
    # Date sans heure
    birth_date = Column(Date)
    hire_date = Column(Date, default=date.today)

"""
[IDEE] DATE EXPLIQUÉ

Stocke : Date seulement (pas d'heure)
Format : YYYY-MM-DD
Exemple : 2024-01-18

Utilisation :
[OK] Dates de naissance
[OK] Dates d'embauche
[OK] Deadlines (jour seulement)
[OK] Anniversaires

Manipulation :
"""
person = Person(name='Alice', birth_date=date(1990, 5, 15))

# Calculer âge
from datetime import date

today = date.today()
age = today.year - person.birth_date.year
if today.month < person.birth_date.month:
    age -= 1

print(f"{person.name} a {age} ans")


"""
TIME - HEURE SEULEMENT
"""

from sqlalchemy import Time
from datetime import time

class Schedule(Base):
    __tablename__ = 'schedules'
    
    id = Column(Integer, primary_key=True)
    
    # Heure sans date
    start_time = Column(Time)
    end_time = Column(Time)

"""
[IDEE] TIME EXPLIQUÉ

Stocke : Heure seulement (pas de date)
Format : HH:MM:SS
Exemple : 14:30:00

Utilisation :
[OK] Horaires d'ouverture
[OK] Planning récurrent
[OK] Alarmes

Exemple :
"""
schedule = Schedule(
    start_time=time(9, 0),    # 09:00
    end_time=time(17, 30)     # 17:30
)


"""
[IDEE] DATE vs DATETIME vs TIME

┌─────────┬──────────────┬────────────────┐
│  Type   │    Stocke    │    Exemple     │
├─────────┼──────────────┼────────────────┤
│DateTime │ Date + Heure │ 2024-01-18     │
│         │              │ 14:30:00       │
├─────────┼──────────────┼────────────────┤
│  Date   │ Date seule   │ 2024-01-18     │
├─────────┼──────────────┼────────────────┤
│  Time   │ Heure seule  │ 14:30:00       │
└─────────┴──────────────┴────────────────┘

Choisir :
- Timestamp complet -> DateTime
- Jour seulement -> Date
- Heure récurrente -> Time
"""


# ----------------------------------------------------------------------------
# [OK] TYPES BOOLÉENS
# ----------------------------------------------------------------------------

"""
BOOLEAN - VRAI/FAUX
"""

from sqlalchemy import Boolean

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    
    # Boolean avec défaut
    is_active = Column(Boolean, default=True)
    is_admin = Column(Boolean, default=False)
    email_verified = Column(Boolean, default=False, nullable=False)

"""
[IDEE] BOOLEAN EXPLIQUÉ

Valeurs : True ou False (Python)
Stockage SQL : Dépend du SGBD

SQLite :
    INTEGER (0 = False, 1 = True)

PostgreSQL :
    BOOLEAN natif (true/false)

MySQL :
    TINYINT(1) (0 = False, 1 = True)

Utilisation :
[OK] Flags (actif/inactif)
[OK] Permissions
[OK] États (vérifié/non vérifié)
[OK] Options (activé/désactivé)


Manipulation :
"""
user = User(username='alice')
print(user.is_active)  # True (défaut)

# Changer valeur
user.is_active = False
user.email_verified = True

# Utiliser dans requêtes
active_users = session.query(User).filter(User.is_active == True).all()
admins = session.query(User).filter_by(is_admin=True).all()


"""
[ATTENTION] NULLABLE AVEC BOOLEAN

Boolean nullable=True :
    Valeurs possibles : True, False, None
    3 états : Oui, Non, Inconnu

Boolean nullable=False :
    Valeurs possibles : True, False
    2 états : Oui, Non
"""

class Product(Base):
    __tablename__ = 'products'
    id = Column(Integer, primary_key=True)
    
    # 2 états (recommandé)
    in_stock = Column(Boolean, default=False, nullable=False)
    
    # 3 états (rare)
    verified = Column(Boolean, nullable=True)
    # None = pas encore vérifié
    # True = vérifié OK
    # False = vérifié KO


# ----------------------------------------------------------------------------
# [DOSSIER] TYPES AVANCÉS
# ----------------------------------------------------------------------------

"""
JSON - DONNÉES JSON
"""

from sqlalchemy import JSON

class Config(Base):
    __tablename__ = 'configs'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    
    # Colonne JSON
    settings = Column(JSON)

"""
[IDEE] JSON EXPLIQUÉ

Stocke : Objets JSON (dict, list)
Support : PostgreSQL (natif), MySQL 5.7+, SQLite 3.9+

Utilisation :
[OK] Paramètres flexibles
[OK] Métadonnées
[OK] Données semi-structurées
[OK] Configurations

Exemple :
"""
config = Config(
    name='app_config',
    settings={
        'theme': 'dark',
        'language': 'fr',
        'notifications': {
            'email': True,
            'sms': False
        },
        'features': ['chat', 'video', 'files']
    }
)

session.add(config)
session.commit()

# Accès
print(config.settings['theme'])  # 'dark'
print(config.settings['notifications']['email'])  # True

# Modification
config.settings['theme'] = 'light'
session.commit()

"""
[ATTENTION] MODIFICATION EN PLACE

Problème :
"""
config.settings['theme'] = 'light'  # Modifie dict Python
session.commit()  # [X] Peut ne pas détecter le changement !

"""
Solution 1 : Flag modified
"""
from sqlalchemy.orm.attributes import flag_modified

config.settings['theme'] = 'light'
flag_modified(config, 'settings')
session.commit()  # [OK] Détecte changement

"""
Solution 2 : Réassigner
"""
new_settings = config.settings.copy()
new_settings['theme'] = 'light'
config.settings = new_settings
session.commit()  # [OK] Détecte changement


"""
REQUÊTES JSON (PostgreSQL)
"""

# Chercher par clé JSON
users = session.query(User).filter(
    User.settings['theme'].astext == 'dark'
).all()

# Vérifier existence clé
users = session.query(User).filter(
    User.settings.has_key('notifications')
).all()


"""
ENUM - ÉNUMÉRATION
"""

from sqlalchemy import Enum
import enum

# Définir enum Python
class UserRole(enum.Enum):
    USER = 'user'
    ADMIN = 'admin'
    MODERATOR = 'moderator'

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    
    # Colonne Enum
    role = Column(Enum(UserRole), default=UserRole.USER)

"""
[IDEE] ENUM EXPLIQUÉ

Limite valeurs possibles à une liste prédéfinie

Avantages :
[OK] Validation automatique
[OK] Autocomplétion IDE
[OK] Type-safe
[OK] Documentation claire

Utilisation :
"""
user = User(username='alice', role=UserRole.ADMIN)

# Accès
if user.role == UserRole.ADMIN:
    print("Admin access")

# Erreur si valeur invalide
# user.role = 'superuser'  # [X] ValueError

"""
ENUM String (sans classe Python)
"""
status = Column(Enum('pending', 'approved', 'rejected'), default='pending')

# Simple mais moins type-safe


"""
ARRAY - TABLEAUX (PostgreSQL)
"""

from sqlalchemy import ARRAY

class Article(Base):
    __tablename__ = 'articles'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    
    # Array de strings
    tags = Column(ARRAY(String))
    
    # Array de integers
    related_ids = Column(ARRAY(Integer))

"""
[IDEE] ARRAY EXPLIQUÉ

Support : PostgreSQL uniquement
Stocke : Liste de valeurs du même type

Utilisation :
"""
article = Article(
    title='Python Tutorial',
    tags=['python', 'tutorial', 'beginner'],
    related_ids=[1, 5, 10]
)

# Accès
print(article.tags[0])  # 'python'

# Requêtes
articles = session.query(Article).filter(
    Article.tags.contains(['python'])
).all()


"""
INTERVAL - DURÉE (PostgreSQL)
"""

from sqlalchemy import Interval
from datetime import timedelta

class Task(Base):
    __tablename__ = 'tasks'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    
    # Durée
    duration = Column(Interval)

"""
Utilisation :
"""
task = Task(
    name='Coding',
    duration=timedelta(hours=2, minutes=30)
)

print(task.duration.total_seconds())  # 9000.0


"""
UUID - IDENTIFIANT UNIVERSEL
"""

from sqlalchemy import UUID
import uuid

class Resource(Base):
    __tablename__ = 'resources'
    
    # UUID au lieu d'Integer
    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    name = Column(String(100))

"""
[IDEE] UUID EXPLIQUÉ

UUID = Universally Unique Identifier
Format : 550e8400-e29b-41d4-a716-446655440000

Avantages vs Integer :
[OK] Globalement unique (sans coordination)
[OK] Impossible à deviner
[OK] Sécurisé (pas d'énumération)
[OK] Distribué (plusieurs serveurs)

Inconvénients :
[X] Plus long (16 bytes vs 4)
[X] Pas lisible
[X] Index moins performants

Utilisation :
[OK] APIs publiques
[OK] Systèmes distribués
[OK] Sécurité importante
"""


# ============================================================================
# [GUIDE] CHAPITRE 4 : CONTRAINTES ET OPTIONS AVANCÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Toutes les contraintes SQLAlchemy
[OK] Contraintes de table
[OK] Index composites
[OK] Valeurs calculées
[OK] Options de colonnes avancées
"""

# ----------------------------------------------------------------------------
# [VERROUILLE] CONTRAINTES DE COLONNES
# ----------------------------------------------------------------------------

"""
PRIMARY KEY - CLÉ PRIMAIRE
"""

class User(Base):
    __tablename__ = 'users'
    
    # Primary key simple
    id = Column(Integer, primary_key=True, autoincrement=True)
    
    # OU clé primaire composite
    # user_id = Column(Integer, primary_key=True)
    # email = Column(String, primary_key=True)

"""
[IDEE] PRIMARY KEY EXPLIQUÉE

Contraintes :
- Unique (pas de doublons)
- Not NULL
- Index automatique
- Une seule par table (ou composite)

autoincrement=True (défaut pour Integer PK) :
- Auto-incrémente : 1, 2, 3, ...
- Géré par la base de données


PRIMARY KEY COMPOSITE
"""

class Enrollment(Base):
    __tablename__ = 'enrollments'
    
    # Deux colonnes = clé primaire
    student_id = Column(Integer, primary_key=True)
    course_id = Column(Integer, primary_key=True)
    
    enrolled_at = Column(DateTime, default=datetime.utcnow)

"""
Chaque combinaison (student_id, course_id) unique
student_id=1, course_id=1 [OK]
student_id=1, course_id=2 [OK]
student_id=1, course_id=1 [X] (doublon)
"""


"""
UNIQUE - UNICITÉ
"""

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    
    # Colonne unique
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(120), unique=True, nullable=False)

"""
[IDEE] UNIQUE EXPLIQUÉ

Contrainte :
- Valeur doit être unique dans la table
- NULL autorisé (sauf si nullable=False)
- Index automatique (recherche rapide)

Erreur si doublon :
"""
user1 = User(username='alice', email='alice@example.com')
session.add(user1)
session.commit()  # [OK] OK

user2 = User(username='alice', email='alice2@example.com')
session.add(user2)
session.commit()  # [X] IntegrityError: UNIQUE constraint failed


"""
UNIQUE COMPOSITE
"""

from sqlalchemy import UniqueConstraint

class Friendship(Base):
    __tablename__ = 'friendships'
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer)
    friend_id = Column(Integer)
    
    # Contrainte unique sur 2 colonnes
    __table_args__ = (
        UniqueConstraint('user_id', 'friend_id', name='unique_friendship'),
    )

"""
Chaque paire (user_id, friend_id) unique
user_id=1, friend_id=2 [OK]
user_id=1, friend_id=3 [OK]
user_id=1, friend_id=2 [X] (doublon)
"""


"""
NULLABLE - NULL AUTORISÉ
"""

class Product(Base):
    __tablename__ = 'products'
    
    id = Column(Integer, primary_key=True)
    
    # Obligatoire (NOT NULL)
    name = Column(String(100), nullable=False)
    price = Column(Numeric(10, 2), nullable=False)
    
    # Optionnel (NULL OK)
    description = Column(Text, nullable=True)  # défaut
    discount = Column(Numeric(5, 2))  # nullable=True implicite

"""
[IDEE] NULLABLE EXPLIQUÉ

nullable=False :
- Valeur OBLIGATOIRE
- NULL interdit
- Erreur si omis

nullable=True (défaut) :
- Valeur optionnelle
- NULL autorisé


Erreur si manquant :
"""
product = Product(price=19.99)  # [X] name manquant
session.add(product)
session.commit()  # IntegrityError: NOT NULL constraint failed

"""
CHECK - CONTRAINTES PERSONNALISÉES
"""

from sqlalchemy import CheckConstraint

class Product(Base):
    __tablename__ = 'products'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    price = Column(Numeric(10, 2))
    discount = Column(Numeric(5, 2))
    age_rating = Column(Integer)
    
    __table_args__ = (
        # Prix positif
        CheckConstraint('price > 0', name='check_price_positive'),
        
        # Discount entre 0 et 100
        CheckConstraint('discount >= 0 AND discount <= 100', 
                       name='check_discount_range'),
        
        # Age rating valide
        CheckConstraint('age_rating IN (0, 7, 12, 16, 18)', 
                       name='check_age_rating'),
    )

"""
[IDEE] CHECK EXPLIQUÉ

Validation côté base de données
Expression SQL qui doit être vraie

Exemples :
"""
# [OK] OK
product = Product(name='Book', price=29.99, discount=10)

# [X] Erreur
product = Product(name='Book', price=-5)  # price <= 0
product = Product(name='Book', price=29.99, discount=150)  # discount > 100


# ----------------------------------------------------------------------------
# [BOOKMARK_TABS] INDEX
# ----------------------------------------------------------------------------

"""
INDEX SIMPLE
"""

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)  # Index auto
    
    # Index sur colonne unique
    username = Column(String(50), index=True, unique=True)
    
    # Index sur colonne non-unique
    email = Column(String(120), index=True)
    
    # Sans index
    bio = Column(Text)

"""
[IDEE] INDEX EXPLIQUÉ

Index = Structure pour recherche rapide
Comme index d'un livre

Sans index :
    SELECT * FROM users WHERE email = 'alice@example.com'
    -> Parcourt TOUTES les lignes (O(n))

Avec index :
    -> Recherche directe (O(log n))

Créés automatiquement pour :
[OK] Primary keys
[OK] Unique constraints
[OK] Foreign keys (selon SGBD)

Créer manuellement avec index=True :
[OK] Colonnes recherchées souvent
[OK] Colonnes de tri (ORDER BY)
[OK] Colonnes de jointure


[ATTENTION] INCONVÉNIENTS INDEX

[X] Ralentit INSERT/UPDATE/DELETE
[X] Prend de l'espace disque
[X] Trop d'index = contre-productif

Règle : Index seulement si nécessaire
"""


"""
INDEX COMPOSITE
"""

from sqlalchemy import Index

class Log(Base):
    __tablename__ = 'logs'
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer)
    action = Column(String(50))
    created_at = Column(DateTime)
    
    # Index sur plusieurs colonnes
    __table_args__ = (
        Index('idx_user_action', 'user_id', 'action'),
        Index('idx_user_date', 'user_id', 'created_at'),
    )

"""
[IDEE] INDEX COMPOSITE

Index sur (user_id, action) :
Rapide pour :
[OK] WHERE user_id = 1
[OK] WHERE user_id = 1 AND action = 'login'

Lent pour :
[X] WHERE action = 'login' (action pas en premier)


Ordre important !
Index(user_id, action) ≠ Index(action, user_id)

Règle : Colonne la plus filtrée en premier
"""


"""
INDEX UNIQUE
"""

class Email(Base):
    __tablename__ = 'emails'
    
    id = Column(Integer, primary_key=True)
    address = Column(String(120))
    
    __table_args__ = (
        Index('idx_email_unique', 'address', unique=True),
    )

# Équivalent à :
# address = Column(String(120), unique=True)


# ----------------------------------------------------------------------------
# [OBJECTIF] DEFAULT ET ONUPDATE
# ----------------------------------------------------------------------------

"""
DEFAULT - VALEUR PAR DÉFAUT
"""

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    
    # Valeur statique
    role = Column(String(20), default='user')
    is_active = Column(Boolean, default=True)
    
    # Fonction Python
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # Lambda
    uuid = Column(String(36), default=lambda: str(uuid.uuid4()))
    
    # Valeur SQL (server_default)
    status = Column(String(20), server_default='pending')

"""
[IDEE] DEFAULT EXPLIQUÉ

default :
- Valeur Python
- Calculée lors de session.add()
- Avant envoi à la BD

server_default :
- Valeur SQL
- Calculée par la base de données
- Expression SQL brute


Exemples :
"""
# default
user = User(username='alice')
print(user.role)  # 'user' (défaut appliqué immédiatement)
print(user.created_at)  # datetime(...) (fonction appelée)

# server_default
user = User(username='bob')
print(user.status)  # None (défaut pas encore appliqué)
session.add(user)
session.commit()
session.refresh(user)
print(user.status)  # 'pending' (défaut appliqué par BD)


"""
ONUPDATE - MISE À JOUR AUTOMATIQUE
"""

class Post(Base):
    __tablename__ = 'posts'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, 
                       default=datetime.utcnow,
                       onupdate=datetime.utcnow)

"""
Comportement :
"""
# Création
post = Post(title='Hello')
session.add(post)
session.commit()
# created_at = 2024-01-18 10:00:00
# updated_at = 2024-01-18 10:00:00

# Modification
post.title = 'Hello World'
session.commit()
# created_at = 2024-01-18 10:00:00 (inchangé)
# updated_at = 2024-01-18 10:05:00 (mis à jour!)


# ----------------------------------------------------------------------------
# [CALCUL] COLONNES CALCULÉES
# ----------------------------------------------------------------------------

"""
HYBRID PROPERTY - CALCULÉ EN PYTHON
"""

from sqlalchemy.ext.hybrid import hybrid_property

class Rectangle(Base):
    __tablename__ = 'rectangles'
    
    id = Column(Integer, primary_key=True)
    width = Column(Integer)
    height = Column(Integer)
    
    @hybrid_property
    def area(self):
        """Aire calculée"""
        return self.width * self.height

"""
Utilisation :
"""
rect = Rectangle(width=10, height=5)
print(rect.area)  # 50 (calculé)

# Fonctionne aussi dans requêtes !
large = session.query(Rectangle).filter(Rectangle.area > 100).all()


"""
GENERATED COLUMN (PostgreSQL/MySQL)
"""

from sqlalchemy import Computed

class Product(Base):
    __tablename__ = 'products'
    
    id = Column(Integer, primary_key=True)
    price = Column(Numeric(10, 2))
    tax_rate = Column(Numeric(5, 4))
    
    # Colonne calculée côté BD
    price_with_tax = Column(
        Numeric(10, 2),
        Computed('price * (1 + tax_rate)')
    )

"""
Différence hybrid_property vs Computed :

hybrid_property :
- Calculé en Python
- Temps réel
- Pas stocké en BD

Computed :
- Calculé par BD
- Stocké (ou virtuel selon SGBD)
- Plus performant pour requêtes
"""


# ============================================================================
# [DOCS] RÉCAPITULATIF CHAPITRES 3-4
# ============================================================================

"""
[OK] TYPES NUMÉRIQUES
Integer, SmallInteger, BigInteger
Float (approximatif)
Numeric/Decimal (exact, finance)

[OK] TYPES TEXTE
String(length) : Texte court
Text : Texte long

[OK] TYPES DATE/HEURE
DateTime : Date + heure
Date : Date seule
Time : Heure seule

[OK] AUTRES TYPES
Boolean : True/False
JSON : Objets JSON
Enum : Valeurs limitées
ARRAY : Tableaux (PostgreSQL)
UUID : Identifiants uniques

[OK] CONTRAINTES
primary_key, unique, nullable
CheckConstraint
Index (simple et composite)

[OK] VALEURS AUTOMATIQUES
default, server_default
onupdate
Computed columns


-> PROCHAINE ÉTAPE : Partie 2

Vous allez apprendre :
- Relations entre tables (One-to-Many, Many-to-Many)
- Requêtes (SELECT, WHERE, JOIN)
- Requêtes avancées
- Agrégations

[RAPIDE] Continuez avec sqlalchemy_partie2.txt !
"""
# ============================================================================
# [LIVRE] SQLALCHEMY - PARTIE 2 : RELATIONS ET REQUÊTES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 5 : Relations entre Tables
# - Chapitre 6 : Requêtes de Base (CRUD)
# - Chapitre 7 : Requêtes Avancées
# - Chapitre 8 : Jointures et Agrégations
#
# [TEMPS] TEMPS : ~6-8 heures
# [DOCS] PRÉREQUIS : Partie 1 complétée
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 5 : RELATIONS ENTRE TABLES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer relations One-to-Many
[OK] Créer relations Many-to-Many
[OK] Créer relations One-to-One
[OK] Utiliser relationship() et ForeignKey
[OK] Comprendre backref et lazy loading
[OK] Cascade et orphan deletion
"""

# ----------------------------------------------------------------------------
# [LIEN] RELATION ONE-TO-MANY (1-N)
# ----------------------------------------------------------------------------

"""
CONCEPT : UN PARENT -> PLUSIEURS ENFANTS

Exemples :
- Un auteur -> plusieurs livres
- Un utilisateur -> plusieurs posts
- Une catégorie -> plusieurs produits


EXEMPLE : USER -> POSTS
"""

from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, ForeignKey
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
from datetime import datetime

Base = declarative_base()

class User(Base):
    """
    [IDEE] PARENT (côté ONE)
    """
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(120), unique=True, nullable=False)
    
    # [IDEE] RELATIONSHIP : Accès aux posts
    posts = relationship('Post', back_populates='author')

class Post(Base):
    """
    [IDEE] ENFANT (côté MANY)
    """
    __tablename__ = 'posts'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    content = Column(Text)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # [IDEE] FOREIGN KEY : Référence vers user
    user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    
    # [IDEE] RELATIONSHIP : Accès à l'auteur
    author = relationship('User', back_populates='posts')

"""
[IDEE] DÉCORTIQUONS


FOREIGN KEY (Côté ENFANT - Post)
---------------------------------
"""
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)

"""
ForeignKey('users.id') :
- 'users' = nom de TABLE (pas classe!)
- 'id' = nom de COLONNE
- Référence vers users.id
- Crée contrainte en base de données

Contraintes :
- user_id doit exister dans users.id
- Empêche orphelins (post sans user)
- Cascade delete (optionnel)


RELATIONSHIP (Côté PARENT - User)
----------------------------------
"""
posts = relationship('Post', back_populates='author')

"""
'Post' : Nom de CLASSE (pas table!)
back_populates='author' : Nom de relationship dans Post

[IDEE] PAS UNE COLONNE EN BD !
Juste un helper Python pour navigation


RELATIONSHIP (Côté ENFANT - Post)
----------------------------------
"""
author = relationship('User', back_populates='posts')

"""
'User' : Classe parent
back_populates='posts' : Nom de relationship dans User


[IDEE] BACK_POPULATES

Crée navigation bidirectionnelle :
user.posts -> Liste des posts
post.author -> User qui a créé le post


UTILISATION
-----------
"""

# Configuration
engine = create_engine('sqlite:///blog.db', echo=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()

# Créer user
user = User(username='alice', email='alice@example.com')
session.add(user)
session.commit()

# Créer posts
post1 = Post(
    title='Mon premier post',
    content='Contenu du post...',
    author=user  # <- Assigner via relationship
)

post2 = Post(
    title='Deuxième post',
    content='Autre contenu...',
    user_id=user.id  # <- Ou assigner user_id directement
)

session.add_all([post1, post2])
session.commit()

# Accéder aux posts d'un user
user = session.query(User).filter_by(username='alice').first()
for post in user.posts:  # <- Navigation automatique !
    print(f"- {post.title}")
# - Mon premier post
# - Deuxième post

# Accéder à l'auteur d'un post
post = session.query(Post).first()
print(f"Auteur: {post.author.username}")  # <- Navigation automatique !
# Auteur: alice

"""
[IDEE] COMMENT ÇA MARCHE ?

user.posts :
    SQLAlchemy génère automatiquement :
    SELECT * FROM posts WHERE user_id = ?
    
post.author :
    SQLAlchemy génère automatiquement :
    SELECT * FROM users WHERE id = ?


LAZY LOADING
------------
"""

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    
    # lazy='select' (défaut)
    posts = relationship('Post', back_populates='author', lazy='select')

"""
[IDEE] OPTIONS LAZY

lazy='select' (défaut) :
    Charge posts SEULEMENT quand accédés
"""
user = session.query(User).first()  # SELECT users
# Pas encore de requête pour posts

print(user.posts)  # SELECT posts WHERE user_id = ?
# Maintenant posts chargés

"""
lazy='joined' :
    Charge user ET posts en une seule requête (JOIN)
"""
posts = relationship('Post', lazy='joined')

user = session.query(User).first()
# SELECT users LEFT JOIN posts ...
# user.posts déjà disponible (pas de requête supplémentaire)

"""
lazy='subquery' :
    Charge posts dans une sous-requête séparée
"""
posts = relationship('Post', lazy='subquery')

users = session.query(User).all()
# SELECT users
# SELECT posts WHERE user_id IN (1, 2, 3, ...)

"""
lazy='dynamic' :
    Retourne Query object (pour filter/order)
"""
posts = relationship('Post', lazy='dynamic')

# user.posts est maintenant une Query
user.posts.count()  # Compter
user.posts.filter(Post.created_at > some_date).all()  # Filtrer
user.posts.order_by(Post.created_at.desc()).limit(10).all()  # Trier

"""
[IDEE] CHOISIR LAZY

lazy='select' :
    [OK] Défaut, bon pour la plupart des cas
    [OK] Pas de surcharge si non accédé
    [X] Problème N+1 queries

lazy='joined' :
    [OK] Une seule requête (performant)
    [X] Peut charger trop de données
    
lazy='subquery' :
    [OK] 2 requêtes (user + posts)
    [OK] Évite N+1
    [X] Un peu plus complexe

lazy='dynamic' :
    [OK] Filtrage/tri sur relation
    [X] Toujours génère requête
"""


"""
CASCADE - SUPPRESSION EN CASCADE
---------------------------------
"""

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    
    # Cascade : Supprimer posts si user supprimé
    posts = relationship(
        'Post',
        back_populates='author',
        cascade='all, delete-orphan'
    )

"""
[IDEE] OPTIONS CASCADE

'all, delete-orphan' :
    Si user supprimé -> posts supprimés
    Si post retiré de user.posts -> post supprimé

'all, delete' :
    Si user supprimé -> posts supprimés
    Posts retirés de user.posts -> gardés

'save-update' (défaut) :
    Propagation add/merge seulement

'delete' :
    Juste propagation delete

None :
    Pas de cascade


Exemple :
"""
user = session.query(User).first()
session.delete(user)
session.commit()
# Tous les posts de ce user sont AUTOMATIQUEMENT supprimés


"""
DELETE-ORPHAN
-------------
"""

user = session.query(User).first()
post = user.posts[0]

# Retirer post de la liste
user.posts.remove(post)
session.commit()
# post supprimé de la base (orphelin)


# ----------------------------------------------------------------------------
# [LIEN] RELATION MANY-TO-MANY (N-N)
# ----------------------------------------------------------------------------

"""
CONCEPT : PLUSIEURS <-> PLUSIEURS

Exemples :
- Étudiants <-> Cours
- Posts <-> Tags
- Produits <-> Catégories

Nécessite TABLE D'ASSOCIATION


EXEMPLE : POSTS <-> TAGS
"""

from sqlalchemy import Table

# [IDEE] TABLE D'ASSOCIATION
post_tags = Table(
    'post_tags',
    Base.metadata,
    Column('post_id', Integer, ForeignKey('posts.id'), primary_key=True),
    Column('tag_id', Integer, ForeignKey('tags.id'), primary_key=True)
)

class Post(Base):
    __tablename__ = 'posts'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    content = Column(Text)
    
    # [IDEE] RELATION MANY-TO-MANY
    tags = relationship(
        'Tag',
        secondary=post_tags,  # <- Table d'association
        back_populates='posts'
    )

class Tag(Base):
    __tablename__ = 'tags'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True, nullable=False)
    
    # [IDEE] RELATION INVERSE
    posts = relationship(
        'Post',
        secondary=post_tags,
        back_populates='tags'
    )

"""
[IDEE] DÉCORTIQUONS


TABLE D'ASSOCIATION
-------------------
"""
post_tags = Table(
    'post_tags',
    Base.metadata,
    Column('post_id', Integer, ForeignKey('posts.id'), primary_key=True),
    Column('tag_id', Integer, ForeignKey('tags.id'), primary_key=True)
)

"""
Table('post_tags', ...) :
    - Nom de la table
    - Base.metadata (registre)
    - 2 Foreign Keys
    - Les deux = Primary Key composite

[ATTENTION] Table, pas class !
Juste une table intermédiaire, pas un modèle Python


STRUCTURE EN BASE DE DONNÉES :

posts               post_tags          tags
─────               ─────────          ────
id  title           post_id tag_id     id  name
1   "Post 1"        1       1          1   "python"
2   "Post 2"        1       2          2   "flask"
3   "Post 3"        2       1          3   "web"
                    2       3

Post 1 a tags : python, flask
Post 2 a tags : python, web


RELATIONSHIP
------------
"""
tags = relationship(
    'Tag',
    secondary=post_tags,  # Table intermédiaire
    back_populates='posts'
)

"""
secondary=post_tags :
    Indique table d'association
    
back_populates :
    Navigation bidirectionnelle


UTILISATION
-----------
"""

# Créer tags
python_tag = Tag(name='python')
flask_tag = Tag(name='flask')
web_tag = Tag(name='web')

session.add_all([python_tag, flask_tag, web_tag])
session.commit()

# Créer post avec tags
post = Post(title='Apprendre Flask', content='...')
post.tags.append(python_tag)  # Ajouter tag
post.tags.append(flask_tag)

session.add(post)
session.commit()

# Ou assigner liste complète
post.tags = [python_tag, flask_tag, web_tag]

# Accéder aux tags d'un post
post = session.query(Post).first()
for tag in post.tags:
    print(tag.name)
# python
# flask

# Accéder aux posts d'un tag
tag = session.query(Tag).filter_by(name='python').first()
for post in tag.posts:
    print(post.title)
# Apprendre Flask

# Retirer un tag
post.tags.remove(flask_tag)
session.commit()

# Vérifier appartenance
if python_tag in post.tags:
    print("Post a le tag python")


"""
TABLE D'ASSOCIATION AVEC ATTRIBUTS SUPPLÉMENTAIRES
---------------------------------------------------

Problème : Ajouter données sur la relation
Exemple : Date d'inscription étudiant-cours
"""

class Enrollment(Base):
    """
    [IDEE] CLASSE pour table d'association
    (au lieu de Table simple)
    """
    __tablename__ = 'enrollments'
    
    student_id = Column(Integer, ForeignKey('students.id'), primary_key=True)
    course_id = Column(Integer, ForeignKey('courses.id'), primary_key=True)
    
    # Attributs supplémentaires
    enrolled_at = Column(DateTime, default=datetime.utcnow)
    grade = Column(String(2))
    
    # Relations
    student = relationship('Student', back_populates='enrollments')
    course = relationship('Course', back_populates='enrollments')

class Student(Base):
    __tablename__ = 'students'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    
    enrollments = relationship('Enrollment', back_populates='student')
    
    # Helper pour accès direct aux cours
    @property
    def courses(self):
        return [e.course for e in self.enrollments]

class Course(Base):
    __tablename__ = 'courses'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    
    enrollments = relationship('Enrollment', back_populates='course')
    
    @property
    def students(self):
        return [e.student for e in self.enrollments]

"""
Utilisation :
"""
student = Student(name='Alice')
course = Course(name='Math 101')

# Créer inscription avec attributs
enrollment = Enrollment(
    student=student,
    course=course,
    grade='A'
)

session.add_all([student, course, enrollment])
session.commit()

# Accès
student = session.query(Student).first()
for enrollment in student.enrollments:
    print(f"{enrollment.course.name}: {enrollment.grade}")
# Math 101: A


# ----------------------------------------------------------------------------
# [LIEN] RELATION ONE-TO-ONE (1-1)
# ----------------------------------------------------------------------------

"""
CONCEPT : UN <-> UN

Exemples :
- User <-> Profile
- Pays <-> Capitale
- Produit <-> Fiche technique


EXEMPLE : USER <-> PROFILE
"""

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    
    # [IDEE] ONE-TO-ONE : uselist=False
    profile = relationship('Profile', back_populates='user', uselist=False)

class Profile(Base):
    __tablename__ = 'profiles'
    
    id = Column(Integer, primary_key=True)
    bio = Column(Text)
    avatar_url = Column(String(200))
    
    # [IDEE] FOREIGN KEY UNIQUE
    user_id = Column(Integer, ForeignKey('users.id'), unique=True)
    
    user = relationship('User', back_populates='profile')

"""
[IDEE] DIFFÉRENCES AVEC ONE-TO-MANY

uselist=False :
    profile retourne UN objet (pas une liste)
    
unique=True sur foreign key :
    Un profile par user seulement


UTILISATION
-----------
"""

# Créer user et profile
user = User(username='alice')
profile = Profile(
    bio='Développeuse Python',
    avatar_url='https://...',
    user=user
)

session.add_all([user, profile])
session.commit()

# Accès
user = session.query(User).first()
print(user.profile.bio)  # UN profile (pas une liste)

profile = session.query(Profile).first()
print(profile.user.username)


"""
ONE-TO-ONE OPTIONNEL
--------------------
"""

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    
    # Profile optionnel
    profile = relationship('Profile', uselist=False)

class Profile(Base):
    __tablename__ = 'profiles'
    id = Column(Integer, primary_key=True)
    bio = Column(Text)
    
    # Nullable : user peut ne pas avoir de profile
    user_id = Column(Integer, ForeignKey('users.id'), unique=True, nullable=True)
    user = relationship('User')

# User sans profile
user_without = User(username='bob')

# User avec profile
user_with = User(username='alice')
user_with.profile = Profile(bio='...')


# ============================================================================
# CE FICHIER CONTINUE...
# POUR LA LISIBILITÉ, JE VAIS CRÉER LES CHAPITRES 6-8 DANS UN AUTRE FICHIER
# ============================================================================

"""
[DOCS] RÉCAPITULATIF CHAPITRE 5

[OK] ONE-TO-MANY : relationship + ForeignKey
[OK] MANY-TO-MANY : relationship + secondary (Table)
[OK] ONE-TO-ONE : relationship + uselist=False + unique FK
[OK] back_populates : Navigation bidirectionnelle
[OK] lazy : Contrôle chargement (select, joined, subquery, dynamic)
[OK] cascade : Suppression automatique


-> PROCHAINE ÉTAPE : Chapitres 6-8 - Requêtes

Vous allez apprendre :
- CRUD complet (Create, Read, Update, Delete)
- Filtres et conditions
- Tri et pagination
- Jointures
- Agrégations

[RAPIDE] Continuons !
"""
# ============================================================================
# [LIVRE] SQLALCHEMY - PARTIE 2 (SUITE) : REQUÊTES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 6 : Requêtes CRUD (Create, Read, Update, Delete)
# - Chapitre 7 : Requêtes Avancées
# - Chapitre 8 : Jointures et Agrégations
#
# [TEMPS] TEMPS : ~4-5 heures
# [DOCS] PRÉREQUIS : Chapitre 5 complété
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 6 : REQUÊTES CRUD
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] CREATE : Créer des enregistrements
[OK] READ : Lire avec query()
[OK] UPDATE : Modifier des enregistrements  
[OK] DELETE : Supprimer des enregistrements
[OK] Gestion des sessions
[OK] Commit et rollback
"""

# ----------------------------------------------------------------------------
# + CREATE - CRÉER DES ENREGISTREMENTS
# ----------------------------------------------------------------------------

"""
CRÉER UN SEUL ENREGISTREMENT
"""

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    email = Column(String(120))

# Configuration
engine = create_engine('sqlite:///example.db')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()

# [IDEE] CRÉER OBJET
user = User(username='alice', email='alice@example.com')

# [IDEE] AJOUTER À LA SESSION
session.add(user)

# [IDEE] SAUVEGARDER EN BASE
session.commit()

print(f"User créé avec ID: {user.id}")  # ID généré automatiquement

"""
[IDEE] ÉTAPES EXPLIQUÉES

1. Créer l'objet Python
   user = User(...)
   -> Instance en mémoire seulement

2. Ajouter à la session
   session.add(user)
   -> Objet tracké par la session
   -> Pas encore en base de données

3. Commit
   session.commit()
   -> INSERT INTO users ...
   -> Données persistées en BD
   -> ID généré et assigné


CRÉER PLUSIEURS ENREGISTREMENTS
--------------------------------
"""

# Méthode 1 : add() multiple
user1 = User(username='bob', email='bob@example.com')
user2 = User(username='charlie', email='charlie@example.com')

session.add(user1)
session.add(user2)
session.commit()

# Méthode 2 : add_all()
users = [
    User(username='david', email='david@example.com'),
    User(username='eve', email='eve@example.com'),
    User(username='frank', email='frank@example.com')
]

session.add_all(users)
session.commit()

"""
[IDEE] BONNES PRATIQUES CREATE

[OK] Valider données avant add()
[OK] Gérer exceptions (IntegrityError)
[OK] Utiliser try/except avec rollback
[OK] Commit par batch (pas trop souvent)
"""

from sqlalchemy.exc import IntegrityError

try:
    user = User(username='alice', email='alice@example.com')
    session.add(user)
    session.commit()
    print("User créé avec succès")
except IntegrityError as e:
    session.rollback()  # Annuler la transaction
    print(f"Erreur : {e}")
    # Erreur : UNIQUE constraint failed (username existe déjà)


# ----------------------------------------------------------------------------
# [RECHERCHE] READ - LIRE DES ENREGISTREMENTS
# ----------------------------------------------------------------------------

"""
RÉCUPÉRER TOUS LES ENREGISTREMENTS
"""

# Tous les users
users = session.query(User).all()
for user in users:
    print(f"{user.id}: {user.username}")

"""
[IDEE] query(User).all()

1. query(User) : Crée requête SELECT
2. .all() : Exécute et retourne liste

SQL généré : SELECT * FROM users


RÉCUPÉRER UN ENREGISTREMENT
---------------------------
"""

# Premier résultat
user = session.query(User).first()
print(user.username if user else "Aucun user")

# Par primary key
user = session.query(User).get(1)
# OU (SQLAlchemy 2.0+)
from sqlalchemy import select
user = session.get(User, 1)

"""
[IDEE] DIFFÉRENCE first() vs get()

first() :
    - Exécute requête complète
    - Retourne premier résultat ou None
    - Peut avoir WHERE

get(id) :
    - Cherche par primary key seulement
    - Plus rapide (cache session)
    - Retourne objet ou None


FILTRER
-------
"""

# Méthode 1 : filter_by (égalité simple)
user = session.query(User).filter_by(username='alice').first()

# Plusieurs conditions (AND)
user = session.query(User).filter_by(
    username='alice',
    email='alice@example.com'
).first()

# Méthode 2 : filter (conditions complexes)
user = session.query(User).filter(User.username == 'alice').first()

"""
[IDEE] filter_by() vs filter()

filter_by(attr=value) :
    [OK] Simple et lisible
    [OK] Seulement égalité
    [OK] Pas besoin de User.attr
    [X] Pas de conditions complexes

filter(User.attr == value) :
    [OK] Toutes les conditions (>, <, !=, LIKE, etc.)
    [OK] Opérateurs logiques (AND, OR)
    [X] Plus verbeux


OPÉRATEURS DE COMPARAISON
--------------------------
"""

# Égalité
users = session.query(User).filter(User.username == 'alice').all()

# Différent
users = session.query(User).filter(User.username != 'admin').all()

# Plus grand que
users = session.query(User).filter(User.id > 10).all()

# Plus petit que
users = session.query(User).filter(User.id < 100).all()

# Plus grand ou égal
users = session.query(User).filter(User.id >= 5).all()

# Plus petit ou égal
users = session.query(User).filter(User.id <= 50).all()

# IN (dans une liste)
users = session.query(User).filter(
    User.username.in_(['alice', 'bob', 'charlie'])
).all()

# NOT IN
users = session.query(User).filter(
    User.username.notin_(['admin', 'root'])
).all()

# LIKE (correspondance partielle)
users = session.query(User).filter(
    User.email.like('%@gmail.com')
).all()

# ILIKE (insensible à la casse - PostgreSQL)
users = session.query(User).filter(
    User.username.ilike('alice%')
).all()

# IS NULL
users = session.query(User).filter(User.email == None).all()
# OU
users = session.query(User).filter(User.email.is_(None)).all()

# IS NOT NULL
users = session.query(User).filter(User.email != None).all()
# OU
users = session.query(User).filter(User.email.isnot(None)).all()

"""
OPÉRATEURS LOGIQUES
-------------------
"""

from sqlalchemy import and_, or_, not_

# AND (toutes les conditions vraies)
users = session.query(User).filter(
    and_(
        User.username.like('a%'),
        User.id > 5
    )
).all()

# OU chaîner filter()
users = session.query(User).filter(
    User.username.like('a%')
).filter(
    User.id > 5
).all()

# OR (au moins une condition vraie)
users = session.query(User).filter(
    or_(
        User.username == 'alice',
        User.email.like('%@admin.com')
    )
).all()

# NOT
users = session.query(User).filter(
    not_(User.username == 'admin')
).all()

# Combinaisons complexes
users = session.query(User).filter(
    and_(
        User.id > 5,
        or_(
            User.username.like('a%'),
            User.email.like('%@gmail.com')
        )
    )
).all()

"""
TRI (ORDER BY)
--------------
"""

# Ordre croissant (alphabétique)
users = session.query(User).order_by(User.username).all()

# Ordre décroissant
users = session.query(User).order_by(User.id.desc()).all()

# Plusieurs colonnes
users = session.query(User).order_by(
    User.username,
    User.id.desc()
).all()

"""
LIMITE ET OFFSET
----------------
"""

# Premiers 10 résultats
users = session.query(User).limit(10).all()

# Pagination (page 2, 10 par page)
page = 2
per_page = 10
users = session.query(User).offset((page - 1) * per_page).limit(per_page).all()

"""
COMPTER
-------
"""

# Nombre total d'utilisateurs
count = session.query(User).count()
print(f"Total users: {count}")

# Avec filtre
active_count = session.query(User).filter(User.is_active == True).count()


"""
SÉLECTIONNER COLONNES SPÉCIFIQUES
----------------------------------
"""

# Seulement username et email
results = session.query(User.username, User.email).all()
for username, email in results:
    print(f"{username}: {email}")

# Une seule colonne
usernames = session.query(User.username).all()
# [('alice',), ('bob',), ...]

# Ou avec scalar
usernames = session.query(User.username).scalar_subquery()


# ----------------------------------------------------------------------------
# [EDIT] UPDATE - MODIFIER DES ENREGISTREMENTS
# ----------------------------------------------------------------------------

"""
MODIFIER UN OBJET
"""

# 1. Récupérer l'objet
user = session.query(User).filter_by(username='alice').first()

# 2. Modifier les attributs
user.email = 'newemail@example.com'
user.username = 'alice2'

# 3. Commit
session.commit()

"""
[IDEE] MODIFICATION EXPLIQUÉE

SQLAlchemy tracke les changements automatiquement :
1. Objet récupéré -> Ajouté à session
2. Attribut modifié -> Marqué comme "dirty"
3. commit() -> UPDATE SQL généré automatiquement

Pas besoin de session.add() !


MODIFIER PLUSIEURS ATTRIBUTS
-----------------------------
"""

user = session.query(User).get(1)

# Avec setattr
updates = {
    'username': 'alice_updated',
    'email': 'alice_updated@example.com'
}

for key, value in updates.items():
    setattr(user, key, value)

session.commit()

"""
BULK UPDATE (Plusieurs enregistrements)
----------------------------------------
"""

# Mettre à jour tous les users inactifs
session.query(User).filter(
    User.is_active == False
).update({
    'is_active': True
})

session.commit()

"""
[ATTENTION] ATTENTION BULK UPDATE

- Ne charge PAS les objets en mémoire
- Plus rapide pour grandes quantités
- Ne déclenche PAS les événements (hooks)
- Bypass la logique Python
"""

# Exemple : Incrémenter views pour tous les posts
session.query(Post).update({
    Post.views: Post.views + 1
})
session.commit()


# ----------------------------------------------------------------------------
# [X] DELETE - SUPPRIMER DES ENREGISTREMENTS
# ----------------------------------------------------------------------------

"""
SUPPRIMER UN OBJET
"""

# 1. Récupérer l'objet
user = session.query(User).filter_by(username='alice').first()

# 2. Supprimer
session.delete(user)

# 3. Commit
session.commit()

"""
SUPPRIMER PLUSIEURS OBJETS
---------------------------
"""

# Récupérer et supprimer un par un
users = session.query(User).filter(User.is_active == False).all()
for user in users:
    session.delete(user)
session.commit()

"""
BULK DELETE (Plus rapide)
--------------------------
"""

# Supprimer sans charger
session.query(User).filter(
    User.is_active == False
).delete()

session.commit()

"""
[ATTENTION] CASCADE DELETE

Si relations configurées avec cascade :
"""
# User configuré avec cascade='all, delete-orphan'
user = session.query(User).first()
session.delete(user)
session.commit()
# Tous les posts de ce user sont AUSSI supprimés


# ============================================================================
# [GUIDE] CHAPITRE 7 : REQUÊTES AVANCÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Requêtes avec fonctions SQL
[OK] Sous-requêtes
[OK] CASE expressions
[OK] DISTINCT
[OK] GROUP BY et HAVING
[OK] Requêtes raw SQL
"""

# ----------------------------------------------------------------------------
# [NOMBRE] FONCTIONS SQL
# ----------------------------------------------------------------------------

"""
FONCTIONS D'AGRÉGATION
"""

from sqlalchemy import func

# COUNT
user_count = session.query(func.count(User.id)).scalar()
# OU
user_count = session.query(User).count()

# SUM
total_views = session.query(func.sum(Post.views)).scalar()

# AVG (moyenne)
avg_views = session.query(func.avg(Post.views)).scalar()

# MIN
min_id = session.query(func.min(User.id)).scalar()

# MAX
max_id = session.query(func.max(User.id)).scalar()

"""
FONCTIONS DE CHAÎNE
-------------------
"""

# UPPER
users = session.query(
    User.username,
    func.upper(User.username).label('username_upper')
).all()

# LOWER
users = session.query(
    func.lower(User.email)
).all()

# LENGTH
users = session.query(User).filter(
    func.length(User.username) > 5
).all()

# CONCAT
full_name = session.query(
    func.concat(User.first_name, ' ', User.last_name)
).all()

"""
FONCTIONS DE DATE
-----------------
"""

from datetime import datetime, timedelta

# NOW / CURRENT_TIMESTAMP
recent_posts = session.query(Post).filter(
    Post.created_at > func.now() - timedelta(days=7)
).all()

# DATE (extraire date)
posts_today = session.query(Post).filter(
    func.date(Post.created_at) == func.date(func.now())
).all()

# YEAR, MONTH, DAY (PostgreSQL)
posts_2024 = session.query(Post).filter(
    func.extract('year', Post.created_at) == 2024
).all()

"""
CASE - CONDITIONS
-----------------
"""

from sqlalchemy import case

# CASE WHEN
status_label = case(
    (User.is_active == True, 'Active'),
    (User.is_active == False, 'Inactive'),
    else_='Unknown'
)

users = session.query(
    User.username,
    status_label.label('status')
).all()

for username, status in users:
    print(f"{username}: {status}")

# Exemple : Catégoriser par age
age_category = case(
    (User.age < 18, 'Minor'),
    (User.age < 65, 'Adult'),
    else_='Senior'
)

users = session.query(
    User.username,
    age_category.label('category')
).all()


# ----------------------------------------------------------------------------
# [GRAPHIQUE] GROUP BY ET AGRÉGATIONS
# ----------------------------------------------------------------------------

"""
GROUP BY - REGROUPEMENT
"""

# Compter posts par user
results = session.query(
    User.username,
    func.count(Post.id).label('post_count')
).join(Post).group_by(User.id).all()

for username, count in results:
    print(f"{username}: {count} posts")

"""
[IDEE] GROUP BY EXPLIQUÉ

Regroupe les lignes par valeur commune
Permet d'appliquer fonctions d'agrégation

Sans GROUP BY :
    COUNT(*) -> Total tous posts

Avec GROUP BY user_id :
    COUNT(*) -> Total posts par user


HAVING - FILTRER GROUPES
-------------------------
"""

# Users avec plus de 5 posts
results = session.query(
    User.username,
    func.count(Post.id).label('post_count')
).join(Post).group_by(User.id).having(
    func.count(Post.id) > 5
).all()

"""
[IDEE] WHERE vs HAVING

WHERE :
    Filtre AVANT groupement
    Sur colonnes individuelles

HAVING :
    Filtre APRÈS groupement  
    Sur résultats agrégés


Exemple :
"""
# WHERE : Filtrer posts actifs, puis compter
session.query(
    User.username,
    func.count(Post.id)
).join(Post).filter(
    Post.is_published == True  # WHERE
).group_by(User.id).having(
    func.count(Post.id) > 5    # HAVING
).all()


"""
DISTINCT - VALEURS UNIQUES
---------------------------
"""

# Tous les emails uniques
unique_emails = session.query(User.email).distinct().all()

# Compter valeurs distinctes
distinct_count = session.query(func.count(User.email.distinct())).scalar()


# ----------------------------------------------------------------------------
# [SYNC] SOUS-REQUÊTES
# ----------------------------------------------------------------------------

"""
SOUS-REQUÊTE SCALAIRE
"""

# Moyenne des posts par user
avg_posts = session.query(
    func.avg(func.count(Post.id))
).join(User).group_by(User.id).scalar_subquery()

# Users avec plus de posts que la moyenne
users = session.query(User).join(Post).group_by(User.id).having(
    func.count(Post.id) > avg_posts
).all()

"""
SOUS-REQUÊTE DANS FROM
-----------------------
"""

from sqlalchemy.orm import aliased

# Créer sous-requête
subq = session.query(
    Post.user_id,
    func.count(Post.id).label('post_count')
).group_by(Post.user_id).subquery()

# Utiliser comme table
PostCount = aliased(User, subq)

results = session.query(
    User.username,
    subq.c.post_count
).join(subq, User.id == subq.c.user_id).all()

"""
EXISTS
------
"""

from sqlalchemy import exists

# Users qui ont au moins un post
has_posts = exists().where(Post.user_id == User.id)
users = session.query(User).filter(has_posts).all()


# ----------------------------------------------------------------------------
# [NOTE] SQL BRUT
# ----------------------------------------------------------------------------

"""
EXÉCUTER SQL BRUT
"""

from sqlalchemy import text

# Simple SELECT
result = session.execute(
    text("SELECT * FROM users WHERE username = :username"),
    {'username': 'alice'}
)

for row in result:
    print(row)

# Avec modèle
users = session.query(User).from_statement(
    text("SELECT * FROM users WHERE id > :id")
).params(id=10).all()

"""
[ATTENTION] SÉCURITÉ SQL BRUT

[OK] Utiliser paramètres (:param)
[X] JAMAIS de f-strings ou concatenation
"""

# [X] DANGEREUX (SQL Injection)
username = "alice' OR '1'='1"
result = session.execute(
    text(f"SELECT * FROM users WHERE username = '{username}'")
)

# [OK] SÛR
result = session.execute(
    text("SELECT * FROM users WHERE username = :username"),
    {'username': username}
)


# ============================================================================
# [GUIDE] CHAPITRE 8 : JOINTURES ET AGRÉGATIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] INNER JOIN
[OK] LEFT JOIN (OUTER JOIN)
[OK] Jointures sur relations
[OK] Jointures explicites
[OK] Sous-requêtes dans JOIN
"""

# ----------------------------------------------------------------------------
# [LIEN] JOINTURES
# ----------------------------------------------------------------------------

"""
INNER JOIN (par relation)
"""

# Users avec leurs posts
results = session.query(User, Post).join(Post).all()

for user, post in results:
    print(f"{user.username}: {post.title}")

"""
[IDEE] INNER JOIN EXPLIQUÉ

Retourne seulement les lignes qui matchent

User sans posts -> Non inclus
Post sans user -> Non inclus (impossible avec FK)

SQL généré :
SELECT users.*, posts.*
FROM users INNER JOIN posts ON users.id = posts.user_id


LEFT JOIN (OUTER JOIN)
----------------------
"""

# Tous les users (avec ou sans posts)
results = session.query(User, Post).outerjoin(Post).all()

for user, post in results:
    if post:
        print(f"{user.username}: {post.title}")
    else:
        print(f"{user.username}: Pas de posts")

"""
[IDEE] LEFT JOIN EXPLIQUÉ

Retourne TOUS les users
+ posts si existent

User sans posts -> Inclus (post = None)
Post sans user -> Non inclus


CHOISIR JOIN TYPE
-----------------

join() = INNER JOIN :
    [OK] Users qui ONT des posts
    [X] Users sans posts exclus

outerjoin() = LEFT JOIN :
    [OK] TOUS les users
    [OK] Même ceux sans posts


JOINTURE EXPLICITE
------------------
"""

# Sans utiliser relationship
results = session.query(User, Post).join(
    Post,
    User.id == Post.user_id
).all()

# Plusieurs jointures
results = session.query(User).join(
    Post
).join(
    Comment,
    Post.id == Comment.post_id
).all()

"""
JOINTURE SUR TABLE MANY-TO-MANY
--------------------------------
"""

# Posts avec leurs tags
results = session.query(Post, Tag).join(
    post_tags,
    Post.id == post_tags.c.post_id
).join(
    Tag,
    Tag.id == post_tags.c.tag_id
).all()

# OU via relationship
results = session.query(Post).join(Post.tags).all()


"""
[DOCS] RÉCAPITULATIF PARTIE 2

[OK] Relations : One-to-Many, Many-to-Many, One-to-One
[OK] CRUD : Create, Read, Update, Delete
[OK] Filtres : filter, filter_by, opérateurs
[OK] Tri : order_by
[OK] Agrégations : COUNT, SUM, AVG
[OK] GROUP BY, HAVING
[OK] Jointures : join, outerjoin


-> PROCHAINE ÉTAPE : Partie 3

Vous allez apprendre :
- Sessions en détail
- Transactions
- Performance et optimisation
- Eager loading

[RAPIDE] Continuez avec sqlalchemy_partie3.txt !
"""
# ============================================================================
# [LIVRE] SQLALCHEMY - PARTIE 3 : SESSIONS, TRANSACTIONS ET PERFORMANCE
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 9 : Comprendre les Sessions en Profondeur
# - Chapitre 10 : Transactions et Gestion d'Erreurs
# - Chapitre 11 : Performance et Optimisation
# - Chapitre 12 : Eager Loading et N+1 Problem
#
# [TEMPS] TEMPS : ~5-7 heures
# [DOCS] PRÉREQUIS : Parties 1 et 2 complétées
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 9 : COMPRENDRE LES SESSIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce qu'une Session et son cycle de vie
[OK] États des objets (Transient, Pending, Persistent, Detached)
[OK] Identity Map et Unit of Work
[OK] Scoped Session et Thread Safety
[OK] Session Makers et contextes
[OK] Bonnes pratiques de gestion des sessions
"""

# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QU'UNE SESSION ?
# ----------------------------------------------------------------------------

"""
DÉFINITION SIMPLE

Session = "Panier d'achats" pour opérations de base de données
- Suit les objets Python
- Génère SQL au commit()
- Gère transactions
- Cache objets (Identity Map)


ANALOGIE [SHOPPING_TROLLEY]

SESSION = PANIER D'ACHATS AMAZON

1. AJOUTER AU PANIER
   session.add(user)
   -> Objet marqué "à acheter" (pas encore acheté)

2. MODIFIER DANS PANIER
   user.email = 'new@example.com'
   -> Session détecte changement automatiquement

3. SUPPRIMER DU PANIER
   session.delete(user)
   -> Marqué "à supprimer"

4. VALIDER COMMANDE
   session.commit()
   -> TOUT est appliqué en base de données

5. ANNULER COMMANDE
   session.rollback()
   -> Annule tous les changements


CRÉER UNE SESSION
-----------------
"""

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

# Engine
engine = create_engine('sqlite:///example.db')

# Session factory
Session = sessionmaker(bind=engine)

# Créer session
session = Session()

# Utiliser
user = User(username='alice')
session.add(user)
session.commit()

# Fermer
session.close()

"""
[IDEE] SESSIONMAKER EXPLIQUÉ

sessionmaker(bind=engine) :
    Crée une FACTORY (pas une session directe)
    Factory = Moule pour créer sessions

session = Session() :
    Crée UNE session
    Chaque appel = nouvelle session indépendante


POURQUOI SESSIONMAKER ?

Configuration centralisée :
"""

Session = sessionmaker(
    bind=engine,
    autoflush=True,      # Flush auto avant query
    autocommit=False,    # Pas de commit auto
    expire_on_commit=True  # Expire objets après commit
)

# Toutes les sessions héritent de cette config
session1 = Session()
session2 = Session()


# ----------------------------------------------------------------------------
# [SYNC] CYCLE DE VIE D'UNE SESSION
# ----------------------------------------------------------------------------

"""
PATTERN RECOMMANDÉ : CONTEXT MANAGER
"""

# [OK] BON : Fermeture automatique
with Session() as session:
    user = User(username='alice')
    session.add(user)
    session.commit()
# session fermée automatiquement

"""
[IDEE] AVANTAGES CONTEXT MANAGER

[OK] Fermeture garantie (même si erreur)
[OK] Pas d'oubli de close()
[OK] Code propre et lisible


PATTERN : try/except/finally
-----------------------------
"""

session = Session()
try:
    user = User(username='alice')
    session.add(user)
    session.commit()
except Exception as e:
    session.rollback()  # Annuler si erreur
    print(f"Erreur : {e}")
    raise
finally:
    session.close()  # Toujours fermer

"""
PATTERN : FONCTION HELPER
--------------------------
"""

def get_session():
    """
    Helper pour obtenir session
    Utilisé avec dependency injection
    """
    session = Session()
    try:
        yield session
        session.commit()
    except Exception:
        session.rollback()
        raise
    finally:
        session.close()

# Utilisation
for session in get_session():
    user = User(username='alice')
    session.add(user)

"""
PATTERN : SCOPED SESSION
------------------------
"""

from sqlalchemy.orm import scoped_session

# Session liée au thread
Session = scoped_session(sessionmaker(bind=engine))

# Même thread = même session
session1 = Session()
session2 = Session()
print(session1 is session2)  # True (même objet)

# Autre thread = session différente

# Supprimer session du thread
Session.remove()

"""
[IDEE] SCOPED SESSION EXPLIQUÉ

Thread-local storage :
- Chaque thread a SA propre session
- Évite conflits multi-threading
- Pas besoin de passer session partout

Utilisation :
[OK] Applications web (Flask, FastAPI)
[OK] Multi-threading
[OK] Accès global simplifié

[ATTENTION] Penser à remove() !
"""


# ----------------------------------------------------------------------------
# [SCENARIO] ÉTATS DES OBJETS
# ----------------------------------------------------------------------------

"""
4 ÉTATS POSSIBLES

1. TRANSIENT (Transitoire)
2. PENDING (En attente)
3. PERSISTENT (Persistant)
4. DETACHED (Détaché)


1. TRANSIENT - Nouveau, pas dans session
-----------------------------------------
"""

user = User(username='alice')
# État : TRANSIENT
# - Pas dans session
# - Pas en base de données
# - Juste un objet Python normal

from sqlalchemy import inspect

state = inspect(user)
print(state.transient)   # True
print(state.pending)     # False
print(state.persistent)  # False

"""
2. PENDING - Dans session, pas encore en BD
--------------------------------------------
"""

session.add(user)
# État : PENDING
# - Dans session
# - Pas encore en base (pas d'INSERT)
# - Attend commit()

state = inspect(user)
print(state.transient)   # False
print(state.pending)     # True
print(state.persistent)  # False

"""
3. PERSISTENT - Dans session ET en BD
--------------------------------------
"""

session.commit()
# État : PERSISTENT
# - Dans session
# - En base de données
# - ID assigné
# - Modifications trackées

state = inspect(user)
print(state.transient)   # False
print(state.pending)     # False
print(state.persistent)  # True
print(user.id)          # 1 (ID généré)

"""
4. DETACHED - Plus dans session
--------------------------------
"""

session.close()
# État : DETACHED
# - Plus dans session
# - Toujours en base
# - Modifications NON trackées

state = inspect(user)
print(state.detached)    # True
print(state.persistent)  # False

# Modifications non trackées
user.username = 'bob'
# [ATTENTION] Changement PAS sauvegardé (session fermée)

"""
[IDEE] DIAGRAMME DES ÉTATS

    [new User()]
         v
    TRANSIENT
         v session.add()
      PENDING
         v session.commit()
    PERSISTENT <-─────┐
         v           │
    session.close()  │ session.add()
         v           │
     DETACHED ───────┘


INSPECTER ÉTAT
--------------
"""

from sqlalchemy import inspect

def print_state(obj):
    """Afficher état d'un objet"""
    state = inspect(obj)
    
    print(f"Transient: {state.transient}")
    print(f"Pending: {state.pending}")
    print(f"Persistent: {state.persistent}")
    print(f"Detached: {state.detached}")
    
    if state.persistent or state.detached:
        print(f"ID: {obj.id}")

# Utilisation
user = User(username='alice')
print_state(user)
# Transient: True
# Pending: False
# Persistent: False
# Detached: False


# ----------------------------------------------------------------------------
# [WORLD_MAP] IDENTITY MAP
# ----------------------------------------------------------------------------

"""
IDENTITY MAP = CACHE DE SESSION

Garantit : 1 ligne BD = 1 objet Python en mémoire


EXEMPLE
-------
"""

session = Session()

# Requête 1
user1 = session.query(User).filter_by(id=1).first()

# Requête 2 (même ID)
user2 = session.query(User).filter_by(id=1).first()

# Même objet !
print(user1 is user2)  # True (même référence mémoire)

"""
[IDEE] AVANTAGES IDENTITY MAP

1. COHÉRENCE
   Modifications propagées automatiquement
"""
user1.username = 'alice_updated'
print(user2.username)  # 'alice_updated' (même objet)

"""
2. PERFORMANCE
   Deuxième requête ne touche pas la BD
"""
# Première requête : SELECT * FROM users WHERE id = 1
user1 = session.query(User).get(1)

# Deuxième requête : Pas de SQL (cache)
user2 = session.query(User).get(1)

"""
3. ÉVITE CONFLITS
   Impossible d'avoir 2 versions différentes


[ATTENTION] ATTENTION : Cache par SESSION

Différentes sessions = objets différents :
"""

session1 = Session()
session2 = Session()

user1 = session1.query(User).get(1)
user2 = session2.query(User).get(1)

print(user1 is user2)  # False (sessions différentes)


"""
EXPIRATION D'OBJETS
-------------------
"""

user = session.query(User).get(1)
print(user.username)  # 'alice'

# Expirer l'objet (invalider cache)
session.expire(user)

# Prochain accès -> Recharge depuis BD
print(user.username)  # SELECT * FROM users WHERE id = 1

"""
[IDEE] QUAND UTILISER EXPIRE ?

[OK] Après UPDATE SQL brut
[OK] Après trigger base de données
[OK] Rafraîchir données modifiées ailleurs


Expirer tous les objets :
"""
session.expire_all()

"""
REFRESH - RECHARGER IMMÉDIATEMENT
----------------------------------
"""

user = session.query(User).get(1)

# Recharger depuis BD maintenant
session.refresh(user)
# SELECT * FROM users WHERE id = 1


# ----------------------------------------------------------------------------
# [CONFIG] UNIT OF WORK
# ----------------------------------------------------------------------------

"""
UNIT OF WORK = Suivre les changements

Session suit automatiquement :
- Objets nouveaux (INSERT)
- Objets modifiés (UPDATE)  
- Objets supprimés (DELETE)


TRACKER LES CHANGEMENTS
-----------------------
"""

session = Session()

# Créer
user = User(username='alice')
session.add(user)

# Modifier
user.email = 'alice@example.com'

# Session sait qu'il faut :
# - INSERT user (nouveau)
# - Pas besoin de UPDATE (pas encore en BD)

session.commit()
# INSERT INTO users (username, email) VALUES ('alice', 'alice@example.com')

"""
MODIFICATIONS DÉTECTÉES AUTO
-----------------------------
"""

user = session.query(User).get(1)

# Modification
user.username = 'alice_new'

# Session détecte automatiquement (marked as dirty)
print(user in session.dirty)  # True

session.commit()
# UPDATE users SET username = 'alice_new' WHERE id = 1

"""
INSPECTER CHANGEMENTS
---------------------
"""

session = Session()

user1 = User(username='alice')
session.add(user1)

user2 = session.query(User).get(2)
user2.username = 'bob_updated'

user3 = session.query(User).get(3)
session.delete(user3)

# Objets nouveaux
print(session.new)
# IdentitySet([user1])

# Objets modifiés
print(session.dirty)
# IdentitySet([user2])

# Objets à supprimer
print(session.deleted)
# IdentitySet([user3])


"""
FLUSH - SYNCHRONISER SANS COMMIT
---------------------------------
"""

user = User(username='alice')
session.add(user)

# Flush : Envoie SQL sans commit
session.flush()
# INSERT INTO users ...
# ID généré

print(user.id)  # 1 (disponible après flush)

# Mais transaction pas encore committée
# rollback() possible

session.rollback()  # Annule INSERT


"""
[IDEE] FLUSH vs COMMIT

flush() :
    - Envoie SQL à la BD
    - Reste dans transaction
    - ID générés disponibles
    - rollback() possible

commit() :
    - flush() automatique
    - Valide transaction
    - Persistance définitive
    - rollback() impossible


AUTOFLUSH
---------
"""

Session = sessionmaker(bind=engine, autoflush=True)

session = Session()
user = User(username='alice')
session.add(user)

# Query déclenche flush automatique
count = session.query(User).count()
# flush() automatique avant SELECT

"""
[IDEE] POURQUOI AUTOFLUSH ?

Garantit cohérence :
- Nouvelles lignes visibles dans queries
- Modifications prises en compte

Désactiver si besoin :
"""
session.autoflush = False


# ----------------------------------------------------------------------------
# [OBJECTIF] BONNES PRATIQUES SESSIONS
# ----------------------------------------------------------------------------

"""
1. UNE SESSION PAR REQUÊTE WEB
------------------------------
"""

# Flask
@app.route('/users/<int:user_id>')
def get_user(user_id):
    session = Session()
    try:
        user = session.query(User).get(user_id)
        return jsonify(user.to_dict())
    finally:
        session.close()

# OU avec context manager
@app.route('/users/<int:user_id>')
def get_user(user_id):
    with Session() as session:
        user = session.query(User).get(user_id)
        return jsonify(user.to_dict())

"""
2. NE PAS PARTAGER SESSIONS ENTRE THREADS
------------------------------------------
"""

# [X] MAUVAIS
session = Session()  # Session globale

def thread_function():
    user = session.query(User).first()  # Danger !

# [OK] BON
def thread_function():
    session = Session()  # Session locale au thread
    try:
        user = session.query(User).first()
    finally:
        session.close()

"""
3. FERMER TOUJOURS LES SESSIONS
--------------------------------
"""

# [X] MAUVAIS
session = Session()
users = session.query(User).all()
# Oubli de close() -> Fuite de connexions

# [OK] BON
with Session() as session:
    users = session.query(User).all()

"""
4. GÉRER LES ERREURS
--------------------
"""

session = Session()
try:
    user = User(username='alice')
    session.add(user)
    session.commit()
except IntegrityError:
    session.rollback()
    print("Username déjà pris")
except Exception as e:
    session.rollback()
    raise
finally:
    session.close()

"""
5. NE PAS UTILISER OBJETS DÉTACHÉS
-----------------------------------
"""

# [X] MAUVAIS
session = Session()
user = session.query(User).first()
session.close()

user.username = 'new'  # Objet détaché (changement perdu)

# [OK] BON
session = Session()
user = session.query(User).first()
user.username = 'new'
session.commit()  # Changement sauvegardé
session.close()

# OU réattacher
session2 = Session()
session2.add(user)  # Réattacher
session2.commit()


# ============================================================================
# [GUIDE] CHAPITRE 10 : TRANSACTIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre ACID
[OK] Gérer transactions manuellement
[OK] Nested transactions (savepoints)
[OK] Niveaux d'isolation
[OK] Gestion d'erreurs transactionnelles
"""

# ----------------------------------------------------------------------------
# [GEM_STONE] ACID - PROPRIÉTÉS TRANSACTIONNELLES
# ----------------------------------------------------------------------------

"""
ACID = Atomicity, Consistency, Isolation, Durability


A - ATOMICITY (Atomicité)
-------------------------
Tout ou rien : Transaction complète ou annulée entièrement
"""

session = Session()
try:
    # Créer user
    user = User(username='alice', email='alice@example.com')
    session.add(user)
    
    # Créer profile
    profile = Profile(bio='Developer', user=user)
    session.add(profile)
    
    # LES DEUX ou RIEN
    session.commit()
    
except Exception:
    session.rollback()  # Annule USER ET PROFILE

"""
[IDEE] SI ERREUR :
- User pas créé
- Profile pas créé
- Base reste cohérente


C - CONSISTENCY (Cohérence)
---------------------------
Base passe d'état cohérent à état cohérent
Contraintes respectées
"""

# [X] Viole contrainte UNIQUE
try:
    user1 = User(username='alice', email='alice@example.com')
    session.add(user1)
    session.commit()  # OK
    
    user2 = User(username='alice', email='other@example.com')
    session.add(user2)
    session.commit()  # [X] IntegrityError (username unique)
    
except IntegrityError:
    session.rollback()
    # Base reste cohérente (pas de doublon)

"""
I - ISOLATION (Isolation)
-------------------------
Transactions concurrentes n'interfèrent pas
"""

# Transaction 1
session1 = Session()
user = session1.query(User).get(1)
user.balance = 100

# Transaction 2 (en même temps)
session2 = Session()
user2 = session2.query(User).get(1)
print(user2.balance)  # Valeur isolée (selon niveau isolation)

"""
D - DURABILITY (Durabilité)
---------------------------
Données committées = permanentes (même après crash)
"""

session.commit()
# Données sur disque
# Survivent à crash serveur, coupure électricité


# ----------------------------------------------------------------------------
# [SYNC] GESTION MANUELLE DES TRANSACTIONS
# ----------------------------------------------------------------------------

"""
BEGIN - DÉMARRER TRANSACTION
-----------------------------
"""

session = Session()

# Transaction démarre automatiquement au premier SQL
user = session.query(User).first()
# BEGIN (implicite)

# OU explicite
session.begin()

"""
COMMIT - VALIDER
----------------
"""

user = User(username='alice')
session.add(user)

session.commit()
# Valide tous les changements
# Nouvelle transaction démarre après

"""
ROLLBACK - ANNULER
------------------
"""

user = User(username='alice')
session.add(user)

session.rollback()
# Annule tous les changements depuis dernier commit
# user pas ajouté

"""
EXEMPLE COMPLET : TRANSFERT BANCAIRE
-------------------------------------
"""

def transfer_money(from_user_id, to_user_id, amount):
    """
    Transfert atomique entre deux comptes
    """
    session = Session()
    try:
        # Charger comptes
        from_user = session.query(User).get(from_user_id)
        to_user = session.query(User).get(to_user_id)
        
        # Vérifier solde suffisant
        if from_user.balance < amount:
            raise ValueError("Solde insuffisant")
        
        # Effectuer transfert
        from_user.balance -= amount
        to_user.balance += amount
        
        # Tout ou rien
        session.commit()
        return True
        
    except Exception as e:
        session.rollback()
        print(f"Transfert échoué : {e}")
        return False
        
    finally:
        session.close()

"""
[IDEE] GARANTIES :

[OK] Les deux comptes mis à jour ensemble
[OK] Si erreur : aucun changement
[OK] Pas de solde perdu
"""


# ----------------------------------------------------------------------------
# [IMPORTANT] SAVEPOINTS (Transactions imbriquées)
# ----------------------------------------------------------------------------

"""
SAVEPOINT = Point de sauvegarde dans transaction

Permet rollback partiel
"""

session = Session()

# Transaction principale
user1 = User(username='alice')
session.add(user1)

# Savepoint
savepoint = session.begin_nested()
try:
    user2 = User(username='bob')
    session.add(user2)
    session.flush()  # Peut échouer
    
except Exception:
    savepoint.rollback()  # Annule seulement user2
    # user1 toujours dans transaction

# Commit principal
session.commit()
# user1 sauvegardé, user2 annulé

"""
[IDEE] UTILISATION SAVEPOINTS

[OK] Opérations optionnelles
[OK] Batch avec erreurs partielles
[OK] Fallback automatique


EXEMPLE : Batch avec erreurs
-----------------------------
"""

def import_users(users_data):
    """
    Importer users, continuer même si certains échouent
    """
    session = Session()
    imported = []
    failed = []
    
    for data in users_data:
        savepoint = session.begin_nested()
        try:
            user = User(**data)
            session.add(user)
            session.flush()
            imported.append(data['username'])
            
        except Exception as e:
            savepoint.rollback()
            failed.append((data['username'], str(e)))
    
    session.commit()
    
    print(f"Importés : {imported}")
    print(f"Échoués : {failed}")

"""
[IDEE] RÉSULTAT :

Users valides -> Importés
Users invalides -> Ignorés  
Pas de blocage total
"""


# ----------------------------------------------------------------------------
# [VERROUILLE] NIVEAUX D'ISOLATION
# ----------------------------------------------------------------------------

"""
4 NIVEAUX D'ISOLATION (du moins au plus strict)

1. READ UNCOMMITTED
2. READ COMMITTED (défaut PostgreSQL)
3. REPEATABLE READ (défaut MySQL)
4. SERIALIZABLE


CONFIGURER NIVEAU
-----------------
"""

from sqlalchemy import create_engine

# Niveau global
engine = create_engine(
    'postgresql://user:pass@localhost/db',
    isolation_level='REPEATABLE READ'
)

# Niveau par session
session = Session()
session.connection(execution_options={'isolation_level': 'SERIALIZABLE'})

"""
1. READ UNCOMMITTED - Le moins strict
--------------------------------------
"""
# Transaction 1
session1.execute("UPDATE users SET balance = 100 WHERE id = 1")
# Pas encore commit

# Transaction 2
user = session2.query(User).get(1)
print(user.balance)  # Peut voir 100 (dirty read)

"""
[ATTENTION] DIRTY READ :
Lecture de données non committées
Risque si transaction 1 rollback

Utilisation : Rarement (rapports approximatifs)


2. READ COMMITTED - Standard
-----------------------------
"""
# Transaction 1
session1.execute("UPDATE users SET balance = 100 WHERE id = 1")

# Transaction 2
user = session2.query(User).get(1)
print(user.balance)  # Voit ancienne valeur

# Transaction 1
session1.commit()

# Transaction 2
user = session2.query(User).get(1)
print(user.balance)  # Voit nouvelle valeur (100)

"""
[OK] Pas de dirty read
[ATTENTION] Lecture peut changer dans même transaction (non-repeatable read)

Utilisation : Défaut (bon compromis)


3. REPEATABLE READ - Plus strict
---------------------------------
"""
# Transaction 1
user1 = session1.query(User).get(1)
print(user1.balance)  # 50

# Transaction 2
session2.execute("UPDATE users SET balance = 100 WHERE id = 1")
session2.commit()

# Transaction 1 (relecture)
user1_again = session1.query(User).get(1)
print(user1_again.balance)  # Toujours 50 (repeatable)

"""
[OK] Lectures cohérentes dans transaction
[ATTENTION] Phantom reads possibles (nouvelles lignes)

Utilisation : Calculs critiques, rapports


4. SERIALIZABLE - Le plus strict
---------------------------------
Transactions exécutées séquentiellement (simulation)
"""

# Évite tous les problèmes
# Mais lent (lock fort)

"""
Utilisation : Opérations critiques (finance)


[IDEE] CHOISIR NIVEAU

READ UNCOMMITTED : Jamais (dirty reads dangereux)
READ COMMITTED : Défaut (bon pour la plupart)
REPEATABLE READ : Rapports, analytics
SERIALIZABLE : Finance, inventaire


# ============================================================================
# CE FICHIER CONTINUE DANS sqlalchemy_partie3_suite.txt
# ============================================================================

"""
[DOCS] RÉCAPITULATIF CHAPITRES 9-10

[OK] Session : "Panier" pour opérations DB
[OK] États objets : Transient, Pending, Persistent, Detached
[OK] Identity Map : Cache par session
[OK] Unit of Work : Track changements auto
[OK] ACID : Atomicité, Cohérence, Isolation, Durabilité
[OK] Transactions : begin, commit, rollback
[OK] Savepoints : Rollback partiel
[OK] Niveaux isolation : Read Committed, Repeatable Read, etc.


-> PROCHAINE ÉTAPE : Chapitres 11-12

Vous allez apprendre :
- Optimisation performance
- Problème N+1
- Eager loading
- Caching

[RAPIDE] Continuez !
"""
# ============================================================================
# [LIVRE] SQLALCHEMY - PARTIE 3 (SUITE) : PERFORMANCE
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 11 : Performance et Optimisation
# - Chapitre 12 : Eager Loading et Problème N+1
#
# [TEMPS] TEMPS : ~3-4 heures
# [DOCS] PRÉREQUIS : Chapitres 9-10 complétés
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 11 : PERFORMANCE ET OPTIMISATION
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Identifier problèmes de performance
[OK] Optimiser requêtes
[OK] Utiliser index efficacement
[OK] Batch operations
[OK] Connection pooling
[OK] Profiling et debugging SQL
"""

# ----------------------------------------------------------------------------
# [GRAPHIQUE] MESURER LA PERFORMANCE
# ----------------------------------------------------------------------------

"""
ACTIVER ECHO SQL
"""

from sqlalchemy import create_engine

# Afficher toutes les requêtes SQL
engine = create_engine('sqlite:///db.db', echo=True)

# session.query(User).all()
# -> SELECT users.id, users.username, users.email FROM users

"""
[IDEE] echo=True INDISPENSABLE

[OK] Voir SQL généré
[OK] Détecter requêtes inutiles
[OK] Comprendre comportement ORM
[OK] Debug problèmes N+1

[ATTENTION] Désactiver en production (performance)


MESURER TEMPS D'EXÉCUTION
--------------------------
"""

import time

start = time.time()

users = session.query(User).all()

end = time.time()
print(f"Temps: {end - start:.4f}s")

"""
PROFILING DÉTAILLÉ
------------------
"""

from sqlalchemy import event
from sqlalchemy.engine import Engine

@event.listens_for(Engine, "before_cursor_execute")
def receive_before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    conn.info.setdefault('query_start_time', []).append(time.time())

@event.listens_for(Engine, "after_cursor_execute")
def receive_after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    total = time.time() - conn.info['query_start_time'].pop()
    print(f"Query: {statement}")
    print(f"Time: {total:.4f}s\n")


# ----------------------------------------------------------------------------
# [RAPIDE] OPTIMISATIONS REQUÊTES
# ----------------------------------------------------------------------------

"""
1. SÉLECTIONNER COLONNES NÉCESSAIRES
-------------------------------------
"""

# [X] MAUVAIS : Charge tout
users = session.query(User).all()
for user in users:
    print(user.username)

# [OK] BON : Seulement username
usernames = session.query(User.username).all()
for (username,) in usernames:
    print(username)

"""
2. UTILISER LIMIT
-----------------
"""

# [X] MAUVAIS : Charge 1 million de lignes
users = session.query(User).all()
first_10 = users[:10]

# [OK] BON : Limite SQL
first_10 = session.query(User).limit(10).all()

"""
3. PAGINATION EFFICACE
----------------------
"""

# [X] MAUVAIS : OFFSET élevé lent
page = 1000
per_page = 20
users = session.query(User).offset(page * per_page).limit(per_page).all()

# [OK] BON : Pagination par clé (keyset pagination)
last_id = 19980  # Dernier ID de page précédente

users = session.query(User).filter(
    User.id > last_id
).order_by(User.id).limit(per_page).all()

"""
4. EXISTS vs COUNT
------------------
"""

# [X] MAUVAIS : Compte tout
count = session.query(User).filter(User.is_active == True).count()
if count > 0:
    print("Des users actifs existent")

# [OK] BON : Vérifie seulement existence
from sqlalchemy import exists

has_active = session.query(
    exists().where(User.is_active == True)
).scalar()

if has_active:
    print("Des users actifs existent")

"""
5. BULK OPERATIONS
------------------
"""

# [X] MAUVAIS : 1000 requêtes
for i in range(1000):
    user = User(username=f'user{i}')
    session.add(user)
    session.commit()  # Commit à chaque fois

# [OK] BON : 1 requête
users = [User(username=f'user{i}') for i in range(1000)]
session.bulk_save_objects(users)
session.commit()

"""
[IDEE] bulk_save_objects

[OK] Beaucoup plus rapide
[OK] Minimal overhead
[X] Pas d'events (before_insert, etc.)
[X] Pas de relationship population
[X] Pas de ID retournés automatiquement


Bulk Update :
"""
# [X] MAUVAIS
for user in session.query(User).filter(User.is_active == False):
    user.is_active = True
session.commit()

# [OK] BON
session.query(User).filter(
    User.is_active == False
).update({'is_active': True})
session.commit()


# ----------------------------------------------------------------------------
# [CARD_INDEX] INDEX ET OPTIMISATION
# ----------------------------------------------------------------------------

"""
INDEX - RAPPEL
"""

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)  # Index auto
    username = Column(String, unique=True)   # Index auto
    email = Column(String, index=True)       # Index manuel
    created_at = Column(DateTime)

"""
[IDEE] QUAND CRÉER INDEX

[OK] Colonnes WHERE fréquentes
[OK] Colonnes ORDER BY
[OK] Colonnes JOIN
[OK] Foreign keys

[X] Tables petites (< 1000 lignes)
[X] Colonnes rarement filtrées
[X] Colonnes avec peu de valeurs uniques


INDEX COMPOSITE
---------------
"""

from sqlalchemy import Index

class Log(Base):
    __tablename__ = 'logs'
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer)
    action = Column(String)
    created_at = Column(DateTime)
    
    __table_args__ = (
        Index('idx_user_action', 'user_id', 'action'),
        Index('idx_user_date', 'user_id', 'created_at'),
    )

"""
[IDEE] ORDRE INDEX COMPOSITE

Index(user_id, action) :
    Rapide pour :
    [OK] WHERE user_id = 1
    [OK] WHERE user_id = 1 AND action = 'login'
    
    Lent pour :
    [X] WHERE action = 'login' (action pas en premier)

Règle : Colonne la plus filtrée en premier


EXPLAIN QUERY
-------------
"""

# Voir plan d'exécution
from sqlalchemy import text

result = session.execute(
    text("EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'alice@example.com'")
)

for row in result:
    print(row)

# SCAN TABLE users (sans index)
# SEARCH TABLE users USING INDEX idx_email (avec index)


# ----------------------------------------------------------------------------
# [PLUGIN] CONNECTION POOLING
# ----------------------------------------------------------------------------

"""
POOL DE CONNEXIONS

Réutilise connexions au lieu de créer/fermer à chaque fois


CONFIGURATION
-------------
"""

from sqlalchemy.pool import QueuePool, NullPool, StaticPool

engine = create_engine(
    'postgresql://user:pass@localhost/db',
    
    # Type de pool
    poolclass=QueuePool,  # Défaut (file d'attente)
    
    # Taille du pool
    pool_size=5,          # Connexions permanentes
    max_overflow=10,      # Connexions temporaires max
    
    # Timeout
    pool_timeout=30,      # Attente max pour obtenir connexion
    
    # Recyclage
    pool_recycle=3600,    # Recycler connexion après 1h
    
    # Pre-ping
    pool_pre_ping=True    # Tester connexion avant utilisation
)

"""
[IDEE] OPTIONS EXPLIQUÉES

pool_size=5 :
    5 connexions permanentes dans pool
    Toujours disponibles

max_overflow=10 :
    10 connexions supplémentaires si nécessaire
    Total max = pool_size + max_overflow = 15
    Fermées quand plus utilisées

pool_timeout=30 :
    Si toutes connexions utilisées
    Attendre 30s avant erreur

pool_recycle=3600 :
    Renouveler connexion après 1h
    Évite timeout serveur (MySQL 8h défaut)

pool_pre_ping=True :
    Teste connexion (SELECT 1) avant utilisation
    Détecte connexions mortes
    Léger overhead mais sécurité


TYPES DE POOLS
--------------

QueuePool (défaut) :
    File d'attente
    pool_size + max_overflow
    Production normale

NullPool :
    Pas de pool (reconnexion à chaque fois)
    Tests, dev

StaticPool :
    Une seule connexion partagée
    SQLite en mémoire
"""

# SQLite en mémoire (1 connexion)
engine = create_engine(
    'sqlite:///:memory:',
    poolclass=StaticPool
)


# ============================================================================
# [GUIDE] CHAPITRE 12 : EAGER LOADING ET PROBLÈME N+1
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce que le problème N+1
[OK] Détecter N+1
[OK] Résoudre avec joinedload
[OK] Résoudre avec subqueryload
[OK] Résoudre avec selectinload
[OK] Choisir la bonne stratégie
"""

# ----------------------------------------------------------------------------
# [LENT] PROBLÈME N+1
# ----------------------------------------------------------------------------

"""
DÉFINITION

N+1 queries = 1 requête + N requêtes supplémentaires
Très mauvais pour performance


EXEMPLE PROBLÉMATIQUE
---------------------
"""

# [X] MAUVAIS : Problème N+1
users = session.query(User).all()  # 1 requête

for user in users:
    print(f"{user.username} a {len(user.posts)} posts")  # N requêtes !
    # SELECT * FROM posts WHERE user_id = 1
    # SELECT * FROM posts WHERE user_id = 2
    # SELECT * FROM posts WHERE user_id = 3
    # ...

"""
[IDEE] QUE SE PASSE-T-IL ?

1. Query users : SELECT * FROM users  (1 requête)
2. Pour chaque user, accéder posts :
   - user.posts déclenche nouvelle requête
   - 100 users = 100 requêtes supplémentaires
   - Total : 1 + 100 = 101 requêtes !

[ATTENTION] DÉSASTREUX pour performance


DÉTECTER N+1
------------

Avec echo=True :
"""

engine = create_engine('sqlite:///db.db', echo=True)

users = session.query(User).all()
# SELECT * FROM users

for user in users:
    print(len(user.posts))
# SELECT * FROM posts WHERE user_id = ?
# SELECT * FROM posts WHERE user_id = ?
# SELECT * FROM posts WHERE user_id = ?
# ...

# Voir plein de SELECT similaires = N+1 !


# ----------------------------------------------------------------------------
# [OK] SOLUTION 1 : JOINEDLOAD
# ----------------------------------------------------------------------------

"""
JOINEDLOAD - LEFT OUTER JOIN
"""

from sqlalchemy.orm import joinedload

# [OK] BON : 1 seule requête
users = session.query(User).options(
    joinedload(User.posts)
).all()

# SELECT users.*, posts.*
# FROM users LEFT OUTER JOIN posts ON users.id = posts.user_id

# Tous les users ET leurs posts en 1 requête

for user in users:
    print(f"{user.username} a {len(user.posts)} posts")
    # Pas de requête supplémentaire !

"""
[IDEE] JOINEDLOAD EXPLIQUÉ

Utilise LEFT OUTER JOIN :
- Charge parents ET enfants en 1 requête
- users.posts déjà en mémoire
- Pas de lazy loading

Avantages :
[OK] 1 seule requête
[OK] Simple

Inconvénients :
[X] Peut générer beaucoup de données dupliquées
[X] Lent si beaucoup de relations

Quand utiliser :
[OK] Relation One-to-One
[OK] Relation One-to-Many avec peu d'enfants
[X] Many-to-Many (dédoublement)


JOINEDLOAD MULTIPLE NIVEAUX
----------------------------
"""

# User -> Posts -> Comments
users = session.query(User).options(
    joinedload(User.posts).joinedload(Post.comments)
).all()

# 1 requête avec 2 JOIN


# ----------------------------------------------------------------------------
# [OK] SOLUTION 2 : SUBQUERYLOAD
# ----------------------------------------------------------------------------

"""
SUBQUERYLOAD - SOUS-REQUÊTE SÉPARÉE
"""

from sqlalchemy.orm import subqueryload

users = session.query(User).options(
    subqueryload(User.posts)
).all()

# Requête 1 : SELECT * FROM users
# Requête 2 : SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...)

# 2 requêtes au lieu de N+1

"""
[IDEE] SUBQUERYLOAD EXPLIQUÉ

2 requêtes :
1. Charge tous les users
2. Charge tous leurs posts (IN clause)

Avantages :
[OK] 2 requêtes (pas N+1)
[OK] Pas de duplication
[OK] Bon pour One-to-Many avec beaucoup d'enfants

Inconvénients :
[X] 2 requêtes (vs 1 pour joinedload)

Quand utiliser :
[OK] One-to-Many avec beaucoup d'enfants
[OK] Éviter duplication
"""


# ----------------------------------------------------------------------------
# [OK] SOLUTION 3 : SELECTINLOAD (Recommandé SQLAlchemy 2.0+)
# ----------------------------------------------------------------------------

"""
SELECTINLOAD - SELECT IN (Moderne)
"""

from sqlalchemy.orm import selectinload

users = session.query(User).options(
    selectinload(User.posts)
).all()

# Requête 1 : SELECT * FROM users
# Requête 2 : SELECT * FROM posts WHERE user_id IN (1, 2, 3, ...)

"""
[IDEE] SELECTINLOAD EXPLIQUÉ

Similaire à subqueryload mais :
[OK] Plus simple (pas de sous-requête)
[OK] Plus rapide
[OK] Recommandé SQLAlchemy 2.0+

Avantages :
[OK] 2 requêtes (pas N+1)
[OK] Pas de duplication
[OK] Performant

Quand utiliser :
[OK] DÉFAUT pour SQLAlchemy 2.0+
[OK] One-to-Many
[OK] Many-to-Many


EAGER LOAD MULTIPLE RELATIONS
------------------------------
"""

users = session.query(User).options(
    selectinload(User.posts),
    selectinload(User.comments),
    selectinload(User.profile)
).all()

# 4 requêtes au lieu de 3*N+1


# ----------------------------------------------------------------------------
# [GRAPHIQUE] COMPARAISON STRATÉGIES
# ----------------------------------------------------------------------------

"""
┌──────────────┬───────────┬────────────┬──────────────┐
│              │ REQUÊTES  │ DUPLICATION│ UTILISATION  │
├──────────────┼───────────┼────────────┼──────────────┤
│ Lazy (défaut)│ N+1       │ Aucune     │ [X] Éviter    │
├──────────────┼───────────┼────────────┼──────────────┤
│ joinedload   │ 1         │ Élevée     │ One-to-One   │
│              │           │            │ Peu enfants  │
├──────────────┼───────────┼────────────┼──────────────┤
│ subqueryload │ 2         │ Aucune     │ Legacy       │
├──────────────┼───────────┼────────────┼──────────────┤
│ selectinload │ 2         │ Aucune     │ [OK] Défaut    │
│              │           │            │ Recommandé   │
└──────────────┴───────────┴────────────┴──────────────┘


CHOISIR LA BONNE STRATÉGIE
---------------------------

One-to-One (User -> Profile) :
    -> joinedload (1 requête)

One-to-Many, peu enfants (User -> Posts, < 10) :
    -> joinedload

One-to-Many, beaucoup enfants (User -> Posts, > 10) :
    -> selectinload

Many-to-Many :
    -> selectinload

Défaut SQLAlchemy 2.0+ :
    -> selectinload
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] EAGER LOADING PAR DÉFAUT
# ----------------------------------------------------------------------------

"""
CONFIGURER LAZY DANS MODÈLE
"""

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String)
    
    # Lazy par défaut
    posts = relationship('Post', lazy='select')
    
    # Eager avec joined
    profile = relationship('Profile', lazy='joined')
    
    # Eager avec selectin
    comments = relationship('Comment', lazy='selectin')

"""
[IDEE] lazy OPTIONS

'select' (défaut) :
    Charge au premier accès (lazy)
    Problème N+1 potentiel

'joined' :
    LEFT OUTER JOIN
    Eager loading

'selectin' :
    IN clause
    Eager loading (recommandé)

'subquery' :
    Sous-requête
    Eager loading (legacy)

'dynamic' :
    Query object
    Pour filtrage
"""


"""
[DOCS] RÉCAPITULATIF PARTIE 3 COMPLÈTE

[OK] Sessions : États, Identity Map, Unit of Work
[OK] Transactions : ACID, commit, rollback, savepoints
[OK] Niveaux isolation : Read Committed, Repeatable Read
[OK] Performance : Mesure, optimisation, index
[OK] Connection pooling : Configuration
[OK] Problème N+1 : Détection et résolution
[OK] Eager loading : joinedload, selectinload


-> PROCHAINE ÉTAPE : Partie 4

Vous allez apprendre :
- Migrations avec Alembic
- Déploiement production
- Testing
- Best practices finales

[RAPIDE] Dernier sprint !
"""
# ============================================================================
# [LIVRE] SQLALCHEMY - PARTIE 4 : PRODUCTION ET BEST PRACTICES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 13 : Migrations avec Alembic
# - Chapitre 14 : Testing
# - Chapitre 15 : Déploiement Production
# - Chapitre 16 : Best Practices Finales
#
# [TEMPS] TEMPS : ~5-7 heures
# [DOCS] PRÉREQUIS : Parties 1-3 complétées
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 13 : MIGRATIONS AVEC ALEMBIC
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Pourquoi utiliser Alembic
[OK] Initialiser Alembic
[OK] Créer migrations automatiques
[OK] Créer migrations manuelles
[OK] Appliquer et annuler migrations
[OK] Gérer branches de migrations
"""

# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI ALEMBIC ?
# ----------------------------------------------------------------------------

"""
PROBLÈME : ÉVOLUTION DU SCHÉMA

Version 1 de votre app :
"""

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String)
    email = Column(String)

# Créer tables
Base.metadata.create_all(engine)

"""
Plusieurs semaines plus tard :
"""

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String)
    email = Column(String)
    phone = Column(String)  # <- NOUVEAU !
    is_active = Column(Boolean, default=True)  # <- NOUVEAU !

"""
[X] PROBLÈME

Base.metadata.create_all(engine) :
- NE modifie PAS tables existantes
- Crée seulement nouvelles tables
- Ignore nouvelles colonnes


SOLUTIONS SANS ALEMBIC

1. SQL manuel
"""
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT 1;

"""
[X] Problèmes :
- Erreur si colonne existe déjà
- Pas de traçabilité
- Pas de rollback
- Équipe désynchronisée

2. Drop + Recreate
"""
Base.metadata.drop_all(engine)  # [ATTENTION] PERD TOUTES LES DONNÉES !
Base.metadata.create_all(engine)

"""
[X] Inacceptable en production


[OK] SOLUTION : ALEMBIC

Alembic = Outil de migration de schéma
- Historique versionné
- Migrations réversibles (up/down)
- Génération automatique
- Partageable (Git)


ANALOGIE [DOCS]

Alembic = Git pour base de données

Migration = Commit
alembic upgrade = git checkout
alembic downgrade = git revert
"""


# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION ET INITIALISATION
# ----------------------------------------------------------------------------

"""
INSTALLATION
"""

pip install alembic

"""
INITIALISATION
"""

# Dans dossier projet
alembic init alembic

"""
[IDEE] STRUCTURE CRÉÉE

project/
├── alembic/
│   ├── env.py              # Configuration environnement
│   ├── script.py.mako      # Template de migration
│   ├── README
│   └── versions/           # Dossier des migrations
├── alembic.ini             # Configuration Alembic
└── models.py


CONFIGURATION alembic.ini
--------------------------
"""

# alembic.ini
"""
[alembic]
# Chemin vers dossier migrations
script_location = alembic

# URL de base de données
sqlalchemy.url = sqlite:///./app.db
# OU utiliser variable d'environnement (production)
# sqlalchemy.url = driver://user:pass@localhost/dbname

# Format nom de fichier migration
file_template = %%(rev)s_%%(slug)s

# Timezone
timezone = UTC
"""

"""
CONFIGURATION env.py
--------------------
"""

# alembic/env.py
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
from alembic import context

# Importer vos modèles
from models import Base

# Configuration Alembic
config = context.config

# Metadata de vos modèles
target_metadata = Base.metadata

def run_migrations_online():
    """Mode en ligne (avec connexion BD)"""
    connectable = engine_from_config(
        config.get_section(config.config_ini_section),
        prefix='sqlalchemy.',
        poolclass=pool.NullPool,
    )
    
    with connectable.connect() as connection:
        context.configure(
            connection=connection,
            target_metadata=target_metadata
        )
        
        with context.begin_transaction():
            context.run_migrations()

run_migrations_online()

"""
[IDEE] POINT IMPORTANT

target_metadata = Base.metadata

Alembic compare target_metadata (vos modèles Python)
avec schéma actuel en BD
pour détecter changements
"""


# ----------------------------------------------------------------------------
# [NOTE] CRÉER UNE MIGRATION
# ----------------------------------------------------------------------------

"""
MIGRATION AUTOMATIQUE (Recommandé)
-----------------------------------
"""

# 1. Modifier modèles
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    username = Column(String)
    email = Column(String)
    phone = Column(String)  # <- Ajouté

# 2. Générer migration
alembic revision --autogenerate -m "Add phone to users"

"""
[IDEE] QUE SE PASSE-T-IL ?

1. Alembic lit target_metadata (vos modèles)
2. Compare avec schéma BD actuel
3. Détecte différences
4. Génère script de migration

Fichier créé :
alembic/versions/abc123_add_phone_to_users.py


FICHIER DE MIGRATION GÉNÉRÉ
----------------------------
"""

# alembic/versions/abc123_add_phone_to_users.py
"""Add phone to users

Revision ID: abc123
Revises: xyz456
Create Date: 2024-01-18 10:30:00

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = 'abc123'
down_revision = 'xyz456'  # Migration précédente
branch_labels = None
depends_on = None

def upgrade():
    """Appliquer migration (en avant)"""
    # ### commands auto generated by Alembic ###
    op.add_column('users', sa.Column('phone', sa.String(), nullable=True))
    # ### end Alembic commands ###

def downgrade():
    """Annuler migration (en arrière)"""
    # ### commands auto generated by Alembic ###
    op.drop_column('users', 'phone')
    # ### end Alembic commands ###

"""
[IDEE] ANATOMIE MIGRATION

revision = 'abc123' :
    ID unique de cette migration

down_revision = 'xyz456' :
    Migration précédente (chaînage)

upgrade() :
    Code pour appliquer changements
    
downgrade() :
    Code pour annuler changements


APPLIQUER LA MIGRATION
-----------------------
"""

# Appliquer toutes migrations en attente
alembic upgrade head

# SQL exécuté :
# ALTER TABLE users ADD COLUMN phone VARCHAR;

"""
[IDEE] upgrade head

head = Dernière migration
Applique toutes migrations non appliquées


VÉRIFIER ÉTAT
-------------
"""

# Version actuelle
alembic current

# Historique
alembic history

# Output :
# xyz456 -> abc123 (head), Add phone to users
# def789 -> xyz456, Initial migration
# <base> -> def789, Create users table


# ----------------------------------------------------------------------------
# [OUTIL] MIGRATIONS MANUELLES
# ----------------------------------------------------------------------------

"""
QUAND ÉCRIRE MANUELLEMENT ?

[OK] Migration de données
[OK] Changements complexes
[OK] Alembic ne détecte pas changement
[OK] Renommer colonne (Alembic = drop + add)


CRÉER MIGRATION VIDE
---------------------
"""

alembic revision -m "Migrate user data"

"""
EXEMPLES OPÉRATIONS MANUELLES
------------------------------

1. MIGRATION DE DONNÉES
"""

def upgrade():
    # Migrer données
    op.execute("""
        UPDATE users 
        SET full_name = first_name || ' ' || last_name
        WHERE first_name IS NOT NULL AND last_name IS NOT NULL
    """)
    
    # Supprimer anciennes colonnes
    op.drop_column('users', 'first_name')
    op.drop_column('users', 'last_name')

def downgrade():
    # Ajouter colonnes
    op.add_column('users', sa.Column('first_name', sa.String()))
    op.add_column('users', sa.Column('last_name', sa.String()))
    
    # Impossible de récupérer données originales
    # Laisser None ou valeur par défaut

"""
2. RENOMMER COLONNE
"""

def upgrade():
    op.alter_column('users', 'username', new_column_name='user_name')

def downgrade():
    op.alter_column('users', 'user_name', new_column_name='username')

"""
3. AJOUTER INDEX
"""

def upgrade():
    op.create_index('idx_users_email', 'users', ['email'])

def downgrade():
    op.drop_index('idx_users_email', 'users')

"""
4. AJOUTER FOREIGN KEY
"""

def upgrade():
    op.create_foreign_key(
        'fk_posts_user_id',  # Nom contrainte
        'posts',              # Table source
        'users',              # Table cible
        ['user_id'],          # Colonne source
        ['id']                # Colonne cible
    )

def downgrade():
    op.drop_constraint('fk_posts_user_id', 'posts', type_='foreignkey')

"""
5. CRÉER TABLE
"""

def upgrade():
    op.create_table(
        'posts',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('title', sa.String(200), nullable=False),
        sa.Column('content', sa.Text()),
        sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id'))
    )

def downgrade():
    op.drop_table('posts')


# ----------------------------------------------------------------------------
# [SYNC] GESTION DES MIGRATIONS
# ----------------------------------------------------------------------------

"""
APPLIQUER MIGRATIONS
--------------------
"""

# Dernière migration
alembic upgrade head

# Migration spécifique
alembic upgrade abc123

# Avancer de N migrations
alembic upgrade +2

"""
ANNULER MIGRATIONS
------------------
"""

# Annuler dernière
alembic downgrade -1

# Revenir à migration spécifique
alembic downgrade xyz456

# Tout annuler
alembic downgrade base

"""
HISTORIQUE
----------
"""

# Afficher historique
alembic history --verbose

# Historique avec range
alembic history -r base:head

"""
STAMP - Marquer sans exécuter
------------------------------
"""

# Marquer base comme à la dernière migration
# (utile si BD déjà à jour)
alembic stamp head

"""
MERGE - Fusionner branches
---------------------------
"""

# Si deux développeurs créent migrations en parallèle
alembic merge abc123 def456 -m "Merge migrations"


# ----------------------------------------------------------------------------
# [OBJECTIF] BONNES PRATIQUES ALEMBIC
# ----------------------------------------------------------------------------

"""
1. VÉRIFIER MIGRATION AVANT COMMIT
-----------------------------------
"""

# Toujours tester localement
alembic upgrade head
# Vérifier que ça marche

alembic downgrade -1
# Vérifier que rollback fonctionne

alembic upgrade head
# Réappliquer

"""
2. MESSAGES DESCRIPTIFS
-----------------------
"""

# [X] MAUVAIS
alembic revision -m "update"

# [OK] BON
alembic revision -m "Add email verification fields to users"

"""
3. MIGRATIONS PETITES ET ATOMIQUES
-----------------------------------
"""

# [X] MAUVAIS : 1 migration géante
alembic revision -m "Big refactor: add 10 tables, modify 20 columns"

# [OK] BON : Plusieurs petites migrations
alembic revision -m "Add posts table"
alembic revision -m "Add comments table"
alembic revision -m "Add user.is_verified column"

"""
4. TESTER DOWNGRADE
-------------------
"""

# Toujours tester que downgrade() fonctionne
alembic downgrade -1
alembic upgrade head

"""
5. SAUVEGARDER AVANT MIGRATION PRODUCTION
------------------------------------------
"""

# Backup base de données
pg_dump mydb > backup_before_migration.sql

# Appliquer migration
alembic upgrade head

# Si problème : restaurer
psql mydb < backup_before_migration.sql

"""
6. UTILISER TRANSACTIONS
-------------------------
"""

# alembic.ini
[alembic]
transaction_per_migration = true

# Chaque migration dans sa propre transaction
# Rollback auto si erreur


# ============================================================================
# [GUIDE] CHAPITRE 14 : TESTING
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Configurer tests avec SQLAlchemy
[OK] Utiliser base de données de test
[OK] Fixtures pytest
[OK] Tester modèles
[OK] Tester requêtes
[OK] Mocking
"""

# ----------------------------------------------------------------------------
# [TEST] CONFIGURATION TESTS
# ----------------------------------------------------------------------------

"""
INSTALLATION
"""

pip install pytest pytest-cov

"""
STRUCTURE PROJET
"""

project/
├── src/
│   ├── models.py
│   ├── database.py
│   └── crud.py
├── tests/
│   ├── conftest.py        # Fixtures pytest
│   ├── test_models.py
│   └── test_crud.py
├── alembic/
└── pytest.ini

"""
DATABASE DE TEST
----------------
"""

# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from models import Base

# Base de données en mémoire pour tests
TEST_DATABASE_URL = "sqlite:///:memory:"

@pytest.fixture(scope='function')
def engine():
    """Engine de test"""
    engine = create_engine(TEST_DATABASE_URL, echo=False)
    Base.metadata.create_all(engine)
    yield engine
    Base.metadata.drop_all(engine)

@pytest.fixture(scope='function')
def session(engine):
    """Session de test"""
    Session = sessionmaker(bind=engine)
    session = Session()
    yield session
    session.close()

"""
[IDEE] EXPLICATIONS

scope='function' :
    Nouvelle BD pour chaque test
    Tests isolés

sqlite:///:memory: :
    Base en mémoire (rapide)
    Pas de fichier
    Supprimée après tests

create_all / drop_all :
    Schéma propre pour chaque test


TESTER MODÈLES
--------------
"""

# tests/test_models.py
from models import User

def test_create_user(session):
    """Test création user"""
    user = User(username='alice', email='alice@example.com')
    session.add(user)
    session.commit()
    
    assert user.id is not None
    assert user.username == 'alice'

def test_user_unique_username(session):
    """Test contrainte unique"""
    from sqlalchemy.exc import IntegrityError
    
    user1 = User(username='alice', email='alice1@example.com')
    session.add(user1)
    session.commit()
    
    user2 = User(username='alice', email='alice2@example.com')
    session.add(user2)
    
    with pytest.raises(IntegrityError):
        session.commit()

"""
TESTER RELATIONS
----------------
"""

def test_user_posts_relationship(session):
    """Test relation user-posts"""
    from models import User, Post
    
    user = User(username='alice')
    post1 = Post(title='Post 1', author=user)
    post2 = Post(title='Post 2', author=user)
    
    session.add_all([user, post1, post2])
    session.commit()
    
    # Vérifier relation
    assert len(user.posts) == 2
    assert post1.author.username == 'alice'

"""
TESTER REQUÊTES
---------------
"""

def test_filter_users(session):
    """Test filtrage users"""
    from models import User
    
    # Créer données de test
    users = [
        User(username='alice', is_active=True),
        User(username='bob', is_active=False),
        User(username='charlie', is_active=True)
    ]
    session.add_all(users)
    session.commit()
    
    # Tester requête
    active_users = session.query(User).filter(
        User.is_active == True
    ).all()
    
    assert len(active_users) == 2
    assert all(u.is_active for u in active_users)

"""
FACTORIES (Pour données de test)
---------------------------------
"""

pip install factory-boy

# tests/factories.py
import factory
from models import User, Base

class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        model = User
        sqlalchemy_session_persistence = 'commit'
    
    username = factory.Sequence(lambda n: f'user{n}')
    email = factory.LazyAttribute(lambda obj: f'{obj.username}@example.com')
    is_active = True

# tests/test_with_factory.py
def test_with_factory(session):
    UserFactory._meta.sqlalchemy_session = session
    
    # Créer 10 users facilement
    users = UserFactory.create_batch(10)
    
    assert session.query(User).count() == 10


# ============================================================================
# [GUIDE] CHAPITRE 15 : PRODUCTION
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Configuration production
[OK] Connection pooling production
[OK] Monitoring
[OK] Logging
[OK] Déploiement
"""

# ----------------------------------------------------------------------------
# [CONFIG] CONFIGURATION PRODUCTION
# ----------------------------------------------------------------------------

"""
VARIABLES D'ENVIRONNEMENT
--------------------------
"""

# config.py
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

class Config:
    """Configuration de base"""
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class DevelopmentConfig(Config):
    """Développement"""
    DATABASE_URL = 'sqlite:///dev.db'
    DEBUG = True
    ECHO = True

class ProductionConfig(Config):
    """Production"""
    DATABASE_URL = os.getenv('DATABASE_URL')
    DEBUG = False
    ECHO = False
    
    # Pool de connexions
    POOL_SIZE = 20
    MAX_OVERFLOW = 10
    POOL_PRE_PING = True
    POOL_RECYCLE = 3600

# database.py
ENV = os.getenv('ENVIRONMENT', 'development')
config_dict = {
    'development': DevelopmentConfig,
    'production': ProductionConfig
}
config = config_dict[ENV]

engine = create_engine(
    config.DATABASE_URL,
    echo=config.ECHO,
    pool_size=getattr(config, 'POOL_SIZE', 5),
    max_overflow=getattr(config, 'MAX_OVERFLOW', 10),
    pool_pre_ping=getattr(config, 'POOL_PRE_PING', True),
    pool_recycle=getattr(config, 'POOL_RECYCLE', 3600)
)

"""
LOGGING PRODUCTION
------------------
"""

import logging

# Configuration logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('sqlalchemy.log'),
        logging.StreamHandler()
    ]
)

# Logger SQLAlchemy
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)

"""
MONITORING
----------
"""

from sqlalchemy import event
from sqlalchemy.engine import Engine
import time

@event.listens_for(Engine, "before_cursor_execute")
def receive_before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    conn.info.setdefault('query_start_time', []).append(time.time())

@event.listens_for(Engine, "after_cursor_execute")
def receive_after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    total = time.time() - conn.info['query_start_time'].pop()
    
    # Logger requêtes lentes
    if total > 1.0:  # > 1 seconde
        logging.warning(f"Slow query ({total:.2f}s): {statement}")


# ============================================================================
# [GUIDE] CHAPITRE 16 : BEST PRACTICES FINALES
# ============================================================================

"""
[OK] ARCHITECTURE

1. SÉPARER CONCERNS
-------------------
"""

project/
├── models/           # Modèles
│   ├── user.py
│   └── post.py
├── repositories/     # Accès données
│   ├── user_repo.py
│   └── post_repo.py
├── services/         # Logique métier
│   └── user_service.py
└── api/             # Endpoints

"""
2. UTILISER REPOSITORIES
-------------------------
"""

# repositories/user_repo.py
class UserRepository:
    def __init__(self, session):
        self.session = session
    
    def get_by_id(self, user_id):
        return self.session.query(User).get(user_id)
    
    def get_by_username(self, username):
        return self.session.query(User).filter_by(
            username=username
        ).first()
    
    def create(self, **kwargs):
        user = User(**kwargs)
        self.session.add(user)
        return user

"""
[OK] PERFORMANCE

1. TOUJOURS EAGER LOAD
-----------------------
"""

# [X] MAUVAIS
users = session.query(User).all()
for user in users:
    print(user.posts)  # N+1

# [OK] BON
users = session.query(User).options(
    selectinload(User.posts)
).all()

"""
2. LIMITER RÉSULTATS
--------------------
"""

# Toujours utiliser limit() pour listes
users = session.query(User).limit(100).all()

"""
3. INDEX SUR COLONNES FILTRÉES
-------------------------------
"""

class User(Base):
    __tablename__ = 'users'
    email = Column(String, index=True)  # Souvent filtré

"""
[OK] SÉCURITÉ

1. TOUJOURS PARAMÉTRER REQUÊTES
--------------------------------
"""

# [X] DANGEREUX
username = request.args.get('username')
session.execute(f"SELECT * FROM users WHERE username = '{username}'")

# [OK] SÛR
session.query(User).filter_by(username=username).first()

"""
2. VALIDER ENTRÉES
------------------
"""

from pydantic import BaseModel, EmailStr

class UserCreate(BaseModel):
    username: str
    email: EmailStr

"""
[OK] MIGRATIONS

1. TESTER AVANT PRODUCTION
---------------------------
"""

# Staging
alembic upgrade head

# Tester app

# Production
alembic upgrade head

"""
2. SAUVEGARDER BD
-----------------
"""

# Avant migration
pg_dump mydb > backup.sql

"""
[DOCS] RÉCAPITULATIF COMPLET

VOUS MAÎTRISEZ MAINTENANT :

Partie 1 :
[OK] Installation et configuration
[OK] Modèles et colonnes
[OK] Types de données
[OK] Contraintes

Partie 2 :
[OK] Relations (1-N, N-N, 1-1)
[OK] CRUD complet
[OK] Requêtes avancées
[OK] Jointures

Partie 3 :
[OK] Sessions et états
[OK] Transactions
[OK] Performance
[OK] Eager loading

Partie 4 :
[OK] Migrations Alembic
[OK] Testing
[OK] Production
[OK] Best practices


[BRAVO] FÉLICITATIONS !

Vous êtes maintenant un expert SQLAlchemy !

Vous pouvez :
[OK] Créer applications avec base de données
[OK] Gérer relations complexes
[OK] Optimiser performance
[OK] Déployer en production
[OK] Maintenir schéma avec migrations


[RAPIDE] PROCHAINES ÉTAPES

1. Pratiquer sur projets réels
2. Contribuer open-source
3. Explorer features avancées (Events, Hybrid properties)
4. Apprendre async SQLAlchemy

Bonne chance ! [FORCE]
"""