mod backend;
mod ddl;
mod migration;
mod parameter;
mod query;
mod table;
use crate::{Context, ValidationError, ValidationErrors};
pub use backend::{Dialect, SqlBackend};
pub use migration::{
MigrationChange, MigrationOptions, MigrationPlan, MigrationReport, SqlDefault,
};
pub use parameter::{AutoKey, Key, SqlParameter, SqlType, SqlValue};
pub use query::Select;
use sqlx::{pool::PoolConnection, Database};
pub use table::{SqlModel, SqlRow};
#[cfg(feature = "axum")]
const SQLITE_CONSTRAINT: u32 = 19;
pub trait ValidationConnection {
const RELEASES_POOL_SLOT: bool;
}
impl<DB: Database> ValidationConnection for PoolConnection<DB> {
const RELEASES_POOL_SLOT: bool = true;
}
#[cfg(feature = "sqlite")]
impl ValidationConnection for &mut sqlx::SqliteConnection {
const RELEASES_POOL_SLOT: bool = false;
}
#[cfg(feature = "postgres")]
impl ValidationConnection for &mut sqlx::PgConnection {
const RELEASES_POOL_SLOT: bool = false;
}
#[cfg(feature = "mysql")]
impl ValidationConnection for &mut sqlx::MySqlConnection {
const RELEASES_POOL_SLOT: bool = false;
}
fn check_validation_connection<T: SqlModel, C: ValidationConnection>() -> Result<(), SqlError> {
if !C::RELEASES_POOL_SLOT && T::async_required(&mut std::collections::HashSet::new()) {
return Err(field_error(
"sql_async_connection",
"async model checks require a pool; a borrowed connection or transaction retains its connection",
));
}
Ok(())
}
#[derive(Debug)]
pub enum SqlError {
Validation(ValidationErrors),
Database(sqlx::Error),
NotFound,
InvalidColumn(String),
Migration {
code: &'static str,
message: String,
},
}
impl std::fmt::Display for SqlError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Validation(e) => e.fmt(f),
Self::Database(e) => e.fmt(f),
Self::NotFound => f.write_str("row not found"),
Self::InvalidColumn(name) => write!(f, "unknown model column: {name}"),
Self::Migration { code, message } => write!(f, "{code}: {message}"),
}
}
}
impl std::error::Error for SqlError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Validation(e) => Some(e),
Self::Database(e) => Some(e),
_ => None,
}
}
}
impl From<ValidationErrors> for SqlError {
fn from(e: ValidationErrors) -> Self {
Self::Validation(e)
}
}
impl From<sqlx::Error> for SqlError {
fn from(e: sqlx::Error) -> Self {
Self::Database(e)
}
}
impl SqlError {
pub fn at(self, field: &str) -> Self {
match self {
Self::Validation(e) => Self::Validation(Context::prepend_errors(e, field)),
other => other,
}
}
}
pub(crate) fn field_error(code: &str, message: &str) -> SqlError {
SqlError::Validation(ValidationError::new(code, message).into())
}
#[derive(Debug, Clone, Copy)]
pub struct Reference {
pub table: &'static str,
pub column: &'static str,
pub on_delete: OnDelete,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum OnDelete {
#[default]
NoAction,
Restrict,
Cascade,
SetNull,
}
impl OnDelete {
fn sql(self) -> &'static str {
match self {
Self::NoAction => "NO ACTION",
Self::Restrict => "RESTRICT",
Self::Cascade => "CASCADE",
Self::SetNull => "SET NULL",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Column {
pub name: &'static str,
pub sql_type: SqlType,
pub nullable: bool,
pub primary_key: bool,
pub auto_increment: bool,
pub unique: bool,
pub index: bool,
pub references: Option<Reference>,
pub length: Option<usize>,
pub rename_from: Option<&'static str>,
pub default: Option<SqlDefault>,
}
impl Column {
pub fn mysql_length(&self) -> Option<usize> {
if matches!(self.sql_type, SqlType::Text | SqlType::Blob)
&& (self.length.is_some()
|| self.primary_key
|| self.unique
|| self.index
|| self.references.is_some())
{
Some(self.length.unwrap_or(191))
} else {
None
}
}
}
#[doc(hidden)]
pub fn bind_value<DB: SqlBackend, V: SqlValue>(
value: &V,
column: &Column,
query: &mut sqlx::QueryBuilder<DB>,
) -> Result<(), SqlError> {
if DB::DIALECT == Dialect::MySql {
let parameter = value.to_parameter()?;
if let Some(limit) = column.mysql_length() {
let length = match ¶meter {
SqlParameter::Text(value) => value.chars().count(),
SqlParameter::Blob(value) => value.len(),
_ => 0,
};
if length > limit {
return Err(field_error(
"sql_length",
"value exceeds the database column length",
));
}
}
DB::bind_parameter(parameter, query);
Ok(())
} else {
DB::bind(value, query)
}
}
#[derive(Debug, Clone, Copy)]
pub enum Compare {
Equal,
NotEqual,
Less,
LessOrEqual,
Greater,
GreaterOrEqual,
}
impl Compare {
fn sql(self) -> &'static str {
match self {
Self::Equal => "=",
Self::NotEqual => "<>",
Self::Less => "<",
Self::LessOrEqual => "<=",
Self::Greater => ">",
Self::GreaterOrEqual => ">=",
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Order {
Ascending,
Descending,
}
#[cfg(feature = "axum")]
impl ::axum::response::IntoResponse for SqlError {
fn into_response(self) -> ::axum::response::Response {
use ::axum::http::StatusCode;
let (status, code, message) = match self {
Self::Validation(errors) => {
return (
StatusCode::UNPROCESSABLE_ENTITY,
::axum::Json(crate::json!({"detail":errors.to_json()})),
)
.into_response()
}
Self::NotFound => (StatusCode::NOT_FOUND, "not_found", "row not found"),
Self::InvalidColumn(_) => (
StatusCode::BAD_REQUEST,
"sql_column",
"unknown model column",
),
Self::Database(sqlx::Error::Database(ref error))
if matches!(
error.kind(),
sqlx::error::ErrorKind::UniqueViolation
| sqlx::error::ErrorKind::ForeignKeyViolation
| sqlx::error::ErrorKind::NotNullViolation
| sqlx::error::ErrorKind::CheckViolation
| sqlx::error::ErrorKind::ExclusionViolation
) || error.code().is_some_and(|code| code.starts_with("23"))
|| error
.code()
.and_then(|code| code.parse::<u32>().ok())
.is_some_and(|code| code & 0xff == SQLITE_CONSTRAINT) =>
{
(
StatusCode::CONFLICT,
"sql_constraint",
"database constraint violation",
)
}
Self::Database(_) | Self::Migration { .. } => (
StatusCode::INTERNAL_SERVER_ERROR,
"sql_error",
"database operation failed",
),
};
let error = ValidationError::new(code, message);
(status, ::axum::Json(crate::json!({"detail":[error]}))).into_response()
}
}
#[doc(hidden)]
#[macro_export]
#[cfg(feature = "postgres")]
macro_rules! __rydantic_postgres { ($($item:tt)*) => { $($item)* }; }
#[doc(hidden)]
#[macro_export]
#[cfg(not(feature = "postgres"))]
macro_rules! __rydantic_postgres {
($($item:tt)*) => {};
}
#[doc(hidden)]
#[macro_export]
#[cfg(feature = "mysql")]
macro_rules! __rydantic_mysql { ($($item:tt)*) => { $($item)* }; }
#[doc(hidden)]
#[macro_export]
#[cfg(not(feature = "mysql"))]
macro_rules! __rydantic_mysql {
($($item:tt)*) => {};
}
#[doc(hidden)]
#[macro_export]
#[cfg(feature = "sqlite")]
macro_rules! __rydantic_sqlite { ($($item:tt)*) => { $($item)* }; }
#[doc(hidden)]
#[macro_export]
#[cfg(not(feature = "sqlite"))]
macro_rules! __rydantic_sqlite {
($($item:tt)*) => {};
}