Skip to main content

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}