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, 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.
11///
12/// Attach it with [`ResultExt::with_query`]. The error message and the
13/// `Debug` output show the SQL and how many parameters were bound, never
14/// their values: they often hold personal data or secrets, and errors end up
15/// in logs (`.unwrap()` prints `Debug`). The values stay available in
16/// [`params`](Self::params).
17#[derive(Clone)]
18pub struct QueryContext {
19    /// Rendered SQL statement.
20    pub sql: CompactString,
21    /// Debug-rendered parameter values, truncated to keep errors bounded.
22    pub params: Box<[CompactString]>,
23    /// Total number of parameters, including any omitted from `params`.
24    pub param_count: usize,
25}
26
27impl QueryContext {
28    /// Captures `sql` and the `Debug` form of each parameter.
29    ///
30    /// At most 32 parameters are kept, each cut to 128 characters, so the
31    /// error stays small.
32    pub fn new<V: core::fmt::Debug>(sql: &str, params: &[&V]) -> Self {
33        let rendered = params
34            .iter()
35            .take(MAX_CONTEXT_PARAMS)
36            .map(|param| truncate_param(format!("{param:?}")))
37            .collect::<Vec<_>>()
38            .into_boxed_slice();
39
40        Self {
41            sql: sql.into(),
42            params: rendered,
43            param_count: params.len(),
44        }
45    }
46}
47
48impl core::fmt::Debug for QueryContext {
49    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
50        f.debug_struct("QueryContext")
51            .field("sql", &self.sql)
52            .field("param_count", &self.param_count)
53            .finish_non_exhaustive()
54    }
55}
56
57fn truncate_param(mut value: String) -> CompactString {
58    if value.chars().count() <= MAX_CONTEXT_PARAM_CHARS {
59        return value.into();
60    }
61
62    let mut truncated = String::new();
63    for ch in value.drain(..).take(MAX_CONTEXT_PARAM_CHARS) {
64        truncated.push(ch);
65    }
66    truncated.push_str("...");
67    truncated.into()
68}
69
70/// Every error a Drizzle query, conversion or migration can return.
71///
72/// Driver errors are wrapped in a variant for that driver when its feature
73/// is enabled. [`DrizzleError::QueryFailed`] adds the failing SQL and
74/// parameters to another error.
75#[derive(Debug, Error)]
76pub enum DrizzleError {
77    /// The statement failed to execute.
78    #[error("Execution error: {0}")]
79    ExecutionError(compact_str::CompactString),
80
81    /// The statement failed to prepare.
82    #[error("Prepare error: {0}")]
83    PrepareError(compact_str::CompactString),
84
85    /// A query that returns exactly one row, such as an aggregate with no
86    /// GROUP BY, returned none.
87    #[error("No rows found")]
88    NotFound,
89
90    /// A transaction could not begin, commit or roll back.
91    #[error("Transaction error: {0}")]
92    TransactionError(compact_str::CompactString),
93
94    /// A row could not be mapped to the target type.
95    #[error("Mapping error: {0}")]
96    Mapping(compact_str::CompactString),
97
98    /// The statement is invalid.
99    #[error("Statement error: {0}")]
100    Statement(compact_str::CompactString),
101
102    /// The query is invalid.
103    #[error("Query error: {0}")]
104    Query(CompactString),
105
106    /// Another error, with the SQL and parameters of the query that caused
107    /// it. The message shows the parameter count, not the values; see
108    /// [`QueryContext`].
109    #[error("{source}\n  sql: {sql}\n  params: {count} bound", sql = .ctx.sql, count = .ctx.param_count)]
110    QueryFailed {
111        /// Captured SQL and parameter context.
112        ctx: Box<QueryContext>,
113        /// Original error.
114        #[source]
115        source: Box<DrizzleError>,
116    },
117
118    /// Parameters could not be bound: missing, duplicate or unexpected
119    /// bindings, or a value that could not be converted.
120    #[error("Parameter conversion error: {0}")]
121    ParameterError(compact_str::CompactString),
122
123    /// An integer did not fit the target type.
124    #[error("Integer conversion error: {0}")]
125    TryFromInt(#[from] core::num::TryFromIntError),
126
127    /// Text could not be parsed as an integer.
128    #[error("Parse int error: {0}")]
129    ParseInt(#[from] core::num::ParseIntError),
130
131    /// Text could not be parsed as a float.
132    #[error("Parse float error: {0}")]
133    ParseFloat(#[from] core::num::ParseFloatError),
134
135    /// Text could not be parsed as a boolean.
136    #[error("Parse bool error: {0}")]
137    ParseBool(#[from] core::str::ParseBoolError),
138
139    /// A value could not be converted to the requested type.
140    #[error("Type conversion error: {0}")]
141    ConversionError(compact_str::CompactString),
142
143    /// The schema is invalid (for example a cycle in table dependencies).
144    #[error("Schema error: {0}")]
145    Schema(compact_str::CompactString),
146
147    /// A dirty migration cannot be repaired without violating its execution
148    /// safety contract.
149    #[error("Migration `{tag}` cannot be repaired safely: {reason}")]
150    UnsafeMigrationRepair {
151        /// Migration tag recorded in the tracking table.
152        tag: CompactString,
153        /// Safety requirement that prevents automatic repair.
154        reason: CompactString,
155    },
156
157    /// The selected adapter cannot provide a migration's required execution
158    /// semantics.
159    #[error("{adapter} cannot execute this migration: {requirement}")]
160    UnsupportedMigrationExecution {
161        /// Runtime adapter that cannot provide the required semantics.
162        adapter: CompactString,
163        /// Migration execution capability the adapter does not provide.
164        requirement: CompactString,
165    },
166
167    /// Any other error.
168    #[error("Database error: {0}")]
169    Other(compact_str::CompactString),
170
171    /// Error returned by a wire driver whose concrete type is intentionally
172    /// kept out of drizzle-core's public dependency graph.
173    #[cfg(feature = "driver-error")]
174    #[error("{driver} error: {source}")]
175    Driver {
176        /// Stable adapter name used in diagnostics.
177        driver: CompactString,
178        /// Original driver error, retained as the error source.
179        #[source]
180        source: Box<dyn std::error::Error + Send + Sync>,
181        /// What kind of failure the driver reported; see
182        /// [`DrizzleError::kind`].
183        kind: ErrorKind,
184        /// The constraint the failure names, if any; see
185        /// [`DrizzleError::constraint`].
186        constraint: Option<CompactString>,
187    },
188
189    /// Error from a higher-level subsystem kept as a typed source.
190    #[cfg(feature = "driver-error")]
191    #[error("{context}: {source}")]
192    External {
193        /// Operation or subsystem that failed.
194        context: CompactString,
195        /// Original error, retained as the error source.
196        #[source]
197        source: Box<dyn std::error::Error + Send + Sync>,
198    },
199
200    /// An error from `rusqlite`.
201    #[cfg(feature = "rusqlite")]
202    #[error("Rusqlite error: {0}")]
203    Rusqlite(#[from] rusqlite::Error),
204
205    /// An error from `turso`.
206    #[cfg(feature = "turso")]
207    #[error("Turso error: {0}")]
208    Turso(#[from] turso::Error),
209
210    /// An error from `libsql`.
211    #[cfg(feature = "libsql")]
212    #[error("LibSQL error: {0}")]
213    LibSQL(#[from] libsql::Error),
214
215    /// An error from `tokio-postgres`. The message is the server's, with its
216    /// SQLSTATE; the full `DbError` (detail, constraint, ...) is the source.
217    #[cfg(feature = "tokio-postgres")]
218    #[error("Postgres error: {}", postgres_message(.0))]
219    Postgres(#[from] tokio_postgres::Error),
220
221    /// An error from `postgres`. The message is the server's, with its
222    /// SQLSTATE; the full `DbError` (detail, constraint, ...) is the source.
223    #[cfg(all(feature = "postgres-sync", not(feature = "tokio-postgres")))]
224    #[error("Postgres error: {}", postgres_message(.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            kind: ErrorKind::Other,
254            constraint: None,
255        }
256    }
257
258    /// Records the [`ErrorKind`] and constraint of a [`Driver`](Self::Driver)
259    /// error, for adapters that read them from the driver's error. Other
260    /// errors are returned unchanged.
261    #[cfg(feature = "driver-error")]
262    #[doc(hidden)]
263    #[must_use]
264    pub fn classified(mut self, kind: ErrorKind, constraint: Option<&str>) -> Self {
265        if let Self::Driver {
266            kind: stored,
267            constraint: stored_constraint,
268            ..
269        } = &mut self
270        {
271            *stored = kind;
272            *stored_constraint = constraint.map(Into::into);
273        }
274        self
275    }
276
277    /// Wraps an external subsystem error while preserving its source chain.
278    #[cfg(feature = "driver-error")]
279    pub fn external(
280        context: impl Into<CompactString>,
281        source: impl std::error::Error + Send + Sync + 'static,
282    ) -> Self {
283        Self::External {
284            context: context.into(),
285            source: Box::new(source),
286        }
287    }
288}
289
290/// What kind of failure an error is, named the same on every driver.
291///
292/// [`DrizzleError::kind`] reads it from the driver's own error: the
293/// SQLSTATE on PostgreSQL, the extended result code (or SQLite's message,
294/// where the driver reports only text) on SQLite, libsql, turso, D1 and
295/// Durable Objects, and the server error number on MySQL.
296#[non_exhaustive]
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
298pub enum ErrorKind {
299    /// A `UNIQUE` or `PRIMARY KEY` constraint rejected a duplicate value.
300    UniqueViolation,
301    /// A `FOREIGN KEY` constraint rejected a row: its parent is missing, or
302    /// a child still points at the row being removed.
303    ForeignKeyViolation,
304    /// A `NOT NULL` column got no value.
305    NotNullViolation,
306    /// A `CHECK` constraint rejected a row.
307    CheckViolation,
308    /// A serializable transaction conflicted with a concurrent one. Run the
309    /// transaction again.
310    SerializationFailure,
311    /// The database broke a deadlock by failing this statement. Run the
312    /// transaction again.
313    Deadlock,
314    /// The database or a row is locked by another connection, and waiting
315    /// for it failed (SQLite `SQLITE_BUSY`, a lock timeout). Try again.
316    Busy,
317    /// A query that returns exactly one row returned none.
318    NotFound,
319    /// Any other failure.
320    Other,
321}
322
323impl ErrorKind {
324    /// Whether a constraint rejected the statement.
325    #[must_use]
326    pub const fn is_constraint_violation(self) -> bool {
327        matches!(
328            self,
329            Self::UniqueViolation
330                | Self::ForeignKeyViolation
331                | Self::NotNullViolation
332                | Self::CheckViolation
333        )
334    }
335
336    /// Whether running the statement or transaction again can succeed:
337    /// [`SerializationFailure`](Self::SerializationFailure),
338    /// [`Deadlock`](Self::Deadlock) and [`Busy`](Self::Busy).
339    #[must_use]
340    pub const fn is_retryable(self) -> bool {
341        matches!(
342            self,
343            Self::SerializationFailure | Self::Deadlock | Self::Busy
344        )
345    }
346}
347
348impl DrizzleError {
349    /// What kind of failure this is, the same on every driver.
350    ///
351    /// It looks through [`QueryFailed`](Self::QueryFailed) to the driver's
352    /// error.
353    ///
354    /// # Examples
355    ///
356    /// ```
357    /// use drizzle_core::error::{DrizzleError, ErrorKind};
358    ///
359    /// let error = DrizzleError::Other("UNIQUE constraint failed: users.email".into());
360    /// assert_eq!(error.kind(), ErrorKind::UniqueViolation);
361    /// assert!(!error.is_retryable());
362    /// ```
363    #[must_use]
364    pub fn kind(&self) -> ErrorKind {
365        match self {
366            Self::QueryFailed { source, .. } => source.kind(),
367            Self::NotFound => ErrorKind::NotFound,
368            Self::Other(message) | Self::ExecutionError(message) => message_kind(message),
369            #[cfg(feature = "driver-error")]
370            Self::Driver { kind, .. } => *kind,
371            #[cfg(feature = "rusqlite")]
372            Self::Rusqlite(error) => rusqlite_kind(error),
373            #[cfg(feature = "turso")]
374            Self::Turso(error) => turso_kind(error),
375            #[cfg(feature = "libsql")]
376            Self::LibSQL(error) => libsql_kind(error),
377            #[cfg(any(feature = "tokio-postgres", feature = "postgres-sync"))]
378            Self::Postgres(error) => error
379                .as_db_error()
380                .map_or(ErrorKind::Other, |db| sqlstate_kind(db.code().code())),
381            _ => ErrorKind::Other,
382        }
383    }
384
385    /// The name of the constraint the failure names, when the database
386    /// reports one: the constraint on PostgreSQL and MySQL, and a `CHECK` on
387    /// SQLite, which SQLite names by its expression when it has no name.
388    /// SQLite names only the columns of a `UNIQUE` failure, so that is `None`.
389    ///
390    /// It looks through [`QueryFailed`](Self::QueryFailed).
391    #[must_use]
392    pub fn constraint(&self) -> Option<&str> {
393        match self {
394            Self::QueryFailed { source, .. } => source.constraint(),
395            Self::Other(message) | Self::ExecutionError(message) => message_constraint(message),
396            #[cfg(feature = "driver-error")]
397            Self::Driver { constraint, .. } => constraint.as_deref(),
398            #[cfg(feature = "rusqlite")]
399            Self::Rusqlite(rusqlite::Error::SqliteFailure(_, Some(message))) => {
400                sqlite_check_name(message)
401            }
402            #[cfg(feature = "turso")]
403            Self::Turso(turso::Error::Constraint(message)) => sqlite_check_name(message),
404            #[cfg(feature = "libsql")]
405            Self::LibSQL(
406                libsql::Error::SqliteFailure(_, message)
407                | libsql::Error::RemoteSqliteFailure(_, _, message),
408            ) => sqlite_check_name(message),
409            #[cfg(any(feature = "tokio-postgres", feature = "postgres-sync"))]
410            Self::Postgres(error) => error.as_db_error().and_then(|db| db.constraint()),
411            _ => None,
412        }
413    }
414
415    /// Whether running the statement or transaction again can succeed; see
416    /// [`ErrorKind::is_retryable`].
417    #[must_use]
418    pub fn is_retryable(&self) -> bool {
419        self.kind().is_retryable()
420    }
421
422    /// The SQL and parameters of the query that failed, when drizzle ran it.
423    #[must_use]
424    pub fn query(&self) -> Option<&QueryContext> {
425        match self {
426            Self::QueryFailed { ctx, .. } => Some(ctx),
427            _ => None,
428        }
429    }
430}
431
432/// SQLite primary result codes, and the extended codes of constraint
433/// failures. An extended code keeps its primary code in its low byte.
434const SQLITE_BUSY: i32 = 5;
435const SQLITE_LOCKED: i32 = 6;
436const SQLITE_CONSTRAINT_CHECK: i32 = 275;
437const SQLITE_CONSTRAINT_FOREIGNKEY: i32 = 787;
438const SQLITE_CONSTRAINT_NOTNULL: i32 = 1299;
439const SQLITE_CONSTRAINT_PRIMARYKEY: i32 = 1555;
440const SQLITE_CONSTRAINT_UNIQUE: i32 = 2067;
441
442/// The kind of an SQLite (extended) result code, or `None` when the code
443/// alone does not say.
444#[cfg_attr(not(any(feature = "rusqlite", feature = "libsql")), allow(dead_code))]
445const fn sqlite_code_kind(code: i32) -> Option<ErrorKind> {
446    match code {
447        SQLITE_CONSTRAINT_UNIQUE | SQLITE_CONSTRAINT_PRIMARYKEY => Some(ErrorKind::UniqueViolation),
448        SQLITE_CONSTRAINT_FOREIGNKEY => Some(ErrorKind::ForeignKeyViolation),
449        SQLITE_CONSTRAINT_NOTNULL => Some(ErrorKind::NotNullViolation),
450        SQLITE_CONSTRAINT_CHECK => Some(ErrorKind::CheckViolation),
451        _ if matches!(code & 0xff, SQLITE_BUSY | SQLITE_LOCKED) => Some(ErrorKind::Busy),
452        _ => None,
453    }
454}
455
456#[cfg(feature = "rusqlite")]
457fn rusqlite_kind(error: &rusqlite::Error) -> ErrorKind {
458    match error {
459        rusqlite::Error::SqliteFailure(failure, message) => sqlite_code_kind(failure.extended_code)
460            .unwrap_or_else(|| {
461                message
462                    .as_deref()
463                    .map_or(ErrorKind::Other, sqlite_message_kind)
464            }),
465        _ => ErrorKind::Other,
466    }
467}
468
469#[cfg(feature = "libsql")]
470fn libsql_kind(error: &libsql::Error) -> ErrorKind {
471    match error {
472        libsql::Error::SqliteFailure(code, message)
473        | libsql::Error::RemoteSqliteFailure(_, code, message) => {
474            sqlite_code_kind(*code).unwrap_or_else(|| sqlite_message_kind(message))
475        }
476        // A remote (Hrana) connection reports only the server's message.
477        error => sqlite_message_kind(&error.to_string()),
478    }
479}
480
481#[cfg(feature = "turso")]
482fn turso_kind(error: &turso::Error) -> ErrorKind {
483    match error {
484        // MVCC: another transaction wrote the row first, or committed after
485        // this one's snapshot. Only running the transaction again helps.
486        turso::Error::Error(message) if message.contains("Write-write conflict") => {
487            ErrorKind::SerializationFailure
488        }
489        turso::Error::BusySnapshot(_) => ErrorKind::SerializationFailure,
490        turso::Error::Constraint(message) | turso::Error::Error(message) => {
491            sqlite_message_kind(message)
492        }
493        turso::Error::Busy(_) => ErrorKind::Busy,
494        _ => ErrorKind::Other,
495    }
496}
497
498/// The kind named by an error message: a PostgreSQL SQLSTATE written as
499/// `SQLState: 23505` (the AWS Data API), else SQLite's constraint and lock
500/// text (D1, Durable Objects and turso report only text).
501fn message_kind(message: &str) -> ErrorKind {
502    // The SQLSTATE ends the message, after a detail line that can quote the
503    // row's values, so the last one is the server's.
504    match message.rsplit_once("SQLState: ") {
505        Some((_, code)) => code.get(..5).map_or(ErrorKind::Other, sqlstate_kind),
506        None => sqlite_message_kind(message),
507    }
508}
509
510/// The constraint an error message names: a PostgreSQL message's
511/// `constraint "name"` (the AWS Data API), else an SQLite `CHECK`.
512fn message_constraint(message: &str) -> Option<&str> {
513    if !message.contains("SQLState: ") {
514        return sqlite_check_name(message);
515    }
516    // Only the first line: the detail line can quote the row's values.
517    let primary = message.lines().next().unwrap_or(message);
518    let primary = primary
519        .split_once("Detail:")
520        .map_or(primary, |(head, _)| head);
521    primary
522        .split_once("constraint \"")
523        .and_then(|(_, rest)| rest.split_once('"'))
524        .map(|(name, _)| name)
525}
526
527/// The kind named by SQLite's error message text, which is stable across
528/// SQLite versions and drivers.
529fn sqlite_message_kind(message: &str) -> ErrorKind {
530    if message.contains("UNIQUE constraint failed") {
531        ErrorKind::UniqueViolation
532    } else if message.contains("FOREIGN KEY constraint failed") {
533        ErrorKind::ForeignKeyViolation
534    } else if message.contains("NOT NULL constraint failed") {
535        ErrorKind::NotNullViolation
536    } else if message.contains("CHECK constraint failed") {
537        ErrorKind::CheckViolation
538    } else if message.contains("database is locked")
539        || message.contains("database table is locked")
540        || message.contains("SQLITE_BUSY")
541    {
542        ErrorKind::Busy
543    } else {
544        ErrorKind::Other
545    }
546}
547
548/// The constraint named by SQLite's `CHECK constraint failed: <name>`,
549/// without the `: SQLITE_CONSTRAINT` suffix D1 and Durable Objects add.
550fn sqlite_check_name(message: &str) -> Option<&str> {
551    let (_, rest) = message.split_once("CHECK constraint failed: ")?;
552    let rest = rest.lines().next().unwrap_or(rest);
553    let name = rest
554        .split_once(": SQLITE_")
555        .map_or(rest, |(name, _)| name)
556        .trim();
557    (!name.is_empty()).then_some(name)
558}
559
560/// The kind of a PostgreSQL SQLSTATE.
561fn sqlstate_kind(code: &str) -> ErrorKind {
562    match code {
563        "23505" => ErrorKind::UniqueViolation,
564        "23503" => ErrorKind::ForeignKeyViolation,
565        "23502" => ErrorKind::NotNullViolation,
566        "23514" => ErrorKind::CheckViolation,
567        "40001" => ErrorKind::SerializationFailure,
568        "40P01" => ErrorKind::Deadlock,
569        "55P03" => ErrorKind::Busy,
570        _ => ErrorKind::Other,
571    }
572}
573
574/// The server's message and SQLSTATE for a Postgres error. The driver's own
575/// `Display` is only "db error". `DbError`'s `Display` adds the DETAIL line,
576/// which for a constraint violation holds the row's values
577/// (`Key (email)=(...) already exists.`), so it is left out.
578#[cfg(feature = "tokio-postgres")]
579fn postgres_message(error: &tokio_postgres::Error) -> String {
580    error.as_db_error().map_or_else(
581        || format!("{error}"),
582        |db| format!("{} ({})", db.message(), db.code().code()),
583    )
584}
585
586#[cfg(all(feature = "postgres-sync", not(feature = "tokio-postgres")))]
587fn postgres_message(error: &postgres::Error) -> String {
588    error.as_db_error().map_or_else(
589        || format!("{error}"),
590        |db| format!("{} ({})", db.message(), db.code().code()),
591    )
592}
593
594/// `Result` with [`DrizzleError`] as the error type.
595pub type Result<T> = core::result::Result<T, DrizzleError>;
596
597/// Adds the failing query's SQL and parameters to an error.
598///
599/// # Examples
600///
601/// ```
602/// use drizzle_core::error::{DrizzleError, QueryContext, ResultExt};
603///
604/// let result: Result<(), DrizzleError> = Err(DrizzleError::NotFound);
605/// let error = result
606///     .with_query(|| QueryContext::new("SELECT * FROM users WHERE id = ?", &[&7]))
607///     .unwrap_err();
608///
609/// assert_eq!(
610///     error.to_string(),
611///     "No rows found\n  sql: SELECT * FROM users WHERE id = ?\n  params: 1 bound",
612/// );
613/// ```
614pub trait ResultExt<T> {
615    /// On error, wraps it in [`DrizzleError::QueryFailed`] with the context
616    /// from `ctx`. `ctx` only runs on the error path, and an error that
617    /// already has context keeps it.
618    fn with_query<F>(self, ctx: F) -> Result<T>
619    where
620        F: FnOnce() -> QueryContext;
621}
622
623impl<T, E> ResultExt<T> for core::result::Result<T, E>
624where
625    E: Into<DrizzleError>,
626{
627    fn with_query<F>(self, ctx: F) -> Result<T>
628    where
629        F: FnOnce() -> QueryContext,
630    {
631        self.map_err(|error| {
632            let source = error.into();
633            match source {
634                DrizzleError::QueryFailed { .. } => source,
635                other => DrizzleError::QueryFailed {
636                    ctx: Box::new(ctx()),
637                    source: Box::new(other),
638                },
639            }
640        })
641    }
642}
643
644#[cfg(test)]
645mod display_tests {
646    use super::*;
647    use crate::prelude::ToString;
648
649    // Parameter values are often personal data or secrets; the message that
650    // reaches logs shows only how many were bound.
651    #[test]
652    fn query_failed_display_omits_parameter_values() {
653        let error = Err::<(), _>(DrizzleError::NotFound)
654            .with_query(|| {
655                QueryContext::new(
656                    "SELECT * FROM users WHERE email = $1",
657                    &[&"ada@example.com"],
658                )
659            })
660            .unwrap_err();
661
662        let message = error.to_string();
663        assert_eq!(
664            message,
665            "No rows found\n  sql: SELECT * FROM users WHERE email = $1\n  params: 1 bound"
666        );
667        // `.unwrap()` prints `Debug`.
668        let debug = format!("{error:?}");
669        assert!(!debug.contains("ada@example.com"), "{debug}");
670        let DrizzleError::QueryFailed { ctx, .. } = error else {
671            panic!("expected QueryFailed");
672        };
673        assert_eq!(ctx.params.as_ref(), ["\"ada@example.com\""]);
674    }
675}
676
677#[cfg(all(test, feature = "driver-error"))]
678mod tests {
679    use super::*;
680    use std::error::Error as _;
681
682    #[test]
683    fn external_errors_keep_their_source() {
684        let error =
685            DrizzleError::external("schema diff", std::io::Error::other("invalid snapshot"));
686
687        assert_eq!(error.to_string(), "schema diff: invalid snapshot");
688        assert_eq!(
689            error.source().map(ToString::to_string),
690            Some("invalid snapshot".to_string())
691        );
692    }
693}
694
695#[cfg(test)]
696mod kind_tests {
697    use super::*;
698
699    #[test]
700    fn sqlite_messages_name_their_kind() {
701        // D1 and Durable Objects report SQLite's text with their own prefix
702        // and suffix.
703        let cases = [
704            (
705                "D1_ERROR: UNIQUE constraint failed: users.email: SQLITE_CONSTRAINT",
706                ErrorKind::UniqueViolation,
707            ),
708            (
709                "FOREIGN KEY constraint failed: SQLITE_CONSTRAINT",
710                ErrorKind::ForeignKeyViolation,
711            ),
712            (
713                "NOT NULL constraint failed: users.name",
714                ErrorKind::NotNullViolation,
715            ),
716            (
717                "CHECK constraint failed: users_age_check",
718                ErrorKind::CheckViolation,
719            ),
720            ("database is locked", ErrorKind::Busy),
721            ("no such table: users", ErrorKind::Other),
722        ];
723        for (message, kind) in cases {
724            assert_eq!(
725                DrizzleError::Other(message.into()).kind(),
726                kind,
727                "{message}"
728            );
729        }
730    }
731
732    #[test]
733    fn a_named_sqlite_check_is_the_constraint() {
734        let error = DrizzleError::Other(
735            "D1_ERROR: CHECK constraint failed: positive_age: SQLITE_CONSTRAINT".into(),
736        );
737        assert_eq!(error.constraint(), Some("positive_age"));
738        // SQLite names only the columns of a UNIQUE failure.
739        let error = DrizzleError::Other("UNIQUE constraint failed: users.email".into());
740        assert_eq!(error.constraint(), None);
741    }
742
743    #[test]
744    fn aws_data_api_messages_carry_the_sqlstate() {
745        let error = DrizzleError::Other(
746            "aws ExecuteStatement: ERROR: duplicate key value violates unique constraint \
747             \"users_email_key\"; SQLState: 23505"
748                .into(),
749        );
750        assert_eq!(error.kind(), ErrorKind::UniqueViolation);
751        assert_eq!(error.constraint(), Some("users_email_key"));
752
753        let serialization =
754            DrizzleError::Other("aws ExecuteStatement: ...; SQLState: 40001".into());
755        assert_eq!(serialization.kind(), ErrorKind::SerializationFailure);
756        assert!(serialization.is_retryable());
757        let deadlock = DrizzleError::Other("aws ExecuteStatement: ...; SQLState: 40P01".into());
758        assert_eq!(deadlock.kind(), ErrorKind::Deadlock);
759        assert!(deadlock.is_retryable());
760    }
761
762    #[test]
763    fn row_values_in_a_detail_line_do_not_change_the_kind() {
764        // PostgreSQL's DETAIL quotes the row, and the SQLSTATE comes last.
765        let error = DrizzleError::Other(
766            "aws ExecuteStatement: ERROR: null value in column \"name\" of relation \"users\" \
767             violates not-null constraint\n  Detail: Failing row contains (1, database is locked, \
768             SQLState: 40001, constraint \"fake\").; SQLState: 23502"
769                .into(),
770        );
771        assert_eq!(error.kind(), ErrorKind::NotNullViolation);
772        assert!(!error.is_retryable());
773        assert_eq!(error.constraint(), None);
774    }
775
776    #[test]
777    fn an_unnamed_sqlite_check_is_named_by_its_expression() {
778        let error = DrizzleError::Other(
779            "D1_ERROR: CHECK constraint failed: time >= '00:00': SQLITE_CONSTRAINT".into(),
780        );
781        assert_eq!(error.constraint(), Some("time >= '00:00'"));
782    }
783
784    #[test]
785    fn the_query_context_is_looked_through() {
786        let error: Result<()> = Err(DrizzleError::Other(
787            "CHECK constraint failed: positive_age".into(),
788        ));
789        let error = error
790            .with_query(|| QueryContext::new("INSERT INTO users VALUES (?)", &[&-1]))
791            .unwrap_err();
792        assert_eq!(error.kind(), ErrorKind::CheckViolation);
793        assert_eq!(error.constraint(), Some("positive_age"));
794        assert_eq!(
795            error.query().map(|query| query.sql.as_str()),
796            Some("INSERT INTO users VALUES (?)")
797        );
798        assert!(DrizzleError::NotFound.query().is_none());
799    }
800
801    #[test]
802    fn kinds_say_whether_they_are_constraints_or_retryable() {
803        assert_eq!(DrizzleError::NotFound.kind(), ErrorKind::NotFound);
804        for kind in [
805            ErrorKind::UniqueViolation,
806            ErrorKind::ForeignKeyViolation,
807            ErrorKind::NotNullViolation,
808            ErrorKind::CheckViolation,
809        ] {
810            assert!(
811                kind.is_constraint_violation() && !kind.is_retryable(),
812                "{kind:?}"
813            );
814        }
815        for kind in [
816            ErrorKind::SerializationFailure,
817            ErrorKind::Deadlock,
818            ErrorKind::Busy,
819        ] {
820            assert!(
821                kind.is_retryable() && !kind.is_constraint_violation(),
822                "{kind:?}"
823            );
824        }
825        for kind in [ErrorKind::NotFound, ErrorKind::Other] {
826            assert!(
827                !kind.is_retryable() && !kind.is_constraint_violation(),
828                "{kind:?}"
829            );
830        }
831    }
832
833    #[cfg(feature = "rusqlite")]
834    #[test]
835    fn rusqlite_extended_codes_name_their_kind() {
836        let failure = |code: i32, message: &str| {
837            DrizzleError::Rusqlite(rusqlite::Error::SqliteFailure(
838                rusqlite::ffi::Error::new(code),
839                Some(message.into()),
840            ))
841        };
842        assert_eq!(
843            failure(1555, "UNIQUE constraint failed: users.id").kind(),
844            ErrorKind::UniqueViolation
845        );
846        assert_eq!(
847            failure(787, "FOREIGN KEY constraint failed").kind(),
848            ErrorKind::ForeignKeyViolation
849        );
850        let check = failure(275, "CHECK constraint failed: positive_age");
851        assert_eq!(check.kind(), ErrorKind::CheckViolation);
852        assert_eq!(check.constraint(), Some("positive_age"));
853        assert_eq!(failure(5, "database is locked").kind(), ErrorKind::Busy);
854        assert_eq!(failure(517, "database is locked").kind(), ErrorKind::Busy);
855    }
856
857    #[cfg(feature = "turso")]
858    #[test]
859    fn turso_errors_name_their_kind() {
860        assert_eq!(
861            DrizzleError::Turso(turso::Error::Busy("database is locked".into())).kind(),
862            ErrorKind::Busy
863        );
864        assert_eq!(
865            DrizzleError::Turso(turso::Error::Constraint(
866                "FOREIGN KEY constraint failed".into()
867            ))
868            .kind(),
869            ErrorKind::ForeignKeyViolation
870        );
871        let conflict = DrizzleError::Turso(turso::Error::Error("Write-write conflict".into()));
872        assert_eq!(conflict.kind(), ErrorKind::SerializationFailure);
873        assert!(conflict.is_retryable());
874        assert_eq!(
875            DrizzleError::Turso(turso::Error::BusySnapshot("snapshot is stale".into())).kind(),
876            ErrorKind::SerializationFailure
877        );
878    }
879
880    #[cfg(feature = "libsql")]
881    #[test]
882    fn libsql_codes_name_their_kind() {
883        assert_eq!(
884            DrizzleError::LibSQL(libsql::Error::SqliteFailure(
885                1299,
886                "NOT NULL constraint failed: users.name".into()
887            ))
888            .kind(),
889            ErrorKind::NotNullViolation
890        );
891        // An embedded replica forwards writes to its primary.
892        let remote = DrizzleError::LibSQL(libsql::Error::RemoteSqliteFailure(
893            19,
894            275,
895            "CHECK constraint failed: positive_age".into(),
896        ));
897        assert_eq!(remote.kind(), ErrorKind::CheckViolation);
898        assert_eq!(remote.constraint(), Some("positive_age"));
899        // A remote (Hrana) connection reports only the server's message.
900        let hrana = DrizzleError::LibSQL(libsql::Error::Hrana(
901            "SQLite error: UNIQUE constraint failed: users.email".into(),
902        ));
903        assert_eq!(hrana.kind(), ErrorKind::UniqueViolation);
904    }
905
906    #[cfg(feature = "driver-error")]
907    #[test]
908    fn driver_errors_keep_the_kind_their_adapter_read() {
909        let error = DrizzleError::driver("MySQL", std::io::Error::other("Deadlock found"))
910            .classified(ErrorKind::Deadlock, None);
911        assert_eq!(error.kind(), ErrorKind::Deadlock);
912        assert!(error.is_retryable());
913
914        let error = DrizzleError::driver("MySQL", std::io::Error::other("Duplicate entry"))
915            .classified(ErrorKind::UniqueViolation, Some("users_email_unique"));
916        assert_eq!(error.constraint(), Some("users_email_unique"));
917
918        let unchanged = DrizzleError::NotFound.classified(ErrorKind::Deadlock, None);
919        assert_eq!(unchanged.kind(), ErrorKind::NotFound);
920    }
921}