keelson_psql/statement/merge.rs
1use std::borrow::Cow;
2
3use keelson_core::clause::{
4 HasJoins, HasReturning, HasTableRef, HasWith, Join, Returning, Set, TableRef, With,
5};
6use keelson_core::expr::{Expr, IntoExpr, IntoExprList};
7use keelson_core::{Dialect, Error, Expression, Mod, Query, QueryExtensions, QueryType, SqlWriter};
8
9use super::HasTargetTable;
10use crate::Psql;
11use crate::extras::Overriding;
12
13/// A PostgreSQL `MERGE` (PostgreSQL 15+).
14///
15/// From <https://www.postgresql.org/docs/17/sql-merge.html>:
16///
17/// ```text
18/// [ WITH with_query [, ...] ]
19/// MERGE INTO [ ONLY ] target_table_name [ * ] [ [ AS ] target_alias ]
20/// USING data_source ON join_condition
21/// when_clause [...]
22/// [ RETURNING … ]
23///
24/// when_clause:
25/// WHEN MATCHED [ AND condition ] THEN { merge_update | merge_delete | DO NOTHING }
26/// | WHEN NOT MATCHED BY SOURCE [ AND condition ] THEN
27/// { merge_update | merge_delete | DO NOTHING }
28/// | WHEN NOT MATCHED [ BY TARGET ] [ AND condition ] THEN
29/// { merge_insert | DO NOTHING }
30/// ```
31///
32/// Three parts of the production are newer than 15 and are marked where the API
33/// produces them: `WHEN NOT MATCHED BY SOURCE`, the explicit `BY TARGET`
34/// spelling, and `RETURNING` are all PostgreSQL 17+.
35///
36/// The target lives in [`HasTargetTable`] — like an `UPDATE`'s table — and the
37/// `USING` source in [`HasTableRef`], like a `DELETE`'s `USING` item, which is
38/// what lets one [`TableChain`](crate::shared::TableChain) serve both slots.
39/// The source also carries [`HasJoins`]: gram.y's `MergeStmt` reads
40/// `USING table_ref ON a_expr`, and a `table_ref` may be a `joined_table`, so a
41/// joined source is grammatical. The target is a `relation_expr_opt_alias`,
42/// which admits no joins — the same split an `UPDATE` has.
43///
44/// Which actions a `WHEN` clause may take depends on which `WHEN` it is, and that
45/// is enforced by the chain types in [`mod@crate::merge`] rather than re-checked
46/// here: a [`MergeWhen`] holds whatever it was built with.
47#[derive(Debug, Clone, Default)]
48pub struct MergeQuery {
49 /// `WITH …`. `MERGE` takes a plain `WITH`; PostgreSQL rejects
50 /// `WITH RECURSIVE` on it at analysis time, which is why
51 /// [`mod@crate::merge`] does not re-export `recursive`.
52 pub with: With,
53 /// The target: `MERGE INTO [ ONLY ] table [ * ] [ AS alias ]`.
54 pub target: TableRef,
55 /// The data source: a table or a parenthesised query, with an alias.
56 pub source: TableRef,
57 /// The `ON` join condition. Several entries are `AND`-joined, as in a join's
58 /// `ON`.
59 pub on: Vec<Expr>,
60 /// The `WHEN` clauses, applied in order — the grammar requires at least one.
61 pub whens: Vec<MergeWhen>,
62 /// `RETURNING …` (PostgreSQL 17+).
63 pub returning: Returning,
64}
65
66impl MergeQuery {
67 /// A `MERGE` with nothing set yet.
68 pub fn new() -> MergeQuery {
69 MergeQuery::default()
70 }
71
72 /// Apply more mods to an existing query.
73 pub fn apply(&mut self, mods: impl Mod<MergeQuery>) {
74 mods.apply(self);
75 }
76}
77
78impl Expression for MergeQuery {
79 fn write_sql(&self, w: &mut SqlWriter<'_>) {
80 w.write_if(!self.with.is_empty(), "", &self.with, " ");
81
82 // Every one of these is grammatically required — sql-merge.html has no
83 // brackets around USING, ON or the when-clause list — so an absent one
84 // is a recorded failure, never a shorter statement.
85 if self.target.is_empty() {
86 w.record_error(Error::Incomplete("the target table of a MERGE"));
87 return;
88 }
89 if self.source.is_empty() {
90 w.record_error(Error::Incomplete("the USING source of a MERGE"));
91 return;
92 }
93 if self.on.is_empty() {
94 w.record_error(Error::Incomplete("the ON condition of a MERGE"));
95 return;
96 }
97 if self.whens.is_empty() {
98 w.record_error(Error::Incomplete("the WHEN clauses of a MERGE"));
99 return;
100 }
101
102 w.push_str("MERGE INTO ");
103 w.write_expr(&self.target);
104 w.push_str(" USING ");
105 w.write_expr(&self.source);
106 w.write_slice(&self.on, " ON ", " AND ", "");
107 w.write_slice(&self.whens, " ", " ", "");
108 w.write_if(!self.returning.is_empty(), " ", &self.returning, "");
109 }
110}
111
112impl Query for MergeQuery {
113 fn query_type(&self) -> QueryType {
114 QueryType::Merge
115 }
116
117 fn dialect(&self) -> &dyn Dialect {
118 &Psql
119 }
120}
121
122impl<H, L, M> QueryExtensions<H, L, M> for MergeQuery {}
123
124impl IntoExpr for MergeQuery {
125 fn into_expr(self) -> Expr {
126 crate::query(self)
127 }
128}
129
130impl IntoExprList for MergeQuery {
131 fn into_expr_list(self) -> Vec<Expr> {
132 vec![self.into_expr()]
133 }
134}
135
136impl HasWith for MergeQuery {
137 fn with_mut(&mut self) -> &mut With {
138 &mut self.with
139 }
140}
141
142impl HasTargetTable for MergeQuery {
143 fn target_table_mut(&mut self) -> &mut TableRef {
144 &mut self.target
145 }
146}
147
148impl HasTableRef for MergeQuery {
149 fn table_ref_mut(&mut self) -> &mut TableRef {
150 &mut self.source
151 }
152}
153
154impl HasJoins for MergeQuery {
155 fn joins_mut(&mut self) -> &mut Vec<Join> {
156 // The joins belong to the USING source — the one slot of a MERGE that
157 // is a full `table_ref` in gram.y. They cannot be dropped silently: an
158 // absent source is already recorded as Incomplete before rendering
159 // reaches the point where its joins would have been written.
160 &mut self.source.joins
161 }
162}
163
164impl HasReturning for MergeQuery {
165 fn returning_mut(&mut self) -> &mut Returning {
166 &mut self.returning
167 }
168}
169
170/// One `WHEN … THEN …` clause of a [`MergeQuery`].
171#[derive(Debug, Clone)]
172pub struct MergeWhen {
173 /// Which of the three `WHEN` forms this is.
174 pub kind: MergeMatchKind,
175 /// The `AND condition` refinement. Several entries are `AND`-joined.
176 pub condition: Vec<Expr>,
177 /// What `THEN` does.
178 pub action: MergeAction,
179}
180
181impl Expression for MergeWhen {
182 fn write_sql(&self, w: &mut SqlWriter<'_>) {
183 w.push_str(self.kind.as_str());
184 w.write_slice(&self.condition, " AND ", " AND ", "");
185 w.push_str(" THEN ");
186 match &self.action {
187 MergeAction::Update(set) => {
188 if set.is_empty() {
189 // `UPDATE SET` with nothing after it is not a merge_update,
190 // and the keywords are already half-written by the time an
191 // empty list would render — so this is recorded, as every
192 // unfillable clause is.
193 w.record_error(Error::Incomplete("the assignments of a MERGE UPDATE"));
194 return;
195 }
196 w.push_str("UPDATE SET ");
197 w.write_expr(set);
198 }
199 MergeAction::Delete => w.push_str("DELETE"),
200 MergeAction::DoNothing => w.push_str("DO NOTHING"),
201 MergeAction::Insert(insert) => w.write_expr(insert),
202 }
203 }
204}
205
206/// Which `WHEN` form a [`MergeWhen`] is.
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum MergeMatchKind {
209 /// `WHEN MATCHED` — the source row found a target row.
210 Matched,
211 /// `WHEN NOT MATCHED [ BY TARGET ]` — the source row found no target row.
212 ///
213 /// `BY TARGET` spells out the default; it exists (PostgreSQL 17+) to read
214 /// well next to `BY SOURCE`, and is written only when asked for —
215 /// see the `OFFSET`/`FETCH` entry in `docs/sql-rendering.md` for the rule.
216 NotMatched {
217 /// Whether to write the explicit `BY TARGET` (PostgreSQL 17+).
218 by_target: bool,
219 },
220 /// `WHEN NOT MATCHED BY SOURCE` — the target row has no source row
221 /// (PostgreSQL 17+).
222 NotMatchedBySource,
223}
224
225impl MergeMatchKind {
226 /// The clause head, as written.
227 pub fn as_str(self) -> &'static str {
228 match self {
229 MergeMatchKind::Matched => "WHEN MATCHED",
230 MergeMatchKind::NotMatched { by_target: false } => "WHEN NOT MATCHED",
231 MergeMatchKind::NotMatched { by_target: true } => "WHEN NOT MATCHED BY TARGET",
232 MergeMatchKind::NotMatchedBySource => "WHEN NOT MATCHED BY SOURCE",
233 }
234 }
235}
236
237/// What a [`MergeWhen`]'s `THEN` does.
238#[derive(Debug, Clone)]
239pub enum MergeAction {
240 /// `UPDATE SET …` — a matched arm. The `Set` is the same assignment list an
241 /// `UPDATE` carries, keyword supplied here.
242 Update(Set),
243 /// `DELETE` — a matched arm.
244 Delete,
245 /// `DO NOTHING` — any arm.
246 DoNothing,
247 /// `INSERT …` — a not-matched arm.
248 Insert(MergeInsert),
249}
250
251/// The `merge_insert` production:
252///
253/// ```text
254/// INSERT [( column_name [, ...] )]
255/// [ OVERRIDING { SYSTEM | USER } VALUE ]
256/// { VALUES ( { expression | DEFAULT } [, ...] ) | DEFAULT VALUES }
257/// ```
258///
259/// One row only — unlike an `INSERT` statement's `VALUES` list — and no source
260/// query, which is why this is its own shape rather than a reuse of
261/// [`Values`](keelson_core::clause::Values). An empty row is `DEFAULT VALUES`,
262/// the same reading [`InsertQuery`](super::InsertQuery) gives an empty row
263/// source.
264#[derive(Debug, Clone, Default)]
265pub struct MergeInsert {
266 /// The insert column list. Quoted.
267 pub columns: Vec<Cow<'static, str>>,
268 /// `OVERRIDING … VALUE`.
269 pub overriding: Option<Overriding>,
270 /// The single row's cells. Empty means `DEFAULT VALUES`.
271 pub row: Vec<Expr>,
272}
273
274impl Expression for MergeInsert {
275 fn write_sql(&self, w: &mut SqlWriter<'_>) {
276 w.push_str("INSERT");
277 if !self.columns.is_empty() {
278 w.push_str(" (");
279 for (i, column) in self.columns.iter().enumerate() {
280 if i > 0 {
281 w.push_str(", ");
282 }
283 w.push_quoted(&[column]);
284 }
285 w.push_str(")");
286 }
287 if let Some(overriding) = &self.overriding {
288 w.push_str(" OVERRIDING ");
289 w.push_str(overriding.as_str());
290 w.push_str(" VALUE");
291 }
292 if self.row.is_empty() {
293 w.push_str(" DEFAULT VALUES");
294 } else {
295 w.write_slice(&self.row, " VALUES (", ", ", ")");
296 }
297 }
298}