QoreDB parle à seize moteurs de bases de données, et chacun d'eux échoue à sa manière. sqlx renvoie ses propres erreurs, tiberius pour SQL Server les siennes, le driver mongodb a encore un autre vocabulaire. Si ces erreurs remontaient telles quelles jusqu'à l'interface, chaque écran devrait connaître les modes de défaillance de chaque librairie. QoreDB fait l'inverse : un seul type d'erreur, une traduction au niveau du driver, et un nettoyage systématique avant la sortie du process.
Un seul type d'erreur pour tous les moteurs
Le type central est défini dans src-tauri/crates/qore-core/src/error.rs. C'est une énumération EngineError qui dérive thiserror::Error pour l'affichage, et Serialize / Deserialize pour traverser la frontière Rust vers le frontend. Chaque famille d'échec — connexion, authentification, syntaxe, exécution, timeout, tunnel SSH, TLS — a sa propre variante.
#[derive(Debug, Error, Serialize, Deserialize)]
pub enum EngineError {
#[error("Connection failed: {message}")]
ConnectionFailed { message: String },
#[error("Operation timed out after {timeout_ms}ms")]
Timeout { timeout_ms: u64 },
#[error("Too many concurrent queries ({current}/{limit})")]
TooManyConcurrentQueries { current: u32, limit: u32 },
#[error("Feature not supported: {message}")]
NotSupported { message: String },
// ...
}Le point important est que les variantes portent des champs nommés, pas de simples chaînes. Timeout transporte un timeout_ms: u64, TooManyConcurrentQueries conserve le compteur courant et la limite, ResultTooLarge le nombre de lignes et le plafond. L'information reste typée : le frontend peut décider quoi afficher sans réanalyser un texte libre.
Des constructeurs plutôt que des littéraux
Construire une variante à la main est verbeux. Le module expose donc des fonctions de fabrique qui acceptent tout ce qui se convertit en chaîne via impl Into<String>. Le code appelant reste court, et le nom de la fabrique documente l'intention.
pub fn execution_error(msg: impl Into<String>) -> Self {
Self::ExecutionError { message: msg.into() }
}
pub fn too_many_queries(current: u32, limit: u32) -> Self {
Self::TooManyConcurrentQueries { current, limit }
}Chaque driver traduit ses erreurs natives
La traduction se fait au plus près de la source, dans le driver lui-même. Le pattern est toujours le même : un .map_err(...) qui enveloppe l'erreur native dans la bonne variante d'EngineError. Une connexion MongoDB qui échoue devient un ConnectionFailed, une requête SQL rejetée devient un ExecutionError.
// drivers/mongodb.rs
let mut options = ClientOptions::parse(&conn_str)
.await
.map_err(|e| EngineError::connection_failed(e.to_string()))?;
// drivers/postgres.rs
let current_db: (String,) = sqlx::query_as("SELECT current_database()")
.fetch_one(pool)
.await
.map_err(|e| EngineError::execution_error(e.to_string()))?;C'est cohérent avec le parti pris de QoreDB : on n'émule pas les moteurs, mais on unifie la façon dont ils rapportent leurs échecs. Le reste de l'application ne voit jamais un type d'erreur propre à sqlx ou au driver Mongo ; il ne voit que des EngineError.
EngineResult et la propagation avec ?
Pour éviter de répéter Result<T, EngineError> partout, le module définit l'alias EngineResult<T>. Combiné à l'opérateur ?, il rend la propagation implicite : chaque appel faillible remonte l'erreur au niveau supérieur sans code intermédiaire. Une couche de service enveloppe ce type dans ServiceError, avec une conversion From<EngineError> pour que la remontée reste automatique.
pub type EngineResult<T> = Result<T, EngineError>;
// qore-service/src/error.rs
pub enum ServiceError {
Engine(EngineError),
Message(String),
}
impl From<EngineError> for ServiceError {
fn from(e: EngineError) -> Self {
ServiceError::Engine(e)
}
}Nettoyer le message avant qu'il ne sorte du process
Un message d'erreur brut est un risque de fuite. Une chaîne de connexion peut contenir postgres://user:pass@host, un chemin absolu révèle l'arborescence de la machine. Avant d'exposer un message, QoreDB le passe par sanitize_error_message, qui applique une série d'expressions régulières compilées une seule fois via OnceLock. Les identifiants dans les URL sont masqués, les paramètres password= remplacés, les chemins Unix et Windows absolus réduits à un marqueur.
// postgres://user:pass@host -> postgres://***@host
(Regex::new(r"(?i)((?:postgres|mysql|mongodb|redis|rediss)://)([^@]+)@").unwrap(),
"${1}***@"),
(Regex::new(r"(?i)(password|passwd|pwd)\s*=\s*\S+").unwrap(),
"${1}=***"),
// Chemins absolus Unix ou Windows.
(Regex::new(r"(/(?:Users|home|tmp|var|etc)/\S+|[A-Z]:\\[^\s:]+)").unwrap(),
"[path]"),La frontière Tauri ne laisse passer que du texte nettoyé
Les commandes Tauri, qui sont le seul point de contact entre le backend Rust et l'interface, renvoient des chaînes déjà sanitisées. Le motif .map_err(|e| e.sanitized_message())? revient partout, de la résolution d'une connexion sauvegardée à l'export de schéma. Le test de connexion, par exemple, place le message nettoyé dans un champ error de sa réponse plutôt que de faire échouer la commande.
Err(e) => Ok(DriverInfoResponse {
success: false,
driver: None,
error: Some(e.sanitized_message()),
}),Un parti pris cohérent de bout en bout
Le chemin d'une erreur dans QoreDB est linéaire : le driver traduit l'échec natif en EngineError typé, la propagation remonte via EngineResult et l'opérateur ?, et la frontière avec le frontend n'expose qu'un message nettoyé. Le même soin qui pousse QoreDB à refuser d'émuler les fonctionnalités absentes d'un moteur se retrouve ici : plutôt que de laisser fuiter un détail sensible, on préfère un message court et sûr. Une erreur reste une information utile — jamais une porte ouverte.
Restez informé des nouveautés
Rejoignez notre newsletter pour recevoir les mises à jour majeures, les nouveaux drivers et nos coulisses techniques.

