Skip to content
Merged
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
target
Cargo.lock
!diesel_cli/Cargo.lock
.env
.env
2 changes: 2 additions & 0 deletions diesel/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ pub type RawValue<'a, DB> = <DB as HasRawValue<'a>>::RawValue;
pub trait SupportsReturningClause {}
/// Does this backend support 'ON CONFLICT' clause?
pub trait SupportsOnConflictClause {}
/// Does this backend support 'WHERE' clauses on 'ON CONFLICT' clauses?
pub trait SupportsOnConflictTargetDecorations {}
/// Does this backend support the bare `DEFAULT` keyword?
pub trait SupportsDefaultKeyword {}
/// Does this backend use the standard `SAVEPOINT` syntax?
Expand Down
2 changes: 2 additions & 0 deletions diesel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,8 @@ pub mod prelude {
pub use crate::macros::prelude::*;
#[doc(inline)]
pub use crate::query_builder::AsChangeset;
#[doc(inline)]
pub use crate::query_builder::DecoratableTarget;
#[doc(hidden)]
pub use crate::query_dsl::GroupByDsl;
#[doc(inline)]
Expand Down
1 change: 1 addition & 0 deletions diesel/src/pg/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ impl TypeMetadata for Pg {

impl SupportsReturningClause for Pg {}
impl SupportsOnConflictClause for Pg {}
impl SupportsOnConflictTargetDecorations for Pg {}
impl SupportsDefaultKeyword for Pg {}
impl UsesAnsiSavepointSyntax for Pg {}
1 change: 1 addition & 0 deletions diesel/src/query_builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub use self::sql_query::{BoxedSqlQuery, SqlQuery};
pub use self::update_statement::{
AsChangeset, BoxedUpdateStatement, IntoUpdateTarget, UpdateStatement, UpdateTarget,
};
pub use self::upsert::on_conflict_target_decorations::DecoratableTarget;

pub use self::limit_clause::{LimitClause, NoLimitClause};
pub use self::limit_offset_clause::{BoxedLimitOffsetClause, LimitOffsetClause};
Expand Down
1 change: 1 addition & 0 deletions diesel/src/query_builder/upsert/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub(crate) mod into_conflict_clause;
pub(crate) mod on_conflict_actions;
pub(crate) mod on_conflict_clause;
pub(crate) mod on_conflict_target;
pub(crate) mod on_conflict_target_decorations;
71 changes: 71 additions & 0 deletions diesel/src/query_builder/upsert/on_conflict_target_decorations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use crate::backend::{Backend, SupportsOnConflictClause, SupportsOnConflictTargetDecorations};
use crate::expression::Expression;
use crate::query_builder::upsert::on_conflict_target::{ConflictTarget, NoConflictTarget};
use crate::query_builder::where_clause::{NoWhereClause, WhereAnd, WhereClause};
use crate::query_builder::{AstPass, QueryFragment, QueryResult};
use crate::sql_types::BoolOrNullableBool;

pub trait UndecoratedConflictTarget {}

impl UndecoratedConflictTarget for NoConflictTarget {}
impl<T> UndecoratedConflictTarget for ConflictTarget<T> {}

/// Interface to add information to conflict targets.
/// Designed to be open for further additions to conflict targets like constraints
pub trait DecoratableTarget<P> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason why this needs to be a separate trait and we cannot reuse FilterDsl here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review @weiznich!

My initial thought also was to use FilterDsl. However, I created a new trait for 2 reasons

  1. For me it is semantically different to add a WHERE clause on a SELECT or UPDATE statement which filters the statement as a whole. In contrast the WHERE clause added to a conflict target specifies the on conflict policy. These statements are usually chained together so to me it made sense to indicate that the WHERE clause groups with the conflict target.
  2. I designed the DecoratableTarget such that it easily allows for further specification of the on conflict policy such as adding constraint names as indicated in the PostgreSQL docs on insert.

does that make sense to you?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds reasonable 👍

/// Output type of filter_target operation
type FilterOutput;
/// equivalent to filter of FilterDsl but aimed at conflict targets
fn filter_target(self, predicate: P) -> Self::FilterOutput;
}

#[derive(Debug)]
pub struct DecoratedConflictTarget<T, U> {
target: T,
where_clause: U,
}

impl<T, P> DecoratableTarget<P> for T
where
P: Expression,
P::SqlType: BoolOrNullableBool,
T: UndecoratedConflictTarget,
{
type FilterOutput = DecoratedConflictTarget<T, WhereClause<P>>;

fn filter_target(self, predicate: P) -> Self::FilterOutput {
DecoratedConflictTarget {
target: self,
where_clause: NoWhereClause.and(predicate),
}
}
}

impl<T, U, P> DecoratableTarget<P> for DecoratedConflictTarget<T, U>
where
P: Expression,
P::SqlType: BoolOrNullableBool,
U: WhereAnd<P>,
{
type FilterOutput = DecoratedConflictTarget<T, <U as WhereAnd<P>>::Output>;

fn filter_target(self, predicate: P) -> Self::FilterOutput {
DecoratedConflictTarget {
target: self.target,
where_clause: self.where_clause.and(predicate),
}
}
}

impl<DB, T, U> QueryFragment<DB> for DecoratedConflictTarget<T, U>
where
T: QueryFragment<DB>,
U: QueryFragment<DB>,
DB: Backend + SupportsOnConflictClause + SupportsOnConflictTargetDecorations,
{
fn walk_ast(&self, mut out: AstPass<DB>) -> QueryResult<()> {
self.target.walk_ast(out.reborrow())?;
self.where_clause.walk_ast(out.reborrow())?;
Ok(())
}
}
1 change: 1 addition & 0 deletions diesel/src/upsert/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::query_builder::upsert::on_conflict_actions::Excluded;

mod on_conflict_extension;

pub use self::on_conflict_extension::DecoratableTarget;
pub use self::on_conflict_extension::*;
#[cfg(feature = "postgres")]
pub use crate::pg::query_builder::on_constraint::*;
Expand Down
18 changes: 18 additions & 0 deletions diesel/src/upsert/on_conflict_extension.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::expression::Expression;
use crate::query_builder::upsert::into_conflict_clause::IntoConflictValueClause;
use crate::query_builder::upsert::on_conflict_actions::*;
use crate::query_builder::upsert::on_conflict_clause::*;
use crate::query_builder::upsert::on_conflict_target::*;
pub use crate::query_builder::upsert::on_conflict_target_decorations::DecoratableTarget;
use crate::query_builder::{AsChangeset, InsertStatement, UndecoratedInsertRecord};
use crate::query_source::QuerySource;
use crate::sql_types::BoolOrNullableBool;

impl<T, U, Op, Ret> InsertStatement<T, U, Op, Ret>
where
Expand Down Expand Up @@ -199,6 +202,21 @@ where
}
}

