From ae692a98dca475ae7290eec0d285bc29ded54404 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Mon, 28 Mar 2022 15:38:03 +0800 Subject: [PATCH] On conflict update by values and expressions --- Cargo.toml | 1 + src/backend/query_builder.rs | 12 +++ src/query/insert.rs | 33 +------ src/query/on_conflict.rs | 164 +++++++++++++++++++++++++++++++++-- tests/mysql/query.rs | 56 ++++++++++++ tests/postgres/query.rs | 56 ++++++++++++ tests/sqlite/query.rs | 56 ++++++++++++ 7 files changed, 342 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 078cf9d7d..a5a302dfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ time = { version = "^0.2", optional = true } [dev-dependencies] criterion = { version = "0.3", features = ["html_reports"] } +pretty_assertions = { version = "^1" } [features] backend-mysql = [] diff --git a/src/backend/query_builder.rs b/src/backend/query_builder.rs index 2e0734cbe..0b08964eb 100644 --- a/src/backend/query_builder.rs +++ b/src/backend/query_builder.rs @@ -1143,6 +1143,18 @@ pub trait QueryBuilder: QuotedBuilder { false }); } + OnConflictAction::UpdateExprs(column_exprs) => { + self.prepare_on_conflict_do_update_keywords(sql, collector); + column_exprs.iter().fold(true, |first, (col, expr)| { + if !first { + write!(sql, ", ").unwrap() + } + col.prepare(sql, self.quote()); + write!(sql, " = ").unwrap(); + self.prepare_simple_expr(expr, sql, collector); + false + }); + } } } } diff --git a/src/query/insert.rs b/src/query/insert.rs index 06af64e19..db4c4ee0d 100644 --- a/src/query/insert.rs +++ b/src/query/insert.rs @@ -295,36 +295,9 @@ impl InsertStatement { /// /// # Examples /// - /// ``` - /// use sea_query::{tests_cfg::*, *}; - /// - /// let query = Query::insert() - /// .into_table(Glyph::Table) - /// .columns(vec![Glyph::Aspect, Glyph::Image]) - /// .values_panic(vec![ - /// 2.into(), - /// 3.into(), - /// ]) - /// .on_conflict( - /// OnConflict::column(Glyph::Id) - /// .update_columns([Glyph::Aspect, Glyph::Image]) - /// .to_owned(), - /// ) - /// .to_owned(); - /// - /// assert_eq!( - /// query.to_string(MysqlQueryBuilder), - /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (2, 3) ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`), `image` = VALUES(`image`)"# - /// ); - /// assert_eq!( - /// query.to_string(PostgresQueryBuilder), - /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2, 3) ON CONFLICT ("id") DO UPDATE SET "aspect" = "excluded"."aspect", "image" = "excluded"."image""# - /// ); - /// assert_eq!( - /// query.to_string(SqliteQueryBuilder), - /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2, 3) ON CONFLICT ("id") DO UPDATE SET "aspect" = "excluded"."aspect", "image" = "excluded"."image""# - /// ); - /// ``` + /// - [`OnConflict::update_columns`]: Update column value of existing row with inserting value + /// - [`OnConflict::update_values`]: Update column value of existing row with value + /// - [`OnConflict::update_exprs`]: Update column value of existing row with expression pub fn on_conflict(&mut self, on_conflict: OnConflict) -> &mut Self { self.on_conflict = Some(on_conflict); self diff --git a/src/query/on_conflict.rs b/src/query/on_conflict.rs index b393f20ea..58811fac9 100644 --- a/src/query/on_conflict.rs +++ b/src/query/on_conflict.rs @@ -1,4 +1,4 @@ -use crate::{DynIden, IntoIden}; +use crate::{DynIden, Expr, IntoIden, SimpleExpr, Value}; #[derive(Debug, Clone)] pub struct OnConflict { @@ -16,16 +16,20 @@ pub enum OnConflictTarget { /// Represents ON CONFLICT (upsert) actions #[derive(Debug, Clone)] pub enum OnConflictAction { - /// Update the column values of existing row instead of insert + /// Update column value of existing row with inserting value UpdateColumns(Vec), + /// Update column value of existing row with expression + UpdateExprs(Vec<(DynIden, SimpleExpr)>), } impl OnConflict { + /// Create a ON CONFLICT expression without target column, + /// a special method designed for MySQL pub fn new() -> Self { Default::default() } - // Set ON CONFLICT target column + /// Set ON CONFLICT target column pub fn column(column: C) -> Self where C: IntoIden, @@ -33,7 +37,7 @@ impl OnConflict { Self::columns(vec![column]) } - // Set ON CONFLICT target columns + /// Set ON CONFLICT target columns pub fn columns(columns: I) -> Self where C: IntoIden, @@ -47,7 +51,7 @@ impl OnConflict { } } - // Set ON CONFLICT update column + /// Set ON CONFLICT update column pub fn update_column(&mut self, column: C) -> &mut Self where C: IntoIden, @@ -55,7 +59,40 @@ impl OnConflict { self.update_columns(vec![column]) } - // Set ON CONFLICT update columns + /// Set ON CONFLICT update columns + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::insert() + /// .into_table(Glyph::Table) + /// .columns(vec![Glyph::Aspect, Glyph::Image]) + /// .values_panic(vec![ + /// 2.into(), + /// 3.into(), + /// ]) + /// .on_conflict( + /// OnConflict::column(Glyph::Id) + /// .update_columns([Glyph::Aspect, Glyph::Image]) + /// .to_owned(), + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (2, 3) ON DUPLICATE KEY UPDATE `aspect` = VALUES(`aspect`), `image` = VALUES(`image`)"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2, 3) ON CONFLICT ("id") DO UPDATE SET "aspect" = "excluded"."aspect", "image" = "excluded"."image""# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2, 3) ON CONFLICT ("id") DO UPDATE SET "aspect" = "excluded"."aspect", "image" = "excluded"."image""# + /// ); + /// ``` pub fn update_columns(&mut self, columns: I) -> &mut Self where C: IntoIden, @@ -66,6 +103,121 @@ impl OnConflict { )); self } + + /// Set ON CONFLICT update value + pub fn update_value(&mut self, column_value: (C, Value)) -> &mut Self + where + C: IntoIden, + { + self.update_values(vec![column_value]) + } + + /// Set ON CONFLICT update values + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::insert() + /// .into_table(Glyph::Table) + /// .columns(vec![Glyph::Aspect, Glyph::Image]) + /// .values_panic(vec![ + /// 2.into(), + /// 3.into(), + /// ]) + /// .on_conflict( + /// OnConflict::column(Glyph::Id) + /// .update_values([ + /// (Glyph::Aspect, "04108048005887010020060000204E0180400400".into()), + /// (Glyph::Image, 3.1415.into()), + /// ]) + /// .to_owned() + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (2, 3) ON DUPLICATE KEY UPDATE `aspect` = '04108048005887010020060000204E0180400400', `image` = 3.1415"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2, 3) ON CONFLICT ("id") DO UPDATE SET "aspect" = '04108048005887010020060000204E0180400400', "image" = 3.1415"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2, 3) ON CONFLICT ("id") DO UPDATE SET "aspect" = '04108048005887010020060000204E0180400400', "image" = 3.1415"# + /// ); + /// ``` + pub fn update_values(&mut self, column_values: I) -> &mut Self + where + C: IntoIden, + I: IntoIterator, + { + self.action = Some(OnConflictAction::UpdateExprs( + column_values + .into_iter() + .map(|(c, v)| (c.into_iden(), Expr::val(v).into())) + .collect(), + )); + self + } + + /// Set ON CONFLICT update expr + pub fn update_expr(&mut self, column_expr: (C, SimpleExpr)) -> &mut Self + where + C: IntoIden, + { + self.update_exprs(vec![column_expr]) + } + + /// Set ON CONFLICT update exprs + /// + /// # Examples + /// + /// ``` + /// use sea_query::{tests_cfg::*, *}; + /// + /// let query = Query::insert() + /// .into_table(Glyph::Table) + /// .columns(vec![Glyph::Aspect, Glyph::Image]) + /// .values_panic(vec![ + /// 2.into(), + /// 3.into(), + /// ]) + /// .on_conflict( + /// OnConflict::column(Glyph::Id) + /// .update_expr((Glyph::Image, Expr::val(1).add(2))) + /// .to_owned() + /// ) + /// .to_owned(); + /// + /// assert_eq!( + /// query.to_string(MysqlQueryBuilder), + /// r#"INSERT INTO `glyph` (`aspect`, `image`) VALUES (2, 3) ON DUPLICATE KEY UPDATE `image` = 1 + 2"# + /// ); + /// assert_eq!( + /// query.to_string(PostgresQueryBuilder), + /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2, 3) ON CONFLICT ("id") DO UPDATE SET "image" = 1 + 2"# + /// ); + /// assert_eq!( + /// query.to_string(SqliteQueryBuilder), + /// r#"INSERT INTO "glyph" ("aspect", "image") VALUES (2, 3) ON CONFLICT ("id") DO UPDATE SET "image" = 1 + 2"# + /// ); + /// ``` + pub fn update_exprs(&mut self, column_exprs: I) -> &mut Self + where + C: IntoIden, + I: IntoIterator, + { + self.action = Some(OnConflictAction::UpdateExprs( + column_exprs + .into_iter() + .map(|(c, e)| (c.into_iden(), e)) + .collect(), + )); + self + } } impl Default for OnConflict { diff --git a/tests/mysql/query.rs b/tests/mysql/query.rs index 9fc4a2ff8..883435280 100644 --- a/tests/mysql/query.rs +++ b/tests/mysql/query.rs @@ -1,4 +1,5 @@ use super::*; +use pretty_assertions::assert_eq; #[test] fn select_1() { @@ -1162,6 +1163,61 @@ fn insert_on_conflict_2() { ); } +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_3() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![Glyph::Aspect, Glyph::Image]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .update_values([ + (Glyph::Aspect, "04108048005887010020060000204E0180400400".into()), + (Glyph::Image, 3.1415.into()), + ]) + .to_owned() + ) + .to_string(MysqlQueryBuilder), + [ + r#"INSERT INTO `glyph` (`aspect`, `image`)"#, + r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, + r#"ON DUPLICATE KEY UPDATE `aspect` = '04108048005887010020060000204E0180400400', `image` = 3.1415"#, + ] + .join(" ") + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_4() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![Glyph::Aspect, Glyph::Image]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .update_expr((Glyph::Image, Expr::val(1).add(2))) + .to_owned() + ) + .to_string(MysqlQueryBuilder), + [ + r#"INSERT INTO `glyph` (`aspect`, `image`)"#, + r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, + r#"ON DUPLICATE KEY UPDATE `image` = 1 + 2"#, + ] + .join(" ") + ); +} + #[test] fn update_1() { assert_eq!( diff --git a/tests/postgres/query.rs b/tests/postgres/query.rs index 59d0bef46..f6993c4ac 100644 --- a/tests/postgres/query.rs +++ b/tests/postgres/query.rs @@ -1,4 +1,5 @@ use super::*; +use pretty_assertions::assert_eq; #[test] fn select_1() { @@ -1149,6 +1150,61 @@ fn insert_on_conflict_2() { ); } +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_3() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![Glyph::Aspect, Glyph::Image]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .update_values([ + (Glyph::Aspect, "04108048005887010020060000204E0180400400".into()), + (Glyph::Image, 3.1415.into()), + ]) + .to_owned() + ) + .to_string(PostgresQueryBuilder), + [ + r#"INSERT INTO "glyph" ("aspect", "image")"#, + r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, + r#"ON CONFLICT ("id", "aspect") DO UPDATE SET "aspect" = '04108048005887010020060000204E0180400400', "image" = 3.1415"#, + ] + .join(" ") + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_4() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![Glyph::Aspect, Glyph::Image]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .update_expr((Glyph::Image, Expr::val(1).add(2))) + .to_owned() + ) + .to_string(PostgresQueryBuilder), + [ + r#"INSERT INTO "glyph" ("aspect", "image")"#, + r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, + r#"ON CONFLICT ("id", "aspect") DO UPDATE SET "image" = 1 + 2"#, + ] + .join(" ") + ); +} + #[test] fn update_1() { assert_eq!( diff --git a/tests/sqlite/query.rs b/tests/sqlite/query.rs index 69dbb4ccb..a7ad4c34e 100644 --- a/tests/sqlite/query.rs +++ b/tests/sqlite/query.rs @@ -1,4 +1,5 @@ use super::*; +use pretty_assertions::assert_eq; #[test] fn select_1() { @@ -1115,6 +1116,61 @@ fn insert_on_conflict_2() { ); } +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_3() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![Glyph::Aspect, Glyph::Image]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .update_values([ + (Glyph::Aspect, "04108048005887010020060000204E0180400400".into()), + (Glyph::Image, 3.1415.into()), + ]) + .to_owned() + ) + .to_string(SqliteQueryBuilder), + [ + r#"INSERT INTO "glyph" ("aspect", "image")"#, + r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, + r#"ON CONFLICT ("id", "aspect") DO UPDATE SET "aspect" = '04108048005887010020060000204E0180400400', "image" = 3.1415"#, + ] + .join(" ") + ); +} + +#[test] +#[allow(clippy::approx_constant)] +fn insert_on_conflict_4() { + assert_eq!( + Query::insert() + .into_table(Glyph::Table) + .columns(vec![Glyph::Aspect, Glyph::Image]) + .values_panic(vec![ + "04108048005887010020060000204E0180400400".into(), + 3.1415.into(), + ]) + .on_conflict( + OnConflict::columns([Glyph::Id, Glyph::Aspect]) + .update_expr((Glyph::Image, Expr::val(1).add(2))) + .to_owned() + ) + .to_string(SqliteQueryBuilder), + [ + r#"INSERT INTO "glyph" ("aspect", "image")"#, + r#"VALUES ('04108048005887010020060000204E0180400400', 3.1415)"#, + r#"ON CONFLICT ("id", "aspect") DO UPDATE SET "image" = 1 + 2"#, + ] + .join(" ") + ); +} + #[test] fn update_1() { assert_eq!(