diff --git a/diesel_derives/src/as_changeset.rs b/diesel_derives/src/as_changeset.rs index 4b8dec211889..b4089ada5fed 100644 --- a/diesel_derives/src/as_changeset.rs +++ b/diesel_derives/src/as_changeset.rs @@ -92,13 +92,18 @@ fn field_changeset_ty( treat_none_as_null: bool, lifetime: Option, ) -> syn::Type { - let column_name = field.column_name(); - if !treat_none_as_null && is_option_ty(&field.ty) { - let field_ty = inner_of_option_ty(&field.ty); - parse_quote!(std::option::Option>) - } else { + if field.has_flag("embed") { let field_ty = &field.ty; - parse_quote!(diesel::dsl::Eq<#table_name::#column_name, #lifetime #field_ty>) + parse_quote!(#lifetime #field_ty) + } else { + let column_name = field.column_name(); + if !treat_none_as_null && is_option_ty(&field.ty) { + let field_ty = inner_of_option_ty(&field.ty); + parse_quote!(std::option::Option>) + } else { + let field_ty = &field.ty; + parse_quote!(diesel::dsl::Eq<#table_name::#column_name, #lifetime #field_ty>) + } } } @@ -109,14 +114,19 @@ fn field_changeset_expr( lifetime: Option, ) -> syn::Expr { let field_access = field.name.access(); - let column_name = field.column_name(); - if !treat_none_as_null && is_option_ty(&field.ty) { - if lifetime.is_some() { - parse_quote!(self#field_access.as_ref().map(|x| #table_name::#column_name.eq(x))) + if field.has_flag("embed") { + parse_quote!(#lifetime self#field_access) + } else { + let column_name = field.column_name(); + let column: syn::Expr = parse_quote!(#table_name::#column_name); + if !treat_none_as_null && is_option_ty(&field.ty) { + if lifetime.is_some() { + parse_quote!(self#field_access.as_ref().map(|x| #column.eq(x))) + } else { + parse_quote!(self#field_access.map(|x| #column.eq(x))) + } } else { - parse_quote!(self#field_access.map(|x| #table_name::#column_name.eq(x))) + parse_quote!(#column.eq(#lifetime self#field_access)) } - } else { - parse_quote!(#table_name::#column_name.eq(#lifetime self#field_access)) } } diff --git a/diesel_derives/src/lib.rs b/diesel_derives/src/lib.rs index 6f766f4ca1a0..a0281512c244 100644 --- a/diesel_derives/src/lib.rs +++ b/diesel_derives/src/lib.rs @@ -70,6 +70,11 @@ use diagnostic_shim::*; /// annotate your struct with `#[changeset_options(treat_none_as_null = /// "true")]`. /// +/// Your struct can also contain fields which implement `AsChangeset`. This is +/// useful when you want to have one field map to more than one column (for +/// example, an enum that maps to a label and a value column). Add +/// `#[diesel(embed)]` to any such fields. +/// /// # Attributes /// /// ## Optional type attributes @@ -88,9 +93,11 @@ use diagnostic_shim::*; /// * `#[column_name = "some_column_name"]`, overrides the column name /// of the current field to `some_column_name`. By default the field /// name is used as column name. +/// * `#[diesel(embed)]`, specifies that the current field maps not only +/// to single database field, but is a type that implements `AsChangeset` #[proc_macro_derive( AsChangeset, - attributes(table_name, primary_key, column_name, changeset_options) + attributes(diesel, table_name, primary_key, column_name, changeset_options) )] pub fn derive_as_changeset(input: TokenStream) -> TokenStream { expand_proc_macro(input, as_changeset::derive) diff --git a/diesel_derives/tests/as_changeset.rs b/diesel_derives/tests/as_changeset.rs index e00975100032..2e8430826d65 100644 --- a/diesel_derives/tests/as_changeset.rs +++ b/diesel_derives/tests/as_changeset.rs @@ -1,3 +1,5 @@ +use diesel::expression::{bound::Bound, grouped::Grouped, operators}; +use diesel::sql_types::{Nullable, Text}; use diesel::*; use helpers::*; use schema::*; @@ -447,3 +449,137 @@ fn option_fields_are_assigned_null_when_specified() { let actual = users::table.order(users::id).load(&connection); assert_eq!(Ok(expected), actual); } + +#[test] +fn update_user_with_embed() { + #[derive(AsChangeset)] + #[table_name = "users"] + struct UserForm { + name: Option, + #[diesel(embed)] + hair_color: HairColor, + } + + #[allow(dead_code)] + enum HairColor { + Bald, + Black, + White, + Other(String), + } + + impl AsChangeset for HairColor { + type Target = users::table; + type Changeset = + Option, String>>>>; + + fn as_changeset(self) -> Self::Changeset { + match self { + HairColor::Black => Some(users::hair_color.eq("Black".to_string())), + HairColor::White => Some(users::hair_color.eq("White".to_string())), + HairColor::Other(x) => Some(users::hair_color.eq(x)), + HairColor::Bald => None, + } + } + } + + impl AsChangeset for &HairColor { + type Target = users::table; + type Changeset = + Option, String>>>>; + + fn as_changeset(self) -> Self::Changeset { + match self { + HairColor::Black => Some(users::hair_color.eq("Black".to_string())), + HairColor::White => Some(users::hair_color.eq("White".to_string())), + HairColor::Other(x) => Some(users::hair_color.eq(x.clone())), + HairColor::Bald => None, + } + } + } + + let connection = connection_with_sean_and_tess_in_users_table(); + + diesel::update(users::table) + .filter(users::id.eq(1)) + .set(UserForm { + name: None, + hair_color: HairColor::Black, + }) + .execute(&connection) + .unwrap(); + + let expected = vec![ + (1, String::from("Sean"), Some(String::from("Black"))), + (2, String::from("Tess"), Some(String::from("brown"))), + ]; + let actual = users::table.order(users::id).load(&connection); + assert_eq!(Ok(expected), actual); +} + +#[test] +fn update_user_with_embed_that_sets_null() { + #[derive(AsChangeset)] + #[table_name = "users"] + struct UserForm { + name: Option, + #[diesel(embed)] + hair_color: HairColor, + } + + #[allow(dead_code)] + enum HairColor { + Bald, + Black, + White, + Other(String), + } + + impl AsChangeset for HairColor { + type Target = users::table; + type Changeset = + Option, String>>>>; + + fn as_changeset(self) -> Self::Changeset { + match self { + HairColor::Black => Some(users::hair_color.eq("Black".to_string())), + HairColor::White => Some(users::hair_color.eq("White".to_string())), + HairColor::Other(x) => Some(users::hair_color.eq(x)), + HairColor::Bald => None, + } + } + } + + impl AsChangeset for &HairColor { + type Target = users::table; + type Changeset = + Option, String>>>>; + + fn as_changeset(self) -> Self::Changeset { + match self { + HairColor::Black => Some(users::hair_color.eq("Black".to_string())), + HairColor::White => Some(users::hair_color.eq("White".to_string())), + HairColor::Other(x) => Some(users::hair_color.eq(x.clone())), + HairColor::Bald => None, + } + } + } + + let connection = connection_with_sean_and_tess_in_users_table(); + + diesel::update(users::table) + .filter(users::id.eq(1)) + .set(UserForm { + name: None, + hair_color: HairColor::Bald, + }) + .execute(&connection) + .unwrap(); + + let expected = vec![ + (1, String::from("Sean"), Some(String::from("Black"))), + (2, String::from("Tess"), None), + ]; + let actual = users::table.order(users::id).load(&connection); + assert_eq!(Ok(expected), actual); +}