Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support skipping fields in Queryable derive (falls back to Default impl) #2933

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions diesel_derives/src/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ impl Field {
self.flags.has_flag(flag)
}

pub fn has_skip_flag(&self) -> bool {
self.has_flag("skip")
}

pub fn ty_for_serialize(&self) -> Result<Option<syn::Type>, Diagnostic> {
if let Some(meta) = self.flags.nested_item("serialize_as")? {
let ty = meta.ty_value()?;
Expand All @@ -100,11 +104,19 @@ impl Field {
}
}

pub fn ty_for_deserialize(&self) -> Result<Cow<syn::Type>, Diagnostic> {
if let Some(meta) = self.flags.nested_item("deserialize_as")? {
meta.ty_value().map(Cow::Owned)
} else {
Ok(Cow::Borrowed(&self.ty))
/// Returns None if has `skip` flag
pub fn ty_for_deserialize(&self) -> Result<Option<Cow<syn::Type>>, Diagnostic> {
match (
self.flags.nested_item("deserialize_as")?,
self.has_skip_flag(),
) {
(Some(meta), false) => Ok(Some(Cow::Owned(meta.ty_value()?))),
(None, false) => Ok(Some(Cow::Borrowed(&self.ty))),
(None, true) => Ok(None),
(Some(_), true) => Err(self
.flags
.span()
.error("Cannot have both `deserialize_as` and `skip` attributes")),
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions diesel_derives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,9 @@ pub fn derive_query_id(input: TokenStream) -> TokenStream {
/// Then `Type` is converted via
/// [`.try_into`](https://doc.rust-lang.org/stable/std/convert/trait.TryInto.html#tymethod.try_into)
/// into the field type. By default this derive will deserialize directly into the field type
/// * `[diesel(skip)]`, instead of deserializing anything into that type, deserialization will
/// behave as if this field wasn't here, and it will simply be set using the `Default` impl
/// of the type of the field.
///
/// # Examples
///
Expand Down Expand Up @@ -772,6 +775,7 @@ pub fn derive_queryable_by_name(input: TokenStream) -> TokenStream {
/// * `#[diesel(embed)]`, specifies that the current field maps not only
/// single database column, but is a type that implements
/// `Selectable` on it's own
/// * `[diesel(skip)]`, field is ignored for the `Selectable` impl. Consistent with `Queryable`.
#[proc_macro_derive(Selectable, attributes(table_name, column_name, sql_type, diesel))]
pub fn derive_selectable(input: TokenStream) -> TokenStream {
expand_proc_macro(input, selectable::derive)
Expand Down
22 changes: 15 additions & 7 deletions diesel_derives/src/queryable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,22 @@ pub fn derive(item: syn::DeriveInput) -> Result<proc_macro2::TokenStream, Diagno
let field_ty = model
.fields()
.iter()
.map(Field::ty_for_deserialize)
.filter_map(|f| Field::ty_for_deserialize(f).transpose())
.collect::<Result<Vec<_>, _>>()?;
let field_ty = &field_ty;
let build_expr = model.fields().iter().enumerate().map(|(i, f)| {
let i = syn::Index::from(i);
f.name.assign(parse_quote!(row.#i.try_into()?))
});
let sql_type = (0..model.fields().len())
let build_expr = {
let mut non_default_field_idx = 0;
model.fields().iter().map(move |f| {
f.name.assign(if f.has_flag("skip") {
parse_quote!(std::default::Default::default())
} else {
let i = syn::Index::from(non_default_field_idx);
non_default_field_idx += 1;
parse_quote!(row.#i.try_into()?)
})
})
};
let sql_type = (0..field_ty.len())
.map(|i| {
let i = syn::Ident::new(&format!("__ST{}", i), proc_macro2::Span::call_site());
quote!(#i)
Expand All @@ -32,7 +40,7 @@ pub fn derive(item: syn::DeriveInput) -> Result<proc_macro2::TokenStream, Diagno
generics
.params
.push(parse_quote!(__DB: diesel::backend::Backend));
for id in 0..model.fields().len() {
for id in 0..field_ty.len() {
let ident = syn::Ident::new(&format!("__ST{}", id), proc_macro2::Span::call_site());
generics.params.push(parse_quote!(#ident));
}
Expand Down
9 changes: 7 additions & 2 deletions diesel_derives/src/selectable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@ pub fn derive(item: syn::DeriveInput) -> Result<proc_macro2::TokenStream, Diagno

let struct_name = &item.ident;

let field_columns_ty = model.fields().iter().map(|f| field_column_ty(f, &model));
let fields = model
.fields()
.iter()
.filter(|f| !f.has_skip_flag())
.collect::<Vec<_>>();

let field_columns_inst = model.fields().iter().map(|f| field_column_inst(f, &model));
let field_columns_ty = fields.iter().map(|f| field_column_ty(f, &model));
let field_columns_inst = fields.iter().map(|f| field_column_inst(f, &model));

Ok(wrap_in_dummy_mod(quote! {
use diesel::expression::Selectable;
Expand Down
40 changes: 36 additions & 4 deletions diesel_derives/tests/queryable.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use diesel::dsl::sql;
use diesel::sql_types::Integer;
use diesel::sql_types::{Integer, Text};
use diesel::*;

use helpers::connection;

#[test]
fn named_struct_definition() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Queryable)]
#[derive(Debug, PartialEq, Eq, Queryable)]
struct MyStruct {
foo: i32,
bar: i32,
Expand All @@ -19,7 +19,7 @@ fn named_struct_definition() {

#[test]
fn tuple_struct() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Queryable)]
#[derive(Debug, PartialEq, Eq, Queryable)]
struct MyStruct(#[column_name = "foo"] i32, #[column_name = "bar"] i32);

let conn = &mut connection();
Expand All @@ -29,10 +29,42 @@ fn tuple_struct() {

#[test]
fn tuple_struct_without_column_name_annotations() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Queryable)]
#[derive(Debug, PartialEq, Eq, Queryable)]
struct MyStruct(i32, i32);

let conn = &mut connection();
let data = select(sql::<(Integer, Integer)>("1, 2")).get_result(conn);
assert_eq!(Ok(MyStruct(1, 2)), data);
}

#[test]
fn named_struct_definition_with_skip() {
#[derive(Debug, PartialEq, Eq, Queryable)]
struct MyStruct {
foo: i32,
#[diesel(skip)]
should_be_default: Vec<i32>,
bar: String,
}

let conn = &mut connection();
let data = select(sql::<(Integer, Text)>("1, '2'")).get_result(conn);
assert_eq!(
Ok(MyStruct {
foo: 1,
should_be_default: Vec::default(),
bar: "2".to_string(),
}),
data
);
}

#[test]
fn tuple_struct_with_skip() {
#[derive(Debug, PartialEq, Eq, Queryable)]
struct MyStruct(i32, #[diesel(skip)] Option<i32>, String);

let conn = &mut connection();
let data = select(sql::<(Integer, Text)>("1, '2'")).get_result(conn);
assert_eq!(Ok(MyStruct(1, None, "2".to_string())), data);
}
17 changes: 17 additions & 0 deletions diesel_derives/tests/selectable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,20 @@ fn embedded_option() {
let data = my_structs::table.select(A::as_select()).get_result(conn);
assert!(data.is_err());
}

#[test]
fn named_struct_definition_with_skip() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Queryable, Selectable)]
#[table_name = "my_structs"]
struct MyStruct {
foo: i32,
#[diesel(skip)]
bar: i32,
}

let conn = &mut connection();
let data = my_structs::table
.select(MyStruct::as_select())
.get_result(conn);
assert!(data.is_err());
}