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    /// A dirty migration cannot be repaired without violating its execution
145    /// safety contract.
146    #[error("Migration `{tag}` cannot be repaired safely: {reason}")]
147    UnsafeMigrationRepair {
148        /// Migration tag recorded in the tracking table.
149        tag: CompactString,
150        /// Safety requirement that prevents automatic repair.
151        reason: CompactString,
152    },
153
154    /// The selected adapter cannot provide a migration's required execution
155    /// semantics.
156    #[error("{adapter} cannot execute this migration: {requirement}")]
157    UnsupportedMigrationExecution {
158        /// Runtime adapter that cannot provide the required semantics.
159        adapter: CompactString,
160        /// Migration execution capability the adapter does not provide.
161        requirement: CompactString,
162    },
163
164    /// Generic error
165    #[error("Database error: {0}")]
166    Other(compact_str::CompactString),
167
168    /// Error returned by a wire driver whose concrete type is intentionally
169    /// kept out of drizzle-core's public dependency graph.
170    #[cfg(feature = "driver-error")]
171    #[error("{driver} error: {source}")]
172    Driver {
173        /// Stable adapter name used in diagnostics.
174        driver: CompactString,
175        /// Original driver error, retained as the error source.
176        #[source]
177        source: Box<dyn std::error::Error + Send + Sync>,
178    },
179
180    /// Error from a higher-level subsystem kept as a typed source.
181    #[cfg(feature = "driver-error")]
182    #[error("{context}: {source}")]
183    External {
184        /// Operation or subsystem that failed.
185        context: CompactString,
186        /// Original error, retained as the error source.
187        #[source]
188        source: Box<dyn std::error::Error + Send + Sync>,
189    },
190
191    /// Rusqlite specific errors
192    #[cfg(feature = "rusqlite")]
193    #[error("Rusqlite error: {0}")]
194    Rusqlite(#[from] rusqlite::Error),
195
196    /// Turso specific errors
197    #[cfg(feature = "turso")]
198    #[error("Turso error: {0}")]
199    Turso(#[from] turso::Error),
200
201    /// `LibSQL` specific errors
202    #[cfg(feature = "libsql")]
203    #[error("LibSQL error: {0}")]
204    LibSQL(#[from] libsql::Error),
205
206    /// Postgres specific errors
207    #[cfg(feature = "tokio-postgres")]
208    #[error("Postgres error: {0}")]
209    Postgres(#[from] tokio_postgres::Error),
210
211    #[cfg(all(feature = "postgres-sync", not(feature = "tokio-postgres")))]
212    #[error("Postgres error: {0}")]
213    Postgres(#[from] postgres::Error),
214
215    /// UUID parsing error
216    #[cfg(feature = "uuid")]
217    #[error("UUID error: {0}")]
218    UuidError(#[from] uuid::Error),
219
220    /// JSON serialization/deserialization error
221    #[cfg(feature = "serde")]
222    #[error("JSON error: {0}")]
223    JsonError(#[from] serde_json::Error),
224
225    /// Infallible conversion error (should never happen)
226    #[error("Infallible conversion error")]
227    Infallible(#[from] core::convert::Infallible),
228}
229
230impl DrizzleError {
231    /// Wraps a concrete wire-driver error without exposing its type in the
232    /// public error enum.
233    #[cfg(feature = "driver-error")]
234    pub fn driver(
235        driver: impl Into<CompactString>,
236        source: impl std::error::Error + Send + Sync + 'static,
237    ) -> Self {
238        Self::Driver {
239            driver: driver.into(),
240            source: Box::new(source),
241        }
242    }
243
244    /// Wraps an external subsystem error while preserving its source chain.
245    #[cfg(feature = "driver-error")]
246    pub fn external(
247        context: impl Into<CompactString>,
248        source: impl std::error::Error + Send + Sync + 'static,
249    ) -> Self {
250        Self::External {
251            context: context.into(),
252            source: Box::new(source),
253        }
254    }
255}
256
257/// Result type for database operations
258pub type Result<T> = core::result::Result<T, DrizzleError>;
259
260/// Attaches SQL and parameter context to a database error.
261pub trait ResultExt<T> {
262    /// Attach SQL and parameter context lazily on the error path.
263    fn with_query<F>(self, ctx: F) -> Result<T>
264    where
265        F: FnOnce() -> QueryContext;
266}
267
268impl<T, E> ResultExt<T> for core::result::Result<T, E>
269where
270    E: Into<DrizzleError>,
271{
272    fn with_query<F>(self, ctx: F) -> Result<T>
273    where
274        F: FnOnce() -> QueryContext,
275    {
276        self.map_err(|error| {
277            let source = error.into();
278            match source {
279                DrizzleError::QueryFailed { .. } => source,
280                other => DrizzleError::QueryFailed {
281                    ctx: Box::new(ctx()),
282                    source: Box::new(other),
283                },
284            }
285        })
286    }
287}
288
289#[cfg(all(test, feature = "driver-error"))]
290mod tests {
291    use super::*;
292    use std::error::Error as _;
293
294    #[test]
295    fn external_errors_keep_their_source() {
296        let error =
297            DrizzleError::external("schema diff", std::io::Error::other("invalid snapshot"));
298
299        assert_eq!(error.to_string(), "schema diff: invalid snapshot");
300        assert_eq!(
301            error.source().map(ToString::to_string),
302            Some("invalid snapshot".to_string())
303        );
304    }
305}