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

Issue:
table have more than one primary_key can not use  on_conflict(diesel::dsl::DuplicatedKeys) 
<!--
If you want to report a bug, we added some points below which help us track down the problem faster.
-->

## Setup
schema.rs
```
diesel::table! {
    sys_api (path, method) {
        #[max_length = 255]
        path -> Varchar,
        #[max_length = 50]
        method -> Varchar,
        #[max_length = 50]
        name -> Varchar,
        #[max_length = 50]
        scope -> Varchar,
        #[max_length = 10]
        state -> Varchar,
    }
}
```
models.rs
```
#[derive(
    Clone,
    Debug,
    Serialize,
    Deserialize,
    Default,
    PartialEq,
    Queryable,
    Insertable,
    Identifiable,
    AsChangeset,
)]
#[diesel(table_name = sys_api)]
#[diesel(primary_key(path,method))]
pub struct SysApi {
    pub id: u64,
    pub path: String,
    pub method: String,
    pub name: String,
    pub scope: String,
    pub state: String,
}
```
### Versions

- **Rust:**1.76
- **Diesel:**2.14
- **Database:**mysql
- **Operating System**windows

### Feature Flags

- **diesel:**

## Problem Description
table have more than one primary_key can not use  on_conflict(diesel::dsl::DuplicatedKeys) 

### What are you trying to accomplish?


### What is the expected output?


### What is the actual output?
```
 Diesel,
     |     ^^^^^^ the trait `QueryFragment<diesel::mysql::Mysql, mysql::backend::MysqlOnConflictClause>` is not implemented for `query_builder::upsert::on_conflict_clause::OnConflictValues<query_builder::insert_statement::ValuesClause<(DefaultableColumnInsertValue<ColumnInsertValue<schema::sys_api::columns::path, expression::bound::Bound<Text, &std::string::String>>>, DefaultableColumnInsertValue<ColumnInsertValue<schema::sys_api::columns::method, expression::bound::Bound<Text, &std::string::String>>>, DefaultableColumnInsertValue<ColumnInsertValue<schema::sys_api::columns::name, expression::bound::Bound<Text, &std::string::String>>>, DefaultableColumnInsertValue<ColumnInsertValue<schema::sys_api::columns::scope, expression::bound::Bound<Text, &std::string::String>>>, DefaultableColumnInsertValue<ColumnInsertValue<schema::sys_api::columns::state, expression::bound::Bound<Text, &std::string::String>>>), schema::sys_api::table>, query_builder::upsert::on_conflict_target::ConflictTarget<DuplicatedKeys>, query_builder::upsert::on_conflict_actions::DoUpdate<(query_builder::update_statement::changeset::Assign<query_builder::update_statement::changeset::ColumnWrapperForUpdate<schema::sys_api::columns::name>, SqlLiteral<Text>>, query_builder::update_statement::changeset::Assign<query_builder::update_statement::changeset::ColumnWrapperForUpdate<schema::sys_api::columns::scope>, SqlLiteral<Text>>, query_builder::update_statement::changeset::Assign<query_builder::update_statement::changeset::ColumnWrapperForUpdate<schema::sys_api::columns::state>, SqlLiteral<Text>>), schema::sys_api::table>>`
     |
     = help: the following other types implement trait `QueryFragment<DB, SP>`:
               <query_builder::upsert::on_conflict_clause::OnConflictValues<Values, Target, Action> as QueryFragment<diesel::mysql::Mysql, mysql::backend::MysqlOnConflictClause>>
               <query_builder::upsert::on_conflict_clause::OnConflictValues<Values, Target, Action> as QueryFragment<DB>>
               <query_builder::upsert::on_conflict_clause::OnConflictValues<Values, Target, Action> as QueryFragment<DB, SD>>
               <query_builder::upsert::on_conflict_clause::OnConflictValues<Values, Target, Action, query_builder::where_clause::WhereClause<Expr>> as QueryFragment<DB>>
     = note: required for `OnConflictValues<ValuesClause<(DefaultableColumnInsertValue<ColumnInsertValue<path, Bound<Text, &String>>>, ..., ..., ..., ...), ...>, ..., ...>` to implement `QueryFragment<diesel::mysql::Mysql>`
     = note: the full type name has been written to 'D:\code\rust\cx\target\debug\deps\cx_biz_sys-6a229df924011786.long-type-17862108743910028546.txt'
     = note: 1 redundant requirement hidden
     = note: required for `InsertStatement<table, OnConflictValues<ValuesClause<(DefaultableColumnInsertValue<ColumnInsertValue<path, ...>>, ..., ..., ..., ...), ...>, ..., ...>>` to implement `QueryFragment<diesel::mysql::Mysql>`
     = note: the full type name has been written to 'D:\code\rust\cx\target\debug\deps\cx_biz_sys-6a229df924011786.long-type-2431980552689726686.txt'
     = note: required for `InsertStatement<table, OnConflictValues<ValuesClause<(DefaultableColumnInsertValue<ColumnInsertValue<path, ...>>, ..., ..., ..., ...), ...>, ..., ...>>` to implement `ExecuteDsl<PooledConnection<ConnectionManager<MysqlConnection>>, diesel::mysql::Mysql>`
     = note: the full type name has been written to 'D:\code\rust\cx\target\debug\deps\cx_biz_sys-6a229df924011786.long-type-2431980552689726686.txt'
note: required by a bound in `diesel::RunQueryDsl::execute`
    --> D:\code\rust\diesel\diesel\src\query_dsl\mod.rs:1431:15
     |
1428 |     fn execute(self, conn: &mut Conn) -> QueryResult<usize>
     |        ------- required by a bound in this associated function
...
1431 |         Self: methods::ExecuteDsl<Conn>,
     |               ^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RunQueryDsl::execute`
     = note: this error originates in the derive macro `Diesel` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
