From eaaa700a38d36e6cc06237b19bb096a35b96891a Mon Sep 17 00:00:00 2001 From: Xiretza Date: Mon, 11 Aug 2025 18:18:46 +0000 Subject: [PATCH 1/2] Allow single-field named structs to be transparent This more closely matches the criteria for e.g. #[repr(transparent)] and #[serde(transparent)]. --- sqlx-macros-core/src/derives/decode.rs | 22 ++++++++++++++-------- sqlx-macros-core/src/derives/encode.rs | 23 ++++++++++++++--------- sqlx-macros-core/src/derives/type.rs | 26 ++++++++++++-------------- 3 files changed, 40 insertions(+), 31 deletions(-) diff --git a/sqlx-macros-core/src/derives/decode.rs b/sqlx-macros-core/src/derives/decode.rs index 7af9643d93..98e47a4807 100644 --- a/sqlx-macros-core/src/derives/decode.rs +++ b/sqlx-macros-core/src/derives/decode.rs @@ -8,18 +8,17 @@ use quote::quote; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{ - parse_quote, Arm, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, FieldsNamed, - FieldsUnnamed, Stmt, TypeParamBound, Variant, + parse_quote, Arm, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, FieldsNamed, Stmt, + TypeParamBound, Variant, }; pub fn expand_derive_decode(input: &DeriveInput) -> syn::Result { let attrs = parse_container_attributes(&input.attrs)?; match &input.data { - Data::Struct(DataStruct { - fields: Fields::Unnamed(FieldsUnnamed { unnamed, .. }), - .. - }) if unnamed.len() == 1 => { - expand_derive_decode_transparent(input, unnamed.first().unwrap()) + Data::Struct(DataStruct { fields, .. }) + if fields.len() == 1 && (matches!(fields, Fields::Unnamed(_)) || attrs.transparent) => + { + expand_derive_decode_transparent(input, fields.iter().next().unwrap()) } Data::Enum(DataEnum { variants, .. }) => match attrs.repr { Some(_) => expand_derive_decode_weak_enum(input, variants), @@ -72,6 +71,12 @@ fn expand_derive_decode_transparent( .push(parse_quote!(#ty: ::sqlx::decode::Decode<'r, DB>)); let (impl_generics, _, where_clause) = generics.split_for_impl(); + let field_ident = if let Some(ident) = &field.ident { + quote! { #ident } + } else { + quote! { 0 } + }; + let tts = quote!( #[automatically_derived] impl #impl_generics ::sqlx::decode::Decode<'r, DB> for #ident #ty_generics #where_clause { @@ -83,7 +88,8 @@ fn expand_derive_decode_transparent( dyn ::std::error::Error + 'static + ::std::marker::Send + ::std::marker::Sync, >, > { - <#ty as ::sqlx::decode::Decode<'r, DB>>::decode(value).map(Self) + <#ty as ::sqlx::decode::Decode<'r, DB>>::decode(value) + .map(|val| Self { #field_ident: val }) } } ); diff --git a/sqlx-macros-core/src/derives/encode.rs b/sqlx-macros-core/src/derives/encode.rs index 9f26aba0a9..05c451aef1 100644 --- a/sqlx-macros-core/src/derives/encode.rs +++ b/sqlx-macros-core/src/derives/encode.rs @@ -9,18 +9,17 @@ use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{ parse_quote, Data, DataEnum, DataStruct, DeriveInput, Expr, Field, Fields, FieldsNamed, - FieldsUnnamed, Lifetime, LifetimeParam, Stmt, TypeParamBound, Variant, + Lifetime, LifetimeParam, Stmt, TypeParamBound, Variant, }; pub fn expand_derive_encode(input: &DeriveInput) -> syn::Result { let args = parse_container_attributes(&input.attrs)?; match &input.data { - Data::Struct(DataStruct { - fields: Fields::Unnamed(FieldsUnnamed { unnamed, .. }), - .. - }) if unnamed.len() == 1 => { - expand_derive_encode_transparent(input, unnamed.first().unwrap()) + Data::Struct(DataStruct { fields, .. }) + if fields.len() == 1 && (matches!(fields, Fields::Unnamed(_)) || args.transparent) => + { + expand_derive_encode_transparent(input, fields.iter().next().unwrap()) } Data::Enum(DataEnum { variants, .. }) => match args.repr { Some(_) => expand_derive_encode_weak_enum(input, variants), @@ -77,6 +76,12 @@ fn expand_derive_encode_transparent( .push(parse_quote!(#ty: ::sqlx::encode::Encode<#lifetime, DB>)); let (impl_generics, _, where_clause) = generics.split_for_impl(); + let field_ident = if let Some(ident) = &field.ident { + quote! { #ident } + } else { + quote! { 0 } + }; + Ok(quote!( #[automatically_derived] impl #impl_generics ::sqlx::encode::Encode<#lifetime, DB> for #ident #ty_generics @@ -86,15 +91,15 @@ fn expand_derive_encode_transparent( &self, buf: &mut ::ArgumentBuffer<#lifetime>, ) -> ::std::result::Result<::sqlx::encode::IsNull, ::sqlx::error::BoxDynError> { - <#ty as ::sqlx::encode::Encode<#lifetime, DB>>::encode_by_ref(&self.0, buf) + <#ty as ::sqlx::encode::Encode<#lifetime, DB>>::encode_by_ref(&self.#field_ident, buf) } fn produces(&self) -> Option { - <#ty as ::sqlx::encode::Encode<#lifetime, DB>>::produces(&self.0) + <#ty as ::sqlx::encode::Encode<#lifetime, DB>>::produces(&self.#field_ident) } fn size_hint(&self) -> usize { - <#ty as ::sqlx::encode::Encode<#lifetime, DB>>::size_hint(&self.0) + <#ty as ::sqlx::encode::Encode<#lifetime, DB>>::size_hint(&self.#field_ident) } } )) diff --git a/sqlx-macros-core/src/derives/type.rs b/sqlx-macros-core/src/derives/type.rs index 8ecd55230c..bfa43ed505 100644 --- a/sqlx-macros-core/src/derives/type.rs +++ b/sqlx-macros-core/src/derives/type.rs @@ -7,8 +7,7 @@ use quote::{quote, quote_spanned}; use syn::punctuated::Punctuated; use syn::token::Comma; use syn::{ - parse_quote, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, FieldsNamed, - FieldsUnnamed, Variant, + parse_quote, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, FieldsNamed, Variant, }; pub fn expand_derive_type(input: &DeriveInput) -> syn::Result { @@ -16,18 +15,10 @@ pub fn expand_derive_type(input: &DeriveInput) -> syn::Result { match &input.data { // Newtype structs: // struct Foo(i32); - Data::Struct(DataStruct { - fields: Fields::Unnamed(FieldsUnnamed { unnamed, .. }), - .. - }) => { - if unnamed.len() == 1 { - expand_derive_has_sql_type_transparent(input, unnamed.first().unwrap()) - } else { - Err(syn::Error::new_spanned( - input, - "structs with zero or more than one unnamed field are not supported", - )) - } + Data::Struct(DataStruct { fields, .. }) + if fields.len() == 1 && (matches!(fields, Fields::Unnamed(_)) || attrs.transparent) => + { + expand_derive_has_sql_type_transparent(input, fields.iter().next().unwrap()) } // Record types // struct Foo { foo: i32, bar: String } @@ -35,6 +26,13 @@ pub fn expand_derive_type(input: &DeriveInput) -> syn::Result { fields: Fields::Named(FieldsNamed { named, .. }), .. }) => expand_derive_has_sql_type_struct(input, named), + Data::Struct(DataStruct { + fields: Fields::Unnamed(..), + .. + }) => Err(syn::Error::new_spanned( + input, + "structs with zero or more than one unnamed field are not supported", + )), Data::Struct(DataStruct { fields: Fields::Unit, .. From d1252761804fff60278c641bc60eae957e93c4dd Mon Sep 17 00:00:00 2001 From: Xiretza Date: Tue, 19 Aug 2025 15:50:59 +0000 Subject: [PATCH 2/2] Add tests, fix error messages --- sqlx-macros-core/src/derives/attributes.rs | 2 +- sqlx-macros-core/src/derives/decode.rs | 2 +- sqlx-macros-core/src/derives/encode.rs | 2 +- sqlx-macros-core/src/derives/type.rs | 3 ++- tests/mysql/derives.rs | 22 +++++++++++++++++++++- tests/postgres/derives.rs | 14 +++++++++++++- tests/sqlite/derives.rs | 20 ++++++++++++++++++++ 7 files changed, 59 insertions(+), 6 deletions(-) diff --git a/sqlx-macros-core/src/derives/attributes.rs b/sqlx-macros-core/src/derives/attributes.rs index c69687908b..6109863833 100644 --- a/sqlx-macros-core/src/derives/attributes.rs +++ b/sqlx-macros-core/src/derives/attributes.rs @@ -281,7 +281,7 @@ pub fn check_struct_attributes( assert_attribute!( !attributes.transparent, - "unexpected #[sqlx(transparent)]", + "#[sqlx(transparent)] is only valid for structs with exactly one field", input ); diff --git a/sqlx-macros-core/src/derives/decode.rs b/sqlx-macros-core/src/derives/decode.rs index 98e47a4807..1d4f74df21 100644 --- a/sqlx-macros-core/src/derives/decode.rs +++ b/sqlx-macros-core/src/derives/decode.rs @@ -34,7 +34,7 @@ pub fn expand_derive_decode(input: &DeriveInput) -> syn::Result { .. }) => Err(syn::Error::new_spanned( input, - "structs with zero or more than one unnamed field are not supported", + "tuple structs may only have a single field", )), Data::Struct(DataStruct { fields: Fields::Unit, diff --git a/sqlx-macros-core/src/derives/encode.rs b/sqlx-macros-core/src/derives/encode.rs index 05c451aef1..8850efdf4f 100644 --- a/sqlx-macros-core/src/derives/encode.rs +++ b/sqlx-macros-core/src/derives/encode.rs @@ -35,7 +35,7 @@ pub fn expand_derive_encode(input: &DeriveInput) -> syn::Result { .. }) => Err(syn::Error::new_spanned( input, - "structs with zero or more than one unnamed field are not supported", + "tuple structs may only have a single field", )), Data::Struct(DataStruct { fields: Fields::Unit, diff --git a/sqlx-macros-core/src/derives/type.rs b/sqlx-macros-core/src/derives/type.rs index bfa43ed505..e94b12b615 100644 --- a/sqlx-macros-core/src/derives/type.rs +++ b/sqlx-macros-core/src/derives/type.rs @@ -15,6 +15,7 @@ pub fn expand_derive_type(input: &DeriveInput) -> syn::Result { match &input.data { // Newtype structs: // struct Foo(i32); + // struct Foo { field: i32 }; Data::Struct(DataStruct { fields, .. }) if fields.len() == 1 && (matches!(fields, Fields::Unnamed(_)) || attrs.transparent) => { @@ -31,7 +32,7 @@ pub fn expand_derive_type(input: &DeriveInput) -> syn::Result { .. }) => Err(syn::Error::new_spanned( input, - "structs with zero or more than one unnamed field are not supported", + "tuple structs may only have a single field", )), Data::Struct(DataStruct { fields: Fields::Unit, diff --git a/tests/mysql/derives.rs b/tests/mysql/derives.rs index 825c7b8e2c..4322219dee 100644 --- a/tests/mysql/derives.rs +++ b/tests/mysql/derives.rs @@ -1,5 +1,5 @@ use sqlx_mysql::MySql; -use sqlx_test::new; +use sqlx_test::{new, test_type}; #[sqlx::test] async fn test_derive_strong_enum() -> anyhow::Result<()> { @@ -300,3 +300,23 @@ async fn test_derive_weak_enum() -> anyhow::Result<()> { Ok(()) } + +#[derive(PartialEq, Eq, Debug, sqlx::Type)] +#[sqlx(transparent)] +struct TransparentTuple(i64); + +#[derive(PartialEq, Eq, Debug, sqlx::Type)] +#[sqlx(transparent)] +struct TransparentNamed { + field: i64, +} + +test_type!(transparent_tuple(MySql, + "0" == TransparentTuple(0), + "23523" == TransparentTuple(23523) +)); + +test_type!(transparent_named(MySql, + "0" == TransparentNamed { field: 0 }, + "23523" == TransparentNamed { field: 23523 }, +)); diff --git a/tests/postgres/derives.rs b/tests/postgres/derives.rs index 86eac02065..350bb04089 100644 --- a/tests/postgres/derives.rs +++ b/tests/postgres/derives.rs @@ -12,6 +12,13 @@ use std::ops::Bound; #[sqlx(transparent)] struct Transparent(i32); +// Also possible for single-field named structs +#[derive(PartialEq, Debug, sqlx::Type)] +#[sqlx(transparent)] +struct TransparentNamed { + field: i32, +} + #[derive(PartialEq, Debug, sqlx::Type)] // https://github.com/launchbadge/sqlx/issues/2611 // Previously, the derive would generate a `PgHasArrayType` impl that errored on an @@ -143,11 +150,16 @@ struct FloatRange(PgRange); #[sqlx(type_name = "int4rangeL0pC")] struct RangeInclusive(PgRange); -test_type!(transparent(Postgres, +test_type!(transparent_tuple(Postgres, "0" == Transparent(0), "23523" == Transparent(23523) )); +test_type!(transparent_named(Postgres, + "0" == TransparentNamed { field: 0 }, + "23523" == TransparentNamed { field: 23523 }, +)); + test_type!(transparent_array(Postgres, "'{}'::int8[]" == TransparentArray(vec![]), "'{ 23523, 123456, 789 }'::int8[]" == TransparentArray(vec![23523, 123456, 789]) diff --git a/tests/sqlite/derives.rs b/tests/sqlite/derives.rs index d91e012b30..3491ab8539 100644 --- a/tests/sqlite/derives.rs +++ b/tests/sqlite/derives.rs @@ -12,3 +12,23 @@ test_type!(origin_enum(Sqlite, "1" == Origin::Foo, "2" == Origin::Bar, )); + +#[derive(PartialEq, Eq, Debug, sqlx::Type)] +#[sqlx(transparent)] +struct TransparentTuple(i64); + +#[derive(PartialEq, Eq, Debug, sqlx::Type)] +#[sqlx(transparent)] +struct TransparentNamed { + field: i64, +} + +test_type!(transparent_tuple(Sqlite, + "0" == TransparentTuple(0), + "23523" == TransparentTuple(23523) +)); + +test_type!(transparent_named(Sqlite, + "0" == TransparentNamed { field: 0 }, + "23523" == TransparentNamed { field: 23523 }, +));