Skip to main content

drizzle_core/
error.rs

1//! The error type shared by every Drizzle crate and driver.
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/// The SQL and parameters of a failed query, kept for the error message.
11///
12/// Attach it with [`ResultExt::with_query`].
13#[derive(Debug, Clone)]
14pub struct QueryContext {
15    /// Rendered SQL statement.
16    pub sql: CompactString,
17    /// Debug-rendered parameter values, truncated to keep errors bounded.
18    pub params: Box<[CompactString]>,
19    /// Total number of parameters, including any omitted from `params`.
20    pub param_count: usize,
21}
22
23impl QueryContext {
24    /// Captures `sql` and the `Debug` form of each parameter.
25    ///
26    /// At most 32 parameters are kept, each cut to 128 characters, so the
27    /// error stays small.
28    pub fn new<V: core::fmt::Debug>(sql: &str, params: &[&V]) -> Self {
29        let rendered = params
30            .iter()
31            .take(MAX_CONTEXT_PARAMS)
32            .map(|param| truncate_param(format!("{param:?}")))
33            .collect::<Vec<_>>()
34            .into_boxed_slice();
35
36        Self {
37            sql: sql.into(),
38            params: rendered,
39            param_count: params.len(),
40        }
41    }
42
43    fn params_display(&self) -> String {
44        if self.param_count == 0 {
45            return "[]".to_string();
46        }
47
48        let mut rendered = String::from("[");
49        for (index, param) in self.params.iter().enumerate() {
50            if index > 0 {
51                rendered.push_str(", ");
52            }
53            rendered.push_str(param.as_str());
54        }
55        if self.param_count > self.params.len() {
56            if !self.params.is_empty() {
57                rendered.push_str(", ");
58            }
59            rendered.push_str("...");
60            rendered.push_str(&format!("(+{} more)", self.param_count - self.params.len()));
61        }
62        rendered.push(']');
63        rendered
64    }
65}
66
67fn truncate_param(mut value: String) -> CompactString {
68    if value.chars().count() <= MAX_CONTEXT_PARAM_CHARS {
69        return value.into();
70    }
71
72    let mut truncated = String::new();
73    for ch in value.drain(..).take(MAX_CONTEXT_PARAM_CHARS) {
74        truncated.push(ch);
75    }
76    truncated.push_str("...");
77    truncated.into()
78}
79
80/// Every error a Drizzle query, conversion or migration can return.
81///
82/// Driver errors are wrapped in a variant for that driver when its feature
83/// is enabled. [`DrizzleError::QueryFailed`] adds the failing SQL and
84/// parameters to another error.
85#[derive(Debug, Error)]
86pub enum DrizzleError {
87    /// The statement failed to execute.
88    #[error("Execution error: {0}")]
89    ExecutionError(compact_str::CompactString),
90
91    /// The statement failed to prepare.
92    #[error("Prepare error: {0}")]
93    PrepareError(compact_str::CompactString),
94
95    /// No row was returned where one was expected (for example by `.get()`).
96    #[error("No rows found")]
97    NotFound,
98
99    /// A transaction could not begin, commit or roll back.
100    #[error("Transaction error: {0}")]
101    TransactionError(compact_str::CompactString),
102
103    /// A row could not be mapped to the target type.
104    #[error("Mapping error: {0}")]
105    Mapping(compact_str::CompactString),
106
107    /// The statement is invalid.
108    #[error("Statement error: {0}")]
109    Statement(compact_str::CompactString),
110
111    /// The query is invalid.
112    #[error("Query error: {0}")]
113    Query(CompactString),
114
115    /// Another error, with the SQL and parameters of the query that caused
116    /// it.
117    #[error("{source}\n  sql: {sql}\n  params: {params}", sql = .ctx.sql, params = .ctx.params_display())]
118    QueryFailed {
119        /// Captured SQL and parameter context.
120        ctx: Box<QueryContext>,
121        /// Original error.
122        #[source]
123        source: Box<DrizzleError>,
124    },
125
126    /// Parameters could not be bound: missing, duplicate or unexpected
127    /// bindings, or a value that could not be converted.
128    #[error("Parameter conversion error: {0}")]
129    ParameterError(compact_str::CompactString),
130
131    /// An integer did not fit the target type.
132    #[error("Integer conversion error: {0}")]
133    TryFromInt(#[from] core::num::TryFromIntError),
134
135    /// Text could not be parsed as an integer.
136    #[error("Parse int error: {0}")]
137    ParseInt(#[from] core::num::ParseIntError),
138
139    /// Text could not be parsed as a float.
140    #[error("Parse float error: {0}")]
141    ParseFloat(#[from] core::num::ParseFloatError),
142
143    /// Text could not be parsed as a boolean.
144    #[error("Parse bool error: {0}")]
145    ParseBool(#[from] core::str::ParseBoolError),
146
147    /// A value could not be converted to the requested type.
148    #[error("Type conversion error: {0}")]
149    ConversionError(compact_str::CompactString),
150
151    /// The schema is invalid (for example a cycle in table dependencies).
152    #[error("Schema error: {0}")]
153    Schema(compact_str::CompactString),
154
155    /// A dirty migration cannot be repaired without violating its execution
156    /// safety contract.
157    #[error("Migration `{tag}` cannot be repaired safely: {reason}")]
158    UnsafeMigrationRepair {
159        /// Migration tag recorded in the tracking table.
160        tag: CompactString,
161        /// Safety requirement that prevents automatic repair.
162        reason: CompactString,
163    },
164
165    /// The selected adapter cannot provide a migration's required execution
166    /// semantics.
167    #[error("{adapter} cannot execute this migration: {requirement}")]
168    UnsupportedMigrationExecution {
169        /// Runtime adapter that cannot provide the required semantics.
170        adapter: CompactString,
171        /// Migration execution capability the adapter does not provide.
172        requirement: CompactString,
173    },
174
175    /// Any other error.
176    #[error("Database error: {0}")]
177    Other(compact_str::CompactString),
178
179    /// Error returned by a wire driver whose concrete type is intentionally
180    /// kept out of drizzle-core's public dependency graph.
181    #[cfg(feature = "driver-error")]
182    #[error("{driver} error: {source}")]
183    Driver {
184        /// Stable adapter name used in diagnostics.
185        driver: CompactString,
186        /// Original driver error, retained as the error source.
187        #[source]
188        source: Box<dyn std::error::Error + Send + Sync>,
189    },
190
191    /// Error from a higher-level subsystem kept as a typed source.
192    #[cfg(feature = "driver-error")]
193    #[error("{context}: {source}")]
194    External {
195        /// Operation or subsystem that failed.
196        context: CompactString,
197        /// Original error, retained as the error source.
198        #[source]
199        source: Box<dyn std::error::Error + Send + Sync>,
200    },
201
202    /// An error from `rusqlite`.
203    #[cfg(feature = "rusqlite")]
204    #[error("Rusqlite error: {0}")]
205    Rusqlite(#[from] rusqlite::Error),
206
207    /// An error from `turso`.
208    #[cfg(feature = "turso")]
209    #[error("Turso error: {0}")]
210    Turso(#[from] turso::Error),
211
212    /// An error from `libsql`.
213    #[cfg(feature = "libsql")]
214    #[error("LibSQL error: {0}")]
215    LibSQL(#[from] libsql::Error),
216
217    /// An error from `tokio-postgres`.
218    #[cfg(feature = "tokio-postgres")]
219    #[error("Postgres error: {0}")]
220    Postgres(#[from] tokio_postgres::Error),
221
222    /// An error from `postgres`.
223    #[cfg(all(feature = "postgres-sync", not(feature = "tokio-postgres")))]
224    #[error("Postgres error: {0}")]
225    Postgres(#[from] postgres::Error),
226
227    /// A UUID could not be parsed.
228    #[cfg(feature = "uuid")]
229    #[error("UUID error: {0}")]
230    UuidError(#[from] uuid::Error),
231
232    /// A JSON value could not be serialized or deserialized.
233    #[cfg(feature = "serde")]
234    #[error("JSON error: {0}")]
235    JsonError(#[from] serde_json::Error),
236
237    /// Never constructed; lets `?` work on infallible conversions.
238    #[error("Infallible conversion error")]
239    Infallible(#[from] core::convert::Infallible),
240}
241
242impl DrizzleError {
243    /// Wraps a concrete wire-driver error without exposing its type in the
244    /// public error enum.
245    #[cfg(feature = "driver-error")]
246    pub fn driver(
247        driver: impl Into<CompactString>,
248        source: impl std::error::Error + Send + Sync + 'static,
249    ) -> Self {
250        Self::Driver {
251            driver: driver.into(),
252            source: Box::new(source),
253        }
254    }
255
256    /// Wraps an external subsystem error while preserving its source chain.
257    #[cfg(feature = "driver-error")]
258    pub fn external(
259        context: impl Into<CompactString>,
260        source: impl std::error::Error + Send + Sync + 'static,
261    ) -> Self {
262        Self::External {
263            context: context.into(),
264            source: Box::new(source),
265        }
266    }
267}
268
269/// `Result` with [`DrizzleError`] as the error type.
270pub type Result<T> = core::result::Result<T, DrizzleError>;
271
272/// Adds the failing query's SQL and parameters to an error.
273///
274/// # Examples
275///
276/// ```
277/// use drizzle_core::error::{DrizzleError, QueryContext, ResultExt};
278///
279/// let result: Result<(), DrizzleError> = Err(DrizzleError::NotFound);
280/// let error = result
281///     .with_query(|| QueryContext::new("SELECT * FROM users WHERE id = ?", &[&7]))
282///     .unwrap_err();
283///
284/// assert_eq!(
285///     error.to_string(),
286///     "No rows found\n  sql: SELECT * FROM users WHERE id = ?\n  params: [7]",
287/// );
288/// ```
289pub trait ResultExt<T> {
290    /// On error, wraps it in [`DrizzleError::QueryFailed`] with the context
291    /// from `ctx`. `ctx` only runs on the error path, and an error that
292    /// already has context keeps it.
293    fn with_query<F>(self, ctx: F) -> Result<T>
294    where
295        F: FnOnce() -> QueryContext;
296}
297
298impl<T, E> ResultExt<T> for core::result::Result<T, E>
299where
300    E: Into<DrizzleError>,
301{
302    fn with_query<F>(self, ctx: F) -> Result<T>
303    where
304        F: FnOnce() -> QueryContext,
305    {
306        self.map_err(|error| {
307            let source = error.into();
308            match source {
309                DrizzleError::QueryFailed { .. } => source,
310                other => DrizzleError::QueryFailed {
311                    ctx: Box::new(ctx()),
312                    source: Box::new(other),
313                },
314            }
315        })
316    }
317}
318
319#[cfg(all(test, feature = "driver-error"))]
320mod tests {
321    use super::*;
322    use std::error::Error as _;
323
324    #[test]
325    fn external_errors_keep_their_source() {
326        let error =
327            DrizzleError::external("schema diff", std::io::Error::other("invalid snapshot"));
328
329        assert_eq!(error.to_string(), "schema diff: invalid snapshot");
330        assert_eq!(
331            error.source().map(ToString::to_string),
332            Some("invalid snapshot".to_string())
333        );
334    }
335}