# ============================================================================
# [LIVRE] BASES DE DONNÉES - GUIDE ULTRA-DÉTAILLÉ POUR DÉBUTANTS
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR MAÎTRISER LES BASES DE DONNÉES DE ZÉRO À EXPERT
#
# Ce guide est organisé en 5 parties progressives :
#
# PARTIE 1 : FONDAMENTAUX (database_partie1.txt)
# - Chapitre 0 : Introduction aux Bases de Données
# - Chapitre 1 : Concepts Fondamentaux
# - Chapitre 2 : SQL de Base (SELECT, INSERT, UPDATE, DELETE)
# - Chapitre 3 : Types de Données
# - Chapitre 4 : Contraintes et Clés
#
# PARTIE 2 : SQL AVANCÉ (database_partie2.txt)
# - Chapitre 5 : Requêtes Complexes (JOIN, UNION, Sous-requêtes)
# - Chapitre 6 : Fonctions et Agrégations
# - Chapitre 7 : Index et Optimisation
# - Chapitre 8 : Vues et Procédures Stockées
#
# PARTIE 3 : CONCEPTION ET MODÉLISATION (database_partie3.txt)
# - Chapitre 9 : Modélisation de Données
# - Chapitre 10 : Normalisation
# - Chapitre 11 : Diagrammes ER
# - Chapitre 12 : Design Patterns
#
# PARTIE 4 : BASES DE DONNÉES AVEC PYTHON (database_partie4.txt)
# - Chapitre 13 : SQLite avec Python
# - Chapitre 14 : PostgreSQL avec Python
# - Chapitre 15 : SQLAlchemy ORM
# - Chapitre 16 : Migrations et Alembic
#
# PARTIE 5 : PRODUCTION ET AVANCÉ (database_partie5.txt)
# - Chapitre 17 : Transactions et ACID
# - Chapitre 18 : Performance et Tuning
# - Chapitre 19 : Sécurité des Bases de Données
# - Chapitre 20 : NoSQL et Bases de Données Modernes
#
# [TEMPS] TEMPS DE LECTURE TOTAL : ~25-30 heures
# [DOCS] PRÉREQUIS : Aucun (commence vraiment de zéro)
#
# [IDEE] COMMENT UTILISER CE GUIDE :
# 1. Lisez les parties dans l'ordre
# 2. Testez TOUS les exemples (avec SQLite d'abord)
# 3. Faites les exercices pratiques
# 4. Créez vos propres projets
#
# ============================================================================

"""
[OBJECTIF] PHILOSOPHIE DE CE GUIDE

COMMENT ? -> Explications pas à pas avec exemples concrets
POURQUOI ? -> Raisons et contexte de chaque concept
QUAND ? -> Cas d'usage réels et scénarios pratiques
PRATIQUE -> Exemples testables immédiatement

Ce guide vise à être VOTRE SEULE RÉFÉRENCE pour les bases de données !
Vous n'aurez PLUS BESOIN de chercher ailleurs.
"""

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

"""
[IDEE] Information importante / Point clé
[REFLEXION] Question / Réflexion / Problème posé
[OK] Bonne pratique / Solution correcte
[X] Mauvaise pratique / À éviter
[ATTENTION] Attention / Avertissement / Pièges courants
[CLE] Point clé à retenir absolument
[COURS] Exercice pratique à faire
[DOCS] Résumé de section
[OBJECTIF] Objectif d'apprentissage
[TEMPS] Temps estimé
[RAPIDE] Prêt pour la suite
[HOT] Astuce importante / Pro tip
[BUG] Bug / Erreur courante
[DESIGN] Exemple visuel / Diagramme
[GRAPHIQUE] Tableau comparatif
[LIEN] Relation / Lien entre concepts
"""

# ============================================================================
# [OUTILS] PRÉPARATION DE L'ENVIRONNEMENT
# ============================================================================

"""
AVANT DE COMMENCER

1. INSTALLER PYTHON (si pas déjà fait)
   - Version recommandée : Python 3.8+
   - Vérifier : python --version

2. INSTALLER SQLite (DÉJÀ INCLUS AVEC PYTHON !)
   SQLite est intégré à Python, rien à installer ! [BRAVO]
   
3. INSTALLER UN OUTIL DE VISUALISATION (optionnel mais recommandé)
   
   Option A : DB Browser for SQLite (RECOMMANDÉ pour débutants)
   - Gratuit et facile
   - Interface graphique
   - Télécharger : https://sqlitebrowser.org/
   
   Option B : DBeaver (pour utilisateurs avancés)
   - Gratuit et puissant
   - Supporte tous les SGBD
   - Télécharger : https://dbeaver.io/
   
   Option C : DataGrip (professionnel, payant)
   - Le plus complet
   - Télécharger : https://www.jetbrains.com/datagrip/

4. CRÉER UN DOSSIER DE TRAVAIL
"""

# Créer structure de projet
import os

# Dossier principal
os.makedirs('database_learning', exist_ok=True)
os.chdir('database_learning')

# Sous-dossiers
os.makedirs('databases', exist_ok=True)  # Pour fichiers .db
os.makedirs('scripts', exist_ok=True)    # Pour scripts SQL
os.makedirs('python', exist_ok=True)     # Pour code Python

"""
Structure créée :
database_learning/
├── databases/      # Fichiers de base de données (.db)
├── scripts/        # Scripts SQL (.sql)
├── python/         # Code Python (.py)
└── README.md       # Notes personnelles

[IDEE] POURQUOI CETTE STRUCTURE ?

[OK] Organisation claire
[OK] Séparation données/code
[OK] Facile à retrouver
[OK] Prêt pour Git (optionnel)
"""

# ============================================================================
# [DOSSIER] CRÉER .gitignore (SI VOUS UTILISEZ GIT)
# ============================================================================

"""
Si vous utilisez Git pour versionner votre apprentissage :
"""

# .gitignore
"""
# Bases de données (données personnelles)
*.db
*.sqlite
*.sqlite3

# Fichiers Python
__pycache__/
*.py[cod]
*$py.class
*.so

# Environnements virtuels
venv/
env/
ENV/

# IDE
.vscode/
.idea/
*.swp

# Système
.DS_Store
Thumbs.db

# Logs
*.log
"""


# ============================================================================
# [GUIDE] CHAPITRE 0 : INTRODUCTION AUX BASES DE DONNÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est une base de données et pourquoi c'est essentiel
[OK] Les différents types de bases de données
[OK] Quand utiliser SQL vs NoSQL
[OK] Les principaux SGBD et leurs différences
[OK] Comment choisir la bonne base de données
"""


# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QU'UNE BASE DE DONNÉES ?
# ----------------------------------------------------------------------------

"""
DÉFINITION SIMPLE

Une base de données (DB) = Système organisé pour stocker, gérer et 
récupérer des données de manière efficace et sécurisée.


[IDEE] ANALOGIE : LA BIBLIOTHÈQUE [DOCS]

Imaginez une bibliothèque :

SANS SYSTÈME (fichiers éparpillés) :
[X] Livres empilés n'importe comment
[X] Impossible de trouver un livre rapidement
[X] Pas de système de prêt
[X] Perte facile de livres
[X] Plusieurs personnes ne peuvent pas chercher en même temps

AVEC SYSTÈME (base de données) :
[OK] Livres organisés par catégories
[OK] Système de recherche rapide (index)
[OK] Gestion des prêts (transactions)
[OK] Catalogue centralisé
[OK] Accès concurrent possible


[REFLEXION] PROBLÈME : STOCKER DONNÉES SANS BASE DE DONNÉES

Exemple : Application de gestion d'étudiants
"""

# [X] MAUVAISE APPROCHE : Fichiers texte
students = []

# Ajouter étudiant
student = "Jean Dupont,20,Informatique,jean@email.com"
with open('students.txt', 'a') as f:
    f.write(student + '\n')

# Chercher un étudiant
with open('students.txt', 'r') as f:
    for line in f:
        if 'Jean' in line:
            print(line)

"""
[X] PROBLÈMES MAJEURS :

1. PERFORMANCE
   - Chercher = lire TOUT le fichier (O(n))
   - Lent avec beaucoup de données
   
2. CONCURRENT ACCESS
   - Deux programmes ne peuvent pas écrire en même temps
   - Risque de corruption de données
   
3. PAS DE RELATIONS
   - Comment lier étudiant <-> cours ?
   - Duplication de données
   
4. PAS DE VALIDATION
   - Email invalide ? Accepté quand même
   - Âge négatif ? Aucune vérification
   
5. PAS DE SÉCURITÉ
   - Aucun contrôle d'accès
   - Pas de chiffrement
   
6. DIFFICILE À MAINTENIR
   - Format changeant = réécrire tout le code
   - Pas de migration de structure
   
7. PAS DE BACKUP/RECOVERY
   - Fichier corrompu = données perdues
   
8. PAS DE REQUÊTES COMPLEXES
   - "Tous les étudiants de plus de 18 ans en Informatique" = code complexe


[OK] AVEC UNE BASE DE DONNÉES :
"""

import sqlite3

# Connexion à la base de données
conn = sqlite3.connect('students.db')
cursor = conn.cursor()

# Chercher étudiant (rapide grâce aux index)
cursor.execute("SELECT * FROM students WHERE name LIKE '%Jean%'")
results = cursor.fetchall()

# Requête complexe (simple en SQL)
cursor.execute("""
    SELECT * FROM students 
    WHERE age > 18 
    AND major = 'Informatique'
""")

"""
[OK] AVANTAGES :

[OK] PERFORMANCE
   - Index pour recherches rapides (O(log n))
   - Optimisations automatiques
   
[OK] CONCURRENT ACCESS
   - Plusieurs utilisateurs simultanés
   - Gestion automatique des conflits (locks)
   
[OK] RELATIONS
   - Tables liées (foreign keys)
   - Pas de duplication
   
[OK] VALIDATION
   - Contraintes (CHECK, NOT NULL)
   - Types de données
   
[OK] SÉCURITÉ
   - Authentification
   - Permissions
   - Chiffrement
   
[OK] MAINTENABILITÉ
   - Schéma versionné
   - Migrations
   
[OK] BACKUP/RECOVERY
   - Snapshots
   - Journalisation (logs)
   
[OK] REQUÊTES PUISSANTES
   - SQL : langage déclaratif
   - Agrégations, jointures, sous-requêtes
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] TYPES DE BASES DE DONNÉES
# ----------------------------------------------------------------------------

"""
[DESIGN] VUE D'ENSEMBLE

┌─────────────────────────────────────────────────────────┐
│               BASES DE DONNÉES                          │
├──────────────────────┬──────────────────────────────────┤
│   RELATIONNELLES     │        NoSQL                     │
│      (SQL)           │                                  │
├──────────────────────┼──────────────────────────────────┤
│ - PostgreSQL         │ • Document : MongoDB             │
│ - MySQL/MariaDB      │ • Clé-Valeur : Redis             │
│ - SQLite             │ • Colonnes : Cassandra           │
│ - Oracle             │ • Graphes : Neo4j                │
│ - SQL Server         │ • Time-Series : InfluxDB         │
└──────────────────────┴──────────────────────────────────┘


1. BASES DE DONNÉES RELATIONNELLES (SQL)
─────────────────────────────────────────

[IDEE] PRINCIPE : Données organisées en TABLES (relations)

Caractéristiques :
[OK] Schéma fixe (structure définie)
[OK] Tables avec lignes et colonnes
[OK] Relations entre tables (foreign keys)
[OK] Langage SQL standardisé
[OK] ACID (Atomicity, Consistency, Isolation, Durability)


[DESIGN] EXEMPLE VISUEL :

Table : students
┌────┬──────────┬─────┬──────────────┬─────────────────┐
│ id │   name   │ age │    major     │      email      │
├────┼──────────┼─────┼──────────────┼─────────────────┤
│ 1  │ Alice    │ 20  │ Informatique │ alice@email.com │
│ 2  │ Bob      │ 22  │ Mathématiques│ bob@email.com   │
│ 3  │ Charlie  │ 19  │ Informatique │ charlie@em.com  │
└────┴──────────┴─────┴──────────────┴─────────────────┘

Table : courses
┌────┬────────────┬─────────┬──────────┐
│ id │    name    │ credits │ teacher  │
├────┼────────────┼─────────┼──────────┤
│ 1  │ Python     │    3    │ Prof. A  │
│ 2  │ Databases  │    4    │ Prof. B  │
└────┴────────────┴─────────┴──────────┘

Table : enrollments (relation students <-> courses)
┌────────────┬───────────┬────────┐
│ student_id │ course_id │ grade  │
├────────────┼───────────┼────────┤
│     1      │     1     │  A     │
│     1      │     2     │  B+    │
│     2      │     1     │  A-    │
└────────────┴───────────┴────────┘


[IDEE] QUAND UTILISER SQL ?

[OK] Données structurées et relationnelles
[OK] Besoin de transactions ACID
[OK] Requêtes complexes avec jointures
[OK] Intégrité des données cruciale
[OK] Reporting et analytics

EXEMPLES D'APPLICATIONS :
- Banques (transactions, comptes)
- E-commerce (produits, commandes, clients)
- CRM (gestion clients)
- ERP (gestion entreprise)
- Applications métier


2. BASES DE DONNÉES NoSQL
──────────────────────────

[IDEE] PRINCIPE : "Not Only SQL" - Alternatives au modèle relationnel


A. DOCUMENT STORES (MongoDB, CouchDB)
──────────────────────────────────────

Structure : Documents JSON/BSON
"""

# Exemple MongoDB
{
    "_id": "507f1f77bcf86cd799439011",
    "name": "Alice",
    "age": 20,
    "major": "Informatique",
    "courses": [
        {
            "name": "Python",
            "grade": "A",
            "credits": 3
        },
        {
            "name": "Databases", 
            "grade": "B+",
            "credits": 4
        }
    ],
    "address": {
        "street": "123 Main St",
        "city": "Paris"
    }
}

"""
[OK] AVANTAGES :
- Schéma flexible (pas de structure fixe)
- Documents imbriqués (évite jointures)
- Scalabilité horizontale facile
- Développement rapide

[X] INCONVÉNIENTS :
- Pas de jointures natives (ou limitées)
- Duplication de données
- Pas de transactions multi-documents (avant v4.0)

[IDEE] QUAND UTILISER ?
- Données semi-structurées ou changeantes
- Applications agiles (schéma évolutif)
- Hiérarchies naturelles (catalogues produits)

EXEMPLES :
- CMS (WordPress, Drupal)
- Catalogues e-commerce
- Profils utilisateurs
- Logs d'applications


B. CLÉ-VALEUR (Redis, Memcached)
─────────────────────────────────

Structure : Paires clé -> valeur
"""

# Exemple Redis
user:1001:name -> "Alice"
user:1001:age -> "20"
user:1001:email -> "alice@email.com"
session:abc123 -> {"user_id": 1001, "logged_in": true}

"""
[OK] AVANTAGES :
- Extrêmement rapide (en mémoire)
- Simple à utiliser
- Scalabilité horizontale

[X] INCONVÉNIENTS :
- Pas de requêtes complexes
- Pas de relations
- Limité en fonctionnalités

[IDEE] QUAND UTILISER ?
- Cache (sessions, résultats fréquents)
- Files d'attente (queues)
- Compteurs temps réel
- Rate limiting

EXEMPLES :
- Cache d'API
- Sessions utilisateurs
- Leaderboards jeux
- Compteurs de vues


C. COLONNES (Cassandra, HBase)
───────────────────────────────

Structure : Familles de colonnes

[DESIGN] VISUALISATION :

Row Key: user:1001
├─ personal:name -> "Alice"
├─ personal:age -> "20"
├─ contact:email -> "alice@email.com"
└─ contact:phone -> "+33123456789"

[OK] AVANTAGES :
- Écritures très rapides
- Scalabilité massive
- Tolérance aux pannes

[X] INCONVÉNIENTS :
- Requêtes limitées
- Complexe à configurer

[IDEE] QUAND UTILISER ?
- Big Data (téraoctets, pétaoctets)
- Séries temporelles massives
- Logs à grande échelle

EXEMPLES :
- IoT (millions de capteurs)
- Métriques/monitoring
- Historiques de transactions


D. GRAPHES (Neo4j, ArangoDB)
─────────────────────────────

Structure : Nœuds et relations

[DESIGN] VISUALISATION :

(Alice:Student) --[ENROLLED_IN]--> (Python:Course)
(Alice) --[FRIENDS_WITH]--> (Bob:Student)
(Bob) --[ENROLLED_IN]--> (Python)
(Python) --[TAUGHT_BY]--> (Prof_A:Teacher)

[OK] AVANTAGES :
- Relations complexes naturelles
- Requêtes de graphe efficaces
- Découverte de patterns

[X] INCONVÉNIENTS :
- Moins mature que SQL
- Scalabilité limitée

[IDEE] QUAND UTILISER ?
- Réseaux sociaux
- Recommandations
- Détection de fraude
- Graphes de connaissances

EXEMPLES :
- LinkedIn (connexions)
- Netflix (recommandations)
- Détection de fraudes bancaires


3. AUTRES TYPES SPÉCIALISÉS
────────────────────────────

TIME-SERIES (InfluxDB, TimescaleDB)
- Données horodatées
- Métriques, logs, IoT

FULL-TEXT SEARCH (Elasticsearch, Solr)
- Recherche textuelle avancée
- Moteurs de recherche

SPATIAL (PostGIS, MongoDB Geospatial)
- Données géographiques
- Applications de cartographie
"""


# ----------------------------------------------------------------------------
# [RECHERCHE] COMPARAISON SQL vs NoSQL
# ----------------------------------------------------------------------------

"""
[GRAPHIQUE] TABLEAU COMPARATIF DÉTAILLÉ

┌─────────────────┬──────────────────────┬──────────────────────┐
│  CRITÈRE        │        SQL           │       NoSQL          │
├─────────────────┼──────────────────────┼──────────────────────┤
│ Schéma          │ Fixe, prédéfini      │ Flexible, dynamique  │
│ Structure       │ Tables, lignes       │ Documents, KV, etc.  │
│ Relations       │ Foreign keys, JOIN   │ Imbrication, dénorm. │
│ Langage         │ SQL standardisé      │ API spécifique       │
│ Transactions    │ ACID natif           │ BASE (éventuel)      │
│ Scalabilité     │ Verticale (^ CPU)    │ Horizontale (+ nodes)│
│ Consistance     │ Forte (ACID)         │ Éventuelle (BASE)    │
│ Requêtes        │ Complexes (JOIN)     │ Simples (KV, doc)    │
│ Intégrité       │ Contraintes fortes   │ Application-level    │
│ Performance     │ Lecture optimisée    │ Écriture optimisée   │
│ Maturité        │ 40+ ans              │ 10-15 ans            │
│ Courbe appr.    │ Moyenne              │ Facile (début)       │
└─────────────────┴──────────────────────┴──────────────────────┘


[IDEE] ACID vs BASE

ACID (SQL) :
A - Atomicity     : Transaction tout ou rien
C - Consistency   : Données toujours valides
I - Isolation     : Transactions indépendantes
D - Durability    : Données persistantes

BASE (NoSQL) :
BA - Basically Available : Toujours disponible
S  - Soft state          : État peut changer
E  - Eventually consistent : Cohérence à terme


[OBJECTIF] GUIDE DE CHOIX

CHOISIR SQL SI :
[OK] Données structurées et relationnelles
[OK] Besoin de transactions (banque, e-commerce)
[OK] Requêtes complexes fréquentes
[OK] Intégrité données cruciale
[OK] Reporting et analytics
[OK] Équipe familière avec SQL

CHOISIR NoSQL SI :
[OK] Schéma changeant fréquemment
[OK] Scalabilité horizontale nécessaire
[OK] Données semi-structurées
[OK] Performance écriture critique
[OK] Big Data (téraoctets+)
[OK] Développement agile rapide


[HOT] TENDANCE MODERNE : POLYGLOT PERSISTENCE

Ne PAS choisir une seule DB, mais utiliser plusieurs !

Exemple Architecture :
┌─────────────────────────────────────────┐
│          Application Web                │
├─────────────────────────────────────────┤
│ PostgreSQL  -> Données transactionnelles │
│ Redis       -> Cache et sessions         │
│ Elasticsearch -> Recherche full-text    │
│ MongoDB     -> Logs et analytics         │
└─────────────────────────────────────────┘
"""


# ----------------------------------------------------------------------------
# [ARCHIVE] PRINCIPAUX SGBD (Systèmes de Gestion de Bases de Données)
# ----------------------------------------------------------------------------

"""
1. SQLite
─────────

Type : SQL, Relationnel
Modèle : Embarqué (fichier unique)

[OK] AVANTAGES :
- Aucune installation (inclus Python)
- Fichier unique (.db)
- Parfait pour développement
- Léger et rapide
- Zéro configuration
- Cross-platform

[X] LIMITATIONS :
- Pas de concurrent writes (un seul écriture à la fois)
- Pas de gestion utilisateurs
- Fonctionnalités limitées vs PostgreSQL
- Pas pour production haute charge

[IDEE] QUAND UTILISER ?
[OK] Développement et prototypage
[OK] Applications desktop
[OK] Applications mobiles
[OK] Petites applications web
[OK] Tests et apprentissage
[OK] Embedded systems

[X] NE PAS UTILISER SI :
[X] Haute concurrence (1000+ users)
[X] Base > 100 GB
[X] Besoins enterprise (réplication, etc.)


2. PostgreSQL
─────────────

Type : SQL, Relationnel, Open Source
Modèle : Client-Serveur

[OK] AVANTAGES :
- Le plus conforme aux standards SQL
- Très riche en fonctionnalités
- Extensions (PostGIS, Full-text search)
- Excellent pour JSON (JSONB)
- Performance excellente
- Gratuit et open source
- Communauté active

[X] INCONVÉNIENTS :
- Plus complexe à configurer
- Courbe d'apprentissage moyenne

[IDEE] QUAND UTILISER ?
[OK] Production (toutes tailles)
[OK] Applications complexes
[OK] Données géospatiales (avec PostGIS)
[OK] Besoins analytics
[OK] Applications critiques

EXEMPLES :
- Instagram, Spotify, Uber, Reddit


3. MySQL / MariaDB
──────────────────

Type : SQL, Relationnel, Open Source
Modèle : Client-Serveur

[OK] AVANTAGES :
- Très populaire (énorme communauté)
- Facile à apprendre
- Performance excellente (lecture)
- Gratuit
- Nombreux hébergeurs

[X] INCONVÉNIENTS :
- Moins de fonctionnalités que PostgreSQL
- Conformité SQL moyenne
- MariaDB fork de MySQL (historique Oracle)

[IDEE] QUAND UTILISER ?
[OK] Applications web classiques
[OK] WordPress, Drupal, etc.
[OK] Startups (familiarité, communauté)

EXEMPLES :
- Facebook (historiquement), YouTube, Twitter


4. MongoDB
──────────

Type : NoSQL, Document
Modèle : Client-Serveur

[OK] AVANTAGES :
- Schéma flexible (JSON)
- Scalabilité horizontale facile
- Développement rapide
- Requêtes puissantes
- Gratuit (Community Edition)

[X] INCONVÉNIENTS :
- Pas de transactions multi-documents (< v4.0)
- Duplication de données
- Consommation mémoire élevée

[IDEE] QUAND UTILISER ?
[OK] Prototypage rapide
[OK] Données semi-structurées
[OK] Catalogues produits
[OK] CMS, blogs

EXEMPLES :
- eBay, Uber, Adobe


5. Redis
────────

Type : NoSQL, Clé-Valeur
Modèle : In-Memory

[OK] AVANTAGES :
- Extrêmement rapide (microseconde)
- Structures de données riches
- Pub/Sub
- Persistence optionnelle

[X] INCONVÉNIENTS :
- Limité en RAM
- Pas de requêtes complexes

[IDEE] QUAND UTILISER ?
[OK] Cache applicatif
[OK] Sessions
[OK] Queues
[OK] Real-time

EXEMPLES :
- Twitter, GitHub, Stack Overflow


[GRAPHIQUE] POPULARITÉ ET USAGE (2024)

1. Oracle (Enterprise)      - $$$
2. MySQL                    - Gratuit / $
3. SQL Server (Microsoft)   - $$$
4. PostgreSQL               - Gratuit
5. MongoDB                  - Gratuit / $
6. Redis                    - Gratuit / $
7. SQLite                   - Gratuit
8. Cassandra                - Gratuit
9. MariaDB                  - Gratuit
10. Elasticsearch           - Gratuit / $


[IDEE] POUR CE GUIDE, NOUS UTILISERONS :

1. SQLite (Chapitres 1-12)
   -> Apprentissage, pas d'installation
   
2. PostgreSQL (Chapitres 13-16)
   -> Production, fonctionnalités avancées
   
3. MongoDB (Chapitre 20)
   -> Perspective NoSQL
"""


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRÉPARATOIRE
# ----------------------------------------------------------------------------

"""
AVANT DE CONTINUER

[OK] 1. VÉRIFIER PYTHON
"""
import sys
print(f"Python version: {sys.version}")
# Doit afficher 3.8+

"""
[OK] 2. TESTER SQLite (inclus avec Python)
"""
import sqlite3
print(f"SQLite version: {sqlite3.sqlite_version}")
# Doit afficher version SQLite

"""
[OK] 3. CRÉER VOTRE PREMIÈRE BASE DE DONNÉES
"""

# Créer connexion (crée le fichier si n'existe pas)
conn = sqlite3.connect('databases/test.db')

# Créer curseur
cursor = conn.cursor()

# Créer table
cursor.execute("""
    CREATE TABLE IF NOT EXISTS test (
        id INTEGER PRIMARY KEY,
        message TEXT
    )
""")

# Insérer données
cursor.execute("INSERT INTO test (message) VALUES ('Hello Database!')")

# Valider
conn.commit()

# Lire
cursor.execute("SELECT * FROM test")
print(cursor.fetchall())
# Doit afficher : [(1, 'Hello Database!')]

# Fermer
conn.close()

print("[OK] Tout fonctionne ! Vous êtes prêt ! [RAPIDE]")

"""
Si tout fonctionne, passez au Chapitre 1 !

[DOCS] RÉCAPITULATIF CHAPITRE 0

[OK] Vous savez maintenant :
- Pourquoi les bases de données sont essentielles
- Différence entre SQL et NoSQL
- Principaux SGBD et leurs usages
- Quand utiliser quel type de DB
- Votre environnement est prêt !

-> PROCHAINE ÉTAPE : Chapitre 1 - Concepts Fondamentaux

[CLE] POINTS CLÉS À RETENIR :
1. Base de données = Stockage organisé et efficace
2. SQL = Relationnel, structuré, ACID
3. NoSQL = Flexible, scalable, BASE
4. SQLite = Parfait pour apprendre
5. Utilisez le bon outil pour le bon usage !
"""


# ============================================================================
# [GUIDE] CHAPITRE 1 : CONCEPTS FONDAMENTAUX
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre les concepts clés (table, ligne, colonne, clé)
[OK] Différence entre base de données et SGBD
[OK] Rôle d'un schéma
[OK] Qu'est-ce qu'une requête SQL
[OK] Vocabulaire essentiel des bases de données
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] STRUCTURE D'UNE BASE DE DONNÉES RELATIONNELLE
# ----------------------------------------------------------------------------

"""
[DESIGN] HIÉRARCHIE COMPLÈTE

BASE DE DONNÉES (Database)
    │
    ├── TABLE 1 (Table / Relation)
    │   ├── COLONNE 1 (Column / Attribute / Field)
    │   ├── COLONNE 2
    │   ├── COLONNE 3
    │   │
    │   ├── LIGNE 1 (Row / Record / Tuple)
    │   ├── LIGNE 2
    │   └── LIGNE 3
    │
    ├── TABLE 2
    │   └── ...
    │
    └── TABLE 3
        └── ...


[IDEE] DÉTAILLONS CHAQUE NIVEAU


1. BASE DE DONNÉES (Database)
──────────────────────────────

[REFLEXION] QU'EST-CE QUE C'EST ?
Container qui regroupe toutes les tables liées à une application

ANALOGIE : Base de données = Application complète
- Base "school" -> Toutes les données de l'école
- Base "ecommerce" -> Tous les produits, commandes, clients

EXEMPLE :
"""

# Une base de données "school" contient :
school_database = {
    'tables': ['students', 'courses', 'teachers', 'enrollments', 'grades']
}

"""
[IDEE] UN SGBD peut gérer PLUSIEURS bases de données :

PostgreSQL Server :
├── Database: school
├── Database: ecommerce
├── Database: blog
└── Database: analytics


2. TABLE (Table / Relation)
────────────────────────────

[REFLEXION] QU'EST-CE QUE C'EST ?
Structure qui stocke un type spécifique de données

ANALOGIE : Table = Feuille Excel

[DESIGN] VISUALISATION :

Table: students
┌────┬──────────┬─────┬──────────────┬─────────────────┐
│ id │   name   │ age │    major     │      email      │ <- Colonnes
├────┼──────────┼─────┼──────────────┼─────────────────┤
│ 1  │ Alice    │ 20  │ Informatique │ alice@email.com │ <- Ligne 1
│ 2  │ Bob      │ 22  │ Mathématiques│ bob@email.com   │ <- Ligne 2
│ 3  │ Charlie  │ 19  │ Informatique │ charlie@em.com  │ <- Ligne 3
└────┴──────────┴─────┴──────────────┴─────────────────┘
  ^       ^       ^         ^              ^
Cellules individuelles (intersection ligne/colonne)


[IDEE] RÈGLES IMPORTANTES :

1. Nom de table UNIQUE dans la base
2. Contient des LIGNES (enregistrements)
3. Structure définie par des COLONNES
4. Une table = un "type" d'entité (étudiants, cours, etc.)


[CLE] BONNES PRATIQUES NOMMAGE :

[OK] BON :
- Pluriel : students, courses, orders
- Minuscules : students (pas Students)
- Snake_case : student_courses
- Descriptif : user_profiles

[X] MAUVAIS :
- Singulier : student
- Espaces : student courses
- Mots réservés : select, table


3. COLONNE (Column / Attribute / Field)
────────────────────────────────────────

[REFLEXION] QU'EST-CE QUE C'EST ?
Caractéristique/propriété des données stockées

ANALOGIE : Colonne = Champ d'un formulaire

[DESIGN] EXEMPLE :

Table students, colonne "name" :
┌────┬──────────┐
│ id │   name   │ <- Colonne
├────┼──────────┤
│ 1  │ Alice    │
│ 2  │ Bob      │
│ 3  │ Charlie  │
└────┴──────────┘


[IDEE] CARACTÉRISTIQUES D'UNE COLONNE :

1. NOM : Identifiant unique dans la table
2. TYPE : Type de données (INTEGER, TEXT, etc.)
3. CONTRAINTES : Règles (NOT NULL, UNIQUE, etc.)

Exemple SQL :
"""

CREATE TABLE students (
    id INTEGER PRIMARY KEY,           -- Colonne 1
    name TEXT NOT NULL,               -- Colonne 2
    age INTEGER CHECK(age >= 0),      -- Colonne 3
    major TEXT,                       -- Colonne 4
    email TEXT UNIQUE                 -- Colonne 5
);

"""
[CLE] BONNES PRATIQUES :

[OK] BON :
- snake_case : first_name, email_address
- Descriptif : birth_date (pas bd)
- Singulier : name (pas names dans une table students)

[X] MAUVAIS :
- camelCase : firstName
- Abréviations obscures : fn, em
- Mots réservés : date, time


4. LIGNE (Row / Record / Tuple / Enregistrement)
─────────────────────────────────────────────────

[REFLEXION] QU'EST-CE QUE C'EST ?
Une entrée complète de données dans la table

ANALOGIE : Ligne = Fiche individuelle

[DESIGN] EXEMPLE :

Une ligne dans students :
┌────┬──────────┬─────┬──────────────┬─────────────────┐
│ id │   name   │ age │    major     │      email      │
├────┼──────────┼─────┼──────────────┼─────────────────┤
│ 1  │ Alice    │ 20  │ Informatique │ alice@email.com │ <- UNE ligne
└────┴──────────┴─────┴──────────────┴─────────────────┘

Représente UN étudiant avec toutes ses infos


[IDEE] LIGNE = INSTANCE D'ENTITÉ

Table students (le concept) :
- Colonne id
- Colonne name
- Colonne age
...

Ligne (instance concrète) :
- id = 1
- name = "Alice"
- age = 20
...


5. CELLULE (Cell / Value)
──────────────────────────

[REFLEXION] QU'EST-CE QUE C'EST ?
Intersection d'une ligne et d'une colonne = une valeur unique

[DESIGN] EXEMPLE :

┌────┬──────────┬─────┐
│ id │   name   │ age │
├────┼──────────┼─────┤
│ 1  │ Alice    │ 20  │ <- Ligne 1
│                   ^
│                Cellule (ligne 1, colonne age) = 20
└────┴──────────┴─────┘
"""


# ----------------------------------------------------------------------------
# [CLE] CLÉS (KEYS) - CONCEPT ESSENTIEL
# ----------------------------------------------------------------------------

"""
Les clés sont FONDAMENTALES pour comprendre les bases de données !


1. CLÉ PRIMAIRE (Primary Key / PK)
───────────────────────────────────

[REFLEXION] QU'EST-CE QUE C'EST ?
Colonne (ou ensemble de colonnes) qui identifie UNIQUEMENT chaque ligne

[IDEE] ANALOGIE : Numéro de sécurité sociale
- Chaque personne a un numéro UNIQUE
- Impossible d'avoir deux personnes avec le même numéro

[DESIGN] EXEMPLE :

┌────┬──────────┬─────┐
│ id │   name   │ age │ <- id = clé primaire
├────┼──────────┼─────┤
│ 1  │ Alice    │ 20  │
│ 2  │ Bob      │ 22  │
│ 3  │ Charlie  │ 19  │
└────┴──────────┴─────┘

id = 1 identifie UNIQUEMENT Alice
id = 2 identifie UNIQUEMENT Bob


[CLE] RÈGLES CLÉS PRIMAIRES :

1. UNIQUE : Chaque valeur est différente
2. NOT NULL : Ne peut jamais être vide
3. IMMUTABLE : Ne change jamais (bonne pratique)
4. UNE SEULE par table (ou composite)


[OK] BONNES CLÉS PRIMAIRES :

1. ENTIER AUTO-INCRÉMENTÉ (le plus courant)
"""

CREATE TABLE students (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT
);

# SQLite auto-incrémente :
# Ligne 1 -> id = 1
# Ligne 2 -> id = 2
# Ligne 3 -> id = 3 (automatique !)

"""
2. UUID (Universally Unique Identifier)
"""

CREATE TABLE students (
    id TEXT PRIMARY KEY,  -- UUID comme "550e8400-e29b-41d4-a716-446655440000"
    name TEXT
);

# Avantages :
# [OK] Unique globalement (pas de collision)
# [OK] Générable côté application
# [OK] Parfait pour systèmes distribués

"""
3. CLÉ COMPOSITE (plusieurs colonnes)
"""

CREATE TABLE enrollments (
    student_id INTEGER,
    course_id INTEGER,
    grade TEXT,
    PRIMARY KEY (student_id, course_id)
);

# Combinaison (student_id, course_id) est unique
# Étudiant 1 peut être dans cours 1
# Étudiant 1 peut être dans cours 2
# MAIS étudiant 1 ne peut pas être 2 fois dans cours 1


[X] MAUVAISES CLÉS PRIMAIRES :

[X] Email comme PK
CREATE TABLE users (
    email TEXT PRIMARY KEY,  -- [X] Email peut changer !
    name TEXT
);

[X] Nom comme PK
CREATE TABLE users (
    name TEXT PRIMARY KEY,  -- [X] Homonymes possibles !
    age INTEGER
);

[X] Combinaison sémantique
CREATE TABLE users (
    first_name TEXT,
    last_name TEXT,
    PRIMARY KEY (first_name, last_name)  -- [X] Homonymes !
);


2. CLÉ ÉTRANGÈRE (Foreign Key / FK)
────────────────────────────────────

[REFLEXION] QU'EST-CE QUE C'EST ?
Colonne qui RÉFÉRENCE la clé primaire d'une AUTRE table
-> Crée une RELATION entre deux tables

[IDEE] ANALOGIE : Référence bibliographique
- Un article cite un livre
- La citation = clé étrangère vers le livre

[DESIGN] EXEMPLE :

Table: students
┌────┬──────────┐
│ id │   name   │ <- id = Primary Key
├────┼──────────┤
│ 1  │ Alice    │
│ 2  │ Bob      │
└────┴──────────┘

Table: enrollments
┌────────────┬───────────┬────────┐
│ student_id │ course_id │ grade  │
├────────────┼───────────┼────────┤
│     1      │     10    │  A     │ <- student_id = 1 référence Alice
│     2      │     10    │  B+    │ <- student_id = 2 référence Bob
│     1      │     20    │  A-    │ <- student_id = 1 référence Alice
└────────────┴───────────┴────────┘
       ^
   Foreign Key vers students.id


Code SQL :
"""

CREATE TABLE students (
    id INTEGER PRIMARY KEY,
    name TEXT
);

CREATE TABLE enrollments (
    id INTEGER PRIMARY KEY,
    student_id INTEGER,
    course_id INTEGER,
    grade TEXT,
    FOREIGN KEY (student_id) REFERENCES students(id)
    --              ^                               ^
    --         Colonne FK                      Table.Colonne référencée
);

"""
[IDEE] QUE FAIT UNE FOREIGN KEY ?

1. INTÉGRITÉ RÉFÉRENTIELLE
   [X] Impossible d'ajouter enrollment avec student_id = 999 si étudiant 999 n'existe pas
   
2. CASCADE OPTIONS (actions automatiques)
"""

CREATE TABLE enrollments (
    student_id INTEGER,
    FOREIGN KEY (student_id) REFERENCES students(id)
        ON DELETE CASCADE      -- Si étudiant supprimé -> ses enrollments aussi
        ON UPDATE CASCADE      -- Si id étudiant change -> mise à jour auto
);

"""
OPTIONS ON DELETE :
- CASCADE : Supprime lignes dépendantes
- SET NULL : Met FK à NULL
- SET DEFAULT : Met FK à valeur par défaut
- RESTRICT : Empêche suppression (erreur)
- NO ACTION : Idem RESTRICT


3. CLÉ UNIQUE (Unique Key)
───────────────────────────

[REFLEXION] QU'EST-CE QUE C'EST ?
Colonne avec valeurs UNIQUES (mais peut être NULL)

Différence avec Primary Key :
- PK : Unique + NOT NULL + UNE SEULE par table
- Unique : Unique + peut être NULL + PLUSIEURS par table
"""

CREATE TABLE users (
    id INTEGER PRIMARY KEY,        -- Unique + NOT NULL
    email TEXT UNIQUE,             -- Unique mais peut être NULL
    username TEXT UNIQUE,          -- Autre colonne unique
    phone TEXT UNIQUE              -- Encore une autre
);

"""
[DESIGN] EXEMPLE :

┌────┬────────────┬──────────────────┬──────────────┐
│ id │  username  │      email       │    phone     │
├────┼────────────┼──────────────────┼──────────────┤
│ 1  │ alice      │ alice@email.com  │ +33123456789 │
│ 2  │ bob        │ bob@email.com    │ +33987654321 │
│ 3  │ charlie    │ NULL             │ NULL         │ <- OK : NULL autorisé
└────┴────────────┴──────────────────┴──────────────┘

[X] IMPOSSIBLE :
- Ajouter username = "alice" (déjà pris)
- Ajouter email = "alice@email.com" (déjà pris)

[OK] POSSIBLE :
- Plusieurs NULL dans email
- Plusieurs NULL dans phone


4. INDEX (Index)
────────────────

[REFLEXION] QU'EST-CE QUE C'EST ?
Structure de données pour accélérer les recherches

[IDEE] ANALOGIE : Index d'un livre
- Sans index : lire TOUT le livre pour trouver "Python"
- Avec index : aller directement à la page

[DESIGN] IMPACT PERFORMANCE :

Sans index :
SELECT * FROM students WHERE name = 'Alice';
-> Parcourt TOUTES les lignes (O(n))
-> 1 million de lignes = 1 million de comparaisons

Avec index sur "name" :
SELECT * FROM students WHERE name = 'Alice';
-> Recherche dans index (O(log n))
-> 1 million de lignes = ~20 comparaisons

Code :
"""

CREATE INDEX idx_students_name ON students(name);

"""
[ATTENTION] TRADE-OFFS :

[OK] AVANTAGES :
- Recherches très rapides
- ORDER BY rapide
- JOIN rapide

[X] INCONVÉNIENTS :
- Prend de l'espace disque
- Ralentit INSERT/UPDATE/DELETE (index doit être mis à jour)


[IDEE] QUAND CRÉER UN INDEX ?

[OK] OUI :
- Colonnes souvent dans WHERE
- Colonnes dans JOIN
- Colonnes dans ORDER BY
- Foreign keys

[X] NON :
- Tables petites (< 1000 lignes)
- Colonnes rarement utilisées
- Colonnes avec peu de valeurs distinctes (ex: genre M/F)
"""


# ----------------------------------------------------------------------------
# [DESIGN] SCHÉMA DE BASE DE DONNÉES
# ----------------------------------------------------------------------------

"""
[REFLEXION] QU'EST-CE QU'UN SCHÉMA ?

SCHÉMA = Structure/Plan de la base de données
- Définit les tables
- Définit les colonnes et types
- Définit les relations (FK)
- Définit les contraintes


[DESIGN] EXEMPLE COMPLET : Base de données ÉCOLE

Table: students
┌─────────────┬──────────┬──────────┬─────────┐
│   Colonne   │   Type   │ Nullable │  Clé    │
├─────────────┼──────────┼──────────┼─────────┤
│ id          │ INTEGER  │ NOT NULL │ PK      │
│ first_name  │ TEXT     │ NOT NULL │         │
│ last_name   │ TEXT     │ NOT NULL │         │
│ email       │ TEXT     │ NOT NULL │ UNIQUE  │
│ birth_date  │ DATE     │ NULL     │         │
│ major_id    │ INTEGER  │ NULL     │ FK->major│
└─────────────┴──────────┴──────────┴─────────┘

Table: majors
┌─────────┬──────────┬──────────┬─────┐
│ Colonne │   Type   │ Nullable │ Clé │
├─────────┼──────────┼──────────┼─────┤
│ id      │ INTEGER  │ NOT NULL │ PK  │
│ name    │ TEXT     │ NOT NULL │ UN  │
│ credits │ INTEGER  │ NOT NULL │     │
└─────────┴──────────┴──────────┴─────┘

Table: courses
┌─────────────┬──────────┬──────────┬─────┐
│   Colonne   │   Type   │ Nullable │ Clé │
├─────────────┼──────────┼──────────┼─────┤
│ id          │ INTEGER  │ NOT NULL │ PK  │
│ name        │ TEXT     │ NOT NULL │     │
│ credits     │ INTEGER  │ NOT NULL │     │
│ teacher_id  │ INTEGER  │ NULL     │ FK  │
└─────────────┴──────────┴──────────┴─────┘

Table: enrollments (table de liaison)
┌─────────────┬──────────┬──────────┬──────────────┐
│   Colonne   │   Type   │ Nullable │     Clé      │
├─────────────┼──────────┼──────────┼──────────────┤
│ id          │ INTEGER  │ NOT NULL │ PK           │
│ student_id  │ INTEGER  │ NOT NULL │ FK->students  │
│ course_id   │ INTEGER  │ NOT NULL │ FK->courses   │
│ grade       │ TEXT     │ NULL     │              │
│ semester    │ TEXT     │ NOT NULL │              │
└─────────────┴──────────┴──────────┴──────────────┘


Code SQL complet :
"""

CREATE TABLE majors (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    credits INTEGER NOT NULL CHECK(credits > 0)
);

CREATE TABLE students (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    first_name TEXT NOT NULL,
    last_name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    birth_date DATE,
    major_id INTEGER,
    FOREIGN KEY (major_id) REFERENCES majors(id)
);

CREATE TABLE teachers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    department TEXT
);

CREATE TABLE courses (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    credits INTEGER NOT NULL,
    teacher_id INTEGER,
    FOREIGN KEY (teacher_id) REFERENCES teachers(id)
);

CREATE TABLE enrollments (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    student_id INTEGER NOT NULL,
    course_id INTEGER NOT NULL,
    grade TEXT,
    semester TEXT NOT NULL,
    FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
    FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
    UNIQUE(student_id, course_id, semester)  -- Un étudiant ne peut s'inscrire 2 fois au même cours le même semestre
);

"""
[DESIGN] DIAGRAMME DES RELATIONS :

    majors
      │
      │ 1
      │
      v N
   students
      │
      │ N
      │
      v
enrollments <- N -> courses
      │              │
      v              v N
   (grades)      teachers
                     1

Légende :
1 = Un
N = Plusieurs
-> = Relation


[IDEE] TYPES DE RELATIONS :

1. One-to-Many (1-N) : Un major -> Plusieurs students
2. Many-to-Many (N-N) : Plusieurs students <-> Plusieurs courses
   -> Nécessite table intermédiaire (enrollments)
3. One-to-One (1-1) : Un user -> Un profile (rare)
"""


# ----------------------------------------------------------------------------
# [DOCS] VOCABULAIRE ESSENTIEL
# ----------------------------------------------------------------------------

"""
[CLE] TERMES À CONNAÎTRE ABSOLUMENT


BASE DE DONNÉES (Database / DB)
────────────────────────────────
Container de tables reliées

Synonymes : DB, Base


SGBD (Système de Gestion de Bases de Données)
──────────────────────────────────────────────
Logiciel qui gère les bases de données

Anglais : DBMS (Database Management System)

Exemples : SQLite, PostgreSQL, MySQL, MongoDB


TABLE (Table / Relation)
────────────────────────
Structure qui stocke des données

Synonymes : Relation (terme mathématique)


COLONNE (Column / Attribute / Field)
────────────────────────────────────
Caractéristique des données

Synonymes : 
- Attribute (terme relationnel)
- Field (terme pratique)
- Champ (français)


LIGNE (Row / Record / Tuple)
─────────────────────────────
Entrée de données complète

Synonymes :
- Record (enregistrement)
- Tuple (terme mathématique)
- Entry (entrée)


REQUÊTE (Query)
───────────────
Instruction pour interagir avec la DB

Types :
- SELECT : Lire
- INSERT : Créer
- UPDATE : Modifier
- DELETE : Supprimer


SCHÉMA (Schema)
───────────────
Structure de la base de données

Définit :
- Tables
- Colonnes
- Types
- Relations
- Contraintes


CONTRAINTE (Constraint)
───────────────────────
Règle sur les données

Types :
- PRIMARY KEY
- FOREIGN KEY
- UNIQUE
- NOT NULL
- CHECK
- DEFAULT


TRANSACTION
───────────
Ensemble d'opérations atomiques

Tout réussit OU tout échoue

Exemple :
- Débiter compte A
- Créditer compte B
-> Les deux ensemble ou rien


INDEX
─────
Structure pour accélérer recherches

Comme l'index d'un livre


VUE (View)
──────────
Table "virtuelle" basée sur requête

Pas de données stockées, juste une requête


PROCÉDURE STOCKÉE (Stored Procedure)
─────────────────────────────────────
Code SQL sauvegardé et réutilisable

Comme une fonction


TRIGGER (Déclencheur)
─────────────────────
Action automatique sur événement

Exemple : Log à chaque INSERT


JOINTURE (JOIN)
───────────────
Combiner données de plusieurs tables

Types : INNER, LEFT, RIGHT, FULL


NORMALISATION
──────────────
Processus d'organisation optimale

Évite redondance


DÉNORMALISATION
────────────────
Ajout volontaire de redondance

Pour performance
"""


# ============================================================================
# [DOCS] RÉCAPITULATIF CHAPITRE 1
# ============================================================================

"""
[OK] VOUS SAVEZ MAINTENANT :

STRUCTURE :
- Base de données -> Tables -> Colonnes + Lignes
- Table = Structure, Ligne = Données
- Cellule = Intersection ligne/colonne

CLÉS :
- Primary Key : Identifie UNIQUEMENT chaque ligne
- Foreign Key : Référence vers autre table (relation)
- Unique Key : Valeurs uniques (mais peut être NULL)
- Index : Accélère recherches

SCHÉMA :
- Définit structure complète
- Tables, colonnes, types, relations
- Blueprint de la base de données

VOCABULAIRE :
- Tous les termes essentiels
- Synonymes courants
- Anglais <-> Français


[CLE] POINTS CLÉS À RETENIR :

1. Table = Type d'entité (students, courses)
2. Primary Key = Identifiant unique (id)
3. Foreign Key = Lien entre tables (relation)
4. Schéma = Structure complète de la DB
5. Contraintes = Règles sur les données


[COURS] MINI-EXERCICE :

Concevez mentalement le schéma pour une bibliothèque :
- Quelles tables ? (livres, auteurs, emprunts...)
- Quelles colonnes ?
- Quelles relations ?
- Quelles clés ?

(Solution au Chapitre 9 sur la modélisation)


-> PROCHAINE ÉTAPE : Chapitre 2 - SQL de Base

Vous allez apprendre :
- SELECT : Lire des données
- INSERT : Créer des données
- UPDATE : Modifier des données
- DELETE : Supprimer des données

C'est là que ça devient CONCRET ! [RAPIDE]
"""


# ============================================================================
# CE FICHIER CONTINUE...
# MAIS POUR NE PAS DÉPASSER LES LIMITES, JE VAIS CRÉER LES CHAPITRES 2-4
# DANS UN FICHIER SÉPARÉ
# ============================================================================

# FIN DE database_partie1.txt - INTRODUCTION ET CONCEPTS
# CONTINUEZ AVEC database_partie1_suite.txt pour les Chapitres 2-4

# ============================================================================
# [LIVRE] BASES DE DONNÉES - PARTIE 1 (SUITE) : SQL DE BASE
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 2 : SQL de Base (SELECT, INSERT, UPDATE, DELETE)
# - Chapitre 3 : Types de Données
# - Chapitre 4 : Contraintes et Clés
#
# [TEMPS] TEMPS : ~4-6 heures
# [DOCS] PRÉREQUIS : Chapitre 0 et 1 complétés
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 2 : SQL DE BASE - CRUD
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer des tables (CREATE TABLE)
[OK] Insérer des données (INSERT)
[OK] Lire des données (SELECT)
[OK] Modifier des données (UPDATE)
[OK] Supprimer des données (DELETE)
[OK] Filtrer, trier et limiter les résultats
[OK] Écrire des requêtes SQL propres et efficaces
"""


# ----------------------------------------------------------------------------
# [DEMARRAGE] PRÉPARATION : CRÉER NOTRE BASE DE DONNÉES D'EXEMPLE
# ----------------------------------------------------------------------------

"""
Nous allons créer une base de données "school" pour tous nos exemples.
"""

import sqlite3

# Connexion à la base de données
conn = sqlite3.connect('databases/school.db')
cursor = conn.cursor()

print("[OK] Base de données 'school.db' créée !")

"""
[IDEE] QUE SE PASSE-T-IL ?

1. sqlite3.connect('databases/school.db')
   - Si le fichier existe -> ouvre connexion
   - Si le fichier n'existe pas -> crée le fichier

2. cursor = conn.cursor()
   - Curseur = objet pour exécuter des requêtes SQL
   - Comme un pointeur qui parcourt les résultats


[CLE] PATTERN DE BASE :

1. Connexion : conn = sqlite3.connect('file.db')
2. Curseur : cursor = conn.cursor()
3. Exécution : cursor.execute("SQL")
4. Validation : conn.commit() (pour INSERT/UPDATE/DELETE)
5. Lecture : cursor.fetchall() / fetchone() (pour SELECT)
6. Fermeture : conn.close()
"""


# ----------------------------------------------------------------------------
# [CONSTRUCTION] CREATE TABLE - CRÉER UNE TABLE
# ----------------------------------------------------------------------------

"""
SYNTAXE GÉNÉRALE :

CREATE TABLE nom_table (
    colonne1 TYPE contraintes,
    colonne2 TYPE contraintes,
    ...
);


[IDEE] EXEMPLE SIMPLE :
"""

cursor.execute("""
    CREATE TABLE students (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        age INTEGER,
        email TEXT UNIQUE
    )
""")

conn.commit()
print("[OK] Table 'students' créée !")

"""
[IDEE] DÉCORTIQUONS :

CREATE TABLE students
    Créer une table nommée "students"

id INTEGER PRIMARY KEY AUTOINCREMENT
    - id : nom de la colonne
    - INTEGER : type entier
    - PRIMARY KEY : clé primaire
    - AUTOINCREMENT : SQLite incrémente automatiquement
      (1, 2, 3, 4, ...)

name TEXT NOT NULL
    - name : nom de la colonne
    - TEXT : type texte
    - NOT NULL : ne peut pas être vide

age INTEGER
    - age : nom de la colonne
    - INTEGER : type entier
    - (pas de contrainte -> peut être NULL)

email TEXT UNIQUE
    - email : nom de la colonne
    - TEXT : type texte
    - UNIQUE : chaque email doit être différent


[CLE] BONNES PRATIQUES :

[OK] Utilisez des noms descriptifs
[OK] PRIMARY KEY sur chaque table
[OK] AUTOINCREMENT pour IDs
[OK] NOT NULL sur colonnes essentielles
[OK] UNIQUE sur colonnes qui doivent être distinctes
"""


"""
EXEMPLE COMPLET : Plusieurs tables liées
"""

# Table majors
cursor.execute("""
    CREATE TABLE IF NOT EXISTS majors (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL UNIQUE,
        credits INTEGER NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
""")

# Table courses
cursor.execute("""
    CREATE TABLE IF NOT EXISTS courses (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        code TEXT NOT NULL UNIQUE,
        name TEXT NOT NULL,
        credits INTEGER NOT NULL CHECK(credits > 0),
        description TEXT
    )
""")

# Table enrollments (table de liaison)
cursor.execute("""
    CREATE TABLE IF NOT EXISTS enrollments (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        student_id INTEGER NOT NULL,
        course_id INTEGER NOT NULL,
        grade TEXT,
        enrollment_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
        FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
        UNIQUE(student_id, course_id)
    )
""")

conn.commit()
print("[OK] Toutes les tables créées !")

"""
[IDEE] NOUVEAUX ÉLÉMENTS :

IF NOT EXISTS
    Crée seulement si la table n'existe pas déjà
    Évite erreur si on exécute le script plusieurs fois

DEFAULT CURRENT_TIMESTAMP
    Valeur par défaut = date/heure actuelle

CHECK(credits > 0)
    Contrainte : credits doit être > 0

ON DELETE CASCADE
    Si étudiant supprimé -> ses enrollments aussi supprimés


[ATTENTION] ORDRE DE CRÉATION IMPORTANT !

Si table A référence table B (foreign key) :
-> Créer B AVANT A !

students <- enrollments (FK vers students)
         v
    Créer students AVANT enrollments
"""


# ----------------------------------------------------------------------------
# + INSERT - INSÉRER DES DONNÉES
# ----------------------------------------------------------------------------

"""
SYNTAXE 1 : Insérer une ligne
"""

cursor.execute("""
    INSERT INTO students (name, age, email)
    VALUES ('Alice Dupont', 20, 'alice@email.com')
""")

conn.commit()
print("[OK] Alice insérée !")

"""
[IDEE] DÉCORTIQUONS :

INSERT INTO students
    Insérer dans la table "students"

(name, age, email)
    Colonnes concernées (id omis car AUTOINCREMENT)

VALUES ('Alice Dupont', 20, 'alice@email.com')
    Valeurs correspondantes
    - 'Alice Dupont' -> name
    - 20 -> age
    - 'alice@email.com' -> email


[CLE] IMPORTANT :

1. Ordre des VALUES doit correspondre aux colonnes
2. id pas spécifié -> généré automatiquement
3. Texte entre guillemets simples : 'text'
4. Nombres sans guillemets : 20
5. NULL pour valeur nulle : NULL
"""


"""
SYNTAXE 2 : Insérer plusieurs lignes en une fois
"""

students_data = [
    ('Bob Martin', 22, 'bob@email.com'),
    ('Charlie Durand', 19, 'charlie@email.com'),
    ('Diana Lambert', 21, 'diana@email.com'),
    ('Eve Bernard', 20, 'eve@email.com')
]

cursor.executemany("""
    INSERT INTO students (name, age, email)
    VALUES (?, ?, ?)
""", students_data)

conn.commit()
print(f"[OK] {cursor.rowcount} étudiants insérés !")

"""
[IDEE] NOUVEAUX CONCEPTS :

executemany()
    Exécute la même requête avec différentes valeurs
    Plus efficace que boucle avec execute()

(?, ?, ?)
    Placeholders (marqueurs de position)
    Remplacés par valeurs du tuple
    [ATTENTION] TOUJOURS utiliser ? pour éviter SQL injection !

cursor.rowcount
    Nombre de lignes affectées


[X] MAUVAIS (SQL Injection possible) :
"""

# Ne JAMAIS faire ça !
name = "Alice"
cursor.execute(f"INSERT INTO students (name) VALUES ('{name}')")
# Si name = "'); DROP TABLE students; --" -> catastrophe !

"""
[OK] BON (sécurisé) :
"""

name = "Alice"
cursor.execute("INSERT INTO students (name) VALUES (?)", (name,))
# SQLite échappe automatiquement les valeurs

"""
SYNTAXE 3 : Insérer depuis une autre table
"""

# Créer table temporaire
cursor.execute("""
    CREATE TABLE IF NOT EXISTS temp_students (
        name TEXT,
        age INTEGER
    )
""")

cursor.execute("INSERT INTO temp_students VALUES ('Test User', 25)")

# Copier de temp_students vers students
cursor.execute("""
    INSERT INTO students (name, age)
    SELECT name, age FROM temp_students
""")

conn.commit()

"""
[IDEE] INSERT ... SELECT

Insérer résultats d'une requête SELECT
Très utile pour migrations, copies, etc.
"""


"""
EXEMPLE COMPLET : Insérer avec relations
"""

# 1. Insérer majors
majors_data = [
    ('Informatique', 180),
    ('Mathématiques', 180),
    ('Physique', 180),
    ('Biologie', 200)
]

cursor.executemany("""
    INSERT INTO majors (name, credits)
    VALUES (?, ?)
""", majors_data)

# 2. Insérer courses
courses_data = [
    ('CS101', 'Introduction à Python', 3, 'Bases de la programmation Python'),
    ('CS102', 'Bases de données', 4, 'SQL et conception de BDD'),
    ('MATH201', 'Algèbre linéaire', 3, 'Matrices et vecteurs'),
    ('PHY101', 'Physique générale', 4, 'Mécanique et thermodynamique')
]

cursor.executemany("""
    INSERT INTO courses (code, name, credits, description)
    VALUES (?, ?, ?, ?)
""", courses_data)

# 3. Récupérer IDs pour enrollments
cursor.execute("SELECT id FROM students WHERE name = 'Alice Dupont'")
alice_id = cursor.fetchone()[0]

cursor.execute("SELECT id FROM courses WHERE code = 'CS101'")
cs101_id = cursor.fetchone()[0]

# 4. Insérer enrollment
cursor.execute("""
    INSERT INTO enrollments (student_id, course_id, grade)
    VALUES (?, ?, ?)
""", (alice_id, cs101_id, 'A'))

conn.commit()
print("[OK] Données complètes insérées !")

"""
[IDEE] RÉCUPÉRER L'ID INSÉRÉ :

Méthode 1 : lastrowid
"""

cursor.execute("INSERT INTO students (name) VALUES ('New Student')")
new_id = cursor.lastrowid
print(f"ID inséré : {new_id}")

"""
Méthode 2 : RETURNING (PostgreSQL, SQLite 3.35+)
"""

# cursor.execute("""
#     INSERT INTO students (name) VALUES ('New Student')
#     RETURNING id
# """)
# new_id = cursor.fetchone()[0]


# ----------------------------------------------------------------------------
# [RECHERCHE] SELECT - LIRE DES DONNÉES
# ----------------------------------------------------------------------------

"""
SELECT = La requête la plus utilisée !

SYNTAXE DE BASE :

SELECT colonnes
FROM table
WHERE conditions
ORDER BY colonne
LIMIT nombre;


[IDEE] EXEMPLE 1 : Tout sélectionner
"""

cursor.execute("SELECT * FROM students")
all_students = cursor.fetchall()

print("\n[LISTE] Tous les étudiants :")
for student in all_students:
    print(student)

"""
[IDEE] DÉCORTIQUONS :

SELECT *
    Sélectionne TOUTES les colonnes
    * = wildcard (joker)

FROM students
    Depuis la table students

fetchall()
    Récupère TOUTES les lignes en une liste de tuples


RÉSULTAT :
[
    (1, 'Alice Dupont', 20, 'alice@email.com'),
    (2, 'Bob Martin', 22, 'bob@email.com'),
    ...
]


[CLE] fetchall() vs fetchone() vs fetchmany()
"""

# fetchall() : Toutes les lignes
cursor.execute("SELECT * FROM students")
all_rows = cursor.fetchall()
print(all_rows)  # Liste de tuples

# fetchone() : Une seule ligne
cursor.execute("SELECT * FROM students")
first_row = cursor.fetchone()
print(first_row)  # Un tuple

# fetchmany(n) : n lignes
cursor.execute("SELECT * FROM students")
three_rows = cursor.fetchmany(3)
print(three_rows)  # Liste de 3 tuples

"""
[IDEE] EXEMPLE 2 : Sélectionner colonnes spécifiques
"""

cursor.execute("SELECT name, email FROM students")
names_emails = cursor.fetchall()

print("\n[LISTE] Noms et emails :")
for name, email in names_emails:
    print(f"{name} : {email}")

"""
[IDEE] POURQUOI PAS * ?

[X] SELECT * :
- Retourne toutes les colonnes (même inutiles)
- Consomme plus de bande passante
- Moins clair (on ne sait pas ce qu'on récupère)
- Problèmes si structure table change

[OK] SELECT name, email :
- Seulement ce dont on a besoin
- Plus performant
- Code plus clair
- Résistant aux changements de schéma


[IDEE] EXEMPLE 3 : WHERE - Filtrer les résultats
"""

cursor.execute("""
    SELECT name, age
    FROM students
    WHERE age >= 21
""")

adults = cursor.fetchall()
print("\n[LISTE] Étudiants de 21 ans et plus :")
for name, age in adults:
    print(f"{name} : {age} ans")

"""
[IDEE] OPÉRATEURS DE COMPARAISON :

=       Égal
!=, <>  Différent
>       Plus grand
<       Plus petit
>=      Plus grand ou égal
<=      Plus petit ou égal
LIKE    Correspondance de pattern
IN      Dans une liste
BETWEEN Entre deux valeurs
IS NULL Est NULL
IS NOT NULL N'est pas NULL


EXEMPLES :
"""

# Égalité
cursor.execute("SELECT * FROM students WHERE age = 20")

# Différent
cursor.execute("SELECT * FROM students WHERE age != 20")

# LIKE (pattern matching)
cursor.execute("SELECT * FROM students WHERE name LIKE 'A%'")
# % = n'importe quels caractères
# 'A%' = commence par A
# '%e' = finit par e
# '%ar%' = contient ar

# IN (dans une liste)
cursor.execute("SELECT * FROM students WHERE age IN (20, 21, 22)")

# BETWEEN
cursor.execute("SELECT * FROM students WHERE age BETWEEN 20 AND 22")

# IS NULL
cursor.execute("SELECT * FROM students WHERE email IS NULL")

# IS NOT NULL
cursor.execute("SELECT * FROM students WHERE email IS NOT NULL")

"""
[IDEE] EXEMPLE 4 : Opérateurs logiques (AND, OR, NOT)
"""

# AND (ET)
cursor.execute("""
    SELECT name, age
    FROM students
    WHERE age >= 20 AND age <= 22
""")

# OR (OU)
cursor.execute("""
    SELECT name
    FROM students
    WHERE age < 20 OR age > 25
""")

# NOT (NON)
cursor.execute("""
    SELECT name
    FROM students
    WHERE NOT age = 20
""")

# Combinaison (utilisez parenthèses pour clarté)
cursor.execute("""
    SELECT name, age
    FROM students
    WHERE (age = 20 OR age = 21) AND name LIKE 'A%'
""")

"""
[ATTENTION] ORDRE D'ÉVALUATION :

1. NOT
2. AND
3. OR

Utilisez TOUJOURS des parenthèses pour être explicite !

[X] Ambigu :
WHERE age = 20 OR age = 21 AND name LIKE 'A%'

[OK] Clair :
WHERE (age = 20 OR age = 21) AND name LIKE 'A%'
"""


"""
[IDEE] EXEMPLE 5 : ORDER BY - Trier les résultats
"""

# Tri croissant (ASC par défaut)
cursor.execute("""
    SELECT name, age
    FROM students
    ORDER BY age
""")

# Tri décroissant
cursor.execute("""
    SELECT name, age
    FROM students
    ORDER BY age DESC
""")

# Tri sur plusieurs colonnes
cursor.execute("""
    SELECT name, age
    FROM students
    ORDER BY age DESC, name ASC
""")
# D'abord par age décroissant, puis par name croissant si même age

results = cursor.fetchall()
print("\n[LISTE] Étudiants triés (age DESC, name ASC) :")
for name, age in results:
    print(f"{name} : {age} ans")

"""
[IDEE] ASC vs DESC

ASC (Ascending) : Croissant (A->Z, 1->9)
DESC (Descending) : Décroissant (Z->A, 9->1)

Défaut = ASC
"""


"""
[IDEE] EXEMPLE 6 : LIMIT et OFFSET - Pagination
"""

# 5 premiers résultats
cursor.execute("""
    SELECT name
    FROM students
    ORDER BY name
    LIMIT 5
""")

# Sauter 2, prendre 3 (pagination)
cursor.execute("""
    SELECT name
    FROM students
    ORDER BY name
    LIMIT 3 OFFSET 2
""")

results = cursor.fetchall()
print("\n[LISTE] Résultats paginés (3 résultats, skip 2) :")
for (name,) in results:
    print(name)

"""
[IDEE] PAGINATION AVEC LIMIT/OFFSET

Page 1 (résultats 1-10) :
SELECT * FROM students LIMIT 10 OFFSET 0

Page 2 (résultats 11-20) :
SELECT * FROM students LIMIT 10 OFFSET 10

Page 3 (résultats 21-30) :
SELECT * FROM students LIMIT 10 OFFSET 20

Formule :
OFFSET = (page - 1) * taille_page
"""

# Fonction de pagination
def get_page(page_num, page_size=10):
    offset = (page_num - 1) * page_size
    cursor.execute(f"""
        SELECT * FROM students
        ORDER BY id
        LIMIT {page_size} OFFSET {offset}
    """)
    return cursor.fetchall()

# Page 1
page1 = get_page(1)
print(f"\nPage 1 : {len(page1)} résultats")


"""
[IDEE] EXEMPLE 7 : DISTINCT - Valeurs uniques
"""

# Sans DISTINCT (doublons possibles)
cursor.execute("SELECT age FROM students")
all_ages = cursor.fetchall()
print("\nTous les âges :", all_ages)

# Avec DISTINCT (pas de doublons)
cursor.execute("SELECT DISTINCT age FROM students ORDER BY age")
unique_ages = cursor.fetchall()
print("Âges uniques :", unique_ages)

"""
[IDEE] DISTINCT élimine les doublons

Sans DISTINCT : [20, 20, 21, 22, 22, 22]
Avec DISTINCT : [20, 21, 22]
"""


"""
[IDEE] EXEMPLE 8 : AS - Alias (renommer colonnes/tables)
"""

cursor.execute("""
    SELECT 
        name AS student_name,
        age AS student_age,
        email AS contact_email
    FROM students
""")

results = cursor.fetchall()
print("\n[LISTE] Avec alias :")
for name, age, email in results:
    print(f"Nom: {name}, Âge: {age}, Contact: {email}")

"""
[IDEE] POURQUOI ALIAS ?

1. CLARTÉ
   SELECT name AS student_name
   Plus clair que juste "name"

2. ÉVITER CONFLITS
   Quand même nom dans plusieurs tables

3. CALCULS
   SELECT age * 12 AS age_in_months

4. LISIBILITÉ CODE
   Colonnes avec noms descriptifs
"""


"""
[IDEE] EXEMPLE COMPLET : Requête complexe
"""

cursor.execute("""
    SELECT 
        name AS student_name,
        age,
        email
    FROM students
    WHERE 
        age >= 20 
        AND email IS NOT NULL
        AND name LIKE '%a%'
    ORDER BY age DESC, name ASC
    LIMIT 5
""")

results = cursor.fetchall()
print("\n[LISTE] Requête complexe :")
print("Étudiants de 20+ ans, avec email, nom contient 'a', triés par âgev nom^, max 5")
for name, age, email in results:
    print(f"  {name} ({age} ans) - {email}")

"""
[IDEE] ORDRE D'EXÉCUTION SQL :

1. FROM      - Table source
2. WHERE     - Filtrer lignes
3. SELECT    - Choisir colonnes
4. DISTINCT  - Éliminer doublons
5. ORDER BY  - Trier
6. LIMIT     - Limiter résultats

[ATTENTION] Ordre d'ÉCRITURE ≠ Ordre d'EXÉCUTION !

Écrit :
SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT

Exécuté :
FROM -> WHERE -> SELECT -> ORDER BY -> LIMIT
"""


# ----------------------------------------------------------------------------
# [EDIT] UPDATE - MODIFIER DES DONNÉES
# ----------------------------------------------------------------------------

"""
SYNTAXE :

UPDATE table
SET colonne1 = valeur1, colonne2 = valeur2
WHERE condition;


[ATTENTION] ATTENTION : WHERE est CRUCIAL !

Sans WHERE -> TOUTES les lignes sont modifiées !


[IDEE] EXEMPLE 1 : Modifier une ligne
"""

cursor.execute("""
    UPDATE students
    SET age = 21
    WHERE name = 'Alice Dupont'
""")

conn.commit()
print(f"[OK] {cursor.rowcount} ligne(s) modifiée(s)")

"""
[IDEE] DÉCORTIQUONS :

UPDATE students
    Table à modifier

SET age = 21
    Nouvelle valeur pour la colonne age

WHERE name = 'Alice Dupont'
    Condition : seulement pour Alice


[IDEE] EXEMPLE 2 : Modifier plusieurs colonnes
"""

cursor.execute("""
    UPDATE students
    SET 
        age = 23,
        email = 'bob.new@email.com'
    WHERE name = 'Bob Martin'
""")

conn.commit()

"""
[IDEE] EXEMPLE 3 : Modifier plusieurs lignes
"""

cursor.execute("""
    UPDATE students
    SET age = age + 1
    WHERE age >= 20
""")

conn.commit()
print(f"[OK] {cursor.rowcount} étudiants vieillis d'un an")

"""
[IDEE] age = age + 1

Incrémente age de chaque ligne
Basé sur valeur actuelle
"""


"""
[IDEE] EXEMPLE 4 : Modifier avec calcul
"""

cursor.execute("""
    UPDATE courses
    SET credits = credits * 2
    WHERE credits < 4
""")

conn.commit()

"""
[ATTENTION] ERREURS COURANTES :

[X] OUBLIER WHERE :
"""

# DANGER : Modifie TOUTES les lignes !
# cursor.execute("UPDATE students SET age = 25")

"""
[OK] BONNE PRATIQUE : Toujours vérifier avec SELECT d'abord
"""

# 1. Vérifier ce qui va être modifié
cursor.execute("SELECT * FROM students WHERE age = 20")
to_update = cursor.fetchall()
print(f"Lignes à modifier : {len(to_update)}")

# 2. Si OK, modifier
if len(to_update) > 0:
    cursor.execute("UPDATE students SET age = 21 WHERE age = 20")
    conn.commit()

"""
[HOT] CONSEIL PRO : Transactions

BEGIN TRANSACTION
UPDATE ...
-- Vérifier résultat
ROLLBACK;  -- Annuler si pas bon
-- OU
COMMIT;    -- Valider si bon
"""


# ----------------------------------------------------------------------------
# [X] DELETE - SUPPRIMER DES DONNÉES
# ----------------------------------------------------------------------------

"""
SYNTAXE :

DELETE FROM table
WHERE condition;


[ATTENTION] ATTENTION : WHERE est CRUCIAL !

Sans WHERE -> TOUTES les lignes sont supprimées !


[IDEE] EXEMPLE 1 : Supprimer une ligne
"""

cursor.execute("""
    DELETE FROM students
    WHERE name = 'Test User'
""")

conn.commit()
print(f"[OK] {cursor.rowcount} ligne(s) supprimée(s)")

"""
[IDEE] EXEMPLE 2 : Supprimer plusieurs lignes
"""

cursor.execute("""
    DELETE FROM students
    WHERE age < 18
""")

conn.commit()

"""
[IDEE] EXEMPLE 3 : Supprimer avec conditions complexes
"""

cursor.execute("""
    DELETE FROM students
    WHERE 
        age > 25 
        AND email IS NULL
""")

conn.commit()

"""
[ATTENTION] ERREURS COURANTES :

[X] OUBLIER WHERE :
"""

# DANGER : Supprime TOUTES les lignes !
# cursor.execute("DELETE FROM students")

"""
[X] CASCADE mal configuré :

Si foreign key sans ON DELETE CASCADE :
-> Erreur si on supprime parent avec enfants


[OK] BONNE PRATIQUE : SELECT avant DELETE
"""

# 1. Vérifier ce qui va être supprimé
cursor.execute("SELECT * FROM students WHERE age > 30")
to_delete = cursor.fetchall()
print(f"Lignes à supprimer : {len(to_delete)}")

# 2. Confirmer visuellement
for student in to_delete:
    print(f"  - {student[1]} ({student[2]} ans)")

# 3. Si OK, supprimer
# cursor.execute("DELETE FROM students WHERE age > 30")
# conn.commit()

"""
[IDEE] DIFFÉRENCE DELETE vs TRUNCATE vs DROP

DELETE FROM table WHERE ...
    - Supprime lignes spécifiques
    - WHERE optionnel (mais dangereux sans)
    - Peut être ROLLBACK (transaction)
    - Déclenche triggers

TRUNCATE TABLE table
    - Supprime TOUTES les lignes
    - Réinitialise auto-increment
    - Plus rapide que DELETE
    - Ne peut pas ROLLBACK (certains SGBD)
    - Ne déclenche pas triggers
    - Pas supporté par SQLite (utiliser DELETE)

DROP TABLE table
    - Supprime la TABLE complète (structure + données)
    - IRREVERSIBLE
    - [ATTENTION] TRÈS DANGEREUX
"""

# DROP TABLE (utilisez avec EXTRÊME prudence)
# cursor.execute("DROP TABLE IF EXISTS temp_students")


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 1 : CRUD COMPLET
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer une mini-application de gestion d'étudiants


CAHIER DES CHARGES :

1. Créer table "students" avec :
   - id (PK, auto-increment)
   - first_name (NOT NULL)
   - last_name (NOT NULL)
   - email (UNIQUE)
   - birth_date (DATE)
   - gpa (REAL, entre 0 et 4)

2. Insérer 5 étudiants

3. Afficher tous les étudiants triés par nom

4. Modifier le GPA d'un étudiant

5. Supprimer les étudiants avec GPA < 2.0

6. Afficher statistiques :
   - Nombre total d'étudiants
   - GPA moyen
   - Étudiant avec meilleur GPA


SOLUTION :
"""

# 1. Créer table
cursor.execute("""
    CREATE TABLE IF NOT EXISTS students_exercise (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        first_name TEXT NOT NULL,
        last_name TEXT NOT NULL,
        email TEXT UNIQUE,
        birth_date DATE,
        gpa REAL CHECK(gpa >= 0 AND gpa <= 4)
    )
""")

# 2. Insérer étudiants
students = [
    ('Alice', 'Dupont', 'alice.dupont@email.com', '2003-05-15', 3.8),
    ('Bob', 'Martin', 'bob.martin@email.com', '2002-08-22', 3.2),
    ('Charlie', 'Durand', 'charlie.durand@email.com', '2003-01-10', 2.9),
    ('Diana', 'Lambert', 'diana.lambert@email.com', '2002-11-30', 3.9),
    ('Eve', 'Bernard', 'eve.bernard@email.com', '2003-03-18', 1.8)
]

cursor.executemany("""
    INSERT INTO students_exercise (first_name, last_name, email, birth_date, gpa)
    VALUES (?, ?, ?, ?, ?)
""", students)

conn.commit()

# 3. Afficher tous (triés)
print("\n[LISTE] Tous les étudiants (triés par nom) :")
cursor.execute("""
    SELECT first_name, last_name, email, gpa
    FROM students_exercise
    ORDER BY last_name, first_name
""")

for first, last, email, gpa in cursor.fetchall():
    print(f"  {first} {last} ({email}) - GPA: {gpa}")

# 4. Modifier GPA
cursor.execute("""
    UPDATE students_exercise
    SET gpa = 3.0
    WHERE email = 'charlie.durand@email.com'
""")
conn.commit()
print("\n[OK] GPA de Charlie modifié")

# 5. Supprimer GPA < 2.0
cursor.execute("""
    DELETE FROM students_exercise
    WHERE gpa < 2.0
""")
conn.commit()
print(f"[OK] {cursor.rowcount} étudiant(s) supprimé(s) (GPA < 2.0)")

# 6. Statistiques
cursor.execute("SELECT COUNT(*) FROM students_exercise")
total = cursor.fetchone()[0]

cursor.execute("SELECT AVG(gpa) FROM students_exercise")
avg_gpa = cursor.fetchone()[0]

cursor.execute("""
    SELECT first_name, last_name, gpa
    FROM students_exercise
    ORDER BY gpa DESC
    LIMIT 1
""")
best = cursor.fetchone()

print("\n[GRAPHIQUE] Statistiques :")
print(f"  Total étudiants : {total}")
print(f"  GPA moyen : {avg_gpa:.2f}")
print(f"  Meilleur GPA : {best[0]} {best[1]} ({best[2]})")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 2
# ----------------------------------------------------------------------------

"""
[OK] VOUS SAVEZ MAINTENANT :

CREATE TABLE :
- Créer structure de tables
- Définir colonnes et types
- Ajouter contraintes

INSERT :
- Insérer une ligne : VALUES (...)
- Insérer plusieurs : executemany()
- Utiliser placeholders (?)
- Récupérer lastrowid

SELECT :
- Tout : SELECT *
- Colonnes spécifiques : SELECT col1, col2
- Filtrer : WHERE
- Opérateurs : =, !=, >, <, LIKE, IN, BETWEEN
- Logique : AND, OR, NOT
- Trier : ORDER BY
- Limiter : LIMIT, OFFSET
- Alias : AS
- Uniques : DISTINCT

UPDATE :
- Modifier lignes : SET col = val
- Conditions : WHERE
- Calculer : SET col = col + 1
- [ATTENTION] Toujours vérifier avec SELECT d'abord

DELETE :
- Supprimer lignes : WHERE
- [ATTENTION] WHERE crucial
- [ATTENTION] Toujours vérifier avec SELECT d'abord


[CLE] POINTS CLÉS :

1. CRUD = Create, Read, Update, Delete
2. Placeholders (?) pour sécurité
3. WHERE toujours pour UPDATE/DELETE
4. SELECT avant UPDATE/DELETE
5. ORDER BY pour tri
6. LIMIT/OFFSET pour pagination


[OBJECTIF] PROCHAINE ÉTAPE : Chapitre 3 - Types de Données

Vous allez apprendre tous les types de données disponibles !
"""


# ============================================================================
# [GUIDE] CHAPITRE 3 : TYPES DE DONNÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Tous les types de données en SQL
[OK] Différences entre SGBD (SQLite, PostgreSQL, MySQL)
[OK] Choisir le bon type pour chaque cas
[OK] Conversions et casting
[OK] Stockage efficace
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] TYPES DE DONNÉES EN SQLite
# ----------------------------------------------------------------------------

"""
SQLite a un système de types FLEXIBLE (type affinity).

[IDEE] 5 CLASSES DE STOCKAGE :

1. NULL      - Valeur nulle
2. INTEGER   - Entier signé
3. REAL      - Nombre à virgule flottante
4. TEXT      - Chaîne de caractères (UTF-8)
5. BLOB      - Binary Large Object (données binaires)


[DESIGN] TABLEAU COMPARATIF :

┌─────────────┬──────────────┬─────────────────┬────────────────┐
│ Type SQL    │ Stockage     │ Taille          │ Usage          │
├─────────────┼──────────────┼─────────────────┼────────────────┤
│ NULL        │ NULL         │ 0 bytes         │ Valeur absente │
│ INTEGER     │ INTEGER      │ 1-8 bytes       │ IDs, compteurs │
│ REAL        │ REAL         │ 8 bytes         │ Prix, mesures  │
│ TEXT        │ TEXT         │ Variable        │ Noms, emails   │
│ BLOB        │ BLOB         │ Variable        │ Images, files  │
└─────────────┴──────────────┴─────────────────┴────────────────┘


1. INTEGER
──────────

[IDEE] ENTIER SIGNÉ (peut être négatif)

Taille : 1, 2, 3, 4, 6 ou 8 bytes (selon valeur)

Plage :
-9,223,372,036,854,775,808 à 9,223,372,036,854,775,807
(environ -9 quintillions à 9 quintillions)
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS demo_integer (
        tiny_int INTEGER,        -- Petit nombre
        medium_int INTEGER,      -- Nombre moyen
        big_int INTEGER,         -- Grand nombre
        negative_int INTEGER     -- Négatif OK
    )
""")

cursor.execute("""
    INSERT INTO demo_integer VALUES
    (10, 1000, 1000000000, -500)
""")

conn.commit()

"""
[IDEE] USAGES COURANTS :

[OK] IDs (primary keys)
[OK] Compteurs
[OK] Âges
[OK] Quantités
[OK] Années
[OK] Booléens (0 = false, 1 = true)

Exemples :
"""

CREATE TABLE users (
    id INTEGER PRIMARY KEY,           -- ID
    age INTEGER CHECK(age >= 0),      -- Âge
    login_count INTEGER DEFAULT 0,    -- Compteur
    is_active INTEGER DEFAULT 1,      -- Boolean (0/1)
    birth_year INTEGER                -- Année
);

"""
[HOT] ASTUCE : Boolean avec INTEGER

SQLite n'a pas de type BOOLEAN natif
-> Utilisez INTEGER avec 0/1
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS demo_boolean (
        is_active INTEGER DEFAULT 1,
        is_verified INTEGER DEFAULT 0
    )
""")

# Insérer
cursor.execute("INSERT INTO demo_boolean VALUES (1, 0)")

# Requêter
cursor.execute("SELECT * FROM demo_boolean WHERE is_active = 1")


"""
2. REAL
───────

[IDEE] NOMBRE À VIRGULE FLOTTANTE (décimal)

Taille : 8 bytes
Format : IEEE floating point (double precision)

Précision : ~15 chiffres significatifs
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS demo_real (
        price REAL,
        latitude REAL,
        longitude REAL,
        percentage REAL,
        scientific REAL
    )
""")

cursor.execute("""
    INSERT INTO demo_real VALUES
    (19.99, 48.8566, 2.3522, 0.15, 6.022e23)
""")

conn.commit()

"""
[IDEE] USAGES COURANTS :

[OK] Prix (attention : voir DECIMAL plus bas)
[OK] Coordonnées géographiques
[OK] Pourcentages
[OK] Mesures physiques
[OK] Calculs scientifiques

[ATTENTION] ATTENTION : PRÉCISION !

Les nombres à virgule flottante sont APPROXIMATIFS !
"""

# Démonstration du problème de précision
cursor.execute("SELECT 0.1 + 0.2")
result = cursor.fetchone()[0]
print(f"0.1 + 0.2 = {result}")  # Peut afficher 0.30000000000000004

"""
[IDEE] Pour l'ARGENT : Utilisez INTEGER (centimes) !

[X] MAUVAIS :
"""
CREATE TABLE products (
    price REAL  -- 19.99 peut devenir 19.990000000001
);

"""
[OK] BON :
"""
CREATE TABLE products (
    price_cents INTEGER  -- 1999 (19.99 € = 1999 centimes)
);

# Affichage : price_cents / 100
cursor.execute("SELECT price_cents / 100.0 AS price FROM products")


"""
3. TEXT
───────

[IDEE] CHAÎNE DE CARACTÈRES (UTF-8)

Taille : Variable (dépend du contenu)
Encodage : UTF-8, UTF-16BE ou UTF-16LE

Longueur max : 1 milliard de caractères (théorique)
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS demo_text (
        short_text TEXT,
        long_text TEXT,
        unicode_text TEXT,
        json_text TEXT
    )
""")

cursor.execute("""
    INSERT INTO demo_text VALUES
    (
        'Court',
        'Texte très long avec beaucoup de contenu...',
        '你好世界 [MONDE] émojis OK [OK]',
        '{"name": "Alice", "age": 20}'
    )
""")

conn.commit()

"""
[IDEE] USAGES COURANTS :

[OK] Noms, prénoms
[OK] Emails
[OK] Adresses
[OK] Descriptions
[OK] URLs
[OK] JSON (SQLite 3.9+)
[OK] Texte formaté (Markdown, HTML)

Exemples :
"""

CREATE TABLE users (
    username TEXT NOT NULL,
    email TEXT UNIQUE,
    bio TEXT,                    -- Long texte OK
    avatar_url TEXT,
    preferences TEXT             -- JSON
);

"""
[IDEE] PAS DE LIMITE DE LONGUEUR EN SQLite

Autres SGBD ont VARCHAR(n) :
- MySQL : VARCHAR(255)
- PostgreSQL : VARCHAR(n) ou TEXT

SQLite : TEXT = illimité (pratiquement)


[HOT] ASTUCE : Stockage JSON dans TEXT
"""

import json

# Créer données JSON
user_prefs = {
    "theme": "dark",
    "language": "fr",
    "notifications": True
}

# Convertir en string JSON
json_str = json.dumps(user_prefs)

# Insérer
cursor.execute("""
    INSERT INTO users (username, preferences)
    VALUES (?, ?)
""", ("alice", json_str))

# Lire et parser
cursor.execute("SELECT preferences FROM users WHERE username = 'alice'")
prefs_str = cursor.fetchone()[0]
prefs = json.loads(prefs_str)
print(prefs["theme"])  # 'dark'


"""
4. BLOB
───────

[IDEE] BINARY LARGE OBJECT (données binaires brutes)

Taille : Variable
Usage : Fichiers binaires (images, PDFs, etc.)

[ATTENTION] Généralement PAS recommandé pour gros fichiers !
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS demo_blob (
        id INTEGER PRIMARY KEY,
        file_data BLOB,
        file_name TEXT,
        file_type TEXT
    )
""")

# Lire image
with open('example.png', 'rb') as f:
    image_data = f.read()

# Insérer
cursor.execute("""
    INSERT INTO demo_blob (file_data, file_name, file_type)
    VALUES (?, ?, ?)
""", (image_data, 'example.png', 'image/png'))

conn.commit()

"""
[IDEE] USAGES (rares) :

[OK] Petits fichiers (<1MB)
[OK] Thumbnails
[OK] Icônes
[OK] Certificats
[OK] Signatures

[X] NE PAS UTILISER POUR :

[X] Grosses images
[X] Vidéos
[X] Documents volumineux

-> Stockez le CHEMIN vers le fichier, pas le fichier !


[OK] BONNE PRATIQUE :
"""

CREATE TABLE photos (
    id INTEGER PRIMARY KEY,
    filename TEXT NOT NULL,           -- 'photo_123.jpg'
    filepath TEXT NOT NULL,           -- '/uploads/2024/01/photo_123.jpg'
    url TEXT,                         -- 'https://cdn.example.com/photo_123.jpg'
    size_bytes INTEGER,
    uploaded_at TIMESTAMP
);


"""
5. NULL
───────

[IDEE] ABSENCE DE VALEUR

NULL ≠ 0
NULL ≠ ''
NULL ≠ False

NULL = "Je ne sais pas" / "Pas applicable"
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS demo_null (
        id INTEGER PRIMARY KEY,
        optional_field TEXT,         -- Peut être NULL
        required_field TEXT NOT NULL -- Ne peut PAS être NULL
    )
""")

# Insérer avec NULL
cursor.execute("""
    INSERT INTO demo_null (id, required_field)
    VALUES (1, 'Required')
""")
# optional_field = NULL automatiquement

# Insérer NULL explicitement
cursor.execute("""
    INSERT INTO demo_null VALUES (2, NULL, 'Another')
""")

conn.commit()

"""
[IDEE] OPÉRATIONS AVEC NULL :

Comparaisons normales NE MARCHENT PAS :
"""

# [X] MAUVAIS
cursor.execute("SELECT * FROM demo_null WHERE optional_field = NULL")
# Retourne 0 lignes (toujours)

# [OK] BON
cursor.execute("SELECT * FROM demo_null WHERE optional_field IS NULL")
cursor.execute("SELECT * FROM demo_null WHERE optional_field IS NOT NULL")

"""
[IDEE] NULL dans calculs :

NULL + 5 = NULL
NULL * 2 = NULL
CONCAT(NULL, 'text') = NULL

Toute opération avec NULL -> NULL


[HOT] FONCTIONS POUR NULL :
"""

# COALESCE : Première valeur non-NULL
cursor.execute("""
    SELECT COALESCE(optional_field, 'Default') 
    FROM demo_null
""")

# IFNULL (SQLite specific)
cursor.execute("""
    SELECT IFNULL(optional_field, 'Default') 
    FROM demo_null
""")

# NULLIF : Retourne NULL si égal
cursor.execute("""
    SELECT NULLIF(optional_field, 'N/A') 
    FROM demo_null
""")


# ----------------------------------------------------------------------------
# [SYNC] CONVERSIONS ET CASTING
# ----------------------------------------------------------------------------

"""
SQLite fait des conversions AUTOMATIQUES (type affinity)

Mais parfois il faut forcer avec CAST :
"""

# Texte vers entier
cursor.execute("SELECT CAST('123' AS INTEGER)")
print(cursor.fetchone()[0])  # 123 (entier)

# Entier vers texte
cursor.execute("SELECT CAST(123 AS TEXT)")
print(cursor.fetchone()[0])  # '123' (texte)

# Texte vers réel
cursor.execute("SELECT CAST('3.14' AS REAL)")
print(cursor.fetchone()[0])  # 3.14 (réel)

"""
[IDEE] FONCTIONS DE CONVERSION :
"""

# typeof() : Voir le type
cursor.execute("SELECT typeof(123), typeof('text'), typeof(3.14), typeof(NULL)")
print(cursor.fetchone())  # ('integer', 'text', 'real', 'null')

# length() : Longueur
cursor.execute("SELECT length('Hello')")
print(cursor.fetchone()[0])  # 5

# round() : Arrondir
cursor.execute("SELECT round(3.14159, 2)")
print(cursor.fetchone()[0])  # 3.14


# ----------------------------------------------------------------------------
# [CALENDRIER] DATES ET HEURES
# ----------------------------------------------------------------------------

"""
[ATTENTION] SQLite N'A PAS de type DATE/TIME natif !

Options :
1. TEXT (ISO8601) : '2024-01-18 15:30:00'
2. INTEGER (Unix timestamp) : 1705590600
3. REAL (Julian day) : 2459603.5


[IDEE] RECOMMANDATION : TEXT avec format ISO8601
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS demo_datetime (
        id INTEGER PRIMARY KEY,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        updated_at TEXT,
        birth_date TEXT  -- Format: 'YYYY-MM-DD'
    )
""")

# Insérer avec date actuelle
cursor.execute("""
    INSERT INTO demo_datetime (birth_date)
    VALUES ('2003-05-15')
""")

conn.commit()

"""
[IDEE] FONCTIONS DATE/HEURE SQLite :

CURRENT_TIMESTAMP : Date+heure actuelle
CURRENT_DATE : Date actuelle
CURRENT_TIME : Heure actuelle
date() : Extraire date
time() : Extraire heure
datetime() : Manipuler dates
strftime() : Formater dates
"""

# Date actuelle
cursor.execute("SELECT CURRENT_DATE")
print(cursor.fetchone()[0])  # '2024-01-18'

# Heure actuelle
cursor.execute("SELECT CURRENT_TIME")
print(cursor.fetchone()[0])  # '15:30:45'

# Date + heure
cursor.execute("SELECT CURRENT_TIMESTAMP")
print(cursor.fetchone()[0])  # '2024-01-18 15:30:45'

# Ajouter 7 jours
cursor.execute("SELECT date('now', '+7 days')")
print(cursor.fetchone()[0])  # Date dans 7 jours

# Formater
cursor.execute("SELECT strftime('%d/%m/%Y', 'now')")
print(cursor.fetchone()[0])  # '18/01/2024'

# Calculer âge
cursor.execute("""
    SELECT 
        birth_date,
        CAST((julianday('now') - julianday(birth_date)) / 365.25 AS INTEGER) AS age
    FROM demo_datetime
    WHERE birth_date IS NOT NULL
""")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 3
# ----------------------------------------------------------------------------

"""
[OK] VOUS SAVEZ MAINTENANT :

TYPES SQLite :
- NULL : Absence de valeur
- INTEGER : Entiers (IDs, compteurs, booléens)
- REAL : Décimaux (prix en centimes!, coordonnées)
- TEXT : Chaînes (noms, JSON, illimité)
- BLOB : Binaire (rarement, petits fichiers)

CONVERSIONS :
- CAST(val AS type)
- typeof(val)
- Conversions automatiques

DATES :
- TEXT format ISO8601
- CURRENT_TIMESTAMP
- Fonctions date(), datetime(), strftime()

BONNES PRATIQUES :
- Prix -> INTEGER (centimes)
- Fichiers -> TEXT (chemin), pas BLOB
- Dates -> TEXT ISO8601
- Booléens -> INTEGER (0/1)
- JSON -> TEXT


[CLE] CHOIX DU TYPE :

ID -> INTEGER PRIMARY KEY
Nom -> TEXT NOT NULL
Email -> TEXT UNIQUE
Prix -> INTEGER (centimes)
Description -> TEXT
Photo -> TEXT (URL)
Actif -> INTEGER (0/1)
Créé le -> TEXT DEFAULT CURRENT_TIMESTAMP


-> PROCHAINE ÉTAPE : Chapitre 4 - Contraintes et Clés

Vous allez maîtriser toutes les contraintes !
"""


# ============================================================================
# [GUIDE] CHAPITRE 4 : CONTRAINTES ET CLÉS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Toutes les contraintes SQL
[OK] Primary Key en détail
[OK] Foreign Key et intégrité référentielle
[OK] Contraintes CHECK personnalisées
[OK] DEFAULT et auto-valeurs
[OK] Créer des schémas robustes
"""


# ----------------------------------------------------------------------------
# [SECURISE] CONTRAINTES (CONSTRAINTS)
# ----------------------------------------------------------------------------

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

CONTRAINTE = Règle sur les données pour garantir :
- VALIDITÉ
- COHÉRENCE
- INTÉGRITÉ


[GRAPHIQUE] TYPES DE CONTRAINTES :

1. PRIMARY KEY    - Clé primaire (ID unique)
2. FOREIGN KEY    - Clé étrangère (relation)
3. UNIQUE         - Valeurs uniques
4. NOT NULL       - Pas de NULL
5. CHECK          - Condition personnalisée
6. DEFAULT        - Valeur par défaut


1. PRIMARY KEY
──────────────

[IDEE] IDENTIFIANT UNIQUE DE CHAQUE LIGNE

Règles :
- UNIQUE : Chaque valeur différente
- NOT NULL : Jamais NULL
- UNE SEULE par table (ou composite)
"""

CREATE TABLE students (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT
);

"""
[IDEE] AUTOINCREMENT :

Sans AUTOINCREMENT :
"""
cursor.execute("""
    CREATE TABLE demo_pk1 (
        id INTEGER PRIMARY KEY,
        name TEXT
    )
""")

cursor.executemany("INSERT INTO demo_pk1 (name) VALUES (?)", 
                   [('Alice',), ('Bob',), ('Charlie',)])

# IDs : 1, 2, 3
# Si on supprime Bob (id=2) puis ajoute Diana -> id=2 (réutilisé!)

"""
Avec AUTOINCREMENT :
"""
cursor.execute("""
    CREATE TABLE demo_pk2 (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT
    )
""")

cursor.executemany("INSERT INTO demo_pk2 (name) VALUES (?)", 
                   [('Alice',), ('Bob',), ('Charlie',)])

# IDs : 1, 2, 3
# Si on supprime Bob (id=2) puis ajoute Diana -> id=4 (pas réutilisé)

"""
[ATTENTION] AUTOINCREMENT :

[OK] AVANTAGES :
- IDs jamais réutilisés
- Évite confusions

[X] INCONVÉNIENTS :
- Légèrement plus lent
- Table sqlite_sequence créée

[IDEE] RECOMMANDATION :
- Utilisez AUTOINCREMENT sauf si performance critique


[HOT] CLÉ PRIMAIRE COMPOSITE (plusieurs colonnes) :
"""

CREATE TABLE enrollments (
    student_id INTEGER,
    course_id INTEGER,
    grade TEXT,
    PRIMARY KEY (student_id, course_id)
);

"""
Combinaison (student_id, course_id) doit être unique
- Étudiant 1 + Cours 10 [OK]
- Étudiant 1 + Cours 20 [OK]
- Étudiant 1 + Cours 10 [X] (déjà existe)


2. FOREIGN KEY
──────────────

[IDEE] CLÉ ÉTRANGÈRE - RÉFÉRENCE VERS AUTRE TABLE

Garantit INTÉGRITÉ RÉFÉRENTIELLE
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS departments (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS employees (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        department_id INTEGER,
        FOREIGN KEY (department_id) REFERENCES departments(id)
    )
""")

"""
[IDEE] QUE FAIT FOREIGN KEY ?

1. VALIDATION :
   [X] Impossible d'ajouter employee avec department_id=999 si département 999 n'existe pas

2. ACTIONS CASCADE :
"""

CREATE TABLE employees (
    id INTEGER PRIMARY KEY,
    department_id INTEGER,
    FOREIGN KEY (department_id) REFERENCES departments(id)
        ON DELETE CASCADE      -- Si département supprimé -> employés aussi
        ON UPDATE CASCADE      -- Si id département change -> mise à jour auto
);

"""
[IDEE] OPTIONS ON DELETE / ON UPDATE :

CASCADE      : Action propagée (suppression/modification)
SET NULL     : FK devient NULL
SET DEFAULT  : FK devient valeur DEFAULT
RESTRICT     : Empêche l'action (erreur)
NO ACTION    : Idem RESTRICT


EXEMPLES :
"""

# ON DELETE CASCADE
CREATE TABLE posts (
    id INTEGER PRIMARY KEY,
    user_id INTEGER,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
# User supprimé -> Ses posts supprimés

# ON DELETE SET NULL
CREATE TABLE posts (
    id INTEGER PRIMARY KEY,
    user_id INTEGER,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);
# User supprimé -> posts.user_id = NULL

# ON DELETE RESTRICT
CREATE TABLE posts (
    id INTEGER PRIMARY KEY,
    user_id INTEGER,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT
);
# User avec posts -> Erreur si tentative suppression


"""
[ATTENTION] SQLite : Foreign keys DÉSACTIVÉES par défaut !

Activer :
"""

cursor.execute("PRAGMA foreign_keys = ON")

"""
Vérifier :
"""

cursor.execute("PRAGMA foreign_keys")
print(cursor.fetchone())  # (1,) si activé


"""
3. UNIQUE
─────────

[IDEE] VALEURS UNIQUES (mais peut être NULL)
"""

CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    email TEXT UNIQUE,          -- Chaque email unique
    username TEXT UNIQUE,       -- Chaque username unique
    phone TEXT UNIQUE           -- Chaque phone unique
);

"""
Différence PK vs UNIQUE :

PRIMARY KEY :
- Unique + NOT NULL
- UNE SEULE par table
- Index créé automatiquement

UNIQUE :
- Unique mais peut être NULL
- PLUSIEURS par table
- Index créé automatiquement


[IDEE] UNIQUE peut avoir plusieurs NULL :
"""

cursor.execute("""
    CREATE TABLE demo_unique (
        id INTEGER PRIMARY KEY,
        email TEXT UNIQUE
    )
""")

cursor.execute("INSERT INTO demo_unique VALUES (1, NULL)")
cursor.execute("INSERT INTO demo_unique VALUES (2, NULL)")  # [OK] OK
cursor.execute("INSERT INTO demo_unique VALUES (3, 'test@email.com')")
cursor.execute("INSERT INTO demo_unique VALUES (4, 'test@email.com')")  # [X] Erreur


"""
[HOT] UNIQUE COMPOSITE (plusieurs colonnes) :
"""

CREATE TABLE ratings (
    user_id INTEGER,
    movie_id INTEGER,
    rating INTEGER,
    UNIQUE(user_id, movie_id)  -- Combinaison unique
);

# User 1 peut noter Movie 10
# User 2 peut noter Movie 10
# Mais User 1 ne peut pas noter Movie 10 deux fois


"""
4. NOT NULL
───────────

[IDEE] VALEUR OBLIGATOIRE (ne peut pas être NULL)
"""

CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    username TEXT NOT NULL,     -- Obligatoire
    email TEXT NOT NULL,        -- Obligatoire
    bio TEXT                    -- Optionnel (NULL OK)
);

"""
[IDEE] USAGE :

Colonnes essentielles -> NOT NULL
Colonnes optionnelles -> NULL OK


[X] Tentative d'insérer NULL :
"""

cursor.execute("""
    CREATE TABLE demo_not_null (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL
    )
""")

# cursor.execute("INSERT INTO demo_not_null (id) VALUES (1)")
# [X] Erreur : NOT NULL constraint failed: demo_not_null.name


"""
5. CHECK
────────

[IDEE] CONDITION PERSONNALISÉE
"""

CREATE TABLE products (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    price INTEGER CHECK(price > 0),                    -- Prix positif
    stock INTEGER CHECK(stock >= 0),                   -- Stock non-négatif
    discount REAL CHECK(discount >= 0 AND discount <= 100)  -- 0-100%
);

cursor.execute("""
    CREATE TABLE students_check (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        age INTEGER CHECK(age >= 18 AND age <= 100),
        gpa REAL CHECK(gpa >= 0 AND gpa <= 4),
        email TEXT CHECK(email LIKE '%@%')  -- Doit contenir @
    )
""")

"""
[IDEE] EXEMPLES CHECK :

Age entre 18 et 100 :
CHECK(age >= 18 AND age <= 100)

Email contient @ :
CHECK(email LIKE '%@%')

Date de fin après date de début :
CHECK(end_date > start_date)

Genre valide :
CHECK(gender IN ('M', 'F', 'Other'))

Prix positif :
CHECK(price > 0)


[HOT] CHECK avec nom personnalisé :
"""

CREATE TABLE products (
    price INTEGER,
    CONSTRAINT positive_price CHECK(price > 0)
);

# Message d'erreur plus clair si violation


"""
6. DEFAULT
──────────

[IDEE] VALEUR PAR DÉFAUT SI NON FOURNIE
"""

CREATE TABLE users (
    id INTEGER PRIMARY KEY,
    username TEXT NOT NULL,
    is_active INTEGER DEFAULT 1,                      -- 1 par défaut
    role TEXT DEFAULT 'user',                         -- 'user' par défaut
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,        -- Date actuelle
    login_count INTEGER DEFAULT 0                     -- 0 par défaut
);

cursor.execute("""
    INSERT INTO users (username)
    VALUES ('alice')
""")

# is_active = 1
# role = 'user'
# created_at = date actuelle
# login_count = 0

"""
[IDEE] VALEURS DEFAULT COURANTES :

Booléens :
DEFAULT 0 ou DEFAULT 1

Compteurs :
DEFAULT 0

Dates :
DEFAULT CURRENT_TIMESTAMP
DEFAULT CURRENT_DATE

Statuts :
DEFAULT 'pending'
DEFAULT 'active'

Chaînes vides :
DEFAULT ''


[ATTENTION] DEFAULT vs NULL :

Colonne sans DEFAULT et sans NOT NULL -> NULL si omise
Colonne avec DEFAULT -> Valeur DEFAULT si omise
"""


# ----------------------------------------------------------------------------
# [CONSTRUCTION] EXEMPLE COMPLET : Schéma Robuste
# ----------------------------------------------------------------------------

"""
Créons un schéma complet avec toutes les contraintes :
"""

# Activer foreign keys
cursor.execute("PRAGMA foreign_keys = ON")

# Table users
cursor.execute("""
    CREATE TABLE IF NOT EXISTS users_complete (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT NOT NULL UNIQUE CHECK(length(username) >= 3),
        email TEXT NOT NULL UNIQUE CHECK(email LIKE '%@%.%'),
        password_hash TEXT NOT NULL,
        is_active INTEGER DEFAULT 1 CHECK(is_active IN (0, 1)),
        role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin', 'moderator')),
        created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
        updated_at TEXT
    )
""")

# Table posts
cursor.execute("""
    CREATE TABLE IF NOT EXISTS posts_complete (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        title TEXT NOT NULL CHECK(length(title) >= 5),
        content TEXT NOT NULL,
        is_published INTEGER DEFAULT 0 CHECK(is_published IN (0, 1)),
        views INTEGER DEFAULT 0 CHECK(views >= 0),
        created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
        published_at TEXT,
        FOREIGN KEY (user_id) REFERENCES users_complete(id) ON DELETE CASCADE,
        CHECK(published_at IS NULL OR published_at >= created_at)
    )
""")

# Table comments
cursor.execute("""
    CREATE TABLE IF NOT EXISTS comments_complete (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        post_id INTEGER NOT NULL,
        user_id INTEGER NOT NULL,
        content TEXT NOT NULL CHECK(length(content) >= 1),
        is_approved INTEGER DEFAULT 0 CHECK(is_approved IN (0, 1)),
        created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
        FOREIGN KEY (post_id) REFERENCES posts_complete(id) ON DELETE CASCADE,
        FOREIGN KEY (user_id) REFERENCES users_complete(id) ON DELETE CASCADE
    )
""")

conn.commit()

"""
[IDEE] ANALYSE DU SCHÉMA :

users_complete :
- username : MIN 3 caractères, UNIQUE
- email : Format validé, UNIQUE
- is_active : Boolean (0/1)
- role : Valeurs limitées
- created_at : Auto, NOT NULL

posts_complete :
- user_id : FK vers users, CASCADE
- title : MIN 5 caractères
- views : Non-négatif
- published_at >= created_at : Cohérence

comments_complete :
- Deux FK (post, user), CASCADE
- content : Non vide
"""


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 2 : Schéma E-Commerce
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer schéma e-commerce complet


CAHIER DES CHARGES :

Tables :
1. customers (clients)
2. products (produits)
3. orders (commandes)
4. order_items (articles commandés)

Contraintes :
- Toutes les PK auto-increment
- FK appropriées avec CASCADE
- Validations (prix > 0, quantité >= 0, etc.)
- DEFAULT où pertinent
- Email valide
- Dates cohérentes


SOLUTION :
"""

cursor.execute("PRAGMA foreign_keys = ON")

# 1. Customers
cursor.execute("""
    CREATE TABLE IF NOT EXISTS customers (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        first_name TEXT NOT NULL,
        last_name TEXT NOT NULL,
        email TEXT NOT NULL UNIQUE CHECK(email LIKE '%@%.%'),
        phone TEXT,
        address TEXT,
        city TEXT,
        country TEXT DEFAULT 'France',
        created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
    )
""")

# 2. Products
cursor.execute("""
    CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        description TEXT,
        price_cents INTEGER NOT NULL CHECK(price_cents > 0),
        stock INTEGER DEFAULT 0 CHECK(stock >= 0),
        is_active INTEGER DEFAULT 1 CHECK(is_active IN (0, 1)),
        created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
    )
""")

# 3. Orders
cursor.execute("""
    CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        customer_id INTEGER NOT NULL,
        status TEXT DEFAULT 'pending' CHECK(
            status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled')
        ),
        total_cents INTEGER NOT NULL CHECK(total_cents >= 0),
        created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
        updated_at TEXT,
        shipped_at TEXT,
        delivered_at TEXT,
        FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT,
        CHECK(shipped_at IS NULL OR shipped_at >= created_at),
        CHECK(delivered_at IS NULL OR delivered_at >= shipped_at)
    )
""")

# 4. Order Items
cursor.execute("""
    CREATE TABLE IF NOT EXISTS order_items (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        order_id INTEGER NOT NULL,
        product_id INTEGER NOT NULL,
        quantity INTEGER NOT NULL CHECK(quantity > 0),
        unit_price_cents INTEGER NOT NULL CHECK(unit_price_cents > 0),
        total_cents INTEGER NOT NULL CHECK(total_cents > 0),
        FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
        FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT,
        CHECK(total_cents = quantity * unit_price_cents)
    )
""")

conn.commit()
print("[OK] Schéma e-commerce créé !")

"""
[IDEE] POINTS CLÉS DU SCHÉMA :

1. FOREIGN KEYS :
   - customer_id -> RESTRICT (ne pas supprimer si commandes)
   - order_id -> CASCADE (supprimer items si commande supprimée)
   - product_id -> RESTRICT (ne pas supprimer si commandé)

2. CHECKS :
   - Prix positifs
   - Stock non-négatif
   - Status limité à valeurs valides
   - Dates cohérentes (shipped >= created, delivered >= shipped)
   - total_cents = quantity * unit_price_cents

3. DEFAULTS :
   - Nouveaux produits actifs
   - Nouvelles commandes 'pending'
   - Pays 'France'
   - Timestamps automatiques
"""


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 4 ET PARTIE 1 COMPLÈTE
# ----------------------------------------------------------------------------

"""
[OK] VOUS SAVEZ MAINTENANT :

CONTRAINTES :
- PRIMARY KEY : ID unique, AUTOINCREMENT
- FOREIGN KEY : Relations, CASCADE
- UNIQUE : Valeurs distinctes
- NOT NULL : Obligatoire
- CHECK : Validation personnalisée
- DEFAULT : Valeur par défaut

BONNES PRATIQUES :
- PRIMARY KEY AUTOINCREMENT sur toutes les tables
- FOREIGN KEY avec ON DELETE approprié
- NOT NULL sur colonnes essentielles
- CHECK pour validation métier
- DEFAULT pour valeurs communes
- UNIQUE sur emails, usernames
- PRAGMA foreign_keys = ON


[CLE] PATTERN COMPLET :

CREATE TABLE nom (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    colonne1 TYPE NOT NULL CHECK(...),
    colonne2 TYPE UNIQUE,
    colonne3 TYPE DEFAULT val,
    fk_id INTEGER,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (fk_id) REFERENCES autre(id) ON DELETE CASCADE
);


[BRAVO] FÉLICITATIONS ! PARTIE 1 TERMINÉE !

Vous maîtrisez maintenant :
[OK] Concepts fondamentaux (tables, colonnes, lignes, clés)
[OK] SQL de base (CREATE, INSERT, SELECT, UPDATE, DELETE)
[OK] Tous les types de données
[OK] Toutes les contraintes

Vous pouvez créer des schémas complets et robustes ! [RAPIDE]


-> PROCHAINE ÉTAPE : PARTIE 2 - SQL Avancé

La Partie 2 couvrira :
- Requêtes complexes (JOIN, UNION, sous-requêtes)
- Fonctions et agrégations (COUNT, SUM, AVG)
- Index et optimisation
- Vues et procédures stockées

Vous allez devenir expert en SQL ! [FORCE]
"""

# Fermer la connexion proprement
conn.close()
print("\n[OK] Partie 1 complétée ! Bravo ! [BRAVO]")

# ============================================================================
# [LIVRE] BASES DE DONNÉES - PARTIE 2 : SQL AVANCÉ
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 5 : Requêtes Complexes (JOIN, UNION, Sous-requêtes)
# - Chapitre 6 : Fonctions et Agrégations
# - Chapitre 7 : Index et Optimisation
# - Chapitre 8 : Vues et Procédures Stockées
#
# [TEMPS] TEMPS : ~8-10 heures
# [DOCS] PRÉREQUIS : Partie 1 complétée
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 5 : REQUÊTES COMPLEXES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Combiner données de plusieurs tables (JOIN)
[OK] Tous les types de JOIN (INNER, LEFT, RIGHT, FULL)
[OK] Utiliser UNION et UNION ALL
[OK] Écrire des sous-requêtes (subqueries)
[OK] Sous-requêtes corrélées
[OK] WITH (Common Table Expressions)
[OK] Requêtes complexes multi-niveaux
"""

import sqlite3

# Connexion et préparation
conn = sqlite3.connect('databases/advanced_school.db')
cursor = conn.cursor()

# Activer foreign keys
cursor.execute("PRAGMA foreign_keys = ON")


# ----------------------------------------------------------------------------
# [DEMARRAGE] PRÉPARATION : BASE DE DONNÉES D'EXEMPLE COMPLÈTE
# ----------------------------------------------------------------------------

"""
Créons une base de données riche pour nos exemples
"""

# Table students
cursor.execute("""
    CREATE TABLE IF NOT EXISTS students (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        first_name TEXT NOT NULL,
        last_name TEXT NOT NULL,
        email TEXT UNIQUE,
        birth_date DATE,
        major_id INTEGER,
        enrollment_year INTEGER,
        gpa REAL CHECK(gpa >= 0 AND gpa <= 4)
    )
""")

# Table majors
cursor.execute("""
    CREATE TABLE IF NOT EXISTS majors (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL UNIQUE,
        department TEXT NOT NULL,
        credits_required INTEGER
    )
""")

# Table courses
cursor.execute("""
    CREATE TABLE IF NOT EXISTS courses (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        code TEXT NOT NULL UNIQUE,
        name TEXT NOT NULL,
        credits INTEGER NOT NULL,
        department TEXT,
        professor_id INTEGER
    )
""")

# Table professors
cursor.execute("""
    CREATE TABLE IF NOT EXISTS professors (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        department TEXT NOT NULL,
        email TEXT UNIQUE,
        hire_date DATE
    )
""")

# Table enrollments (students <-> courses)
cursor.execute("""
    CREATE TABLE IF NOT EXISTS enrollments (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        student_id INTEGER NOT NULL,
        course_id INTEGER NOT NULL,
        semester TEXT NOT NULL,
        grade TEXT,
        FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE,
        FOREIGN KEY (course_id) REFERENCES courses(id) ON DELETE CASCADE,
        UNIQUE(student_id, course_id, semester)
    )
""")

# Insérer données de test
majors_data = [
    ('Informatique', 'Sciences', 180),
    ('Mathématiques', 'Sciences', 180),
    ('Physique', 'Sciences', 180),
    ('Littérature', 'Lettres', 120),
    ('Histoire', 'Lettres', 120)
]

cursor.executemany("""
    INSERT OR IGNORE INTO majors (name, department, credits_required)
    VALUES (?, ?, ?)
""", majors_data)

professors_data = [
    ('Dr. Alice Martin', 'Sciences', 'alice.martin@univ.fr', '2010-09-01'),
    ('Dr. Bob Durand', 'Sciences', 'bob.durand@univ.fr', '2012-01-15'),
    ('Dr. Claire Bernard', 'Lettres', 'claire.bernard@univ.fr', '2008-03-20'),
    ('Dr. David Lambert', 'Sciences', 'david.lambert@univ.fr', '2015-09-01')
]

cursor.executemany("""
    INSERT OR IGNORE INTO professors (name, department, email, hire_date)
    VALUES (?, ?, ?, ?)
""", professors_data)

students_data = [
    ('Alice', 'Dupont', 'alice.d@email.com', '2003-05-15', 1, 2022, 3.8),
    ('Bob', 'Martin', 'bob.m@email.com', '2002-08-22', 1, 2021, 3.2),
    ('Charlie', 'Durand', 'charlie.d@email.com', '2003-01-10', 2, 2022, 3.5),
    ('Diana', 'Lambert', 'diana.l@email.com', '2002-11-30', 1, 2021, 3.9),
    ('Eve', 'Bernard', 'eve.b@email.com', '2003-03-18', 3, 2023, 3.6),
    ('Frank', 'Moreau', 'frank.m@email.com', '2002-07-25', 2, 2022, 3.1),
    ('Grace', 'Simon', 'grace.s@email.com', '2003-09-12', 4, 2023, 3.7),
    ('Henry', 'Petit', 'henry.p@email.com', '2002-12-05', NULL, 2022, 2.9)
]

cursor.executemany("""
    INSERT OR IGNORE INTO students (first_name, last_name, email, birth_date, major_id, enrollment_year, gpa)
    VALUES (?, ?, ?, ?, ?, ?, ?)
""", students_data)

courses_data = [
    ('CS101', 'Introduction à Python', 3, 'Sciences', 1),
    ('CS102', 'Structures de données', 4, 'Sciences', 1),
    ('MATH201', 'Algèbre linéaire', 3, 'Sciences', 2),
    ('MATH202', 'Calcul différentiel', 4, 'Sciences', 2),
    ('PHY101', 'Physique générale', 4, 'Sciences', 4),
    ('LIT101', 'Littérature française', 3, 'Lettres', 3),
    ('HIST101', 'Histoire moderne', 3, 'Lettres', 3)
]

cursor.executemany("""
    INSERT OR IGNORE INTO courses (code, name, credits, department, professor_id)
    VALUES (?, ?, ?, ?, ?)
""", courses_data)

enrollments_data = [
    (1, 1, '2023-Fall', 'A'),
    (1, 2, '2023-Fall', 'B+'),
    (1, 3, '2023-Fall', 'A-'),
    (2, 1, '2023-Fall', 'B'),
    (2, 2, '2023-Fall', 'B-'),
    (3, 3, '2023-Fall', 'A'),
    (3, 4, '2023-Fall', 'A'),
    (4, 1, '2023-Fall', 'A'),
    (4, 2, '2023-Fall', 'A'),
    (5, 5, '2023-Fall', 'B+'),
    (6, 3, '2023-Fall', 'B'),
    (6, 4, '2023-Fall', 'B+'),
    (7, 6, '2023-Fall', 'A-')
]

cursor.executemany("""
    INSERT OR IGNORE INTO enrollments (student_id, course_id, semester, grade)
    VALUES (?, ?, ?, ?)
""", enrollments_data)

conn.commit()
print("[OK] Base de données de test créée !")


# ----------------------------------------------------------------------------
# [LIEN] JOIN - COMBINER DES TABLES
# ----------------------------------------------------------------------------

"""
[IDEE] POURQUOI JOIN ?

Problème : Données réparties dans plusieurs tables
Solution : JOIN pour combiner les données


[REFLEXION] EXEMPLE DU PROBLÈME :

Table students :
┌────┬───────┬──────────┬──────────┐
│ id │ name  │ email    │ major_id │
├────┼───────┼──────────┼──────────┤
│ 1  │ Alice │ alice@.. │    1     │
│ 2  │ Bob   │ bob@..   │    1     │
└────┴───────┴──────────┴──────────┘

Table majors :
┌────┬──────────────┐
│ id │     name     │
├────┼──────────────┤
│ 1  │ Informatique │
│ 2  │ Math         │
└────┴──────────────┘

[REFLEXION] Comment afficher "Alice - Informatique" ?
-> JOIN !


[DESIGN] TYPES DE JOIN :

┌──────────────┬────────────────────────────────┐
│   Type       │        Résultat                │
├──────────────┼────────────────────────────────┤
│ INNER JOIN   │ Seulement correspondances      │
│ LEFT JOIN    │ Toutes lignes gauche + match  │
│ RIGHT JOIN   │ Toutes lignes droite + match  │
│ FULL JOIN    │ Tout                           │
│ CROSS JOIN   │ Produit cartésien              │
└──────────────┴────────────────────────────────┘
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] INNER JOIN - Le plus courant
# ----------------------------------------------------------------------------

"""
[IDEE] INNER JOIN : Seulement les correspondances

SYNTAXE :

SELECT colonnes
FROM table1
INNER JOIN table2 ON table1.col = table2.col


[DESIGN] VISUALISATION :

Table A          Table B
   ┌───┐           ┌───┐
   │   │           │   │
   │ ┌─┴─────────┬─┴─┐ │
   │ │  INNER    │   │ │
   │ │   JOIN    │   │ │
   │ └─┬─────────┴─┬─┘ │
   │   │           │   │
   └───┘           └───┘
        ^
   Seulement cette partie


[IDEE] EXEMPLE 1 : Students + Majors
"""

cursor.execute("""
    SELECT 
        students.first_name,
        students.last_name,
        majors.name AS major_name
    FROM students
    INNER JOIN majors ON students.major_id = majors.id
""")

results = cursor.fetchall()
print("\n[LISTE] Étudiants avec leur spécialité (INNER JOIN) :")
for first, last, major in results:
    print(f"  {first} {last} - {major}")

"""
[IDEE] DÉCORTIQUONS :

FROM students
    Table principale (gauche)

INNER JOIN majors
    Table à joindre (droite)

ON students.major_id = majors.id
    Condition de jointure
    major_id (students) correspond à id (majors)


[ATTENTION] RÉSULTAT :
Seulement étudiants AVEC major
Henry (major_id = NULL) -> ABSENT !


[IDEE] EXEMPLE 2 : Students + Courses (via enrollments)
"""

cursor.execute("""
    SELECT 
        students.first_name || ' ' || students.last_name AS student_name,
        courses.code,
        courses.name AS course_name,
        enrollments.grade
    FROM students
    INNER JOIN enrollments ON students.id = enrollments.student_id
    INNER JOIN courses ON enrollments.course_id = courses.id
    ORDER BY student_name, courses.code
""")

results = cursor.fetchall()
print("\n[LISTE] Étudiants et leurs cours (DOUBLE JOIN) :")
for student, code, course, grade in results:
    print(f"  {student} | {code} - {course} | Note: {grade}")

"""
[IDEE] JOINTURES MULTIPLES :

FROM students          (Table 1)
JOIN enrollments       (Table 2) ON students.id = enrollments.student_id
JOIN courses           (Table 3) ON enrollments.course_id = courses.id

Chaîne : students -> enrollments -> courses


[IDEE] EXEMPLE 3 : Tout joindre (4 tables)
"""

cursor.execute("""
    SELECT 
        s.first_name || ' ' || s.last_name AS student,
        m.name AS major,
        c.code,
        c.name AS course,
        p.name AS professor,
        e.grade
    FROM students s
    INNER JOIN majors m ON s.major_id = m.id
    INNER JOIN enrollments e ON s.id = e.student_id
    INNER JOIN courses c ON e.course_id = c.id
    INNER JOIN professors p ON c.professor_id = p.id
    WHERE e.grade IS NOT NULL
    ORDER BY student, c.code
""")

results = cursor.fetchall()
print("\n[LISTE] Vue complète (4 JOINS) :")
for student, major, code, course, prof, grade in results[:5]:  # 5 premiers
    print(f"  {student} ({major}) | {code}: {course} | Prof: {prof} | {grade}")

"""
[IDEE] ALIAS DE TABLES :

students s      -> s.first_name
majors m        -> m.name
enrollments e   -> e.grade

Avantages :
[OK] Code plus court
[OK] Plus lisible
[OK] Évite répétitions


[CLE] BONNES PRATIQUES JOIN :

[OK] Utilisez alias courts et cohérents
[OK] Qualifiez TOUTES les colonnes (table.colonne)
[OK] Indentez pour lisibilité
[OK] Ordre : FROM -> JOIN -> JOIN -> WHERE -> ORDER BY
"""


# ----------------------------------------------------------------------------
# <- LEFT JOIN (LEFT OUTER JOIN)
# ----------------------------------------------------------------------------

"""
[IDEE] LEFT JOIN : TOUTES les lignes de gauche + correspondances

SYNTAXE identique à INNER JOIN, mais résultat différent


[DESIGN] VISUALISATION :

Table A          Table B
   ┌───┐           ┌───┐
   │ ┌─┴─────────┬─┴─┐ │
   │ │  LEFT     │   │ │
   │ │  JOIN     │   │ │
   │ └─┬─────────┴─┬─┘ │
   │   │           │   │
   └───┘           └───┘
   ^                ^
Tout A        + Correspondances B


[IDEE] EXEMPLE 1 : Tous les étudiants (même sans major)
"""

cursor.execute("""
    SELECT 
        students.first_name,
        students.last_name,
        majors.name AS major_name
    FROM students
    LEFT JOIN majors ON students.major_id = majors.id
    ORDER BY students.last_name
""")

results = cursor.fetchall()
print("\n[LISTE] Tous les étudiants (LEFT JOIN) :")
for first, last, major in results:
    major_display = major if major else "Pas de spécialité"
    print(f"  {first} {last} - {major_display}")

"""
[IDEE] RÉSULTAT :
Tous les étudiants, y compris Henry (major = NULL)


[CLE] DIFFÉRENCE INNER vs LEFT :

INNER JOIN :
- Henry absent (major_id = NULL)
- 7 lignes

LEFT JOIN :
- Henry présent avec major_name = NULL
- 8 lignes


[IDEE] EXEMPLE 2 : Cours sans inscription
"""

cursor.execute("""
    SELECT 
        courses.code,
        courses.name,
        COUNT(enrollments.id) AS enrollment_count
    FROM courses
    LEFT JOIN enrollments ON courses.id = enrollments.course_id
    GROUP BY courses.id, courses.code, courses.name
    ORDER BY enrollment_count DESC, courses.code
""")

results = cursor.fetchall()
print("\n[LISTE] Cours et nombre d'inscrits (LEFT JOIN) :")
for code, name, count in results:
    print(f"  {code} - {name} : {count} inscrit(s)")

"""
[IDEE] UTILITÉ :

LEFT JOIN permet de trouver :
- Étudiants sans major
- Cours sans inscription
- Professeurs sans cours
- Commandes sans paiement
- etc.


[IDEE] EXEMPLE 3 : Trouver étudiants sans inscription
"""

cursor.execute("""
    SELECT 
        s.first_name,
        s.last_name
    FROM students s
    LEFT JOIN enrollments e ON s.id = e.student_id
    WHERE e.id IS NULL
""")

results = cursor.fetchall()
print("\n[LISTE] Étudiants sans cours (LEFT JOIN + WHERE NULL) :")
for first, last in results:
    print(f"  {first} {last}")

"""
[IDEE] PATTERN : LEFT JOIN + WHERE NULL

Pour trouver "orphelins" (lignes sans correspondance)

SELECT A.*
FROM A
LEFT JOIN B ON A.id = B.a_id
WHERE B.id IS NULL

-> Toutes les lignes de A qui n'ont PAS de correspondance dans B
"""


# ----------------------------------------------------------------------------
# -> RIGHT JOIN (Pas SQLite, PostgreSQL/MySQL)
# ----------------------------------------------------------------------------

"""
[ATTENTION] SQLite NE SUPPORTE PAS RIGHT JOIN !

Mais vous pouvez inverser avec LEFT JOIN :

RIGHT JOIN :
SELECT * FROM A RIGHT JOIN B ...

Équivalent avec LEFT :
SELECT * FROM B LEFT JOIN A ...


[DESIGN] VISUALISATION :

Table A          Table B
   ┌───┐           ┌───┐
   │ ┌─┴─────────┬─┴─┐ │
   │ │  RIGHT    │   │ │
   │ │  JOIN     │   │ │
   │ └─┬─────────┴─┬─┘ │
   │   │           │   │
   └───┘           └───┘
        ^                ^
   Correspondances   + Tout B


[IDEE] EXEMPLE (PostgreSQL syntax) :

-- RIGHT JOIN
SELECT * FROM students RIGHT JOIN majors ON students.major_id = majors.id

-- Équivalent LEFT JOIN
SELECT * FROM majors LEFT JOIN students ON students.major_id = majors.id
"""


# ----------------------------------------------------------------------------
# [SYNC] FULL OUTER JOIN (Pas SQLite natif)
# ----------------------------------------------------------------------------

"""
[ATTENTION] SQLite NE SUPPORTE PAS FULL OUTER JOIN !

Solution : UNION de LEFT et RIGHT


[DESIGN] VISUALISATION :

Table A          Table B
   ┌───┐           ┌───┐
   │ ┌─┴─────────┬─┴─┐ │
   │ │   FULL    │   │ │
   │ │  OUTER    │   │ │
   │ └─┬─────────┴─┬─┘ │
   │   │           │   │
   └───┘           └───┘
   ^       ^           ^
 Tout A  Correspondances  Tout B


[IDEE] SIMULATION FULL OUTER (SQLite) :
"""

# LEFT JOIN
cursor.execute("""
    SELECT s.id AS student_id, s.first_name, m.id AS major_id, m.name AS major_name
    FROM students s
    LEFT JOIN majors m ON s.major_id = m.id
""")
left_results = cursor.fetchall()

# RIGHT JOIN (inversé)
cursor.execute("""
    SELECT s.id AS student_id, s.first_name, m.id AS major_id, m.name AS major_name
    FROM majors m
    LEFT JOIN students s ON s.major_id = m.id
    WHERE s.id IS NULL
""")
right_results = cursor.fetchall()

# Union
full_results = left_results + right_results

print("\n[LISTE] FULL OUTER JOIN simulé :")
for student_id, first_name, major_id, major_name in full_results[:10]:
    print(f"  Student: {first_name or 'N/A'} | Major: {major_name or 'N/A'}")


# ----------------------------------------------------------------------------
# [X] CROSS JOIN - Produit cartésien
# ----------------------------------------------------------------------------

"""
[IDEE] CROSS JOIN : TOUTES les combinaisons possibles

[ATTENTION] Rarement utilisé (très gourmand)


SYNTAXE :

SELECT *
FROM table1
CROSS JOIN table2


[DESIGN] EXEMPLE :

Table A (3 lignes)    Table B (2 lignes)
A1                    B1
A2                    B2
A3

CROSS JOIN -> 3 × 2 = 6 lignes :
A1, B1
A1, B2
A2, B1
A2, B2
A3, B1
A3, B2


[IDEE] EXEMPLE CONCRET : Combinaisons étudiants × cours
"""

cursor.execute("""
    SELECT 
        s.first_name || ' ' || s.last_name AS student,
        c.code || ' - ' || c.name AS course
    FROM students s
    CROSS JOIN courses c
    LIMIT 10
""")

results = cursor.fetchall()
print("\n[LISTE] CROSS JOIN (10 premières combinaisons) :")
for student, course in results:
    print(f"  {student} × {course}")

"""
[IDEE] USAGES RARES :

[OK] Générer toutes les combinaisons (tests)
[OK] Calendriers (jours × créneaux)
[OK] Grilles (X × Y)

[X] NE PAS utiliser pour données normales !
  -> Performance terrible avec grandes tables
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] SELF JOIN - Joindre une table avec elle-même
# ----------------------------------------------------------------------------

"""
[IDEE] SELF JOIN : Comparer lignes de la même table

Cas d'usage :
- Hiérarchies (employé -> manager)
- Comparaisons (étudiants avec même major)
- Relations (amis, followers)


[IDEE] EXEMPLE : Créer table avec hiérarchie
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS employees (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        manager_id INTEGER,
        FOREIGN KEY (manager_id) REFERENCES employees(id)
    )
""")

employees_data = [
    (1, 'CEO Alice', NULL),
    (2, 'VP Bob', 1),
    (3, 'VP Charlie', 1),
    (4, 'Manager Diana', 2),
    (5, 'Manager Eve', 2),
    (6, 'Employee Frank', 4),
    (7, 'Employee Grace', 4)
]

cursor.executemany("INSERT OR IGNORE INTO employees VALUES (?, ?, ?)", employees_data)

# SELF JOIN
cursor.execute("""
    SELECT 
        e.name AS employee,
        m.name AS manager
    FROM employees e
    LEFT JOIN employees m ON e.manager_id = m.id
    ORDER BY e.id
""")

results = cursor.fetchall()
print("\n[LISTE] SELF JOIN (employés et managers) :")
for emp, mgr in results:
    mgr_display = mgr if mgr else "PDG (pas de manager)"
    print(f"  {emp} -> Manager: {mgr_display}")

"""
[IDEE] ASTUCE SELF JOIN :

Utilisez ALIAS différents :
- e pour employees (employés)
- m pour managers (managers)

Sinon confusion !


[IDEE] EXEMPLE 2 : Étudiants du même major
"""

cursor.execute("""
    SELECT 
        s1.first_name || ' ' || s1.last_name AS student1,
        s2.first_name || ' ' || s2.last_name AS student2,
        m.name AS major
    FROM students s1
    INNER JOIN students s2 ON s1.major_id = s2.major_id AND s1.id < s2.id
    INNER JOIN majors m ON s1.major_id = m.id
    ORDER BY m.name, s1.last_name
""")

results = cursor.fetchall()
print("\n[LISTE] SELF JOIN (étudiants du même major) :")
for s1, s2, major in results[:10]:
    print(f"  {s1} & {s2} -> {major}")

"""
[IDEE] s1.id < s2.id

Évite doublons :
Alice-Bob [OK]
Bob-Alice [X] (déjà eu Alice-Bob)
"""


# ----------------------------------------------------------------------------
# [MELANGE] UNION et UNION ALL
# ----------------------------------------------------------------------------

"""
[IDEE] UNION : Combiner résultats de plusieurs SELECT

SYNTAXE :

SELECT col1, col2 FROM table1
UNION
SELECT col1, col2 FROM table2


[CLE] RÈGLES :

1. Même NOMBRE de colonnes
2. Types COMPATIBLES
3. Noms colonnes du 1er SELECT


[IDEE] UNION vs UNION ALL :

UNION :
- Élimine doublons
- Plus lent (tri nécessaire)

UNION ALL :
- Garde doublons
- Plus rapide


[IDEE] EXEMPLE 1 : Liste combinée étudiants + professeurs
"""

cursor.execute("""
    SELECT first_name || ' ' || last_name AS name, 'Étudiant' AS type
    FROM students
    UNION
    SELECT name, 'Professeur' AS type
    FROM professors
    ORDER BY name
""")

results = cursor.fetchall()
print("\n[LISTE] UNION (étudiants + professeurs) :")
for name, type_ in results[:10]:
    print(f"  {name} ({type_})")

"""
[IDEE] EXEMPLE 2 : UNION ALL (avec doublons)
"""

cursor.execute("""
    SELECT 'Sciences' AS department, COUNT(*) AS count
    FROM students WHERE major_id IN (1, 2, 3)
    
    UNION ALL
    
    SELECT department, COUNT(*)
    FROM professors
    GROUP BY department
    
    ORDER BY department
""")

results = cursor.fetchall()
print("\n[LISTE] UNION ALL (comptages par département) :")
for dept, count in results:
    print(f"  {dept}: {count}")

"""
[IDEE] USAGES UNION :

[OK] Combiner tables similaires (archives + actuel)
[OK] Requêtes complexes (cas A OU cas B)
[OK] Rapports consolidés
[OK] Migrations de données
"""


# ----------------------------------------------------------------------------
# [PACKAGE] SOUS-REQUÊTES (Subqueries)
# ----------------------------------------------------------------------------

"""
[IDEE] SOUS-REQUÊTE : SELECT dans un SELECT

Types :
1. Sous-requête scalaire (une valeur)
2. Sous-requête de ligne (plusieurs colonnes)
3. Sous-requête de table (plusieurs lignes)


[OBJECTIF] SOUS-REQUÊTE SCALAIRE (une valeur)
"""

# Étudiant avec le meilleur GPA
cursor.execute("""
    SELECT first_name, last_name, gpa
    FROM students
    WHERE gpa = (SELECT MAX(gpa) FROM students)
""")

result = cursor.fetchone()
print(f"\n[TROPHEE] Meilleur GPA : {result[0]} {result[1]} ({result[2]})")

"""
[IDEE] DÉCORTIQUONS :

WHERE gpa = (SELECT MAX(gpa) FROM students)
                     ^
            Sous-requête : retourne une seule valeur (3.9)

Équivalent à :
WHERE gpa = 3.9


[IDEE] EXEMPLE 2 : Au-dessus de la moyenne
"""

cursor.execute("""
    SELECT 
        first_name || ' ' || last_name AS student,
        gpa,
        ROUND((SELECT AVG(gpa) FROM students), 2) AS avg_gpa
    FROM students
    WHERE gpa > (SELECT AVG(gpa) FROM students)
    ORDER BY gpa DESC
""")

results = cursor.fetchall()
print("\n[LISTE] Étudiants au-dessus de la moyenne :")
for student, gpa, avg in results:
    print(f"  {student}: {gpa} (moyenne: {avg})")


"""
[OBJECTIF] SOUS-REQUÊTE IN (liste de valeurs)
"""

# Étudiants inscrits dans des cours de Sciences
cursor.execute("""
    SELECT DISTINCT first_name || ' ' || last_name AS student
    FROM students
    WHERE id IN (
        SELECT DISTINCT student_id
        FROM enrollments
        WHERE course_id IN (
            SELECT id
            FROM courses
            WHERE department = 'Sciences'
        )
    )
    ORDER BY student
""")

results = cursor.fetchall()
print("\n[LISTE] Étudiants en cours de Sciences (sous-requêtes imbriquées) :")
for (student,) in results:
    print(f"  {student}")

"""
[IDEE] Sous-requêtes IMBRIQUÉES :

3 niveaux :
1. courses avec department = 'Sciences' -> IDs
2. enrollments avec ces course_id -> student_ids
3. students avec ces IDs -> noms


[OBJECTIF] SOUS-REQUÊTE EXISTS (vérifier existence)
"""

# Majors avec au moins un étudiant
cursor.execute("""
    SELECT name
    FROM majors m
    WHERE EXISTS (
        SELECT 1
        FROM students s
        WHERE s.major_id = m.id
    )
""")

results = cursor.fetchall()
print("\n[LISTE] Majors avec étudiants (EXISTS) :")
for (major,) in results:
    print(f"  {major}")

"""
[IDEE] EXISTS vs IN :

EXISTS :
[OK] Plus rapide (arrête dès qu'il trouve)
[OK] Mieux pour grandes tables
[OK] Peut utiliser corrélation

IN :
[OK] Plus simple à comprendre
[OK] Bon pour petites listes


[OBJECTIF] SOUS-REQUÊTE NOT EXISTS (absence)
"""

# Cours sans inscription
cursor.execute("""
    SELECT code, name
    FROM courses c
    WHERE NOT EXISTS (
        SELECT 1
        FROM enrollments e
        WHERE e.course_id = c.id
    )
""")

results = cursor.fetchall()
print("\n[LISTE] Cours sans inscription (NOT EXISTS) :")
for code, name in results:
    print(f"  {code} - {name}")


"""
[OBJECTIF] SOUS-REQUÊTE CORRÉLÉE
"""

# Pour chaque étudiant, compter ses cours
cursor.execute("""
    SELECT 
        s.first_name || ' ' || s.last_name AS student,
        (
            SELECT COUNT(*)
            FROM enrollments e
            WHERE e.student_id = s.id
        ) AS course_count
    FROM students s
    ORDER BY course_count DESC, student
""")

results = cursor.fetchall()
print("\n[LISTE] Nombre de cours par étudiant (sous-requête corrélée) :")
for student, count in results:
    print(f"  {student}: {count} cours")

"""
[IDEE] SOUS-REQUÊTE CORRÉLÉE :

WHERE e.student_id = s.id
           ^           ^
      Table interne  Table externe

Référence la ligne actuelle de la requête externe
Exécutée POUR CHAQUE ligne externe
-> Peut être lente !


[OBJECTIF] SOUS-REQUÊTE DANS FROM (table dérivée)
"""

cursor.execute("""
    SELECT 
        dept,
        AVG(avg_gpa) AS department_avg
    FROM (
        SELECT 
            m.department AS dept,
            AVG(s.gpa) AS avg_gpa
        FROM students s
        JOIN majors m ON s.major_id = m.id
        GROUP BY m.id, m.department
    ) AS major_averages
    GROUP BY dept
""")

results = cursor.fetchall()
print("\n[LISTE] GPA moyen par département (sous-requête dans FROM) :")
for dept, avg in results:
    print(f"  {dept}: {avg:.2f}")

"""
[IDEE] Table dérivée dans FROM :

FROM (...) AS alias

Crée table temporaire utilisable comme table normale
ALIAS OBLIGATOIRE !
"""


# ----------------------------------------------------------------------------
# [NOTE] WITH (Common Table Expressions - CTE)
# ----------------------------------------------------------------------------

"""
[IDEE] WITH : Requêtes temporaires nommées

Avantages vs sous-requêtes :
[OK] Plus lisible
[OK] Réutilisable
[OK] Peut être récursif


SYNTAXE :

WITH nom_cte AS (
    SELECT ...
)
SELECT ...
FROM nom_cte


[IDEE] EXEMPLE 1 : CTE simple
"""

cursor.execute("""
    WITH high_gpa_students AS (
        SELECT id, first_name, last_name, gpa
        FROM students
        WHERE gpa >= 3.5
    )
    SELECT 
        first_name || ' ' || last_name AS student,
        gpa
    FROM high_gpa_students
    ORDER BY gpa DESC
""")

results = cursor.fetchall()
print("\n[LISTE] CTE : Étudiants excellent GPA (≥3.5) :")
for student, gpa in results:
    print(f"  {student}: {gpa}")

"""
[IDEE] EXEMPLE 2 : Plusieurs CTE
"""

cursor.execute("""
    WITH 
    student_courses AS (
        SELECT 
            student_id,
            COUNT(*) AS course_count
        FROM enrollments
        GROUP BY student_id
    ),
    student_info AS (
        SELECT 
            id,
            first_name || ' ' || last_name AS name,
            gpa
        FROM students
    )
    SELECT 
        si.name,
        si.gpa,
        COALESCE(sc.course_count, 0) AS courses
    FROM student_info si
    LEFT JOIN student_courses sc ON si.id = sc.student_id
    ORDER BY courses DESC, si.gpa DESC
""")

results = cursor.fetchall()
print("\n[LISTE] CTE multiples : Étudiants avec cours et GPA :")
for name, gpa, courses in results:
    print(f"  {name}: {courses} cours, GPA {gpa}")

"""
[IDEE] POURQUOI WITH ?

[X] SANS WITH (sous-requêtes imbriquées) :
SELECT ...
FROM (SELECT ... FROM (SELECT ... FROM ...))

[OK] AVEC WITH (clair et modulaire) :
WITH step1 AS (SELECT ...),
     step2 AS (SELECT ... FROM step1)
SELECT ... FROM step2


[HOT] CTE RÉCURSIVE (avancé)
"""

# Créer séquence de nombres 1 à 10
cursor.execute("""
    WITH RECURSIVE numbers(n) AS (
        SELECT 1
        UNION ALL
        SELECT n + 1 FROM numbers WHERE n < 10
    )
    SELECT n FROM numbers
""")

results = cursor.fetchall()
print("\n[LISTE] CTE récursive (nombres 1-10) :")
print("  " + ", ".join(str(n[0]) for n in results))

"""
[IDEE] CTE RÉCURSIVE :

1. Cas de base : SELECT 1
2. Cas récursif : SELECT n + 1 FROM numbers
3. Condition d'arrêt : WHERE n < 10

Usages :
- Hiérarchies (arbre organisationnel)
- Graphes
- Séquences
- Dates consécutives
"""

# Hiérarchie employés (récursif)
cursor.execute("""
    WITH RECURSIVE employee_hierarchy AS (
        -- Cas de base : CEO (pas de manager)
        SELECT id, name, manager_id, 0 AS level
        FROM employees
        WHERE manager_id IS NULL
        
        UNION ALL
        
        -- Cas récursif : employés avec manager
        SELECT e.id, e.name, e.manager_id, eh.level + 1
        FROM employees e
        JOIN employee_hierarchy eh ON e.manager_id = eh.id
    )
    SELECT 
        SUBSTR('                    ', 1, level * 2) || name AS hierarchy,
        level
    FROM employee_hierarchy
    ORDER BY level, name
""")

results = cursor.fetchall()
print("\n[LISTE] Hiérarchie organisationnelle (CTE récursive) :")
for hierarchy, level in results:
    print(f"  {hierarchy}")


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 3 : Requêtes Complexes
# ----------------------------------------------------------------------------

"""
OBJECTIF : Maîtriser JOIN, sous-requêtes et CTE


CAHIER DES CHARGES :

1. Liste complète : étudiant, major, cours, professeur, note
   (4 JOINS)

2. Étudiants n'ayant aucun cours
   (LEFT JOIN + WHERE NULL)

3. Top 3 cours les plus populaires
   (JOIN + GROUP BY + ORDER BY + LIMIT)

4. Étudiants avec GPA > moyenne de leur major
   (Sous-requête corrélée)

5. Statistiques par département avec CTE
   (WITH + agrégations)


SOLUTIONS :
"""

# 1. Vue complète
print("\n" + "="*60)
print("EXERCICE 1 : Vue complète (4 JOINS)")
print("="*60)

cursor.execute("""
    SELECT 
        s.first_name || ' ' || s.last_name AS student,
        m.name AS major,
        c.code,
        c.name AS course,
        p.name AS professor,
        e.grade
    FROM students s
    LEFT JOIN majors m ON s.major_id = m.id
    LEFT JOIN enrollments e ON s.id = e.student_id
    LEFT JOIN courses c ON e.course_id = c.id
    LEFT JOIN professors p ON c.professor_id = p.id
    WHERE e.id IS NOT NULL
    ORDER BY student, c.code
    LIMIT 10
""")

for row in cursor.fetchall():
    print(f"  {row[0]} ({row[1]}) | {row[2]}: {row[3]} | Prof: {row[4]} | {row[5]}")


# 2. Étudiants sans cours
print("\n" + "="*60)
print("EXERCICE 2 : Étudiants sans cours (LEFT JOIN + WHERE NULL)")
print("="*60)

cursor.execute("""
    SELECT s.first_name || ' ' || s.last_name AS student
    FROM students s
    LEFT JOIN enrollments e ON s.id = e.student_id
    WHERE e.id IS NULL
""")

for (student,) in cursor.fetchall():
    print(f"  {student}")


# 3. Top cours populaires
print("\n" + "="*60)
print("EXERCICE 3 : Top 3 cours (GROUP BY + LIMIT)")
print("="*60)

cursor.execute("""
    SELECT 
        c.code,
        c.name,
        COUNT(e.id) AS enrollment_count
    FROM courses c
    LEFT JOIN enrollments e ON c.id = e.course_id
    GROUP BY c.id, c.code, c.name
    ORDER BY enrollment_count DESC, c.code
    LIMIT 3
""")

for code, name, count in cursor.fetchall():
    print(f"  {code} - {name}: {count} inscriptions")


# 4. Au-dessus moyenne du major
print("\n" + "="*60)
print("EXERCICE 4 : GPA > moyenne du major (sous-requête corrélée)")
print("="*60)

cursor.execute("""
    SELECT 
        s.first_name || ' ' || s.last_name AS student,
        m.name AS major,
        s.gpa,
        ROUND((
            SELECT AVG(s2.gpa)
            FROM students s2
            WHERE s2.major_id = s.major_id
        ), 2) AS major_avg
    FROM students s
    JOIN majors m ON s.major_id = m.id
    WHERE s.gpa > (
        SELECT AVG(s2.gpa)
        FROM students s2
        WHERE s2.major_id = s.major_id
    )
    ORDER BY m.name, s.gpa DESC
""")

for student, major, gpa, avg in cursor.fetchall():
    print(f"  {student} ({major}): {gpa} > {avg}")


# 5. Stats par département (CTE)
print("\n" + "="*60)
print("EXERCICE 5 : Statistiques département (WITH)")
print("="*60)

cursor.execute("""
    WITH dept_stats AS (
        SELECT 
            m.department,
            COUNT(DISTINCT s.id) AS student_count,
            AVG(s.gpa) AS avg_gpa,
            COUNT(DISTINCT e.course_id) AS distinct_courses
        FROM majors m
        LEFT JOIN students s ON m.id = s.major_id
        LEFT JOIN enrollments e ON s.id = e.student_id
        GROUP BY m.department
    )
    SELECT 
        department,
        student_count,
        ROUND(avg_gpa, 2) AS avg_gpa,
        distinct_courses
    FROM dept_stats
    ORDER BY student_count DESC
""")

print(f"  {'Département':<15} {'Étudiants':<12} {'GPA moyen':<12} {'Cours'}")
print("  " + "-"*50)
for dept, students, gpa, courses in cursor.fetchall():
    print(f"  {dept:<15} {students:<12} {gpa:<12} {courses}")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 5
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

JOINS :
- INNER JOIN : Correspondances seulement
- LEFT JOIN : Toutes gauche + correspondances
- RIGHT JOIN : PostgreSQL/MySQL (inverser LEFT)
- CROSS JOIN : Produit cartésien (rare)
- SELF JOIN : Table avec elle-même

UNION :
- UNION : Combiner sans doublons
- UNION ALL : Combiner avec doublons

SOUS-REQUÊTES :
- Scalaire : Une valeur
- IN/NOT IN : Liste
- EXISTS/NOT EXISTS : Vérifier existence
- Corrélée : Référence requête externe
- Dans FROM : Table dérivée

CTE (WITH) :
- Requêtes nommées réutilisables
- Plus lisible que sous-requêtes
- CTE récursives pour hiérarchies


[CLE] PATTERNS ESSENTIELS :

Trouver orphelins :
SELECT A.* FROM A LEFT JOIN B ON ... WHERE B.id IS NULL

Top N :
SELECT ... ORDER BY ... LIMIT N

Au-dessus moyenne :
WHERE valeur > (SELECT AVG(valeur) FROM table)

Hiérarchie :
WITH RECURSIVE cte AS (base UNION ALL recursif)


-> PROCHAINE ÉTAPE : Chapitre 6 - Fonctions et Agrégations

Vous allez maîtriser COUNT, SUM, AVG, GROUP BY, HAVING !
"""


# ============================================================================
# [GUIDE] CHAPITRE 6 : FONCTIONS ET AGRÉGATIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser fonctions d'agrégation (COUNT, SUM, AVG, MIN, MAX)
[OK] Grouper données (GROUP BY)
[OK] Filtrer groupes (HAVING)
[OK] Fonctions de chaînes
[OK] Fonctions de dates
[OK] Fonctions mathématiques
[OK] Fonctions conditionnelles (CASE WHEN)
[OK] Fonctions de fenêtre (Window Functions)
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] FONCTIONS D'AGRÉGATION
# ----------------------------------------------------------------------------

"""
[IDEE] AGRÉGATION : Calculer UNE valeur à partir de PLUSIEURS lignes


[DESIGN] FONCTIONS DE BASE :

┌──────────┬────────────────────────────┐
│ Fonction │        Description         │
├──────────┼────────────────────────────┤
│ COUNT()  │ Compter lignes             │
│ SUM()    │ Somme                      │
│ AVG()    │ Moyenne                    │
│ MIN()    │ Minimum                    │
│ MAX()    │ Maximum                    │
└──────────┴────────────────────────────┘


1. COUNT() - Compter
────────────────────
"""

# Nombre total d'étudiants
cursor.execute("SELECT COUNT(*) FROM students")
print(f"\n[GRAPHIQUE] Nombre d'étudiants : {cursor.fetchone()[0]}")

# Nombre d'étudiants avec email
cursor.execute("SELECT COUNT(email) FROM students")
print(f"Étudiants avec email : {cursor.fetchone()[0]}")

# Nombre d'étudiants avec major
cursor.execute("SELECT COUNT(major_id) FROM students")
print(f"Étudiants avec major : {cursor.fetchone()[0]}")

# Nombre de majors différents
cursor.execute("SELECT COUNT(DISTINCT major_id) FROM students")
print(f"Majors différents : {cursor.fetchone()[0]}")

"""
[IDEE] COUNT(*) vs COUNT(colonne) :

COUNT(*) :
- Compte TOUTES les lignes
- Même avec NULL

COUNT(colonne) :
- Compte lignes où colonne IS NOT NULL
- Ignore NULL

COUNT(DISTINCT colonne) :
- Valeurs uniques seulement


2. SUM() - Somme
────────────────
"""

# Total de crédits de tous les cours
cursor.execute("SELECT SUM(credits) FROM courses")
print(f"\n[GRAPHIQUE] Total crédits offerts : {cursor.fetchone()[0]}")

# Total crédits par département
cursor.execute("""
    SELECT department, SUM(credits) AS total_credits
    FROM courses
    GROUP BY department
    ORDER BY total_credits DESC
""")

print("\nCrédits par département :")
for dept, total in cursor.fetchall():
    print(f"  {dept}: {total} crédits")

"""
[IDEE] SUM() ignore NULL
Si colonne a NULL -> pas inclus dans somme


3. AVG() - Moyenne
──────────────────
"""

# GPA moyen
cursor.execute("SELECT AVG(gpa) FROM students")
print(f"\n[GRAPHIQUE] GPA moyen : {cursor.fetchone()[0]:.2f}")

# GPA moyen par major
cursor.execute("""
    SELECT 
        m.name AS major,
        ROUND(AVG(s.gpa), 2) AS avg_gpa,
        COUNT(s.id) AS student_count
    FROM students s
    JOIN majors m ON s.major_id = m.id
    GROUP BY m.id, m.name
    ORDER BY avg_gpa DESC
""")

print("\nGPA moyen par major :")
for major, avg, count in cursor.fetchall():
    print(f"  {major}: {avg} ({count} étudiants)")

"""
[IDEE] AVG() ignore NULL
Moyenne calculée seulement sur valeurs non-NULL


4. MIN() et MAX()
─────────────────
"""

# GPA min et max
cursor.execute("SELECT MIN(gpa), MAX(gpa) FROM students")
min_gpa, max_gpa = cursor.fetchone()
print(f"\n[GRAPHIQUE] GPA min: {min_gpa}, max: {max_gpa}")

# Plus ancien et plus récent professeur
cursor.execute("""
    SELECT 
        MIN(hire_date) AS first_hire,
        MAX(hire_date) AS last_hire
    FROM professors
""")

first, last = cursor.fetchone()
print(f"\nProfesseurs : premier embauché {first}, dernier {last}")

"""
[IDEE] MIN/MAX fonctionnent aussi avec :
- Texte (ordre alphabétique)
- Dates (ordre chronologique)
"""

# Étudiant alphabétiquement premier et dernier
cursor.execute("""
    SELECT 
        MIN(last_name) AS first_alpha,
        MAX(last_name) AS last_alpha
    FROM students
""")

print(f"Noms alphabétiques : {cursor.fetchone()}")


# ----------------------------------------------------------------------------
# [UTILISATEURS] GROUP BY - Grouper les données
# ----------------------------------------------------------------------------

"""
[IDEE] GROUP BY : Créer des groupes pour agréger

SYNTAXE :

SELECT colonne, AGREGATION(...)
FROM table
GROUP BY colonne


[CLE] RÈGLE IMPORTANTE :

Toute colonne dans SELECT (hors agrégation) doit être dans GROUP BY !


[X] MAUVAIS :
SELECT major_id, name, AVG(gpa)  -- name pas dans GROUP BY
FROM students
GROUP BY major_id

[OK] BON :
SELECT major_id, AVG(gpa)
FROM students
GROUP BY major_id


[IDEE] EXEMPLE 1 : Compter par groupe
"""

cursor.execute("""
    SELECT 
        enrollment_year,
        COUNT(*) AS student_count
    FROM students
    GROUP BY enrollment_year
    ORDER BY enrollment_year
""")

print("\n[GRAPHIQUE] Étudiants par année d'inscription :")
for year, count in cursor.fetchall():
    print(f"  {year}: {count} étudiants")

"""
[IDEE] EXEMPLE 2 : GROUP BY multiple colonnes
"""

cursor.execute("""
    SELECT 
        m.department,
        s.enrollment_year,
        COUNT(*) AS count
    FROM students s
    JOIN majors m ON s.major_id = m.id
    GROUP BY m.department, s.enrollment_year
    ORDER BY m.department, s.enrollment_year
""")

print("\n[GRAPHIQUE] Étudiants par département et année :")
for dept, year, count in cursor.fetchall():
    print(f"  {dept} {year}: {count}")

"""
[IDEE] EXEMPLE 3 : Plusieurs agrégations
"""

cursor.execute("""
    SELECT 
        m.name AS major,
        COUNT(s.id) AS students,
        ROUND(AVG(s.gpa), 2) AS avg_gpa,
        MIN(s.gpa) AS min_gpa,
        MAX(s.gpa) AS max_gpa
    FROM majors m
    LEFT JOIN students s ON m.id = s.major_id
    GROUP BY m.id, m.name
    HAVING COUNT(s.id) > 0
    ORDER BY students DESC
""")

print("\n[GRAPHIQUE] Statistiques complètes par major :")
print(f"  {'Major':<15} {'Étudiants':<12} {'Moy':<6} {'Min':<6} {'Max'}")
print("  " + "-"*50)
for major, students, avg, min_val, max_val in cursor.fetchall():
    print(f"  {major:<15} {students:<12} {avg:<6} {min_val:<6} {max_val}")


# ----------------------------------------------------------------------------
# [RECHERCHE] HAVING - Filtrer les groupes
# ----------------------------------------------------------------------------

"""
[IDEE] HAVING : WHERE pour les groupes

Différence WHERE vs HAVING :

WHERE :
- Filtre AVANT groupement
- Filtre lignes individuelles
- Pas d'agrégations

HAVING :
- Filtre APRÈS groupement
- Filtre groupes
- Avec agrégations


[CLE] ORDRE D'EXÉCUTION :

1. FROM
2. WHERE     <- Filtre lignes
3. GROUP BY
4. HAVING    <- Filtre groupes
5. SELECT
6. ORDER BY


[IDEE] EXEMPLE 1 : Majors avec plus de 2 étudiants
"""

cursor.execute("""
    SELECT 
        m.name AS major,
        COUNT(s.id) AS student_count
    FROM majors m
    LEFT JOIN students s ON m.id = s.major_id
    GROUP BY m.id, m.name
    HAVING COUNT(s.id) > 2
    ORDER BY student_count DESC
""")

print("\n[GRAPHIQUE] Majors avec > 2 étudiants (HAVING) :")
for major, count in cursor.fetchall():
    print(f"  {major}: {count}")

"""
[IDEE] EXEMPLE 2 : WHERE + HAVING
"""

cursor.execute("""
    SELECT 
        m.name AS major,
        COUNT(s.id) AS high_achievers
    FROM majors m
    JOIN students s ON m.id = s.major_id
    WHERE s.gpa >= 3.5                    -- WHERE : filtre étudiants
    GROUP BY m.id, m.name
    HAVING COUNT(s.id) >= 2               -- HAVING : filtre groupes
    ORDER BY high_achievers DESC
""")

print("\n[GRAPHIQUE] Majors avec ≥2 étudiants excellents (GPA≥3.5) :")
for major, count in cursor.fetchall():
    print(f"  {major}: {count} high achievers")

"""
[IDEE] EXEMPLE 3 : Cours populaires (>3 inscriptions)
"""

cursor.execute("""
    SELECT 
        c.code,
        c.name,
        COUNT(e.id) AS enrollments
    FROM courses c
    JOIN enrollments e ON c.id = e.course_id
    GROUP BY c.id, c.code, c.name
    HAVING COUNT(e.id) > 3
    ORDER BY enrollments DESC
""")

print("\n[GRAPHIQUE] Cours très populaires (>3 inscriptions) :")
for code, name, count in cursor.fetchall():
    print(f"  {code} - {name}: {count}")


# ----------------------------------------------------------------------------
# [TEXTE] FONCTIONS DE CHAÎNES
# ----------------------------------------------------------------------------

"""
[IDEE] FONCTIONS TEXTE COURANTES


1. Concaténation
────────────────
"""

cursor.execute("""
    SELECT 
        first_name || ' ' || last_name AS full_name,
        UPPER(email) AS email_upper
    FROM students
    LIMIT 3
""")

print("\n[NOTE] Concaténation :")
for name, email in cursor.fetchall():
    print(f"  {name} - {email}")

"""
2. UPPER() / LOWER() / TRIM()
──────────────────────────────
"""

cursor.execute("""
    SELECT 
        UPPER('hello') AS upper_case,
        LOWER('WORLD') AS lower_case,
        TRIM('  spaces  ') AS trimmed
""")

print(f"\n[NOTE] Transformations : {cursor.fetchone()}")

"""
3. LENGTH() / SUBSTR()
──────────────────────
"""

cursor.execute("""
    SELECT 
        name,
        LENGTH(name) AS name_length,
        SUBSTR(name, 1, 3) AS first_3_chars
    FROM majors
""")

print("\n[NOTE] Longueur et sous-chaînes :")
for name, length, first_3 in cursor.fetchall():
    print(f"  {name} ({length} chars) -> {first_3}...")

"""
4. REPLACE()
────────────
"""

cursor.execute("""
    SELECT 
        name,
        REPLACE(name, 'é', 'e') AS normalized
    FROM professors
    LIMIT 2
""")

print("\n[NOTE] Remplacement :")
for original, normalized in cursor.fetchall():
    print(f"  {original} -> {normalized}")

"""
5. INSTR() / LIKE
─────────────────
"""

# Position de '@' dans email
cursor.execute("""
    SELECT 
        email,
        INSTR(email, '@') AS at_position
    FROM students
    WHERE email IS NOT NULL
    LIMIT 3
""")

print("\n[NOTE] Position du @ :")
for email, pos in cursor.fetchall():
    print(f"  {email} -> @ à position {pos}")


# ----------------------------------------------------------------------------
# [CALENDRIER] FONCTIONS DE DATES
# ----------------------------------------------------------------------------

"""
[IDEE] FONCTIONS DATE/HEURE SQLite


1. Date actuelle
────────────────
"""

cursor.execute("""
    SELECT 
        DATE('now') AS today,
        TIME('now') AS current_time,
        DATETIME('now') AS current_datetime
""")

print(f"\n[CALENDRIER] Date/Heure actuelle : {cursor.fetchone()}")

"""
2. Calculer âge
───────────────
"""

cursor.execute("""
    SELECT 
        first_name || ' ' || last_name AS student,
        birth_date,
        CAST((JULIANDAY('now') - JULIANDAY(birth_date)) / 365.25 AS INTEGER) AS age
    FROM students
    WHERE birth_date IS NOT NULL
    ORDER BY age DESC
    LIMIT 5
""")

print("\n[CALENDRIER] Âge des étudiants :")
for name, birth, age in cursor.fetchall():
    print(f"  {name} (né le {birth}) : {age} ans")

"""
3. Ajouter/Soustraire jours
───────────────────────────
"""

cursor.execute("""
    SELECT 
        DATE('now') AS today,
        DATE('now', '+7 days') AS next_week,
        DATE('now', '-1 month') AS last_month,
        DATE('now', '+1 year') AS next_year
""")

print(f"\n[CALENDRIER] Calculs de dates : {cursor.fetchone()}")

"""
4. Extraire composants
──────────────────────
"""

cursor.execute("""
    SELECT 
        hire_date,
        STRFTIME('%Y', hire_date) AS year,
        STRFTIME('%m', hire_date) AS month,
        STRFTIME('%d', hire_date) AS day,
        STRFTIME('%w', hire_date) AS day_of_week
    FROM professors
    LIMIT 2
""")

print("\n[CALENDRIER] Composants de date :")
for date, year, month, day, dow in cursor.fetchall():
    print(f"  {date} -> Année:{year}, Mois:{month}, Jour:{day}, DoW:{dow}")

"""
[IDEE] STRFTIME formats :

%Y - Année (4 chiffres)
%m - Mois (01-12)
%d - Jour (01-31)
%H - Heure (00-23)
%M - Minutes (00-59)
%S - Secondes (00-59)
%w - Jour de semaine (0-6, 0=dimanche)
%j - Jour de l'année (001-366)
%W - Semaine de l'année (00-53)
"""


# ----------------------------------------------------------------------------
# [NOMBRE] FONCTIONS MATHÉMATIQUES
# ----------------------------------------------------------------------------

"""
[IDEE] FONCTIONS MATH SQLite


1. ABS() / ROUND()
──────────────────
"""

cursor.execute("""
    SELECT 
        ABS(-42) AS absolute,
        ROUND(3.14159, 2) AS rounded,
        ROUND(3.7) AS rounded_int
""")

print(f"\n[NOMBRE] Math de base : {cursor.fetchone()}")

"""
2. MIN() / MAX() sur calculs
────────────────────────────
"""

cursor.execute("""
    SELECT 
        gpa,
        ROUND(gpa * 25, 0) AS percentage
    FROM students
    WHERE gpa IS NOT NULL
    ORDER BY gpa DESC
    LIMIT 5
""")

print("\n[NOMBRE] GPA en pourcentage :")
for gpa, pct in cursor.fetchall():
    print(f"  GPA {gpa} = {pct}%")

"""
3. RANDOM()
───────────
"""

cursor.execute("""
    SELECT 
        first_name || ' ' || last_name AS student,
        ABS(RANDOM() % 100) AS random_number
    FROM students
    LIMIT 5
""")

print("\n[NOMBRE] Nombres aléatoires :")
for name, rand in cursor.fetchall():
    print(f"  {name}: {rand}")


# ----------------------------------------------------------------------------
# [MELANGE] CASE WHEN - Logique conditionnelle
# ----------------------------------------------------------------------------

"""
[IDEE] CASE WHEN : IF/ELSE en SQL

SYNTAXE :

CASE
    WHEN condition1 THEN resultat1
    WHEN condition2 THEN resultat2
    ELSE resultat_defaut
END


[IDEE] EXEMPLE 1 : Catégoriser GPA
"""

cursor.execute("""
    SELECT 
        first_name || ' ' || last_name AS student,
        gpa,
        CASE
            WHEN gpa >= 3.7 THEN 'Excellent'
            WHEN gpa >= 3.3 THEN 'Très bien'
            WHEN gpa >= 3.0 THEN 'Bien'
            WHEN gpa >= 2.0 THEN 'Passable'
            ELSE 'À risque'
        END AS performance
    FROM students
    WHERE gpa IS NOT NULL
    ORDER BY gpa DESC
""")

print("\n[OBJECTIF] Catégories de performance :")
for name, gpa, perf in cursor.fetchall():
    print(f"  {name}: {gpa} - {perf}")

"""
[IDEE] EXEMPLE 2 : CASE dans agrégation
"""

cursor.execute("""
    SELECT 
        enrollment_year,
        COUNT(*) AS total,
        SUM(CASE WHEN gpa >= 3.5 THEN 1 ELSE 0 END) AS high_achievers,
        SUM(CASE WHEN gpa < 3.0 THEN 1 ELSE 0 END) AS at_risk
    FROM students
    WHERE gpa IS NOT NULL
    GROUP BY enrollment_year
    ORDER BY enrollment_year
""")

print("\n[OBJECTIF] Répartition par année :")
print(f"  {'Année':<8} {'Total':<8} {'Excellents':<12} {'À risque'}")
print("  " + "-"*40)
for year, total, high, risk in cursor.fetchall():
    print(f"  {year:<8} {total:<8} {high:<12} {risk}")

"""
[IDEE] EXEMPLE 3 : Créer colonnes dérivées
"""

cursor.execute("""
    SELECT 
        code,
        name,
        credits,
        CASE
            WHEN credits <= 2 THEN 'Court'
            WHEN credits = 3 THEN 'Standard'
            ELSE 'Intensif'
        END AS course_type,
        CASE
            WHEN department = 'Sciences' THEN 'STEM'
            ELSE 'Non-STEM'
        END AS category
    FROM courses
""")

print("\n[OBJECTIF] Classification des cours :")
for code, name, credits, type_, cat in cursor.fetchall():
    print(f"  {code}: {type_} ({credits} cr) - {cat}")


# Ce fichier continue mais pour ne pas dépasser les limites,
# je vais créer la suite dans un fichier séparé...

conn.commit()
print("\n[OK] Chapitre 6 en cours - Suite dans le prochain fichier...")

# ============================================================================
# [LIVRE] BASES DE DONNÉES - PARTIE 2 (SUITE) : WINDOW FUNCTIONS, INDEX ET VUES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Fin Chapitre 6 : Window Functions (Fonctions de fenêtre)
# - Chapitre 7 : Index et Optimisation
# - Chapitre 8 : Vues et Procédures Stockées
#
# [TEMPS] TEMPS : ~4-6 heures
# [DOCS] PRÉREQUIS : Chapitres 5-6 (début) complétés
# ============================================================================

import sqlite3

# Connexion
conn = sqlite3.connect('databases/advanced_school.db')
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = ON")


# ============================================================================
# [GUIDE] CHAPITRE 6 (SUITE) : WINDOW FUNCTIONS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS : Maîtriser les fonctions de fenêtre (avancé)

[IDEE] WINDOW FUNCTIONS : Calculs sur un "groupe" sans grouper

Différence avec GROUP BY :
- GROUP BY : Réduit lignes (une par groupe)
- Window : GARDE toutes les lignes


[ATTENTION] SQLite support limité des Window Functions (version 3.25+)
Vérifier version :
"""

cursor.execute("SELECT sqlite_version()")
version = cursor.fetchone()[0]
print(f"[IMPORTANT] SQLite version : {version}")

if tuple(map(int, version.split('.'))) >= (3, 25):
    print("[OK] Window Functions supportées !")
else:
    print("[ATTENTION] Window Functions NON supportées (SQLite < 3.25)")


"""
[IDEE] SYNTAXE DE BASE :

fonction_agregation() OVER (
    PARTITION BY colonne
    ORDER BY colonne
    ROWS BETWEEN ... AND ...
)


COMPOSANTS :

OVER : Définit la fenêtre
PARTITION BY : Divise en groupes (optionnel)
ORDER BY : Ordre dans la fenêtre
ROWS/RANGE : Limite la fenêtre


[OBJECTIF] FONCTIONS COURANTES :

ROW_NUMBER() : Numéro de ligne
RANK() : Rang (avec ex-aequo)
DENSE_RANK() : Rang dense
LAG() / LEAD() : Ligne précédente/suivante
FIRST_VALUE() / LAST_VALUE() : Première/dernière valeur
SUM/AVG/COUNT() OVER : Agrégation mobile
"""


# ----------------------------------------------------------------------------
# [NOMBRE] ROW_NUMBER() - Numérotation
# ----------------------------------------------------------------------------

"""
[IDEE] ROW_NUMBER() : Numéro unique pour chaque ligne

EXEMPLE 1 : Numéroter tous les étudiants
"""

cursor.execute("""
    SELECT 
        ROW_NUMBER() OVER (ORDER BY gpa DESC) AS rank,
        first_name || ' ' || last_name AS student,
        gpa
    FROM students
    WHERE gpa IS NOT NULL
""")

print("\n[NOMBRE] ROW_NUMBER : Classement par GPA")
for rank, student, gpa in cursor.fetchall()[:5]:
    print(f"  #{rank}: {student} - {gpa}")

"""
[IDEE] EXEMPLE 2 : Numéroter par groupe (PARTITION BY)
"""

cursor.execute("""
    SELECT 
        m.name AS major,
        s.first_name || ' ' || s.last_name AS student,
        s.gpa,
        ROW_NUMBER() OVER (
            PARTITION BY m.id
            ORDER BY s.gpa DESC
        ) AS rank_in_major
    FROM students s
    JOIN majors m ON s.major_id = m.id
    WHERE s.gpa IS NOT NULL
    ORDER BY m.name, rank_in_major
""")

print("\n[NOMBRE] ROW_NUMBER par major :")
current_major = None
for major, student, gpa, rank in cursor.fetchall():
    if major != current_major:
        print(f"\n  {major} :")
        current_major = major
    print(f"    #{rank}: {student} - {gpa}")

"""
[IDEE] PARTITION BY :

Réinitialise la numérotation pour chaque groupe
Comme faire plusieurs ROW_NUMBER() séparés
"""


# ----------------------------------------------------------------------------
# [TROPHEE] RANK() et DENSE_RANK()
# ----------------------------------------------------------------------------

"""
[IDEE] RANK() : Rang avec gaps pour ex-aequo
[IDEE] DENSE_RANK() : Rang sans gaps


EXEMPLE : Différence RANK vs DENSE_RANK
"""

cursor.execute("""
    SELECT 
        first_name || ' ' || last_name AS student,
        gpa,
        ROW_NUMBER() OVER (ORDER BY gpa DESC) AS row_num,
        RANK() OVER (ORDER BY gpa DESC) AS rank,
        DENSE_RANK() OVER (ORDER BY gpa DESC) AS dense_rank
    FROM students
    WHERE gpa IS NOT NULL
    ORDER BY gpa DESC
""")

print("\n[TROPHEE] Comparaison ROW_NUMBER / RANK / DENSE_RANK :")
print(f"  {'Étudiant':<20} {'GPA':<6} {'Row':<5} {'Rank':<6} {'Dense'}")
print("  " + "-"*55)
for student, gpa, row, rank, dense in cursor.fetchall():
    print(f"  {student:<20} {gpa:<6} {row:<5} {rank:<6} {dense}")

"""
[IDEE] EXPLICATION :

GPA 3.9 -> Rang 1
GPA 3.8 -> Rang 2
GPA 3.7 -> Rang 3
GPA 3.7 -> Rang 3 (ex-aequo)
GPA 3.6 -> Rang 5 (RANK), Rang 4 (DENSE_RANK)
          ^ gap     ^ pas de gap


QUAND UTILISER ?

RANK() : Compétitions (place 1, 2, 3, 3, 5...)
DENSE_RANK() : Niveaux (niveau 1, 2, 3, 3, 4...)
ROW_NUMBER() : Identifiant unique


[IDEE] EXEMPLE PRATIQUE : Top 3 par major
"""

cursor.execute("""
    WITH ranked AS (
        SELECT 
            m.name AS major,
            s.first_name || ' ' || s.last_name AS student,
            s.gpa,
            RANK() OVER (
                PARTITION BY m.id
                ORDER BY s.gpa DESC
            ) AS rank
        FROM students s
        JOIN majors m ON s.major_id = m.id
        WHERE s.gpa IS NOT NULL
    )
    SELECT major, student, gpa, rank
    FROM ranked
    WHERE rank <= 3
    ORDER BY major, rank
""")

print("\n[TROPHEE] Top 3 étudiants par major :")
current_major = None
for major, student, gpa, rank in cursor.fetchall():
    if major != current_major:
        print(f"\n  {major} :")
        current_major = major
    print(f"    {rank}. {student} - {gpa}")


# ----------------------------------------------------------------------------
# <--> LAG() et LEAD() - Accéder lignes adjacentes
# ----------------------------------------------------------------------------

"""
[IDEE] LAG() : Valeur de la ligne PRÉCÉDENTE
[IDEE] LEAD() : Valeur de la ligne SUIVANTE


SYNTAXE :

LAG(colonne, offset, default) OVER (ORDER BY ...)
LEAD(colonne, offset, default) OVER (ORDER BY ...)


EXEMPLE 1 : Comparer avec ligne précédente
"""

cursor.execute("""
    SELECT 
        first_name || ' ' || last_name AS student,
        gpa,
        LAG(gpa) OVER (ORDER BY gpa DESC) AS previous_gpa,
        gpa - LAG(gpa) OVER (ORDER BY gpa DESC) AS difference
    FROM students
    WHERE gpa IS NOT NULL
    ORDER BY gpa DESC
""")

print("\n<- LAG : Différence avec étudiant précédent :")
for student, gpa, prev, diff in cursor.fetchall()[:5]:
    if prev:
        print(f"  {student} ({gpa}) - Diff: {diff:+.2f}")
    else:
        print(f"  {student} ({gpa}) - Premier")

"""
[IDEE] EXEMPLE 2 : Évolution temporelle (professeurs)
"""

cursor.execute("""
    SELECT 
        name,
        hire_date,
        LAG(hire_date) OVER (ORDER BY hire_date) AS prev_hire,
        JULIANDAY(hire_date) - JULIANDAY(
            LAG(hire_date) OVER (ORDER BY hire_date)
        ) AS days_since_prev
    FROM professors
    ORDER BY hire_date
""")

print("\n<- Intervalle entre embauches :")
for name, hire, prev, days in cursor.fetchall():
    if prev:
        print(f"  {name} ({hire}) - {int(days)} jours après {prev}")
    else:
        print(f"  {name} ({hire}) - Premier embauché")

"""
[IDEE] EXEMPLE 3 : LEAD pour prédiction
"""

cursor.execute("""
    SELECT 
        enrollment_year AS year,
        COUNT(*) AS students,
        LEAD(COUNT(*)) OVER (ORDER BY enrollment_year) AS next_year_students,
        LEAD(COUNT(*)) OVER (ORDER BY enrollment_year) - COUNT(*) AS growth
    FROM students
    GROUP BY enrollment_year
    ORDER BY enrollment_year
""")

print("\n-> LEAD : Croissance étudiants :")
for year, students, next_year, growth in cursor.fetchall():
    if next_year:
        print(f"  {year}: {students} étudiants -> {next_year} ({growth:+d})")
    else:
        print(f"  {year}: {students} étudiants -> Dernière année")


# ----------------------------------------------------------------------------
# [GRAPHIQUE] Agrégations mobiles (Running totals)
# ----------------------------------------------------------------------------

"""
[IDEE] SUM/AVG/COUNT OVER : Agrégations cumulatives


EXEMPLE 1 : Somme cumulative
"""

cursor.execute("""
    SELECT 
        enrollment_year,
        COUNT(*) AS new_students,
        SUM(COUNT(*)) OVER (
            ORDER BY enrollment_year
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ) AS cumulative_students
    FROM students
    GROUP BY enrollment_year
    ORDER BY enrollment_year
""")

print("\n[GRAPHIQUE] Somme cumulative des étudiants :")
for year, new, cumulative in cursor.fetchall():
    print(f"  {year}: +{new} nouveaux -> {cumulative} total")

"""
[IDEE] ROWS BETWEEN ... AND ... :

UNBOUNDED PRECEDING : Début de partition
CURRENT ROW : Ligne actuelle
UNBOUNDED FOLLOWING : Fin de partition
N PRECEDING : N lignes avant
N FOLLOWING : N lignes après


EXEMPLES :

-- Somme cumulative (depuis début)
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

-- Moyenne mobile 3 lignes
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING

-- Total de partition
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING


[IDEE] EXEMPLE 2 : Moyenne mobile
"""

cursor.execute("""
    SELECT 
        s.first_name || ' ' || s.last_name AS student,
        s.gpa,
        ROUND(AVG(s.gpa) OVER (
            ORDER BY s.gpa DESC
            ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
        ), 2) AS moving_avg
    FROM students s
    WHERE s.gpa IS NOT NULL
    ORDER BY s.gpa DESC
""")

print("\n[GRAPHIQUE] Moyenne mobile (fenêtre de 3) :")
for student, gpa, moving in cursor.fetchall()[:7]:
    print(f"  {student}: {gpa} (moyenne mobile: {moving})")

"""
[IDEE] EXEMPLE 3 : Pourcentage du total
"""

cursor.execute("""
    SELECT 
        m.name AS major,
        COUNT(s.id) AS students,
        ROUND(
            100.0 * COUNT(s.id) / SUM(COUNT(s.id)) OVER (),
            1
        ) AS percentage
    FROM majors m
    LEFT JOIN students s ON m.id = s.major_id
    GROUP BY m.id, m.name
    HAVING COUNT(s.id) > 0
    ORDER BY students DESC
""")

print("\n[GRAPHIQUE] Répartition étudiants par major (%) :")
for major, students, pct in cursor.fetchall():
    bar = "█" * int(pct / 5)
    print(f"  {major:<15} {students:>2} ({pct:>5.1f}%) {bar}")


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 4 : Window Functions
# ----------------------------------------------------------------------------

"""
OBJECTIF : Maîtriser les fonctions de fenêtre


CAHIER DES CHARGES :

1. Classement global des étudiants (RANK)
2. Top 2 étudiants par major (PARTITION BY + RANK)
3. Différence de GPA avec le suivant (LEAD)
4. Pourcentage cumulatif des étudiants par année (SUM OVER)
5. Moyenne mobile GPA (3 étudiants)


SOLUTIONS :
"""

print("\n" + "="*60)
print("EXERCICE 4 : Window Functions")
print("="*60)

# 1. Classement global
print("\n1⃣ Classement global (RANK) :")
cursor.execute("""
    SELECT 
        RANK() OVER (ORDER BY gpa DESC) AS rank,
        first_name || ' ' || last_name AS student,
        gpa
    FROM students
    WHERE gpa IS NOT NULL
""")
for rank, student, gpa in cursor.fetchall()[:5]:
    print(f"  #{rank}: {student} - {gpa}")

# 2. Top 2 par major
print("\n2⃣ Top 2 par major (PARTITION + RANK) :")
cursor.execute("""
    WITH ranked AS (
        SELECT 
            m.name AS major,
            s.first_name || ' ' || s.last_name AS student,
            s.gpa,
            RANK() OVER (PARTITION BY m.id ORDER BY s.gpa DESC) AS rank
        FROM students s
        JOIN majors m ON s.major_id = m.id
        WHERE s.gpa IS NOT NULL
    )
    SELECT major, student, gpa, rank
    FROM ranked
    WHERE rank <= 2
    ORDER BY major, rank
""")
current_major = None
for major, student, gpa, rank in cursor.fetchall():
    if major != current_major:
        print(f"\n  {major}:")
        current_major = major
    print(f"    {rank}. {student} ({gpa})")

# 3. Différence avec suivant
print("\n3⃣ Écart avec étudiant suivant (LEAD) :")
cursor.execute("""
    SELECT 
        first_name || ' ' || last_name AS student,
        gpa,
        LEAD(gpa) OVER (ORDER BY gpa DESC) AS next_gpa,
        gpa - LEAD(gpa) OVER (ORDER BY gpa DESC) AS gap
    FROM students
    WHERE gpa IS NOT NULL
    ORDER BY gpa DESC
    LIMIT 5
""")
for student, gpa, next_gpa, gap in cursor.fetchall():
    if next_gpa:
        print(f"  {student} ({gpa}) - Écart: {gap:.2f}")
    else:
        print(f"  {student} ({gpa}) - Dernier")

# 4. Pourcentage cumulatif
print("\n4⃣ Cumul étudiants par année (%) :")
cursor.execute("""
    SELECT 
        enrollment_year,
        COUNT(*) AS new,
        SUM(COUNT(*)) OVER (ORDER BY enrollment_year) AS cumul,
        ROUND(
            100.0 * SUM(COUNT(*)) OVER (ORDER BY enrollment_year) /
            SUM(COUNT(*)) OVER (),
            1
        ) AS pct_cumul
    FROM students
    GROUP BY enrollment_year
    ORDER BY enrollment_year
""")
for year, new, cumul, pct in cursor.fetchall():
    print(f"  {year}: +{new} -> {cumul} total ({pct}%)")

# 5. Moyenne mobile
print("\n5⃣ Moyenne mobile GPA (fenêtre 3) :")
cursor.execute("""
    SELECT 
        first_name || ' ' || last_name AS student,
        gpa,
        ROUND(AVG(gpa) OVER (
            ORDER BY gpa DESC
            ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
        ), 2) AS moving_avg
    FROM students
    WHERE gpa IS NOT NULL
    ORDER BY gpa DESC
    LIMIT 7
""")
for student, gpa, avg in cursor.fetchall():
    print(f"  {student}: {gpa} (moy. mobile: {avg})")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 6 COMPLET
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

AGRÉGATIONS :
- COUNT, SUM, AVG, MIN, MAX
- GROUP BY pour grouper
- HAVING pour filtrer groupes
- COUNT(*) vs COUNT(col) vs COUNT(DISTINCT col)

FONCTIONS :
- Chaînes : UPPER, LOWER, TRIM, LENGTH, SUBSTR, REPLACE
- Dates : DATE, TIME, DATETIME, STRFTIME, JULIANDAY
- Math : ABS, ROUND, RANDOM
- Conditionnelles : CASE WHEN ... THEN ... ELSE ... END

WINDOW FUNCTIONS :
- ROW_NUMBER() : Numérotation unique
- RANK() / DENSE_RANK() : Classement
- LAG() / LEAD() : Lignes adjacentes
- SUM/AVG/COUNT OVER : Agrégations mobiles
- PARTITION BY : Groupes dans fenêtres
- ROWS BETWEEN : Définir fenêtre


[CLE] PATTERNS ESSENTIELS :

Classement :
RANK() OVER (ORDER BY col DESC)

Top N par groupe :
RANK() OVER (PARTITION BY groupe ORDER BY col) ... WHERE rank <= N

Cumul :
SUM(col) OVER (ORDER BY col ROWS UNBOUNDED PRECEDING)

Moyenne mobile :
AVG(col) OVER (ORDER BY col ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)

Pourcentage :
100.0 * col / SUM(col) OVER ()


-> PROCHAINE ÉTAPE : Chapitre 7 - Index et Optimisation
"""


# ============================================================================
# [GUIDE] CHAPITRE 7 : INDEX ET OPTIMISATION
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Pourquoi et quand créer des index
[OK] Types d'index (B-Tree, Hash, etc.)
[OK] Créer et supprimer des index
[OK] Analyser performance des requêtes (EXPLAIN)
[OK] Optimiser requêtes lentes
[OK] Trade-offs des index
[OK] Best practices d'optimisation
"""


# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI LES INDEX ?
# ----------------------------------------------------------------------------

"""
[IDEE] PROBLÈME : Recherche lente sans index

Imaginez chercher "Alice" dans 1 million de lignes...


SANS INDEX :
┌─────┬───────┬───────┐
│ id  │ name  │ email │
├─────┼───────┼───────┤
│  1  │ Bob   │ ...   │ <- Vérifier
│  2  │ Eve   │ ...   │ <- Vérifier
│  3  │ Dan   │ ...   │ <- Vérifier
│ ... │ ...   │ ...   │ <- Vérifier tout
│ 1M  │ Alice │ ...   │ <- Trouvé !
└─────┴───────┴───────┘

-> Parcours COMPLET (Full Table Scan)
-> O(n) = 1 million de comparaisons
-> TRÈS LENT !


AVEC INDEX :
Index sur 'name' (structure B-Tree) :

        [M]
       /   \
    [F]     [S]
   /  \     /  \
 [A] [J] [N] [Z]
  v
Alice -> Ligne 1000000

-> Recherche dichotomique
-> O(log n) ≈ 20 comparaisons
-> 50,000x PLUS RAPIDE !


[DESIGN] ANALOGIE : Index d'un livre

SANS INDEX :
- Chercher "Python" -> Lire TOUT le livre

AVEC INDEX :
- Regarder index alphabétique
- "Python : page 234"
- Aller directement page 234


[IDEE] DÉMONSTRATION PERFORMANCE
"""

# Créer grande table pour test
cursor.execute("""
    CREATE TABLE IF NOT EXISTS large_table (
        id INTEGER PRIMARY KEY,
        name TEXT,
        value INTEGER
    )
""")

# Insérer beaucoup de données
import random
import time

data = [(i, f"Name_{i}", random.randint(1, 1000)) for i in range(10000)]
cursor.executemany("INSERT OR IGNORE INTO large_table VALUES (?, ?, ?)", data)
conn.commit()

# Test SANS index
start = time.time()
cursor.execute("SELECT * FROM large_table WHERE name = 'Name_5000'")
cursor.fetchall()
time_without_index = time.time() - start

print(f"\n[TEMPS] Recherche SANS index : {time_without_index*1000:.2f}ms")

# Créer index
cursor.execute("CREATE INDEX IF NOT EXISTS idx_large_name ON large_table(name)")

# Test AVEC index
start = time.time()
cursor.execute("SELECT * FROM large_table WHERE name = 'Name_5000'")
cursor.fetchall()
time_with_index = time.time() - start

print(f"[TEMPS] Recherche AVEC index : {time_with_index*1000:.2f}ms")
print(f"[RAPIDE] Amélioration : {time_without_index/time_with_index:.1f}x plus rapide")


# ----------------------------------------------------------------------------
# [CONSTRUCTION] CRÉER DES INDEX
# ----------------------------------------------------------------------------

"""
SYNTAXE :

CREATE INDEX nom_index ON table(colonne);


[IDEE] EXEMPLE 1 : Index simple
"""

cursor.execute("""
    CREATE INDEX IF NOT EXISTS idx_students_email 
    ON students(email)
""")

print("\n[OK] Index créé sur students.email")

"""
[IDEE] EXEMPLE 2 : Index composite (plusieurs colonnes)
"""

cursor.execute("""
    CREATE INDEX IF NOT EXISTS idx_enrollments_student_course
    ON enrollments(student_id, course_id)
""")

print("[OK] Index composite créé sur enrollments")

"""
[IDEE] Index composite :

L'ORDRE compte !

INDEX (col1, col2) :
[OK] Rapide : WHERE col1 = ?
[OK] Rapide : WHERE col1 = ? AND col2 = ?
[X] PAS rapide : WHERE col2 = ?

Comme annuaire téléphonique :
- Trié par NOM puis PRÉNOM
- Chercher "Dupont" -> Rapide
- Chercher "Dupont Jean" -> Rapide
- Chercher "Jean" -> Lent (pas en premier)


[IDEE] EXEMPLE 3 : Index unique
"""

cursor.execute("""
    CREATE UNIQUE INDEX IF NOT EXISTS idx_students_email_unique
    ON students(email)
""")

"""
UNIQUE INDEX :
- Garantit unicité (comme UNIQUE constraint)
- + Performance de l'index


[IDEE] EXEMPLE 4 : Index partiel (WHERE clause)
"""

cursor.execute("""
    CREATE INDEX IF NOT EXISTS idx_students_active_gpa
    ON students(gpa)
    WHERE gpa >= 3.5
""")

print("[OK] Index partiel créé (GPA ≥ 3.5 seulement)")

"""
INDEX PARTIEL :
- Seulement certaines lignes
- Plus petit -> Plus rapide
- Moins d'espace disque

Utile pour :
- Requêtes fréquentes sur sous-ensemble
- WHERE actif = TRUE
- WHERE date >= '2024-01-01'


[IDEE] LISTER LES INDEX
"""

cursor.execute("""
    SELECT name, tbl_name, sql 
    FROM sqlite_master 
    WHERE type = 'index' 
    AND tbl_name = 'students'
""")

print("\n[LISTE] Index sur table students :")
for name, table, sql in cursor.fetchall():
    if sql:  # NULL pour index auto (PK)
        print(f"  {name}")


# ----------------------------------------------------------------------------
# [SUPPRIMER] SUPPRIMER DES INDEX
# ----------------------------------------------------------------------------

"""
SYNTAXE :

DROP INDEX nom_index;


[IDEE] Quand supprimer ?
- Index inutilisé
- Trop d'index (ralentit INSERT/UPDATE)
- Changement de schéma
"""

# cursor.execute("DROP INDEX IF EXISTS idx_large_name")


# ----------------------------------------------------------------------------
# [RECHERCHE] EXPLAIN - Analyser les requêtes
# ----------------------------------------------------------------------------

"""
[IDEE] EXPLAIN : Voir le plan d'exécution de la requête

SYNTAXE :

EXPLAIN QUERY PLAN SELECT ...


[IDEE] EXEMPLE 1 : Recherche SANS index
"""

cursor.execute("""
    EXPLAIN QUERY PLAN
    SELECT * FROM students WHERE last_name = 'Dupont'
""")

print("\n[RECHERCHE] EXPLAIN sans index sur last_name :")
for row in cursor.fetchall():
    print(f"  {row}")

"""
Résultat typique :
SCAN TABLE students

-> SCAN = Parcours complet (LENT)


[IDEE] EXEMPLE 2 : Recherche AVEC index
"""

cursor.execute("CREATE INDEX IF NOT EXISTS idx_students_lastname ON students(last_name)")

cursor.execute("""
    EXPLAIN QUERY PLAN
    SELECT * FROM students WHERE last_name = 'Dupont'
""")

print("\n[RECHERCHE] EXPLAIN avec index sur last_name :")
for row in cursor.fetchall():
    print(f"  {row}")

"""
Résultat typique :
SEARCH TABLE students USING INDEX idx_students_lastname

-> SEARCH + USING INDEX = Utilise index (RAPIDE)


[IDEE] EXEMPLE 3 : JOIN avec index
"""

cursor.execute("""
    EXPLAIN QUERY PLAN
    SELECT s.first_name, m.name
    FROM students s
    JOIN majors m ON s.major_id = m.id
""")

print("\n[RECHERCHE] EXPLAIN JOIN :")
for row in cursor.fetchall():
    print(f"  {row}")


# ----------------------------------------------------------------------------
# [OBJECTIF] QUAND CRÉER DES INDEX ?
# ----------------------------------------------------------------------------

"""
[OK] CRÉER INDEX SI :

1. Colonne dans WHERE fréquemment
   WHERE email = ?
   WHERE created_at > ?

2. Colonne dans JOIN
   JOIN table ON col = other.col

3. Colonne dans ORDER BY
   ORDER BY created_at DESC

4. FOREIGN KEY (automatique dans certains SGBD, pas SQLite)
   
5. Colonne unique (email, username)


[X] NE PAS créer index si :

1. Table petite (< 1000 lignes)
   -> Index inutile, overhead

2. Colonne rarement utilisée dans WHERE

3. Colonne avec peu de valeurs distinctes
   -> genre (M/F), actif (0/1)
   -> Index inefficace

4. Colonne fréquemment modifiée
   -> Index ralentit UPDATE

5. Déjà beaucoup d'index sur la table
   -> INSERT/UPDATE lents


[CLE] RÈGLE D'OR :

Index = Trade-off
[OK] SELECT plus rapide
[X] INSERT/UPDATE/DELETE plus lents
[X] Espace disque


[IDEE] COMBIEN D'INDEX ?

Table lecture intensive -> Plus d'index OK
Table écriture intensive -> Moins d'index
Typique : 3-5 index par table
"""


# ----------------------------------------------------------------------------
# [RAPIDE] OPTIMISATION DES REQUÊTES
# ----------------------------------------------------------------------------

"""
[OBJECTIF] TECHNIQUES D'OPTIMISATION


1. UTILISER INDEX
─────────────────
"""

# [X] MAUVAIS (fonction sur colonne indexée)
cursor.execute("SELECT * FROM students WHERE LOWER(email) = 'alice@email.com'")

# [OK] BON (colonne directe)
cursor.execute("SELECT * FROM students WHERE email = 'alice@email.com'")

"""
[IDEE] Fonction sur colonne -> Index inutilisable !

WHERE YEAR(created_at) = 2024  [X]
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'  [OK]


2. ÉVITER SELECT *
──────────────────
"""

# [X] MAUVAIS
cursor.execute("SELECT * FROM students")

# [OK] BON
cursor.execute("SELECT id, first_name, last_name FROM students")

"""
[IDEE] SELECT * :
- Retourne colonnes inutiles
- Plus de bande passante
- Cache moins efficace


3. LIMITER RÉSULTATS
────────────────────
"""

# [OK] Toujours utiliser LIMIT si possible
cursor.execute("SELECT * FROM students ORDER BY gpa DESC LIMIT 10")

"""
4. UTILISER EXISTS au lieu de COUNT
────────────────────────────────────
"""

# [X] LENT (compte tout)
cursor.execute("""
    SELECT (SELECT COUNT(*) FROM enrollments WHERE student_id = 1) > 0
""")

# [OK] RAPIDE (arrête dès qu'il trouve)
cursor.execute("""
    SELECT EXISTS(SELECT 1 FROM enrollments WHERE student_id = 1)
""")

"""
5. ÉVITER OR sur colonnes différentes
──────────────────────────────────────
"""

# [X] LENT (n'utilise pas index)
cursor.execute("SELECT * FROM students WHERE first_name = 'Alice' OR last_name = 'Dupont'")

# [OK] RAPIDE (deux requêtes avec index + UNION)
cursor.execute("""
    SELECT * FROM students WHERE first_name = 'Alice'
    UNION
    SELECT * FROM students WHERE last_name = 'Dupont'
""")

"""
6. INDEX COVERING
─────────────────

Index "couvrant" = Index contient TOUTES les colonnes nécessaires
-> Pas besoin d'accéder à la table !
"""

# Créer index couvrant
cursor.execute("""
    CREATE INDEX IF NOT EXISTS idx_students_name_gpa
    ON students(last_name, first_name, gpa)
""")

# Requête couverte par l'index
cursor.execute("""
    SELECT first_name, last_name, gpa
    FROM students
    WHERE last_name = 'Dupont'
""")

"""
EXPLAIN montrera :
SEARCH USING COVERING INDEX

-> Encore plus rapide !


7. BATCH INSERTS
────────────────
"""

# [X] LENT (1000 commits)
for i in range(1000):
    cursor.execute("INSERT INTO table VALUES (?)", (i,))
    conn.commit()

# [OK] RAPIDE (1 commit)
data = [(i,) for i in range(1000)]
cursor.executemany("INSERT INTO table VALUES (?)", data)
conn.commit()


# ----------------------------------------------------------------------------
# [GRAPHIQUE] ANALYSE DE PERFORMANCE
# ----------------------------------------------------------------------------

"""
[IDEE] SQLITE_STAT1 : Statistiques des index

SQLite collecte stats pour optimiser requêtes
"""

cursor.execute("ANALYZE")
conn.commit()

cursor.execute("SELECT * FROM sqlite_stat1")
print("\n[GRAPHIQUE] Statistiques des index :")
for row in cursor.fetchall():
    print(f"  {row}")

"""
[IDEE] VACUUM : Réorganiser base de données

Après beaucoup de DELETE/UPDATE :
- Espace fragmenté
- Performance dégradée

VACUUM :
- Reconstruit BD
- Récupère espace
- Optimise performance
"""

# cursor.execute("VACUUM")  # Peut être long sur grandes BD


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 5 : Optimisation
# ----------------------------------------------------------------------------

"""
OBJECTIF : Optimiser requêtes lentes


CAHIER DES CHARGES :

1. Identifier requête lente avec EXPLAIN
2. Créer index approprié
3. Vérifier amélioration avec EXPLAIN
4. Mesurer temps d'exécution
5. Comparer avant/après


SOLUTION : Optimiser recherche d'étudiants par major et GPA
"""

print("\n" + "="*60)
print("EXERCICE 5 : Optimisation de requête")
print("="*60)

# Requête à optimiser
query = """
    SELECT s.first_name, s.last_name, s.gpa, m.name AS major
    FROM students s
    JOIN majors m ON s.major_id = m.id
    WHERE s.gpa >= 3.5 AND m.department = 'Sciences'
    ORDER BY s.gpa DESC
"""

# 1. EXPLAIN avant optimisation
print("\n1⃣ EXPLAIN AVANT optimisation :")
cursor.execute(f"EXPLAIN QUERY PLAN {query}")
for row in cursor.fetchall():
    print(f"  {row}")

# 2. Mesurer temps AVANT
start = time.time()
cursor.execute(query)
results_before = cursor.fetchall()
time_before = time.time() - start
print(f"\n[TEMPS] Temps AVANT : {time_before*1000:.2f}ms")

# 3. Créer index stratégiques
print("\n2⃣ Création des index :")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_students_gpa ON students(gpa)")
print("  [OK] Index sur students.gpa")

cursor.execute("CREATE INDEX IF NOT EXISTS idx_majors_dept ON majors(department)")
print("  [OK] Index sur majors.department")

# 4. EXPLAIN après optimisation
print("\n3⃣ EXPLAIN APRÈS optimisation :")
cursor.execute(f"EXPLAIN QUERY PLAN {query}")
for row in cursor.fetchall():
    print(f"  {row}")

# 5. Mesurer temps APRÈS
start = time.time()
cursor.execute(query)
results_after = cursor.fetchall()
time_after = time.time() - start
print(f"\n[TEMPS] Temps APRÈS : {time_after*1000:.2f}ms")

# 6. Comparaison
if time_before > 0:
    improvement = time_before / max(time_after, 0.0001)
    print(f"\n[RAPIDE] Amélioration : {improvement:.1f}x plus rapide")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 7
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

INDEX :
- CREATE INDEX : Accélérer SELECT
- Index composite : (col1, col2)
- Index unique : Contrainte + performance
- Index partiel : WHERE dans index
- DROP INDEX : Supprimer index

OPTIMISATION :
- EXPLAIN QUERY PLAN : Analyser requêtes
- SCAN vs SEARCH : Avec/sans index
- Trade-offs : SELECT rapide vs INSERT lent
- Techniques : éviter SELECT *, utiliser LIMIT

ANALYSE :
- ANALYZE : Collecter statistiques
- VACUUM : Réorganiser BD
- Mesurer temps d'exécution


[CLE] RÈGLES D'OR :

1. Index sur colonnes WHERE fréquentes
2. Index sur FOREIGN KEY
3. Index sur colonnes ORDER BY
4. Pas trop d'index (3-5 max)
5. EXPLAIN avant optimiser
6. Mesurer, ne pas deviner


[ATTENTION] PIÈGES COURANTS :

[X] Fonction sur colonne indexée
[X] SELECT * systématique
[X] Trop d'index (INSERT lent)
[X] Index sur petites tables
[X] Index sur colonnes peu distinctes


-> PROCHAINE ÉTAPE : Chapitre 8 - Vues et Procédures
"""


# ============================================================================
# [GUIDE] CHAPITRE 8 : VUES ET PROCÉDURES STOCKÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer et utiliser des vues (VIEWS)
[OK] Vues matérialisées (concept)
[OK] Triggers (déclencheurs)
[OK] Procédures stockées (limitées en SQLite)
[OK] Fonctions personnalisées
"""


# ----------------------------------------------------------------------------
# [EYE] VUES (VIEWS)
# ----------------------------------------------------------------------------

"""
[IDEE] VUE : Requête SQL sauvegardée et réutilisable

VUE = Table "virtuelle"
- Pas de données stockées
- Exécute requête à chaque SELECT
- Simplifie requêtes complexes


SYNTAXE :

CREATE VIEW nom_vue AS
SELECT ...


[IDEE] EXEMPLE 1 : Vue simple
"""

cursor.execute("""
    CREATE VIEW IF NOT EXISTS v_students_with_major AS
    SELECT 
        s.id,
        s.first_name || ' ' || s.last_name AS full_name,
        s.email,
        s.gpa,
        m.name AS major_name,
        m.department
    FROM students s
    LEFT JOIN majors m ON s.major_id = m.id
""")

print("\n[EYE] Vue créée : v_students_with_major")

# Utiliser la vue comme une table
cursor.execute("SELECT full_name, major_name, gpa FROM v_students_with_major WHERE gpa >= 3.5")

print("\nÉtudiants excellents (via vue) :")
for name, major, gpa in cursor.fetchall():
    print(f"  {name} ({major}) - {gpa}")

"""
[IDEE] AVANTAGES DES VUES :

[OK] SIMPLICITÉ
   Requête complexe -> Vue simple à utiliser

[OK] RÉUTILISATION
   Code DRY (Don't Repeat Yourself)

[OK] SÉCURITÉ
   Masquer colonnes sensibles
   Filtrer données (vues par département)

[OK] ABSTRACTION
   Changer schéma sans casser code
   Vue reste stable


[IDEE] EXEMPLE 2 : Vue avec agrégation
"""

cursor.execute("""
    CREATE VIEW IF NOT EXISTS v_student_stats AS
    SELECT 
        s.id,
        s.first_name || ' ' || s.last_name AS student,
        s.gpa,
        COUNT(e.id) AS course_count,
        SUM(c.credits) AS total_credits
    FROM students s
    LEFT JOIN enrollments e ON s.id = e.student_id
    LEFT JOIN courses c ON e.course_id = c.id
    GROUP BY s.id, student, s.gpa
""")

cursor.execute("SELECT * FROM v_student_stats ORDER BY total_credits DESC LIMIT 5")

print("\n[EYE] Statistiques étudiants (vue agrégée) :")
for id, student, gpa, courses, credits in cursor.fetchall():
    print(f"  {student}: {courses} cours, {credits or 0} crédits, GPA {gpa}")

"""
[IDEE] EXEMPLE 3 : Vue pour sécurité
"""

cursor.execute("""
    CREATE VIEW IF NOT EXISTS v_public_students AS
    SELECT 
        id,
        first_name,
        last_name,
        major_id
    FROM students
    -- Masque email, birth_date (données sensibles)
""")

"""
Application peut utiliser v_public_students
au lieu de students directement
-> Pas d'accès aux données sensibles


[IDEE] LISTER LES VUES
"""

cursor.execute("""
    SELECT name, sql 
    FROM sqlite_master 
    WHERE type = 'view'
""")

print("\n[LISTE] Vues existantes :")
for name, sql in cursor.fetchall():
    print(f"  {name}")

"""
[IDEE] SUPPRIMER VUE
"""

# cursor.execute("DROP VIEW IF EXISTS v_public_students")


# ----------------------------------------------------------------------------
# [SYNC] TRIGGERS (Déclencheurs)
# ----------------------------------------------------------------------------

"""
[IDEE] TRIGGER : Action automatique sur événement

Événements :
- BEFORE INSERT
- AFTER INSERT
- BEFORE UPDATE
- AFTER UPDATE
- BEFORE DELETE
- AFTER DELETE


SYNTAXE :

CREATE TRIGGER nom_trigger
AFTER INSERT ON table
FOR EACH ROW
BEGIN
    -- Actions SQL
END;


[IDEE] EXEMPLE 1 : Log automatique des modifications
"""

# Table de logs
cursor.execute("""
    CREATE TABLE IF NOT EXISTS student_logs (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        student_id INTEGER,
        action TEXT,
        old_gpa REAL,
        new_gpa REAL,
        changed_at TEXT DEFAULT CURRENT_TIMESTAMP
    )
""")

# Trigger sur UPDATE de GPA
cursor.execute("""
    CREATE TRIGGER IF NOT EXISTS log_gpa_change
    AFTER UPDATE OF gpa ON students
    FOR EACH ROW
    BEGIN
        INSERT INTO student_logs (student_id, action, old_gpa, new_gpa)
        VALUES (NEW.id, 'UPDATE', OLD.gpa, NEW.gpa);
    END
""")

print("\n[SYNC] Trigger créé : log_gpa_change")

# Tester le trigger
cursor.execute("UPDATE students SET gpa = 4.0 WHERE id = 1")
conn.commit()

cursor.execute("SELECT * FROM student_logs")
print("\n[LISTE] Logs générés par trigger :")
for row in cursor.fetchall():
    print(f"  {row}")

"""
[IDEE] NEW et OLD :

NEW : Nouvelles valeurs (INSERT, UPDATE)
OLD : Anciennes valeurs (UPDATE, DELETE)


[IDEE] EXEMPLE 2 : Validation automatique
"""

cursor.execute("""
    CREATE TRIGGER IF NOT EXISTS validate_gpa
    BEFORE INSERT ON students
    FOR EACH ROW
    WHEN NEW.gpa < 0 OR NEW.gpa > 4
    BEGIN
        SELECT RAISE(ABORT, 'GPA must be between 0 and 4');
    END
""")

print("[SYNC] Trigger de validation créé")

# Tester validation
try:
    cursor.execute("""
        INSERT INTO students (first_name, last_name, gpa)
        VALUES ('Test', 'Invalid', 5.0)
    """)
except sqlite3.IntegrityError as e:
    print(f"[OK] Trigger a empêché l'insertion : {e}")

"""
[IDEE] EXEMPLE 3 : Cascade personnalisé
"""

cursor.execute("""
    CREATE TRIGGER IF NOT EXISTS archive_deleted_student
    BEFORE DELETE ON students
    FOR EACH ROW
    BEGIN
        INSERT INTO student_archive (id, name, deleted_at)
        VALUES (OLD.id, OLD.first_name || ' ' || OLD.last_name, CURRENT_TIMESTAMP);
    END
""")

"""
[ATTENTION] Créer table archive d'abord !


[IDEE] LISTER TRIGGERS
"""

cursor.execute("""
    SELECT name, tbl_name, sql 
    FROM sqlite_master 
    WHERE type = 'trigger'
""")

print("\n[LISTE] Triggers existants :")
for name, table, sql in cursor.fetchall():
    print(f"  {name} sur {table}")

"""
[IDEE] SUPPRIMER TRIGGER
"""

# cursor.execute("DROP TRIGGER IF EXISTS log_gpa_change")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 8 ET PARTIE 2 COMPLÈTE
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

VUES :
- CREATE VIEW : Requête réutilisable
- Vues simples et agrégées
- Sécurité et abstraction
- DROP VIEW : Supprimer vue

TRIGGERS :
- CREATE TRIGGER : Action automatique
- BEFORE/AFTER INSERT/UPDATE/DELETE
- NEW et OLD : Accès valeurs
- Validation et logging


[BRAVO] FÉLICITATIONS ! PARTIE 2 TERMINÉE !

Vous maîtrisez maintenant :
[OK] Requêtes complexes (JOIN, UNION, sous-requêtes, CTE)
[OK] Fonctions et agrégations (COUNT, SUM, AVG, GROUP BY, HAVING)
[OK] Window Functions (RANK, LAG, LEAD, agrégations mobiles)
[OK] Index et optimisation (CREATE INDEX, EXPLAIN)
[OK] Vues et triggers (CREATE VIEW, CREATE TRIGGER)

Vous êtes maintenant capable de :
[OK] Écrire des requêtes SQL avancées
[OK] Optimiser performance des bases de données
[OK] Créer des systèmes automatisés avec triggers
[OK] Structurer code avec vues


-> PROCHAINE ÉTAPE : PARTIE 3 - Conception et Modélisation

La Partie 3 couvrira :
- Modélisation de données (entités, attributs)
- Normalisation (1NF, 2NF, 3NF, BCNF)
- Diagrammes ER (Entity-Relationship)
- Design patterns pour bases de données

Vous allez apprendre à CONCEVOIR des bases de données ! [DESIGN]
"""

conn.close()
print("\n[OK] Partie 2 complétée ! Bravo ! [BRAVO]")

# ============================================================================
# [LIVRE] BASES DE DONNÉES - PARTIE 3 : CONCEPTION ET MODÉLISATION
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 9 : Modélisation de Données (Entités, Attributs, Relations)
# - Chapitre 10 : Normalisation (1NF, 2NF, 3NF, BCNF)
# - Chapitre 11 : Diagrammes ER (Entity-Relationship)
# - Chapitre 12 : Design Patterns
#
# [TEMPS] TEMPS : ~8-10 heures
# [DOCS] PRÉREQUIS : Parties 1 et 2 complétées
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 9 : MODÉLISATION DE DONNÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Analyser les besoins et identifier les entités
[OK] Définir attributs et types appropriés
[OK] Modéliser les relations entre entités
[OK] Choisir les clés primaires et étrangères
[OK] Créer un modèle conceptuel de données
"""

import sqlite3

# Connexion
conn = sqlite3.connect('databases/modeling_demo.db')
cursor = conn.cursor()


# ----------------------------------------------------------------------------
# [DESIGN] QU'EST-CE QUE LA MODÉLISATION ?
# ----------------------------------------------------------------------------

"""
[IDEE] MODÉLISATION = Représenter le monde réel en base de données

[DESIGN] ANALOGIE : Architecte et maison

Architecte :
1. Comprend besoins client
2. Dessine plans
3. Détaille chaque pièce
4. Construit

Modélisation BD :
1. Analyse besoins métier
2. Crée modèle conceptuel
3. Définit tables/colonnes
4. Implémente SQL


[CLE] 3 NIVEAUX DE MODÉLISATION :

1. CONCEPTUEL (What)
   - Entités et relations
   - Indépendant de la technologie
   - Diagrammes ER

2. LOGIQUE (How - général)
   - Tables, colonnes, types
   - Normalisation
   - Contraintes
   - Indépendant du SGBD

3. PHYSIQUE (How - spécifique)
   - Index, partitions
   - Optimisation
   - Spécifique au SGBD


Ce chapitre : CONCEPTUEL et début LOGIQUE
"""


# ----------------------------------------------------------------------------
# [ENTREPRISE] EXEMPLE FIL ROUGE : Système de Bibliothèque
# ----------------------------------------------------------------------------

"""
[DOCS] CAHIER DES CHARGES : Bibliothèque Universitaire

BESOINS :
- Gérer collection de livres
- Suivre emprunts des membres
- Auteurs et leurs livres
- Catégories de livres
- Historique des emprunts
- Réservations

RÈGLES MÉTIER :
- Un livre peut avoir plusieurs auteurs
- Un auteur peut écrire plusieurs livres
- Un membre peut emprunter max 5 livres
- Durée emprunt : 14 jours
- Amende si retard
- Livre peut être réservé si emprunté
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] ÉTAPE 1 : IDENTIFIER LES ENTITÉS
# ----------------------------------------------------------------------------

"""
[IDEE] ENTITÉ = Chose du monde réel à modéliser

Comment identifier ?
-> Chercher les NOMS dans le cahier des charges


[DOCS] ENTITÉS IDENTIFIÉES :

1. LIVRE
   - Chose physique à gérer
   - A des attributs (titre, ISBN, etc.)

2. AUTEUR
   - Personne qui écrit
   - A des attributs (nom, biographie, etc.)

3. MEMBRE
   - Personne qui emprunte
   - A des attributs (nom, email, etc.)

4. CATÉGORIE
   - Classification des livres
   - A des attributs (nom, description)

5. EMPRUNT
   - Action d'emprunter
   - Lie membre et livre
   - A des attributs (date, retour)

6. RÉSERVATION
   - Action de réserver
   - Lie membre et livre
   - A des attributs (date)


[CLE] RÈGLE : Entité vs Attribut ?

ENTITÉ si :
[OK] A plusieurs attributs propres
[OK] Existe indépendamment
[OK] Relation avec autres entités

ATTRIBUT si :
[OK] Une seule valeur
[OK] Décrit l'entité
[OK] Pas de relation propre

Exemples :
- Email : ATTRIBUT (décrit membre)
- Adresse : ATTRIBUT ou ENTITÉ ? Dépend !
  -> Si juste rue+ville : ATTRIBUT
  -> Si complexe avec historique : ENTITÉ
"""


# ----------------------------------------------------------------------------
# [NOTE] ÉTAPE 2 : DÉFINIR LES ATTRIBUTS
# ----------------------------------------------------------------------------

"""
[IDEE] ATTRIBUT = Propriété d'une entité


[DOCS] LIVRE
─────────
"""
CREATE TABLE books (
    -- Clé primaire
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    
    -- Identifiants
    isbn TEXT UNIQUE NOT NULL,  -- International Standard Book Number
    title TEXT NOT NULL,
    
    -- Détails publication
    publication_year INTEGER,
    edition INTEGER DEFAULT 1,
    pages INTEGER,
    language TEXT DEFAULT 'fr',
    
    -- Description
    description TEXT,
    
    -- Disponibilité
    copies_total INTEGER NOT NULL DEFAULT 1,
    copies_available INTEGER NOT NULL DEFAULT 1,
    
    -- Métadonnées
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    updated_at TEXT,
    
    -- Contraintes
    CHECK(copies_available >= 0),
    CHECK(copies_available <= copies_total),
    CHECK(pages > 0),
    CHECK(publication_year > 1000)
);

"""
[CLE] CHOIX DES ATTRIBUTS :

[OK] isbn : Identifiant unique standard
[OK] title : Obligatoire (NOT NULL)
[OK] copies : Gérer exemplaires multiples
[OK] created_at : Audit trail
[OK] CHECK : Validation métier


[UTILISATEUR] AUTEUR
──────────
"""
CREATE TABLE authors (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    
    first_name TEXT NOT NULL,
    last_name TEXT NOT NULL,
    
    birth_date DATE,
    death_date DATE,
    
    nationality TEXT,
    biography TEXT,
    
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    
    CHECK(death_date IS NULL OR death_date >= birth_date)
);

"""
[UTILISATEURS] MEMBRE
──────────
"""
CREATE TABLE members (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    
    -- Identité
    first_name TEXT NOT NULL,
    last_name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    phone TEXT,
    
    -- Adresse (simplifié - pourrait être entité séparée)
    address TEXT,
    city TEXT,
    postal_code TEXT,
    
    -- Adhésion
    member_since DATE NOT NULL DEFAULT CURRENT_DATE,
    membership_expires DATE,
    is_active INTEGER DEFAULT 1 CHECK(is_active IN (0, 1)),
    
    -- Métadonnées
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    
    CHECK(email LIKE '%@%'),
    CHECK(membership_expires IS NULL OR membership_expires > member_since)
);

"""
[DOSSIER] CATÉGORIE
─────────────
"""
CREATE TABLE categories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT UNIQUE NOT NULL,
    description TEXT,
    parent_category_id INTEGER,  -- Pour hiérarchie (Fiction > Science-Fiction)
    
    FOREIGN KEY (parent_category_id) REFERENCES categories(id)
);

"""
[CLE] TYPES DE DONNÉES - GUIDE RAPIDE :

┌────────────────┬────────────────┬─────────────────────┐
│ Type donnée    │ Type SQL       │ Exemple             │
├────────────────┼────────────────┼─────────────────────┤
│ ID auto        │ INTEGER PK AI  │ 1, 2, 3...          │
│ Texte court    │ TEXT           │ Nom, email          │
│ Texte long     │ TEXT           │ Description, bio    │
│ Nombre entier  │ INTEGER        │ Pages, année        │
│ Décimal        │ REAL           │ Prix (en centimes!) │
│ Boolean        │ INTEGER 0/1    │ is_active           │
│ Date           │ TEXT ISO8601   │ '2024-01-18'        │
│ DateTime       │ TEXT ISO8601   │ '2024-01-18 10:30'  │
└────────────────┴────────────────┴─────────────────────┘


[IDEE] ATTRIBUTS OBLIGATOIRES vs OPTIONNELS :

NOT NULL -> Toujours requis
NULL OK -> Optionnel

Exemples :
[OK] title NOT NULL (livre doit avoir titre)
[OK] phone NULL OK (téléphone optionnel)
[OK] isbn UNIQUE NOT NULL (identifiant unique obligatoire)


[IDEE] VALEURS PAR DÉFAUT :

Utilisez DEFAULT pour valeurs communes :
- created_at DEFAULT CURRENT_TIMESTAMP
- is_active DEFAULT 1
- language DEFAULT 'fr'
- copies_total DEFAULT 1
"""


# ----------------------------------------------------------------------------
# [LIEN] ÉTAPE 3 : MODÉLISER LES RELATIONS
# ----------------------------------------------------------------------------

"""
[IDEE] RELATION = Lien entre entités

3 TYPES DE RELATIONS :

1. ONE-TO-MANY (1-N)
2. MANY-TO-MANY (N-N)
3. ONE-TO-ONE (1-1)


1⃣ ONE-TO-MANY (1-N)
─────────────────────

[IDEE] UNE entité liée à PLUSIEURS de l'autre
   Mais l'autre liée à UNE SEULE de la première

Exemples :
- Un membre -> Plusieurs emprunts
- Une catégorie -> Plusieurs livres
- Un auteur -> Plusieurs livres (non, voir plus bas!)


[DESIGN] REPRÉSENTATION :

Member ──< Loan
   1        N

Un membre a plusieurs emprunts
Un emprunt appartient à un seul membre


[IDEE] IMPLÉMENTATION : Foreign Key dans le "N"
"""

CREATE TABLE loans (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    
    -- Foreign Keys
    member_id INTEGER NOT NULL,      -- Le "N" a FK vers le "1"
    book_id INTEGER NOT NULL,
    
    -- Dates
    loan_date DATE NOT NULL DEFAULT CURRENT_DATE,
    due_date DATE NOT NULL,
    return_date DATE,
    
    -- Statut
    status TEXT DEFAULT 'active' CHECK(status IN ('active', 'returned', 'overdue')),
    
    -- Amendes
    fine_amount INTEGER DEFAULT 0,  -- En centimes
    fine_paid INTEGER DEFAULT 0,
    
    -- Contraintes
    FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE CASCADE,
    FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE RESTRICT,
    
    CHECK(due_date > loan_date),
    CHECK(return_date IS NULL OR return_date >= loan_date),
    CHECK(fine_paid >= 0 AND fine_paid <= fine_amount)
);

"""
[IDEE] ON DELETE :

CASCADE : Si membre supprimé -> Emprunts supprimés
RESTRICT : Si livre a emprunts -> Erreur si suppression
SET NULL : Foreign key devient NULL
SET DEFAULT : Foreign key devient valeur DEFAULT


[CLE] RÈGLE 1-N :

Foreign Key dans la table du côté "N" (many)
- loans.member_id -> members.id
- loans.book_id -> books.id


2⃣ MANY-TO-MANY (N-N)
──────────────────────

[IDEE] PLUSIEURS des deux côtés

Exemples :
- Un livre -> Plusieurs auteurs
- Un auteur -> Plusieurs livres
- Un livre -> Plusieurs catégories
- Une catégorie -> Plusieurs livres


[DESIGN] REPRÉSENTATION :

Book >──< Author
  N       N

Un livre a plusieurs auteurs
Un auteur écrit plusieurs livres


[IDEE] IMPLÉMENTATION : Table de liaison (Junction Table)
"""

CREATE TABLE book_authors (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    book_id INTEGER NOT NULL,
    author_id INTEGER NOT NULL,
    author_order INTEGER DEFAULT 1,  -- Premier auteur, deuxième, etc.
    
    FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
    FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE,
    
    UNIQUE(book_id, author_id)  -- Éviter doublons
);

"""
[CLE] TABLE DE LIAISON :

Nom : book_authors (ou books_authors)
Contenu :
- FK vers book
- FK vers author
- UNIQUE sur combinaison (évite doublons)
- Optionnel : attributs supplémentaires (author_order)


[IDEE] AUTRE EXEMPLE N-N : Livres <-> Catégories
"""

CREATE TABLE book_categories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    book_id INTEGER NOT NULL,
    category_id INTEGER NOT NULL,
    
    FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
    FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
    
    UNIQUE(book_id, category_id)
);

"""
3⃣ ONE-TO-ONE (1-1)
────────────────────

[IDEE] UNE de chaque côté (rare)

Exemples :
- Un membre -> Un profil détaillé
- Un livre -> Une fiche d'inventaire
- Un utilisateur -> Un paramètre de compte


[DESIGN] REPRÉSENTATION :

Member ── Profile
  1        1


[IDEE] IMPLÉMENTATION : FK UNIQUE dans une table
"""

CREATE TABLE member_profiles (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    member_id INTEGER UNIQUE NOT NULL,  -- UNIQUE = 1-1
    
    photo_url TEXT,
    preferred_genres TEXT,  -- JSON
    reading_goal INTEGER,
    privacy_settings TEXT,  -- JSON
    
    FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE CASCADE
);

"""
[CLE] 1-1 vs 1-N :

1-N : FK simple
1-1 : FK + UNIQUE


[IDEE] QUAND UTILISER 1-1 ?

[OK] Séparer données volumineuses (photos)
[OK] Séparer données optionnelles
[OK] Séparer données sensibles (sécurité)
[OK] Différentes vitesses d'accès

[ATTENTION] Souvent, on peut aussi ajouter colonnes à table principale
"""


# ----------------------------------------------------------------------------
# [DESIGN] ÉTAPE 4 : DIAGRAMME CONCEPTUEL
# ----------------------------------------------------------------------------

"""
[IDEE] DIAGRAMME = Représentation visuelle du modèle

Format texte (ERD simplifié) :


┌─────────────┐         ┌──────────────┐
│   MEMBERS   │1      N │    LOANS     │
│─────────────│─────────│──────────────│
│ id (PK)     │         │ id (PK)      │
│ first_name  │         │ member_id FK │
│ last_name   │         │ book_id FK   │
│ email       │         │ loan_date    │
│ is_active   │         │ due_date     │
│ ...         │         │ return_date  │
└─────────────┘         │ status       │
                        └──────────────┘
                               │N
                               │
                               │1
                        ┌──────────────┐
                        │    BOOKS     │
                        │──────────────│
                        │ id (PK)      │
                        │ isbn         │
                        │ title        │
                        │ copies       │
                        │ ...          │
                        └──────────────┘
                               │N
                               │
                               │N (via book_authors)
                               │
                        ┌──────────────┐
                        │   AUTHORS    │
                        │──────────────│
                        │ id (PK)      │
                        │ first_name   │
                        │ last_name    │
                        │ ...          │
                        └──────────────┘


[CLE] LÉGENDE :

PK : Primary Key
FK : Foreign Key
1  : One
N  : Many

│  : Relation
──< : One-to-Many
>──< : Many-to-Many
"""


# ----------------------------------------------------------------------------
# [SAUVEGARDE] ÉTAPE 5 : IMPLÉMENTATION COMPLÈTE
# ----------------------------------------------------------------------------

"""
Créons tout le schéma de la bibliothèque :
"""

# Activer foreign keys
cursor.execute("PRAGMA foreign_keys = ON")

# 1. Authors
cursor.execute("""
    CREATE TABLE IF NOT EXISTS authors (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        first_name TEXT NOT NULL,
        last_name TEXT NOT NULL,
        birth_date DATE,
        death_date DATE,
        nationality TEXT,
        biography TEXT,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        CHECK(death_date IS NULL OR death_date >= birth_date)
    )
""")

# 2. Categories
cursor.execute("""
    CREATE TABLE IF NOT EXISTS categories (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT UNIQUE NOT NULL,
        description TEXT,
        parent_category_id INTEGER,
        FOREIGN KEY (parent_category_id) REFERENCES categories(id)
    )
""")

# 3. Books
cursor.execute("""
    CREATE TABLE IF NOT EXISTS books (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        isbn TEXT UNIQUE NOT NULL,
        title TEXT NOT NULL,
        publication_year INTEGER,
        edition INTEGER DEFAULT 1,
        pages INTEGER,
        language TEXT DEFAULT 'fr',
        description TEXT,
        copies_total INTEGER NOT NULL DEFAULT 1,
        copies_available INTEGER NOT NULL DEFAULT 1,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        updated_at TEXT,
        CHECK(copies_available >= 0),
        CHECK(copies_available <= copies_total),
        CHECK(pages > 0)
    )
""")

# 4. Members
cursor.execute("""
    CREATE TABLE IF NOT EXISTS members (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        first_name TEXT NOT NULL,
        last_name TEXT NOT NULL,
        email TEXT UNIQUE NOT NULL,
        phone TEXT,
        address TEXT,
        city TEXT,
        postal_code TEXT,
        member_since DATE NOT NULL DEFAULT (date('now')),
        membership_expires DATE,
        is_active INTEGER DEFAULT 1 CHECK(is_active IN (0, 1)),
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        CHECK(email LIKE '%@%')
    )
""")

# 5. Book_Authors (N-N)
cursor.execute("""
    CREATE TABLE IF NOT EXISTS book_authors (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        book_id INTEGER NOT NULL,
        author_id INTEGER NOT NULL,
        author_order INTEGER DEFAULT 1,
        FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
        FOREIGN KEY (author_id) REFERENCES authors(id) ON DELETE CASCADE,
        UNIQUE(book_id, author_id)
    )
""")

# 6. Book_Categories (N-N)
cursor.execute("""
    CREATE TABLE IF NOT EXISTS book_categories (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        book_id INTEGER NOT NULL,
        category_id INTEGER NOT NULL,
        FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
        FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE,
        UNIQUE(book_id, category_id)
    )
""")

# 7. Loans (1-N avec members et books)
cursor.execute("""
    CREATE TABLE IF NOT EXISTS loans (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        member_id INTEGER NOT NULL,
        book_id INTEGER NOT NULL,
        loan_date DATE NOT NULL DEFAULT (date('now')),
        due_date DATE NOT NULL,
        return_date DATE,
        status TEXT DEFAULT 'active' CHECK(status IN ('active', 'returned', 'overdue')),
        fine_amount INTEGER DEFAULT 0,
        fine_paid INTEGER DEFAULT 0,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE CASCADE,
        FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE RESTRICT,
        CHECK(due_date > loan_date),
        CHECK(return_date IS NULL OR return_date >= loan_date),
        CHECK(fine_paid >= 0 AND fine_paid <= fine_amount)
    )
""")

# 8. Reservations
cursor.execute("""
    CREATE TABLE IF NOT EXISTS reservations (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        member_id INTEGER NOT NULL,
        book_id INTEGER NOT NULL,
        reservation_date DATE NOT NULL DEFAULT (date('now')),
        expiration_date DATE,
        status TEXT DEFAULT 'active' CHECK(status IN ('active', 'fulfilled', 'cancelled', 'expired')),
        notified INTEGER DEFAULT 0 CHECK(notified IN (0, 1)),
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE CASCADE,
        FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE,
        UNIQUE(member_id, book_id, status)
    )
""")

conn.commit()
print("[OK] Schéma bibliothèque créé !")


# ----------------------------------------------------------------------------
# [GRAPHIQUE] ÉTAPE 6 : PEUPLER AVEC DONNÉES DE TEST
# ----------------------------------------------------------------------------

"""
Ajoutons des données réalistes pour tester :
"""

# Authors
authors_data = [
    ('Victor', 'Hugo', '1802-02-26', '1885-05-22', 'France', 'Écrivain romantique français'),
    ('Antoine', 'de Saint-Exupéry', '1900-06-29', '1944-07-31', 'France', 'Aviateur et écrivain'),
    ('J.K.', 'Rowling', '1965-07-31', NULL, 'Royaume-Uni', 'Auteure de Harry Potter'),
    ('Isaac', 'Asimov', '1920-01-02', '1992-04-06', 'États-Unis', 'Maître de la science-fiction'),
    ('Agatha', 'Christie', '1890-09-15', '1976-01-12', 'Royaume-Uni', 'Reine du crime')
]

cursor.executemany("""
    INSERT INTO authors (first_name, last_name, birth_date, death_date, nationality, biography)
    VALUES (?, ?, ?, ?, ?, ?)
""", authors_data)

# Categories
categories_data = [
    ('Fiction', 'Œuvres de fiction', NULL),
    ('Science-Fiction', 'Fiction scientifique', 1),
    ('Policier', 'Romans policiers et thrillers', 1),
    ('Jeunesse', 'Littérature pour enfants', NULL),
    ('Classique', 'Grands classiques', NULL)
]

cursor.executemany("""
    INSERT INTO categories (name, description, parent_category_id)
    VALUES (?, ?, ?)
""", categories_data)

# Books
books_data = [
    ('978-2-07-036-822-4', 'Les Misérables', 1862, 1, 1488, 'fr', 'Roman social de Victor Hugo'),
    ('978-2-07-061-275-8', 'Le Petit Prince', 1943, 1, 96, 'fr', 'Conte philosophique'),
    ('978-2-07-054-127-6', "Harry Potter à l'école des sorciers", 1997, 1, 320, 'fr', 'Premier tome'),
    ('978-2-07-036-956-6', 'Fondation', 1951, 1, 280, 'fr', 'Cycle de Fondation'),
    ('978-2-253-03-474-7', 'Le Crime de l\'Orient-Express', 1934, 1, 256, 'fr', 'Hercule Poirot enquête')
]

cursor.executemany("""
    INSERT INTO books (isbn, title, publication_year, edition, pages, language, description)
    VALUES (?, ?, ?, ?, ?, ?, ?)
""", books_data)

# Link books to authors
cursor.executemany("""
    INSERT INTO book_authors (book_id, author_id, author_order)
    VALUES (?, ?, ?)
""", [
    (1, 1, 1),  # Les Misérables - Hugo
    (2, 2, 1),  # Le Petit Prince - Saint-Exupéry
    (3, 3, 1),  # Harry Potter - Rowling
    (4, 4, 1),  # Fondation - Asimov
    (5, 5, 1)   # Orient-Express - Christie
])

# Link books to categories
cursor.executemany("""
    INSERT INTO book_categories (book_id, category_id)
    VALUES (?, ?)
""", [
    (1, 5),  # Les Misérables - Classique
    (2, 4),  # Le Petit Prince - Jeunesse
    (2, 5),  # Le Petit Prince - Classique
    (3, 4),  # Harry Potter - Jeunesse
    (4, 2),  # Fondation - Science-Fiction
    (5, 3)   # Orient-Express - Policier
])

# Members
members_data = [
    ('Alice', 'Dupont', 'alice.dupont@email.com', '0601020304', '10 rue de la Paix', 'Paris', '75001'),
    ('Bob', 'Martin', 'bob.martin@email.com', '0602030405', '20 avenue Victor Hugo', 'Lyon', '69001'),
    ('Charlie', 'Durand', 'charlie.durand@email.com', '0603040506', '30 boulevard Haussmann', 'Marseille', '13001')
]

cursor.executemany("""
    INSERT INTO members (first_name, last_name, email, phone, address, city, postal_code)
    VALUES (?, ?, ?, ?, ?, ?, ?)
""", members_data)

# Loans
cursor.execute("""
    INSERT INTO loans (member_id, book_id, loan_date, due_date, status)
    VALUES 
        (1, 1, date('now', '-5 days'), date('now', '+9 days'), 'active'),
        (1, 2, date('now', '-3 days'), date('now', '+11 days'), 'active'),
        (2, 4, date('now', '-20 days'), date('now', '-6 days'), 'overdue'),
        (3, 5, date('now', '-10 days'), date('now', '+4 days'), 'active')
""")

# Reservations
cursor.execute("""
    INSERT INTO reservations (member_id, book_id, reservation_date, status)
    VALUES 
        (2, 3, date('now'), 'active'),
        (3, 1, date('now', '-1 day'), 'active')
""")

conn.commit()
print("[OK] Données de test insérées !")


# ----------------------------------------------------------------------------
# [RECHERCHE] ÉTAPE 7 : TESTER LE MODÈLE
# ----------------------------------------------------------------------------

"""
Vérifions que notre modèle fonctionne bien :
"""

print("\n[DOCS] TEST 1 : Livres avec auteurs et catégories")
cursor.execute("""
    SELECT 
        b.title,
        GROUP_CONCAT(DISTINCT a.first_name || ' ' || a.last_name, ', ') AS authors,
        GROUP_CONCAT(DISTINCT c.name, ', ') AS categories,
        b.copies_available || '/' || b.copies_total AS copies
    FROM books b
    LEFT JOIN book_authors ba ON b.id = ba.book_id
    LEFT JOIN authors a ON ba.author_id = a.id
    LEFT JOIN book_categories bc ON b.id = bc.book_id
    LEFT JOIN categories c ON bc.category_id = c.id
    GROUP BY b.id, b.title
""")

for title, authors, categories, copies in cursor.fetchall():
    print(f"\n  [GUIDE] {title}")
    print(f"     Auteurs: {authors}")
    print(f"     Catégories: {categories}")
    print(f"     Exemplaires: {copies}")


print("\n\n[UTILISATEURS] TEST 2 : Emprunts actifs")
cursor.execute("""
    SELECT 
        m.first_name || ' ' || m.last_name AS member,
        b.title,
        l.loan_date,
        l.due_date,
        CASE 
            WHEN l.status = 'overdue' THEN '[ATTENTION] RETARD'
            ELSE '[OK] OK'
        END AS status
    FROM loans l
    JOIN members m ON l.member_id = m.id
    JOIN books b ON l.book_id = b.id
    WHERE l.status IN ('active', 'overdue')
    ORDER BY l.due_date
""")

for member, title, loan, due, status in cursor.fetchall():
    print(f"  {member} -> {title}")
    print(f"    Emprunté le: {loan}, À rendre: {due} {status}")


print("\n\n[ANNONCE] TEST 3 : Réservations en attente")
cursor.execute("""
    SELECT 
        m.first_name || ' ' || m.last_name AS member,
        b.title,
        r.reservation_date
    FROM reservations r
    JOIN members m ON r.member_id = m.id
    JOIN books b ON r.book_id = b.id
    WHERE r.status = 'active'
""")

for member, title, res_date in cursor.fetchall():
    print(f"  {member} attend: {title} (depuis {res_date})")


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 7 : Modélisation E-Commerce
# ----------------------------------------------------------------------------

"""
OBJECTIF : Modéliser un système e-commerce complet


CAHIER DES CHARGES :

Fonctionnalités :
- Catalogue de produits avec variantes (tailles, couleurs)
- Panier d'achats
- Commandes et historique
- Clients avec adresses multiples
- Paiements
- Avis et notes produits

Entités à identifier :
- Produits, Variantes, Catégories
- Clients, Adresses
- Paniers, Lignes de panier
- Commandes, Lignes de commande
- Paiements
- Avis


SOLUTION :
"""

print("\n" + "="*60)
print("EXERCICE : Modèle E-Commerce")
print("="*60)

# Customers
cursor.execute("""
    CREATE TABLE IF NOT EXISTS customers (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        first_name TEXT NOT NULL,
        last_name TEXT NOT NULL,
        email TEXT UNIQUE NOT NULL,
        password_hash TEXT NOT NULL,
        phone TEXT,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        CHECK(email LIKE '%@%')
    )
""")

# Addresses (1-N avec customers)
cursor.execute("""
    CREATE TABLE IF NOT EXISTS addresses (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        customer_id INTEGER NOT NULL,
        address_type TEXT CHECK(address_type IN ('billing', 'shipping', 'both')),
        street_address TEXT NOT NULL,
        city TEXT NOT NULL,
        state TEXT,
        postal_code TEXT NOT NULL,
        country TEXT DEFAULT 'France',
        is_default INTEGER DEFAULT 0 CHECK(is_default IN (0, 1)),
        FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE
    )
""")

# Product_categories
cursor.execute("""
    CREATE TABLE IF NOT EXISTS product_categories (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT UNIQUE NOT NULL,
        slug TEXT UNIQUE NOT NULL,
        description TEXT,
        parent_id INTEGER,
        FOREIGN KEY (parent_id) REFERENCES product_categories(id)
    )
""")

# Products
cursor.execute("""
    CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        category_id INTEGER,
        name TEXT NOT NULL,
        slug TEXT UNIQUE NOT NULL,
        description TEXT,
        price_cents INTEGER NOT NULL CHECK(price_cents >= 0),
        compare_at_price_cents INTEGER,
        cost_cents INTEGER,
        sku TEXT UNIQUE,
        is_active INTEGER DEFAULT 1 CHECK(is_active IN (0, 1)),
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (category_id) REFERENCES product_categories(id)
    )
""")

# Product_variants (taille, couleur, etc.)
cursor.execute("""
    CREATE TABLE IF NOT EXISTS product_variants (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        product_id INTEGER NOT NULL,
        sku TEXT UNIQUE NOT NULL,
        name TEXT NOT NULL,  -- "Large / Rouge"
        price_cents INTEGER,  -- NULL = utilise prix du produit
        stock_quantity INTEGER DEFAULT 0 CHECK(stock_quantity >= 0),
        is_active INTEGER DEFAULT 1 CHECK(is_active IN (0, 1)),
        FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE
    )
""")

# Carts
cursor.execute("""
    CREATE TABLE IF NOT EXISTS carts (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        customer_id INTEGER,  -- NULL = panier invité
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        updated_at TEXT,
        FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE CASCADE
    )
""")

# Cart_items (N-N entre carts et product_variants)
cursor.execute("""
    CREATE TABLE IF NOT EXISTS cart_items (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        cart_id INTEGER NOT NULL,
        product_variant_id INTEGER NOT NULL,
        quantity INTEGER NOT NULL DEFAULT 1 CHECK(quantity > 0),
        price_cents INTEGER NOT NULL,  -- Prix au moment de l'ajout
        added_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (cart_id) REFERENCES carts(id) ON DELETE CASCADE,
        FOREIGN KEY (product_variant_id) REFERENCES product_variants(id)
    )
""")

# Orders
cursor.execute("""
    CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        customer_id INTEGER NOT NULL,
        billing_address_id INTEGER NOT NULL,
        shipping_address_id INTEGER NOT NULL,
        status TEXT DEFAULT 'pending' CHECK(
            status IN ('pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded')
        ),
        subtotal_cents INTEGER NOT NULL,
        tax_cents INTEGER DEFAULT 0,
        shipping_cents INTEGER DEFAULT 0,
        total_cents INTEGER NOT NULL,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (customer_id) REFERENCES customers(id),
        FOREIGN KEY (billing_address_id) REFERENCES addresses(id),
        FOREIGN KEY (shipping_address_id) REFERENCES addresses(id)
    )
""")

# Order_items
cursor.execute("""
    CREATE TABLE IF NOT EXISTS order_items (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        order_id INTEGER NOT NULL,
        product_variant_id INTEGER NOT NULL,
        product_name TEXT NOT NULL,  -- Snapshot
        variant_name TEXT,  -- Snapshot
        quantity INTEGER NOT NULL CHECK(quantity > 0),
        unit_price_cents INTEGER NOT NULL,
        total_cents INTEGER NOT NULL,
        FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
        FOREIGN KEY (product_variant_id) REFERENCES product_variants(id),
        CHECK(total_cents = quantity * unit_price_cents)
    )
""")

# Payments
cursor.execute("""
    CREATE TABLE IF NOT EXISTS payments (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        order_id INTEGER NOT NULL,
        payment_method TEXT CHECK(payment_method IN ('card', 'paypal', 'bank_transfer')),
        amount_cents INTEGER NOT NULL,
        status TEXT DEFAULT 'pending' CHECK(
            status IN ('pending', 'completed', 'failed', 'refunded')
        ),
        transaction_id TEXT,
        paid_at TEXT,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (order_id) REFERENCES orders(id)
    )
""")

# Reviews
cursor.execute("""
    CREATE TABLE IF NOT EXISTS reviews (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        product_id INTEGER NOT NULL,
        customer_id INTEGER NOT NULL,
        rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
        title TEXT,
        comment TEXT,
        verified_purchase INTEGER DEFAULT 0 CHECK(verified_purchase IN (0, 1)),
        created_at TEXT DEFAULT CURRENT_TIMESTAMP,
        FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
        FOREIGN KEY (customer_id) REFERENCES customers(id),
        UNIQUE(product_id, customer_id)  -- Un avis par client par produit
    )
""")

conn.commit()
print("\n[OK] Schéma e-commerce créé !")

print("\n[GRAPHIQUE] ANALYSE DU MODÈLE :")
print("\nEntités principales :")
print("  - Customers (1) ──< Addresses (N)")
print("  - Products (1) ──< Product_variants (N)")
print("  - Carts (1) ──< Cart_items (N) >──< Product_variants")
print("  - Orders (1) ──< Order_items (N)")
print("  - Orders (1) ──< Payments (N)")
print("  - Products (1) ──< Reviews (N) >──< Customers")

print("\nRelations clés :")
print("  [OK] 1-N : Customer -> Addresses")
print("  [OK] 1-N : Product -> Variants")
print("  [OK] N-N : Cart <-> Variants (via Cart_items)")
print("  [OK] 1-N : Order -> Items")
print("  [OK] Snapshot : Order_items stocke nom produit (historique)")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 9
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

MODÉLISATION :
- Identifier entités (noms dans cahier des charges)
- Définir attributs (propriétés des entités)
- Choisir types de données appropriés
- Modéliser relations (1-N, N-N, 1-1)

ENTITÉS :
- Entité = Chose du monde réel
- A des attributs propres
- Relations avec autres entités

ATTRIBUTS :
- Propriétés descriptives
- Types appropriés (INTEGER, TEXT, DATE)
- Contraintes (NOT NULL, UNIQUE, CHECK)
- Valeurs par défaut (DEFAULT)

RELATIONS :
- 1-N : FK dans table "N"
- N-N : Table de liaison
- 1-1 : FK UNIQUE

IMPLÉMENTATION :
- CREATE TABLE pour entités
- FOREIGN KEY pour relations
- Contraintes pour validation


[CLE] PROCESSUS DE MODÉLISATION :

1. Analyser besoins métier
2. Identifier entités (noms)
3. Lister attributs par entité
4. Déterminer relations
5. Créer diagramme conceptuel
6. Implémenter en SQL
7. Tester avec données réelles


-> PROCHAINE ÉTAPE : Chapitre 10 - Normalisation

Vous allez apprendre à organiser vos données efficacement !
"""

conn.close()
print("\n[OK] Chapitre 9 terminé ! [BRAVO]\n")

# ============================================================================
# [LIVRE] BASES DE DONNÉES - PARTIE 3 (SUITE) : NORMALISATION ET DESIGN PATTERNS
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 10 : Normalisation (1NF, 2NF, 3NF, BCNF)
# - Chapitre 11 : Diagrammes ER et Outils
# - Chapitre 12 : Design Patterns et Best Practices
#
# [TEMPS] TEMPS : ~6-8 heures
# [DOCS] PRÉREQUIS : Chapitre 9 complété
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 10 : NORMALISATION
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce que la normalisation et pourquoi c'est important
[OK] Les anomalies de données (insertion, modification, suppression)
[OK] Les formes normales (1NF, 2NF, 3NF, BCNF)
[OK] Normaliser une base de données
[OK] Quand dénormaliser (trade-offs)
"""

import sqlite3

conn = sqlite3.connect('databases/normalization_demo.db')
cursor = conn.cursor()


# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI LA NORMALISATION ?
# ----------------------------------------------------------------------------

"""
[IDEE] NORMALISATION = Organiser données pour éviter redondance et anomalies


[DESIGN] ANALOGIE : Ranger une bibliothèque

[X] MAL RANGÉ :
- Même livre à plusieurs endroits (redondance)
- Difficile de trouver (incohérence)
- Gaspillage d'espace

[OK] BIEN RANGÉ :
- Chaque livre à UN endroit
- Classification logique
- Facile à retrouver


[ROUGE] PROBLÈMES SANS NORMALISATION :

Table non normalisée (MAUVAIS) :
┌────┬──────────┬──────────┬─────────────┬──────────┐
│ ID │ Student  │ Courses  │ Professors  │ Emails   │
├────┼──────────┼──────────┼─────────────┼──────────┤
│ 1  │ Alice    │ CS101,   │ Dr.Martin,  │ alice@   │
│    │          │ MATH201  │ Dr.Durand   │          │
│ 2  │ Bob      │ CS101    │ Dr.Martin   │ bob@     │
└────┴──────────┴──────────┴─────────────┴──────────┘

Problèmes :
[X] Cours dans une seule colonne (difficile à requêter)
[X] Duplication (Dr.Martin répété)
[X] Si on change prof de CS101, faut modifier partout !
[X] Si Alice abandonne MATH201, on perd info sur Dr.Durand


[IDEE] 3 TYPES D'ANOMALIES :

1. ANOMALIE D'INSERTION
   Impossible d'ajouter cours sans étudiant
   Exemple : Nouveau cours CS102 créé mais personne inscrit
   -> Impossible de stocker !

2. ANOMALIE DE MODIFICATION
   Modifier info = modifier partout
   Exemple : Dr.Martin devient Dr.Martin-Dupont
   -> Faut changer dans TOUTES les lignes

3. ANOMALIE DE SUPPRESSION
   Supprimer ligne = perdre info
   Exemple : Bob se désinscrit de CS101
   -> On perd l'info que CS101 existe et est enseigné par Dr.Martin
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] PREMIÈRE FORME NORMALE (1NF)
# ----------------------------------------------------------------------------

"""
[IDEE] 1NF : Valeurs ATOMIQUES (indivisibles)


[CLE] RÈGLES 1NF :

[OK] Chaque colonne contient valeurs ATOMIQUES (pas de listes)
[OK] Chaque colonne contient UN SEUL type de données
[OK] Chaque colonne a un nom UNIQUE
[OK] L'ordre des lignes n'a pas d'importance


[X] VIOLATION 1NF :
"""

# Table NON conforme 1NF
cursor.execute("""
    CREATE TABLE IF NOT EXISTS students_bad (
        id INTEGER PRIMARY KEY,
        name TEXT,
        courses TEXT,  -- [X] Liste : "CS101, MATH201, PHY101"
        phones TEXT    -- [X] Liste : "0601020304, 0605060708"
    )
""")

cursor.execute("""
    INSERT INTO students_bad VALUES
    (1, 'Alice', 'CS101, MATH201', '0601020304, 0605060708'),
    (2, 'Bob', 'CS101', '0602030405')
""")

print("[X] Table NON conforme 1NF :")
cursor.execute("SELECT * FROM students_bad")
for row in cursor.fetchall():
    print(f"  {row}")

print("\nProblèmes :")
print("  - Impossible de chercher 'CS101' facilement")
print("  - Impossible de compter combien ont CS101")
print("  - Difficile d'ajouter/supprimer un cours")

"""
[OK] CONFORME 1NF : Séparer en lignes
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS students_1nf (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS student_courses (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        student_id INTEGER,
        course_code TEXT,
        FOREIGN KEY (student_id) REFERENCES students_1nf(id)
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS student_phones (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        student_id INTEGER,
        phone TEXT,
        phone_type TEXT,  -- 'mobile', 'home'
        FOREIGN KEY (student_id) REFERENCES students_1nf(id)
    )
""")

# Données
cursor.execute("INSERT INTO students_1nf VALUES (1, 'Alice'), (2, 'Bob')")
cursor.execute("""
    INSERT INTO student_courses (student_id, course_code) VALUES
    (1, 'CS101'), (1, 'MATH201'), (2, 'CS101')
""")
cursor.execute("""
    INSERT INTO student_phones (student_id, phone, phone_type) VALUES
    (1, '0601020304', 'mobile'), (1, '0605060708', 'home'),
    (2, '0602030405', 'mobile')
""")

print("\n[OK] Table conforme 1NF :")
cursor.execute("""
    SELECT s.name, sc.course_code
    FROM students_1nf s
    LEFT JOIN student_courses sc ON s.id = sc.student_id
""")
for row in cursor.fetchall():
    print(f"  {row}")

print("\nAvantages :")
print("  [OK] Facile de chercher 'CS101'")
print("  [OK] Facile de compter inscriptions")
print("  [OK] Facile d'ajouter/supprimer cours")

"""
[CLE] RÉSUMÉ 1NF :

[X] AVANT (colonnes avec listes) :
┌────┬───────┬──────────────────┐
│ id │ name  │ courses          │
├────┼───────┼──────────────────┤
│ 1  │ Alice │ CS101, MATH201   │
└────┴───────┴──────────────────┘

[OK] APRÈS (valeurs atomiques) :
┌────┬───────┐    ┌────┬────────────┬─────────────┐
│ id │ name  │    │ id │ student_id │ course_code │
├────┼───────┤    ├────┼────────────┼─────────────┤
│ 1  │ Alice │    │ 1  │     1      │ CS101       │
└────┴───────┘    │ 2  │     1      │ MATH201     │
                  └────┴────────────┴─────────────┘
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] DEUXIÈME FORME NORMALE (2NF)
# ----------------------------------------------------------------------------

"""
[IDEE] 2NF : 1NF + Pas de dépendance partielle


[CLE] RÈGLES 2NF :

[OK] Doit être en 1NF
[OK] Chaque colonne non-clé dépend de TOUTE la clé primaire
   (pas seulement une partie)


[ATTENTION] Concerne seulement tables avec CLÉ PRIMAIRE COMPOSITE


[X] VIOLATION 2NF :
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS enrollments_bad (
        student_id INTEGER,
        course_id INTEGER,
        student_name TEXT,    -- [X] Dépend SEULEMENT de student_id
        course_title TEXT,    -- [X] Dépend SEULEMENT de course_id
        grade TEXT,           -- [OK] Dépend de (student_id, course_id)
        PRIMARY KEY (student_id, course_id)
    )
""")

cursor.execute("""
    INSERT INTO enrollments_bad VALUES
    (1, 101, 'Alice', 'Intro to Python', 'A'),
    (1, 201, 'Alice', 'Database Design', 'B'),
    (2, 101, 'Bob', 'Intro to Python', 'B')
""")

print("\n[X] Table NON conforme 2NF :")
cursor.execute("SELECT * FROM enrollments_bad")
for row in cursor.fetchall():
    print(f"  {row}")

print("\nProblèmes :")
print("  - student_name répété (redondance)")
print("  - course_title répété (redondance)")
print("  - Si on change nom Alice, faut changer partout")

"""
[IDEE] DÉPENDANCES :

Clé primaire : (student_id, course_id)

student_name dépend de student_id SEULEMENT [X]
course_title dépend de course_id SEULEMENT [X]
grade dépend de (student_id, course_id) [OK]

-> Dépendances partielles = violation 2NF


[OK] CONFORME 2NF : Séparer en tables
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS students_2nf (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS courses_2nf (
        id INTEGER PRIMARY KEY,
        title TEXT NOT NULL
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS enrollments_2nf (
        student_id INTEGER,
        course_id INTEGER,
        grade TEXT,
        PRIMARY KEY (student_id, course_id),
        FOREIGN KEY (student_id) REFERENCES students_2nf(id),
        FOREIGN KEY (course_id) REFERENCES courses_2nf(id)
    )
""")

# Données
cursor.execute("INSERT INTO students_2nf VALUES (1, 'Alice'), (2, 'Bob')")
cursor.execute("""
    INSERT INTO courses_2nf VALUES
    (101, 'Intro to Python'),
    (201, 'Database Design')
""")
cursor.execute("""
    INSERT INTO enrollments_2nf VALUES
    (1, 101, 'A'), (1, 201, 'B'), (2, 101, 'B')
""")

print("\n[OK] Tables conformes 2NF :")
cursor.execute("""
    SELECT s.name, c.title, e.grade
    FROM enrollments_2nf e
    JOIN students_2nf s ON e.student_id = s.id
    JOIN courses_2nf c ON e.course_id = c.id
""")
for row in cursor.fetchall():
    print(f"  {row}")

print("\nAvantages :")
print("  [OK] Pas de redondance de noms/titres")
print("  [OK] Modifier nom Alice = un seul endroit")
print("  [OK] Ajouter cours sans inscription = possible")

"""
[CLE] RÉSUMÉ 2NF :

[X] AVANT (dépendances partielles) :
┌────────────┬───────────┬──────────────┬──────────────────┬───────┐
│ student_id │ course_id │ student_name │ course_title     │ grade │
├────────────┼───────────┼──────────────┼──────────────────┼───────┤
│     1      │    101    │ Alice        │ Intro to Python  │   A   │
│     1      │    201    │ Alice        │ Database Design  │   B   │
└────────────┴───────────┴──────────────┴──────────────────┴───────┘
         v             v
    Redondance    Redondance

[OK] APRÈS (pas de dépendance partielle) :
Students          Courses           Enrollments
┌────┬───────┐   ┌─────┬────────┐  ┌────────┬─────────┬───────┐
│ id │ name  │   │ id  │ title  │  │ s_id   │ c_id    │ grade │
├────┼───────┤   ├─────┼────────┤  ├────────┼─────────┼───────┤
│ 1  │ Alice │   │ 101 │ Py...  │  │   1    │  101    │   A   │
└────┴───────┘   └─────┴────────┘  └────────┴─────────┴───────┘
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] TROISIÈME FORME NORMALE (3NF)
# ----------------------------------------------------------------------------

"""
[IDEE] 3NF : 2NF + Pas de dépendance transitive


[CLE] RÈGLES 3NF :

[OK] Doit être en 2NF
[OK] Pas de dépendance transitive
   (colonne non-clé ne doit PAS dépendre d'une autre colonne non-clé)


[X] VIOLATION 3NF :
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS employees_bad (
        id INTEGER PRIMARY KEY,
        name TEXT,
        department_id INTEGER,
        department_name TEXT,    -- [X] Dépend de department_id (transitive)
        department_location TEXT -- [X] Dépend de department_id (transitive)
    )
""")

cursor.execute("""
    INSERT INTO employees_bad VALUES
    (1, 'Alice', 10, 'IT', 'Building A'),
    (2, 'Bob', 10, 'IT', 'Building A'),
    (3, 'Charlie', 20, 'HR', 'Building B')
""")

print("\n[X] Table NON conforme 3NF :")
cursor.execute("SELECT * FROM employees_bad")
for row in cursor.fetchall():
    print(f"  {row}")

print("\nProblèmes :")
print("  - department_name répété pour chaque employé IT")
print("  - Si IT déménage, faut changer partout")

"""
[IDEE] DÉPENDANCE TRANSITIVE :

id -> department_id -> department_name
id -> department_id -> department_location

department_name dépend de id VIA department_id [X]

Règle : Colonne non-clé ne doit dépendre QUE de la clé primaire,
        pas d'une autre colonne non-clé


[OK] CONFORME 3NF : Séparer en tables
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS departments (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        location TEXT
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS employees_3nf (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        department_id INTEGER,
        FOREIGN KEY (department_id) REFERENCES departments(id)
    )
""")

# Données
cursor.execute("""
    INSERT INTO departments VALUES
    (10, 'IT', 'Building A'),
    (20, 'HR', 'Building B')
""")

cursor.execute("""
    INSERT INTO employees_3nf VALUES
    (1, 'Alice', 10),
    (2, 'Bob', 10),
    (3, 'Charlie', 20)
""")

print("\n[OK] Tables conformes 3NF :")
cursor.execute("""
    SELECT e.name, d.name AS department, d.location
    FROM employees_3nf e
    JOIN departments d ON e.department_id = d.id
""")
for row in cursor.fetchall():
    print(f"  {row}")

print("\nAvantages :")
print("  [OK] Pas de redondance département")
print("  [OK] Changer localisation IT = un seul endroit")
print("  [OK] Ajouter département sans employé = possible")

"""
[CLE] RÉSUMÉ 3NF :

[X] AVANT (dépendance transitive) :
┌────┬─────────┬────────────┬─────────────────┬────────────────────┐
│ id │ name    │ dept_id    │ dept_name       │ dept_location      │
├────┼─────────┼────────────┼─────────────────┼────────────────────┤
│ 1  │ Alice   │     10     │ IT              │ Building A         │
│ 2  │ Bob     │     10     │ IT              │ Building A         │
└────┴─────────┴────────────┴─────────────────┴────────────────────┘
                      v              v
                 Redondance    Redondance

[OK] APRÈS (pas de dépendance transitive) :
Employees                 Departments
┌────┬─────────┬─────────┐  ┌────┬──────┬────────────┐
│ id │ name    │ dept_id │  │ id │ name │ location   │
├────┼─────────┼─────────┤  ├────┼──────┼────────────┤
│ 1  │ Alice   │   10    │  │ 10 │ IT   │ Building A │
│ 2  │ Bob     │   10    │  │ 20 │ HR   │ Building B │
└────┴─────────┴─────────┘  └────┴──────┴────────────┘
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] FORME NORMALE DE BOYCE-CODD (BCNF)
# ----------------------------------------------------------------------------

"""
[IDEE] BCNF : Version plus stricte de 3NF


[CLE] RÈGLES BCNF :

[OK] Doit être en 3NF
[OK] Chaque déterminant est une clé candidate


[IDEE] DIFFÉRENCE 3NF vs BCNF :

3NF : Colonne non-clé ne dépend pas d'autre colonne non-clé
BCNF : TOUT déterminant doit être clé candidate


[ATTENTION] Cas rare où 3NF ≠ BCNF :

[X] VIOLATION BCNF (mais conforme 3NF) :
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS course_instructors_bad (
        student_id INTEGER,
        course_id INTEGER,
        instructor_id INTEGER,
        -- Contrainte métier : Un prof enseigne UN SEUL cours
        PRIMARY KEY (student_id, course_id)
    )
""")

"""
Problème :
instructor_id -> course_id (un prof = un cours)
Mais instructor_id n'est PAS clé candidate

Donc violation BCNF


[OK] CONFORME BCNF :
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS course_instructors (
        course_id INTEGER PRIMARY KEY,
        instructor_id INTEGER UNIQUE  -- Un prof = un cours
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS student_courses_bcnf (
        student_id INTEGER,
        course_id INTEGER,
        PRIMARY KEY (student_id, course_id),
        FOREIGN KEY (course_id) REFERENCES course_instructors(course_id)
    )
""")

"""
[CLE] BCNF en pratique :

La plupart des schémas bien conçus en 3NF sont aussi en BCNF
BCNF important pour cas très spécifiques avec contraintes complexes


[ATTENTION] En pratique : Visez 3NF, BCNF est un bonus
"""


# ----------------------------------------------------------------------------
# [GRAPHIQUE] PROCESSUS DE NORMALISATION COMPLET
# ----------------------------------------------------------------------------

"""
[IDEE] EXEMPLE COMPLET : Normaliser une table de commandes


[X] TABLE NON NORMALISÉE (0NF) :
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS orders_0nf (
        order_id INTEGER PRIMARY KEY,
        customer_name TEXT,
        customer_email TEXT,
        customer_address TEXT,
        products TEXT,              -- [X] Liste : "Product1, Product2"
        prices TEXT,                -- [X] Liste : "10.99, 25.50"
        quantities TEXT,            -- [X] Liste : "2, 1"
        order_total REAL,
        order_date TEXT
    )
""")

cursor.execute("""
    INSERT INTO orders_0nf VALUES
    (1, 'Alice', 'alice@email.com', '10 rue Paris', 
     'Laptop, Mouse', '999.99, 25.50', '1, 2', 1050.99, '2024-01-15'),
    (2, 'Bob', 'bob@email.com', '20 rue Lyon',
     'Keyboard', '79.99', '1', 79.99, '2024-01-16')
""")

print("\n[X] Table 0NF (pas normalisée) :")
cursor.execute("SELECT * FROM orders_0nf LIMIT 1")
print(cursor.fetchone())

"""
ÉTAPE 1 : Atteindre 1NF (valeurs atomiques)
───────────────────────────────────────────
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS orders_1nf (
        order_id INTEGER,
        customer_name TEXT,
        customer_email TEXT,
        customer_address TEXT,
        product_name TEXT,     -- [OK] Atomique
        price REAL,            -- [OK] Atomique
        quantity INTEGER,      -- [OK] Atomique
        order_date TEXT,
        PRIMARY KEY (order_id, product_name)
    )
""")

cursor.execute("""
    INSERT INTO orders_1nf VALUES
    (1, 'Alice', 'alice@email.com', '10 rue Paris', 'Laptop', 999.99, 1, '2024-01-15'),
    (1, 'Alice', 'alice@email.com', '10 rue Paris', 'Mouse', 25.50, 2, '2024-01-15'),
    (2, 'Bob', 'bob@email.com', '20 rue Lyon', 'Keyboard', 79.99, 1, '2024-01-16')
""")

print("\n[OK] Table 1NF (valeurs atomiques) :")
cursor.execute("SELECT * FROM orders_1nf")
for row in cursor.fetchall():
    print(f"  {row}")

"""
ÉTAPE 2 : Atteindre 2NF (pas de dépendance partielle)
─────────────────────────────────────────────────────

Clé primaire composite : (order_id, product_name)

Dépendances :
- customer_name dépend de order_id SEULEMENT [X]
- customer_email dépend de order_id SEULEMENT [X]
- customer_address dépend de order_id SEULEMENT [X]
- order_date dépend de order_id SEULEMENT [X]
- price dépend de product_name SEULEMENT [X]
- quantity dépend de (order_id, product_name) [OK]

-> Séparer en tables
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS orders_2nf (
        order_id INTEGER PRIMARY KEY,
        customer_name TEXT,
        customer_email TEXT,
        customer_address TEXT,
        order_date TEXT
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS products_2nf (
        product_id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT UNIQUE,
        price REAL
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS order_items_2nf (
        order_id INTEGER,
        product_id INTEGER,
        quantity INTEGER,
        PRIMARY KEY (order_id, product_id),
        FOREIGN KEY (order_id) REFERENCES orders_2nf(order_id),
        FOREIGN KEY (product_id) REFERENCES products_2nf(product_id)
    )
""")

# Données
cursor.execute("""
    INSERT INTO orders_2nf VALUES
    (1, 'Alice', 'alice@email.com', '10 rue Paris', '2024-01-15'),
    (2, 'Bob', 'bob@email.com', '20 rue Lyon', '2024-01-16')
""")

cursor.execute("""
    INSERT INTO products_2nf (name, price) VALUES
    ('Laptop', 999.99),
    ('Mouse', 25.50),
    ('Keyboard', 79.99)
""")

cursor.execute("""
    INSERT INTO order_items_2nf VALUES
    (1, 1, 1), (1, 2, 2), (2, 3, 1)
""")

print("\n[OK] Tables 2NF (pas de dépendance partielle) :")
cursor.execute("""
    SELECT o.order_id, o.customer_name, p.name, oi.quantity
    FROM orders_2nf o
    JOIN order_items_2nf oi ON o.order_id = oi.order_id
    JOIN products_2nf p ON oi.product_id = p.product_id
""")
for row in cursor.fetchall():
    print(f"  {row}")

"""
ÉTAPE 3 : Atteindre 3NF (pas de dépendance transitive)
──────────────────────────────────────────────────────

Dans orders_2nf :
- customer_name, customer_email, customer_address
  dépendent TOUS de order_id
  
MAIS : email et address dépendent vraiment du CUSTOMER
       pas de la commande !

-> Séparer customers
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS customers_3nf (
        customer_id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT,
        email TEXT UNIQUE,
        address TEXT
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS orders_3nf (
        order_id INTEGER PRIMARY KEY,
        customer_id INTEGER,
        order_date TEXT,
        FOREIGN KEY (customer_id) REFERENCES customers_3nf(customer_id)
    )
""")

# Données
cursor.execute("""
    INSERT INTO customers_3nf (name, email, address) VALUES
    ('Alice', 'alice@email.com', '10 rue Paris'),
    ('Bob', 'bob@email.com', '20 rue Lyon')
""")

cursor.execute("""
    INSERT INTO orders_3nf VALUES
    (1, 1, '2024-01-15'),
    (2, 2, '2024-01-16')
""")

print("\n[OK] Tables 3NF (pas de dépendance transitive) :")
cursor.execute("""
    SELECT o.order_id, c.name, c.email, o.order_date
    FROM orders_3nf o
    JOIN customers_3nf c ON o.customer_id = c.customer_id
""")
for row in cursor.fetchall():
    print(f"  {row}")

"""
[BRAVO] SCHÉMA FINAL NORMALISÉ (3NF) :

Customers
┌─────────────┬──────┬───────┬─────────┐
│ customer_id │ name │ email │ address │
└─────────────┴──────┴───────┴─────────┘
       ^
       │ FK
Orders
┌──────────┬─────────────┬────────────┐
│ order_id │ customer_id │ order_date │
└──────────┴─────────────┴────────────┘
       ^
       │ FK
Order_Items
┌──────────┬────────────┬──────────┐
│ order_id │ product_id │ quantity │
└──────────┴────────────┴──────────┘
                  ^
                  │ FK
Products
┌────────────┬──────┬───────┐
│ product_id │ name │ price │
└────────────┴──────┴───────┘
"""


# ----------------------------------------------------------------------------
# [SCALES] DÉNORMALISATION (Quand casser les règles)
# ----------------------------------------------------------------------------

"""
[IDEE] DÉNORMALISATION = Ajouter redondance intentionnellement


[REFLEXION] POURQUOI DÉNORMALISER ?

[RAPIDE] PERFORMANCE :
- Éviter JOIN coûteux
- Réduire nombre de requêtes
- Accélérer lectures fréquentes

[ARGENT] SIMPLIFICATION :
- Requêtes plus simples
- Moins de tables à joindre


[ATTENTION] QUAND DÉNORMALISER ?

[OK] Lectures >> Écritures (10:1 ou plus)
[OK] Performance critique
[OK] Données rarement modifiées
[OK] Rapports et analytics


[X] NE PAS dénormaliser si :

[X] Écritures fréquentes
[X] Données changeant souvent
[X] Système transactionnel


[IDEE] EXEMPLES DÉNORMALISATION :

1. COLONNES CALCULÉES
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS orders_denorm (
        order_id INTEGER PRIMARY KEY,
        customer_id INTEGER,
        order_total REAL,  -- [X] Calculable depuis order_items
        item_count INTEGER -- [X] Calculable depuis order_items
    )
""")

"""
Avantages :
[OK] SELECT order_total rapide (pas de SUM)
[OK] Pas de JOIN avec order_items

Inconvénients :
[X] Faut mettre à jour si item change
[X] Risque incohérence


2. DONNÉES DUPLIQUÉES (Snapshot)
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS order_items_denorm (
        id INTEGER PRIMARY KEY,
        order_id INTEGER,
        product_id INTEGER,
        product_name TEXT,   -- [X] Dupliqué depuis products
        unit_price REAL,     -- [X] Dupliqué depuis products
        quantity INTEGER
    )
""")

"""
Avantages :
[OK] Historique préservé (prix au moment de commande)
[OK] Pas de JOIN avec products pour afficher commande

Use case valide : E-commerce (garder prix historique)


3. TABLES DE RÉSUMÉ (Materialized Views)
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS daily_sales_summary (
        date TEXT PRIMARY KEY,
        total_orders INTEGER,
        total_revenue REAL,
        avg_order_value REAL,
        updated_at TEXT
    )
""")

"""
Avantages :
[OK] Dashboard rapide
[OK] Pas de calcul temps réel

Inconvénients :
[X] Faut régénérer (trigger ou cron)


[CLE] BEST PRACTICES DÉNORMALISATION :

1. Documenter (commentaires dans schéma)
2. Maintenir cohérence (triggers)
3. Mesurer gains réels (benchmarks)
4. Garder source de vérité
5. Ne pas tout dénormaliser !


[SCALES] NORMALISATION vs DÉNORMALISATION :

┌──────────────────┬─────────────────┬────────────────────┐
│                  │ Normalisation   │ Dénormalisation    │
├──────────────────┼─────────────────┼────────────────────┤
│ Redondance       │ Minimale        │ Intentionnelle     │
│ Cohérence        │ Facile          │ Difficile          │
│ Performance READ │ Peut être lente │ Rapide             │
│ Performance WRITE│ Rapide          │ Peut être lente    │
│ Complexité       │ Plus de tables  │ Moins de tables    │
│ Use case         │ OLTP            │ OLAP/Analytics     │
└──────────────────┴─────────────────┴────────────────────┘

OLTP = Online Transaction Processing (e-commerce, banking)
OLAP = Online Analytical Processing (reporting, analytics)
"""


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 8 : Normalisation Complète
# ----------------------------------------------------------------------------

"""
OBJECTIF : Normaliser une table mal conçue


TABLE INITIALE (mauvaise) :
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS hospital_bad (
        record_id INTEGER PRIMARY KEY,
        patient_name TEXT,
        patient_dob TEXT,
        patient_address TEXT,
        doctor_name TEXT,
        doctor_specialty TEXT,
        doctor_phone TEXT,
        appointment_date TEXT,
        diagnosis TEXT,
        medications TEXT,  -- Liste : "Med1, Med2"
        medication_dosages TEXT  -- Liste : "10mg, 20mg"
    )
""")

cursor.execute("""
    INSERT INTO hospital_bad VALUES
    (1, 'Alice Dupont', '1990-05-15', '10 rue Paris', 
     'Dr. Martin', 'Cardiology', '0601020304',
     '2024-01-15', 'Hypertension', 'Lisinopril, Aspirin', '10mg, 81mg'),
    (2, 'Alice Dupont', '1990-05-15', '10 rue Paris',
     'Dr. Durand', 'General', '0602030405',
     '2024-01-20', 'Flu', 'Tamiflu', '75mg')
""")

print("\n" + "="*60)
print("EXERCICE : Normaliser schéma hôpital")
print("="*60)

print("\n[X] TABLE INITIALE (problèmes multiples) :")
cursor.execute("SELECT * FROM hospital_bad")
for row in cursor.fetchall():
    print(f"  {row}")

"""
SOLUTION : Normalisation progressive


ÉTAPE 1 : 1NF (atomique)
"""

cursor.execute("""
    CREATE TABLE IF NOT EXISTS patients (
        patient_id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        date_of_birth TEXT,
        address TEXT
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS doctors (
        doctor_id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        specialty TEXT,
        phone TEXT
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS medications (
        medication_id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT UNIQUE NOT NULL
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS appointments (
        appointment_id INTEGER PRIMARY KEY AUTOINCREMENT,
        patient_id INTEGER,
        doctor_id INTEGER,
        appointment_date TEXT NOT NULL,
        diagnosis TEXT,
        FOREIGN KEY (patient_id) REFERENCES patients(patient_id),
        FOREIGN KEY (doctor_id) REFERENCES doctors(doctor_id)
    )
""")

cursor.execute("""
    CREATE TABLE IF NOT EXISTS prescriptions (
        prescription_id INTEGER PRIMARY KEY AUTOINCREMENT,
        appointment_id INTEGER,
        medication_id INTEGER,
        dosage TEXT,
        FOREIGN KEY (appointment_id) REFERENCES appointments(appointment_id),
        FOREIGN KEY (medication_id) REFERENCES medications(medication_id)
    )
""")

# Insérer données
cursor.execute("""
    INSERT INTO patients (name, date_of_birth, address)
    VALUES ('Alice Dupont', '1990-05-15', '10 rue Paris')
""")

cursor.execute("""
    INSERT INTO doctors (name, specialty, phone) VALUES
    ('Dr. Martin', 'Cardiology', '0601020304'),
    ('Dr. Durand', 'General', '0602030405')
""")

cursor.execute("""
    INSERT INTO medications (name) VALUES
    ('Lisinopril'), ('Aspirin'), ('Tamiflu')
""")

cursor.execute("""
    INSERT INTO appointments (patient_id, doctor_id, appointment_date, diagnosis) VALUES
    (1, 1, '2024-01-15', 'Hypertension'),
    (1, 2, '2024-01-20', 'Flu')
""")

cursor.execute("""
    INSERT INTO prescriptions (appointment_id, medication_id, dosage) VALUES
    (1, 1, '10mg'), (1, 2, '81mg'),
    (2, 3, '75mg')
""")

conn.commit()

print("\n[OK] SCHÉMA NORMALISÉ (3NF) :")
cursor.execute("""
    SELECT 
        p.name AS patient,
        d.name AS doctor,
        a.appointment_date,
        a.diagnosis,
        GROUP_CONCAT(m.name || ' ' || pr.dosage, ', ') AS medications
    FROM appointments a
    JOIN patients p ON a.patient_id = p.patient_id
    JOIN doctors d ON a.doctor_id = d.doctor_id
    LEFT JOIN prescriptions pr ON a.appointment_id = pr.appointment_id
    LEFT JOIN medications m ON pr.medication_id = m.medication_id
    GROUP BY a.appointment_id
""")

for row in cursor.fetchall():
    print(f"\n  Patient: {row[0]}")
    print(f"  Docteur: {row[1]}")
    print(f"  Date: {row[2]}")
    print(f"  Diagnostic: {row[3]}")
    print(f"  Médicaments: {row[4]}")

print("\n[OK] Avantages du schéma normalisé :")
print("  - Pas de redondance patient/docteur")
print("  - Médicaments réutilisables")
print("  - Historique complet préservé")
print("  - Facile d'ajouter rendez-vous/médicaments")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 10
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

NORMALISATION :
- Pourquoi normaliser (éviter anomalies)
- Formes normales (1NF, 2NF, 3NF, BCNF)
- Processus de normalisation
- Identifier violations

1NF - Valeurs Atomiques :
[X] Pas de listes dans colonnes
[OK] Chaque valeur indivisible

2NF - Pas de Dépendance Partielle :
[X] Colonne dépend d'une PARTIE de clé composite
[OK] Séparer en tables

3NF - Pas de Dépendance Transitive :
[X] Colonne non-clé dépend d'autre colonne non-clé
[OK] Séparer en tables

BCNF - Déterminant = Clé :
[OK] Version stricte de 3NF

DÉNORMALISATION :
- Quand (performance READ)
- Comment (colonnes calculées, snapshots)
- Trade-offs (cohérence vs vitesse)


[CLE] PROCESSUS :

1. Identifier anomalies
2. Atteindre 1NF (atomique)
3. Atteindre 2NF (dépendances partielles)
4. Atteindre 3NF (dépendances transitives)
5. Vérifier BCNF si nécessaire
6. Dénormaliser si justifié


[SCALES] RÈGLE D'OR :

Normaliser d'abord (3NF minimum)
Dénormaliser seulement si :
- Performance critique
- Mesures prouvent le besoin
- Maintenance cohérence assurée


-> PROCHAINE ÉTAPE : Chapitre 11 - Diagrammes ER

Vous allez apprendre à visualiser vos modèles !
"""

conn.close()
print("\n[OK] Chapitre 10 terminé ! [BRAVO]\n")

# ============================================================================
# [LIVRE] BASES DE DONNÉES - PARTIE 4 : INTÉGRATION PYTHON
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 13 : Python + SQLite (sqlite3, context managers, connection pooling)
# - Chapitre 14 : Python + PostgreSQL (psycopg2, asyncpg)
# - Chapitre 15 : SQLAlchemy ORM
# - Chapitre 16 : Migrations et Alembic
#
# [TEMPS] TEMPS : ~10-12 heures
# [DOCS] PRÉREQUIS : Parties 1-3 complétées
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 13 : PYTHON + SQLite
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser sqlite3 comme un pro
[OK] Gérer connexions et curseurs proprement
[OK] Paramètres et SQL injection
[OK] Transactions et rollback
[OK] Context managers et best practices
[OK] Connection pooling
[OK] Types de données Python <-> SQLite
[OK] Créer fonctions et agrégations personnalisées
"""


# ----------------------------------------------------------------------------
# [RAPIDE] BASES : sqlite3 Module
# ----------------------------------------------------------------------------

"""
[IDEE] sqlite3 : Module standard Python (pas d'installation nécessaire)


WORKFLOW DE BASE :

1. Import
2. Connect
3. Cursor
4. Execute
5. Commit
6. Close
"""

import sqlite3
from pathlib import Path

# 1. Import (déjà fait)

# 2. Connect
db_path = Path('databases/python_integration.db')
db_path.parent.mkdir(exist_ok=True)

conn = sqlite3.connect(db_path)

# 3. Cursor
cursor = conn.cursor()

# 4. Execute
cursor.execute("""
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        username TEXT NOT NULL UNIQUE,
        email TEXT NOT NULL,
        created_at TEXT DEFAULT CURRENT_TIMESTAMP
    )
""")

# 5. Commit (pour modifications)
conn.commit()

# 6. Close (important!)
cursor.close()
conn.close()

print("[OK] Base de données créée et fermée proprement")

"""
[IDEE] TYPES DE CONNEXION :

1. Fichier persistant
"""
conn = sqlite3.connect('mydb.db')

"""
2. Base en mémoire (temporaire, rapide)
"""
conn = sqlite3.connect(':memory:')

"""
3. Base partagée en mémoire
"""
conn = sqlite3.connect('file::memory:?cache=shared', uri=True)


# ----------------------------------------------------------------------------
# [VERROUILLE] SÉCURITÉ : Éviter SQL Injection
# ----------------------------------------------------------------------------

"""
[IDEE] SQL INJECTION : Attaque par injection de code SQL malveillant


[X] DANGER : Interpolation de chaînes
"""

# NE JAMAIS FAIRE ÇA !
username = "admin' OR '1'='1"
# query = f"SELECT * FROM users WHERE username = '{username}'"
# cursor.execute(query)
# -> SQL Injection ! Retourne tous les utilisateurs

"""
Requête générée :
SELECT * FROM users WHERE username = 'admin' OR '1'='1'
                                               ^
                                        Toujours vrai !


[OK] SOLUTION 1 : Placeholders (?)
"""

conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        username TEXT,
        email TEXT
    )
""")

# Sécurisé : Placeholder
username = "admin' OR '1'='1"
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))

# SQLite échappe automatiquement
# Requête : SELECT * FROM users WHERE username = 'admin'' OR ''1''=''1'
#                                                    ^^ Échappé

print("\n[OK] Placeholder sécurisé (aucun résultat) :")
print(cursor.fetchall())

"""
[IDEE] TYPES DE PLACEHOLDERS :


STYLE QMARK (?) - Recommandé SQLite
"""
cursor.execute("INSERT INTO users (username, email) VALUES (?, ?)", 
               ("alice", "alice@email.com"))

"""
STYLE NAMED (:name) - Plus lisible
"""
cursor.execute("""
    INSERT INTO users (username, email) 
    VALUES (:user, :email)
""", {"user": "bob", "email": "bob@email.com"})

"""
[CLE] RÈGLES D'OR :

[OK] TOUJOURS utiliser placeholders
[OK] JAMAIS de f-strings ou % avec données utilisateur
[OK] JAMAIS de concaténation manuelle


[X] MAUVAIS :
"""
# user_input = "..."
# cursor.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
# cursor.execute("SELECT * FROM users WHERE name = '%s'" % user_input)

"""
[OK] BON :
"""
# cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))


# ----------------------------------------------------------------------------
# [SYNC] CRUD OPERATIONS PROPRES
# ----------------------------------------------------------------------------

"""
[IDEE] PATTERNS POUR OPÉRATIONS COURANTES
"""

conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

# Créer table
cursor.execute("""
    CREATE TABLE products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        price REAL NOT NULL,
        stock INTEGER DEFAULT 0
    )
""")

"""
1⃣ CREATE (INSERT)
"""

# Une ligne
cursor.execute("""
    INSERT INTO products (name, price, stock)
    VALUES (?, ?, ?)
""", ("Laptop", 999.99, 10))

# Récupérer ID inséré
new_id = cursor.lastrowid
print(f"\n[OK] Produit inséré avec ID : {new_id}")

# Plusieurs lignes (executemany)
products = [
    ("Mouse", 25.50, 50),
    ("Keyboard", 79.99, 30),
    ("Monitor", 299.99, 15)
]

cursor.executemany("""
    INSERT INTO products (name, price, stock)
    VALUES (?, ?, ?)
""", products)

print(f"[OK] {cursor.rowcount} produits insérés")

conn.commit()

"""
2⃣ READ (SELECT)
"""

# fetchone() - Une ligne
cursor.execute("SELECT * FROM products WHERE id = ?", (1,))
product = cursor.fetchone()
print(f"\n[PACKAGE] fetchone() : {product}")

# fetchall() - Toutes les lignes
cursor.execute("SELECT * FROM products WHERE price < ?", (100,))
cheap_products = cursor.fetchall()
print(f"\n[PACKAGE] fetchall() ({len(cheap_products)} résultats) :")
for p in cheap_products:
    print(f"  {p}")

# fetchmany(n) - n lignes
cursor.execute("SELECT * FROM products")
some_products = cursor.fetchmany(2)
print(f"\n[PACKAGE] fetchmany(2) : {len(some_products)} résultats")

# Itérer sur curseur (memory efficient)
cursor.execute("SELECT * FROM products")
print("\n[PACKAGE] Itération directe :")
for row in cursor:
    print(f"  {row[1]} : ${row[2]}")

"""
[IDEE] ROW FACTORY : Accéder par nom de colonne
"""

# Par défaut : tuples
conn.row_factory = sqlite3.Row  # Active Row factory

cursor = conn.cursor()
cursor.execute("SELECT * FROM products WHERE id = 1")
product = cursor.fetchone()

print(f"\n[OUTIL] Row factory activé :")
print(f"  Nom : {product['name']}")
print(f"  Prix : {product['price']}")
print(f"  Stock : {product['stock']}")

# Aussi accessible par index
print(f"  ID (index) : {product[0]}")

# Convertir en dict
product_dict = dict(product)
print(f"  Dict : {product_dict}")

"""
3⃣ UPDATE
"""

# Réinitialiser row_factory pour suite
conn.row_factory = None
cursor = conn.cursor()

cursor.execute("""
    UPDATE products
    SET stock = stock - ?
    WHERE id = ?
""", (5, 1))

print(f"\n[OK] {cursor.rowcount} produit(s) mis à jour")

# Update avec vérification
cursor.execute("""
    UPDATE products
    SET price = ?
    WHERE id = ? AND price != ?
""", (999.00, 1, 999.00))

if cursor.rowcount == 0:
    print("[ATTENTION] Aucun changement (prix déjà à jour)")

conn.commit()

"""
4⃣ DELETE
"""

cursor.execute("DELETE FROM products WHERE stock = 0")
deleted = cursor.rowcount
print(f"\n[OK] {deleted} produit(s) en rupture supprimé(s)")

conn.commit()


# ----------------------------------------------------------------------------
# [SYNC] TRANSACTIONS
# ----------------------------------------------------------------------------

"""
[IDEE] TRANSACTION : Groupe d'opérations atomiques

Propriétés ACID :
- Atomicity : Tout ou rien
- Consistency : État cohérent
- Isolation : Transactions indépendantes
- Durability : Changements permanents


MODE PAR DÉFAUT : AUTOCOMMIT OFF

Chaque modification nécessite commit()
"""

conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE accounts (
        id INTEGER PRIMARY KEY,
        name TEXT,
        balance REAL
    )
""")

cursor.executemany("""
    INSERT INTO accounts (name, balance) VALUES (?, ?)
""", [("Alice", 1000), ("Bob", 500)])

conn.commit()

"""
[IDEE] TRANSACTION MANUELLE :

BEGIN -> Operations -> COMMIT ou ROLLBACK
"""

try:
    # Début transaction (implicite au premier execute)
    
    # Transférer 200 d'Alice vers Bob
    cursor.execute("""
        UPDATE accounts SET balance = balance - ?
        WHERE name = ?
    """, (200, "Alice"))
    
    # Simuler erreur
    # raise Exception("Erreur bancaire!")
    
    cursor.execute("""
        UPDATE accounts SET balance = balance + ?
        WHERE name = ?
    """, (200, "Bob"))
    
    # Tout OK -> Commit
    conn.commit()
    print("\n[OK] Transfert réussi (COMMIT)")
    
except Exception as e:
    # Erreur -> Rollback
    conn.rollback()
    print(f"\n[X] Erreur : {e}")
    print("<- Transaction annulée (ROLLBACK)")

# Vérifier soldes
cursor.execute("SELECT name, balance FROM accounts")
print("\n[ARGENT] Soldes finaux :")
for name, balance in cursor.fetchall():
    print(f"  {name} : ${balance}")

"""
[IDEE] ISOLATION LEVELS :

SQLite supporte :
- DEFERRED (défaut)
- IMMEDIATE
- EXCLUSIVE
"""

# Démarrer transaction exclusive
cursor.execute("BEGIN EXCLUSIVE")
# ... opérations
conn.commit()

"""
[CLE] BEST PRACTICES TRANSACTIONS :

[OK] Courtes et rapides
[OK] Un seul type d'opération (lecture OU écriture)
[OK] Toujours gérer exceptions
[OK] Toujours commit() ou rollback()
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] CONTEXT MANAGERS (with statement)
# ----------------------------------------------------------------------------

"""
[IDEE] CONTEXT MANAGER : Gestion automatique ressources


[X] SANS CONTEXT MANAGER :
"""

def get_user_bad(user_id):
    conn = sqlite3.connect('db.db')
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
    user = cursor.fetchone()
    cursor.close()
    conn.close()  # Oubli facile !
    return user

"""
Problèmes :
- Oubli de close()
- Exception -> connexion jamais fermée
- Code répétitif


[OK] AVEC CONTEXT MANAGER :
"""

def get_user_good(user_id):
    with sqlite3.connect('db.db') as conn:
        cursor = conn.cursor()
        cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
        return cursor.fetchone()
    # Fermeture automatique (même si exception)

"""
[IDEE] Avantages :

[OK] Fermeture automatique
[OK] Commit automatique si pas d'erreur
[OK] Rollback automatique si erreur
[OK] Code plus propre


[HOT] CONTEXT MANAGER CUSTOM :
"""

from contextlib import contextmanager

@contextmanager
def get_db_cursor(db_path):
    """Context manager pour curseur BD"""
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row  # Row factory activé
    cursor = conn.cursor()
    
    try:
        yield cursor  # Donner curseur
        conn.commit()  # Commit si succès
    except Exception:
        conn.rollback()  # Rollback si erreur
        raise
    finally:
        cursor.close()
        conn.close()

# Utilisation
"""
with get_db_cursor('db.db') as cursor:
    cursor.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
    # Commit automatique
"""

print("\n[OK] Context manager custom créé")

"""
[IDEE] CLASSE DATABASE WRAPPER :
"""

class Database:
    """Wrapper simple pour SQLite"""
    
    def __init__(self, db_path):
        self.db_path = db_path
        self.conn = None
        
    def connect(self):
        """Ouvrir connexion"""
        if not self.conn:
            self.conn = sqlite3.connect(self.db_path)
            self.conn.row_factory = sqlite3.Row
        return self.conn
    
    def close(self):
        """Fermer connexion"""
        if self.conn:
            self.conn.close()
            self.conn = None
    
    def __enter__(self):
        """Support context manager"""
        return self.connect()
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Cleanup context manager"""
        if exc_type is None:
            self.conn.commit()
        else:
            self.conn.rollback()
        self.close()
        return False
    
    def execute(self, query, params=None):
        """Execute une requête"""
        conn = self.connect()
        cursor = conn.cursor()
        
        if params:
            cursor.execute(query, params)
        else:
            cursor.execute(query)
        
        return cursor
    
    def fetchone(self, query, params=None):
        """Fetch une ligne"""
        cursor = self.execute(query, params)
        return cursor.fetchone()
    
    def fetchall(self, query, params=None):
        """Fetch toutes lignes"""
        cursor = self.execute(query, params)
        return cursor.fetchall()
    
    def commit(self):
        """Commit transaction"""
        if self.conn:
            self.conn.commit()

# Utilisation
db = Database(':memory:')

with db:
    db.execute("""
        CREATE TABLE users (
            id INTEGER PRIMARY KEY,
            name TEXT
        )
    """)
    db.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))

# Auto-commit et fermeture

db = Database(':memory:')
db.connect()
db.execute("CREATE TABLE test (id INTEGER)")
user = db.fetchone("SELECT * FROM users WHERE id = ?", (1,))
db.close()

print("\n[OK] Classe Database wrapper créée")


# ----------------------------------------------------------------------------
# [OUTIL] FONCTIONS ET AGRÉGATIONS PERSONNALISÉES
# ----------------------------------------------------------------------------

"""
[IDEE] ÉTENDRE SQLite avec Python !


1⃣ FONCTION SCALAIRE (une valeur -> une valeur)
"""

conn = sqlite3.connect(':memory:')

# Fonction Python
def python_upper(text):
    """Convertir en majuscules"""
    return text.upper() if text else None

# Enregistrer dans SQLite
conn.create_function("py_upper", 1, python_upper)
#                      ^         ^   ^
#                    nom     nb args fonction

# Utiliser dans SQL
cursor = conn.cursor()
cursor.execute("SELECT py_upper('hello world')")
print(f"\n[OUTIL] Fonction personnalisée : {cursor.fetchone()[0]}")

"""
[IDEE] FONCTION PLUS COMPLEXE : Validation email
"""

import re

def is_valid_email(email):
    """Vérifier format email"""
    if not email:
        return 0
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return 1 if re.match(pattern, email) else 0

conn.create_function("is_valid_email", 1, is_valid_email)

cursor.execute("""
    CREATE TABLE contacts (
        id INTEGER PRIMARY KEY,
        email TEXT
    )
""")

cursor.executemany("""
    INSERT INTO contacts (email) VALUES (?)
""", [("alice@email.com",), ("invalid.email",), ("bob@test.org",)])

cursor.execute("""
    SELECT email, is_valid_email(email) AS valid
    FROM contacts
""")

print("\n[OUTIL] Validation email personnalisée :")
for email, valid in cursor.fetchall():
    status = "[OK]" if valid else "[X]"
    print(f"  {status} {email}")

"""
2⃣ FONCTION AGRÉGATION (plusieurs valeurs -> une valeur)
"""

class MedianAggregate:
    """Calculer médiane (SQLite n'a pas de MEDIAN natif)"""
    
    def __init__(self):
        self.values = []
    
    def step(self, value):
        """Appelé pour chaque ligne"""
        if value is not None:
            self.values.append(value)
    
    def finalize(self):
        """Appelé à la fin pour retourner résultat"""
        if not self.values:
            return None
        
        sorted_values = sorted(self.values)
        n = len(sorted_values)
        
        if n % 2 == 0:
            # Pair : moyenne des 2 du milieu
            return (sorted_values[n//2 - 1] + sorted_values[n//2]) / 2
        else:
            # Impair : valeur du milieu
            return sorted_values[n//2]

# Enregistrer
conn.create_aggregate("median", 1, MedianAggregate)

# Tester
cursor.execute("""
    CREATE TABLE scores (
        id INTEGER PRIMARY KEY,
        score REAL
    )
""")

cursor.executemany("""
    INSERT INTO scores (score) VALUES (?)
""", [(85,), (90,), (78,), (92,), (88,)])

cursor.execute("""
    SELECT 
        AVG(score) AS moyenne,
        median(score) AS mediane
    FROM scores
""")

avg, median = cursor.fetchone()
print(f"\n[OUTIL] Agrégation personnalisée :")
print(f"  Moyenne : {avg}")
print(f"  Médiane : {median}")


# ----------------------------------------------------------------------------
# [SYNC] TYPES DE DONNÉES : Python <-> SQLite
# ----------------------------------------------------------------------------

"""
[IDEE] CONVERSIONS AUTOMATIQUES :

Python -> SQLite :
┌──────────────┬─────────────┐
│ Python       │ SQLite      │
├──────────────┼─────────────┤
│ None         │ NULL        │
│ int          │ INTEGER     │
│ float        │ REAL        │
│ str          │ TEXT        │
│ bytes        │ BLOB        │
└──────────────┴─────────────┘

SQLite -> Python :
┌──────────────┬─────────────┐
│ SQLite       │ Python      │
├──────────────┼─────────────┤
│ NULL         │ None        │
│ INTEGER      │ int         │
│ REAL         │ float       │
│ TEXT         │ str         │
│ BLOB         │ bytes       │
└──────────────┴─────────────┘


[IDEE] TYPES SPÉCIAUX : datetime
"""

from datetime import datetime, date

conn = sqlite3.connect(':memory:')
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE events (
        id INTEGER PRIMARY KEY,
        name TEXT,
        event_date TEXT
    )
""")

# Stocker datetime comme TEXT (ISO8601)
now = datetime.now()
cursor.execute("""
    INSERT INTO events (name, event_date)
    VALUES (?, ?)
""", ("Meeting", now.isoformat()))

# Récupérer et convertir
cursor.execute("SELECT event_date FROM events")
date_str = cursor.fetchone()[0]
date_obj = datetime.fromisoformat(date_str)

print(f"\n[CALENDRIER] Datetime :")
print(f"  Stocké : {date_str}")
print(f"  Python : {date_obj}")

"""
[IDEE] ADAPTATEURS CUSTOM :
"""

# Adapter : Python -> SQLite
def adapt_datetime(dt):
    return dt.isoformat()

# Converter : SQLite -> Python
def convert_datetime(s):
    return datetime.fromisoformat(s.decode())

# Enregistrer
sqlite3.register_adapter(datetime, adapt_datetime)
sqlite3.register_converter("timestamp", convert_datetime)

# Utiliser
conn = sqlite3.connect(':memory:', detect_types=sqlite3.PARSE_DECLTYPES)
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE logs (
        id INTEGER PRIMARY KEY,
        message TEXT,
        created_at timestamp
    )
""")

# Insérer datetime directement
cursor.execute("""
    INSERT INTO logs (message, created_at)
    VALUES (?, ?)
""", ("Test log", datetime.now()))

# Récupérer comme datetime
cursor.execute("SELECT created_at FROM logs")
retrieved = cursor.fetchone()[0]
print(f"\n[CALENDRIER] Adapter/Converter :")
print(f"  Type : {type(retrieved)}")
print(f"  Valeur : {retrieved}")


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 9 : Application TODO Complète
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer application TODO avec bonnes pratiques


FONCTIONNALITÉS :
- CRUD complet
- Validation
- Transactions
- Context managers
- Fonctions personnalisées
"""

print("\n" + "="*60)
print("EXERCICE : Application TODO")
print("="*60)

class TodoDatabase:
    """Gestionnaire base de données TODO"""
    
    def __init__(self, db_path=':memory:'):
        self.db_path = db_path
        self.conn = None
        self._setup()
    
    def _setup(self):
        """Initialiser BD"""
        self.connect()
        
        # Créer tables
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS todos (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                title TEXT NOT NULL CHECK(length(title) >= 3),
                description TEXT,
                priority INTEGER DEFAULT 1 CHECK(priority BETWEEN 1 AND 5),
                completed INTEGER DEFAULT 0 CHECK(completed IN (0, 1)),
                due_date TEXT,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                completed_at TEXT
            )
        """)
        
        # Créer fonction custom : is_overdue
        def is_overdue(due_date, completed):
            if completed == 1 or not due_date:
                return 0
            from datetime import datetime
            due = datetime.fromisoformat(due_date)
            return 1 if due < datetime.now() else 0
        
        self.conn.create_function("is_overdue", 2, is_overdue)
        
        self.conn.commit()
    
    def connect(self):
        """Ouvrir connexion"""
        if not self.conn:
            self.conn = sqlite3.connect(self.db_path)
            self.conn.row_factory = sqlite3.Row
        return self.conn
    
    def close(self):
        """Fermer connexion"""
        if self.conn:
            self.conn.close()
            self.conn = None
    
    def add_todo(self, title, description=None, priority=1, due_date=None):
        """Ajouter TODO"""
        cursor = self.conn.execute("""
            INSERT INTO todos (title, description, priority, due_date)
            VALUES (?, ?, ?, ?)
        """, (title, description, priority, due_date))
        
        self.conn.commit()
        return cursor.lastrowid
    
    def get_todo(self, todo_id):
        """Récupérer TODO par ID"""
        cursor = self.conn.execute("""
            SELECT * FROM todos WHERE id = ?
        """, (todo_id,))
        return cursor.fetchone()
    
    def get_all_todos(self, include_completed=True):
        """Récupérer tous les TODOs"""
        if include_completed:
            query = "SELECT * FROM todos ORDER BY completed, priority DESC, created_at"
        else:
            query = "SELECT * FROM todos WHERE completed = 0 ORDER BY priority DESC, created_at"
        
        cursor = self.conn.execute(query)
        return cursor.fetchall()
    
    def get_overdue_todos(self):
        """Récupérer TODOs en retard"""
        cursor = self.conn.execute("""
            SELECT * FROM todos
            WHERE is_overdue(due_date, completed) = 1
            ORDER BY due_date
        """)
        return cursor.fetchall()
    
    def update_todo(self, todo_id, **kwargs):
        """Mettre à jour TODO"""
        # Construire SET clause dynamiquement
        valid_fields = ['title', 'description', 'priority', 'due_date']
        updates = []
        values = []
        
        for field, value in kwargs.items():
            if field in valid_fields:
                updates.append(f"{field} = ?")
                values.append(value)
        
        if not updates:
            return 0
        
        values.append(todo_id)
        query = f"UPDATE todos SET {', '.join(updates)} WHERE id = ?"
        
        cursor = self.conn.execute(query, values)
        self.conn.commit()
        return cursor.rowcount
    
    def complete_todo(self, todo_id):
        """Marquer TODO comme complété"""
        cursor = self.conn.execute("""
            UPDATE todos
            SET completed = 1,
                completed_at = CURRENT_TIMESTAMP
            WHERE id = ? AND completed = 0
        """, (todo_id,))
        
        self.conn.commit()
        return cursor.rowcount
    
    def delete_todo(self, todo_id):
        """Supprimer TODO"""
        cursor = self.conn.execute("""
            DELETE FROM todos WHERE id = ?
        """, (todo_id,))
        
        self.conn.commit()
        return cursor.rowcount
    
    def get_statistics(self):
        """Statistiques"""
        cursor = self.conn.execute("""
            SELECT 
                COUNT(*) AS total,
                SUM(completed) AS completed,
                SUM(CASE WHEN completed = 0 THEN 1 ELSE 0 END) AS active,
                SUM(is_overdue(due_date, completed)) AS overdue
            FROM todos
        """)
        return dict(cursor.fetchone())

# Tester l'application
db = TodoDatabase()

# Ajouter TODOs
from datetime import datetime, timedelta

todo1 = db.add_todo(
    "Apprendre SQL",
    "Terminer le guide complet",
    priority=5,
    due_date=(datetime.now() + timedelta(days=7)).isoformat()
)

todo2 = db.add_todo(
    "Faire les courses",
    "Acheter fruits et légumes",
    priority=3,
    due_date=(datetime.now() - timedelta(days=1)).isoformat()  # En retard !
)

todo3 = db.add_todo(
    "Réviser Python",
    priority=4
)

print(f"\n[OK] {todo1}, {todo2}, {todo3} TODOs créés")

# Lister tous
print("\n[LISTE] Tous les TODOs :")
for todo in db.get_all_todos():
    status = "[OK]" if todo['completed'] else "[BLANC]"
    priority = "[ROUGE]" * todo['priority']
    print(f"  {status} [{todo['id']}] {todo['title']} {priority}")

# TODOs en retard
print("\n[ATTENTION] TODOs en retard :")
for todo in db.get_overdue_todos():
    print(f"  [ROUGE] [{todo['id']}] {todo['title']} (deadline: {todo['due_date']})")

# Compléter un TODO
db.complete_todo(1)
print(f"\n[OK] TODO #{1} complété")

# Statistiques
stats = db.get_statistics()
print("\n[GRAPHIQUE] Statistiques :")
print(f"  Total : {stats['total']}")
print(f"  Complétés : {stats['completed']}")
print(f"  Actifs : {stats['active']}")
print(f"  En retard : {stats['overdue']}")

# Nettoyer
db.close()
print("\n[OK] Application TODO complète !")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 13
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

BASES sqlite3 :
- connect(), cursor(), execute(), commit(), close()
- Modes connexion (fichier, :memory:)
- Types de curseur (tuple, Row)

SÉCURITÉ :
- Placeholders (?, :name)
- Éviter SQL injection
- JAMAIS f-strings avec user input

CRUD :
- INSERT : execute(), executemany(), lastrowid
- SELECT : fetchone(), fetchall(), fetchmany(), itération
- UPDATE : rowcount
- DELETE : rowcount

TRANSACTIONS :
- commit(), rollback()
- try/except avec rollback
- Isolation levels

CONTEXT MANAGERS :
- with sqlite3.connect()
- Context managers custom
- Classe wrapper Database

FONCTIONS CUSTOM :
- create_function() : scalaires
- create_aggregate() : agrégations
- Adaptateurs/Converters pour types

BEST PRACTICES :
[OK] Toujours placeholders
[OK] Context managers
[OK] Gestion erreurs
[OK] row_factory pour lisibilité
[OK] Fermer connexions


[CLE] PATTERN COMPLET :

```python
class Database:
    def __init__(self, path):
        self.path = path
    
    def __enter__(self):
        self.conn = sqlite3.connect(self.path)
        self.conn.row_factory = sqlite3.Row
        return self.conn
    
    def __exit__(self, *args):
        self.conn.commit()
        self.conn.close()

with Database('db.db') as conn:
    conn.execute("INSERT INTO users (name) VALUES (?)", ("Alice",))
```


-> PROCHAINE ÉTAPE : Chapitre 14 - PostgreSQL

Vous allez apprendre à utiliser PostgreSQL avec Python !
"""

print("\n[OK] Chapitre 13 terminé ! [BRAVO]\n")

# ============================================================================
# [LIVRE] BASES DE DONNÉES - PARTIE 4 (SUITE) : ORM ET MIGRATIONS
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 15 : SQLAlchemy ORM (Models, Sessions, Relationships)
# - Chapitre 16 : Migrations avec Alembic
#
# [TEMPS] TEMPS : ~6-8 heures
# [DOCS] PRÉREQUIS : Chapitre 13 complété
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 15 : SQLALCHEMY ORM
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce qu'un ORM et pourquoi l'utiliser
[OK] Configurer SQLAlchemy
[OK] Définir modèles (tables en Python)
[OK] CRUD avec l'ORM
[OK] Relations (One-to-Many, Many-to-Many)
[OK] Requêtes avancées
[OK] Sessions et transactions
[OK] Best practices SQLAlchemy
"""


# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QU'UN ORM ?
# ----------------------------------------------------------------------------

"""
[IDEE] ORM = Object-Relational Mapping

Mapper tables SQL <-> Classes Python


[DESIGN] ANALOGIE : Traducteur

Vous parlez Python ->  ORM traduit  -> Base de données comprend SQL
                    <-  ORM traduit  <-


SANS ORM (SQL brut) :
"""
# cursor.execute("""
#     INSERT INTO users (name, email) VALUES (?, ?)
# """, ("Alice", "alice@email.com"))
#
# cursor.execute("SELECT * FROM users WHERE name = ?", ("Alice",))
# row = cursor.fetchone()
# user = {"id": row[0], "name": row[1], "email": row[2]}

"""
AVEC ORM (SQLAlchemy) :
"""
# user = User(name="Alice", email="alice@email.com")
# session.add(user)
# session.commit()
#
# user = session.query(User).filter_by(name="Alice").first()
# print(user.name, user.email)

"""
[IDEE] AVANTAGES ORM :

[OK] Code Python idiomatique (pas de SQL brut)
[OK] Protection SQL injection automatique
[OK] Abstraction SGBD (même code pour SQLite, PostgreSQL, MySQL)
[OK] Migrations facilitées
[OK] Relations entre objets naturelles
[OK] Validation et business logic dans modèles

[X] INCONVÉNIENTS ORM :

[X] Courbe d'apprentissage
[X] Performance (requêtes générées parfois sous-optimales)
[X] Complexité cachée (N+1 queries)
[X] Overhead mémoire


[SCALES] QUAND UTILISER ORM ?

[OK] Applications CRUD standards
[OK] Modèles métier complexes
[OK] Équipe préfère Python à SQL
[OK] Changements SGBD possibles

[X] Requêtes analytiques complexes
[X] Performance ultra-critique
[X] Équipe expert SQL
"""


# ----------------------------------------------------------------------------
# [RAPIDE] INSTALLATION ET SETUP
# ----------------------------------------------------------------------------

"""
[IDEE] INSTALLATION :

```bash
pip install sqlalchemy --break-system-packages
```

Version : SQLAlchemy 2.0+ (nouvelle syntaxe)


[IDEE] IMPORTS DE BASE :
"""

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

"""
[IDEE] ARCHITECTURE SQLALCHEMY :

1. ENGINE : Connexion à la BD
2. BASE : Classe de base pour modèles
3. SESSION : Unité de travail (transactions)
4. MODELS : Classes Python = Tables SQL
"""


# ----------------------------------------------------------------------------
# [OUTIL] CONFIGURATION ENGINE ET BASE
# ----------------------------------------------------------------------------

"""
[IDEE] ENGINE : Point d'entrée vers la BD


SYNTAXE :
"""

# SQLite en mémoire
engine = create_engine('sqlite:///:memory:', echo=True)
# echo=True : Affiche SQL généré (debug)

"""
Autres connexions :

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

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

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


[IDEE] BASE : Classe de base pour tous les modèles
"""

Base = declarative_base()

"""
[IDEE] SESSION : Factory pour créer sessions
"""

Session = sessionmaker(bind=engine)

print("[OK] SQLAlchemy configuré (Engine, Base, Session)")


# ----------------------------------------------------------------------------
# [NOTE] DÉFINIR DES MODÈLES
# ----------------------------------------------------------------------------

"""
[IDEE] MODÈLE = Classe Python représentant une table


EXEMPLE 1 : Modèle simple
"""

class User(Base):
    __tablename__ = 'users'
    
    # Colonnes
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(100), unique=True, nullable=False)
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, server_default=func.now())
    
    def __repr__(self):
        """Représentation string"""
        return f"<User(id={self.id}, username='{self.username}')>"

"""
[IDEE] DÉCORTIQUONS :

__tablename__ = 'users'
    Nom de la table SQL

id = Column(Integer, primary_key=True)
    Clé primaire auto-increment

username = Column(String(50), unique=True, nullable=False)
    VARCHAR(50), UNIQUE, NOT NULL

created_at = Column(DateTime, server_default=func.now())
    TIMESTAMP DEFAULT CURRENT_TIMESTAMP

__repr__()
    Affichage lisible (optionnel mais utile)


[IDEE] TYPES DE COLONNES COURANTS :

┌──────────────┬────────────────────────┐
│ SQLAlchemy   │ SQL équivalent         │
├──────────────┼────────────────────────┤
│ Integer      │ INTEGER                │
│ String(n)    │ VARCHAR(n)             │
│ Text         │ TEXT                   │
│ Float        │ REAL/FLOAT             │
│ Boolean      │ BOOLEAN (INTEGER 0/1)  │
│ DateTime     │ TIMESTAMP              │
│ Date         │ DATE                   │
│ Time         │ TIME                   │
│ JSON         │ JSON (PostgreSQL)      │
└──────────────┴────────────────────────┘


[IDEE] CONTRAINTES COURANTES :

primary_key=True     : Clé primaire
unique=True          : UNIQUE
nullable=False       : NOT NULL
default=value        : DEFAULT value
server_default=      : DEFAULT calculé côté BD
index=True           : Créer index
"""


"""
EXEMPLE 2 : Modèle avec plus de contraintes
"""

class Product(Base):
    __tablename__ = 'products'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False, index=True)
    description = Column(Text)
    price = Column(Float, nullable=False)
    stock = Column(Integer, default=0)
    sku = Column(String(50), unique=True, nullable=False)
    is_available = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    
    def __repr__(self):
        return f"<Product(id={self.id}, name='{self.name}', price=${self.price})>"

"""
[IDEE] onupdate=datetime.utcnow

Mettre à jour automatiquement quand ligne modifiée


[IDEE] CRÉER LES TABLES :
"""

# Créer toutes les tables définies
Base.metadata.create_all(engine)

print("\n[OK] Tables créées : users, products")


# ----------------------------------------------------------------------------
# [SYNC] CRUD AVEC L'ORM
# ----------------------------------------------------------------------------

"""
[IDEE] SESSION : Unité de travail

Session = "panier" d'opérations avant commit
"""

session = Session()

"""
1⃣ CREATE (INSERT)
"""

# Créer instance
user1 = User(
    username='alice',
    email='alice@email.com'
)

# Ajouter à session
session.add(user1)

# Commit en BD
session.commit()

print(f"\n[OK] User créé : {user1} (ID: {user1.id})")

# Créer plusieurs
users = [
    User(username='bob', email='bob@email.com'),
    User(username='charlie', email='charlie@email.com'),
    User(username='diana', email='diana@email.com')
]

session.add_all(users)
session.commit()

print(f"[OK] {len(users)} users créés")

"""
[IDEE] WORKFLOW :

1. Créer objet Python
2. session.add() ou session.add_all()
3. session.commit()

[ATTENTION] ID assigné APRÈS commit !
"""

"""
2⃣ READ (SELECT)
"""

# Tous les users
all_users = session.query(User).all()
print(f"\n[LISTE] Tous les users ({len(all_users)}) :")
for user in all_users:
    print(f"  {user}")

# Premier user
first_user = session.query(User).first()
print(f"\n[LISTE] Premier user : {first_user}")

# Filtrer
alice = session.query(User).filter_by(username='alice').first()
print(f"\n[LISTE] User 'alice' : {alice.email}")

# Filter avec conditions
active_users = session.query(User).filter(User.is_active == True).all()
print(f"\n[LISTE] Users actifs : {len(active_users)}")

# Compter
count = session.query(User).count()
print(f"\n[LISTE] Nombre total : {count}")

# Get par ID
user_id_1 = session.get(User, 1)
print(f"\n[LISTE] User ID=1 : {user_id_1}")

"""
[IDEE] MÉTHODES QUERY COURANTES :

.all()              : Liste de tous les résultats
.first()            : Premier résultat (ou None)
.one()              : Un résultat (erreur si 0 ou 2+)
.one_or_none()      : Un résultat ou None
.count()            : Nombre de résultats
.filter()           : Filtrer avec conditions
.filter_by()        : Filtrer avec kwargs (égalité seulement)
.order_by()         : Trier
.limit()            : Limiter résultats
.offset()           : Skip résultats


[IDEE] DIFFÉRENCE filter() vs filter_by() :
"""

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

# filter : conditions complexes
user = session.query(User).filter(User.username == 'alice').first()
users = session.query(User).filter(User.id > 2).all()

"""
[IDEE] OPÉRATEURS DANS filter() :

==   : Égal
!=   : Différent
>    : Plus grand
<    : Plus petit
>=   : Plus grand ou égal
<=   : Plus petit ou égal
.in_()           : Dans liste
.like()          : Pattern matching
.ilike()         : Like insensible à casse
.between()       : Entre deux valeurs
.is_(None)       : IS NULL
.is_not(None)    : IS NOT NULL
"""

# Exemples
users = session.query(User).filter(User.id.in_([1, 2, 3])).all()
users = session.query(User).filter(User.username.like('a%')).all()
users = session.query(User).filter(User.email.is_not(None)).all()

"""
3⃣ UPDATE
"""

# Méthode 1 : Modifier objet
alice = session.query(User).filter_by(username='alice').first()
alice.email = 'alice.new@email.com'
session.commit()

print(f"\n[OK] Email d'Alice modifié : {alice.email}")

# Méthode 2 : Bulk update
session.query(User).filter(User.id > 2).update({
    User.is_active: False
})
session.commit()

print("[OK] Users ID>2 désactivés")

"""
4⃣ DELETE
"""

# Méthode 1 : Supprimer objet
user_to_delete = session.query(User).filter_by(username='diana').first()
if user_to_delete:
    session.delete(user_to_delete)
    session.commit()
    print(f"\n[OK] User 'diana' supprimé")

# Méthode 2 : Bulk delete
deleted_count = session.query(User).filter(User.is_active == False).delete()
session.commit()

print(f"[OK] {deleted_count} users inactifs supprimés")


# ----------------------------------------------------------------------------
# [LIEN] RELATIONS (RELATIONSHIPS)
# ----------------------------------------------------------------------------

"""
[IDEE] RELATIONS : Lier tables entre elles


1⃣ ONE-TO-MANY (1-N)
"""

# Recréer Base pour éviter conflits
Base = declarative_base()

class Author(Base):
    __tablename__ = 'authors'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    
    # Relation : un auteur a plusieurs livres
    books = relationship("Book", back_populates="author")
    
    def __repr__(self):
        return f"<Author(id={self.id}, name='{self.name}')>"

class Book(Base):
    __tablename__ = 'books'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    author_id = Column(Integer, ForeignKey('authors.id'))
    
    # Relation : un livre a un auteur
    author = relationship("Author", back_populates="books")
    
    def __repr__(self):
        return f"<Book(id={self.id}, title='{self.title}')>"

"""
[IDEE] DÉCORTIQUONS :

author_id = Column(Integer, ForeignKey('authors.id'))
    Clé étrangère vers authors.id

books = relationship("Book", back_populates="author")
    Relation ORM (pas une vraie colonne SQL)
    Permet : author.books (liste de livres)

author = relationship("Author", back_populates="books")
    Relation inverse
    Permet : book.author (objet auteur)

back_populates
    Synchronise les deux côtés de la relation


[IDEE] UTILISATION :
"""

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

# Créer auteur
author = Author(name='Victor Hugo')
session.add(author)
session.commit()

# Créer livres avec auteur
book1 = Book(title='Les Misérables', author=author)
book2 = Book(title='Notre-Dame de Paris', author=author)

session.add_all([book1, book2])
session.commit()

print(f"\n[DOCS] Auteur créé : {author}")
print(f"   Livres : {author.books}")

# Accéder auteur depuis livre
print(f"\n[GUIDE] Livre : {book1.title}")
print(f"   Auteur : {book1.author.name}")

"""
[IDEE] CASCADE DELETE :
"""

# Recréer avec cascade
Base = declarative_base()

class Author(Base):
    __tablename__ = 'authors'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    
    # cascade="all, delete-orphan" : Si auteur supprimé -> livres aussi
    books = relationship("Book", back_populates="author", 
                        cascade="all, delete-orphan")

class Book(Base):
    __tablename__ = 'books'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    author_id = Column(Integer, ForeignKey('authors.id', ondelete='CASCADE'))
    
    author = relationship("Author", back_populates="books")

"""
[IDEE] OPTIONS CASCADE :

"all"                : Toutes opérations
"delete"             : DELETE propagé
"delete-orphan"      : Supprimer si plus de parent
"merge"              : MERGE propagé
"save-update"        : Changements propagés
"all, delete-orphan" : Combinaison (courant)


2⃣ MANY-TO-MANY (N-N)
"""

from sqlalchemy import Table

Base = declarative_base()

# Table de liaison (association table)
book_categories = Table(
    'book_categories',
    Base.metadata,
    Column('book_id', Integer, ForeignKey('books.id')),
    Column('category_id', Integer, ForeignKey('categories.id'))
)

class Book(Base):
    __tablename__ = 'books'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    
    # Relation N-N via table de liaison
    categories = relationship(
        "Category",
        secondary=book_categories,
        back_populates="books"
    )
    
    def __repr__(self):
        return f"<Book('{self.title}')>"

class Category(Base):
    __tablename__ = 'categories'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(50))
    
    # Relation inverse
    books = relationship(
        "Book",
        secondary=book_categories,
        back_populates="categories"
    )
    
    def __repr__(self):
        return f"<Category('{self.name}')>"

"""
[IDEE] DÉCORTIQUONS :

book_categories = Table(...)
    Table de liaison (pas de classe)
    Juste colonnes FK

secondary=book_categories
    Indique quelle table utiliser pour liaison

back_populates
    Synchronisation bidirectionnelle


[IDEE] UTILISATION :
"""

Base.metadata.create_all(engine)
session = Session()

# Créer catégories
fiction = Category(name='Fiction')
scifi = Category(name='Science-Fiction')
classic = Category(name='Classique')

session.add_all([fiction, scifi, classic])

# Créer livre avec catégories
book = Book(title='Fondation')
book.categories.append(fiction)
book.categories.append(scifi)

session.add(book)
session.commit()

print(f"\n[GUIDE] Livre : {book.title}")
print(f"   Catégories : {[c.name for c in book.categories]}")

# Accès inverse
print(f"\n[DOSSIER] Catégorie : {fiction.name}")
print(f"   Livres : {[b.title for b in fiction.books]}")

"""
3⃣ ONE-TO-ONE (1-1)
"""

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50))
    
    # Relation 1-1
    profile = relationship("Profile", back_populates="user", uselist=False)

class Profile(Base):
    __tablename__ = 'profiles'
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'), unique=True)
    bio = Column(Text)
    avatar_url = Column(String(200))
    
    # Relation inverse
    user = relationship("User", back_populates="profile")

"""
[IDEE] uselist=False

Indique relation 1-1 (pas liste)
user.profile -> un objet Profile (pas liste)
"""


# ----------------------------------------------------------------------------
# [RECHERCHE] REQUÊTES AVANCÉES
# ----------------------------------------------------------------------------

"""
[IDEE] JOINS
"""

# Recréer modèles Author/Book simples
Base = declarative_base()

class Author(Base):
    __tablename__ = 'authors'
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    books = relationship("Book", back_populates="author")

class Book(Base):
    __tablename__ = 'books'
    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    author_id = Column(Integer, ForeignKey('authors.id'))
    author = relationship("Author", back_populates="books")

Base.metadata.create_all(engine)
session = Session()

# Données test
author1 = Author(name='Victor Hugo')
author2 = Author(name='Molière')

book1 = Book(title='Les Misérables', author=author1)
book2 = Book(title='Le Tartuffe', author=author2)

session.add_all([author1, author2, book1, book2])
session.commit()

# JOIN automatique via relationship
books_with_authors = session.query(Book).all()
for book in books_with_authors:
    print(f"\n[GUIDE] {book.title}")
    print(f"   Auteur : {book.author.name}")  # Charge auteur automatiquement

"""
[ATTENTION] N+1 QUERY PROBLEM :

Chaque book.author -> requête SQL séparée
1 requête pour books + N requêtes pour auteurs = N+1 requêtes !


[OK] SOLUTION : joinedload (eager loading)
"""

from sqlalchemy.orm import joinedload

books = session.query(Book).options(joinedload(Book.author)).all()
# Une seule requête avec JOIN

for book in books:
    print(f"[GUIDE] {book.title} - {book.author.name}")

"""
[IDEE] STRATÉGIES DE CHARGEMENT :

joinedload    : LEFT OUTER JOIN (une requête)
selectinload  : Requête séparée optimisée
lazyload      : Charger à la demande (défaut)
noload        : Ne pas charger


[IDEE] AGRÉGATIONS
"""

from sqlalchemy import func

# Compter livres par auteur
results = session.query(
    Author.name,
    func.count(Book.id).label('book_count')
).join(Book).group_by(Author.name).all()

print("\n[GRAPHIQUE] Livres par auteur :")
for name, count in results:
    print(f"  {name} : {count} livre(s)")

"""
[IDEE] ORDER BY, LIMIT, OFFSET
"""

# Trier
books = session.query(Book).order_by(Book.title).all()

# Trier descendant
from sqlalchemy import desc
books = session.query(Book).order_by(desc(Book.title)).all()

# Pagination
page = 1
per_page = 10
books = session.query(Book).limit(per_page).offset((page - 1) * per_page).all()

"""
[IDEE] SOUS-REQUÊTES
"""

from sqlalchemy import select

# Sous-requête
subq = session.query(func.avg(Book.id)).scalar_subquery()
books_above_avg = session.query(Book).filter(Book.id > subq).all()


# ----------------------------------------------------------------------------
# [SYNC] SESSIONS ET TRANSACTIONS
# ----------------------------------------------------------------------------

"""
[IDEE] SESSION : Unité de travail


LIFECYCLE SESSION :

1. Créer session
2. Opérations (add, query, etc.)
3. Commit ou Rollback
4. Fermer session
"""

session = Session()

try:
    # Opérations
    user = User(username='test')
    session.add(user)
    
    # Commit
    session.commit()
    
except Exception as e:
    # Rollback en cas d'erreur
    session.rollback()
    print(f"Erreur : {e}")
    
finally:
    # Toujours fermer
    session.close()

"""
[IDEE] CONTEXT MANAGER :
"""

# Session auto-close
from contextlib import contextmanager

@contextmanager
def get_session():
    session = Session()
    try:
        yield session
        session.commit()
    except:
        session.rollback()
        raise
    finally:
        session.close()

# Utilisation
with get_session() as session:
    user = User(username='context_user')
    session.add(user)
    # Auto-commit et close

"""
[IDEE] SCOPED SESSION (thread-safe)
"""

from sqlalchemy.orm import scoped_session

# Session par thread
session_factory = sessionmaker(bind=engine)
ScopedSession = scoped_session(session_factory)

# Utilisation
session = ScopedSession()
# ... opérations
ScopedSession.remove()  # Cleanup


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 10 : Blog avec SQLAlchemy
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer application Blog complète


MODÈLES :
- User (auteurs)
- Post (articles)
- Comment (commentaires)
- Tag (étiquettes)

RELATIONS :
- User 1-N Post
- Post 1-N Comment
- User 1-N Comment
- Post N-N Tag
"""

print("\n" + "="*60)
print("EXERCICE : Application Blog")
print("="*60)

from sqlalchemy import Table, Column, Integer, String, Text, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import declarative_base, relationship, Session
from sqlalchemy.sql import func
from datetime import datetime

Base = declarative_base()

# Table de liaison Post <-> Tag
post_tags = Table(
    'post_tags',
    Base.metadata,
    Column('post_id', Integer, ForeignKey('posts.id')),
    Column('tag_id', Integer, ForeignKey('tags.id'))
)

class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(100), unique=True, nullable=False)
    password_hash = Column(String(128), nullable=False)
    is_admin = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # Relations
    posts = relationship("Post", back_populates="author", cascade="all, delete-orphan")
    comments = relationship("Comment", back_populates="author", cascade="all, delete-orphan")
    
    def __repr__(self):
        return f"<User(username='{self.username}')>"

class Post(Base):
    __tablename__ = 'posts'
    
    id = Column(Integer, primary_key=True)
    title = Column(String(200), nullable=False)
    slug = Column(String(200), unique=True, nullable=False)
    content = Column(Text, nullable=False)
    excerpt = Column(String(300))
    published = Column(Boolean, default=False)
    author_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
    published_at = Column(DateTime)
    
    # Relations
    author = relationship("User", back_populates="posts")
    comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan")
    tags = relationship("Tag", secondary=post_tags, back_populates="posts")
    
    def __repr__(self):
        return f"<Post(title='{self.title}')>"
    
    @property
    def comment_count(self):
        return len(self.comments)

class Comment(Base):
    __tablename__ = 'comments'
    
    id = Column(Integer, primary_key=True)
    content = Column(Text, nullable=False)
    author_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    post_id = Column(Integer, ForeignKey('posts.id'), nullable=False)
    approved = Column(Boolean, default=False)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # Relations
    author = relationship("User", back_populates="comments")
    post = relationship("Post", back_populates="comments")
    
    def __repr__(self):
        return f"<Comment(id={self.id}, post='{self.post.title}')>"

class Tag(Base):
    __tablename__ = 'tags'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True, nullable=False)
    slug = Column(String(50), unique=True, nullable=False)
    
    # Relations
    posts = relationship("Post", secondary=post_tags, back_populates="tags")
    
    def __repr__(self):
        return f"<Tag(name='{self.name}')>"

# Créer tables
engine = create_engine('sqlite:///:memory:', echo=False)
Base.metadata.create_all(engine)
session = Session(engine)

# Créer données
user1 = User(username='alice', email='alice@blog.com', password_hash='hashed')
user2 = User(username='bob', email='bob@blog.com', password_hash='hashed')

tag_python = Tag(name='Python', slug='python')
tag_sql = Tag(name='SQL', slug='sql')
tag_web = Tag(name='Web', slug='web')

post1 = Post(
    title='Introduction à SQLAlchemy',
    slug='intro-sqlalchemy',
    content='SQLAlchemy est un ORM puissant...',
    excerpt='Découvrez SQLAlchemy',
    published=True,
    author=user1,
    published_at=datetime.utcnow()
)
post1.tags.extend([tag_python, tag_sql])

post2 = Post(
    title='Bases de données en Python',
    slug='bases-donnees-python',
    content='Guide complet des BDD...',
    published=True,
    author=user1
)
post2.tags.extend([tag_python, tag_sql])

comment1 = Comment(
    content='Super article !',
    author=user2,
    post=post1,
    approved=True
)

session.add_all([user1, user2, tag_python, tag_sql, tag_web, post1, post2, comment1])
session.commit()

print("\n[OK] Blog créé avec données")

# Requêtes
print("\n[NEWSPAPER] Posts publiés :")
posts = session.query(Post).filter(Post.published == True).all()
for post in posts:
    print(f"\n  [NOTE] {post.title}")
    print(f"     Par : {post.author.username}")
    print(f"     Tags : {', '.join([t.name for t in post.tags])}")
    print(f"     Commentaires : {post.comment_count}")

# Posts par tag
print("\n[LABEL] Posts avec tag 'Python' :")
python_posts = session.query(Post).join(Post.tags).filter(Tag.name == 'Python').all()
for post in python_posts:
    print(f"  - {post.title}")

# Statistiques auteur
from sqlalchemy import func

print("\n[GRAPHIQUE] Statistiques par auteur :")
stats = session.query(
    User.username,
    func.count(Post.id).label('post_count')
).join(User.posts).group_by(User.id).all()

for username, count in stats:
    print(f"  {username} : {count} post(s)")

session.close()
print("\n[OK] Exercice Blog terminé !")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 15
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

ORM BASICS :
- create_engine() : Connexion
- Base = declarative_base() : Classe de base
- sessionmaker() : Factory sessions
- Column() : Définir colonnes
- __tablename__ : Nom table

MODÈLES :
- Hériter de Base
- Définir colonnes avec types
- Contraintes (nullable, unique, default)
- __repr__() pour affichage

CRUD :
- CREATE : session.add(), session.commit()
- READ : session.query().filter().all()
- UPDATE : modifier objet, commit
- DELETE : session.delete(), commit

RELATIONS :
- 1-N : ForeignKey + relationship()
- N-N : Table liaison + secondary
- 1-1 : ForeignKey unique + uselist=False
- back_populates : Bidirectionnel
- cascade : Propagation

REQUÊTES :
- filter(), filter_by()
- order_by(), limit(), offset()
- joinedload() : Éviter N+1
- Agrégations avec func

SESSIONS :
- Context managers
- commit(), rollback()
- Scoped sessions


[CLE] PATTERN COMPLET :

```python
# Définir modèles
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    name = Column(String(50))

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

# Session
session = Session(engine)

# CRUD
user = User(name='Alice')
session.add(user)
session.commit()

users = session.query(User).filter_by(name='Alice').all()

session.close()
```


-> PROCHAINE ÉTAPE : Chapitre 16 - Migrations

Vous allez apprendre à gérer évolution du schéma !
"""

print("\n[OK] Chapitre 15 terminé ! [BRAVO]\n")

# ============================================================================
# [LIVRE] BASES DE DONNÉES - PARTIE 4 (FIN) : MIGRATIONS AVEC ALEMBIC
# ============================================================================
#
# [OBJECTIF] CE CHAPITRE COUVRE :
# - Chapitre 16 : Migrations avec Alembic
#   • Qu'est-ce qu'une migration
#   • Setup Alembic
#   • Créer et appliquer migrations
#   • Migrations automatiques vs manuelles
#   • Rollback et historique
#   • Best practices migrations
#
# [TEMPS] TEMPS : ~3-4 heures
# [DOCS] PRÉREQUIS : Chapitre 15 (SQLAlchemy) complété
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 16 : MIGRATIONS AVEC ALEMBIC
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Qu'est-ce qu'une migration de base de données
[OK] Pourquoi utiliser un outil de migration
[OK] Configurer Alembic
[OK] Créer migrations (auto et manuelles)
[OK] Appliquer et annuler migrations
[OK] Gérer historique de versions
[OK] Migrations en équipe
[OK] Best practices et pièges
"""


# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QU'UNE MIGRATION ?
# ----------------------------------------------------------------------------

"""
[IDEE] MIGRATION = Changement contrôlé du schéma de base de données


[DESIGN] ANALOGIE : Git pour votre base de données

Git pour code :
- Commits : Historique des changements
- Branches : Versions parallèles
- Revert : Annuler changements

Migrations pour BD :
- Versions : Historique du schéma
- Migrations : Changements incrémentaux
- Downgrade : Annuler changements


[ROUGE] PROBLÈME SANS MIGRATIONS :

Équipe de 3 développeurs :

Alice (local) :
CREATE TABLE users (
    id INTEGER,
    name TEXT
);

Bob (local) :
CREATE TABLE users (
    id INTEGER,
    name TEXT,
    email TEXT  <- Ajouté
);

Production :
CREATE TABLE users (
    id INTEGER,
    name TEXT
);

[X] Incohérence !
[X] Comment synchroniser ?
[X] Comment déployer changements Bob ?


[OK] SOLUTION AVEC MIGRATIONS :

1. Bob crée migration :
   "Ajouter colonne email à users"

2. Bob commit migration (fichier Python)

3. Alice pull et applique migration
   -> Sa BD a maintenant email

4. Production applique migration
   -> Production à jour

[OK] Cohérence garantie
[OK] Historique traçable
[OK] Rollback possible


[IDEE] TYPES DE CHANGEMENTS :

Migrations pour :
[OK] Ajouter/supprimer tables
[OK] Ajouter/supprimer colonnes
[OK] Modifier types de colonnes
[OK] Ajouter/supprimer index
[OK] Ajouter/supprimer contraintes
[OK] Migrations de données


[CLE] POURQUOI ALEMBIC ?

Alternatives : Django migrations, Flask-Migrate
Alembic : Standalone, intégration SQLAlchemy

[OK] Généré automatiquement depuis modèles
[OK] Support toutes opérations SQL
[OK] Migrations réversibles (upgrade/downgrade)
[OK] Gestion branches et merges
[OK] Indépendant du framework
"""


# ----------------------------------------------------------------------------
# [RAPIDE] INSTALLATION ET SETUP
# ----------------------------------------------------------------------------

"""
[IDEE] INSTALLATION :

```bash
pip install alembic --break-system-packages
```


[IDEE] INITIALISATION PROJET :

Structure recommandée :

myproject/
├── alembic/              <- Dossier généré par Alembic
│   ├── versions/         <- Fichiers de migration
│   ├── env.py            <- Configuration environnement
│   └── script.py.mako    <- Template migrations
├── alembic.ini           <- Config Alembic
├── models.py             <- Modèles SQLAlchemy
└── main.py


[IDEE] COMMANDES ALEMBIC DE BASE :

```bash
# Initialiser Alembic
alembic init alembic

# Créer migration automatique
alembic revision --autogenerate -m "Description"

# Créer migration manuelle
alembic revision -m "Description"

# Appliquer migrations
alembic upgrade head

# Voir historique
alembic history

# Annuler dernière migration
alembic downgrade -1

# Voir état actuel
alembic current
```


[IDEE] SIMULATION (sans vraie install) :
"""

print("="*60)
print("CHAPITRE 16 : Migrations avec Alembic")
print("="*60)

"""
Comme nous sommes dans un environnement de démonstration,
voici comment Alembic fonctionne en pratique :


ÉTAPE 1 : Setup initial
"""

# alembic init alembic

"""
Crée :
- alembic/
  - versions/ (vide au début)
  - env.py
  - script.py.mako
- alembic.ini


ÉTAPE 2 : Configuration alembic.ini
"""

ALEMBIC_INI_CONTENT = """
# alembic.ini

[alembic]
# Chemin vers dossier migrations
script_location = alembic

# URL base de données (à configurer)
sqlalchemy.url = sqlite:///myapp.db

# Template pour fichiers migration
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev)s_%%(slug)s

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic
"""

print("\n[NOTE] Fichier alembic.ini :")
print("  - sqlalchemy.url : Connexion BD")
print("  - script_location : Dossier migrations")

"""
[IDEE] MIEUX : URL dans variables d'environnement
"""

# alembic.ini
# sqlalchemy.url = 

# env.py
"""
from os import getenv

config.set_main_option(
    'sqlalchemy.url',
    getenv('DATABASE_URL', 'sqlite:///default.db')
)
"""


# ----------------------------------------------------------------------------
# [NOTE] MODÈLES SQLALCHEMY (Base)
# ----------------------------------------------------------------------------

"""
[IDEE] Définissons modèles pour nos exemples
"""

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

Base = declarative_base()

# Version 1 des modèles (initiale)
class User(Base):
    __tablename__ = 'users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), nullable=False, unique=True)
    email = Column(String(100), nullable=False, unique=True)
    created_at = Column(DateTime, default=datetime.utcnow)

class Product(Base):
    __tablename__ = 'products'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100), nullable=False)
    price = Column(Float, nullable=False)
    stock = Column(Integer, default=0)

print("\n[OK] Modèles définis : User, Product")


# ----------------------------------------------------------------------------
# [SYNC] CONFIGURATION env.py
# ----------------------------------------------------------------------------

"""
[IDEE] env.py : Lien entre Alembic et modèles SQLAlchemy


CONTENU TYPIQUE :
"""

ENV_PY_CONTENT = """
# alembic/env.py

from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig

# Import Base pour autogenerate
from models import Base

# Config Alembic
config = context.config

# Setup logging
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# Métadonnées pour autogenerate
target_metadata = Base.metadata


def run_migrations_online():
    '''Migrations en mode online (BD connectée)'''
    
    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()
"""

print("\n[NOTE] Fichier env.py configuré :")
print("  - Import Base depuis models.py")
print("  - target_metadata = Base.metadata")


# ----------------------------------------------------------------------------
# [OBJECTIF] MIGRATION INITIALE
# ----------------------------------------------------------------------------

"""
[IDEE] CRÉER PREMIÈRE MIGRATION (schéma initial)


COMMANDE :
"""
# alembic revision --autogenerate -m "Initial schema"

"""
Génère fichier :
alembic/versions/20240119_1420_abc123_initial_schema.py


CONTENU TYPIQUE :
"""

INITIAL_MIGRATION = '''
"""Initial schema

Revision ID: abc123def456
Revises: 
Create Date: 2024-01-19 14:20:00

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers
revision = 'abc123def456'
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
    """Appliquer migration (forward)"""
    
    # Créer table users
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('username', sa.String(50), nullable=False),
        sa.Column('email', sa.String(100), nullable=False),
        sa.Column('created_at', sa.DateTime(), nullable=True),
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('username'),
        sa.UniqueConstraint('email')
    )
    
    # Créer table products
    op.create_table(
        'products',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.String(100), nullable=False),
        sa.Column('price', sa.Float(), nullable=False),
        sa.Column('stock', sa.Integer(), nullable=True),
        sa.PrimaryKeyConstraint('id')
    )
    
    # Créer index
    op.create_index('ix_users_email', 'users', ['email'])


def downgrade():
    """Annuler migration (rollback)"""
    
    op.drop_index('ix_users_email', 'users')
    op.drop_table('products')
    op.drop_table('users')
'''

print("\n[FICHIER] Migration initiale générée :")
print("  - Fichier : alembic/versions/abc123_initial_schema.py")
print("  - upgrade() : Créer tables")
print("  - downgrade() : Supprimer tables")

"""
[IDEE] DÉCORTIQUONS :

revision = 'abc123def456'
    ID unique de cette migration

down_revision = None
    Migration précédente (None = première)

upgrade()
    Appliquer changements (forward)

downgrade()
    Annuler changements (backward)


[IDEE] APPLIQUER MIGRATION :
"""
# alembic upgrade head

"""
Sortie :
INFO  [alembic.runtime.migration] Running upgrade  -> abc123, Initial schema

[OK] Tables créées !


[IDEE] VÉRIFIER :
"""
# alembic current

"""
Sortie :
abc123def456 (head)
"""

print("\n[OK] Migration initiale appliquée (simulation)")


# ----------------------------------------------------------------------------
# [OUTIL] AJOUTER COLONNE (Migration 2)
# ----------------------------------------------------------------------------

"""
[IDEE] SCÉNARIO : Ajouter colonne 'bio' à User


ÉTAPE 1 : Modifier modèle
"""

class UserV2(Base):
    __tablename__ = 'users_v2'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), nullable=False, unique=True)
    email = Column(String(100), nullable=False, unique=True)
    bio = Column(Text)  # <- NOUVEAU
    created_at = Column(DateTime, default=datetime.utcnow)

"""
ÉTAPE 2 : Générer migration
"""
# alembic revision --autogenerate -m "Add bio to users"

"""
Fichier généré :
"""

ADD_BIO_MIGRATION = '''
"""Add bio to users

Revision ID: def456ghi789
Revises: abc123def456
Create Date: 2024-01-19 15:30:00

"""
from alembic import op
import sqlalchemy as sa

revision = 'def456ghi789'
down_revision = 'abc123def456'  # <- Pointe vers migration précédente


def upgrade():
    """Ajouter colonne bio"""
    
    op.add_column(
        'users',
        sa.Column('bio', sa.Text(), nullable=True)
    )


def downgrade():
    """Supprimer colonne bio"""
    
    op.drop_column('users', 'bio')
'''

print("\n[FICHIER] Migration 2 générée (ajouter bio) :")
print("  - down_revision : abc123def456")
print("  - upgrade() : op.add_column()")
print("  - downgrade() : op.drop_column()")

"""
ÉTAPE 3 : Appliquer
"""
# alembic upgrade head

print("[OK] Colonne 'bio' ajoutée (simulation)")


# ----------------------------------------------------------------------------
# [LIEN] AJOUTER TABLE LIÉE (Migration 3)
# ----------------------------------------------------------------------------

"""
[IDEE] SCÉNARIO : Ajouter table 'orders' liée à 'users'


ÉTAPE 1 : Ajouter modèle
"""

class Order(Base):
    __tablename__ = 'orders'
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
    total = Column(Float, nullable=False)
    status = Column(String(20), default='pending')
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # Relation
    user = relationship("User", backref="orders")

"""
ÉTAPE 2 : Générer migration
"""
# alembic revision --autogenerate -m "Add orders table"

"""
Fichier généré :
"""

ADD_ORDERS_MIGRATION = '''
"""Add orders table

Revision ID: ghi789jkl012
Revises: def456ghi789
Create Date: 2024-01-19 16:00:00

"""
from alembic import op
import sqlalchemy as sa

revision = 'ghi789jkl012'
down_revision = 'def456ghi789'


def upgrade():
    """Créer table orders"""
    
    op.create_table(
        'orders',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('user_id', sa.Integer(), nullable=False),
        sa.Column('total', sa.Float(), nullable=False),
        sa.Column('status', sa.String(20), nullable=True),
        sa.Column('created_at', sa.DateTime(), nullable=True),
        sa.PrimaryKeyConstraint('id'),
        sa.ForeignKeyConstraint(['user_id'], ['users.id'])
    )
    
    # Index pour performance
    op.create_index('ix_orders_user_id', 'orders', ['user_id'])


def downgrade():
    """Supprimer table orders"""
    
    op.drop_index('ix_orders_user_id', 'orders')
    op.drop_table('orders')
'''

print("\n[FICHIER] Migration 3 générée (ajouter orders) :")
print("  - Créer table avec FK vers users")
print("  - Créer index sur user_id")

"""
APPLIQUER :
"""
# alembic upgrade head

print("[OK] Table 'orders' créée (simulation)")


# ----------------------------------------------------------------------------
# [GRAPHIQUE] HISTORIQUE DES MIGRATIONS
# ----------------------------------------------------------------------------

"""
[IDEE] VOIR HISTORIQUE :
"""
# alembic history

"""
Sortie :

abc123def456 -> def456ghi789 (head), Add bio to users
<base> -> abc123def456, Initial schema


Lecture :
- <base> : État vide
- abc123 : Migration initiale
- def456 : Ajout bio
- ghi789 : Ajout orders (HEAD = dernière)


[IDEE] ÉTAT ACTUEL :
"""
# alembic current

"""
Sortie :
ghi789jkl012 (head)
"""

print("\n[GRAPHIQUE] Historique migrations :")
print("  <base> -> abc123 (Initial)")
print("  abc123 -> def456 (Add bio)")
print("  def456 -> ghi789 (Add orders) <- HEAD")


# ----------------------------------------------------------------------------
# [BLACK_LEFT-POINTING_DOUBLE_TRIANGLE_WITH_VERTICAL_BAR] ROLLBACK (Downgrade)
# ----------------------------------------------------------------------------

"""
[IDEE] ANNULER MIGRATIONS


SCÉNARIOS :

1. Annuler dernière migration
"""
# alembic downgrade -1

"""
Exécute downgrade() de ghi789jkl012
-> Supprime table orders
-> État : def456ghi789


2. Revenir à migration spécifique
"""
# alembic downgrade abc123def456

"""
Exécute tous les downgrade() entre HEAD et abc123
-> État : Migration initiale


3. Tout annuler
"""
# alembic downgrade base

"""
Exécute tous les downgrade()
-> État : BD vide


4. Réappliquer après rollback
"""
# alembic upgrade head

"""
Ré-exécute tous les upgrade() manquants


[IDEE] EXEMPLE WORKFLOW :
"""

print("\n[BLACK_LEFT-POINTING_DOUBLE_TRIANGLE_WITH_VERTICAL_BAR] Simulation Rollback :")
print("\n1. État actuel : ghi789 (orders exists)")

# alembic downgrade -1
print("2. Après downgrade -1 : def456 (orders supprimée)")

# alembic downgrade abc123
print("3. Après downgrade abc123 : abc123 (bio supprimée)")

# alembic upgrade head
print("4. Après upgrade head : ghi789 (tout réappliqué)")


# ----------------------------------------------------------------------------
# [EDIT] MIGRATIONS MANUELLES
# ----------------------------------------------------------------------------

"""
[IDEE] QUAND ÉCRIRE MANUELLEMENT ?

--autogenerate détecte :
[OK] Ajout/suppression tables
[OK] Ajout/suppression colonnes
[OK] Modification types (parfois)

--autogenerate NE détecte PAS :
[X] Renommer table/colonne
[X] Migrations de données
[X] Changements complexes


[IDEE] CRÉER MIGRATION MANUELLE :
"""
# alembic revision -m "Rename username to display_name"

"""
Fichier généré (vide) :
"""

MANUAL_MIGRATION = '''
"""Rename username to display_name

Revision ID: jkl012mno345
Revises: ghi789jkl012
Create Date: 2024-01-19 17:00:00

"""
from alembic import op
import sqlalchemy as sa

revision = 'jkl012mno345'
down_revision = 'ghi789jkl012'


def upgrade():
    """Renommer colonne (MANUEL)"""
    
    # SQLite ne supporte pas ALTER COLUMN RENAME directement
    # Solution : Créer nouvelle table, copier, supprimer ancienne
    
    # Méthode alternative : ALTER TABLE (PostgreSQL, MySQL)
    # op.alter_column('users', 'username', new_column_name='display_name')
    
    # Pour SQLite :
    with op.batch_alter_table('users') as batch_op:
        batch_op.alter_column(
            'username',
            new_column_name='display_name'
        )


def downgrade():
    """Annuler renommage"""
    
    with op.batch_alter_table('users') as batch_op:
        batch_op.alter_column(
            'display_name',
            new_column_name='username'
        )
'''

print("\n[EDIT] Migration manuelle (rename column) :")
print("  - Utilise op.batch_alter_table() pour SQLite")
print("  - op.alter_column() pour autres SGBD")

"""
[IDEE] MIGRATION DE DONNÉES :
"""

DATA_MIGRATION = '''
"""Populate default bio for existing users

Revision ID: mno345pqr678
Revises: jkl012mno345
Create Date: 2024-01-19 18:00:00

"""
from alembic import op
import sqlalchemy as sa

revision = 'mno345pqr678'
down_revision = 'jkl012mno345'


def upgrade():
    """Remplir bio par défaut"""
    
    # Créer connexion
    connection = op.get_bind()
    
    # Exécuter SQL brut
    connection.execute(
        sa.text("UPDATE users SET bio = 'No bio yet' WHERE bio IS NULL")
    )


def downgrade():
    """Annuler (optionnel pour migrations données)"""
    pass  # Généralement pas de rollback pour données
'''

print("\n[EDIT] Migration de données :")
print("  - UPDATE pour données existantes")
print("  - Downgrade souvent vide (pas de rollback)")


# ----------------------------------------------------------------------------
# [HERB] BRANCHES ET MERGES
# ----------------------------------------------------------------------------

"""
[IDEE] BRANCHES : Développement parallèle


SCÉNARIO :

Alice travaille sur feature A (branch feature-a)
Bob travaille sur feature B (branch feature-b)


Alice crée migration :
"""
# git checkout feature-a
# alembic revision -m "Add feature A tables"
# Génère : abc123_feature_a

"""
Bob crée migration (en parallèle) :
"""
# git checkout feature-b
# alembic revision -m "Add feature B tables"
# Génère : def456_feature_b

"""
PROBLÈME : Deux migrations avec même down_revision !

abc123_feature_a:
  down_revision = 'ghi789jkl012'

def456_feature_b:
  down_revision = 'ghi789jkl012'

-> Conflit lors du merge !


SOLUTION : Merge migrations
"""
# git checkout main
# git merge feature-a
# git merge feature-b  # Conflit !

# alembic merge -m "Merge feature A and B" abc123 def456

"""
Génère migration de merge :
"""

MERGE_MIGRATION = '''
"""Merge feature A and B

Revision ID: pqr678stu901
Revises: abc123_feature_a, def456_feature_b
Create Date: 2024-01-19 19:00:00

"""
from alembic import op

revision = 'pqr678stu901'
down_revision = ('abc123_feature_a', 'def456_feature_b')  # Tuple !


def upgrade():
    """Merge n'a généralement pas d'opérations"""
    pass


def downgrade():
    """Downgrade vers les deux branches"""
    pass
'''

print("\n[HERB] Merge de branches :")
print("  - down_revision = tuple de 2 révisions")
print("  - Permet Alembic de gérer graphe")


# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 11 : Évolution Schéma E-Commerce
# ----------------------------------------------------------------------------

"""
OBJECTIF : Série de migrations pour évolution application


SCÉNARIO :

Version 1.0 : Schéma initial
Version 1.1 : Ajouter reviews
Version 1.2 : Ajouter wishlist
Version 1.3 : Migrer données
"""

print("\n" + "="*60)
print("EXERCICE : Évolution schéma e-commerce")
print("="*60)

"""
MIGRATION 1 : Schéma initial
"""

ECOMMERCE_V1 = '''
"""E-commerce initial schema

Revision ID: v1_0_init
Revises: 
Create Date: 2024-01-19

"""
from alembic import op
import sqlalchemy as sa

revision = 'v1_0_init'
down_revision = None


def upgrade():
    # Users
    op.create_table(
        'users',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('email', sa.String(100), unique=True, nullable=False),
        sa.Column('password_hash', sa.String(128), nullable=False),
        sa.Column('created_at', sa.DateTime())
    )
    
    # Products
    op.create_table(
        'products',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('name', sa.String(200), nullable=False),
        sa.Column('price', sa.Float(), nullable=False),
        sa.Column('description', sa.Text())
    )
    
    # Orders
    op.create_table(
        'orders',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id')),
        sa.Column('total', sa.Float()),
        sa.Column('status', sa.String(20)),
        sa.Column('created_at', sa.DateTime())
    )


def downgrade():
    op.drop_table('orders')
    op.drop_table('products')
    op.drop_table('users')
'''

print("\n[FICHIER] Migration V1.0 :")
print("  - Tables : users, products, orders")

"""
MIGRATION 2 : Ajouter reviews (V1.1)
"""

ECOMMERCE_V1_1 = '''
"""Add product reviews

Revision ID: v1_1_reviews
Revises: v1_0_init
Create Date: 2024-01-20

"""
from alembic import op
import sqlalchemy as sa

revision = 'v1_1_reviews'
down_revision = 'v1_0_init'


def upgrade():
    # Table reviews
    op.create_table(
        'reviews',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('product_id', sa.Integer(), sa.ForeignKey('products.id')),
        sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id')),
        sa.Column('rating', sa.Integer(), nullable=False),
        sa.Column('comment', sa.Text()),
        sa.Column('created_at', sa.DateTime())
    )
    
    # Contrainte : rating entre 1 et 5
    op.create_check_constraint(
        'check_rating',
        'reviews',
        'rating >= 1 AND rating <= 5'
    )
    
    # Index pour performance
    op.create_index('ix_reviews_product', 'reviews', ['product_id'])


def downgrade():
    op.drop_index('ix_reviews_product', 'reviews')
    op.drop_constraint('check_rating', 'reviews')
    op.drop_table('reviews')
'''

print("\n[FICHIER] Migration V1.1 :")
print("  - Table reviews avec contrainte CHECK")

"""
MIGRATION 3 : Ajouter wishlist (V1.2)
"""

ECOMMERCE_V1_2 = '''
"""Add wishlist feature

Revision ID: v1_2_wishlist
Revises: v1_1_reviews
Create Date: 2024-01-21

"""
from alembic import op
import sqlalchemy as sa

revision = 'v1_2_wishlist'
down_revision = 'v1_1_reviews'


def upgrade():
    # Table wishlist (N-N users <-> products)
    op.create_table(
        'wishlist',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id')),
        sa.Column('product_id', sa.Integer(), sa.ForeignKey('products.id')),
        sa.Column('added_at', sa.DateTime())
    )
    
    # Contrainte unique : un produit une fois par user
    op.create_unique_constraint(
        'unique_user_product',
        'wishlist',
        ['user_id', 'product_id']
    )


def downgrade():
    op.drop_constraint('unique_user_product', 'wishlist')
    op.drop_table('wishlist')
'''

print("\n[FICHIER] Migration V1.2 :")
print("  - Table wishlist avec contrainte UNIQUE composite")

"""
MIGRATION 4 : Migration de données (V1.3)
"""

ECOMMERCE_V1_3 = '''
"""Add default description to products

Revision ID: v1_3_data_migration
Revises: v1_2_wishlist
Create Date: 2024-01-22

"""
from alembic import op
import sqlalchemy as sa

revision = 'v1_3_data_migration'
down_revision = 'v1_2_wishlist'


def upgrade():
    # Remplir descriptions vides
    connection = op.get_bind()
    
    connection.execute(
        sa.text("""
            UPDATE products 
            SET description = 'Description à venir'
            WHERE description IS NULL OR description = ''
        """)
    )
    
    # Rendre description NOT NULL
    op.alter_column('products', 'description', nullable=False)


def downgrade():
    # Permettre NULL à nouveau
    op.alter_column('products', 'description', nullable=True)
'''

print("\n[FICHIER] Migration V1.3 :")
print("  - Migration données (remplir descriptions)")
print("  - Modifier contrainte (NOT NULL)")

print("\n[OK] Exercice terminé : 4 migrations créées")
print("\nPour appliquer :")
print("  alembic upgrade head")
print("\nPour rollback :")
print("  alembic downgrade v1_0_init")


# ----------------------------------------------------------------------------
# [DOCS] BEST PRACTICES MIGRATIONS
# ----------------------------------------------------------------------------

"""
[OK] BEST PRACTICES


1. TOUJOURS TESTER LOCALEMENT
──────────────────────────────
"""
# Tester upgrade
# alembic upgrade head

# Tester downgrade
# alembic downgrade -1

# Retester upgrade
# alembic upgrade head

"""
2. MIGRATIONS PETITES ET ATOMIQUES
───────────────────────────────────

[OK] BON :
- Migration 1 : Ajouter table users
- Migration 2 : Ajouter table orders
- Migration 3 : Ajouter FK orders -> users

[X] MAUVAIS :
- Migration 1 : Ajouter 5 tables + 10 colonnes + migrations données


3. DESCRIPTIONS CLAIRES
────────────────────────

[OK] BON :
alembic revision -m "Add email_verified column to users"

[X] MAUVAIS :
alembic revision -m "Update users"
alembic revision -m "fix"


4. VÉRIFIER MIGRATIONS AUTO
────────────────────────────

--autogenerate n'est pas parfait !

Toujours vérifier fichier généré :
- Opérations correctes ?
- Rien de manquant ?
- Downgrade cohérent ?


5. DONNÉES VS SCHÉMA
─────────────────────

Migrations schéma : Réversibles
Migrations données : Souvent irréversibles

Pour données :
- Downgrade = pass (ou meilleur effort)
- Backup avant migration production
- Test sur copie production


6. NE JAMAIS MODIFIER MIGRATIONS APPLIQUÉES
────────────────────────────────────────────

[X] Si migration déjà appliquée en production, NE PAS modifier

[OK] Créer nouvelle migration pour correction


7. ENVIRONNEMENTS
──────────────────

Workflow :
1. Développement local
2. Test/Staging
3. Production

Même processus partout :
alembic upgrade head


8. CI/CD
─────────

Intégrer dans pipeline :

```yaml
# .gitlab-ci.yml
deploy:
  script:
    - alembic upgrade head
    - python main.py
```


9. BACKUP AVANT MIGRATION PROD
───────────────────────────────

TOUJOURS backup avant :

```bash
# PostgreSQL
pg_dump mydb > backup_before_migration.sql

# SQLite
cp mydb.db mydb.db.backup
```


10. MONITORING
──────────────

Logger migrations :
- Quelle migration appliquée ?
- Quand ?
- Par qui ?
- Durée ?


[CLE] CHECKLIST MIGRATION PRODUCTION :

[WHITE_SQUARE] Migration testée localement
[WHITE_SQUARE] Migration testée sur staging
[WHITE_SQUARE] Backup créé
[WHITE_SQUARE] Downgrade plan (si possible)
[WHITE_SQUARE] Fenêtre maintenance planifiée (si longue)
[WHITE_SQUARE] Monitoring activé
[WHITE_SQUARE] Équipe prévenue
"""

print("\n" + "="*60)
print("BEST PRACTICES MIGRATIONS")
print("="*60)
print("""
[OK] Migrations petites et atomiques
[OK] Descriptions claires
[OK] Toujours tester upgrade ET downgrade
[OK] Vérifier migrations auto
[OK] Ne jamais modifier migrations appliquées
[OK] Backup avant production
[OK] CI/CD intégration
[OK] Monitoring et logs
""")


# ----------------------------------------------------------------------------
# [ATTENTION] PIÈGES COURANTS
# ----------------------------------------------------------------------------

"""
[ATTENTION] PIÈGES À ÉVITER


1. OUBLIER DOWNGRADE
─────────────────────

[X] MAUVAIS :
def downgrade():
    pass

[OK] BON :
def downgrade():
    op.drop_column('users', 'bio')


2. MIGRATIONS DÉPENDANTES
──────────────────────────

Ordre important !

Migration A : Créer table users
Migration B : Créer FK vers users

-> A DOIT être avant B


3. DONNÉES SENSIBLES
─────────────────────

[X] Ne PAS commit migrations avec :
- Passwords en clair
- Clés API
- Données personnelles


4. CONSTRAINTS MANQUANTES
──────────────────────────

--autogenerate peut rater :
- CHECK constraints complexes
- Certains index
- Triggers

-> Vérifier et ajouter manuellement


5. TYPES INCOMPATIBLES
───────────────────────

Changer type colonne :

[X] DANGEREUX direct :
Text -> Integer (perte données !)

[OK] SÉCURISÉ en 3 étapes :
1. Ajouter nouvelle colonne Integer
2. Migrer données (conversion)
3. Supprimer ancienne colonne


6. ROLLBACK IMPOSSIBLE
───────────────────────

Certaines migrations irréversibles :
- DROP TABLE avec données
- Changement type destructif

-> Downgrade = pass + backup nécessaire
"""

print("\n[ATTENTION] Pièges courants :")
print("  1. Oublier downgrade")
print("  2. Ordre migrations incorrecte")
print("  3. Données sensibles commitées")
print("  4. Contraintes manquantes")
print("  5. Conversions types dangereuses")
print("  6. Rollback impossible")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 16 ET PARTIE 4 COMPLÈTE
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

MIGRATIONS :
- Qu'est-ce qu'une migration
- Pourquoi Alembic
- Setup et configuration

COMMANDES ALEMBIC :
- init : Initialiser
- revision : Créer migration
- upgrade : Appliquer
- downgrade : Annuler
- history : Voir historique
- current : État actuel

MIGRATIONS :
- Auto (--autogenerate)
- Manuelles (renommages, données)
- Upgrade et downgrade
- Branches et merges

BEST PRACTICES :
- Petites et atomiques
- Descriptions claires
- Tester upgrade+downgrade
- Backup avant prod
- CI/CD intégration

PIÈGES :
- Oublier downgrade
- Ordre dépendances
- Conversions dangereuses
- Rollback impossible


[BRAVO] PARTIE 4 COMPLÈTE !

Vous maîtrisez :
[OK] sqlite3 (Chapitre 13)
[OK] SQLAlchemy ORM (Chapitre 15)
[OK] Alembic Migrations (Chapitre 16)

Vous pouvez maintenant :
[OK] Gérer bases de données en Python
[OK] Utiliser ORM pour CRUD
[OK] Gérer évolution schéma proprement
[OK] Travailler en équipe sur BD
[OK] Déployer changements en production


-> PROCHAINE ÉTAPE : PARTIE 5

La Partie 5 couvrira :
- Chapitre 17 : Performance et Optimisation avancée
- Chapitre 18 : Sécurité et authentification
- Chapitre 19 : Testing de bases de données
- Chapitre 20 : Patterns architecturaux (Repository, Unit of Work)

Cas d'usage avancés et production-ready ! [RAPIDE]
"""

print("\n" + "="*60)
print("[BRAVO] PARTIE 4 TERMINÉE !")
print("="*60)
print("""
Vous maîtrisez maintenant l'intégration Python-SQL complète :

[OK] Chapter 13 : Python + SQLite (sqlite3)
[OK] Chapter 15 : SQLAlchemy ORM
[OK] Chapter 16 : Alembic Migrations

Prochaine étape : Partie 5 - Cas avancés
""")

print("\n[OK] Chapitre 16 et Partie 4 terminés ! [BRAVO]\n")

# ============================================================================
# [LIVRE] BASES DE DONNÉES - PARTIE 5 : CAS AVANCÉS ET PRODUCTION
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 17 : Performance et Optimisation Avancée
# - Chapitre 18 : Sécurité et Authentification
# - Chapitre 19 : Testing de Bases de Données
# - Chapitre 20 : Patterns Architecturaux (Repository, Unit of Work)
#
# [TEMPS] TEMPS : ~10-12 heures
# [DOCS] PRÉREQUIS : Parties 1-4 complétées
# [PRO] NIVEAU : Production-Ready
# ============================================================================


# ============================================================================
# [GUIDE] CHAPITRE 17 : PERFORMANCE ET OPTIMISATION AVANCÉE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Profiler et identifier goulots d'étranglement
[OK] Optimiser requêtes complexes
[OK] Connection pooling
[OK] Caching strategies
[OK] Pagination efficace
[OK] Bulk operations
[OK] Read replicas et sharding (concepts)
[OK] Monitoring et métriques
"""


# ----------------------------------------------------------------------------
# [RECHERCHE] PROFILING ET ANALYSE
# ----------------------------------------------------------------------------

"""
[IDEE] PROFILING : Mesurer pour optimiser

"Premature optimization is the root of all evil" - Donald Knuth

-> Mesurez AVANT d'optimiser !


1⃣ SQLALCHEMY QUERY PROFILING
"""

from sqlalchemy import create_engine, event, Column, Integer, String, Float
from sqlalchemy.orm import declarative_base, Session
import time

Base = declarative_base()

class Product(Base):
    __tablename__ = 'products'
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    price = Column(Float)

# Engine avec echo
engine = create_engine('sqlite:///:memory:', echo=True)
# echo=True affiche tout le SQL

Base.metadata.create_all(engine)

"""
[IDEE] EVENT LISTENERS : Mesurer timing
"""

@event.listens_for(engine, "before_cursor_execute")
def receive_before_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    """Avant chaque requête"""
    context._query_start_time = time.time()

@event.listens_for(engine, "after_cursor_execute")
def receive_after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
    """Après chaque requête"""
    total_time = time.time() - context._query_start_time
    print(f"\n[TEMPS] Query time: {total_time*1000:.2f}ms")
    print(f"[NOTE] Statement: {statement[:100]}...")

session = Session(engine)

# Insérer données test
products = [Product(name=f"Product {i}", price=10.0 + i) for i in range(100)]
session.add_all(products)
session.commit()

print("\n[RECHERCHE] PROFILING ACTIVÉ")

# Tester requête
products = session.query(Product).filter(Product.price > 50).all()
print(f"[OK] Trouvé {len(products)} produits")

session.close()

"""
[IDEE] LOGGING AVANCÉ :
"""

import logging

# Logger SQLAlchemy
logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)

"""
2⃣ EXPLAIN ANALYZE (PostgreSQL)
"""

# Pour PostgreSQL avec explain
"""
from sqlalchemy import text

with session.begin():
    result = session.execute(
        text("EXPLAIN ANALYZE SELECT * FROM products WHERE price > 50")
    )
    for row in result:
        print(row)
"""

"""
3⃣ QUERY STATISTICS
"""

class QueryProfiler:
    """Profiler personnalisé"""
    
    def __init__(self):
        self.queries = []
    
    def log_query(self, statement, duration):
        self.queries.append({
            'statement': statement,
            'duration': duration,
            'timestamp': time.time()
        })
    
    def get_slow_queries(self, threshold_ms=100):
        """Requêtes > threshold"""
        return [
            q for q in self.queries 
            if q['duration'] * 1000 > threshold_ms
        ]
    
    def get_statistics(self):
        """Statistiques globales"""
        if not self.queries:
            return {}
        
        durations = [q['duration'] for q in self.queries]
        return {
            'total_queries': len(self.queries),
            'total_time': sum(durations),
            'avg_time': sum(durations) / len(durations),
            'min_time': min(durations),
            'max_time': max(durations)
        }

profiler = QueryProfiler()

@event.listens_for(engine, "after_cursor_execute")
def profile_query(conn, cursor, statement, parameters, context, executemany):
    if hasattr(context, '_query_start_time'):
        duration = time.time() - context._query_start_time
        profiler.log_query(statement, duration)

print("\n[GRAPHIQUE] QueryProfiler activé")


# ----------------------------------------------------------------------------
# [RAPIDE] N+1 QUERY PROBLEM
# ----------------------------------------------------------------------------

"""
[IDEE] N+1 PROBLEM : Piège classique ORM


SCÉNARIO :
"""

from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship

Base = declarative_base()

class Author(Base):
    __tablename__ = 'authors'
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    books = relationship("Book", back_populates="author")

class Book(Base):
    __tablename__ = 'books'
    id = Column(Integer, primary_key=True)
    title = Column(String(200))
    author_id = Column(Integer, ForeignKey('authors.id'))
    author = relationship("Author", back_populates="books")

engine = create_engine('sqlite:///:memory:', echo=False)
Base.metadata.create_all(engine)
session = Session(engine)

# Données
for i in range(10):
    author = Author(name=f"Author {i}")
    for j in range(5):
        book = Book(title=f"Book {j} by Author {i}", author=author)
    session.add(author)

session.commit()

"""
[X] N+1 PROBLEM :
"""

print("\n[X] N+1 Problem (MAUVAIS) :")

# 1 requête pour livres
books = session.query(Book).all()

# N requêtes pour auteurs (une par livre)
for book in books[:3]:
    print(f"[GUIDE] {book.title} par {book.author.name}")
    # ^ Chaque .author déclenche SELECT !

# Résultat : 1 + 50 = 51 requêtes !

"""
[OK] SOLUTION 1 : joinedload (Eager Loading)
"""

from sqlalchemy.orm import joinedload

print("\n[OK] Solution : joinedload :")

# 1 seule requête avec JOIN
books = session.query(Book).options(joinedload(Book.author)).all()

for book in books[:3]:
    print(f"[GUIDE] {book.title} par {book.author.name}")

# Résultat : 1 requête !

"""
[OK] SOLUTION 2 : selectinload
"""

from sqlalchemy.orm import selectinload

# 2 requêtes (une pour books, une pour authors)
books = session.query(Book).options(selectinload(Book.author)).all()

"""
[IDEE] COMPARAISON :

joinedload :
- 1 requête avec JOIN
- Peut être lent si beaucoup de colonnes
- Best pour 1-to-1 ou small 1-to-many

selectinload :
- 2 requêtes séparées
- Plus efficace pour large 1-to-many
- Évite duplication données

lazyload (défaut) :
- N+1 requêtes
- [X] À éviter en production
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] PAGINATION EFFICACE
# ----------------------------------------------------------------------------

"""
[IDEE] PAGINATION : Charger données par morceaux


[X] MAUVAIS : Charger tout puis slice
"""

def get_products_bad(page, per_page):
    """Charge TOUT en mémoire !"""
    all_products = session.query(Product).all()  # [X] Millions de lignes !
    start = (page - 1) * per_page
    end = start + per_page
    return all_products[start:end]

"""
[OK] BON : LIMIT et OFFSET
"""

def get_products_good(page, per_page):
    """Pagination SQL"""
    return (
        session.query(Product)
        .limit(per_page)
        .offset((page - 1) * per_page)
        .all()
    )

# Utilisation
page_1 = get_products_good(page=1, per_page=20)
print(f"\n[OK] Page 1 : {len(page_1)} produits")

"""
[IDEE] PAGINATION CURSOR-BASED (Plus performant)

OFFSET lent sur grandes tables !

Problème OFFSET :
- OFFSET 1000000 -> Scanner 1M lignes avant de retourner résultats
"""

def get_products_cursor(last_id=None, limit=20):
    """Pagination par cursor (ID)"""
    query = session.query(Product)
    
    if last_id:
        query = query.filter(Product.id > last_id)
    
    return query.order_by(Product.id).limit(limit).all()

# Utilisation
page_1 = get_products_cursor(limit=20)
last_id = page_1[-1].id if page_1 else None

page_2 = get_products_cursor(last_id=last_id, limit=20)

print(f"[OK] Cursor pagination : Page 1 = {len(page_1)}, Page 2 = {len(page_2)}")

"""
[IDEE] AVANTAGES CURSOR :

[OK] Performance constante (pas OFFSET)
[OK] Pas de données dupliquées si insertions
[OK] Fonctionne avec ordres complexes


[ATTENTION] INCONVÉNIENTS :

[X] Pas de "saut" vers page N
[X] Complexe avec tri non-unique
"""


# ----------------------------------------------------------------------------
# [SYNC] BULK OPERATIONS
# ----------------------------------------------------------------------------

"""
[IDEE] BULK : Opérations groupées efficaces


[X] LENT : Loop individuel
"""

print("\n[X] LENT : Insertions individuelles")
start = time.time()

for i in range(1000):
    product = Product(name=f"Bulk {i}", price=10.0)
    session.add(product)
    session.commit()  # [X] Commit à chaque fois !

print(f"[TEMPS] Temps : {time.time() - start:.2f}s")

"""
[OK] RAPIDE : Bulk insert
"""

print("\n[OK] RAPIDE : Bulk insert")
start = time.time()

products = [
    Product(name=f"Bulk {i}", price=10.0)
    for i in range(1000)
]

session.bulk_save_objects(products)
session.commit()  # [OK] Un seul commit !

print(f"[TEMPS] Temps : {time.time() - start:.2f}s")

"""
[IDEE] BULK UPDATE :
"""

# Update massif
session.query(Product).filter(Product.price < 20).update({
    Product.price: Product.price * 1.1  # Augmentation 10%
})
session.commit()

print("[OK] Bulk update : Prix augmentés")

"""
[IDEE] BULK DELETE :
"""

# Delete massif
deleted = session.query(Product).filter(Product.price > 100).delete()
session.commit()

print(f"[OK] Bulk delete : {deleted} produits supprimés")

"""
[CLE] COMPARAISON PERFORMANCES :

┌─────────────────────┬──────────┬────────────┐
│ Méthode             │ 1K rows  │ 100K rows  │
├─────────────────────┼──────────┼────────────┤
│ Loop + add + commit │ 10s      │ 1000s      │
│ add_all + commit    │ 0.5s     │ 50s        │
│ bulk_save_objects   │ 0.1s     │ 10s        │
│ execute_values (PG) │ 0.05s    │ 5s         │
└─────────────────────┴──────────┴────────────┘
"""


# ----------------------------------------------------------------------------
# [SWIMMER] CONNECTION POOLING
# ----------------------------------------------------------------------------

"""
[IDEE] CONNECTION POOL : Réutiliser connexions


SANS POOL :
Chaque requête :
1. Ouvrir connexion (lent !)
2. Exécuter
3. Fermer connexion


AVEC POOL :
1. Pool de connexions pré-ouvertes
2. Emprunter connexion
3. Exécuter
4. Rendre connexion au pool (pas fermer !)


SQLALCHEMY : Pool automatique !
"""

from sqlalchemy.pool import QueuePool, NullPool

# Engine avec pool (défaut)
engine = create_engine(
    'sqlite:///:memory:',
    poolclass=QueuePool,
    pool_size=5,           # Max 5 connexions actives
    max_overflow=10,       # Max 10 connexions supplémentaires temporaires
    pool_timeout=30,       # Timeout si pool plein
    pool_recycle=3600      # Recycler connexions après 1h
)

print("\n[OK] Connection pool configuré :")
print("  - pool_size : 5")
print("  - max_overflow : 10")
print("  - Total max : 15 connexions")

"""
[IDEE] TYPES DE POOLS :

QueuePool (défaut) :
- File d'attente
- Réutilisation
- Pour apps multi-threads

NullPool :
- Pas de pool
- Nouvelle connexion à chaque fois
- Pour SQLite simple

StaticPool :
- Une seule connexion
- Pour :memory:


[IDEE] MONITORING POOL :
"""

from sqlalchemy import event

@event.listens_for(engine, "connect")
def receive_connect(dbapi_conn, connection_record):
    print("[PLUGIN] Connexion ouverte")

@event.listens_for(engine, "checkout")
def receive_checkout(dbapi_conn, connection_record, connection_proxy):
    print("[SORTIE] Connexion empruntée du pool")

@event.listens_for(engine, "checkin")
def receive_checkin(dbapi_conn, connection_record):
    print("[ENTREE] Connexion rendue au pool")

print("\n[OK] Pool monitoring activé")


# ----------------------------------------------------------------------------
# [SAUVEGARDE] CACHING STRATEGIES
# ----------------------------------------------------------------------------

"""
[IDEE] CACHE : Éviter requêtes répétées


NIVEAUX DE CACHE :

1. Application level (Python dict/Redis)
2. Query cache (SQLAlchemy)
3. Database cache (built-in)


1⃣ APPLICATION CACHE SIMPLE :
"""

from functools import lru_cache
from datetime import datetime, timedelta

class ProductService:
    """Service avec cache"""
    
    def __init__(self, session):
        self.session = session
        self._cache = {}
        self._cache_ttl = {}
    
    def get_product(self, product_id, ttl_seconds=60):
        """Get avec cache"""
        
        # Vérifier cache
        if product_id in self._cache:
            # Vérifier expiration
            if datetime.now() < self._cache_ttl[product_id]:
                print(f"[SAUVEGARDE] Cache HIT : Product {product_id}")
                return self._cache[product_id]
        
        # Cache miss -> Query
        print(f"[RECHERCHE] Cache MISS : Query DB")
        product = self.session.query(Product).get(product_id)
        
        # Stocker dans cache
        self._cache[product_id] = product
        self._cache_ttl[product_id] = datetime.now() + timedelta(seconds=ttl_seconds)
        
        return product
    
    def invalidate_cache(self, product_id=None):
        """Invalider cache"""
        if product_id:
            self._cache.pop(product_id, None)
            self._cache_ttl.pop(product_id, None)
        else:
            self._cache.clear()
            self._cache_ttl.clear()

service = ProductService(session)

# Premier appel : Query
product = service.get_product(1)

# Deuxième appel : Cache
product = service.get_product(1)

print("\n[OK] Cache simple implémenté")

"""
2⃣ REDIS CACHE (Production) :
"""

# Exemple avec Redis (conceptuel)
"""
import redis
import json

redis_client = redis.Redis(host='localhost', port=6379, db=0)

def get_product_cached(product_id):
    # Vérifier cache Redis
    cached = redis_client.get(f'product:{product_id}')
    
    if cached:
        return json.loads(cached)
    
    # Query DB
    product = session.query(Product).get(product_id)
    
    # Stocker dans Redis (TTL 5 min)
    redis_client.setex(
        f'product:{product_id}',
        300,  # 5 minutes
        json.dumps({
            'id': product.id,
            'name': product.name,
            'price': product.price
        })
    )
    
    return product
"""

"""
3⃣ QUERY CACHE (SQLAlchemy dogpile.cache) :
"""

# Exemple avec dogpile.cache (conceptuel)
"""
from sqlalchemy_utils import QueryCache

cache = QueryCache()

@cache.cache_on_arguments()
def get_expensive_query():
    return session.query(Product).filter(...).all()
"""

"""
[CLE] CACHE STRATEGIES :

Cache-Aside (Lazy Loading) :
1. Check cache
2. If miss -> Query DB -> Store cache
3. Return

Write-Through :
1. Write DB
2. Write cache
3. Return

Write-Behind :
1. Write cache
2. Async write DB
3. Return

[IDEE] QUAND CACHER ?

[OK] Données rarement changées
[OK] Requêtes coûteuses
[OK] Haute fréquence lecture

[X] Données temps réel
[X] Données personnalisées
[X] Faible fréquence lecture


[ATTENTION] CACHE INVALIDATION :

"There are only two hard things in Computer Science: 
cache invalidation and naming things." - Phil Karlton

Stratégies :
- TTL (Time To Live)
- Event-based (update -> invalidate)
- Version-based
"""


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 17
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

PROFILING :
- Event listeners
- Query timing
- Slow query detection
- EXPLAIN ANALYZE

OPTIMISATIONS :
- Éviter N+1 (joinedload, selectinload)
- Pagination efficace (cursor-based)
- Bulk operations
- Index appropriés

CONNECTION POOLING :
- QueuePool configuration
- pool_size, max_overflow
- Monitoring connexions

CACHING :
- Application-level cache
- Redis integration
- Cache strategies
- Invalidation patterns


[CLE] RÈGLES PERFORMANCE :

1. Mesurer avant optimiser
2. Index sur colonnes WHERE/JOIN
3. Éviter SELECT *
4. Pagination pour grandes tables
5. Bulk pour opérations massives
6. Pool pour connexions
7. Cache pour données lues fréquemment


-> PROCHAINE ÉTAPE : Chapitre 18 - Sécurité

Protection contre attaques et best practices !
"""

print("\n" + "="*60)
print("[OK] Chapitre 17 terminé - Performance maîtrisée !")
print("="*60)

session.close()


# ============================================================================
# [GUIDE] CHAPITRE 18 : SÉCURITÉ ET AUTHENTIFICATION
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Protéger contre SQL Injection
[OK] Hasher passwords correctement
[OK] Implémenter authentification sécurisée
[OK] RBAC (Role-Based Access Control)
[OK] Protéger données sensibles
[OK] Auditing et logging
[OK] OWASP Top 10 pour bases de données
"""


# ----------------------------------------------------------------------------
# [SECURITE] SQL INJECTION PROTECTION
# ----------------------------------------------------------------------------

"""
[IDEE] SQL INJECTION : Attaque #1 sur bases de données


RAPPEL : TOUJOURS paramètres liés !
"""

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

Base = declarative_base()

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

engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(engine)
session = Session(engine)

# Données test
session.add_all([
    User(username='admin', email='admin@site.com'),
    User(username='alice', email='alice@site.com'),
    User(username='bob', email='bob@site.com')
])
session.commit()

"""
[X] DANGEREUX : Interpolation string
"""

def login_vulnerable(username, password):
    """[X] VULNÉRABLE"""
    query = f"SELECT * FROM users WHERE username = '{username}'"
    # ^ Injection possible !
    
# Attaque :
# username = "admin' OR '1'='1"
# Query devient : SELECT * FROM users WHERE username = 'admin' OR '1'='1'
# -> Retourne TOUS les users !

"""
[OK] SÉCURISÉ : Paramètres liés
"""

def login_secure(username):
    """[OK] SÉCURISÉ"""
    return session.query(User).filter(User.username == username).first()

# Même avec injection :
malicious_input = "admin' OR '1'='1"
user = login_secure(malicious_input)
# -> Cherche username littéralement "admin' OR '1'='1"
# -> Pas trouvé, retourne None

print("\n[SECURITE] SQL Injection : Protected [OK]")

"""
[CLE] RÈGLES ANTI-INJECTION :

[OK] TOUJOURS paramètres liés (?, :name)
[OK] ORM par défaut (SQLAlchemy)
[OK] Valider input utilisateur
[OK] Échapper caractères spéciaux si nécessaire
[X] JAMAIS f-strings avec user input
[X] JAMAIS concaténation directe
"""


# ----------------------------------------------------------------------------
# [SECURISE] PASSWORD HASHING
# ----------------------------------------------------------------------------

"""
[IDEE] PASSWORDS : JAMAIS en clair dans BD !


[X] DANGEREUX : Plaintext
"""

# [X] NE JAMAIS FAIRE ÇA !
# user.password = "mypassword123"

"""
[X] DANGEREUX : MD5/SHA1
"""

import hashlib

# [X] FAIBLE (crackable avec rainbow tables)
# password_hash = hashlib.md5(password.encode()).hexdigest()

"""
[OK] SÉCURISÉ : bcrypt, argon2, scrypt
"""

# Installation : pip install bcrypt
import bcrypt

def hash_password(password: str) -> str:
    """Hasher password avec bcrypt"""
    # Générer salt
    salt = bcrypt.gensalt(rounds=12)  # rounds = coût (4-31)
    
    # Hasher
    hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
    
    return hashed.decode('utf-8')

def verify_password(password: str, hashed: str) -> bool:
    """Vérifier password"""
    return bcrypt.checkpw(
        password.encode('utf-8'),
        hashed.encode('utf-8')
    )

# Utilisation
print("\n[SECURISE] Password Hashing :")

password = "SecurePassword123!"
hashed = hash_password(password)
print(f"  Original : {password}")
print(f"  Hashed : {hashed[:30]}...")

# Vérification
is_valid = verify_password("SecurePassword123!", hashed)
print(f"  Valid password : {is_valid}")

is_invalid = verify_password("WrongPassword", hashed)
print(f"  Wrong password : {is_invalid}")

"""
[IDEE] MODÈLE USER SÉCURISÉ :
"""

from sqlalchemy import Boolean, DateTime
from datetime import datetime

class SecureUser(Base):
    __tablename__ = 'secure_users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True, nullable=False)
    email = Column(String(100), unique=True, nullable=False)
    password_hash = Column(String(128), nullable=False)  # <- Hash
    is_active = Column(Boolean, default=True)
    is_verified = Column(Boolean, default=False)
    failed_login_attempts = Column(Integer, default=0)
    locked_until = Column(DateTime, nullable=True)
    last_login = Column(DateTime)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    def set_password(self, password: str):
        """Hasher et stocker password"""
        self.password_hash = hash_password(password)
    
    def check_password(self, password: str) -> bool:
        """Vérifier password"""
        return verify_password(password, self.password_hash)
    
    def is_locked(self) -> bool:
        """Compte verrouillé ?"""
        if self.locked_until and datetime.utcnow() < self.locked_until:
            return True
        return False

Base.metadata.create_all(engine)
session = Session(engine)

# Créer user
user = SecureUser(username='alice', email='alice@secure.com')
user.set_password('MySecurePass123!')
session.add(user)
session.commit()

print("\n[OK] Secure user créé")

# Login
retrieved = session.query(SecureUser).filter_by(username='alice').first()
if retrieved and retrieved.check_password('MySecurePass123!'):
    print("[OK] Login successful")
else:
    print("[X] Login failed")


# ----------------------------------------------------------------------------
# [SCENARIO] RBAC (Role-Based Access Control)
# ----------------------------------------------------------------------------

"""
[IDEE] RBAC : Permissions par rôles


MODÈLE :

User -> Role -> Permissions


EXEMPLE :
- User "alice" a role "admin"
- Role "admin" a permissions ["read", "write", "delete"]
- -> alice peut read/write/delete
"""

from sqlalchemy import Table, ForeignKey
from sqlalchemy.orm import relationship

# Tables de liaison
user_roles = Table(
    'user_roles',
    Base.metadata,
    Column('user_id', Integer, ForeignKey('rbac_users.id')),
    Column('role_id', Integer, ForeignKey('roles.id'))
)

role_permissions = Table(
    'role_permissions',
    Base.metadata,
    Column('role_id', Integer, ForeignKey('roles.id')),
    Column('permission_id', Integer, ForeignKey('permissions.id'))
)

class RBACUser(Base):
    __tablename__ = 'rbac_users'
    
    id = Column(Integer, primary_key=True)
    username = Column(String(50), unique=True)
    
    # Relations
    roles = relationship("Role", secondary=user_roles, back_populates="users")
    
    def has_permission(self, permission_name: str) -> bool:
        """Vérifier si user a permission"""
        for role in self.roles:
            for perm in role.permissions:
                if perm.name == permission_name:
                    return True
        return False
    
    def has_role(self, role_name: str) -> bool:
        """Vérifier si user a rôle"""
        return any(role.name == role_name for role in self.roles)

class Role(Base):
    __tablename__ = 'roles'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True)
    description = Column(Text)
    
    # Relations
    users = relationship("RBACUser", secondary=user_roles, back_populates="roles")
    permissions = relationship("Permission", secondary=role_permissions, back_populates="roles")

class Permission(Base):
    __tablename__ = 'permissions'
    
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True)
    description = Column(Text)
    
    # Relations
    roles = relationship("Role", secondary=role_permissions, back_populates="permissions")

Base.metadata.create_all(engine)
session = Session(engine)

# Setup RBAC
print("\n[SCENARIO] RBAC Setup :")

# Permissions
perm_read = Permission(name='read', description='Read access')
perm_write = Permission(name='write', description='Write access')
perm_delete = Permission(name='delete', description='Delete access')

session.add_all([perm_read, perm_write, perm_delete])

# Roles
role_viewer = Role(name='viewer', description='Read-only')
role_viewer.permissions.append(perm_read)

role_editor = Role(name='editor', description='Read and write')
role_editor.permissions.extend([perm_read, perm_write])

role_admin = Role(name='admin', description='Full access')
role_admin.permissions.extend([perm_read, perm_write, perm_delete])

session.add_all([role_viewer, role_editor, role_admin])

# Users
alice = RBACUser(username='alice')
alice.roles.append(role_admin)

bob = RBACUser(username='bob')
bob.roles.append(role_editor)

charlie = RBACUser(username='charlie')
charlie.roles.append(role_viewer)

session.add_all([alice, bob, charlie])
session.commit()

# Vérifier permissions
print(f"\n  Alice (admin) :")
print(f"    Can read? {alice.has_permission('read')}")
print(f"    Can delete? {alice.has_permission('delete')}")

print(f"\n  Bob (editor) :")
print(f"    Can write? {bob.has_permission('write')}")
print(f"    Can delete? {bob.has_permission('delete')}")

print(f"\n  Charlie (viewer) :")
print(f"    Can read? {charlie.has_permission('read')}")
print(f"    Can write? {charlie.has_permission('write')}")

"""
[IDEE] DÉCORATEUR POUR VÉRIFIER PERMISSIONS :
"""

from functools import wraps

def require_permission(permission_name: str):
    """Décorateur pour vérifier permission"""
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if not user.has_permission(permission_name):
                raise PermissionError(f"Permission '{permission_name}' required")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

@require_permission('delete')
def delete_resource(user, resource_id):
    print(f"[OK] {user.username} deleted resource {resource_id}")

# Tester
try:
    delete_resource(alice, 123)  # [OK] Admin peut delete
except PermissionError as e:
    print(f"[X] {e}")

try:
    delete_resource(charlie, 456)  # [X] Viewer ne peut pas
except PermissionError as e:
    print(f"[X] {e}")


# ----------------------------------------------------------------------------
# [NOTE] AUDITING ET LOGGING
# ----------------------------------------------------------------------------

"""
[IDEE] AUDIT : Tracer actions utilisateurs


QUOI LOGGER :
[OK] Login/Logout
[OK] Failed login attempts
[OK] Data modifications (Create/Update/Delete)
[OK] Permission checks
[OK] Suspicious activity
"""

class AuditLog(Base):
    __tablename__ = 'audit_logs'
    
    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('rbac_users.id'))
    action = Column(String(50), nullable=False)  # login, create, update, delete
    entity_type = Column(String(50))  # User, Product, Order
    entity_id = Column(Integer)
    old_value = Column(Text)  # JSON avant
    new_value = Column(Text)  # JSON après
    ip_address = Column(String(45))
    user_agent = Column(String(200))
    timestamp = Column(DateTime, default=datetime.utcnow)
    success = Column(Boolean)
    
    # Relation
    user = relationship("RBACUser")

Base.metadata.create_all(engine)

"""
[IDEE] FONCTION AUDIT :
"""

import json

def log_action(session, user_id, action, entity_type=None, entity_id=None, 
               old_value=None, new_value=None, success=True):
    """Logger action"""
    log = AuditLog(
        user_id=user_id,
        action=action,
        entity_type=entity_type,
        entity_id=entity_id,
        old_value=json.dumps(old_value) if old_value else None,
        new_value=json.dumps(new_value) if new_value else None,
        success=success
    )
    session.add(log)

# Exemple : Logger update
print("\n[NOTE] Audit Logging :")

old_product = {'name': 'Product A', 'price': 10.0}
new_product = {'name': 'Product A', 'price': 12.0}

log_action(
    session,
    user_id=alice.id,
    action='update',
    entity_type='Product',
    entity_id=1,
    old_value=old_product,
    new_value=new_product
)

session.commit()

# Consulter logs
logs = session.query(AuditLog).all()
for log in logs:
    print(f"  [{log.timestamp}] {log.user.username} -> {log.action} {log.entity_type}")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 18
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

SÉCURITÉ BASE :
- SQL Injection protection (paramètres)
- Password hashing (bcrypt)
- Never plaintext passwords

AUTHENTIFICATION :
- Secure user model
- Login/Logout
- Failed attempts tracking
- Account locking

AUTHORIZATION (RBAC) :
- Users -> Roles -> Permissions
- Permission checking
- Decorators pour accès

AUDITING :
- Audit logs
- Traçabilité actions
- Détection activité suspecte


[CLE] CHECKLIST SÉCURITÉ BD :

[WHITE_SQUARE] Paramètres liés (jamais string interpolation)
[WHITE_SQUARE] Passwords hashés (bcrypt/argon2)
[WHITE_SQUARE] RBAC implémenté
[WHITE_SQUARE] Audit logging activé
[WHITE_SQUARE] HTTPS pour connexions (production)
[WHITE_SQUARE] Principle of least privilege
[WHITE_SQUARE] Input validation
[WHITE_SQUARE] Rate limiting login
[WHITE_SQUARE] Regular security audits


-> PROCHAINE ÉTAPE : Chapitre 19 - Testing

Tests automatisés pour bases de données !
"""

print("\n" + "="*60)
print("[OK] Chapitre 18 terminé - Sécurité maîtrisée !")
print("="*60)


# ============================================================================
# [GUIDE] CHAPITRE 19 : TESTING DE BASES DE DONNÉES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Stratégies de test pour BD
[OK] Fixtures et données de test
[OK] Test isolation (transactions)
[OK] Test databases vs mocks
[OK] Integration testing
[OK] Test performance
[OK] pytest et SQLAlchemy
"""


# ----------------------------------------------------------------------------
# [TEST] STRATÉGIES DE TEST
# ----------------------------------------------------------------------------

"""
[IDEE] TESTS BD : Plusieurs approches


1⃣ TEST DATABASE (Recommandé)
──────────────────────────────

Créer BD test séparée pour tests

[OK] Tests réalistes
[OK] Détecte problèmes SQL
[OK] Teste vraiment intégration

[X] Plus lent que mocks
[X] Nécessite setup/teardown


2⃣ IN-MEMORY DATABASE
──────────────────────

SQLite :memory: pour tests

[OK] Très rapide
[OK] Pas de cleanup
[OK] Isolation parfaite

[X] Différences SQLite vs PostgreSQL
[X] Moins réaliste


3⃣ MOCKS
─────────

Mock session/queries

[OK] Très rapide
[OK] Pas de BD nécessaire

[X] Ne teste pas SQL réel
[X] Peut manquer bugs


[IDEE] RECOMMANDATION :

Tests unitaires -> In-memory ou mocks
Tests intégration -> Test database
Tests E2E -> Database réelle
"""


# ----------------------------------------------------------------------------
# [OUTIL] SETUP AVEC PYTEST
# ----------------------------------------------------------------------------

"""
[IDEE] PYTEST : Framework de test Python


STRUCTURE PROJET :

myproject/
├── src/
│   ├── models.py
│   ├── services.py
│   └── database.py
├── tests/
│   ├── conftest.py      <- Fixtures pytest
│   ├── test_models.py
│   └── test_services.py
└── pytest.ini


INSTALLATION :
pip install pytest pytest-cov
"""

# conftest.py - Fixtures partagées

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

@pytest.fixture(scope='function')
def engine():
    '''Engine pour chaque test'''
    engine = create_engine('sqlite:///:memory:')
    Base.metadata.create_all(engine)
    yield engine
    Base.metadata.drop_all(engine)
    engine.dispose()

@pytest.fixture(scope='function')
def session(engine):
    '''Session pour chaque test'''
    Session = sessionmaker(bind=engine)
    session = Session()
    yield session
    session.close()

@pytest.fixture(scope='function')
def db_with_data(session):
    '''Session avec données test'''
    # Ajouter données
    user = User(username='testuser', email='test@test.com')
    session.add(user)
    session.commit()
    yield session
"""

"""
[IDEE] EXEMPLE TEST :
"""

# test_models.py

"""
def test_create_user(session):
    '''Test création user'''
    user = User(username='alice', email='alice@test.com')
    session.add(user)
    session.commit()
    
    assert user.id is not None
    assert user.username == 'alice'

def test_unique_username(session):
    '''Test contrainte unique'''
    user1 = User(username='alice', email='alice1@test.com')
    user2 = User(username='alice', email='alice2@test.com')
    
    session.add(user1)
    session.commit()
    
    session.add(user2)
    
    with pytest.raises(IntegrityError):
        session.commit()

def test_query_user(db_with_data):
    '''Test query avec données'''
    user = db_with_data.query(User).filter_by(username='testuser').first()
    
    assert user is not None
    assert user.email == 'test@test.com'
"""

print("\n[TEST] Pytest fixtures configurées")

"""
[IDEE] FACTORY PATTERN POUR DONNÉES TEST :
"""

class UserFactory:
    """Factory pour créer users test"""
    
    _counter = 0
    
    @classmethod
    def create(cls, session, **kwargs):
        """Créer user avec valeurs par défaut"""
        cls._counter += 1
        
        defaults = {
            'username': f'user{cls._counter}',
            'email': f'user{cls._counter}@test.com',
        }
        defaults.update(kwargs)
        
        user = RBACUser(**defaults)
        session.add(user)
        session.commit()
        
        return user

# Utilisation
"""
def test_with_factory(session):
    user1 = UserFactory.create(session)
    user2 = UserFactory.create(session, username='alice')
    
    assert user1.username == 'user1'
    assert user2.username == 'alice'
"""

print("[OK] UserFactory créée")


# ----------------------------------------------------------------------------
# [VERROUILLE] TEST ISOLATION
# ----------------------------------------------------------------------------

"""
[IDEE] ISOLATION : Tests indépendants


PROBLÈME SANS ISOLATION :

Test A crée user "alice"
Test B suppose BD vide
-> Test B échoue car alice existe !


SOLUTION 1 : Transaction Rollback
──────────────────────────────────
"""

# conftest.py
"""
@pytest.fixture(scope='function')
def session(engine):
    '''Session avec transaction rollback'''
    connection = engine.connect()
    transaction = connection.begin()
    
    Session = sessionmaker(bind=connection)
    session = Session()
    
    yield session
    
    session.close()
    transaction.rollback()  # <- Rollback tout
    connection.close()
"""

"""
Avantages :
[OK] Rollback automatique
[OK] Tests rapides
[OK] Pas de cleanup manuel

Inconvénients :
[X] Ne teste pas vraiment commits
[X] Peut masquer bugs commit


SOLUTION 2 : Truncate Tables
─────────────────────────────
"""

"""
@pytest.fixture(scope='function', autouse=True)
def cleanup_db(session):
    '''Vider tables après chaque test'''
    yield
    
    # Après test : vider toutes tables
    for table in reversed(Base.metadata.sorted_tables):
        session.execute(table.delete())
    session.commit()
"""

"""
SOLUTION 3 : Drop/Create
────────────────────────
"""

"""
@pytest.fixture(scope='function')
def session(engine):
    '''Recréer schéma pour chaque test'''
    Base.metadata.drop_all(engine)
    Base.metadata.create_all(engine)
    
    Session = sessionmaker(bind=engine)
    session = Session()
    
    yield session
    
    session.close()
"""

print("\n[VERROUILLE] Test isolation configurée")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRES 18-19
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

CHAPITRE 18 - SÉCURITÉ :
- SQL Injection protection
- Password hashing (bcrypt)
- RBAC (Roles & Permissions)
- Audit logging

CHAPITRE 19 - TESTING :
- Stratégies test (in-memory, test DB)
- Pytest fixtures
- Test isolation
- Factories pour données


-> CHAPITRE 20 FINAL : Architecture Patterns

Repository, Unit of Work, et patterns production !
"""

print("\n" + "="*60)
print("[OK] Chapitres 18-19 terminés !")
print("="*60)


# ============================================================================
# [GUIDE] CHAPITRE 20 : PATTERNS ARCHITECTURAUX
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Repository Pattern
[OK] Unit of Work Pattern
[OK] Service Layer
[OK] Dependency Injection
[OK] Architecture en couches
[OK] Clean Architecture pour BD
"""


# ----------------------------------------------------------------------------
# [PACKAGE] REPOSITORY PATTERN
# ----------------------------------------------------------------------------

"""
[IDEE] REPOSITORY : Abstraction accès données


PROBLÈME SANS REPOSITORY :

Service mélange logique métier + SQL :

```python
def create_order(user_id, items):
    user = session.query(User).get(user_id)  # <- SQL
    
    total = sum(item.price for item in items)  # <- Business logic
    
    order = Order(user=user, total=total)  # <- SQL
    session.add(order)
    session.commit()  # <- SQL
```

-> Couplage fort avec SQLAlchemy
-> Difficile à tester
-> Difficile à changer BD


SOLUTION : Repository Pattern
"""

from abc import ABC, abstractmethod
from typing import List, Optional

class UserRepository(ABC):
    """Interface repository (abstraction)"""
    
    @abstractmethod
    def get_by_id(self, user_id: int) -> Optional[RBACUser]:
        pass
    
    @abstractmethod
    def get_by_username(self, username: str) -> Optional[RBACUser]:
        pass
    
    @abstractmethod
    def get_all(self) -> List[RBACUser]:
        pass
    
    @abstractmethod
    def add(self, user: RBACUser) -> RBACUser:
        pass
    
    @abstractmethod
    def update(self, user: RBACUser) -> RBACUser:
        pass
    
    @abstractmethod
    def delete(self, user_id: int) -> bool:
        pass

class SQLAlchemyUserRepository(UserRepository):
    """Implémentation SQLAlchemy"""
    
    def __init__(self, session: Session):
        self.session = session
    
    def get_by_id(self, user_id: int) -> Optional[RBACUser]:
        return self.session.get(RBACUser, user_id)
    
    def get_by_username(self, username: str) -> Optional[RBACUser]:
        return self.session.query(RBACUser).filter_by(username=username).first()
    
    def get_all(self) -> List[RBACUser]:
        return self.session.query(RBACUser).all()
    
    def add(self, user: RBACUser) -> RBACUser:
        self.session.add(user)
        return user
    
    def update(self, user: RBACUser) -> RBACUser:
        self.session.add(user)
        return user
    
    def delete(self, user_id: int) -> bool:
        user = self.get_by_id(user_id)
        if user:
            self.session.delete(user)
            return True
        return False

# Utilisation
repo = SQLAlchemyUserRepository(session)

user = repo.get_by_username('alice')
print(f"\n[PACKAGE] Repository : Found {user.username if user else 'None'}")

"""
[IDEE] AVANTAGES :

[OK] Abstraction BD
[OK] Facilite tests (mock repository)
[OK] Changement BD facile
[OK] Logique métier séparée


[IDEE] GENERIC REPOSITORY :
"""

from typing import TypeVar, Generic, Type

T = TypeVar('T')

class GenericRepository(Generic[T]):
    """Repository générique"""
    
    def __init__(self, session: Session, model_class: Type[T]):
        self.session = session
        self.model_class = model_class
    
    def get(self, id: int) -> Optional[T]:
        return self.session.get(self.model_class, id)
    
    def get_all(self) -> List[T]:
        return self.session.query(self.model_class).all()
    
    def add(self, entity: T) -> T:
        self.session.add(entity)
        return entity
    
    def delete(self, id: int) -> bool:
        entity = self.get(id)
        if entity:
            self.session.delete(entity)
            return True
        return False

# Utilisation
user_repo = GenericRepository(session, RBACUser)
role_repo = GenericRepository(session, Role)

print("[OK] Generic Repository créé")


# ----------------------------------------------------------------------------
# [SYNC] UNIT OF WORK PATTERN
# ----------------------------------------------------------------------------

"""
[IDEE] UNIT OF WORK : Gérer transactions


PROBLÈME :

Service A commit
Service B rollback
-> Incohérence !


SOLUTION : Unit of Work centralise transactions
"""

class UnitOfWork:
    """Gestionnaire de transaction"""
    
    def __init__(self, session_factory):
        self.session_factory = session_factory
    
    def __enter__(self):
        self.session = self.session_factory()
        
        # Repositories
        self.users = SQLAlchemyUserRepository(self.session)
        self.roles = GenericRepository(self.session, Role)
        self.permissions = GenericRepository(self.session, Permission)
        
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            self.rollback()
        self.session.close()
    
    def commit(self):
        """Commit transaction"""
        self.session.commit()
    
    def rollback(self):
        """Rollback transaction"""
        self.session.rollback()

# Utilisation
SessionFactory = sessionmaker(bind=engine)

with UnitOfWork(SessionFactory) as uow:
    # Créer user
    user = RBACUser(username='newuser')
    uow.users.add(user)
    
    # Créer role
    role = Role(name='newrole')
    uow.roles.add(role)
    
    # Commit atomique
    uow.commit()

print("\n[SYNC] Unit of Work : Transaction atomique [OK]")

"""
[IDEE] AVANTAGES :

[OK] Transactions centralisées
[OK] Cohérence garantie
[OK] Rollback automatique si erreur
[OK] Tous repositories partagent session


[IDEE] EXEMPLE AVEC ERREUR :
"""

try:
    with UnitOfWork(SessionFactory) as uow:
        user = RBACUser(username='testuser2')
        uow.users.add(user)
        
        # Simuler erreur
        raise Exception("Something went wrong!")
        
        uow.commit()  # Jamais atteint
        
except Exception as e:
    print(f"\n[X] Erreur : {e}")
    print("<- Rollback automatique")


# ----------------------------------------------------------------------------
# [CONSTRUCTION] SERVICE LAYER
# ----------------------------------------------------------------------------

"""
[IDEE] SERVICE LAYER : Logique métier


ARCHITECTURE :

Controller -> Service -> Repository -> Database


Service :
- Orchestrate repositories
- Business logic
- Transactions (via UoW)
"""

class UserService:
    """Service pour logique métier users"""
    
    def __init__(self, uow: UnitOfWork):
        self.uow = uow
    
    def register_user(self, username: str, email: str, password: str) -> RBACUser:
        """Enregistrer nouveau user"""
        
        # Vérifier username unique
        existing = self.uow.users.get_by_username(username)
        if existing:
            raise ValueError("Username already exists")
        
        # Créer user
        user = RBACUser(username=username, email=email)
        user.set_password(password)
        
        # Assigner role par défaut
        viewer_role = self.uow.session.query(Role).filter_by(name='viewer').first()
        if viewer_role:
            user.roles.append(viewer_role)
        
        self.uow.users.add(user)
        self.uow.commit()
        
        return user
    
    def authenticate(self, username: str, password: str) -> Optional[RBACUser]:
        """Authentifier user"""
        
        user = self.uow.users.get_by_username(username)
        
        if not user:
            return None
        
        if user.check_password(password):
            # Update last login
            user.last_login = datetime.utcnow()
            self.uow.commit()
            return user
        
        return None
    
    def assign_role(self, user_id: int, role_name: str) -> bool:
        """Assigner rôle à user"""
        
        user = self.uow.users.get_by_id(user_id)
        role = self.uow.session.query(Role).filter_by(name=role_name).first()
        
        if not user or not role:
            return False
        
        if role not in user.roles:
            user.roles.append(role)
            self.uow.commit()
        
        return True

# Utilisation
print("\n[CONSTRUCTION] Service Layer :")

with UnitOfWork(SessionFactory) as uow:
    service = UserService(uow)
    
    # Enregistrer user
    try:
        new_user = service.register_user('serviceuser', 'service@test.com', 'Pass123!')
        print(f"  [OK] User registered : {new_user.username}")
    except ValueError as e:
        print(f"  [X] Error : {e}")

"""
[IDEE] AVANTAGES SERVICE LAYER :

[OK] Logique métier centralisée
[OK] Réutilisable
[OK] Testable (mock UoW)
[OK] Séparation responsabilités
"""


# ----------------------------------------------------------------------------
# [OBJECTIF] ARCHITECTURE COMPLÈTE
# ----------------------------------------------------------------------------

"""
[IDEE] CLEAN ARCHITECTURE POUR BD


COUCHES :

1. MODELS (Entities)
   └─ User, Role, Permission

2. REPOSITORIES (Data Access)
   └─ UserRepository, RoleRepository

3. SERVICES (Business Logic)
   └─ UserService, AuthService

4. CONTROLLERS/API (Presentation)
   └─ UserController, AuthController


DÉPENDANCES :

Controller -> Service -> Repository -> Models
     v          v          v           v
   HTTP      Logic    SQLAlchemy   Database


[IDEE] EXEMPLE COMPLET :
"""

# 1. Models (déjà définis)
# User, Role, Permission

# 2. Repositories
class RoleRepository:
    def __init__(self, session):
        self.session = session
    
    def get_by_name(self, name: str) -> Optional[Role]:
        return self.session.query(Role).filter_by(name=name).first()

# 3. Services
class AuthService:
    """Service authentification"""
    
    def __init__(self, user_repo: UserRepository, role_repo: RoleRepository):
        self.user_repo = user_repo
        self.role_repo = role_repo
    
    def login(self, username: str, password: str) -> Optional[dict]:
        """Login et retourner token (simplifié)"""
        
        user = self.user_repo.get_by_username(username)
        
        if not user or not user.check_password(password):
            return None
        
        # Retourner infos user (sans password!)
        return {
            'user_id': user.id,
            'username': user.username,
            'roles': [role.name for role in user.roles]
        }

# 4. Controller (API)
class UserController:
    """Controller HTTP"""
    
    def __init__(self, auth_service: AuthService):
        self.auth_service = auth_service
    
    def post_login(self, request_data: dict) -> dict:
        """Endpoint POST /login"""
        
        username = request_data.get('username')
        password = request_data.get('password')
        
        if not username or not password:
            return {'error': 'Missing credentials'}, 400
        
        result = self.auth_service.login(username, password)
        
        if result:
            return {'data': result}, 200
        else:
            return {'error': 'Invalid credentials'}, 401

# Wire up (Dependency Injection)
with UnitOfWork(SessionFactory) as uow:
    # Repositories
    user_repo = SQLAlchemyUserRepository(uow.session)
    role_repo = RoleRepository(uow.session)
    
    # Services
    auth_service = AuthService(user_repo, role_repo)
    
    # Controllers
    controller = UserController(auth_service)
    
    # Simuler requête
    request = {'username': 'alice', 'password': 'MySecurePass123!'}
    response, status = controller.post_login(request)
    
    print(f"\n[OBJECTIF] Architecture complète :")
    print(f"  Request : {request}")
    print(f"  Response : {response}")
    print(f"  Status : {status}")


# ----------------------------------------------------------------------------
# [DOCS] RÉCAPITULATIF CHAPITRE 20 ET PARTIE 5 COMPLÈTE
# ----------------------------------------------------------------------------

"""
[OK] VOUS MAÎTRISEZ MAINTENANT :

CHAPITRE 17 - PERFORMANCE :
- Profiling et optimisation
- N+1 queries (joinedload)
- Pagination cursor-based
- Bulk operations
- Connection pooling
- Caching strategies

CHAPITRE 18 - SÉCURITÉ :
- SQL Injection protection
- Password hashing (bcrypt)
- RBAC (Roles & Permissions)
- Audit logging

CHAPITRE 19 - TESTING :
- Test strategies (in-memory, test DB)
- Pytest fixtures
- Test isolation
- Factory pattern

CHAPITRE 20 - ARCHITECTURE :
- Repository Pattern
- Unit of Work Pattern
- Service Layer
- Clean Architecture


[BRAVO] GUIDE COMPLET TERMINÉ !

VOUS ÊTES MAINTENANT CAPABLE DE :

[OK] Concevoir schémas BD normalisés
[OK] Écrire SQL avancé (JOIN, CTE, Window Functions)
[OK] Optimiser performances
[OK] Implémenter sécurité robuste
[OK] Tester code BD
[OK] Architecturer applications proprement
[OK] Utiliser ORM (SQLAlchemy)
[OK] Gérer migrations (Alembic)


VOUS ÊTES UN EXPERT EN BASES DE DONNÉES ! [RAPIDE]


[GUIDE] PARCOURS COMPLET :

PARTIE 1 : Fondamentaux (4-6h)
-> SQL de base, types, contraintes

PARTIE 2 : SQL Avancé (8-10h)
-> JOIN, Window Functions, Index, Vues

PARTIE 3 : Conception (8-10h)
-> Modélisation, Normalisation

PARTIE 4 : Python Integration (10-12h)
-> sqlite3, SQLAlchemy, Alembic

PARTIE 5 : Production (10-12h)
-> Performance, Sécurité, Testing, Architecture


TOTAL : ~40-50 heures de contenu expert ! [COURS]


[SOON_WITH_RIGHTWARDS_ARROW_ABOVE] PROCHAINES ÉTAPES SUGGÉRÉES :

1. Pratiquer avec projet personnel
2. Contribuer à projets open-source
3. Explorer PostgreSQL avancé
4. Approfondir MongoDB (NoSQL)
5. Étudier database sharding
6. Apprendre Redis (cache)
7. Maîtriser Elasticsearch


RESSOURCES RECOMMANDÉES :

[DOCS] Livres :
- "Database Design for Mere Mortals"
- "SQL Performance Explained"
- "Designing Data-Intensive Applications"

[COURS] Pratique :
- LeetCode SQL problems
- SQLZoo exercises
- Mode Analytics SQL tutorial

[OUTILS] Projets :
- Blog avec authentification
- E-commerce avec panier
- API REST avec BD complexe
"""

print("\n" + "="*60)
print("[BRAVO] GUIDE COMPLET TERMINÉ !")
print("="*60)
print("""
Félicitations ! Vous maîtrisez maintenant :

[OK] Partie 1 : Fondamentaux SQL
[OK] Partie 2 : SQL Avancé
[OK] Partie 3 : Conception et Modélisation
[OK] Partie 4 : Python Integration
[OK] Partie 5 : Production & Architecture

Vous êtes prêt(e) pour la production ! [RAPIDE]

Total : ~40-50 heures de contenu expert
""")

print("\n[OK] Partie 5 et Guide complet terminés ! [BRAVO]\n")