Skip to main content

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}