veza/veza-backend-api/internal/database/prepared_statements.go
2025-12-03 20:29:37 +01:00

375 lines
11 KiB
Go

//! 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
}