diff --git a/Cargo.lock b/Cargo.lock index 7c469e085..b0e51713e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3066,7 +3066,7 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.1" +version = "0.7.2" dependencies = [ "anyhow", "async-std", @@ -3095,7 +3095,7 @@ dependencies = [ [[package]] name = "sqlx-cli" -version = "0.7.1" +version = "0.7.2" dependencies = [ "anyhow", "assert_cmd", @@ -3121,7 +3121,7 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.1" +version = "0.7.2" dependencies = [ "ahash 0.8.3", "async-io", @@ -3307,7 +3307,7 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.1" +version = "0.7.2" dependencies = [ "proc-macro2", "quote", @@ -3318,7 +3318,7 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.1" +version = "0.7.2" dependencies = [ "async-std", "dotenvy", @@ -3343,7 +3343,7 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.1" +version = "0.7.2" dependencies = [ "atoi", "base64 0.21.0", @@ -3388,7 +3388,7 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.1" +version = "0.7.2" dependencies = [ "atoi", "base64 0.21.0", @@ -3434,7 +3434,7 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.1" +version = "0.7.2" dependencies = [ "atoi", "chrono", diff --git a/sqlx-core/src/from_row.rs b/sqlx-core/src/from_row.rs index 509e28952..7bd835c45 100644 --- a/sqlx-core/src/from_row.rs +++ b/sqlx-core/src/from_row.rs @@ -91,6 +91,26 @@ use crate::{error::Error, row::Row}; /// will set the value of the field `location` to the default value of `Option`, /// which is `None`. /// +/// Moreover, if the struct has an implementation for [`Default`], you can use the `default`` +/// attribute at the struct level rather than for each single field. If a field does not appear in the result, +/// its value is taken from the `Default` implementation for the struct. +/// For example: +/// +/// ```rust, ignore +/// #[derive(Default, sqlx::FromRow)] +/// #[sqlx(default)] +/// struct Options { +/// option_a: Option, +/// option_b: Option, +/// option_c: Option, +/// } +/// ``` +/// +/// For a derived `Default` implementation this effectively populates each missing field +/// with `Default::default()`, but a manual `Default` implementation can provide +/// different placeholder values, if applicable. +/// +/// This is similar to how `#[serde(default)]` behaves. /// ### `flatten` /// /// If you want to handle a field that implements [`FromRow`], diff --git a/sqlx-macros-core/src/derives/attributes.rs b/sqlx-macros-core/src/derives/attributes.rs index d4583b988..554d7a4eb 100644 --- a/sqlx-macros-core/src/derives/attributes.rs +++ b/sqlx-macros-core/src/derives/attributes.rs @@ -57,6 +57,7 @@ pub struct SqlxContainerAttributes { pub rename_all: Option, pub repr: Option, pub no_pg_array: bool, + pub default: bool, } pub struct SqlxChildAttributes { @@ -74,6 +75,7 @@ pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result syn::Result { + try_set!(default, true, value) + } + u => fail!(u, "unexpected attribute"), }, u => fail!(u, "unexpected attribute"), @@ -156,6 +162,7 @@ pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result; + + if container_attributes.default { + predicates.push(parse_quote!(#ident: ::std::default::Default)); + default_instance = Some(parse_quote!( + let __default = #ident::default(); + )); + } else { + default_instance = None; + } + let reads: Vec = fields .iter() .filter_map(|field| -> Option { @@ -148,6 +159,13 @@ fn expand_derive_from_row_struct( }, e => ::std::result::Result::Err(e) })?;)) + } else if container_attributes.default { + Some(parse_quote!(let #id: #ty = #expr.or_else(|e| match e { + ::sqlx::Error::ColumnNotFound(_) => { + ::std::result::Result::Ok(__default.#id) + }, + e => ::std::result::Result::Err(e) + })?;)) } else { Some(parse_quote!( let #id: #ty = #expr?; @@ -164,6 +182,8 @@ fn expand_derive_from_row_struct( #[automatically_derived] impl #impl_generics ::sqlx::FromRow<#lifetime, R> for #ident #ty_generics #where_clause { fn from_row(row: &#lifetime R) -> ::sqlx::Result { + #default_instance + #(#reads)* ::std::result::Result::Ok(#ident { diff --git a/tests/postgres/derives.rs b/tests/postgres/derives.rs index 47eb7854c..dd05e9290 100644 --- a/tests/postgres/derives.rs +++ b/tests/postgres/derives.rs @@ -621,6 +621,41 @@ async fn test_default() -> anyhow::Result<()> { Ok(()) } +#[cfg(feature = "macros")] +#[sqlx_macros::test] +async fn test_struct_default() -> anyhow::Result<()> { + #[derive(Debug, sqlx::FromRow)] + #[sqlx(default)] + struct HasDefault { + not_default: Option, + default_a: Option, + default_b: Option, + } + + impl Default for HasDefault { + fn default() -> HasDefault { + HasDefault { + not_default: None, + default_a: None, + default_b: Some(0), + } + } + } + + let mut conn = new::().await?; + + let has_default: HasDefault = sqlx::query_as(r#"SELECT 1 AS not_default"#) + .fetch_one(&mut conn) + .await?; + println!("{has_default:?}"); + + assert_eq!(has_default.not_default, Some(1)); + assert_eq!(has_default.default_a, None); + assert_eq!(has_default.default_b, Some(0)); + + Ok(()) +} + #[cfg(feature = "macros")] #[sqlx_macros::test] async fn test_flatten() -> anyhow::Result<()> {