saola-user-facing-errors 0.1.0

User-facing error types for Saola ORM
Documentation
use crate::{KnownError, common, query_engine};
use indoc::formatdoc;
use quaint::connector::NativeConnectionInfo;
use quaint::error::ErrorKind;

#[cfg(any(
    feature = "mssql-native",
    feature = "mysql-native",
    feature = "postgresql-native",
    feature = "sqlite-native"
))]
use quaint::error::NativeErrorKind;

impl From<&quaint::error::DatabaseConstraint> for query_engine::DatabaseConstraint {
    fn from(other: &quaint::error::DatabaseConstraint) -> Self {
        match other {
            quaint::error::DatabaseConstraint::Fields(fields) => Self::Fields(fields.to_vec()),
            quaint::error::DatabaseConstraint::Index(index) => Self::Index(index.to_string()),
            quaint::error::DatabaseConstraint::ForeignKey => Self::ForeignKey,
            quaint::error::DatabaseConstraint::CannotParse => Self::CannotParse,
        }
    }
}

impl From<quaint::error::DatabaseConstraint> for query_engine::DatabaseConstraint {
    fn from(other: quaint::error::DatabaseConstraint) -> Self {
        match other {
            quaint::error::DatabaseConstraint::Fields(fields) => Self::Fields(fields.to_vec()),
            quaint::error::DatabaseConstraint::Index(index) => Self::Index(index),
            quaint::error::DatabaseConstraint::ForeignKey => Self::ForeignKey,
            quaint::error::DatabaseConstraint::CannotParse => Self::CannotParse,
        }
    }
}

