keelson_psql/merge.rs
1//! Mods for [`psql::merge`](crate::merge()) (PostgreSQL 15+).
2//!
3//! [`into`] is the table being merged into; [`using`] is the data source —
4//! a table, or a [`subquery`](crate::subquery) with an alias — and [`on`] is the
5//! join condition between them. The source is gram.y's `table_ref`
6//! (`MergeStmt: … USING table_ref ON a_expr …`), so it may be a `joined_table`:
7//! the join mods ([`inner_join`], [`left_join`], [`right_join`], [`full_join`],
8//! [`cross_join`]) attach to it, never to the target, whose
9//! `relation_expr_opt_alias` production admits no joins. The `WHEN` clauses are
10//! chains, and the chain
11//! type is what enforces the grammar's split: a matched arm
12//! ([`when_matched`], [`when_not_matched_by_source`]) offers `UPDATE`/`DELETE`/
13//! `DO NOTHING`, a not-matched arm ([`when_not_matched`]) offers `INSERT`/
14//! `DO NOTHING`, and the wrong pairing does not compile.
15//!
16//! ```
17//! use keelson_psql as psql;
18//! use keelson_psql::{Chain, arg, merge, quote};
19//!
20//! let q = psql::merge((
21//! merge::into(quote("tags")).as_("t"),
22//! merge::using(quote("posts")).as_("p"),
23//! merge::on(quote(("t", "id")).eq(quote(("p", "id")))),
24//! merge::when_matched().then_update(merge::set_col("name").to(quote(("p", "title")))),
25//! merge::when_not_matched()
26//! .then_insert()
27//! .columns(["id", "name"])
28//! .values((quote(("p", "id")), quote(("p", "title")))),
29//! ));
30//! ```
31//!
32//! Three spellings are PostgreSQL 17+, and each says so where it stands:
33//! [`when_not_matched_by_source`], [`NotMatchedChain::by_target`], and
34//! [`returning`]. `recursive` is deliberately not re-exported — PostgreSQL
35//! rejects `WITH RECURSIVE` on a `MERGE`.
36
37use keelson_core::clause::Set;
38use keelson_core::expr::{Expr, IntoExpr, IntoExprList};
39use keelson_core::{Mod, mod_fn};
40
41use crate::extras::Overriding;
42use crate::statement::{MergeAction, MergeInsert, MergeMatchKind, MergeQuery, MergeWhen};
43
44pub use crate::shared::{
45 cross_join, from_item as using, full_join, inner_join, left_join, returning, right_join, set,
46 set_col, target_table as into, with,
47};
48
49/// The `ON` join condition between target and source. Several calls are
50/// `AND`-joined, as a join's `ON` conditions are.
51pub fn on(condition: impl IntoExpr) -> impl Mod<MergeQuery> {
52 let condition = condition.into_expr();
53 mod_fn(move |q: &mut MergeQuery| q.on.push(condition))
54}
55
56/// `WHEN MATCHED …` — the arm taken when the source row found a target row.
57///
58/// Not a mod until an action is chosen, because `WHEN MATCHED` with no `THEN`
59/// is not a clause: call [`then_update`](MatchedChain::then_update),
60/// [`then_delete`](MatchedChain::then_delete) or
61/// [`then_do_nothing`](MatchedChain::then_do_nothing).
62pub fn when_matched() -> MatchedChain {
63 MatchedChain {
64 kind: MergeMatchKind::Matched,
65 condition: Vec::new(),
66 }
67}
68
69/// `WHEN NOT MATCHED BY SOURCE …` (PostgreSQL 17+) — the arm taken when a
70/// *target* row has no source row. It acts on that target row, so its actions
71/// are the matched ones: `UPDATE`, `DELETE`, `DO NOTHING`.
72pub fn when_not_matched_by_source() -> MatchedChain {
73 MatchedChain {
74 kind: MergeMatchKind::NotMatchedBySource,
75 condition: Vec::new(),
76 }
77}
78
79/// `WHEN NOT MATCHED …` — the arm taken when the source row found no target
80/// row. There is nothing to update or delete, so its actions are
81/// [`then_insert`](NotMatchedChain::then_insert) and
82/// [`then_do_nothing`](NotMatchedChain::then_do_nothing).
83pub fn when_not_matched() -> NotMatchedChain {
84 NotMatchedChain {
85 by_target: false,
86 condition: Vec::new(),
87 }
88}
89
90/// A `WHEN MATCHED` / `WHEN NOT MATCHED BY SOURCE` clause under construction —
91/// the arms that act on an existing target row.
92#[derive(Debug, Clone)]
93pub struct MatchedChain {
94 kind: MergeMatchKind,
95 condition: Vec<Expr>,
96}
97
98impl MatchedChain {
99 /// `AND condition` — refine when this arm applies. Several calls are
100 /// `AND`-joined.
101 #[must_use]
102 pub fn and(mut self, condition: impl IntoExpr) -> MatchedChain {
103 self.condition.push(condition.into_expr());
104 self
105 }
106
107 /// `THEN UPDATE SET …` — the body is built from [`set`]/[`set_col`] mods,
108 /// exactly as an `UPDATE`'s or an upsert's assignment list is.
109 pub fn then_update(self, body: impl Mod<Set>) -> MergeWhenMod {
110 let mut set = Set::default();
111 body.apply(&mut set);
112 self.finish(MergeAction::Update(set))
113 }
114
115 /// `THEN DELETE`.
116 pub fn then_delete(self) -> MergeWhenMod {
117 self.finish(MergeAction::Delete)
118 }
119
120 /// `THEN DO NOTHING` — take this arm and do nothing, which is different
121 /// from not having the arm: a row it captures is consumed by it.
122 pub fn then_do_nothing(self) -> MergeWhenMod {
123 self.finish(MergeAction::DoNothing)
124 }
125
126 fn finish(self, action: MergeAction) -> MergeWhenMod {
127 MergeWhenMod {
128 when: MergeWhen {
129 kind: self.kind,
130 condition: self.condition,
131 action,
132 },
133 }
134 }
135}
136
137/// A `WHEN NOT MATCHED [BY TARGET]` clause under construction — the arm with no
138/// target row, whose only actions are `INSERT` and `DO NOTHING`.
139#[derive(Debug, Clone)]
140pub struct NotMatchedChain {
141 by_target: bool,
142 condition: Vec<Expr>,
143}
144
145impl NotMatchedChain {
146 /// Spell out `BY TARGET` (PostgreSQL 17+). Same meaning as leaving it off;
147 /// see `docs/sql-rendering.md` on optional spellings being written only on
148 /// request.
149 #[must_use]
150 pub fn by_target(mut self) -> NotMatchedChain {
151 self.by_target = true;
152 self
153 }
154
155 /// `AND condition` — refine when this arm applies. Several calls are
156 /// `AND`-joined.
157 #[must_use]
158 pub fn and(mut self, condition: impl IntoExpr) -> NotMatchedChain {
159 self.condition.push(condition.into_expr());
160 self
161 }
162
163 /// `THEN INSERT …` — already a complete mod meaning `INSERT DEFAULT
164 /// VALUES`; [`columns`](MergeInsertChain::columns) and
165 /// [`values`](MergeInsertChain::values) fill in the fuller forms.
166 pub fn then_insert(self) -> MergeInsertChain {
167 MergeInsertChain {
168 when: self,
169 insert: MergeInsert::default(),
170 }
171 }
172
173 /// `THEN DO NOTHING`.
174 pub fn then_do_nothing(self) -> MergeWhenMod {
175 MergeWhenMod {
176 when: MergeWhen {
177 kind: MergeMatchKind::NotMatched {
178 by_target: self.by_target,
179 },
180 condition: self.condition,
181 action: MergeAction::DoNothing,
182 },
183 }
184 }
185}
186
187/// A `THEN INSERT` under construction. The `merge_insert` production takes one
188/// row — not the multi-row list an `INSERT` statement does — and with no row it
189/// is `INSERT DEFAULT VALUES`.
190#[derive(Debug, Clone)]
191pub struct MergeInsertChain {
192 when: NotMatchedChain,
193 insert: MergeInsert,
194}
195
196impl MergeInsertChain {
197 /// The insert column list: `INSERT ("id", "name")`.
198 #[must_use]
199 pub fn columns(
200 mut self,
201 columns: impl IntoIterator<Item = impl Into<std::borrow::Cow<'static, str>>>,
202 ) -> MergeInsertChain {
203 self.insert.columns = columns.into_iter().map(Into::into).collect();
204 self
205 }
206
207 /// `OVERRIDING SYSTEM VALUE` — as on an `INSERT` statement.
208 #[must_use]
209 pub fn overriding_system(mut self) -> MergeInsertChain {
210 self.insert.overriding = Some(Overriding::System);
211 self
212 }
213
214 /// `OVERRIDING USER VALUE` — as on an `INSERT` statement.
215 #[must_use]
216 pub fn overriding_user(mut self) -> MergeInsertChain {
217 self.insert.overriding = Some(Overriding::User);
218 self
219 }
220
221 /// The row: `VALUES ($1, $2)`. A cell may be `DEFAULT`, which is
222 /// [`raw("DEFAULT")`](crate::raw). Replaces any previously set row — the
223 /// production takes exactly one.
224 #[must_use]
225 pub fn values(mut self, row: impl IntoExprList) -> MergeInsertChain {
226 self.insert.row = row.into_expr_list();
227 self
228 }
229}
230
231impl Mod<MergeQuery> for MergeInsertChain {
232 fn apply(self, q: &mut MergeQuery) {
233 q.whens.push(MergeWhen {
234 kind: MergeMatchKind::NotMatched {
235 by_target: self.when.by_target,
236 },
237 condition: self.when.condition,
238 action: MergeAction::Insert(self.insert),
239 });
240 }
241}
242
243/// A finished `WHEN … THEN …` clause, ready to apply.
244#[derive(Debug, Clone)]
245pub struct MergeWhenMod {
246 when: MergeWhen,
247}
248
249impl Mod<MergeQuery> for MergeWhenMod {
250 fn apply(self, q: &mut MergeQuery) {
251 q.whens.push(self.when);
252 }
253}