Skip to main content

drizzle_core/
error.rs

1//! Error types for drizzle-core
2
3use crate::prelude::{Box, String, ToString, Vec, format};
4use compact_str::CompactString;
5use thiserror::Error;
6
7const MAX_CONTEXT_PARAMS: usize = 32;
8const MAX_CONTEXT_PARAM_CHARS: usize = 128;
9
10/// SQL and parameter context captured when a query fails.
11#[derive(Debug, Clone)]
12pub struct QueryContext {
13    /// Rendered SQL statement.
14    pub sql: CompactString,
15    /// Debug-rendered parameter values, truncated to keep errors bounded.
16    pub params: Box<[CompactString]>,
17    /// Total number of parameters, including any omitted from `params`.
18    pub param_count: usize,
19}
20
21impl QueryContext {
22    /// Builds an owned query context from borrowed parameters.
23    pub fn new<V: core::fmt::Debug>(sql: &str, params: &[&V]) -> Self {
24        let rendered = params
25            .iter()
26            .take(MAX_CONTEXT_PARAMS)
27            .map(|param| truncate_param(format!("{param:?}")))
28            .collect::<Vec<_>>()
29            .into_boxed_slice();
30
31        Self {
32            sql: sql.into(),
33            params: rendered,
34            param_count: params.len(),
35        }
36    }
37
38    fn params_display(&self) -> String {
39        if self.param_count == 0 {
40            return "[]".to_string();
41        }
42
43        let mut rendered = String::from("[");
44        for (index, param) in self.params.iter().enumerate() {
45            if index > 0 {
46                rendered.push_str(", ");
47            }
48            rendered.push_str(param.as_str());
49        }
50        if self.param_count > self.params.len() {
51            if !self.params.is_empty() {
52                rendered.push_str(", ");
53            }
54            rendered.push_str("...");
55            rendered.push_str(&format!("(+{} more)", self.param_count - self.params.len()));
56        }
57        rendered.push(']');
58        rendered
59    }
60}
61
62fn truncate_param(mut value: String) -> CompactString {
63    if value.chars().count() <= MAX_CONTEXT_PARAM_CHARS {
64        return value.into();
65    }
66
67    let mut truncated = String::new();
68    for ch in value.drain(..).take(MAX_CONTEXT_PARAM_CHARS) {
69        truncated.push(ch);
70    }
71    truncated.push_str("...");
72    truncated.into()
73}
74
75/// Core error type for drizzle operations
76#[derive(Debug, Error)]
77pub enum DrizzleError {
78    /// Error executing a query
79    #[error("Execution error: {0}")]
80    ExecutionError(compact_str::CompactString),
81
82    /// Error preparing a statement
83    #[error("Prepare error: {0}")]
84    PrepareError(compact_str::CompactString),
85
86    /// No rows returned when at least one was expected
87    #[error("No rows found")]
88    NotFound,
89
90    /// Error with transaction
91    #[error("Transaction error: {0}")]
92    TransactionError(compact_str::CompactString),
93
94    /// Error mapping data
95    #[error("Mapping error: {0}")]
96    Mapping(compact_str::CompactString),
97
98    /// Error in statement
99    #[error("Statement error: {0}")]
100    Statement(compact_str::CompactString),
101
102    /// Error in query
103    #[error("Query error: {0}")]
104    Query(CompactString),
105
106    /// Query error with rendered SQL and parameter context.
107    #[error("{source}\n  sql: {sql}\n  params: {params}", sql = .ctx.sql, params = .ctx.params_display())]
108    QueryFailed {
109        /// Captured SQL and parameter context.
110        ctx: Box<QueryContext>,
111        /// Original error.
112        #[source]
113        source: Box<DrizzleError>,
114    },
115
116    /// Error converting parameters
117    #[error("Parameter conversion error: {0}")]
118    ParameterError(compact_str::CompactString),
119
120    /// Integer conversion error
121    #[error("Integer conversion error: {0}")]
122    TryFromInt(#[from] core::num::TryFromIntError),
123
124    /// Parse int error
125    #[error("Parse int error: {0}")]
126    ParseInt(#[from] core::num::ParseIntError),
127
128    /// Parse float error
129    #[error("Parse float error: {0}")]
130    ParseFloat(#[from] core::num::ParseFloatError),
131
132    /// Parse bool error
133    #[error("Parse bool error: {0}")]
134    ParseBool(#[from] core::str::ParseBoolError),
135
136    /// Type conversion error
137    #[error("Type conversion error: {0}")]
138    ConversionError(compact_str::CompactString),
139
140    /// Schema error (e.g. cycle in table dependencies)
141    #[error("Schema error: {0}")]
142    Schema(compact_str::CompactString),
143
144    /// Generic error
145    #[error("Database error: {0}")]
146    Other(compact_str::CompactString),
147
148    /// Rusqlite specific errors
149    #[cfg(feature = "rusqlite")]
150    #[error("Rusqlite error: {0}")]
151    Rusqlite(#[from] rusqlite::Error),
152
153    /// Turso specific errors
154    #[cfg(feature = "turso")]
155    #[error("Turso error: {0}")]
156    Turso(#[from] turso::Error),
157
158    /// `LibSQL` specific errors
159    #[cfg(feature = "libsql")]
160    #[error("LibSQL error: {0}")]
161    LibSQL(#[from] libsql::Error),
162
163    /// Postgres specific errors
164    #[cfg(feature = "tokio-postgres")]
165    #[error("Postgres error: {0}")]
166    Postgres(#[from] tokio_postgres::Error),
167
168    #[cfg(all(feature = "postgres-sync", not(feature = "tokio-postgres")))]
169    #[error("Postgres error: {0}")]
170    Postgres(#[from] postgres::Error),
171
172    /// UUID parsing error
173    #[cfg(feature = "uuid")]
174    #[error("UUID error: {0}")]
175    UuidError(#[from] uuid::Error),
176
177    /// JSON serialization/deserialization error
178    #[cfg(feature = "serde")]
179    #[error("JSON error: {0}")]
180    JsonError(#[from] serde_json::Error),
181
182    /// Infallible conversion error (should never happen)
183    #[error("Infallible conversion error")]
184    Infallible(#[from] core::convert::Infallible),
185}
186
187/// Result type for database operations
188pub type Result<T> = core::result::Result<T, DrizzleError>;
189
190/// Attaches SQL and parameter context to a database error.
191pub trait ResultExt<T> {
192    /// Attach SQL and parameter context lazily on the error path.
193    fn with_query<F>(self, ctx: F) -> Result<T>
194    where
195        F: FnOnce() -> QueryContext;
196}
197
198impl<T, E> ResultExt<T> for core::result::Result<T, E>
199where
200    E: Into<DrizzleError>,
201{
202    fn with_query<F>(self, ctx: F) -> Result<T>
203    where
204        F: FnOnce() -> QueryContext,
205    {
206        self.map_err(|error| {
207            let source = error.into();
208            match source {
209                DrizzleError::QueryFailed { .. } => source,
210                other => DrizzleError::QueryFailed {
211                    ctx: Box::new(ctx()),
212                    source: Box::new(other),
213                },
214            }
215        })
216    }
217}