use std::fmt;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DbNexusError {
#[cfg(feature = "permission")]
#[error(transparent)]
Permission(#[from] crate::domain::PermissionError),
#[cfg(feature = "permission")]
#[error(transparent)]
PermissionConfig(#[from] crate::domain::PermissionConfigError),
#[error("Unsupported database scheme in URL: {0}")]
UnsupportedDatabaseScheme(String),
}
impl crate::i18n::error_ext::LocalizedMsg for DbNexusError {
fn message_key(&self) -> &'static str {
match self {
#[cfg(feature = "permission")]
Self::Permission(err) => err.message_key(),
#[cfg(feature = "permission")]
Self::PermissionConfig(err) => err.message_key(),
Self::UnsupportedDatabaseScheme(_) => "nexus-unsupported-database",
}
}
fn message_args(&self) -> Vec<(&str, String)> {
match self {
#[cfg(feature = "permission")]
Self::Permission(err) => err.message_args(),
#[cfg(feature = "permission")]
Self::PermissionConfig(err) => err.message_args(),
Self::UnsupportedDatabaseScheme(scheme) => vec![("scheme", scheme.clone())],
}
}
}
pub type DbNexusResult<T> = Result<T, DbNexusError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
Permission,
InjectionRisk,
SyntaxError,
ShardConflict,
}
impl fmt::Display for ErrorCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Permission => write!(f, "Permission"),
Self::InjectionRisk => write!(f, "InjectionRisk"),
Self::SyntaxError => write!(f, "SyntaxError"),
Self::ShardConflict => write!(f, "ShardConflict"),
}
}
}
#[derive(Debug, Clone)]
pub struct QueryErrorReport {
pub category: ErrorCategory,
pub message: String,
pub suggestion: String,
pub table: Option<String>,
pub operation: Option<String>,
}
impl QueryErrorReport {
pub fn new(category: ErrorCategory, message: impl Into<String>, suggestion: impl Into<String>) -> Self {
Self {
category,
message: message.into(),
suggestion: suggestion.into(),
table: None,
operation: None,
}
}
pub fn with_table(mut self, table: impl Into<String>) -> Self {
self.table = Some(table.into());
self
}
pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
self.operation = Some(operation.into());
self
}
}
impl fmt::Display for QueryErrorReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {}\nSuggestion: {}",
self.category, self.message, self.suggestion
)?;
if let Some(table) = &self.table {
write!(f, "\nTable: {}", table)?;
}
if let Some(operation) = &self.operation {
write!(f, "\nOperation: {}", operation)?;
}
Ok(())
}
}
impl std::error::Error for QueryErrorReport {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
impl crate::i18n::error_ext::LocalizedMsg for ErrorCategory {
fn message_key(&self) -> &'static str {
match self {
Self::Permission => "error-category-permission",
Self::InjectionRisk => "error-category-injection-risk",
Self::SyntaxError => "error-category-syntax-error",
Self::ShardConflict => "error-category-shard-conflict",
}
}
}
impl crate::i18n::error_ext::LocalizedMsg for QueryErrorReport {
fn message_key(&self) -> &'static str {
"query-error-report"
}
fn message_args(&self) -> Vec<(&str, String)> {
let mut args = vec![
("category", self.category.to_string()),
("message", self.message.clone()),
("suggestion", self.suggestion.clone()),
];
if let Some(table) = &self.table {
args.push(("table", table.clone()));
}
if let Some(operation) = &self.operation {
args.push(("operation", operation.clone()));
}
args
}
}
impl From<DbNexusError> for QueryErrorReport {
fn from(err: DbNexusError) -> Self {
match err {
#[cfg(feature = "permission")]
DbNexusError::Permission(_) => QueryErrorReport::new(
ErrorCategory::Permission,
err.to_string(),
"Verify the role has the required permissions on the target table",
),
#[cfg(feature = "permission")]
DbNexusError::PermissionConfig(_) => QueryErrorReport::new(
ErrorCategory::Permission,
err.to_string(),
"Check the permission policy configuration file for syntax or schema errors",
),
DbNexusError::UnsupportedDatabaseScheme(_) => QueryErrorReport::new(
ErrorCategory::SyntaxError,
err.to_string(),
"Use a supported database scheme: sqlite, postgres, mysql, or duckdb",
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_category_display() {
assert_eq!(ErrorCategory::Permission.to_string(), "Permission");
assert_eq!(ErrorCategory::InjectionRisk.to_string(), "InjectionRisk");
assert_eq!(ErrorCategory::SyntaxError.to_string(), "SyntaxError");
assert_eq!(ErrorCategory::ShardConflict.to_string(), "ShardConflict");
}
#[test]
fn test_query_error_report_new() {
let report = QueryErrorReport::new(ErrorCategory::SyntaxError, "bad sql", "fix it");
assert_eq!(report.category, ErrorCategory::SyntaxError);
assert_eq!(report.message, "bad sql");
assert_eq!(report.suggestion, "fix it");
assert!(report.table.is_none());
assert!(report.operation.is_none());
}
#[test]
fn test_query_error_report_with_table() {
let report = QueryErrorReport::new(ErrorCategory::Permission, "denied", "check perms").with_table("users");
assert_eq!(report.table.as_deref(), Some("users"));
}
#[test]
fn test_query_error_report_with_operation() {
let report =
QueryErrorReport::new(ErrorCategory::InjectionRisk, "injection", "use params").with_operation("SELECT");
assert_eq!(report.operation.as_deref(), Some("SELECT"));
}
#[test]
fn test_query_error_report_display_full() {
let report = QueryErrorReport::new(
ErrorCategory::InjectionRisk,
"SQL contains UNION",
"Use parameterized queries",
)
.with_table("users")
.with_operation("SELECT");
let display = report.to_string();
assert!(display.contains("[InjectionRisk]"));
assert!(display.contains("SQL contains UNION"));
assert!(display.contains("Suggestion: Use parameterized queries"));
assert!(display.contains("Table: users"));
assert!(display.contains("Operation: SELECT"));
}
#[test]
fn test_query_error_report_display_minimal() {
let report = QueryErrorReport::new(ErrorCategory::SyntaxError, "bad sql", "fix it");
let display = report.to_string();
assert!(display.contains("[SyntaxError]"));
assert!(display.contains("bad sql"));
assert!(!display.contains("Table:"));
assert!(!display.contains("Operation:"));
}
#[test]
fn test_query_error_report_error_trait() {
let report = QueryErrorReport::new(ErrorCategory::SyntaxError, "bad sql", "fix it");
assert!(std::error::Error::source(&report).is_none());
}
#[test]
fn test_from_db_nexus_error_unsupported_scheme() {
let err = DbNexusError::UnsupportedDatabaseScheme("oracle://localhost".to_string());
let report = QueryErrorReport::from(err);
assert_eq!(report.category, ErrorCategory::SyntaxError);
assert!(report.message.contains("oracle"));
}
#[cfg(feature = "permission")]
#[test]
fn test_from_db_nexus_error_permission() {
let err = DbNexusError::Permission(crate::domain::PermissionError::Denied {
resource: "users".to_string(),
operation: "DELETE".to_string(),
});
let report = QueryErrorReport::from(err);
assert_eq!(report.category, ErrorCategory::Permission);
}
}