pub fn invalid_connection_string_description(error_details: &str) -> String {
    let docs = r#"https://www.prisma.io/docs/reference/database-reference/connection-urls"#;

    let details = formatdoc! {r#"
            {} in database URL. Please refer to the documentation in {} for constructing a correct
            connection string. In some cases, certain characters must be escaped. Please
            check the string for any illegal characters."#, error_details, docs};

    details.replace('\n', " ")
}

pub fn render_quaint_error(kind: &ErrorKind, connection_info: Option<&NativeConnectionInfo>) -> Option<KnownError> {
    match kind {
        ErrorKind::DatabaseNotReachable { database_location } => Some(KnownError::new(common::DatabaseNotReachable {
            database_location: database_location.to_string(),
        })),

        ErrorKind::DatabaseDoesNotExist { db_name } => Some(KnownError::new(common::DatabaseDoesNotExist {
            database_name: db_name.to_string(),
        })),

        ErrorKind::DatabaseAccessDenied { db_name } => Some(KnownError::new(common::DatabaseAccessDenied {
            database_name: db_name.to_string(),
        })),

        ErrorKind::DatabaseAlreadyExists { db_name } => Some(KnownError::new(common::DatabaseAlreadyExists {
            database_name: db_name.to_string(),
        })),

        ErrorKind::AuthenticationFailed { user } => Some(KnownError::new(common::IncorrectDatabaseCredentials {
            database_user: user.to_string(),
        })),

        ErrorKind::SocketTimeout => {
            let extra_hint = match connection_info {
                #[cfg(feature = "postgresql-native")]
                Some(NativeConnectionInfo::Postgres(_)) => {
                    "— see https://pris.ly/d/postgresql-connector for more details"
                }
                #[cfg(feature = "mysql-native")]
                Some(NativeConnectionInfo::Mysql(_)) => "— see https://pris.ly/d/mysql-connector for more details",
                #[cfg(feature = "mssql-native")]
                Some(NativeConnectionInfo::Mssql(_)) => "— see https://pris.ly/d/mssql-connector for more details",
                #[cfg(feature = "sqlite-native")]
                Some(NativeConnectionInfo::Sqlite { .. } | NativeConnectionInfo::InMemorySqlite { .. }) => {
                    "— see https://pris.ly/d/sqlite-connector for more details"
                }
                _ => "",
            };

            Some(KnownError::new(common::DatabaseOperationTimeout {
                extra_hint: extra_hint.into(),
            }))
        }
        ErrorKind::TableDoesNotExist { table: model } => Some(KnownError::new(common::InvalidModel {
            model: format!("{model}"),
            kind: common::ModelKind::Table,
        })),
        ErrorKind::UniqueConstraintViolation { constraint } => {
            Some(KnownError::new(query_engine::UniqueKeyViolation {
                constraint: constraint.into(),
            }))
        }

        ErrorKind::DatabaseUrlIsInvalid(details) => Some(KnownError::new(common::InvalidConnectionString {
            details: details.to_owned(),
        })),

        ErrorKind::LengthMismatch { column } => Some(KnownError::new(query_engine::InputValueTooLong {
            column_name: format!("{column}"),
        })),

        ErrorKind::ValueOutOfRange { message } => Some(KnownError::new(query_engine::ValueOutOfRange {
            details: message.clone(),
        })),

        ErrorKind::TlsConnectionError { message } => Some(KnownError::new(common::TlsConnectionError {
            message: message.to_string(),
        })),

        ErrorKind::ConnectionClosed => Some(KnownError::new(common::ConnectionClosed)),

        #[cfg(any(
            feature = "mssql-native",
            feature = "mysql-native",
            feature = "postgresql-native",
            feature = "sqlite-native"
        ))]
        ErrorKind::Native(native_error_kind) => match (native_error_kind, connection_info) {
            #[cfg(feature = "postgresql-native")]
            (NativeErrorKind::ConnectionError(_), Some(NativeConnectionInfo::Postgres(url))) => {
                Some(KnownError::new(common::DatabaseNotReachable {
                    database_location: format!("{}:{}", url.host(), url.port()),
                }))
            }
            #[cfg(feature = "mysql-native")]
            (NativeErrorKind::ConnectionError(_), Some(NativeConnectionInfo::Mysql(url))) => {
                Some(KnownError::new(common::DatabaseNotReachable {
                    database_location: format!("{}:{}", url.host(), url.port()),
                }))
            }
            #[cfg(feature = "mssql-native")]
            (NativeErrorKind::ConnectionError(_), Some(NativeConnectionInfo::Mssql(url))) => {
                Some(KnownError::new(common::DatabaseNotReachable {
                    database_location: format!("{}:{}", url.host(), url.port()),
                }))
            }
            (NativeErrorKind::TlsError { message }, _) => Some(KnownError::new(common::TlsConnectionError {
                message: message.into(),
            })),
            #[cfg(feature = "postgresql-native")]
            (NativeErrorKind::ConnectTimeout, Some(NativeConnectionInfo::Postgres(url))) => {
                Some(KnownError::new(common::DatabaseNotReachable {
                    database_location: format!("{}:{}", url.host(), url.port()),
                }))
            }
            #[cfg(feature = "mysql-native")]
            (NativeErrorKind::ConnectTimeout, Some(NativeConnectionInfo::Mysql(url))) => {
                Some(KnownError::new(common::DatabaseNotReachable {
                    database_location: format!("{}:{}", url.host(), url.port()),
                }))
            }
            #[cfg(feature = "mssql-native")]
            (NativeErrorKind::ConnectTimeout, Some(NativeConnectionInfo::Mssql(url))) => {
                Some(KnownError::new(common::DatabaseNotReachable {
                    database_location: format!("{}:{}", url.host(), url.port()),
                }))
            }
            (NativeErrorKind::PoolTimeout { max_open, timeout, .. }, _) => {
                Some(KnownError::new(query_engine::PoolTimeout {
                    connection_limit: *max_open,
                    timeout: *timeout,
                }))
            }
            (NativeErrorKind::ConnectionClosed, _) => Some(KnownError::new(common::ConnectionClosed)),
            _ => unreachable!(),
        },

        _ => None,
    }
}