impl<Stmt, T, P> DecoratableTarget<P> for IncompleteOnConflict<Stmt, T>
where
P: Expression,
P::SqlType: BoolOrNullableBool,
T: DecoratableTarget<P>,
{
type FilterOutput = IncompleteOnConflict<Stmt, <T as DecoratableTarget<P>>::FilterOutput>;
fn filter_target(self, predicate: P) -> Self::FilterOutput {
IncompleteOnConflict {
stmt: self.stmt,
target: self.target.filter_target(predicate),
}
}
}

/// A partially constructed `ON CONFLICT` clause.
#[derive(Debug, Clone, Copy)]
pub struct IncompleteOnConflict<Stmt, Target> {
Expand Down
40 changes: 40 additions & 0 deletions diesel_tests/tests/debug/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,43 @@ COMMIT;
);
}
}

#[test]
#[cfg(feature = "postgres")]
fn test_upsert() {
// this test ensures we get the right debug string for upserts
use crate::schema::users::dsl::*;

let values = vec![
(name.eq("Sean"), hair_color.eq(Some("black"))),
(name.eq("Tess"), hair_color.eq(None::<&str>)),
];

let upsert_command_single_where = insert_into(users)
.values(&values)
.on_conflict(hair_color)
.filter_target(hair_color.eq("black"))
.do_nothing();
let upsert_single_where_sql_display =
debug_query::<TestBackend, _>(&upsert_command_single_where).to_string();

assert_eq!(
upsert_single_where_sql_display,
r#"INSERT INTO "users" ("name", "hair_color") VALUES ($1, $2), ($3, $4) ON CONFLICT ("hair_color") WHERE "users"."hair_color" = $5 DO NOTHING -- binds: ["Sean", Some("black"), "Tess", None, "black"]"#
);

let upsert_command_second_where = insert_into(users)
.values(&values)
.on_conflict(hair_color)
.filter_target(hair_color.eq("black"))
.filter_target(name.eq("Sean"))
.do_nothing();

let upsert_second_where_sql_display =
debug_query::<TestBackend, _>(&upsert_command_second_where).to_string();

assert_eq!(
upsert_second_where_sql_display,
r#"INSERT INTO "users" ("name", "hair_color") VALUES ($1, $2), ($3, $4) ON CONFLICT ("hair_color") WHERE "users"."hair_color" = $5 AND "users"."name" = $6 DO NOTHING -- binds: ["Sean", Some("black"), "Tess", None, "black", "Sean"]"#
);
}