use crate::prelude::{Box, String, Vec, format};
use compact_str::CompactString;
use thiserror::Error;
const MAX_CONTEXT_PARAMS: usize = 32;
const MAX_CONTEXT_PARAM_CHARS: usize = 128;
#[derive(Clone)]
pub struct QueryContext {
pub sql: CompactString,
pub params: Box<[CompactString]>,
pub param_count: usize,
}
impl QueryContext {
pub fn new<V: core::fmt::Debug>(sql: &str, params: &[&V]) -> Self {
let rendered = params
.iter()
.take(MAX_CONTEXT_PARAMS)
.map(|param| truncate_param(format!("{param:?}")))
.collect::<Vec<_>>()
.into_boxed_slice();
Self {
sql: sql.into(),
params: rendered,
param_count: params.len(),
}
}
}
impl core::fmt::Debug for QueryContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("QueryContext")
.field("sql", &self.sql)
.field("param_count", &self.param_count)
.finish_non_exhaustive()
}
}
fn truncate_param(mut value: String) -> CompactString {
if value.chars().count() <= MAX_CONTEXT_PARAM_CHARS {
return value.into();
}
let mut truncated = String::new();
for ch in value.drain(..).take(MAX_CONTEXT_PARAM_CHARS) {
truncated.push(ch);
}
truncated.push_str("...");
truncated.into()
}
#[derive(Debug, Error)]
pub enum DrizzleError {
#[error("Execution error: {0}")]
ExecutionError(compact_str::CompactString),
#[error("Prepare error: {0}")]
PrepareError(compact_str::CompactString),
#[error("No rows found")]
NotFound,
#[error("Transaction error: {0}")]
TransactionError(compact_str::CompactString),
#[error("Mapping error: {0}")]
Mapping(compact_str::CompactString),
#[error("Statement error: {0}")]
Statement(compact_str::CompactString),
#[error("Query error: {0}")]
Query(CompactString),
#[error("{source}\n sql: {sql}\n params: {count} bound", sql = .ctx.sql, count = .ctx.param_count)]
QueryFailed {
ctx: Box<QueryContext>,
#[source]
source: Box<DrizzleError>,
},
#[error("Parameter conversion error: {0}")]
ParameterError(compact_str::CompactString),
#[error("Integer conversion error: {0}")]
TryFromInt(#[from] core::num::TryFromIntError),
#[error("Parse int error: {0}")]
ParseInt(#[from] core::num::ParseIntError),
#[error("Parse float error: {0}")]
ParseFloat(#[from] core::num::ParseFloatError),
#[error("Parse bool error: {0}")]
ParseBool(#[from] core::str::ParseBoolError),
#[error("Type conversion error: {0}")]
ConversionError(compact_str::CompactString),
#[error("Schema error: {0}")]
Schema(compact_str::CompactString),
#[error("Migration `{tag}` cannot be repaired safely: {reason}")]
UnsafeMigrationRepair {
tag: CompactString,
reason: CompactString,
},
#[error("{adapter} cannot execute this migration: {requirement}")]
UnsupportedMigrationExecution {
adapter: CompactString,
requirement: CompactString,
},
#[error("Database error: {0}")]
Other(compact_str::CompactString),
#[cfg(feature = "driver-error")]
#[error("{driver} error: {source}")]
Driver {
driver: CompactString,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
kind: ErrorKind,
constraint: Option<CompactString>,
},
#[cfg(feature = "driver-error")]
#[error("{context}: {source}")]
External {
context: CompactString,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[cfg(feature = "rusqlite")]
#[error("Rusqlite error: {0}")]
Rusqlite(#[from] rusqlite::Error),
#[cfg(feature = "turso")]
#[error("Turso error: {0}")]
Turso(#[from] turso::Error),
#[cfg(feature = "libsql")]
#[error("LibSQL error: {0}")]
LibSQL(#[from] libsql::Error),
#[cfg(feature = "tokio-postgres")]
#[error("Postgres error: {}", postgres_message(.0))]
Postgres(#[from] tokio_postgres::Error),
#[cfg(all(feature = "postgres-sync", not(feature = "tokio-postgres")))]
#[error("Postgres error: {}", postgres_message(.0))]
Postgres(#[from] postgres::Error),
#[cfg(feature = "uuid")]
#[error("UUID error: {0}")]
UuidError(#[from] uuid::Error),
#[cfg(feature = "serde")]
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Infallible conversion error")]
Infallible(#[from] core::convert::Infallible),
}
impl DrizzleError {
#[cfg(feature = "driver-error")]
pub fn driver(
driver: impl Into<CompactString>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Driver {
driver: driver.into(),
source: Box::new(source),
kind: ErrorKind::Other,
constraint: None,
}
}
#[cfg(feature = "driver-error")]
#[doc(hidden)]
#[must_use]
pub fn classified(mut self, kind: ErrorKind, constraint: Option<&str>) -> Self {
if let Self::Driver {
kind: stored,
constraint: stored_constraint,
..
} = &mut self
{
*stored = kind;
*stored_constraint = constraint.map(Into::into);
}
self
}
#[cfg(feature = "driver-error")]
pub fn external(
context: impl Into<CompactString>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::External {
context: context.into(),
source: Box::new(source),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorKind {
UniqueViolation,
ForeignKeyViolation,
NotNullViolation,
CheckViolation,
SerializationFailure,
Deadlock,
Busy,
NotFound,
Other,
}
impl ErrorKind {
#[must_use]
pub const fn is_constraint_violation(self) -> bool {
matches!(
self,
Self::UniqueViolation
| Self::ForeignKeyViolation
| Self::NotNullViolation
| Self::CheckViolation
)
}
#[must_use]
pub const fn is_retryable(self) -> bool {
matches!(
self,
Self::SerializationFailure | Self::Deadlock | Self::Busy
)
}
}
impl DrizzleError {
#[must_use]
pub fn kind(&self) -> ErrorKind {
match self {
Self::QueryFailed { source, .. } => source.kind(),
Self::NotFound => ErrorKind::NotFound,
Self::Other(message) | Self::ExecutionError(message) => message_kind(message),
#[cfg(feature = "driver-error")]
Self::Driver { kind, .. } => *kind,
#[cfg(feature = "rusqlite")]
Self::Rusqlite(error) => rusqlite_kind(error),
#[cfg(feature = "turso")]
Self::Turso(error) => turso_kind(error),
#[cfg(feature = "libsql")]
Self::LibSQL(error) => libsql_kind(error),
#[cfg(any(feature = "tokio-postgres", feature = "postgres-sync"))]
Self::Postgres(error) => error
.as_db_error()
.map_or(ErrorKind::Other, |db| sqlstate_kind(db.code().code())),
_ => ErrorKind::Other,
}
}
#[must_use]
pub fn constraint(&self) -> Option<&str> {
match self {
Self::QueryFailed { source, .. } => source.constraint(),
Self::Other(message) | Self::ExecutionError(message) => message_constraint(message),
#[cfg(feature = "driver-error")]
Self::Driver { constraint, .. } => constraint.as_deref(),
#[cfg(feature = "rusqlite")]
Self::Rusqlite(rusqlite::Error::SqliteFailure(_, Some(message))) => {
sqlite_check_name(message)
}
#[cfg(feature = "turso")]
Self::Turso(turso::Error::Constraint(message)) => sqlite_check_name(message),
#[cfg(feature = "libsql")]
Self::LibSQL(
libsql::Error::SqliteFailure(_, message)
| libsql::Error::RemoteSqliteFailure(_, _, message),
) => sqlite_check_name(message),
#[cfg(any(feature = "tokio-postgres", feature = "postgres-sync"))]
Self::Postgres(error) => error.as_db_error().and_then(|db| db.constraint()),
_ => None,
}
}
#[must_use]
pub fn is_retryable(&self) -> bool {
self.kind().is_retryable()
}
#[must_use]
pub fn query(&self) -> Option<&QueryContext> {
match self {
Self::QueryFailed { ctx, .. } => Some(ctx),
_ => None,
}
}
}
const SQLITE_BUSY: i32 = 5;
const SQLITE_LOCKED: i32 = 6;
const SQLITE_CONSTRAINT_CHECK: i32 = 275;
const SQLITE_CONSTRAINT_FOREIGNKEY: i32 = 787;
const SQLITE_CONSTRAINT_NOTNULL: i32 = 1299;
const SQLITE_CONSTRAINT_PRIMARYKEY: i32 = 1555;
const SQLITE_CONSTRAINT_UNIQUE: i32 = 2067;
#[cfg_attr(not(any(feature = "rusqlite", feature = "libsql")), allow(dead_code))]
const fn sqlite_code_kind(code: i32) -> Option<ErrorKind> {
match code {
SQLITE_CONSTRAINT_UNIQUE | SQLITE_CONSTRAINT_PRIMARYKEY => Some(ErrorKind::UniqueViolation),
SQLITE_CONSTRAINT_FOREIGNKEY => Some(ErrorKind::ForeignKeyViolation),
SQLITE_CONSTRAINT_NOTNULL => Some(ErrorKind::NotNullViolation),
SQLITE_CONSTRAINT_CHECK => Some(ErrorKind::CheckViolation),
_ if matches!(code & 0xff, SQLITE_BUSY | SQLITE_LOCKED) => Some(ErrorKind::Busy),
_ => None,
}
}
#[cfg(feature = "rusqlite")]
fn rusqlite_kind(error: &rusqlite::Error) -> ErrorKind {
match error {
rusqlite::Error::SqliteFailure(failure, message) => sqlite_code_kind(failure.extended_code)
.unwrap_or_else(|| {
message
.as_deref()
.map_or(ErrorKind::Other, sqlite_message_kind)
}),
_ => ErrorKind::Other,
}
}
#[cfg(feature = "libsql")]
fn libsql_kind(error: &libsql::Error) -> ErrorKind {
match error {
libsql::Error::SqliteFailure(code, message)
| libsql::Error::RemoteSqliteFailure(_, code, message) => {
sqlite_code_kind(*code).unwrap_or_else(|| sqlite_message_kind(message))
}
error => sqlite_message_kind(&error.to_string()),
}
}
#[cfg(feature = "turso")]
fn turso_kind(error: &turso::Error) -> ErrorKind {
match error {
turso::Error::Error(message) if message.contains("Write-write conflict") => {
ErrorKind::SerializationFailure
}
turso::Error::BusySnapshot(_) => ErrorKind::SerializationFailure,
turso::Error::Constraint(message) | turso::Error::Error(message) => {
sqlite_message_kind(message)
}
turso::Error::Busy(_) => ErrorKind::Busy,
_ => ErrorKind::Other,
}
}
fn message_kind(message: &str) -> ErrorKind {
match message.rsplit_once("SQLState: ") {
Some((_, code)) => code.get(..5).map_or(ErrorKind::Other, sqlstate_kind),
None => sqlite_message_kind(message),
}
}
fn message_constraint(message: &str) -> Option<&str> {
if !message.contains("SQLState: ") {
return sqlite_check_name(message);
}
let primary = message.lines().next().unwrap_or(message);
let primary = primary
.split_once("Detail:")
.map_or(primary, |(head, _)| head);
primary
.split_once("constraint \"")
.and_then(|(_, rest)| rest.split_once('"'))
.map(|(name, _)| name)
}
fn sqlite_message_kind(message: &str) -> ErrorKind {
if message.contains("UNIQUE constraint failed") {
ErrorKind::UniqueViolation
} else if message.contains("FOREIGN KEY constraint failed") {
ErrorKind::ForeignKeyViolation
} else if message.contains("NOT NULL constraint failed") {
ErrorKind::NotNullViolation
} else if message.contains("CHECK constraint failed") {
ErrorKind::CheckViolation
} else if message.contains("database is locked")
|| message.contains("database table is locked")
|| message.contains("SQLITE_BUSY")
{
ErrorKind::Busy
} else {
ErrorKind::Other
}
}
fn sqlite_check_name(message: &str) -> Option<&str> {
let (_, rest) = message.split_once("CHECK constraint failed: ")?;
let rest = rest.lines().next().unwrap_or(rest);
let name = rest
.split_once(": SQLITE_")
.map_or(rest, |(name, _)| name)
.trim();
(!name.is_empty()).then_some(name)
}
fn sqlstate_kind(code: &str) -> ErrorKind {
match code {
"23505" => ErrorKind::UniqueViolation,
"23503" => ErrorKind::ForeignKeyViolation,
"23502" => ErrorKind::NotNullViolation,
"23514" => ErrorKind::CheckViolation,
"40001" => ErrorKind::SerializationFailure,
"40P01" => ErrorKind::Deadlock,
"55P03" => ErrorKind::Busy,
_ => ErrorKind::Other,
}
}
#[cfg(feature = "tokio-postgres")]
fn postgres_message(error: &tokio_postgres::Error) -> String {
error.as_db_error().map_or_else(
|| format!("{error}"),
|db| format!("{} ({})", db.message(), db.code().code()),
)
}
#[cfg(all(feature = "postgres-sync", not(feature = "tokio-postgres")))]
fn postgres_message(error: &postgres::Error) -> String {
error.as_db_error().map_or_else(
|| format!("{error}"),
|db| format!("{} ({})", db.message(), db.code().code()),
)
}
pub type Result<T> = core::result::Result<T, DrizzleError>;
pub trait ResultExt<T> {
fn with_query<F>(self, ctx: F) -> Result<T>
where
F: FnOnce() -> QueryContext;
}
impl<T, E> ResultExt<T> for core::result::Result<T, E>
where
E: Into<DrizzleError>,
{
fn with_query<F>(self, ctx: F) -> Result<T>
where
F: FnOnce() -> QueryContext,
{
self.map_err(|error| {
let source = error.into();
match source {
DrizzleError::QueryFailed { .. } => source,
other => DrizzleError::QueryFailed {
ctx: Box::new(ctx()),
source: Box::new(other),
},
}
})
}
}
#[cfg(test)]
mod display_tests {
use super::*;
use crate::prelude::ToString;
#[test]
fn query_failed_display_omits_parameter_values() {
let error = Err::<(), _>(DrizzleError::NotFound)
.with_query(|| {
QueryContext::new(
"SELECT * FROM users WHERE email = $1",
&[&"ada@example.com"],
)
})
.unwrap_err();
let message = error.to_string();
assert_eq!(
message,
"No rows found\n sql: SELECT * FROM users WHERE email = $1\n params: 1 bound"
);
let debug = format!("{error:?}");
assert!(!debug.contains("ada@example.com"), "{debug}");
let DrizzleError::QueryFailed { ctx, .. } = error else {
panic!("expected QueryFailed");
};
assert_eq!(ctx.params.as_ref(), ["\"ada@example.com\""]);
}
}
#[cfg(all(test, feature = "driver-error"))]
mod tests {
use super::*;
use std::error::Error as _;
#[test]
fn external_errors_keep_their_source() {
let error =
DrizzleError::external("schema diff", std::io::Error::other("invalid snapshot"));
assert_eq!(error.to_string(), "schema diff: invalid snapshot");
assert_eq!(
error.source().map(ToString::to_string),
Some("invalid snapshot".to_string())
);
}
}
#[cfg(test)]
mod kind_tests {
use super::*;
#[test]
fn sqlite_messages_name_their_kind() {
let cases = [
(
"D1_ERROR: UNIQUE constraint failed: users.email: SQLITE_CONSTRAINT",
ErrorKind::UniqueViolation,
),
(
"FOREIGN KEY constraint failed: SQLITE_CONSTRAINT",
ErrorKind::ForeignKeyViolation,
),
(
"NOT NULL constraint failed: users.name",
ErrorKind::NotNullViolation,
),
(
"CHECK constraint failed: users_age_check",
ErrorKind::CheckViolation,
),
("database is locked", ErrorKind::Busy),
("no such table: users", ErrorKind::Other),
];
for (message, kind) in cases {
assert_eq!(
DrizzleError::Other(message.into()).kind(),
kind,
"{message}"
);
}
}
#[test]
fn a_named_sqlite_check_is_the_constraint() {
let error = DrizzleError::Other(
"D1_ERROR: CHECK constraint failed: positive_age: SQLITE_CONSTRAINT".into(),
);
assert_eq!(error.constraint(), Some("positive_age"));
let error = DrizzleError::Other("UNIQUE constraint failed: users.email".into());
assert_eq!(error.constraint(), None);
}
#[test]
fn aws_data_api_messages_carry_the_sqlstate() {
let error = DrizzleError::Other(
"aws ExecuteStatement: ERROR: duplicate key value violates unique constraint \
\"users_email_key\"; SQLState: 23505"
.into(),
);
assert_eq!(error.kind(), ErrorKind::UniqueViolation);
assert_eq!(error.constraint(), Some("users_email_key"));
let serialization =
DrizzleError::Other("aws ExecuteStatement: ...; SQLState: 40001".into());
assert_eq!(serialization.kind(), ErrorKind::SerializationFailure);
assert!(serialization.is_retryable());
let deadlock = DrizzleError::Other("aws ExecuteStatement: ...; SQLState: 40P01".into());
assert_eq!(deadlock.kind(), ErrorKind::Deadlock);
assert!(deadlock.is_retryable());
}
#[test]
fn row_values_in_a_detail_line_do_not_change_the_kind() {
let error = DrizzleError::Other(
"aws ExecuteStatement: ERROR: null value in column \"name\" of relation \"users\" \
violates not-null constraint\n Detail: Failing row contains (1, database is locked, \
SQLState: 40001, constraint \"fake\").; SQLState: 23502"
.into(),
);
assert_eq!(error.kind(), ErrorKind::NotNullViolation);
assert!(!error.is_retryable());
assert_eq!(error.constraint(), None);
}
#[test]
fn an_unnamed_sqlite_check_is_named_by_its_expression() {
let error = DrizzleError::Other(
"D1_ERROR: CHECK constraint failed: time >= '00:00': SQLITE_CONSTRAINT".into(),
);
assert_eq!(error.constraint(), Some("time >= '00:00'"));
}
#[test]
fn the_query_context_is_looked_through() {
let error: Result<()> = Err(DrizzleError::Other(
"CHECK constraint failed: positive_age".into(),
));
let error = error
.with_query(|| QueryContext::new("INSERT INTO users VALUES (?)", &[&-1]))
.unwrap_err();
assert_eq!(error.kind(), ErrorKind::CheckViolation);
assert_eq!(error.constraint(), Some("positive_age"));
assert_eq!(
error.query().map(|query| query.sql.as_str()),
Some("INSERT INTO users VALUES (?)")
);
assert!(DrizzleError::NotFound.query().is_none());
}
#[test]
fn kinds_say_whether_they_are_constraints_or_retryable() {
assert_eq!(DrizzleError::NotFound.kind(), ErrorKind::NotFound);
for kind in [
ErrorKind::UniqueViolation,
ErrorKind::ForeignKeyViolation,
ErrorKind::NotNullViolation,
ErrorKind::CheckViolation,
] {
assert!(
kind.is_constraint_violation() && !kind.is_retryable(),
"{kind:?}"
);
}
for kind in [
ErrorKind::SerializationFailure,
ErrorKind::Deadlock,
ErrorKind::Busy,
] {
assert!(
kind.is_retryable() && !kind.is_constraint_violation(),
"{kind:?}"
);
}
for kind in [ErrorKind::NotFound, ErrorKind::Other] {
assert!(
!kind.is_retryable() && !kind.is_constraint_violation(),
"{kind:?}"
);
}
}
#[cfg(feature = "rusqlite")]
#[test]
fn rusqlite_extended_codes_name_their_kind() {
let failure = |code: i32, message: &str| {
DrizzleError::Rusqlite(rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error::new(code),
Some(message.into()),
))
};
assert_eq!(
failure(1555, "UNIQUE constraint failed: users.id").kind(),
ErrorKind::UniqueViolation
);
assert_eq!(
failure(787, "FOREIGN KEY constraint failed").kind(),
ErrorKind::ForeignKeyViolation
);
let check = failure(275, "CHECK constraint failed: positive_age");
assert_eq!(check.kind(), ErrorKind::CheckViolation);
assert_eq!(check.constraint(), Some("positive_age"));
assert_eq!(failure(5, "database is locked").kind(), ErrorKind::Busy);
assert_eq!(failure(517, "database is locked").kind(), ErrorKind::Busy);
}
#[cfg(feature = "turso")]
#[test]
fn turso_errors_name_their_kind() {
assert_eq!(
DrizzleError::Turso(turso::Error::Busy("database is locked".into())).kind(),
ErrorKind::Busy
);
assert_eq!(
DrizzleError::Turso(turso::Error::Constraint(
"FOREIGN KEY constraint failed".into()
))
.kind(),
ErrorKind::ForeignKeyViolation
);
let conflict = DrizzleError::Turso(turso::Error::Error("Write-write conflict".into()));
assert_eq!(conflict.kind(), ErrorKind::SerializationFailure);
assert!(conflict.is_retryable());
assert_eq!(
DrizzleError::Turso(turso::Error::BusySnapshot("snapshot is stale".into())).kind(),
ErrorKind::SerializationFailure
);
}
#[cfg(feature = "libsql")]
#[test]
fn libsql_codes_name_their_kind() {
assert_eq!(
DrizzleError::LibSQL(libsql::Error::SqliteFailure(
1299,
"NOT NULL constraint failed: users.name".into()
))
.kind(),
ErrorKind::NotNullViolation
);
let remote = DrizzleError::LibSQL(libsql::Error::RemoteSqliteFailure(
19,
275,
"CHECK constraint failed: positive_age".into(),
));
assert_eq!(remote.kind(), ErrorKind::CheckViolation);
assert_eq!(remote.constraint(), Some("positive_age"));
let hrana = DrizzleError::LibSQL(libsql::Error::Hrana(
"SQLite error: UNIQUE constraint failed: users.email".into(),
));
assert_eq!(hrana.kind(), ErrorKind::UniqueViolation);
}
#[cfg(feature = "driver-error")]
#[test]
fn driver_errors_keep_the_kind_their_adapter_read() {
let error = DrizzleError::driver("MySQL", std::io::Error::other("Deadlock found"))
.classified(ErrorKind::Deadlock, None);
assert_eq!(error.kind(), ErrorKind::Deadlock);
assert!(error.is_retryable());
let error = DrizzleError::driver("MySQL", std::io::Error::other("Duplicate entry"))
.classified(ErrorKind::UniqueViolation, Some("users_email_unique"));
assert_eq!(error.constraint(), Some("users_email_unique"));
let unchanged = DrizzleError::NotFound.classified(ErrorKind::Deadlock, None);
assert_eq!(unchanged.kind(), ErrorKind::NotFound);
}
}