```

### Are you seeing any additional errors?


### Steps to reproduce

<!--
Please include as much of your codebase as needed to reproduce the error.  If the relevant files are large, please consider linking to a public repository or a [Gist](https://gist.github.com/). This includes normally the following parts:

* The exact code where your hit the problem
* Relevant parts your schema, so any `table!` macro calls required for
* Any other type definitions involved in the code, which produces your problem
-->

## Checklist

- [x] I have already looked over the [issue tracker](https://github.com/diesel-rs/diesel/issues) and the [discussion forum](https://github.com/diesel-rs/diesel/discussions) for similar possible closed issues.
<!--
If you are unsure if your issue is a duplicate of an existing issue please link the issue in question here
--> 
- [x] This issue can be reproduced on Rust's stable channel. (Your issue will be
  closed if this is not the case)
- [x] This issue can be reproduced without requiring a third party crate

<!--
Thank you for your submission!  You're helping make Diesel more robust 🎉

We'll try to respond as quickly as possible.
-->



Discussion / hints:
Thanks for opening this issue. Please provide all the relevant code that is necessary to reproduce your error message, as otherwise it's hard/impossible for anyone to see what's wrong here.  Also please provide the complete error message as emitted by rustc. Based on the provided part of the error message it seems to miss relevant information (like line information and code snippet of the actual error.)
# FAIL code
**schema.rs**
```
diesel::table! {
    sys_api (path, method) {
        #[max_length = 255]
        path -> Varchar,
        #[max_length = 50]
        method -> Varchar,
        #[max_length = 50]
        name -> Varchar,
        #[max_length = 50]
        scope -> Varchar,
        #[max_length = 10]
        state -> Varchar,
    }
}
```
**models.rs**
```
#[derive(
    Clone,
    Debug,
    Serialize,
    Deserialize,
    Default,
    PartialEq,
    Queryable,
    Insertable,
    Identifiable,
    AsChangeset,
)]
#[diesel(table_name = sys_api)]
#[diesel(primary_key(path,method))]
pub struct SysApi {
    pub path: String,
    pub method: String,
    pub name: String,
    pub scope: String,
    pub state: String,
}
```
**insert into code**
```
let v = SysApi::default();
diesel::insert_into(sys_api)
            .values(&v)
            .on_conflict(diesel::dsl::DuplicatedKeys)
            .do_update()
            .set(&v)
            .execute(con)?;
```

**error log** 
```

