mirror of
https://github.com/launchbadge/sqlx.git
synced 2025-10-02 15:25:32 +00:00
Add json(nullable) macro attribute (#3677)
* add json optional attribute parser and expansion * rename attribute * add test * fix tests * fix lints * Add docs
This commit is contained in:
parent
546ec960a9
commit
2aab4cd237
@ -271,6 +271,32 @@ use crate::{error::Error, row::Row};
|
|||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// By default the `#[sqlx(json)]` attribute will assume that the underlying database row is
|
||||||
|
/// _not_ NULL. This can cause issues when your field type is an `Option<T>` because this would be
|
||||||
|
/// represented as the _not_ NULL (in terms of DB) JSON value of `null`.
|
||||||
|
///
|
||||||
|
/// If you wish to describe a database row which _is_ NULLable but _cannot_ contain the JSON value `null`,
|
||||||
|
/// use the `#[sqlx(json(nullable))]` attrubute.
|
||||||
|
///
|
||||||
|
/// For example
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// #[derive(serde::Deserialize)]
|
||||||
|
/// struct Data {
|
||||||
|
/// field1: String,
|
||||||
|
/// field2: u64
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[derive(sqlx::FromRow)]
|
||||||
|
/// struct User {
|
||||||
|
/// id: i32,
|
||||||
|
/// name: String,
|
||||||
|
/// #[sqlx(json(nullable))]
|
||||||
|
/// metadata: Option<Data>
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
/// Would describe a database field which _is_ NULLable but if it exists it must be the JSON representation of `Data`
|
||||||
|
/// and cannot be the JSON value `null`
|
||||||
pub trait FromRow<'r, R: Row>: Sized {
|
pub trait FromRow<'r, R: Row>: Sized {
|
||||||
fn from_row(row: &'r R) -> Result<Self, Error>;
|
fn from_row(row: &'r R) -> Result<Self, Error>;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
use proc_macro2::{Ident, Span, TokenStream};
|
use proc_macro2::{Ident, Span, TokenStream};
|
||||||
use quote::quote_spanned;
|
use quote::quote_spanned;
|
||||||
use syn::{
|
use syn::{
|
||||||
punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr, Meta, Token, Type,
|
parenthesized, punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr,
|
||||||
Variant,
|
Meta, Token, Type, Variant,
|
||||||
};
|
};
|
||||||
|
|
||||||
macro_rules! assert_attribute {
|
macro_rules! assert_attribute {
|
||||||
@ -61,13 +61,18 @@ pub struct SqlxContainerAttributes {
|
|||||||
pub default: bool,
|
pub default: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum JsonAttribute {
|
||||||
|
NonNullable,
|
||||||
|
Nullable,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SqlxChildAttributes {
|
pub struct SqlxChildAttributes {
|
||||||
pub rename: Option<String>,
|
pub rename: Option<String>,
|
||||||
pub default: bool,
|
pub default: bool,
|
||||||
pub flatten: bool,
|
pub flatten: bool,
|
||||||
pub try_from: Option<Type>,
|
pub try_from: Option<Type>,
|
||||||
pub skip: bool,
|
pub skip: bool,
|
||||||
pub json: bool,
|
pub json: Option<JsonAttribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContainerAttributes> {
|
pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContainerAttributes> {
|
||||||
@ -144,7 +149,7 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result<SqlxChildAttri
|
|||||||
let mut try_from = None;
|
let mut try_from = None;
|
||||||
let mut flatten = false;
|
let mut flatten = false;
|
||||||
let mut skip: bool = false;
|
let mut skip: bool = false;
|
||||||
let mut json = false;
|
let mut json = None;
|
||||||
|
|
||||||
for attr in input.iter().filter(|a| a.path().is_ident("sqlx")) {
|
for attr in input.iter().filter(|a| a.path().is_ident("sqlx")) {
|
||||||
attr.parse_nested_meta(|meta| {
|
attr.parse_nested_meta(|meta| {
|
||||||
@ -163,13 +168,21 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result<SqlxChildAttri
|
|||||||
} else if meta.path.is_ident("skip") {
|
} else if meta.path.is_ident("skip") {
|
||||||
skip = true;
|
skip = true;
|
||||||
} else if meta.path.is_ident("json") {
|
} else if meta.path.is_ident("json") {
|
||||||
json = true;
|
if meta.input.peek(syn::token::Paren) {
|
||||||
|
let content;
|
||||||
|
parenthesized!(content in meta.input);
|
||||||
|
let literal: Ident = content.parse()?;
|
||||||
|
assert_eq!(literal.to_string(), "nullable", "Unrecognized `json` attribute. Valid values are `json` or `json(nullable)`");
|
||||||
|
json = Some(JsonAttribute::Nullable);
|
||||||
|
} else {
|
||||||
|
json = Some(JsonAttribute::NonNullable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if json && flatten {
|
if json.is_some() && flatten {
|
||||||
fail!(
|
fail!(
|
||||||
attr,
|
attr,
|
||||||
"Cannot use `json` and `flatten` together on the same field"
|
"Cannot use `json` and `flatten` together on the same field"
|
||||||
|
@ -6,7 +6,7 @@ use syn::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
attributes::{parse_child_attributes, parse_container_attributes},
|
attributes::{parse_child_attributes, parse_container_attributes, JsonAttribute},
|
||||||
rename_all,
|
rename_all,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -99,7 +99,7 @@ fn expand_derive_from_row_struct(
|
|||||||
|
|
||||||
let expr: Expr = match (attributes.flatten, attributes.try_from, attributes.json) {
|
let expr: Expr = match (attributes.flatten, attributes.try_from, attributes.json) {
|
||||||
// <No attributes>
|
// <No attributes>
|
||||||
(false, None, false) => {
|
(false, None, None) => {
|
||||||
predicates
|
predicates
|
||||||
.push(parse_quote!(#ty: ::sqlx::decode::Decode<#lifetime, R::Database>));
|
.push(parse_quote!(#ty: ::sqlx::decode::Decode<#lifetime, R::Database>));
|
||||||
predicates.push(parse_quote!(#ty: ::sqlx::types::Type<R::Database>));
|
predicates.push(parse_quote!(#ty: ::sqlx::types::Type<R::Database>));
|
||||||
@ -107,12 +107,12 @@ fn expand_derive_from_row_struct(
|
|||||||
parse_quote!(__row.try_get(#id_s))
|
parse_quote!(__row.try_get(#id_s))
|
||||||
}
|
}
|
||||||
// Flatten
|
// Flatten
|
||||||
(true, None, false) => {
|
(true, None, None) => {
|
||||||
predicates.push(parse_quote!(#ty: ::sqlx::FromRow<#lifetime, R>));
|
predicates.push(parse_quote!(#ty: ::sqlx::FromRow<#lifetime, R>));
|
||||||
parse_quote!(<#ty as ::sqlx::FromRow<#lifetime, R>>::from_row(__row))
|
parse_quote!(<#ty as ::sqlx::FromRow<#lifetime, R>>::from_row(__row))
|
||||||
}
|
}
|
||||||
// Flatten + Try from
|
// Flatten + Try from
|
||||||
(true, Some(try_from), false) => {
|
(true, Some(try_from), None) => {
|
||||||
predicates.push(parse_quote!(#try_from: ::sqlx::FromRow<#lifetime, R>));
|
predicates.push(parse_quote!(#try_from: ::sqlx::FromRow<#lifetime, R>));
|
||||||
parse_quote!(
|
parse_quote!(
|
||||||
<#try_from as ::sqlx::FromRow<#lifetime, R>>::from_row(__row)
|
<#try_from as ::sqlx::FromRow<#lifetime, R>>::from_row(__row)
|
||||||
@ -130,11 +130,11 @@ fn expand_derive_from_row_struct(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Flatten + Json
|
// Flatten + Json
|
||||||
(true, _, true) => {
|
(true, _, Some(_)) => {
|
||||||
panic!("Cannot use both flatten and json")
|
panic!("Cannot use both flatten and json")
|
||||||
}
|
}
|
||||||
// Try from
|
// Try from
|
||||||
(false, Some(try_from), false) => {
|
(false, Some(try_from), None) => {
|
||||||
predicates
|
predicates
|
||||||
.push(parse_quote!(#try_from: ::sqlx::decode::Decode<#lifetime, R::Database>));
|
.push(parse_quote!(#try_from: ::sqlx::decode::Decode<#lifetime, R::Database>));
|
||||||
predicates.push(parse_quote!(#try_from: ::sqlx::types::Type<R::Database>));
|
predicates.push(parse_quote!(#try_from: ::sqlx::types::Type<R::Database>));
|
||||||
@ -154,8 +154,8 @@ fn expand_derive_from_row_struct(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Try from + Json
|
// Try from + Json mandatory
|
||||||
(false, Some(try_from), true) => {
|
(false, Some(try_from), Some(JsonAttribute::NonNullable)) => {
|
||||||
predicates
|
predicates
|
||||||
.push(parse_quote!(::sqlx::types::Json<#try_from>: ::sqlx::decode::Decode<#lifetime, R::Database>));
|
.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>));
|
predicates.push(parse_quote!(::sqlx::types::Json<#try_from>: ::sqlx::types::Type<R::Database>));
|
||||||
@ -175,14 +175,25 @@ fn expand_derive_from_row_struct(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
// Try from + Json nullable
|
||||||
|
(false, Some(_), Some(JsonAttribute::Nullable)) => {
|
||||||
|
panic!("Cannot use both try from and json nullable")
|
||||||
|
},
|
||||||
// Json
|
// Json
|
||||||
(false, None, true) => {
|
(false, None, Some(JsonAttribute::NonNullable)) => {
|
||||||
predicates
|
predicates
|
||||||
.push(parse_quote!(::sqlx::types::Json<#ty>: ::sqlx::decode::Decode<#lifetime, R::Database>));
|
.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>));
|
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))
|
parse_quote!(__row.try_get::<::sqlx::types::Json<_>, _>(#id_s).map(|x| x.0))
|
||||||
},
|
},
|
||||||
|
(false, None, Some(JsonAttribute::Nullable)) => {
|
||||||
|
predicates
|
||||||
|
.push(parse_quote!(::core::option::Option<::sqlx::types::Json<#ty>>: ::sqlx::decode::Decode<#lifetime, R::Database>));
|
||||||
|
predicates.push(parse_quote!(::core::option::Option<::sqlx::types::Json<#ty>>: ::sqlx::types::Type<R::Database>));
|
||||||
|
|
||||||
|
parse_quote!(__row.try_get::<::core::option::Option<::sqlx::types::Json<_>>, _>(#id_s).map(|x| x.and_then(|y| y.0)))
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if attributes.default {
|
if attributes.default {
|
||||||
|
@ -494,6 +494,31 @@ async fn test_from_row_json_attr() -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx_macros::test]
|
||||||
|
async fn test_from_row_json_attr_nullable() -> anyhow::Result<()> {
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct J {
|
||||||
|
a: u32,
|
||||||
|
b: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
struct Record {
|
||||||
|
#[sqlx(json(nullable))]
|
||||||
|
j: Option<J>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut conn = new::<MySql>().await?;
|
||||||
|
|
||||||
|
let record = sqlx::query_as::<_, Record>("select NULL as j")
|
||||||
|
.fetch_one(&mut conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert!(record.j.is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[sqlx_macros::test]
|
#[sqlx_macros::test]
|
||||||
async fn test_from_row_json_try_from_attr() -> anyhow::Result<()> {
|
async fn test_from_row_json_try_from_attr() -> anyhow::Result<()> {
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user