1use 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#[derive(Clone)]
18pub struct QueryContext {
19 pub sql: CompactString,
21 pub params: Box<[CompactString]>,
23 pub param_count: usize,
25}
26
27impl QueryContext {
28 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#[derive(Debug, Error)]
76pub enum DrizzleError {
77 #[error("Execution error: {0}")]
79 ExecutionError(compact_str::CompactString),
80
81 #[error("Prepare error: {0}")]
83 PrepareError(compact_str::CompactString),
84
85 #[error("No rows found")]
88 NotFound,
89
90 #[error("Transaction error: {0}")]
92 TransactionError(compact_str::CompactString),
93
94 #[error("Mapping error: {0}")]
96 Mapping(compact_str::CompactString),
97
98 #[error("Statement error: {0}")]
100 Statement(compact_str::CompactString),
101
102 #[error("Query error: {0}")]
104 Query(CompactString),
105
106 #[error("{source}\n sql: {sql}\n params: {count} bound", sql = .ctx.sql, count = .ctx.param_count)]
110 QueryFailed {
111 ctx: Box<QueryContext>,
113 #[source]
115 source: Box<DrizzleError>,
116 },
117
118 #[error("Parameter conversion error: {0}")]
121 ParameterError(compact_str::CompactString),
122
123 #[error("Integer conversion error: {0}")]
125 TryFromInt(#[from] core::num::TryFromIntError),
126
127 #[error("Parse int error: {0}")]
129 ParseInt(#[from] core::num::ParseIntError),
130
131 #[error("Parse float error: {0}")]
133 ParseFloat(#[from] core::num::ParseFloatError),
134
135 #[error("Parse bool error: {0}")]
137 ParseBool(#[from] core::str::ParseBoolError),
138
139 #[error("Type conversion error: {0}")]
141 ConversionError(compact_str::CompactString),
142
143 #[error("Schema error: {0}")]
145 Schema(compact_str::CompactString),
146
147 #[error("Migration `{tag}` cannot be repaired safely: {reason}")]
150 UnsafeMigrationRepair {
151 tag: CompactString,
153 reason: CompactString,
155 },
156
157 #[error("{adapter} cannot execute this migration: {requirement}")]
160 UnsupportedMigrationExecution {
161 adapter: CompactString,
163 requirement: CompactString,
165 },
166
167 #[error("Database error: {0}")]
169 Other(compact_str::CompactString),
170
171 #[cfg(feature = "driver-error")]
174 #[error("{driver} error: {source}")]
175 Driver {
176 driver: CompactString,
178 #[source]
180 source: Box<dyn std::error::Error + Send + Sync>,
181 kind: ErrorKind,
184 constraint: Option<CompactString>,
187 },
188
189 #[cfg(feature = "driver-error")]
191 #[error("{context}: {source}")]
192 External {
193 context: CompactString,
195 #[source]
197 source: Box<dyn std::error::Error + Send + Sync>,
198 },
199
200 #[cfg(feature = "rusqlite")]
202 #[error("Rusqlite error: {0}")]
203 Rusqlite(#[from] rusqlite::Error),
204
205 #[cfg(feature = "turso")]
207 #[error("Turso error: {0}")]
208 Turso(#[from] turso::Error),
209
210 #[cfg(feature = "libsql")]
212 #[error("LibSQL error: {0}")]
213 LibSQL(#[from] libsql::Error),
214
215 #[cfg(feature = "tokio-postgres")]
218 #[error("Postgres error: {}", postgres_message(.0))]
219 Postgres(#[from] tokio_postgres::Error),
220
221 #[cfg(all(feature = "postgres-sync", not(feature = "tokio-postgres")))]
224 #[error("Postgres error: {}", postgres_message(.0))]
225 Postgres(#[from] postgres::Error),
226
227 #[cfg(feature = "uuid")]
229 #[error("UUID error: {0}")]
230 UuidError(#[from] uuid::Error),
231
232 #[cfg(feature = "serde")]
234 #[error("JSON error: {0}")]
235 JsonError(#[from] serde_json::Error),
236
237 #[error("Infallible conversion error")]
239 Infallible(#[from] core::convert::Infallible),
240}
241
242impl DrizzleError {
243 #[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 #[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 #[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#[non_exhaustive]
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
298pub enum ErrorKind {
299 UniqueViolation,
301 ForeignKeyViolation,
304 NotNullViolation,
306 CheckViolation,
308 SerializationFailure,
311 Deadlock,
314 Busy,
317 NotFound,
319 Other,
321}
322
323impl ErrorKind {
324 #[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 #[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 #[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 #[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 #[must_use]
418 pub fn is_retryable(&self) -> bool {
419 self.kind().is_retryable()
420 }
421
422 #[must_use]
424 pub fn query(&self) -> Option<&QueryContext> {
425 match self {
426 Self::QueryFailed { ctx, .. } => Some(ctx),
427 _ => None,
428 }
429 }
430}
431
432const 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#[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 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 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
498fn message_kind(message: &str) -> ErrorKind {
502 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
510fn message_constraint(message: &str) -> Option<&str> {
513 if !message.contains("SQLState: ") {
514 return sqlite_check_name(message);
515 }
516 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
527fn 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
548fn 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
560fn 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#[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
594pub type Result<T> = core::result::Result<T, DrizzleError>;
596
597pub trait ResultExt<T> {
615 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 #[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 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 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 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 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 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 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}