Diesel,
     |     ^^^^^^ the trait `QueryFragment<diesel::mysql::Mysql, mysql::backend::MysqlOnConflictClause>` is not implemented for `query_builder::upsert::on_conflict_clause::OnConflictValues<query_builder::insert_statement::ValuesClause<(DefaultableColumnInsertValue<ColumnInsertValue<schema::sys_api::columns::path, expression::bound::Bound<Text, &std::string::String>>>, DefaultableColumnInsertValue<ColumnInsertValue<schema::sys_api::columns::method, expression::bound::Bound<Text, &std::string::String>>>, DefaultableColumnInsertValue<ColumnInsertValue<schema::sys_api::columns::name, expression::bound::Bound<Text, &std::string::String>>>, DefaultableColumnInsertValue<ColumnInsertValue<schema::sys_api::columns::scope, expression::bound::Bound<Text, &std::string::String>>>, DefaultableColumnInsertValue<ColumnInsertValue<schema::sys_api::columns::state, expression::bound::Bound<Text, &std::string::String>>>), schema::sys_api::table>, query_builder::upsert::on_conflict_target::ConflictTarget<DuplicatedKeys>, query_builder::upsert::on_conflict_actions::DoUpdate<(query_builder::update_statement::changeset::Assign<query_builder::update_statement::changeset::ColumnWrapperForUpdate<schema::sys_api::columns::name>, SqlLiteral<Text>>, query_builder::update_statement::changeset::Assign<query_builder::update_statement::changeset::ColumnWrapperForUpdate<schema::sys_api::columns::scope>, SqlLiteral<Text>>, query_builder::update_statement::changeset::Assign<query_builder::update_statement::changeset::ColumnWrapperForUpdate<schema::sys_api::columns::state>, SqlLiteral<Text>>), schema::sys_api::table>>`
     |
     = help: the following other types implement trait `QueryFragment<DB, SP>`:
               <query_builder::upsert::on_conflict_clause::OnConflictValues<Values, Target, Action> as QueryFragment<diesel::mysql::Mysql, mysql::backend::MysqlOnConflictClause>>
               <query_builder::upsert::on_conflict_clause::OnConflictValues<Values, Target, Action> as QueryFragment<DB>>
               <query_builder::upsert::on_conflict_clause::OnConflictValues<Values, Target, Action> as QueryFragment<DB, SD>>
               <query_builder::upsert::on_conflict_clause::OnConflictValues<Values, Target, Action, query_builder::where_clause::WhereClause<Expr>> as QueryFragment<DB>>
     = note: required for `OnConflictValues<ValuesClause<(DefaultableColumnInsertValue<ColumnInsertValue<path, Bound<Text, &String>>>, ..., ..., ..., ...), ...>, ..., ...>` to implement `QueryFragment<diesel::mysql::Mysql>`
     = note: the full type name has been written to 'D:\code\rust\cx\target\debug\deps\cx_biz_sys-6a229df924011786.long-type-17862108743910028546.txt'
     = note: 1 redundant requirement hidden
     = note: required for `InsertStatement<table, OnConflictValues<ValuesClause<(DefaultableColumnInsertValue<ColumnInsertValue<path, ...>>, ..., ..., ..., ...), ...>, ..., ...>>` to implement `QueryFragment<diesel::mysql::Mysql>`
     = note: the full type name has been written to 'D:\code\rust\cx\target\debug\deps\cx_biz_sys-6a229df924011786.long-type-2431980552689726686.txt'
     = note: required for `InsertStatement<table, OnConflictValues<ValuesClause<(DefaultableColumnInsertValue<ColumnInsertValue<path, ...>>, ..., ..., ..., ...), ...>, ..., ...>>` to implement `ExecuteDsl<PooledConnection<ConnectionManager<MysqlConnection>>, diesel::mysql::Mysql>`
     = note: the full type name has been written to 'D:\code\rust\cx\target\debug\deps\cx_biz_sys-6a229df924011786.long-type-2431980552689726686.txt'
note: required by a bound in `diesel::RunQueryDsl::execute`
    --> D:\code\rust\diesel\diesel\src\query_dsl\mod.rs:1431:15
     |
1428 |     fn execute(self, conn: &mut Conn) -> QueryResult<usize>
     |        ------- required by a bound in this associated function
...
1431 |         Self: methods::ExecuteDsl<Conn>,
     |               ^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `RunQueryDsl::execute`
     = note: this error originates in the derive macro `Diesel` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
