diff --git a/sqlx-macros/src/derives/row.rs b/sqlx-macros/src/derives/row.rs index 8f03e68754..2409c7fbfe 100644 --- a/sqlx-macros/src/derives/row.rs +++ b/sqlx-macros/src/derives/row.rs @@ -2,7 +2,7 @@ use proc_macro2::Span; use quote::quote; use syn::{ parse_quote, punctuated::Punctuated, token::Comma, Data, DataStruct, DeriveInput, Field, - Fields, FieldsNamed, Lifetime, Stmt, + Fields, FieldsNamed, FieldsUnnamed, Lifetime, Stmt, }; use super::attributes::parse_child_attributes; @@ -15,12 +15,9 @@ pub fn expand_derive_from_row(input: &DeriveInput) -> syn::Result expand_derive_from_row_struct(input, named), Data::Struct(DataStruct { - fields: Fields::Unnamed(_), + fields: Fields::Unnamed(FieldsUnnamed { unnamed, .. }), .. - }) => Err(syn::Error::new_spanned( - input, - "tuple structs are not supported", - )), + }) => expand_derive_from_row_struct_unnamed(input, unnamed), Data::Struct(DataStruct { fields: Fields::Unit, @@ -111,3 +108,55 @@ fn expand_derive_from_row_struct( } )) } + +fn expand_derive_from_row_struct_unnamed( + input: &DeriveInput, + fields: &Punctuated, +) -> syn::Result { + let ident = &input.ident; + + let generics = &input.generics; + + let (lifetime, provided) = generics + .lifetimes() + .next() + .map(|def| (def.lifetime.clone(), false)) + .unwrap_or_else(|| (Lifetime::new("'a", Span::call_site()), true)); + + let (_, ty_generics, _) = generics.split_for_impl(); + + let mut generics = generics.clone(); + generics.params.insert(0, parse_quote!(R: sqlx::Row)); + + if provided { + generics.params.insert(0, parse_quote!(#lifetime)); + } + + let predicates = &mut generics.make_where_clause().predicates; + + predicates.push(parse_quote!(usize: sqlx::ColumnIndex)); + + for field in fields { + let ty = &field.ty; + + predicates.push(parse_quote!(#ty: sqlx::decode::Decode<#lifetime, R::Database>)); + predicates.push(parse_quote!(#ty: sqlx::types::Type)); + } + + let (impl_generics, _, where_clause) = generics.split_for_impl(); + + let gets = fields + .iter() + .enumerate() + .map(|(idx, _)| quote!(row.try_get(#idx)?)); + + Ok(quote!( + impl #impl_generics sqlx::FromRow<#lifetime, R> for #ident #ty_generics #where_clause { + fn from_row(row: &#lifetime R) -> sqlx::Result { + Ok(#ident ( + #(#gets),* + )) + } + } + )) +} diff --git a/tests/postgres/derives.rs b/tests/postgres/derives.rs index bd45d84335..92a63e563f 100644 --- a/tests/postgres/derives.rs +++ b/tests/postgres/derives.rs @@ -404,6 +404,44 @@ async fn test_from_row_with_rename() -> anyhow::Result<()> { Ok(()) } +#[cfg(feature = "macros")] +#[sqlx_macros::test] +async fn test_from_row_tuple() -> anyhow::Result<()> { + let mut conn = new::().await?; + + #[derive(Debug, sqlx::FromRow)] + struct Account(i32, String); + + let account: Account = sqlx::query_as( + "SELECT * from (VALUES (1, 'Herp Derpinson')) accounts(id, name) where id = $1", + ) + .bind(1_i32) + .fetch_one(&mut conn) + .await?; + + assert_eq!(account.0, 1); + assert_eq!(account.1, "Herp Derpinson"); + + // A _single_ lifetime may be used but only when using the lowest-level API currently (Query::fetch) + + #[derive(sqlx::FromRow)] + struct RefAccount<'a>(i32, &'a str); + + let mut cursor = sqlx::query( + "SELECT * from (VALUES (1, 'Herp Derpinson')) accounts(id, name) where id = $1", + ) + .bind(1_i32) + .fetch(&mut conn); + + let row = cursor.try_next().await?.unwrap(); + let account = RefAccount::from_row(&row)?; + + assert_eq!(account.0, 1); + assert_eq!(account.1, "Herp Derpinson"); + + Ok(()) +} + #[cfg(feature = "macros")] #[sqlx_macros::test] async fn test_default() -> anyhow::Result<()> {