Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
This commit changes some `QueryFragment` impls that previously assumed
that the provided primary key consist only of a single column. The new
implementation allows composite keys as well. In addition I also added
two tests to cover these cases as well.
  • Loading branch information
weiznich committed Dec 15, 2023
1 parent ce16e91 commit e104a1d
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 16 deletions.
87 changes: 71 additions & 16 deletions diesel/src/mysql/query_builder/query_fragment_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: DoNothingHelper,
{
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(())
}
}
Expand All @@ -95,20 +89,14 @@ impl<T, Tab> QueryFragment<Mysql, crate::mysql::backend::MysqlOnConflictClause>
where
T: QueryFragment<Mysql>,
Tab: Table + StaticQueryFragment,
Tab::PrimaryKey: DoNothingHelper,
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())?;
}
Expand Down Expand Up @@ -159,3 +147,70 @@ where
self.0.walk_ast(out)
}
}

trait DoNothingHelper {
fn walk_ast<'b, T>(out: AstPass<'_, 'b, Mysql>) -> QueryResult<()>
where
T: StaticQueryFragment,
T::Component: QueryFragment<Mysql>;
}

impl<C> DoNothingHelper for C
where
C: Column,
{
fn walk_ast<'b, T>(mut out: AstPass<'_, 'b, 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,)*> DoNothingHelper for ($($T,)*)
where $($T: Column,)*
{
fn walk_ast<'b, Table>(mut out: AstPass<'_, 'b, 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);
66 changes: 66 additions & 0 deletions diesel_tests/tests/insert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -865,3 +865,69 @@ 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)
.load::<String>(conn)
.unwrap();

assert_eq!(users[0], "Jane");
assert_eq!(users[1], "Tess");
}

0 comments on commit e104a1d

Please sign in to comment.