diff --git a/sqlx-core/src/from_row.rs b/sqlx-core/src/from_row.rs index 82ebd9c98e..509e289525 100644 --- a/sqlx-core/src/from_row.rs +++ b/sqlx-core/src/from_row.rs @@ -1,5 +1,4 @@ -use crate::error::Error; -use crate::row::Row; +use crate::{error::Error, row::Row}; /// A record that can be built from a row returned by the database. /// @@ -210,6 +209,48 @@ use crate::row::Row; /// /// In MySql, `BigInt` type matches `i64`, but you can convert it to `u64` by `try_from`. /// +/// #### `json` +/// +/// If your database supports a JSON type, you can leverage `#[sqlx(json)]` +/// to automatically integrate JSON deserialization in your [`FromRow`] implementation using [`serde`](https://docs.rs/serde/latest/serde/). +/// +/// ```rust,ignore +/// #[derive(serde::Deserialize)] +/// struct Data { +/// field1: String, +/// field2: u64 +/// } +/// +/// #[derive(sqlx::FromRow)] +/// struct User { +/// id: i32, +/// name: String, +/// #[sqlx(json)] +/// metadata: Data +/// } +/// ``` +/// +/// Given a query like the following: +/// +/// ```sql +/// SELECT +/// 1 AS id, +/// 'Name' AS name, +/// JSON_OBJECT('field1', 'value1', 'field2', 42) AS metadata +/// ``` +/// +/// The `metadata` field will be deserialized used its `serde::Deserialize` implementation: +/// +/// ```rust,ignore +/// User { +/// id: 1, +/// name: "Name", +/// metadata: Data { +/// field1: "value1", +/// field2: 42 +/// } +/// } +/// ``` pub trait FromRow<'r, R: Row>: Sized { fn from_row(row: &'r R) -> Result; } diff --git a/sqlx-macros-core/src/derives/attributes.rs b/sqlx-macros-core/src/derives/attributes.rs index a0f08b1aab..d4583b988a 100644 --- a/sqlx-macros-core/src/derives/attributes.rs +++ b/sqlx-macros-core/src/derives/attributes.rs @@ -1,9 +1,9 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; -use syn::punctuated::Punctuated; -use syn::spanned::Spanned; -use syn::token::Comma; -use syn::{Attribute, DeriveInput, Field, Lit, Meta, MetaNameValue, NestedMeta, Type, Variant}; +use syn::{ + punctuated::Punctuated, spanned::Spanned, token::Comma, Attribute, DeriveInput, Field, Lit, + Meta, MetaNameValue, NestedMeta, Type, Variant, +}; macro_rules! assert_attribute { ($e:expr, $err:expr, $input:expr) => { @@ -65,6 +65,7 @@ pub struct SqlxChildAttributes { pub flatten: bool, pub try_from: Option, pub skip: bool, + pub json: bool, } pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result { @@ -164,6 +165,7 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result syn::Result default = true, Meta::Path(path) if path.is_ident("flatten") => flatten = true, Meta::Path(path) if path.is_ident("skip") => skip = true, + Meta::Path(path) if path.is_ident("json") => json = true, u => fail!(u, "unexpected attribute"), }, u => fail!(u, "unexpected attribute"), } } } + + if json && flatten { + fail!( + attr, + "Cannot use `json` and `flatten` together on the same field" + ); + } } Ok(SqlxChildAttributes { @@ -201,6 +211,7 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result { - predicates.push(parse_quote!(#ty: ::sqlx::FromRow<#lifetime, R>)); - parse_quote!(<#ty as ::sqlx::FromRow<#lifetime, R>>::from_row(row)) - } - (false, None) => { + let id_s = attributes + .rename + .or_else(|| Some(id.to_string().trim_start_matches("r#").to_owned())) + .map(|s| match container_attributes.rename_all { + Some(pattern) => rename_all(&s, pattern), + None => s, + }) + .unwrap(); + + let expr: Expr = match (attributes.flatten, attributes.try_from, attributes.json) { + // + (false, None, false) => { predicates .push(parse_quote!(#ty: ::sqlx::decode::Decode<#lifetime, R::Database>)); predicates.push(parse_quote!(#ty: ::sqlx::types::Type)); - let id_s = attributes - .rename - .or_else(|| Some(id.to_string().trim_start_matches("r#").to_owned())) - .map(|s| match container_attributes.rename_all { - Some(pattern) => rename_all(&s, pattern), - None => s, - }) - .unwrap(); parse_quote!(row.try_get(#id_s)) } - (true,Some(try_from)) => { + // Flatten + (true, None, false) => { + predicates.push(parse_quote!(#ty: ::sqlx::FromRow<#lifetime, R>)); + parse_quote!(<#ty as ::sqlx::FromRow<#lifetime, R>>::from_row(row)) + } + // Flatten + Try from + (true, Some(try_from), false) => { predicates.push(parse_quote!(#try_from: ::sqlx::FromRow<#lifetime, R>)); parse_quote!(<#try_from as ::sqlx::FromRow<#lifetime, R>>::from_row(row).and_then(|v| <#ty as ::std::convert::TryFrom::<#try_from>>::try_from(v).map_err(|e| ::sqlx::Error::ColumnNotFound("FromRow: try_from failed".to_string())))) } - (false,Some(try_from)) => { + // Flatten + Json + (true, _, true) => { + panic!("Cannot use both flatten and json") + } + // Try from + (false, Some(try_from), false) => { predicates .push(parse_quote!(#try_from: ::sqlx::decode::Decode<#lifetime, R::Database>)); predicates.push(parse_quote!(#try_from: ::sqlx::types::Type)); - let id_s = attributes - .rename - .or_else(|| Some(id.to_string().trim_start_matches("r#").to_owned())) - .map(|s| match container_attributes.rename_all { - Some(pattern) => rename_all(&s, pattern), - None => s, - }) - .unwrap(); parse_quote!(row.try_get(#id_s).and_then(|v| <#ty as ::std::convert::TryFrom::<#try_from>>::try_from(v).map_err(|e| ::sqlx::Error::ColumnNotFound("FromRow: try_from failed".to_string())))) } + // Try from + Json + (false, Some(try_from), true) => { + predicates + .push(parse_quote!(::sqlx::types::Json<#try_from>: ::sqlx::decode::Decode<#lifetime, R::Database>)); + predicates.push(parse_quote!(::sqlx::types::Json<#try_from>: ::sqlx::types::Type)); + + parse_quote!( + row.try_get::<::sqlx::types::Json<_>, _>(#id_s).and_then(|v| + <#ty as ::std::convert::TryFrom::<#try_from>>::try_from(v.0) + .map_err(|e| ::sqlx::Error::ColumnNotFound("FromRow: try_from failed".to_string())) + ) + ) + }, + // Json + (false, None, true) => { + predicates + .push(parse_quote!(::sqlx::types::Json<#ty>: ::sqlx::decode::Decode<#lifetime, R::Database>)); + predicates.push(parse_quote!(::sqlx::types::Json<#ty>: ::sqlx::types::Type)); + + parse_quote!(row.try_get::<::sqlx::types::Json<_>, _>(#id_s).map(|x| x.0)) + }, }; if attributes.default { diff --git a/tests/mysql/macros.rs b/tests/mysql/macros.rs index 24807d5606..6d5070f8cb 100644 --- a/tests/mysql/macros.rs +++ b/tests/mysql/macros.rs @@ -468,4 +468,66 @@ async fn test_try_from_attr_with_complex_type() -> anyhow::Result<()> { Ok(()) } +#[sqlx_macros::test] +async fn test_from_row_json_attr() -> anyhow::Result<()> { + #[derive(serde::Deserialize)] + struct J { + a: u32, + b: u32, + } + + #[derive(sqlx::FromRow)] + struct Record { + #[sqlx(json)] + j: J, + } + + let mut conn = new::().await?; + + let record = sqlx::query_as::<_, Record>("select json_object('a', 1, 'b', 2) as j") + .fetch_one(&mut conn) + .await?; + + assert_eq!(record.j.a, 1); + assert_eq!(record.j.b, 2); + + Ok(()) +} + +#[sqlx_macros::test] +async fn test_from_row_json_try_from_attr() -> anyhow::Result<()> { + #[derive(serde::Deserialize)] + struct J { + a: u32, + b: u32, + } + + // Non-deserializable + struct J2 { + sum: u32, + } + + impl std::convert::From for J2 { + fn from(j: J) -> Self { + Self { sum: j.a + j.b } + } + } + + #[derive(sqlx::FromRow)] + struct Record { + #[sqlx(json, try_from = "J")] + j: J2, + } + + let mut conn = new::().await?; + + let record = sqlx::query_as::<_, Record>("select json_object('a', 1, 'b', 2) as j") + .fetch_one(&mut conn) + .await?; + + assert_eq!(record.j.sum, 3); + + Ok(()) +} + // we don't emit bind parameter type-checks for MySQL so testing the overrides is redundant