use std::borrow::Cow;
use crate::error::Error;
use crate::expr::{Expr, IntoExpr, IntoExprList};
use crate::writer::{Expression, SqlWriter};
use super::set::{HasSet, Set};
use super::where_::{HasWhere, Where};
#[derive(Debug, Clone, Default)]
pub struct Conflict {
pub expression: Option<Expr>,
}
impl Conflict {
pub fn set_conflict(&mut self, conflict: impl IntoExpr) {
self.expression = Some(conflict.into_expr());
}
pub fn is_empty(&self) -> bool {
self.expression.is_none()
}
}
impl Expression for Conflict {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.write_if_some(self.expression.as_ref(), "", "");
}
}
pub trait HasConflict {
fn conflict_mut(&mut self) -> &mut Conflict;
}
impl HasConflict for Conflict {
fn conflict_mut(&mut self) -> &mut Conflict {
self
}
}
#[derive(Debug, Clone, Default)]
pub struct ConflictClause {
pub target: ConflictTarget,
pub action: Option<ConflictAction>,
pub set: Set,
pub where_: Where,
}
impl ConflictClause {
pub fn do_nothing() -> Self {
ConflictClause {
action: Some(ConflictAction::Nothing),
..ConflictClause::default()
}
}
pub fn do_update() -> Self {
ConflictClause {
action: Some(ConflictAction::Update),
..ConflictClause::default()
}
}
pub fn is_empty(&self) -> bool {
self.action.is_none()
}
}
impl Expression for ConflictClause {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
let Some(action) = &self.action else {
return;
};
if matches!(action, ConflictAction::Update) && self.set.is_empty() {
w.record_error(Error::Incomplete(
"the assignments of ON CONFLICT DO UPDATE",
));
return;
}
w.push_str("ON CONFLICT");
w.write_if(!self.target.is_empty(), " ", &self.target, "");
w.push_str(" DO ");
w.push_str(action.as_str());
w.write_if(!self.set.is_empty(), " SET ", &self.set, "");
w.write_if(!self.where_.is_empty(), " ", &self.where_, "");
}
}
impl HasSet for ConflictClause {
fn set_mut(&mut self) -> &mut Set {
&mut self.set
}
}
impl HasWhere for ConflictClause {
fn where_mut(&mut self) -> &mut Where {
&mut self.where_
}
}
pub trait HasConflictClause {
fn conflict_clause_mut(&mut self) -> &mut ConflictClause;
}
impl HasConflictClause for ConflictClause {
fn conflict_clause_mut(&mut self) -> &mut ConflictClause {
self
}
}
#[derive(Debug, Clone, Default)]
pub struct ConflictTarget {
pub constraint: Option<Cow<'static, str>>,
pub columns: Vec<Expr>,
pub where_: Where,
}
impl ConflictTarget {
pub fn on_columns(columns: impl IntoExprList) -> Self {
ConflictTarget {
columns: columns.into_expr_list(),
..ConflictTarget::default()
}
}
pub fn on_constraint(name: impl Into<Cow<'static, str>>) -> Self {
ConflictTarget {
constraint: Some(name.into()),
..ConflictTarget::default()
}
}
pub fn is_empty(&self) -> bool {
self.constraint.is_none() && self.columns.is_empty() && self.where_.is_empty()
}
}
impl Expression for ConflictTarget {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
if let Some(constraint) = &self.constraint {
w.push_str("ON CONSTRAINT ");
w.push_quoted(&[constraint]);
return;
}
if self.columns.is_empty() {
if !self.where_.is_empty() {
w.record_error(Error::Incomplete(
"the column list an ON CONFLICT index predicate belongs to",
));
}
return;
}
w.write_slice(&self.columns, "(", ", ", ")");
w.write_if(!self.where_.is_empty(), " ", &self.where_, "");
}
}
impl HasWhere for ConflictTarget {
fn where_mut(&mut self) -> &mut Where {
&mut self.where_
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConflictAction {
Nothing,
Update,
}
impl ConflictAction {
pub fn as_str(self) -> &'static str {
match self {
ConflictAction::Nothing => "NOTHING",
ConflictAction::Update => "UPDATE",
}
}
}
#[cfg(test)]
mod tests {
use keelson_sqlcheck::testing::assert_frag_sql;
use super::*;
use crate::dialect::testing::Numbered;
use crate::expr::{Chain, arg, quote};
use crate::value::Value;
use crate::writer::build;
const FRAME: &str = r#"INSERT INTO users ("id", "name") VALUES (1, 'kubo') {}"#;
const TARGET_FRAME: &str =
r#"INSERT INTO tags ("id", "name") VALUES (1, 'rust') ON CONFLICT {} DO NOTHING"#;
fn sql(e: &impl Expression) -> String {
build(&Numbered, e).expect("render").0
}
#[test]
fn an_actionless_clause_writes_nothing() {
assert_frag_sql(FRAME, &sql(&ConflictClause::default()), "");
assert!(ConflictClause::default().is_empty());
assert_frag_sql(FRAME, &sql(&Conflict::default()), "");
assert!(Conflict::default().is_empty());
}
#[test]
fn do_nothing_needs_no_target() {
assert_frag_sql(
FRAME,
&sql(&ConflictClause::do_nothing()),
"ON CONFLICT DO NOTHING",
);
}
#[test]
fn a_column_target_precedes_the_action() {
let c = ConflictClause {
target: ConflictTarget::on_columns(quote("id")),
..ConflictClause::do_nothing()
};
assert_frag_sql(FRAME, &sql(&c), r#"ON CONFLICT ("id") DO NOTHING"#);
}
#[test]
fn a_constraint_name_beats_the_column_list() {
let mut t = ConflictTarget::on_constraint("tags_name_key");
t.columns = vec![quote("name")];
t.where_.append_where("id IS NOT NULL");
assert_frag_sql(TARGET_FRAME, &sql(&t), r#"ON CONSTRAINT "tags_name_key""#);
}
#[test]
fn a_partial_index_target_carries_the_indexs_own_predicate() {
let mut t = ConflictTarget::on_columns((quote("email"), quote("tenant_id")));
t.where_.append_where("deleted_at IS NULL");
assert_eq!(
build(&Numbered, &t).unwrap().0,
r#"("email", "tenant_id") WHERE deleted_at IS NULL"#
);
assert!(!t.is_empty());
}
#[test]
fn an_empty_target_writes_nothing() {
assert_frag_sql(TARGET_FRAME, &sql(&ConflictTarget::default()), "");
assert!(ConflictTarget::default().is_empty());
}
#[test]
fn an_index_predicate_without_a_column_list_is_a_recorded_failure() {
let mut t = ConflictTarget::default();
t.where_mut().append_where("deleted_at IS NULL");
assert!(!t.is_empty());
let err = build(&Numbered, &t).unwrap_err();
assert!(
matches!(&err, Error::Incomplete(what) if what.contains("column list")),
"got: {err}"
);
}
#[test]
fn do_update_carries_the_set_keyword_and_its_own_where() {
let mut c = ConflictClause {
target: ConflictTarget::on_columns(quote("id")),
..ConflictClause::do_update()
};
c.set_mut()
.append_set(Expr::raw(r#""name" = EXCLUDED."name""#));
c.where_mut()
.append_where(quote(("users", "id")).gt(arg(0i32)));
let (rendered, args) = build(&Numbered, &c).unwrap();
assert_frag_sql(
FRAME,
&rendered,
r#"ON CONFLICT ("id") DO UPDATE SET "name" = EXCLUDED."name" WHERE ("users"."id" > $1)"#,
);
assert_eq!(args, vec![Value::I32(0)]);
}
#[test]
fn do_update_without_assignments_is_a_recorded_failure() {
let err = build(&Numbered, &ConflictClause::do_update()).unwrap_err();
assert!(
matches!(&err, Error::Incomplete(what) if what.contains("assignments")),
"got: {err}"
);
}
#[test]
fn the_two_nested_wheres_are_independent() {
let mut c = ConflictClause::do_update();
c.set_mut().append_set(Expr::raw("a = 1"));
c.target.where_mut().append_where("index_pred");
c.where_mut().append_where("row_pred");
c.target.columns = vec![quote("id")];
assert_eq!(
build(&Numbered, &c).unwrap().0,
r#"ON CONFLICT ("id") WHERE index_pred DO UPDATE SET a = 1 WHERE row_pred"#
);
}
#[test]
fn the_slot_is_transparent_to_whatever_a_dialect_puts_in_it() {
let mut slot = Conflict::default();
slot.set_conflict(Expr::raw("ON DUPLICATE KEY UPDATE `a` = 1"));
assert_eq!(
build(&Numbered, &slot).unwrap().0,
"ON DUPLICATE KEY UPDATE `a` = 1"
);
let mut slot = Conflict::default();
slot.set_conflict(Expr::custom(ConflictClause::do_nothing()));
assert_frag_sql(FRAME, &sql(&slot), "ON CONFLICT DO NOTHING");
}
}