Repository: diesel-rs/diesel (language: rust)

Issue:
Insert Embedded Struct for AsChangeset
Will diesel support #[diesel(embed)] on AsChangeset structs? Would be nice for reuse between Insertable and "update-able" structs.


Discussion / hints:
This would be really cool integrated into diesel.  In the meantime I happen to do exactly this in a macro I wrote and your welcome to come check it out.   [link-to-macro](https://github.com/technetos/postgres-resource/blob/master/postgres_resource_derive/src/lib.rs#L419-L443)
> Will diesel support #[diesel(embed)] on AsChangeset structs?

Yes. I just haven't gotten around to implementing it. IIRC the last time I worked on it, there was a weird interaction between `treat_none_as_null = "true"` and `embed` on an `Option` field that I wanted to think about. Feel free to open a PR for this, though.
Hello, how is the status of this feature ?. I could try to work on it if nobody does right now.
@vnghia That's not implemented yet, but we would be grateful to receive a PR for this. 

You can find the code for the derive here: https://github.com/diesel-rs/diesel/blob/09d5e1050a40d698b10af98bd10495c98ccfaa7f/diesel_derives/src/as_changeset.rs

The `Insertable` derive already supports the `embed` attribute: https://github.com/diesel-rs/diesel/blob/master/diesel_derives/src/insertable.rs
You might be able to use that as blue print for how to approach the implementation.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d10ed964175..b2bd363aa7c8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ Increasing the minimal supported Rust version will always be coupled at least wi
 
 ### Added
 
+* Added embedded struct support for `AsChangeset` via `#[diesel(embed)]`
 * Support for libsqlite3-sys 0.30.0
 * Add support for built-in PostgreSQL range operators and functions
 * Support for postgres multirange type
diff --git a/diesel_derives/src/as_changeset.rs b/diesel_derives/src/as_changeset.rs
index 6755ea1b74d7..44ba64c6cb04 100644
--- a/diesel_derives/src/as_changeset.rs
+++ b/diesel_derives/src/as_changeset.rs
@@ -1,5 +1,5 @@
 use proc_macro2::TokenStream;
-use quote::quote;
+use quote::{quote, quote_spanned};
 use syn::spanned::Spanned as _;
 use syn::{parse_quote, DeriveInput, Expr, Path, Result, Type};
 
@@ -49,6 +49,13 @@ pub fn derive(item: DeriveInput) -> Result<TokenStream> {
         // Use field-level attr. with fallback to the struct-level one.
         let treat_none_as_null = match &field.treat_none_as_null {
             Some(attr) => {
+                if let Some(embed) = &field.embed {
+                    return Err(syn::Error::new(
+                        embed.attribute_span,
+                        "`embed` and `treat_none_as_default_value` are mutually exclusive",
+                    ));
+                }
+
                 if !is_option_ty(&field.ty) {
                     return Err(syn::Error::new(
                         field.ty.span(),
@@ -61,8 +68,8 @@ pub fn derive(item: DeriveInput) -> Result<TokenStream> {
             None => treat_none_as_null,
         };
 
-        match field.serialize_as.as_ref() {
-            Some(AttributeSpanWrapper { item: ty, .. }) => {
+        match (field.serialize_as.as_ref(), field.embed()) {
+            (Some(AttributeSpanWrapper { item: ty, .. }), false) => {
                 direct_field_ty.push(field_changeset_ty_serialize_as(
                     field,
                     table_name,
@@ -78,7 +85,19 @@ pub fn derive(item: DeriveInput) -> Result<TokenStream> {
 
                 generate_borrowed_changeset = false; // as soon as we hit one field with #[diesel(serialize_as)] there is no point in generating the impl of AsChangeset for borrowed structs
             }
-            None => {
+            (Some(AttributeSpanWrapper { attribute_span, .. }), true) => {
+                return Err(syn::Error::new(
+                    *attribute_span,
+                    "`#[diesel(embed)]` cannot be combined with `#[diesel(serialize_as)]`",
+                ));
+            }
+            (None, true) => {
+                direct_field_ty.push(field_changeset_ty_embed(field, None));
+                direct_field_assign.push(field_changeset_expr_embed(field, None));
+                ref_field_ty.push(field_changeset_ty_embed(field, Some(quote!(&'update))));
+                ref_field_assign.push(field_changeset_expr_embed(field, Some(quote!(&))));
+            }
+            (None, false) => {
                 direct_field_ty.push(field_changeset_ty(
                     field,
                     table_name,
@@ -151,6 +170,17 @@ pub fn derive(item: DeriveInput) -> Result<TokenStream> {
     )))
 }
 
+fn field_changeset_ty_embed(field: &Field, lifetime: Option<TokenStream>) -> TokenStream {
+    let field_ty = &field.ty;
+    let span = field.span;
+    quote_spanned!(span=> #lifetime #field_ty)
+}
+
+fn field_changeset_expr_embed(field: &Field, lifetime: Option<TokenStream>) -> TokenStream {
+    let field_name = &field.name;
+    quote!(#lifetime self.#field_name)
+}
+
 fn field_changeset_ty(
     field: &Field,
     table_name: &Path,
diff --git a/diesel_derives/src/lib.rs b/diesel_derives/src/lib.rs
index 40c607d407c5..3b53aca5527b 100644
--- a/diesel_derives/src/lib.rs
+++ b/diesel_derives/src/lib.rs
@@ -62,6 +62,11 @@ mod valid_grouping;
 /// from the name of the corresponding column, you can annotate the field with
 /// `#[diesel(column_name = some_column_name)]`.
 ///
+/// 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.
+///
 /// To provide custom serialization behavior for a field, you can use
 /// `#[diesel(serialize_as = SomeType)]`. If this attribute is present, Diesel
 /// will call `.into` on the corresponding field and serialize the instance of `SomeType`,
@@ -98,6 +103,8 @@ mod valid_grouping;
 /// * `#[diesel(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 a single database field, but is a struct that implements `AsChangeset`.
 /// * `#[diesel(serialize_as = SomeType)]`, instead of serializing the actual
 ///    field type, Diesel will convert the field into `SomeType` using `.into` and
 ///    serialize that instead. By default, this derive will serialize directly using


Test patch:
diff --git a/diesel_compile_tests/Cargo.lock b/diesel_compile_tests/Cargo.lock
index f0856d0eee0b..0628ac40c042 100644
--- a/diesel_compile_tests/Cargo.lock
+++ b/diesel_compile_tests/Cargo.lock
@@ -137,6 +137,7 @@ dependencies = [
  "byteorder",
  "chrono",
  "diesel_derives",
+ "downcast-rs",
  "ipnetwork",
  "itoa",
  "libc",
@@ -180,6 +181,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
 [[package]]
 name = "dsl_auto_type"
 version = "0.1.0"
diff --git a/diesel_compile_tests/tests/fail/derive/embed_and_serialize_as_cannot_be_mixed.rs b/diesel_compile_tests/tests/fail/derive/embed_and_serialize_as_cannot_be_mixed.rs
index 20cdcba7a17e..e0e3d2367c6f 100644
--- a/diesel_compile_tests/tests/fail/derive/embed_and_serialize_as_cannot_be_mixed.rs
+++ b/diesel_compile_tests/tests/fail/derive/embed_and_serialize_as_cannot_be_mixed.rs
@@ -9,7 +9,7 @@ table! {
     }
 }
 
-#[derive(Insertable)]
+#[derive(Insertable, AsChangeset)]
 #[diesel(table_name = users)]
 struct NameAndHairColor<'a> {
     name: &'a str,
@@ -24,4 +24,13 @@ struct User<'a> {
     name_and_hair_color: NameAndHairColor<'a>,
 }
 
+#[derive(AsChangeset)]
+#[diesel(table_name = users)]
+struct UserChangeSet<'a> {
+    id: i32,
+    #[diesel(embed, serialize_as = SomeType)]
+    // to test the compile error, this type doesn't need to exist
+    name_and_hair_color: NameAndHairColor<'a>,
+}
+
 fn main() {}
diff --git a/diesel_compile_tests/tests/fail/derive/embed_and_serialize_as_cannot_be_mixed.stderr b/diesel_compile_tests/tests/fail/derive/embed_and_serialize_as_cannot_be_mixed.stderr
index 566e1261a3b2..9897376307b3 100644
--- a/diesel_compile_tests/tests/fail/derive/embed_and_serialize_as_cannot_be_mixed.stderr
+++ b/diesel_compile_tests/tests/fail/derive/embed_and_serialize_as_cannot_be_mixed.stderr
@@ -3,3 +3,9 @@ error: `#[diesel(embed)]` cannot be combined with `#[diesel(serialize_as)]`
    |
 22 |     #[diesel(embed, serialize_as = SomeType)]
    |       ^^^^^^
+
+error: `#[diesel(embed)]` cannot be combined with `#[diesel(serialize_as)]`
+  --> tests/fail/derive/embed_and_serialize_as_cannot_be_mixed.rs:31:7
+   |
+31 |     #[diesel(embed, serialize_as = SomeType)]
+   |       ^^^^^^
diff --git a/diesel_derives/tests/as_changeset.rs b/diesel_derives/tests/as_changeset.rs
index 544a64cec0aa..6c1802592fdd 100644
--- a/diesel_derives/tests/as_changeset.rs
+++ b/diesel_derives/tests/as_changeset.rs
@@ -843,3 +843,51 @@ fn option_fields_are_correctly_detected() {
     // Causes a compile error if the field is not detected as `Option<T>`
     define!((((Option<String>))));
 }
+
+#[test]
+fn embedded_struct() {
+    #[derive(AsChangeset)]
+    #[diesel(table_name = users)]
+    struct UserAttributes<'a> {
+        hair_color: &'a str,
+        r#type: &'a str,
+    }
+
+    #[derive(AsChangeset)]
+    #[diesel(table_name = users)]
+    struct UserForm<'a> {
+        name: &'a str,
+        #[diesel(embed)]
+        attribute: UserAttributes<'a>,
+    }
+
+    let connection = &mut connection_with_sean_and_tess_in_users_table();
+
+    update(users::table.find(1))
+        .set(&UserForm {
+            name: "Jim",
+            attribute: UserAttributes {
+                hair_color: "blue",
+                r#type: "super",
+            },
+        })
+        .execute(connection)
+        .unwrap();
+
+    let expected = vec![
+        (
+            1,
+            String::from("Jim"),
+            Some(String::from("blue")),
+            Some(String::from("super")),
+        ),
+        (
+            2,
+            String::from("Tess"),
+            Some(String::from("brown")),
+            Some(String::from("admin")),
+        ),
+    ];
+    let actual = users::table.order(users::id).load(connection);
+    assert_eq!(Ok(expected), actual);
+}