Skip to content

Commit

Permalink
Add json attribute to FromRow derive
Browse files Browse the repository at this point in the history
  • Loading branch information
95ulisse committed Sep 26, 2022
1 parent 76ae286 commit d45c4ea
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 21 deletions.
11 changes: 11 additions & 0 deletions sqlx-macros/src/derives/attributes.rs
Expand Up @@ -72,6 +72,7 @@ pub struct SqlxChildAttributes {
pub default: bool,
pub flatten: bool,
pub try_from: Option<Ident>,
pub json: bool,
}

pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContainerAttributes> {
Expand Down Expand Up @@ -181,6 +182,7 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result<SqlxChildAttri
let mut default = false;
let mut try_from = None;
let mut flatten = false;
let mut json = false;

for attr in input.iter().filter(|a| a.path.is_ident("sqlx")) {
let meta = attr
Expand All @@ -203,19 +205,28 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result<SqlxChildAttri
}) if path.is_ident("try_from") => try_set!(try_from, val.parse()?, value),
Meta::Path(path) if path.is_ident("default") => default = true,
Meta::Path(path) if path.is_ident("flatten") => flatten = 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 {
rename,
default,
flatten,
try_from,
json,
})
}

Expand Down
64 changes: 43 additions & 21 deletions sqlx-macros/src/derives/row.rs
Expand Up @@ -72,45 +72,67 @@ fn expand_derive_from_row_struct(
let attributes = parse_child_attributes(&field.attrs).unwrap();
let ty = &field.ty;

let expr: Expr = match (attributes.flatten, attributes.try_from) {
(true, 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) {
// Flatten
(true, None, false) => {
predicates.push(parse_quote!(#ty: ::sqlx::FromRow<#lifetime, R>));
parse_quote!(<#ty as ::sqlx::FromRow<#lifetime, R>>::from_row(row))
}
(false, None) => {
// <No attributes>
(false, None, false) => {
predicates
.push(parse_quote!(#ty: ::sqlx::decode::Decode<#lifetime, R::Database>));
predicates.push(parse_quote!(#ty: ::sqlx::types::Type<R::Database>));

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 + 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)) => {
// 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<R::Database>));

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()))))
}
// 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<R::Database>));

parse_quote!(row.try_get::<::sqlx::types::Json<_>, _>(#id_s).map(|x| x.0))
},
// 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<R::Database>));

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()))
)
)
},
// Flatten + Json
(true, _, true) => {
panic!("Cannot use both flatten and json")
}
};

if attributes.default {
Expand Down
62 changes: 62 additions & 0 deletions tests/mysql/macros.rs
Expand Up @@ -435,4 +435,66 @@ async fn test_try_from_attr_with_flatten() -> 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::<MySql>().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<J> 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::<MySql>().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

0 comments on commit d45c4ea

Please sign in to comment.