//! Gestionnaire de requêtes préparées pour optimiser les performances //! //! Ce module implémente un cache de requêtes préparées pour améliorer //! les performances et la sécurité des requêtes SQL fréquentes. package database import ( "context" "database/sql" "fmt" "sync" "go.uber.org/zap" ) // PreparedStatement représente une requête préparée avec son nom type PreparedStatement struct { Name string Query string Stmt *sql.Stmt } // PreparedStatementManager gère le cache des requêtes préparées type PreparedStatementManager struct { db *sql.DB statements map[string]*PreparedStatement mutex sync.RWMutex logger *zap.Logger } // NewPreparedStatementManager crée un nouveau gestionnaire de requêtes préparées func NewPreparedStatementManager(db *sql.DB, logger *zap.Logger) *PreparedStatementManager { return &PreparedStatementManager{ db: db, statements: make(map[string]*PreparedStatement), logger: logger, } } // Prepare prépare une requête SQL et la met en cache func (psm *PreparedStatementManager) Prepare(ctx context.Context, name, query string) error { psm.mutex.Lock() defer psm.mutex.Unlock() // Vérifier si la requête est déjà préparée if _, exists := psm.statements[name]; exists { psm.logger.Debug("Statement already prepared", zap.String("name", name)) return nil } // Préparer la requête stmt, err := psm.db.PrepareContext(ctx, query) if err != nil { psm.logger.Error("Failed to prepare statement", zap.String("name", name), zap.String("query", query), zap.Error(err)) return fmt.Errorf("failed to prepare statement %s: %w", name, err) } // Mettre en cache psm.statements[name] = &PreparedStatement{ Name: name, Query: query, Stmt: stmt, } psm.logger.Debug("Statement prepared successfully", zap.String("name", name)) return nil } // GetStatement récupère une requête préparée depuis le cache func (psm *PreparedStatementManager) GetStatement(name string) (*sql.Stmt, error) { psm.mutex.RLock() defer psm.mutex.RUnlock() stmt, exists := psm.statements[name] if !exists { return nil, fmt.Errorf("statement %s not found", name) } return stmt.Stmt, nil } // Execute exécute une requête préparée avec des arguments func (psm *PreparedStatementManager) Execute(ctx context.Context, name string, args ...interface{}) (sql.Result, error) { stmt, err := psm.GetStatement(name) if err != nil { return nil, err } return stmt.ExecContext(ctx, args...) } // Query exécute une requête préparée et retourne des lignes func (psm *PreparedStatementManager) Query(ctx context.Context, name string, args ...interface{}) (*sql.Rows, error) { stmt, err := psm.GetStatement(name) if err != nil { return nil, err } return stmt.QueryContext(ctx, args...) } // QueryRow exécute une requête préparée et retourne une ligne func (psm *PreparedStatementManager) QueryRow(ctx context.Context, name string, args ...interface{}) *sql.Row { stmt, err := psm.GetStatement(name) if err != nil { // Retourner une erreur dans le Row return &sql.Row{} } return stmt.QueryRowContext(ctx, args...) } // Initialize prépare toutes les requêtes fréquemment utilisées func (psm *PreparedStatementManager) Initialize(ctx context.Context) error { psm.logger.Info("Initializing prepared statements...") // Requêtes utilisateur statements := map[string]string{ "get_user_by_id": ` SELECT id, username, email, password_hash, created_at, updated_at, deleted_at FROM users WHERE id = $1 AND deleted_at IS NULL`, "get_user_by_email": ` SELECT id, username, email, password_hash, created_at, updated_at, deleted_at FROM users WHERE email = $1 AND deleted_at IS NULL`, "get_user_by_username": ` SELECT id, username, email, password_hash, created_at, updated_at, deleted_at FROM users WHERE username = $1 AND deleted_at IS NULL`, "create_user": ` INSERT INTO users (username, email, password_hash, created_at, updated_at) VALUES ($1, $2, $3, $4, $5) RETURNING id`, "update_user": ` UPDATE users SET username = $2, email = $3, updated_at = $4 WHERE id = $1 AND deleted_at IS NULL`, "delete_user": ` UPDATE users SET deleted_at = $2 WHERE id = $1`, // Requêtes de session "get_session_by_token": ` SELECT id, user_id, token, created_at, expires_at, ip_address, user_agent, is_valid FROM sessions WHERE token = $1 AND expires_at > $2 AND is_valid = true`, "create_session": ` INSERT INTO sessions (user_id, token, created_at, expires_at, ip_address, user_agent) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`, "revoke_session": ` UPDATE sessions SET is_valid = false, revoked_at = $2 WHERE token = $1`, "revoke_user_sessions": ` UPDATE sessions SET is_valid = false, revoked_at = $2 WHERE user_id = $1 AND is_valid = true`, "cleanup_expired_sessions": ` DELETE FROM sessions WHERE expires_at < $1`, // Requêtes de messages "get_messages_by_room": ` SELECT m.id, m.room_id, m.user_id, m.content, m.type, m.parent_id, m.is_edited, m.is_deleted, m.created_at, m.updated_at, u.username, u.email FROM messages m JOIN users u ON m.user_id = u.id WHERE m.room_id = $1 AND m.created_at < $2 ORDER BY m.created_at DESC LIMIT $3`, "create_message": ` INSERT INTO messages (room_id, user_id, content, type, parent_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, "update_message": ` UPDATE messages SET content = $2, is_edited = true, updated_at = $3 WHERE id = $1 AND user_id = $4`, "delete_message": ` UPDATE messages SET is_deleted = true, updated_at = $2 WHERE id = $1`, // Requêtes de tracks "get_track_by_id": ` SELECT id, user_id, title, artist, duration, file_path, file_size, mime_type, status, created_at, updated_at FROM tracks WHERE id = $1 AND status = 'active'`, "get_user_tracks": ` SELECT id, user_id, title, artist, duration, file_path, file_size, mime_type, status, created_at, updated_at FROM tracks WHERE user_id = $1 AND created_at < $2 AND status = 'active' ORDER BY created_at DESC LIMIT $3`, "create_track": ` INSERT INTO tracks (user_id, title, artist, duration, file_path, file_size, mime_type, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`, "update_track": ` UPDATE tracks SET title = $2, artist = $3, updated_at = $4 WHERE id = $1 AND user_id = $5`, "delete_track": ` UPDATE tracks SET status = 'deleted', updated_at = $2 WHERE id = $1`, // Requêtes de rooms "get_room_by_id": ` SELECT id, name, description, type, is_private, created_by, created_at, updated_at FROM rooms WHERE id = $1`, "get_user_rooms": ` SELECT r.id, r.name, r.description, r.type, r.is_private, r.created_by, r.created_at, r.updated_at FROM rooms r JOIN room_users ru ON r.id = ru.room_id WHERE ru.user_id = $1 AND r.created_at < $2 ORDER BY r.created_at DESC LIMIT $3`, "create_room": ` INSERT INTO rooms (name, description, type, is_private, created_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, "add_user_to_room": ` INSERT INTO room_users (room_id, user_id, created_at) VALUES ($1, $2, $3) ON CONFLICT (room_id, user_id) DO NOTHING`, "remove_user_from_room": ` DELETE FROM room_users WHERE room_id = $1 AND user_id = $2`, // Requêtes d'audit "create_audit_log": ` INSERT INTO audit_logs (user_id, action, entity_type, entity_id, ip_address, user_agent, details, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`, "get_audit_logs": ` SELECT id, user_id, action, entity_type, entity_id, ip_address, user_agent, details, created_at FROM audit_logs WHERE user_id = $1 AND created_at < $2 ORDER BY created_at DESC LIMIT $3`, // Requêtes de recherche "search_tracks": ` SELECT id, user_id, title, artist, duration, file_path, file_size, mime_type, status, created_at, updated_at, ts_rank(to_tsvector('english', title || ' ' || artist), plainto_tsquery('english', $1)) as rank FROM tracks WHERE status = 'active' AND to_tsvector('english', title || ' ' || artist) @@ plainto_tsquery('english', $1) ORDER BY rank DESC, created_at DESC LIMIT $2`, "search_messages": ` SELECT m.id, m.room_id, m.user_id, m.content, m.type, m.created_at, u.username, u.email, ts_rank(to_tsvector('english', m.content), plainto_tsquery('english', $1)) as rank FROM messages m JOIN users u ON m.user_id = u.id WHERE m.room_id = $2 AND to_tsvector('english', m.content) @@ plainto_tsquery('english', $1) ORDER BY rank DESC, m.created_at DESC LIMIT $3`, } // Préparer toutes les requêtes for name, query := range statements { if err := psm.Prepare(ctx, name, query); err != nil { psm.logger.Error("Failed to prepare statement", zap.String("name", name), zap.Error(err)) return err } } psm.logger.Info("All prepared statements initialized successfully", zap.Int("count", len(statements))) return nil } // Close ferme toutes les requêtes préparées func (psm *PreparedStatementManager) Close() error { psm.mutex.Lock() defer psm.mutex.Unlock() var lastErr error for name, stmt := range psm.statements { if err := stmt.Stmt.Close(); err != nil { psm.logger.Error("Failed to close statement", zap.String("name", name), zap.Error(err)) lastErr = err } } // Vider le cache psm.statements = make(map[string]*PreparedStatement) psm.logger.Info("All prepared statements closed") return lastErr } // GetStats retourne les statistiques des requêtes préparées func (psm *PreparedStatementManager) GetStats() map[string]interface{} { psm.mutex.RLock() defer psm.mutex.RUnlock() stats := map[string]interface{}{ "total_statements": len(psm.statements), "statements": make([]string, 0, len(psm.statements)), } for name := range psm.statements { stats["statements"] = append(stats["statements"].([]string), name) } return stats } // RefreshStatement rafraîchit une requête préparée (utile après reconnexion DB) func (psm *PreparedStatementManager) RefreshStatement(ctx context.Context, name string) error { psm.mutex.Lock() defer psm.mutex.Unlock() stmt, exists := psm.statements[name] if !exists { return fmt.Errorf("statement %s not found", name) } // Fermer l'ancienne requête if err := stmt.Stmt.Close(); err != nil { psm.logger.Warn("Failed to close old statement", zap.String("name", name), zap.Error(err)) } // Préparer la nouvelle requête newStmt, err := psm.db.PrepareContext(ctx, stmt.Query) if err != nil { return fmt.Errorf("failed to refresh statement %s: %w", name, err) } stmt.Stmt = newStmt psm.logger.Debug("Statement refreshed", zap.String("name", name)) return nil } // RefreshAllStatements rafraîchit toutes les requêtes préparées func (psm *PreparedStatementManager) RefreshAllStatements(ctx context.Context) error { psm.mutex.Lock() defer psm.mutex.Unlock() var lastErr error for name, stmt := range psm.statements { // Fermer l'ancienne requête if err := stmt.Stmt.Close(); err != nil { psm.logger.Warn("Failed to close old statement", zap.String("name", name), zap.Error(err)) } // Préparer la nouvelle requête newStmt, err := psm.db.PrepareContext(ctx, stmt.Query) if err != nil { psm.logger.Error("Failed to refresh statement", zap.String("name", name), zap.Error(err)) lastErr = err continue } stmt.Stmt = newStmt } psm.logger.Info("All statements refreshed") return lastErr }