Skip to content

Commit

Permalink
refactor: lift type mappings into driver crates (#2970)
Browse files Browse the repository at this point in the history
Motivated by #2917
  • Loading branch information
abonander authored Mar 30, 2024
1 parent 1c7b3d0 commit 02c68a4
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 150 deletions.
1 change: 1 addition & 0 deletions sqlx-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub mod raw_sql;
pub mod row;
pub mod rt;
pub mod sync;
pub mod type_checking;
pub mod type_info;
pub mod value;

Expand Down
188 changes: 188 additions & 0 deletions sqlx-core/src/type_checking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
use crate::database::Database;
use crate::decode::Decode;
use crate::type_info::TypeInfo;
use crate::value::Value;
use std::any::Any;
use std::fmt;
use std::fmt::{Debug, Formatter};

/// The type of query parameter checking done by a SQL database.
#[derive(PartialEq, Eq)]
pub enum ParamChecking {
/// Parameter checking is weak or nonexistent (uses coercion or allows mismatches).
Weak,
/// Parameter checking is strong (types must match exactly).
Strong,
}

/// Type-checking extensions for the `Database` trait.
///
/// Mostly supporting code for the macros, and for `Debug` impls.
pub trait TypeChecking: Database {
/// Describes how the database in question typechecks query parameters.
const PARAM_CHECKING: ParamChecking;

/// Get the full path of the Rust type that corresponds to the given `TypeInfo`, if applicable.
///
/// If the type has a borrowed equivalent suitable for query parameters,
/// this is that borrowed type.
fn param_type_for_id(id: &Self::TypeInfo) -> Option<&'static str>;

/// Get the full path of the Rust type that corresponds to the given `TypeInfo`, if applicable.
///
/// Always returns the owned version of the type, suitable for decoding from `Row`.
fn return_type_for_id(id: &Self::TypeInfo) -> Option<&'static str>;

/// Get the name of the Cargo feature gate that must be enabled to process the given `TypeInfo`,
/// if applicable.
fn get_feature_gate(info: &Self::TypeInfo) -> Option<&'static str>;

/// If `value` is a well-known type, decode and format it using `Debug`.
///
/// If `value` is not a well-known type or could not be decoded, the reason is printed instead.
fn fmt_value_debug(value: &<Self as Database>::Value) -> FmtValue<'_, Self>;
}

/// An adapter for [`Value`] which attempts to decode the value and format it when printed using [`Debug`].
pub struct FmtValue<'v, DB>
where
DB: Database,
{
value: &'v <DB as Database>::Value,
fmt: fn(&'v <DB as Database>::Value, &mut Formatter<'_>) -> fmt::Result,
}

impl<'v, DB> FmtValue<'v, DB>
where
DB: Database,
{
// This API can't take `ValueRef` directly as it would need to pass it to `Decode` by-value,
// which means taking ownership of it. We cannot rely on a `Clone` impl because `SqliteValueRef` doesn't have one.
/// When printed with [`Debug`], attempt to decode `value` as the given type `T` and format it using [`Debug`].
///
/// If `value` could not be decoded as `T`, the reason is printed instead.
pub fn debug<T>(value: &'v <DB as Database>::Value) -> Self
where
T: Decode<'v, DB> + Debug + Any,
{
Self {
value,
fmt: |value, f| {
let info = value.type_info();

match T::decode(value.as_ref()) {
Ok(value) => Debug::fmt(&value, f),
Err(e) => f.write_fmt(format_args!(
"(error decoding SQL type {} as {}: {e:?})",
info.name(),
std::any::type_name::<T>()
)),
}
},
}
}

/// If the type to be decoded is not known or not supported, print the SQL type instead,
/// as well as any applicable SQLx feature that needs to be enabled.
pub fn unknown(value: &'v <DB as Database>::Value) -> Self
where
DB: TypeChecking,
{
Self {
value,
fmt: |value, f| {
let info = value.type_info();

if let Some(feature_gate) = <DB as TypeChecking>::get_feature_gate(&info) {
return f.write_fmt(format_args!(
"(unknown SQL type {}: SQLx feature {feature_gate} not enabled)",
info.name()
));
}

f.write_fmt(format_args!("(unknown SQL type {})", info.name()))
},
}
}
}

impl<'v, DB> Debug for FmtValue<'v, DB>
where
DB: Database,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
(self.fmt)(&self.value, f)
}
}

#[doc(hidden)]
#[macro_export]
macro_rules! select_input_type {
($ty:ty, $input:ty) => {
stringify!($input)
};
($ty:ty) => {
stringify!($ty)
};
}

#[macro_export]
macro_rules! impl_type_checking {
(
$database:path {
$($(#[$meta:meta])? $ty:ty $(| $input:ty)?),*$(,)?
},
ParamChecking::$param_checking:ident,
feature-types: $ty_info:ident => $get_gate:expr,
) => {
impl $crate::type_checking::TypeChecking for $database {
const PARAM_CHECKING: $crate::type_checking::ParamChecking = $crate::type_checking::ParamChecking::$param_checking;

fn param_type_for_id(info: &Self::TypeInfo) -> Option<&'static str> {
match () {
$(
$(#[$meta])?
_ if <$ty as sqlx_core::types::Type<$database>>::type_info() == *info => Some($crate::select_input_type!($ty $(, $input)?)),
)*
$(
$(#[$meta])?
_ if <$ty as sqlx_core::types::Type<$database>>::compatible(info) => Some(select_input_type!($ty $(, $input)?)),
)*
_ => None
}
}

fn return_type_for_id(info: &Self::TypeInfo) -> Option<&'static str> {
match () {
$(
$(#[$meta])?
_ if <$ty as sqlx_core::types::Type<$database>>::type_info() == *info => Some(stringify!($ty)),
)*
$(
$(#[$meta])?
_ if <$ty as sqlx_core::types::Type<$database>>::compatible(info) => Some(stringify!($ty)),
)*
_ => None
}
}

fn get_feature_gate($ty_info: &Self::TypeInfo) -> Option<&'static str> {
$get_gate
}

fn fmt_value_debug(value: &Self::Value) -> $crate::type_checking::FmtValue<Self> {
use $crate::value::Value;

let info = value.type_info();

match () {
$(
$(#[$meta])?
_ if <$ty as sqlx_core::types::Type<$database>>::compatible(&info) => $crate::type_checking::FmtValue::debug::<$ty>(value),
)*
_ => $crate::type_checking::FmtValue::unknown(value),
}
}
}
};
}
71 changes: 71 additions & 0 deletions sqlx-macros-core/src/database/impls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
macro_rules! impl_database_ext {
(
$database:path,
row: $row:path,
$(describe-blocking: $describe:path,)?
) => {
impl $crate::database::DatabaseExt for $database {
const DATABASE_PATH: &'static str = stringify!($database);
const ROW_PATH: &'static str = stringify!($row);
impl_describe_blocking!($database, $($describe)?);
}
}
}

macro_rules! impl_describe_blocking {
($database:path $(,)?) => {
fn describe_blocking(
query: &str,
database_url: &str,
) -> sqlx_core::Result<sqlx_core::describe::Describe<Self>> {
use $crate::database::CachingDescribeBlocking;

// This can't be a provided method because the `static` can't reference `Self`.
static CACHE: CachingDescribeBlocking<$database> = CachingDescribeBlocking::new();

CACHE.describe(query, database_url)
}
};
($database:path, $describe:path) => {
fn describe_blocking(
query: &str,
database_url: &str,
) -> sqlx_core::Result<sqlx_core::describe::Describe<Self>> {
$describe(query, database_url)
}
};
}

// The paths below will also be emitted from the macros, so they need to match the final facade.
mod sqlx {
#[cfg(feature = "mysql")]
pub use sqlx_mysql as mysql;

#[cfg(feature = "postgres")]
pub use sqlx_postgres as postgres;

#[cfg(feature = "sqlite")]
pub use sqlx_sqlite as sqlite;
}

// NOTE: type mappings have been moved to `src/type_checking.rs` in their respective driver crates.
#[cfg(feature = "mysql")]
impl_database_ext! {
sqlx::mysql::MySql,
row: sqlx::mysql::MySqlRow,
}

#[cfg(feature = "postgres")]
impl_database_ext! {
sqlx::postgres::Postgres,
row: sqlx::postgres::PgRow,
}

#[cfg(feature = "sqlite")]
impl_database_ext! {
sqlx::sqlite::Sqlite,
row: sqlx::sqlite::SqliteRow,
// Since proc-macros don't benefit from async, we can make a describe call directly
// which also ensures that the database is closed afterwards, regardless of errors.
describe-blocking: sqlx_sqlite::describe_blocking,
}
Loading

0 comments on commit 02c68a4

Please sign in to comment.