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    /// Rusqlite specific errors
169    #[cfg(feature = "rusqlite")]
170    #[error("Rusqlite error: {0}")]
171    Rusqlite(#[from] rusqlite::Error),
172
173    /// Turso specific errors
174    #[cfg(feature = "turso")]
175    #[error("Turso error: {0}")]
176    Turso(#[from] turso::Error),
177
178    /// `LibSQL` specific errors
179    #[cfg(feature = "libsql")]
180    #[error("LibSQL error: {0}")]
181    LibSQL(#[from] libsql::Error),
182
183    /// Postgres specific errors
184    #[cfg(feature = "tokio-postgres")]
185    #[error("Postgres error: {0}")]
186    Postgres(#[from] tokio_postgres::Error),
187
188    #[cfg(all(feature = "postgres-sync", not(feature = "tokio-postgres")))]
189    #[error("Postgres error: {0}")]
190    Postgres(#[from] postgres::Error),
191
192    /// UUID parsing error
193    #[cfg(feature = "uuid")]
194    #[error("UUID error: {0}")]
195    UuidError(#[from] uuid::Error),
196
197    /// JSON serialization/deserialization error
198    #[cfg(feature = "serde")]
199    #[error("JSON error: {0}")]
200    JsonError(#[from] serde_json::Error),
201
202    /// Infallible conversion error (should never happen)
203    #[error("Infallible conversion error")]
204    Infallible(#[from] core::convert::Infallible),
205}
206
207/// Result type for database operations
208pub type Result<T> = core::result::Result<T, DrizzleError>;
209
210/// Attaches SQL and parameter context to a database error.
211pub trait ResultExt<T> {
212    /// Attach SQL and parameter context lazily on the error path.
213    fn with_query<F>(self, ctx: F) -> Result<T>
214    where
215        F: FnOnce() -> QueryContext;
216}
217
218impl<T, E> ResultExt<T> for core::result::Result<T, E>
219where
220    E: Into<DrizzleError>,
221{
222    fn with_query<F>(self, ctx: F) -> Result<T>
223    where
224        F: FnOnce() -> QueryContext,
225    {
226        self.map_err(|error| {
227            let source = error.into();
228            match source {
229                DrizzleError::QueryFailed { .. } => source,
230                other => DrizzleError::QueryFailed {
231                    ctx: Box::new(ctx()),
232                    source: Box::new(other),
233                },
234            }
235        })
236    }
237}