Skip to content

Commit 2aab4cd

Browse files
authored
Add json(nullable) macro attribute (#3677)
* add json optional attribute parser and expansion * rename attribute * add test * fix tests * fix lints * Add docs
1 parent 546ec96 commit 2aab4cd

File tree

4 files changed

+90
-15
lines changed

4 files changed

+90
-15
lines changed

sqlx-core/src/from_row.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,32 @@ use crate::{error::Error, row::Row};
271271
/// }
272272
/// }
273273
/// ```
274+
///
275+
/// By default the `#[sqlx(json)]` attribute will assume that the underlying database row is
276+
/// _not_ NULL. This can cause issues when your field type is an `Option<T>` because this would be
277+
/// represented as the _not_ NULL (in terms of DB) JSON value of `null`.
278+
///
279+
/// If you wish to describe a database row which _is_ NULLable but _cannot_ contain the JSON value `null`,
280+
/// use the `#[sqlx(json(nullable))]` attrubute.
281+
///
282+
/// For example
283+
/// ```rust,ignore
284+
/// #[derive(serde::Deserialize)]
285+
/// struct Data {
286+
/// field1: String,
287+
/// field2: u64
288+
/// }
289+
///
290+
/// #[derive(sqlx::FromRow)]
291+
/// struct User {
292+
/// id: i32,
293+
/// name: String,
294+
/// #[sqlx(json(nullable))]
295+
/// metadata: Option<Data>
296+
/// }
297+
/// ```
298+
/// Would describe a database field which _is_ NULLable but if it exists it must be the JSON representation of `Data`
299+
/// and cannot be the JSON value `null`
274300
pub trait FromRow<'r, R: Row>: Sized {
275301
fn from_row(row: &'r R) -> Result<Self, Error>;
276302
}

sqlx-macros-core/src/derives/attributes.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use proc_macro2::{Ident, Span, TokenStream};
22
use quote::quote_spanned;
33
use syn::{
4-
punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr, Meta, Token, Type,
5-
Variant,
4+
parenthesized, punctuated::Punctuated, token::Comma, Attribute, DeriveInput, Field, LitStr,
5+
Meta, Token, Type, Variant,
66
};
77

88
macro_rules! assert_attribute {
@@ -61,13 +61,18 @@ pub struct SqlxContainerAttributes {
6161
pub default: bool,
6262
}
6363

64+
pub enum JsonAttribute {
65+
NonNullable,
66+
Nullable,
67+
}
68+
6469
pub struct SqlxChildAttributes {
6570
pub rename: Option<String>,
6671
pub default: bool,
6772
pub flatten: bool,
6873
pub try_from: Option<Type>,
6974
pub skip: bool,
70-
pub json: bool,
75+
pub json: Option<JsonAttribute>,
7176
}
7277

7378
pub fn parse_container_attributes(input: &[Attribute]) -> syn::Result<SqlxContainerAttributes> {
@@ -144,7 +149,7 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result<SqlxChildAttri
144149
let mut try_from = None;
145150
let mut flatten = false;
146151
let mut skip: bool = false;
147-
let mut json = false;
152+
let mut json = None;
148153

149154
for attr in input.iter().filter(|a| a.path().is_ident("sqlx")) {
150155
attr.parse_nested_meta(|meta| {
@@ -163,13 +168,21 @@ pub fn parse_child_attributes(input: &[Attribute]) -> syn::Result<SqlxChildAttri
163168
} else if meta.path.is_ident("skip") {
164169
skip = true;
165170
} else if meta.path.is_ident("json") {
166-
json = true;
171+
if meta.input.peek(syn::token::Paren) {
172+
let content;
173+
parenthesized!(content in meta.input);
174+
let literal: Ident = content.parse()?;
175+
assert_eq!(literal.to_string(), "nullable", "Unrecognized `json` attribute. Valid values are `json` or `json(nullable)`");
176+
json = Some(JsonAttribute::Nullable);
177+
} else {
178+
json = Some(JsonAttribute::NonNullable);
179+
}
167180
}
168181

169182
Ok(())
170183
})?;
171184

172-
if json && flatten {
185+
if json.is_some() && flatten {
173186
fail!(
174187
attr,
175188
"Cannot use `json` and `flatten` together on the same field"

sqlx-macros-core/src/derives/row.rs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use syn::{
66
};
77

88
use super::{
9-
attributes::{parse_child_attributes, parse_container_attributes},
9+
attributes::{parse_child_attributes, parse_container_attributes, JsonAttribute},
1010
rename_all,
1111
};
1212

@@ -99,20 +99,20 @@ fn expand_derive_from_row_struct(
9999

100100
let expr: Expr = match (attributes.flatten, attributes.try_from, attributes.json) {
101101
// <No attributes>
102-
(false, None, false) => {
102+
(false, None, None) => {
103103
predicates
104104
.push(parse_quote!(#ty: ::sqlx::decode::Decode<#lifetime, R::Database>));
105105
predicates.push(parse_quote!(#ty: ::sqlx::types::Type<R::Database>));
106106

107107
parse_quote!(__row.try_get(#id_s))
108108
}
109109
// Flatten
110-
(true, None, false) => {
110+
(true, None, None) => {
111111
predicates.push(parse_quote!(#ty: ::sqlx::FromRow<#lifetime, R>));
112112
parse_quote!(<#ty as ::sqlx::FromRow<#lifetime, R>>::from_row(__row))
113113
}
114114
// Flatten + Try from
115-
(true, Some(try_from), false) => {
115+
(true, Some(try_from), None) => {
116116
predicates.push(parse_quote!(#try_from: ::sqlx::FromRow<#lifetime, R>));
117117
parse_quote!(
118118
<#try_from as ::sqlx::FromRow<#lifetime, R>>::from_row(__row)
@@ -130,11 +130,11 @@ fn expand_derive_from_row_struct(
130130
)
131131
}
132132
// Flatten + Json
133-
(true, _, true) => {
133+
(true, _, Some(_)) => {
134134
panic!("Cannot use both flatten and json")
135135
}
136136
// Try from
137-
(false, Some(try_from), false) => {
137+
(false, Some(try_from), None) => {
138138
predicates
139139
.push(parse_quote!(#try_from: ::sqlx::decode::Decode<#lifetime, R::Database>));
140140
predicates.push(parse_quote!(#try_from: ::sqlx::types::Type<R::Database>));
@@ -154,8 +154,8 @@ fn expand_derive_from_row_struct(
154154
})
155155
)
156156
}
157-
// Try from + Json
158-
(false, Some(try_from), true) => {
157+
// Try from + Json mandatory
158+
(false, Some(try_from), Some(JsonAttribute::NonNullable)) => {
159159
predicates
160160
.push(parse_quote!(::sqlx::types::Json<#try_from>: ::sqlx::decode::Decode<#lifetime, R::Database>));
161161
predicates.push(parse_quote!(::sqlx::types::Json<#try_from>: ::sqlx::types::Type<R::Database>));
@@ -175,14 +175,25 @@ fn expand_derive_from_row_struct(
175175
})
176176
)
177177
},
178+
// Try from + Json nullable
179+
(false, Some(_), Some(JsonAttribute::Nullable)) => {
180+
panic!("Cannot use both try from and json nullable")
181+
},
178182
// Json
179-
(false, None, true) => {
183+
(false, None, Some(JsonAttribute::NonNullable)) => {
180184
predicates
181185
.push(parse_quote!(::sqlx::types::Json<#ty>: ::sqlx::decode::Decode<#lifetime, R::Database>));
182186
predicates.push(parse_quote!(::sqlx::types::Json<#ty>: ::sqlx::types::Type<R::Database>));
183187

184188
parse_quote!(__row.try_get::<::sqlx::types::Json<_>, _>(#id_s).map(|x| x.0))
185189
},
190+
(false, None, Some(JsonAttribute::Nullable)) => {
191+
predicates
192+
.push(parse_quote!(::core::option::Option<::sqlx::types::Json<#ty>>: ::sqlx::decode::Decode<#lifetime, R::Database>));
193+
predicates.push(parse_quote!(::core::option::Option<::sqlx::types::Json<#ty>>: ::sqlx::types::Type<R::Database>));
194+
195+
parse_quote!(__row.try_get::<::core::option::Option<::sqlx::types::Json<_>>, _>(#id_s).map(|x| x.and_then(|y| y.0)))
196+
},
186197
};
187198

188199
if attributes.default {

tests/mysql/macros.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,31 @@ async fn test_from_row_json_attr() -> anyhow::Result<()> {
494494
Ok(())
495495
}
496496

497+
#[sqlx_macros::test]
498+
async fn test_from_row_json_attr_nullable() -> anyhow::Result<()> {
499+
#[derive(serde::Deserialize)]
500+
#[allow(dead_code)]
501+
struct J {
502+
a: u32,
503+
b: u32,
504+
}
505+
506+
#[derive(sqlx::FromRow)]
507+
struct Record {
508+
#[sqlx(json(nullable))]
509+
j: Option<J>,
510+
}
511+
512+
let mut conn = new::<MySql>().await?;
513+
514+
let record = sqlx::query_as::<_, Record>("select NULL as j")
515+
.fetch_one(&mut conn)
516+
.await?;
517+
518+
assert!(record.j.is_none());
519+
Ok(())
520+
}
521+
497522
#[sqlx_macros::test]
498523
async fn test_from_row_json_try_from_attr() -> anyhow::Result<()> {
499524
#[derive(serde::Deserialize)]

0 commit comments

Comments
 (0)