```
## when id use one Field primary_key this code  cargo check pass

# Pass Code

**schema.rs**
```
diesel::table! {
    sys_api (id) {
        id -> Unsigned<BigInt>,
        #[max_length = 255]
        path -> Varchar,
        #[max_length = 50]
        method -> Varchar,
        #[max_length = 50]
        name -> Varchar,
        #[max_length = 50]
        scope -> Varchar,
        #[max_length = 10]
        state -> Varchar,
    }
}
```
**models.rs**
```
#[derive(
    Clone,
    Debug,
    Serialize,
    Deserialize,
    Default,
    PartialEq,
    Queryable,
    Insertable,
    Identifiable,
    AsChangeset,
)]
#[diesel(table_name = sys_api)]
#[diesel(primary_key(id))]
pub struct SysApi {
    pub id: u64,
    pub path: String,
    pub method: String,
    pub name: String,
    pub scope: String,
    pub state: String,
}
```
**insert into code**
```
let v = SysApi::default();
diesel::insert_into(sys_api)
            .values(&v)
            .on_conflict(diesel::dsl::DuplicatedKeys)
            .do_update()
            .set(&v)
            .execute(con)?;
```

@weiznich please help confirm again
As this is a project run in my free time I can only work on stuff if I have time. That usually means that it can take several days/weeks/month until you hear back from me about something. Pinging me usually puts such things down on my priority list.

Fix patch:
diff --git a/diesel/src/mysql/query_builder/query_fragment_impls.rs b/diesel/src/mysql/query_builder/query_fragment_impls.rs
index 0b4276aa8213..daa450b9e373 100644
--- a/diesel/src/mysql/query_builder/query_fragment_impls.rs
+++ b/diesel/src/mysql/query_builder/query_fragment_impls.rs
@@ -76,17 +76,11 @@ impl<T> QueryFragment<Mysql, crate::mysql::backend::MysqlOnConflictClause> for D
 where
     T: Table + StaticQueryFragment,
     T::Component: QueryFragment<Mysql>,
-    T::PrimaryKey: Column,
+    T::PrimaryKey: DoNothingClauseHelper,
 {
     fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Mysql>) -> QueryResult<()> {
         out.push_sql(" UPDATE ");
-        T::STATIC_COMPONENT.walk_ast(out.reborrow())?;
-        out.push_sql(".");
-        out.push_identifier(<T::PrimaryKey as Column>::NAME)?;
-        out.push_sql(" = ");
-        T::STATIC_COMPONENT.walk_ast(out.reborrow())?;
-        out.push_sql(".");
-        out.push_identifier(<T::PrimaryKey as Column>::NAME)?;
+        T::PrimaryKey::walk_ast::<T>(out.reborrow())?;
         Ok(())
     }
 }
@@ -95,20 +89,14 @@ impl<T, Tab> QueryFragment<Mysql, crate::mysql::backend::MysqlOnConflictClause>
 where
     T: QueryFragment<Mysql>,
     Tab: Table + StaticQueryFragment,
+    Tab::PrimaryKey: DoNothingClauseHelper,
     Tab::Component: QueryFragment<Mysql>,
-    Tab::PrimaryKey: Column,
 {
     fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Mysql>) -> QueryResult<()> {
         out.unsafe_to_cache_prepared();
         out.push_sql(" UPDATE ");
         if self.changeset.is_noop(out.backend())? {
-            Tab::STATIC_COMPONENT.walk_ast(out.reborrow())?;
-            out.push_sql(".");
-            out.push_identifier(<Tab::PrimaryKey as Column>::NAME)?;
-            out.push_sql(" = ");
-            Tab::STATIC_COMPONENT.walk_ast(out.reborrow())?;
-            out.push_sql(".");
-            out.push_identifier(<Tab::PrimaryKey as Column>::NAME)?;
+            Tab::PrimaryKey::walk_ast::<Tab>(out.reborrow())?;
         } else {
             self.changeset.walk_ast(out.reborrow())?;
         }
@@ -159,3 +147,74 @@ where
         self.0.walk_ast(out)
     }
 }
+
+/// This is a helper trait
+/// that provideds a fake `DO NOTHING` clause
+/// based on reassigning the possible
+/// composite primary key to itself
+trait DoNothingClauseHelper {
+    fn walk_ast<T>(out: AstPass<'_, '_, Mysql>) -> QueryResult<()>
+    where
+        T: StaticQueryFragment,
+        T::Component: QueryFragment<Mysql>;
+}
+
+impl<C> DoNothingClauseHelper for C
+where
+    C: Column,
+{
+    fn walk_ast<T>(mut out: AstPass<'_, '_, Mysql>) -> QueryResult<()>
+    where
+        T: StaticQueryFragment,
+        T::Component: QueryFragment<Mysql>,
+    {
+        T::STATIC_COMPONENT.walk_ast(out.reborrow())?;
+        out.push_sql(".");
+        out.push_identifier(C::NAME)?;
+        out.push_sql(" = ");
+        T::STATIC_COMPONENT.walk_ast(out.reborrow())?;
+        out.push_sql(".");
+        out.push_identifier(C::NAME)?;
+        Ok(())
+    }
+}
+
+macro_rules! do_nothing_for_composite_keys {
+    ($(
+        $Tuple:tt {
+            $(($idx:tt) -> $T:ident, $ST:ident, $TT:ident,)+
+        }
+    )+) => {
+        $(
+            impl<$($T,)*> DoNothingClauseHelper for ($($T,)*)
+            where $($T: Column,)*
+            {
+                fn walk_ast<Table>(mut out: AstPass<'_, '_, Mysql>) -> QueryResult<()>
+                where
+                    Table: StaticQueryFragment,
+                    Table::Component: QueryFragment<Mysql>,
+                {
+                    let mut first = true;
+                    $(
+                        #[allow(unused_assignments)]
+                        if first {
+                            first = false;
+                        } else {
+                            out.push_sql(", ");
+                        }
+                        Table::STATIC_COMPONENT.walk_ast(out.reborrow())?;
+                        out.push_sql(".");
+                        out.push_identifier($T::NAME)?;
+                        out.push_sql(" = ");
+                        Table::STATIC_COMPONENT.walk_ast(out.reborrow())?;
+                        out.push_sql(".");
+                        out.push_identifier($T::NAME)?;
+                    )*
+                    Ok(())
+                }
+            }
+        )*
+    }
+}
+
+diesel_derives::__diesel_for_each_tuple!(do_nothing_for_composite_keys);


Test patch:
diff --git a/diesel_tests/tests/insert.rs b/diesel_tests/tests/insert.rs
index 410d6174f156..74f8d125f64a 100644
--- a/diesel_tests/tests/insert.rs
+++ b/diesel_tests/tests/insert.rs
@@ -865,3 +865,70 @@ fn mixed_defaultable_insert() {
 
     assert_eq!(Ok(expected_data), actual_data);
 }
+
+// regression test for https://github.com/diesel-rs/diesel/issues/3872
+#[test]
+fn upsert_with_composite_primary_key_do_nothing() {
+    table! {
+        users (id, name) {
+            id -> Integer,
+            name -> Text,
+            hair_color -> Nullable<Text>,
+        }
+    }
+
+    let conn = &mut connection_with_sean_and_tess_in_users_table();
+
+    diesel::insert_into(users::table)
+        .values((users::id.eq(1), users::name.eq("John")))
+        .on_conflict_do_nothing()
+        .execute(conn)
+        .unwrap();
+    let users = users::table
+        .select(users::name)
+        .load::<String>(conn)
+        .unwrap();
+
+    assert_eq!(users[0], "Sean");
+    assert_eq!(users[1], "Tess");
+}
+
+// regression test for https://github.com/diesel-rs/diesel/issues/3872
+#[test]
+fn upsert_with_composite_primary_key_do_update() {
+    table! {
+        users (id, name) {
+            id -> Integer,
+            name -> Text,
+            hair_color -> Nullable<Text>,
+        }
+    }
+
+    let conn = &mut connection_with_sean_and_tess_in_users_table();
+
+    #[cfg(feature = "mysql")]
+    diesel::insert_into(users::table)
+        .values((users::id.eq(1), users::name.eq("John")))
+        .on_conflict(diesel::dsl::DuplicatedKeys)
+        .do_update()
+        .set(users::name.eq("Jane"))
+        .execute(conn)
+        .unwrap();
+
+    #[cfg(not(feature = "mysql"))]
+    diesel::insert_into(users::table)
+        .values((users::id.eq(1), users::name.eq("John")))
+        .on_conflict(users::id)
+        .do_update()
+        .set(users::name.eq("Jane"))
+        .execute(conn)
+        .unwrap();
+    let users = users::table
+        .select(users::name)
+        .order(users::id)
+        .load::<String>(conn)
+        .unwrap();
+
+    assert_eq!(users[0], "Jane");
+    assert_eq!(users[1], "Tess");
+}