mirror of
https://github.com/launchbadge/sqlx.git
synced 2025-09-29 22:12:04 +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 {
|
||||
fn from_row(row: &'r R) -> Result<Self, Error>;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::quote_spanned;
|
||||
use syn::{
|
||||
punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr, Meta, Token, Type,
|
||||
Variant,
|
||||
parenthesized, punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr,
|
||||
Meta, Token, Type, Variant,
|
||||
};
|
||||
|
||||
macro_rules! assert_attribute {
|
||||
@ -61,13 +61,18 @@ pub struct SqlxContainerAttributes {
|
||||
pub default: bool,
|
||||
}
|
||||
|
||||
pub enum JsonAttribute {
|
||||
NonNullable,
|
||||
Nullable,
|
||||
}
|
||||
|
||||
pub struct SqlxChildAttributes {
|
||||
pub rename: Option<String>,
|
||||
pub default: bool,
|
||||
pub flatten: bool,
|
||||
pub try_from: Option<Type>,
|
||||
pub skip: bool,
|
||||
pub json: bool,
|
||||
pub json: Option<JsonAttribute>,
|
||||
}
|
||||
|
||||
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 flatten = 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")) {
|
||||
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") {
|
||||
skip = true;
|
||||
} 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(())
|
||||
})?;
|
||||
|
||||
if json && flatten {
|
||||
if json.is_some() && flatten {
|
||||
fail!(
|
||||
attr,
|
||||
"Cannot use `json` and `flatten` together on the same field"
|
||||
|
@ -6,7 +6,7 @@ use syn::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
attributes::{parse_child_attributes, parse_container_attributes},
|
||||
attributes::{parse_child_attributes, parse_container_attributes, JsonAttribute},
|
||||
rename_all,
|
||||
};
|
||||
|
||||
@ -99,7 +99,7 @@ fn expand_derive_from_row_struct(
|
||||
|
||||
let expr: Expr = match (attributes.flatten, attributes.try_from, attributes.json) {
|
||||
// <No attributes>
|
||||
(false, None, false) => {
|
||||
(false, None, None) => {
|
||||
predicates
|
||||
.push(parse_quote!(#ty: ::sqlx::decode::Decode<#lifetime, 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))
|
||||
}
|
||||
// Flatten
|
||||
(true, None, false) => {
|
||||
(true, None, None) => {
|
||||
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) => {
|
||||
(true, Some(try_from), None) => {
|
||||
predicates.push(parse_quote!(#try_from: ::sqlx::FromRow<#lifetime, R>));
|
||||
parse_quote!(
|
||||
<#try_from as ::sqlx::FromRow<#lifetime, R>>::from_row(__row)
|
||||
@ -130,11 +130,11 @@ fn expand_derive_from_row_struct(
|
||||
)
|
||||
}
|
||||
// Flatten + Json
|
||||
(true, _, true) => {
|
||||
(true, _, Some(_)) => {
|
||||
panic!("Cannot use both flatten and json")
|
||||
}
|
||||
// Try from
|
||||
(false, Some(try_from), false) => {
|
||||
(false, Some(try_from), None) => {
|
||||
predicates
|
||||
.push(parse_quote!(#try_from: ::sqlx::decode::Decode<#lifetime, 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
|
||||
(false, Some(try_from), true) => {
|
||||
// Try from + Json mandatory
|
||||
(false, Some(try_from), Some(JsonAttribute::NonNullable)) => {
|
||||
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>));
|
||||
@ -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
|
||||
(false, None, true) => {
|
||||
(false, None, Some(JsonAttribute::NonNullable)) => {
|
||||
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))
|
||||
},
|
||||
(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 {
|
||||
|
@ -494,6 +494,31 @@ async fn test_from_row_json_attr() -> anyhow::Result<()> {
|
||||
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]
|
||||
async fn test_from_row_json_try_from_attr() -> anyhow::Result<()> {
|
||||
#[derive(serde::Deserialize)]
|
||||
|
Loading…
x
Reference in New Issue
Block a user