1use std::borrow::Cow;
2
3use crate::error::Error;
4use crate::expr::{Expr, IntoExpr, IntoExprList};
5use crate::writer::{Expression, SqlWriter};
6
7use super::set::{HasSet, Set};
8use super::where_::{HasWhere, Where};
9
10#[derive(Debug, Clone, Default)]
19pub struct Conflict {
20 pub expression: Option<Expr>,
22}
23
24impl Conflict {
25 pub fn set_conflict(&mut self, conflict: impl IntoExpr) {
27 self.expression = Some(conflict.into_expr());
28 }
29
30 pub fn is_empty(&self) -> bool {
32 self.expression.is_none()
33 }
34}
35
36impl Expression for Conflict {
37 fn write_sql(&self, w: &mut SqlWriter<'_>) {
38 w.write_if_some(self.expression.as_ref(), "", "");
39 }
40}
41
42pub trait HasConflict {
44 fn conflict_mut(&mut self) -> &mut Conflict;
46}
47
48impl HasConflict for Conflict {
49 fn conflict_mut(&mut self) -> &mut Conflict {
50 self
51 }
52}
53
54#[derive(Debug, Clone, Default)]
79pub struct ConflictClause {
80 pub target: ConflictTarget,
82 pub action: Option<ConflictAction>,
85 pub set: Set,
87 pub where_: Where,
89}
90
91impl ConflictClause {
92 pub fn do_nothing() -> Self {
94 ConflictClause {
95 action: Some(ConflictAction::Nothing),
96 ..ConflictClause::default()
97 }
98 }
99
100 pub fn do_update() -> Self {
102 ConflictClause {
103 action: Some(ConflictAction::Update),
104 ..ConflictClause::default()
105 }
106 }
107
108 pub fn is_empty(&self) -> bool {
110 self.action.is_none()
111 }
112}
113
114impl Expression for ConflictClause {
115 fn write_sql(&self, w: &mut SqlWriter<'_>) {
116 let Some(action) = &self.action else {
117 return;
118 };
119 if matches!(action, ConflictAction::Update) && self.set.is_empty() {
120 w.record_error(Error::Incomplete(
121 "the assignments of ON CONFLICT DO UPDATE",
122 ));
123 return;
124 }
125
126 w.push_str("ON CONFLICT");
127 w.write_if(!self.target.is_empty(), " ", &self.target, "");
128 w.push_str(" DO ");
129 w.push_str(action.as_str());
130
131 w.write_if(!self.set.is_empty(), " SET ", &self.set, "");
134 w.write_if(!self.where_.is_empty(), " ", &self.where_, "");
135 }
136}
137
138impl HasSet for ConflictClause {
139 fn set_mut(&mut self) -> &mut Set {
140 &mut self.set
141 }
142}
143
144impl HasWhere for ConflictClause {
145 fn where_mut(&mut self) -> &mut Where {
146 &mut self.where_
147 }
148}
149
150pub trait HasConflictClause {
153 fn conflict_clause_mut(&mut self) -> &mut ConflictClause;
155}
156
157impl HasConflictClause for ConflictClause {
158 fn conflict_clause_mut(&mut self) -> &mut ConflictClause {
159 self
160 }
161}
162
163#[derive(Debug, Clone, Default)]
169pub struct ConflictTarget {
170 pub constraint: Option<Cow<'static, str>>,
172 pub columns: Vec<Expr>,
174 pub where_: Where,
177}
178
179impl ConflictTarget {
180 pub fn on_columns(columns: impl IntoExprList) -> Self {
182 ConflictTarget {
183 columns: columns.into_expr_list(),
184 ..ConflictTarget::default()
185 }
186 }
187
188 pub fn on_constraint(name: impl Into<Cow<'static, str>>) -> Self {
190 ConflictTarget {
191 constraint: Some(name.into()),
192 ..ConflictTarget::default()
193 }
194 }
195
196 pub fn is_empty(&self) -> bool {
198 self.constraint.is_none() && self.columns.is_empty() && self.where_.is_empty()
199 }
200}
201
202impl Expression for ConflictTarget {
203 fn write_sql(&self, w: &mut SqlWriter<'_>) {
204 if let Some(constraint) = &self.constraint {
205 w.push_str("ON CONSTRAINT ");
206 w.push_quoted(&[constraint]);
207 return;
208 }
209
210 if self.columns.is_empty() {
211 if !self.where_.is_empty() {
219 w.record_error(Error::Incomplete(
220 "the column list an ON CONFLICT index predicate belongs to",
221 ));
222 }
223 return;
224 }
225
226 w.write_slice(&self.columns, "(", ", ", ")");
227 w.write_if(!self.where_.is_empty(), " ", &self.where_, "");
228 }
229}
230
231impl HasWhere for ConflictTarget {
232 fn where_mut(&mut self) -> &mut Where {
233 &mut self.where_
234 }
235}
236
237#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239pub enum ConflictAction {
240 Nothing,
242 Update,
244}
245
246impl ConflictAction {
247 pub fn as_str(self) -> &'static str {
249 match self {
250 ConflictAction::Nothing => "NOTHING",
251 ConflictAction::Update => "UPDATE",
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use keelson_sqlcheck::testing::assert_frag_sql;
259
260 use super::*;
261 use crate::dialect::testing::Numbered;
262 use crate::expr::{Chain, arg, quote};
263 use crate::value::Value;
264 use crate::writer::build;
265
266 const FRAME: &str = r#"INSERT INTO users ("id", "name") VALUES (1, 'kubo') {}"#;
271 const TARGET_FRAME: &str =
274 r#"INSERT INTO tags ("id", "name") VALUES (1, 'rust') ON CONFLICT {} DO NOTHING"#;
275
276 fn sql(e: &impl Expression) -> String {
277 build(&Numbered, e).expect("render").0
278 }
279
280 #[test]
281 fn an_actionless_clause_writes_nothing() {
282 assert_frag_sql(FRAME, &sql(&ConflictClause::default()), "");
283 assert!(ConflictClause::default().is_empty());
284 assert_frag_sql(FRAME, &sql(&Conflict::default()), "");
285 assert!(Conflict::default().is_empty());
286 }
287
288 #[test]
289 fn do_nothing_needs_no_target() {
290 assert_frag_sql(
293 FRAME,
294 &sql(&ConflictClause::do_nothing()),
295 "ON CONFLICT DO NOTHING",
296 );
297 }
298
299 #[test]
300 fn a_column_target_precedes_the_action() {
301 let c = ConflictClause {
302 target: ConflictTarget::on_columns(quote("id")),
303 ..ConflictClause::do_nothing()
304 };
305 assert_frag_sql(FRAME, &sql(&c), r#"ON CONFLICT ("id") DO NOTHING"#);
306 }
307
308 #[test]
309 fn a_constraint_name_beats_the_column_list() {
310 let mut t = ConflictTarget::on_constraint("tags_name_key");
314 t.columns = vec![quote("name")];
315 t.where_.append_where("id IS NOT NULL");
316 assert_frag_sql(TARGET_FRAME, &sql(&t), r#"ON CONSTRAINT "tags_name_key""#);
317 }
318
319 #[test]
320 fn a_partial_index_target_carries_the_indexs_own_predicate() {
321 let mut t = ConflictTarget::on_columns((quote("email"), quote("tenant_id")));
331 t.where_.append_where("deleted_at IS NULL");
332 assert_eq!(
333 build(&Numbered, &t).unwrap().0,
334 r#"("email", "tenant_id") WHERE deleted_at IS NULL"#
335 );
336 assert!(!t.is_empty());
337 }
338
339 #[test]
340 fn an_empty_target_writes_nothing() {
341 assert_frag_sql(TARGET_FRAME, &sql(&ConflictTarget::default()), "");
342 assert!(ConflictTarget::default().is_empty());
343 }
344
345 #[test]
346 fn an_index_predicate_without_a_column_list_is_a_recorded_failure() {
347 let mut t = ConflictTarget::default();
350 t.where_mut().append_where("deleted_at IS NULL");
351 assert!(!t.is_empty());
352 let err = build(&Numbered, &t).unwrap_err();
353 assert!(
356 matches!(&err, Error::Incomplete(what) if what.contains("column list")),
357 "got: {err}"
358 );
359 }
360
361 #[test]
362 fn do_update_carries_the_set_keyword_and_its_own_where() {
363 let mut c = ConflictClause {
366 target: ConflictTarget::on_columns(quote("id")),
367 ..ConflictClause::do_update()
368 };
369 c.set_mut()
370 .append_set(Expr::raw(r#""name" = EXCLUDED."name""#));
371 c.where_mut()
372 .append_where(quote(("users", "id")).gt(arg(0i32)));
373
374 let (rendered, args) = build(&Numbered, &c).unwrap();
375 assert_frag_sql(
376 FRAME,
377 &rendered,
378 r#"ON CONFLICT ("id") DO UPDATE SET "name" = EXCLUDED."name" WHERE ("users"."id" > $1)"#,
379 );
380 assert_eq!(args, vec![Value::I32(0)]);
381 }
382
383 #[test]
384 fn do_update_without_assignments_is_a_recorded_failure() {
385 let err = build(&Numbered, &ConflictClause::do_update()).unwrap_err();
388 assert!(
391 matches!(&err, Error::Incomplete(what) if what.contains("assignments")),
392 "got: {err}"
393 );
394 }
395
396 #[test]
397 fn the_two_nested_wheres_are_independent() {
398 let mut c = ConflictClause::do_update();
399 c.set_mut().append_set(Expr::raw("a = 1"));
400 c.target.where_mut().append_where("index_pred");
401 c.where_mut().append_where("row_pred");
402 c.target.columns = vec![quote("id")];
403
404 assert_eq!(
410 build(&Numbered, &c).unwrap().0,
411 r#"ON CONFLICT ("id") WHERE index_pred DO UPDATE SET a = 1 WHERE row_pred"#
412 );
413 }
414
415 #[test]
416 fn the_slot_is_transparent_to_whatever_a_dialect_puts_in_it() {
417 let mut slot = Conflict::default();
422 slot.set_conflict(Expr::raw("ON DUPLICATE KEY UPDATE `a` = 1"));
423 assert_eq!(
424 build(&Numbered, &slot).unwrap().0,
425 "ON DUPLICATE KEY UPDATE `a` = 1"
426 );
427
428 let mut slot = Conflict::default();
429 slot.set_conflict(Expr::custom(ConflictClause::do_nothing()));
430 assert_frag_sql(FRAME, &sql(&slot), "ON CONFLICT DO NOTHING");
431 }
432}