Skip to main content

keelson_mysql/
shared.rs

1//! The mods, written once against the `Has*` traits and re-exported per statement.
2//!
3//! Nothing in here names a query type. A mod is `Mod<Q>` for every `Q` that
4//! implements the clause trait it needs, so `where_` is one function serving
5//! `SELECT`, `UPDATE` and `DELETE` — and refusing to compile against an `INSERT`,
6//! which has no `WHERE`.
7//!
8//! Three shapes recur.
9//!
10//! **A plain mod** is a function returning `impl Mod<Q>`, built from
11//! [`keelson_core::mod_fn`].
12//!
13//! **A chain** is a struct that is itself a mod and has builder methods. It exists
14//! wherever a clause has decorations that must be set together rather than one mod
15//! at a time: `from(..).as_("u").use_index(["PRIMARY"])` replaces the whole
16//! from-item once, so no later mod can silently wipe an earlier one.
17//!
18//! **A slot** is how one chain type reaches different fields of different queries.
19//! `select::from` and `delete::using` are the same chain with a different
20//! [`TableSlot`]; the marker is a type parameter, so which field is written is
21//! decided at compile time and there is one implementation of the builder methods.
22
23use std::borrow::Cow;
24use std::marker::PhantomData;
25
26use keelson_core::clause::{
27    Combine, Cte, GroupByWith, HasCombines, HasGroupBy, HasHaving, HasJoins, HasLimit, HasLocks,
28    HasOffset, HasOrderBy, HasSelectList, HasSet, HasTableRef, HasValues, HasWhere, HasWindows,
29    HasWith, IndexHint, IndexHintKind, IndexHintScope, Join, JoinKind, Lock, LockStrength,
30    LockWait, NamedWindow, OrderBy, OrderDef, OrderDirection, Set, SetOp, TableRef, Values, Window,
31};
32use keelson_core::expr::{Expr, IntoExpr, IntoExprList, IntoIdent};
33use keelson_core::{Expression, Mod, SqlWriter, mod_fn};
34
35use crate::extras::{
36    HasDuplicateKeyUpdate, HasHints, HasModifiers, HasRowAlias, Modifier, RowAlias, row_value,
37    values_of,
38};
39use crate::statement::{HasDeleteTables, HasExtraTables, HasTargetTable};
40
41// ---------------------------------------------------------------------------
42// WITH
43// ---------------------------------------------------------------------------
44
45/// A common table expression under construction.
46///
47/// `with("recent", body)` is already a complete mod; the one method adds the only
48/// optional part MySQL's `with_query` has.
49///
50/// MySQL's production is
51/// `cte_name [(col_name [, col_name] ...)] AS (subquery)` (*15.2.20*) — there is no
52/// `MATERIALIZED`, no `SEARCH` and no `CYCLE`, so this chain has none of the
53/// methods PostgreSQL's does.
54#[derive(Debug, Clone)]
55pub struct CteChain {
56    cte: Cte,
57}
58
59/// `WITH \`name\` AS (body)`.
60///
61/// `body` is any expression, so a hand-written fragment works and a query goes in
62/// directly, because the query types implement
63/// [`keelson_core::expr::IntoExpr`]. It is *not* parenthesised here —
64/// [`Cte`] supplies the parentheses.
65pub fn with(name: impl Into<Cow<'static, str>>, body: impl IntoExpr) -> CteChain {
66    CteChain {
67        cte: Cte::new(name, body),
68    }
69}
70
71impl CteChain {
72    /// Name the CTE's output columns: ``WITH `c` (`a`, `b`) AS (…)``.
73    #[must_use]
74    pub fn columns(
75        mut self,
76        columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
77    ) -> CteChain {
78        self.cte.columns = columns.into_iter().map(Into::into).collect();
79        self
80    }
81}
82
83impl<Q: HasWith> Mod<Q> for CteChain {
84    fn apply(self, q: &mut Q) {
85        q.with_mut().append_cte(self.cte);
86    }
87}
88
89/// `WITH RECURSIVE` rather than `WITH`.
90///
91/// A property of the whole list, not of one entry. MySQL requires it whenever any
92/// CTE in the list refers to itself.
93pub fn recursive<Q: HasWith>(recursive: bool) -> impl Mod<Q> {
94    mod_fn(move |q: &mut Q| q.with_mut().set_recursive(recursive))
95}
96
97// ---------------------------------------------------------------------------
98// Optimizer hints
99// ---------------------------------------------------------------------------
100
101/// One optimizer hint, written verbatim inside the `/*+ … */` comment:
102/// `optimizer_hint("BKA(users, posts)")`.
103///
104/// The hint language has its own grammar with some forty names, several of which
105/// take a query-block-qualified table list. Modelling it would be a second dialect
106/// inside this one, so the general form is this and only the fixed-shape hints get
107/// their own mods below.
108pub fn optimizer_hint<Q: HasHints>(hint: impl Into<Cow<'static, str>>) -> impl Mod<Q> {
109    let hint = hint.into();
110    mod_fn(move |q: &mut Q| q.hints_mut().append_hint(hint))
111}
112
113/// `MAX_EXECUTION_TIME(n)` — give up after `n` milliseconds. `SELECT` only, and
114/// MySQL ignores it on anything else.
115pub fn max_execution_time<Q: HasHints>(millis: u64) -> impl Mod<Q> {
116    optimizer_hint(format!("MAX_EXECUTION_TIME({millis})"))
117}
118
119/// `SET_VAR(name = value)` — change a system variable for this statement alone.
120pub fn set_var<Q: HasHints>(assignment: impl Into<Cow<'static, str>>) -> impl Mod<Q> {
121    optimizer_hint(format!("SET_VAR({})", assignment.into()))
122}
123
124/// `QB_NAME(name)` — name this query block so a later hint can qualify a table
125/// with it.
126pub fn qb_name<Q: HasHints>(name: impl Into<Cow<'static, str>>) -> impl Mod<Q> {
127    optimizer_hint(format!("QB_NAME({})", name.into()))
128}
129
130/// `RESOURCE_GROUP(name)` — run the statement in a named resource group.
131pub fn resource_group<Q: HasHints>(name: impl Into<Cow<'static, str>>) -> impl Mod<Q> {
132    optimizer_hint(format!("RESOURCE_GROUP({})", name.into()))
133}
134
135// ---------------------------------------------------------------------------
136// Statement modifiers
137// ---------------------------------------------------------------------------
138
139fn modifier<Q: HasModifiers>(modifier: Modifier) -> impl Mod<Q> {
140    mod_fn(move |q: &mut Q| q.modifiers_mut().append_modifier(modifier))
141}
142
143/// `DISTINCT` — drop duplicate result rows. There is no `DISTINCT ON`; that is
144/// PostgreSQL's.
145pub fn distinct<Q: HasModifiers>() -> impl Mod<Q> {
146    modifier(Modifier::Distinct)
147}
148
149/// `DISTINCTROW`, MySQL's synonym for `DISTINCT`.
150pub fn distinct_row<Q: HasModifiers>() -> impl Mod<Q> {
151    modifier(Modifier::DistinctRow)
152}
153
154/// `LOW_PRIORITY` — wait until no client is reading the table.
155pub fn low_priority<Q: HasModifiers>() -> impl Mod<Q> {
156    modifier(Modifier::LowPriority)
157}
158
159/// `HIGH_PRIORITY` — jump ahead of pending writes.
160pub fn high_priority<Q: HasModifiers>() -> impl Mod<Q> {
161    modifier(Modifier::HighPriority)
162}
163
164/// `DELAYED` — accepted for backward compatibility; MySQL 8 treats it as an
165/// ordinary `INSERT` and raises a warning.
166pub fn delayed<Q: HasModifiers>() -> impl Mod<Q> {
167    modifier(Modifier::Delayed)
168}
169
170/// `QUICK` — skip merging index leaves while deleting.
171pub fn quick<Q: HasModifiers>() -> impl Mod<Q> {
172    modifier(Modifier::Quick)
173}
174
175/// `IGNORE` — downgrade the errors that would abort the statement to warnings.
176pub fn ignore<Q: HasModifiers>() -> impl Mod<Q> {
177    modifier(Modifier::Ignore)
178}
179
180/// The `STRAIGHT_JOIN` *modifier*: join every table in the order written.
181///
182/// Not the same thing as [`straight_join`], which is a join operator applying to
183/// one pair of tables.
184pub fn straight<Q: HasModifiers>() -> impl Mod<Q> {
185    modifier(Modifier::StraightJoin)
186}
187
188/// `SQL_SMALL_RESULT` — the result is small; use an in-memory temporary table.
189pub fn sql_small_result<Q: HasModifiers>() -> impl Mod<Q> {
190    modifier(Modifier::SmallResult)
191}
192
193/// `SQL_BIG_RESULT` — the result is large; sort rather than build an index.
194pub fn sql_big_result<Q: HasModifiers>() -> impl Mod<Q> {
195    modifier(Modifier::BigResult)
196}
197
198/// `SQL_BUFFER_RESULT` — force the result into a temporary table, releasing table
199/// locks sooner.
200pub fn sql_buffer_result<Q: HasModifiers>() -> impl Mod<Q> {
201    modifier(Modifier::BufferResult)
202}
203
204/// `SQL_NO_CACHE` — do not touch the query cache.
205pub fn sql_no_cache<Q: HasModifiers>() -> impl Mod<Q> {
206    modifier(Modifier::NoCache)
207}
208
209/// `SQL_CALC_FOUND_ROWS` — count the rows a `LIMIT` discarded, for `FOUND_ROWS()`.
210pub fn sql_calc_found_rows<Q: HasModifiers>() -> impl Mod<Q> {
211    modifier(Modifier::CalcFoundRows)
212}
213
214// ---------------------------------------------------------------------------
215// The projection
216// ---------------------------------------------------------------------------
217
218/// Add to the select list. Several calls accumulate; with none, `*` is written.
219pub fn columns<Q: HasSelectList>(columns: impl IntoExprList) -> impl Mod<Q> {
220    let columns = columns.into_expr_list();
221    mod_fn(move |q: &mut Q| q.select_list_mut().append_select(columns))
222}
223
224/// Add to the *preload* select list, which renders after [`columns`] but is counted
225/// separately, so a relation loader can tell which of the returned columns belong
226/// to the root object.
227pub fn preload_columns<Q: HasSelectList>(columns: impl IntoExprList) -> impl Mod<Q> {
228    let columns = columns.into_expr_list();
229    mod_fn(move |q: &mut Q| q.select_list_mut().append_preload_select(columns))
230}
231
232// ---------------------------------------------------------------------------
233// Table references
234// ---------------------------------------------------------------------------
235
236/// Where a [`TableChain`] puts the table reference it has built.
237///
238/// Implemented by the markers below rather than by a query, so the builder methods
239/// are written once and `select::from` / `update::table` / `delete::from` differ
240/// only in a type parameter.
241pub trait TableSlot<Q> {
242    /// Put `table` where this slot means.
243    fn place(q: &mut Q, table: TableRef);
244}
245
246/// The from-item slot: a `SELECT`'s `FROM`, an `INSERT`'s or `REPLACE`'s target, a
247/// `DELETE`'s `USING`.
248#[derive(Debug, Clone, Copy, Default)]
249pub struct FromSlot;
250
251/// The target slot: the `table_references` an `UPDATE` writes to.
252#[derive(Debug, Clone, Copy, Default)]
253pub struct TargetSlot;
254
255/// The additional-table slot, for the second and later entries of a
256/// comma-separated table list.
257#[derive(Debug, Clone, Copy, Default)]
258pub struct ExtraSlot;
259
260/// The `DELETE FROM` list, which also collects the statement's partitions.
261#[derive(Debug, Clone, Copy, Default)]
262pub struct DeleteSlot;
263
264impl<Q: HasTableRef> TableSlot<Q> for FromSlot {
265    fn place(q: &mut Q, mut table: TableRef) {
266        // Joins already appended to the slot survive, so `from(..)` written after
267        // `inner_join(..)` is not a way to silently lose them.
268        table.joins.append(&mut q.table_ref_mut().joins);
269        *q.table_ref_mut() = table;
270    }
271}
272
273impl<Q: HasTargetTable> TableSlot<Q> for TargetSlot {
274    fn place(q: &mut Q, mut table: TableRef) {
275        table.joins.append(&mut q.target_table_mut().joins);
276        *q.target_table_mut() = table;
277    }
278}
279
280impl<Q: HasExtraTables> TableSlot<Q> for ExtraSlot {
281    fn place(q: &mut Q, table: TableRef) {
282        q.extra_tables_mut().push(table);
283    }
284}
285
286impl<Q: HasDeleteTables> TableSlot<Q> for DeleteSlot {
287    fn place(q: &mut Q, mut table: TableRef) {
288        // `DELETE` writes PARTITION after the alias and once for the whole
289        // statement, so the chain's list is moved out of the table reference. See
290        // `HasDeleteTables`.
291        let partitions = std::mem::take(&mut table.partitions);
292        q.delete_partitions_mut().extend(partitions);
293        q.delete_tables_mut().push(table);
294    }
295}
296
297/// A table reference under construction: the table plus every decoration MySQL's
298/// `table_factor` allows.
299///
300/// ```text
301/// tbl_name [PARTITION (partition_names)] [[AS] alias] [index_hint_list]
302/// [LATERAL] table_subquery [AS] alias [(col_list)]
303/// ```
304#[derive(Debug, Clone)]
305pub struct TableChain<S> {
306    table: TableRef,
307    slot: PhantomData<S>,
308}
309
310fn table_chain<S>(table: impl IntoExpr) -> TableChain<S> {
311    TableChain {
312        table: TableRef::new(table),
313        slot: PhantomData,
314    }
315}
316
317/// A from-item: `FROM <table>`.
318pub fn from_item(table: impl IntoExpr) -> TableChain<FromSlot> {
319    table_chain(table)
320}
321
322/// A further comma-separated table reference. A comma there means `CROSS JOIN`.
323pub fn extra_from_item(table: impl IntoExpr) -> TableChain<ExtraSlot> {
324    table_chain(table)
325}
326
327/// The `table_references` an `UPDATE` writes to.
328pub fn target_table(table: impl IntoExpr) -> TableChain<TargetSlot> {
329    table_chain(table)
330}
331
332/// A table a `DELETE` removes rows from. Several calls give
333/// `DELETE FROM t1, t2 …`.
334pub fn delete_table(table: impl IntoExpr) -> TableChain<DeleteSlot> {
335    table_chain(table)
336}
337
338impl<S> TableChain<S> {
339    /// `AS \`alias\``.
340    #[must_use]
341    pub fn as_(mut self, alias: impl Into<Cow<'static, str>>) -> TableChain<S> {
342        self.table.set_alias(alias);
343        self
344    }
345
346    /// Column aliases: `AS \`t\` (\`a\`, \`b\`)`, which MySQL 8.0.19 allows on a
347    /// derived table. For an `INSERT` or `REPLACE` this is the column list instead.
348    #[must_use]
349    pub fn columns(
350        mut self,
351        columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
352    ) -> TableChain<S> {
353        self.table.set_columns(columns);
354        self
355    }
356
357    /// `LATERAL` — let a derived table refer to columns of the items before it
358    /// (MySQL 8.0.14).
359    ///
360    /// Only grammatical in front of a derived table; on a bare table or CTE
361    /// name this records a `build()` error instead, because
362    /// ``FROM LATERAL `posts` `` is a syntax error with nothing to mean.
363    #[must_use]
364    pub fn lateral(mut self) -> TableChain<S> {
365        self.table = lateral_table(self.table);
366        self
367    }
368
369    /// `PARTITION (\`p0\`, \`p1\`)` — read only these partitions.
370    #[must_use]
371    pub fn partition(
372        mut self,
373        partitions: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
374    ) -> TableChain<S> {
375        self.table.append_partition(partitions);
376        self
377    }
378
379    /// `USE INDEX (…)` — consider only these indexes. An empty list is meaningful:
380    /// `USE INDEX ()` tells MySQL to use none.
381    #[must_use]
382    pub fn use_index(
383        self,
384        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
385    ) -> TableChain<S> {
386        self.index_hint(IndexHintKind::Use, indexes)
387    }
388
389    /// `IGNORE INDEX (…)` — do not consider these indexes.
390    #[must_use]
391    pub fn ignore_index(
392        self,
393        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
394    ) -> TableChain<S> {
395        self.index_hint(IndexHintKind::Ignore, indexes)
396    }
397
398    /// `FORCE INDEX (…)` — a table scan is not acceptable.
399    #[must_use]
400    pub fn force_index(
401        self,
402        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
403    ) -> TableChain<S> {
404        self.index_hint(IndexHintKind::Force, indexes)
405    }
406
407    /// `FOR JOIN` on the hint just added.
408    ///
409    /// Ignored if no hint has been added: a scope with nothing to scope means
410    /// nothing, and `FOR JOIN` is not a clause of its own.
411    #[must_use]
412    pub fn for_join(self) -> TableChain<S> {
413        self.hint_scope(IndexHintScope::Join)
414    }
415
416    /// `FOR ORDER BY` on the hint just added.
417    #[must_use]
418    pub fn for_order_by(self) -> TableChain<S> {
419        self.hint_scope(IndexHintScope::OrderBy)
420    }
421
422    /// `FOR GROUP BY` on the hint just added.
423    #[must_use]
424    pub fn for_group_by(self) -> TableChain<S> {
425        self.hint_scope(IndexHintScope::GroupBy)
426    }
427
428    fn index_hint(
429        mut self,
430        kind: IndexHintKind,
431        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
432    ) -> TableChain<S> {
433        self.table.append_index_hint(IndexHint::new(kind, indexes));
434        self
435    }
436
437    fn hint_scope(mut self, scope: IndexHintScope) -> TableChain<S> {
438        if let Some(hint) = self.table.index_hints.last_mut() {
439            hint.for_ = Some(scope);
440        }
441        self
442    }
443}
444
445impl<Q, S: TableSlot<Q>> Mod<Q> for TableChain<S> {
446    fn apply(self, q: &mut Q) {
447        S::place(q, self.table);
448    }
449}
450
451/// Mark a table reference `LATERAL`, refusing the one item shape the grammar
452/// has no sentence for: a bare table or CTE name ([`Expr::Ident`]).
453///
454/// MySQL's `LATERAL` (8.0.14, *15.2.15.9 Lateral Derived Tables*) is
455/// grammatical only in front of a derived table — the manual's production is
456/// `LATERAL table_subquery [AS] alias` and nothing else takes the keyword, and
457/// there is nothing for it to mean on a name anyway (a base table cannot
458/// reference the items before it). The item is wrapped in [`LateralBareName`],
459/// which records the error `build()` surfaces — catching the mistake at the
460/// `.lateral()` call rather than letting valid-looking SQL leave with
461/// ``LATERAL `posts` `` in it.
462///
463/// Only [`Expr::Ident`] items are judged. A raw fragment could be anything —
464/// progressive enhancement means hand-written SQL is trusted — and derived
465/// tables arrive as other variants.
466fn lateral_table(mut table: TableRef) -> TableRef {
467    table.lateral = true;
468    if matches!(table.expression, Some(Expr::Ident(_))) {
469        let name = table.expression.take().expect("just matched Some");
470        table.expression = Some(Expr::custom(LateralBareName(name)));
471    }
472    table
473}
474
475/// A from-item that was marked `LATERAL` but is a bare table or CTE name.
476///
477/// The chain methods swap this in when `.lateral()` is called on such an item,
478/// so the mistake is caught where it is made; the item still renders, keeping
479/// the debug print honest, while `build()` refuses. The same judgment, for the
480/// same reason, as keelson-psql's `LateralBareName`.
481#[derive(Debug)]
482struct LateralBareName(Expr);
483
484impl Expression for LateralBareName {
485    fn write_sql(&self, w: &mut SqlWriter<'_>) {
486        w.record_error(keelson_core::Error::other(
487            "LATERAL is set on a bare table or CTE name, but LATERAL can precede only a derived table",
488        ));
489        w.write_expr(&self.0);
490    }
491}
492
493// ---------------------------------------------------------------------------
494// Joins
495// ---------------------------------------------------------------------------
496
497/// A join under construction.
498///
499/// `ON` and `USING` are alternatives and `NATURAL` excludes both; nothing enforces
500/// that here, because the caller picks one method and the grammar is what says so.
501#[derive(Debug, Clone)]
502pub struct JoinChain {
503    join: Join,
504}
505
506fn join_chain(kind: JoinKind, to: impl IntoExpr) -> JoinChain {
507    JoinChain {
508        join: Join::new(kind, TableRef::new(to)),
509    }
510}
511
512/// `INNER JOIN <table>`.
513pub fn inner_join(table: impl IntoExpr) -> JoinChain {
514    join_chain(JoinKind::Inner, table)
515}
516
517/// `LEFT JOIN <table>`. MySQL requires an `ON` or `USING` on this one.
518pub fn left_join(table: impl IntoExpr) -> JoinChain {
519    join_chain(JoinKind::Left, table)
520}
521
522/// `RIGHT JOIN <table>`. MySQL requires an `ON` or `USING` on this one.
523pub fn right_join(table: impl IntoExpr) -> JoinChain {
524    join_chain(JoinKind::Right, table)
525}
526
527/// `CROSS JOIN <table>`.
528///
529/// In MySQL `JOIN`, `CROSS JOIN` and `INNER JOIN` are syntactic equivalents, so
530/// unlike PostgreSQL this one *does* take an `ON` or `USING` — hence a
531/// [`PlainJoinChain`] rather than a stripped-down one. What it does not take is
532/// `NATURAL`, which the grammar allows only on `INNER`, `LEFT` and `RIGHT`.
533pub fn cross_join(table: impl IntoExpr) -> PlainJoinChain {
534    PlainJoinChain(join_chain(JoinKind::Cross, table))
535}
536
537/// `STRAIGHT_JOIN <table>` — an `INNER JOIN` that forbids the optimizer from
538/// reading the right table first.
539///
540/// The join operator, not the [`straight`] modifier.
541pub fn straight_join(table: impl IntoExpr) -> PlainJoinChain {
542    PlainJoinChain(join_chain(JoinKind::Custom("STRAIGHT_JOIN".into()), table))
543}
544
545impl JoinChain {
546    /// `AS \`alias\`` on the joined table.
547    #[must_use]
548    pub fn as_(mut self, alias: impl Into<Cow<'static, str>>) -> JoinChain {
549        self.join.to.set_alias(alias);
550        self
551    }
552
553    /// Column aliases on the joined derived table.
554    #[must_use]
555    pub fn columns(
556        mut self,
557        columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
558    ) -> JoinChain {
559        self.join.to.set_columns(columns);
560        self
561    }
562
563    /// `LATERAL` on the joined item — what lets a joined derived table see the
564    /// columns of the item it is joined to.
565    ///
566    /// Only grammatical in front of a derived table; on a bare table or CTE
567    /// name this records a `build()` error instead, because
568    /// ``JOIN LATERAL `posts` `` is a syntax error with nothing to mean.
569    #[must_use]
570    pub fn lateral(mut self) -> JoinChain {
571        self.join.to = lateral_table(self.join.to);
572        self
573    }
574
575    /// `PARTITION (…)` on the joined table.
576    #[must_use]
577    pub fn partition(
578        mut self,
579        partitions: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
580    ) -> JoinChain {
581        self.join.to.append_partition(partitions);
582        self
583    }
584
585    /// `USE INDEX (…)` on the joined table.
586    #[must_use]
587    pub fn use_index(
588        self,
589        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
590    ) -> JoinChain {
591        self.index_hint(IndexHintKind::Use, indexes)
592    }
593
594    /// `IGNORE INDEX (…)` on the joined table.
595    #[must_use]
596    pub fn ignore_index(
597        self,
598        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
599    ) -> JoinChain {
600        self.index_hint(IndexHintKind::Ignore, indexes)
601    }
602
603    /// `FORCE INDEX (…)` on the joined table.
604    #[must_use]
605    pub fn force_index(
606        self,
607        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
608    ) -> JoinChain {
609        self.index_hint(IndexHintKind::Force, indexes)
610    }
611
612    /// `FOR JOIN` on the hint just added.
613    #[must_use]
614    pub fn for_join(self) -> JoinChain {
615        self.hint_scope(IndexHintScope::Join)
616    }
617
618    /// `FOR ORDER BY` on the hint just added.
619    #[must_use]
620    pub fn for_order_by(self) -> JoinChain {
621        self.hint_scope(IndexHintScope::OrderBy)
622    }
623
624    /// `FOR GROUP BY` on the hint just added.
625    #[must_use]
626    pub fn for_group_by(self) -> JoinChain {
627        self.hint_scope(IndexHintScope::GroupBy)
628    }
629
630    /// `NATURAL <kind> JOIN` — derive the join columns from the two items' names.
631    #[must_use]
632    pub fn natural(mut self) -> JoinChain {
633        self.join.natural = true;
634        self
635    }
636
637    /// `ON condition`. Several conditions are `AND`-joined.
638    #[must_use]
639    pub fn on(mut self, condition: impl IntoExpr) -> JoinChain {
640        self.join.append_on(condition);
641        self
642    }
643
644    /// `ON (a = b)`, the overwhelmingly common shape.
645    #[must_use]
646    pub fn on_eq(self, a: impl IntoExpr, b: impl IntoExpr) -> JoinChain {
647        self.on(Expr::binary(a, "=", b).grouped())
648    }
649
650    /// `USING (\`a\`, \`b\`)` — join on equally named columns, merging them.
651    #[must_use]
652    pub fn using(
653        mut self,
654        columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
655    ) -> JoinChain {
656        self.join.append_using(columns);
657        self
658    }
659
660    fn index_hint(
661        mut self,
662        kind: IndexHintKind,
663        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
664    ) -> JoinChain {
665        self.join
666            .to
667            .append_index_hint(IndexHint::new(kind, indexes));
668        self
669    }
670
671    fn hint_scope(mut self, scope: IndexHintScope) -> JoinChain {
672        if let Some(hint) = self.join.to.index_hints.last_mut() {
673            hint.for_ = Some(scope);
674        }
675        self
676    }
677}
678
679impl From<JoinChain> for Join {
680    fn from(chain: JoinChain) -> Join {
681        chain.join
682    }
683}
684
685impl<Q: HasJoins> Mod<Q> for JoinChain {
686    fn apply(self, q: &mut Q) {
687        q.joins_mut().push(self.into());
688    }
689}
690
691/// A `CROSS JOIN` or `STRAIGHT_JOIN` under construction — [`JoinChain`] without
692/// [`natural`](JoinChain::natural), which MySQL's grammar allows only on `INNER`,
693/// `LEFT` and `RIGHT`.
694#[derive(Debug, Clone)]
695pub struct PlainJoinChain(JoinChain);
696
697impl PlainJoinChain {
698    /// `AS \`alias\`` on the joined table.
699    #[must_use]
700    pub fn as_(self, alias: impl Into<Cow<'static, str>>) -> PlainJoinChain {
701        PlainJoinChain(self.0.as_(alias))
702    }
703
704    /// Column aliases on the joined derived table.
705    #[must_use]
706    pub fn columns(
707        self,
708        columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
709    ) -> PlainJoinChain {
710        PlainJoinChain(self.0.columns(columns))
711    }
712
713    /// `LATERAL` on the joined table.
714    #[must_use]
715    pub fn lateral(self) -> PlainJoinChain {
716        PlainJoinChain(self.0.lateral())
717    }
718
719    /// `PARTITION (…)` on the joined table.
720    #[must_use]
721    pub fn partition(
722        self,
723        partitions: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
724    ) -> PlainJoinChain {
725        PlainJoinChain(self.0.partition(partitions))
726    }
727
728    /// `USE INDEX (…)` on the joined table.
729    #[must_use]
730    pub fn use_index(
731        self,
732        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
733    ) -> PlainJoinChain {
734        PlainJoinChain(self.0.use_index(indexes))
735    }
736
737    /// `IGNORE INDEX (…)` on the joined table.
738    #[must_use]
739    pub fn ignore_index(
740        self,
741        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
742    ) -> PlainJoinChain {
743        PlainJoinChain(self.0.ignore_index(indexes))
744    }
745
746    /// `FORCE INDEX (…)` on the joined table.
747    #[must_use]
748    pub fn force_index(
749        self,
750        indexes: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
751    ) -> PlainJoinChain {
752        PlainJoinChain(self.0.force_index(indexes))
753    }
754
755    /// `ON condition`.
756    #[must_use]
757    pub fn on(self, condition: impl IntoExpr) -> PlainJoinChain {
758        PlainJoinChain(self.0.on(condition))
759    }
760
761    /// `ON (a = b)`.
762    #[must_use]
763    pub fn on_eq(self, a: impl IntoExpr, b: impl IntoExpr) -> PlainJoinChain {
764        PlainJoinChain(self.0.on_eq(a, b))
765    }
766
767    /// `USING (\`a\`, \`b\`)`.
768    #[must_use]
769    pub fn using(
770        self,
771        columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
772    ) -> PlainJoinChain {
773        PlainJoinChain(self.0.using(columns))
774    }
775}
776
777impl From<PlainJoinChain> for Join {
778    fn from(chain: PlainJoinChain) -> Join {
779        chain.0.join
780    }
781}
782
783impl<Q: HasJoins> Mod<Q> for PlainJoinChain {
784    fn apply(self, q: &mut Q) {
785        self.0.apply(q);
786    }
787}
788
789impl TableChain<ExtraSlot> {
790    /// A join hanging off *this* comma-separated table reference rather than
791    /// off the leading one: ``FROM `a`, `b` INNER JOIN `c` ON …``.
792    ///
793    /// Grammatical because *15.2.15.2 JOIN Clause* reads
794    /// `table_references: escaped_table_reference [, escaped_table_reference] …`
795    /// and each `table_reference` — not just the first — may be a
796    /// `joined_table`. The comma has *lower* precedence than the join keywords
797    /// (the manual says so under the same section), so `b INNER JOIN c` is one
798    /// list entry and the join's left operand is this item alone. Takes
799    /// the same [`JoinChain`]/[`PlainJoinChain`] the standalone join mods are —
800    /// those reach the leading item through [`HasJoins`], which is why the
801    /// extra items take theirs by method instead. Several calls chain several
802    /// joins onto this item.
803    #[must_use]
804    pub fn join(mut self, join: impl Into<Join>) -> TableChain<ExtraSlot> {
805        self.table.joins.push(join.into());
806        self
807    }
808}
809
810// ---------------------------------------------------------------------------
811// WHERE / HAVING / GROUP BY
812// ---------------------------------------------------------------------------
813
814/// `WHERE condition`. Several calls are `AND`-joined; use [`or`](crate::or) for the
815/// other connective.
816pub fn where_<Q: HasWhere>(condition: impl IntoExpr) -> impl Mod<Q> {
817    let condition = condition.into_expr();
818    mod_fn(move |q: &mut Q| q.where_mut().append_where(condition))
819}
820
821/// `HAVING condition`. Several calls are `AND`-joined.
822pub fn having<Q: HasHaving>(condition: impl IntoExpr) -> impl Mod<Q> {
823    let condition = condition.into_expr();
824    mod_fn(move |q: &mut Q| q.having_mut().append_having(condition))
825}
826
827/// Add a grouping expression.
828///
829/// MySQL's `grouping_element` is a plain expression: there is no `ROLLUP(…)`,
830/// `CUBE(…)` or `GROUPING SETS(…)` element, and no `GROUP BY DISTINCT`. The only
831/// modifier is [`with_rollup`], and it applies to the whole clause.
832pub fn group_by<Q: HasGroupBy>(group: impl IntoExpr) -> impl Mod<Q> {
833    let group = group.into_expr();
834    mod_fn(move |q: &mut Q| q.group_by_mut().append_group(group))
835}
836
837/// `GROUP BY … WITH ROLLUP` — add the super-aggregate rows.
838pub fn with_rollup<Q: HasGroupBy>() -> impl Mod<Q> {
839    mod_fn(move |q: &mut Q| q.group_by_mut().with = Some(GroupByWith::Rollup))
840}
841
842// ---------------------------------------------------------------------------
843// WINDOW
844// ---------------------------------------------------------------------------
845
846/// `WINDOW \`name\` AS (definition)`, the definition built from `mysql::window::*`
847/// and `mysql::frame::*` mods.
848///
849/// A later window may be [`window::based_on`](crate::window::based_on) an earlier
850/// one.
851pub fn window<Q: HasWindows>(
852    name: impl Into<Cow<'static, str>>,
853    definition: impl Mod<Window>,
854) -> impl Mod<Q> {
855    let mut w = Window::default();
856    definition.apply(&mut w);
857    let named = NamedWindow::new(name, w);
858    mod_fn(move |q: &mut Q| q.windows_mut().append_window(named))
859}
860
861// ---------------------------------------------------------------------------
862// ORDER BY
863// ---------------------------------------------------------------------------
864
865/// Which `ORDER BY` an [`OrderChain`] appends to.
866pub trait OrderSlot<Q> {
867    /// The clause to append to.
868    fn slot(q: &mut Q) -> &mut OrderBy;
869}
870
871/// The statement's — or window's — own `ORDER BY`.
872#[derive(Debug, Clone, Copy, Default)]
873pub struct DirectOrder;
874
875/// The `ORDER BY` that applies to the result of a set operation.
876#[derive(Debug, Clone, Copy, Default)]
877pub struct CombinedOrder;
878
879impl<Q: HasOrderBy> OrderSlot<Q> for DirectOrder {
880    fn slot(q: &mut Q) -> &mut OrderBy {
881        q.order_by_mut()
882    }
883}
884
885impl<Q: HasCombines> OrderSlot<Q> for CombinedOrder {
886    fn slot(q: &mut Q) -> &mut OrderBy {
887        &mut q.combines_mut().order_by
888    }
889}
890
891/// One sort key under construction.
892///
893/// MySQL's `ORDER BY` takes a direction and — through the expression — a collation,
894/// and nothing else: there is no `NULLS FIRST`/`NULLS LAST` and no
895/// `USING operator`, so those methods do not exist here.
896#[derive(Debug, Clone)]
897pub struct OrderChain<S> {
898    def: OrderDef,
899    slot: PhantomData<S>,
900}
901
902/// `ORDER BY expression`.
903pub fn order_by(expression: impl IntoExpr) -> OrderChain<DirectOrder> {
904    OrderChain {
905        def: OrderDef::new(expression),
906        slot: PhantomData,
907    }
908}
909
910/// `ORDER BY` over the result of a `UNION`/`INTERSECT`/`EXCEPT`, rather than over
911/// this query.
912pub fn order_by_combined(expression: impl IntoExpr) -> OrderChain<CombinedOrder> {
913    OrderChain {
914        def: OrderDef::new(expression),
915        slot: PhantomData,
916    }
917}
918
919impl<S> OrderChain<S> {
920    /// `ASC` — the default, written out.
921    #[must_use]
922    pub fn asc(mut self) -> OrderChain<S> {
923        self.def.direction = Some(OrderDirection::Asc);
924        self
925    }
926
927    /// `DESC`.
928    #[must_use]
929    pub fn desc(mut self) -> OrderChain<S> {
930        self.def.direction = Some(OrderDirection::Desc);
931        self
932    }
933
934    /// `COLLATE \`name\``, written between the expression and the direction.
935    ///
936    /// The collation name is quoted as an identifier, which MySQL accepts wherever
937    /// a `collation_name` is expected.
938    #[must_use]
939    pub fn collate(mut self, name: impl Into<Cow<'static, str>>) -> OrderChain<S> {
940        self.def.collation = Some(name.into());
941        self
942    }
943}
944
945impl<Q, S: OrderSlot<Q>> Mod<Q> for OrderChain<S> {
946    fn apply(self, q: &mut Q) {
947        // `OrderBy` stores expressions, and an `OrderDef` reaches one as
948        // `Expr::Custom` — the route every struct-shaped clause item takes. Nothing
949        // groups it, so ``ORDER BY `name` DESC`` keeps its shape.
950        S::slot(q).append_order(Expr::custom(self.def));
951    }
952}
953
954// ---------------------------------------------------------------------------
955// LIMIT / OFFSET
956// ---------------------------------------------------------------------------
957
958/// `LIMIT count`.
959///
960/// A number is a literal — `limit(20)` gives `LIMIT 20` — because
961/// [`keelson_core::expr::IntoExpr`] makes it one. `limit(arg(20))` binds
962/// it instead, which MySQL permits in a prepared statement.
963pub fn limit<Q: HasLimit>(count: impl IntoExpr) -> impl Mod<Q> {
964    let count = count.into_expr();
965    mod_fn(move |q: &mut Q| q.limit_mut().set_limit(count))
966}
967
968/// `OFFSET start`.
969///
970/// MySQL spells `LIMIT` and `OFFSET` as one clause, so this needs a [`limit`] to
971/// parse. There is no `LIMIT ALL`.
972pub fn offset<Q: HasOffset>(start: impl IntoExpr) -> impl Mod<Q> {
973    let start = start.into_expr();
974    mod_fn(move |q: &mut Q| q.offset_mut().set_offset(start))
975}
976
977/// `LIMIT` over the result of a set operation rather than over this query.
978pub fn limit_combined<Q: HasCombines>(count: impl IntoExpr) -> impl Mod<Q> {
979    let count = count.into_expr();
980    mod_fn(move |q: &mut Q| q.combines_mut().limit.set_limit(count))
981}
982
983/// `OFFSET` over the result of a set operation rather than over this query.
984pub fn offset_combined<Q: HasCombines>(start: impl IntoExpr) -> impl Mod<Q> {
985    let start = start.into_expr();
986    mod_fn(move |q: &mut Q| q.combines_mut().offset.set_offset(start))
987}
988
989// ---------------------------------------------------------------------------
990// Locking
991// ---------------------------------------------------------------------------
992
993/// A `FOR …` locking clause under construction.
994#[derive(Debug, Clone)]
995pub struct LockChain {
996    lock: Lock,
997}
998
999/// `FOR UPDATE` — lock the rows for writing.
1000///
1001/// MySQL has only two strengths. `FOR NO KEY UPDATE` and `FOR KEY SHARE` are
1002/// PostgreSQL's, and there is nothing here that produces them.
1003pub fn for_update() -> LockChain {
1004    LockChain {
1005        lock: Lock::new(LockStrength::Update),
1006    }
1007}
1008
1009/// `FOR SHARE` (MySQL 8.0) — lock the rows for reading.
1010/// [`select::lock_in_share_mode`](crate::select::lock_in_share_mode) is the older
1011/// spelling.
1012pub fn for_share() -> LockChain {
1013    LockChain {
1014        lock: Lock::new(LockStrength::Share),
1015    }
1016}
1017
1018impl LockChain {
1019    /// `OF \`t\`` — restrict the lock to these tables of the statement.
1020    #[must_use]
1021    pub fn of(
1022        mut self,
1023        tables: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
1024    ) -> LockChain {
1025        self.lock.append_table(tables);
1026        self
1027    }
1028
1029    /// `NOWAIT` — fail rather than wait for a locked row.
1030    #[must_use]
1031    pub fn no_wait(mut self) -> LockChain {
1032        self.lock.wait = Some(LockWait::NoWait);
1033        self
1034    }
1035
1036    /// `SKIP LOCKED` — leave a locked row out of the result.
1037    #[must_use]
1038    pub fn skip_locked(mut self) -> LockChain {
1039        self.lock.wait = Some(LockWait::SkipLocked);
1040        self
1041    }
1042}
1043
1044impl<Q: HasLocks> Mod<Q> for LockChain {
1045    fn apply(self, q: &mut Q) {
1046        q.locks_mut().append_lock(self.lock);
1047    }
1048}
1049
1050// ---------------------------------------------------------------------------
1051// Set operations
1052// ---------------------------------------------------------------------------
1053
1054fn combine<Q: HasCombines>(op: SetOp, all: bool, query: impl IntoExpr) -> impl Mod<Q> {
1055    let mut c = Combine::new(op, query);
1056    c.all = all;
1057    mod_fn(move |q: &mut Q| q.combines_mut().append_combine(c))
1058}
1059
1060/// `UNION (query)` — rows of either, duplicates removed. `UNION DISTINCT` is the
1061/// same thing spelled out, and is not representable because it adds nothing.
1062pub fn union<Q: HasCombines>(query: impl IntoExpr) -> impl Mod<Q> {
1063    combine(SetOp::Union, false, query)
1064}
1065
1066/// `UNION ALL (query)` — rows of either, duplicates kept.
1067pub fn union_all<Q: HasCombines>(query: impl IntoExpr) -> impl Mod<Q> {
1068    combine(SetOp::Union, true, query)
1069}
1070
1071/// `INTERSECT (query)` — rows of both (MySQL 8.0.31).
1072pub fn intersect<Q: HasCombines>(query: impl IntoExpr) -> impl Mod<Q> {
1073    combine(SetOp::Intersect, false, query)
1074}
1075
1076/// `INTERSECT ALL (query)`.
1077pub fn intersect_all<Q: HasCombines>(query: impl IntoExpr) -> impl Mod<Q> {
1078    combine(SetOp::Intersect, true, query)
1079}
1080
1081/// `EXCEPT (query)` — rows of this query that are not in the other (MySQL 8.0.31).
1082pub fn except<Q: HasCombines>(query: impl IntoExpr) -> impl Mod<Q> {
1083    combine(SetOp::Except, false, query)
1084}
1085
1086/// `EXCEPT ALL (query)`.
1087pub fn except_all<Q: HasCombines>(query: impl IntoExpr) -> impl Mod<Q> {
1088    combine(SetOp::Except, true, query)
1089}
1090
1091// ---------------------------------------------------------------------------
1092// SET
1093// ---------------------------------------------------------------------------
1094
1095/// One assignment, written out: ``set(quote("a").eq(arg(1)))``.
1096///
1097/// A whole expression rather than a column/value pair, so that the left-hand side
1098/// can be qualified — which a multiple-table `UPDATE` needs.
1099pub fn set<Q: HasSet>(assignment: impl IntoExpr) -> impl Mod<Q> {
1100    let assignment = assignment.into_expr();
1101    mod_fn(move |q: &mut Q| q.set_mut().append_set(assignment))
1102}
1103
1104/// The left-hand side of an assignment: `set_col("a").to(arg(1))`.
1105///
1106/// Not a mod on its own — an assignment with no value is not one — so
1107/// [`to`](SetChain::to) or [`to_arg`](SetChain::to_arg) has to be called.
1108#[derive(Debug, Clone)]
1109pub struct SetChain {
1110    column: Expr,
1111}
1112
1113/// Assign to a column. `set_col(("t", "a"))` qualifies it, which a multiple-table
1114/// `UPDATE` requires.
1115pub fn set_col(column: impl IntoIdent) -> SetChain {
1116    SetChain {
1117        column: Expr::ident(column),
1118    }
1119}
1120
1121impl SetChain {
1122    /// ``\`col\` = value``, where `value` is an expression.
1123    pub fn to<Q: HasSet>(self, value: impl IntoExpr) -> impl Mod<Q> {
1124        set(Expr::binary(self.column, "=", value))
1125    }
1126
1127    /// ``\`col\` = ?`` — bind `value` as an argument.
1128    pub fn to_arg<Q: HasSet>(self, value: impl keelson_core::ToValue) -> impl Mod<Q> {
1129        set(Expr::binary(self.column, "=", Expr::arg(value)))
1130    }
1131}
1132
1133/// ``\`col\` = VALUES(\`col\`)`` for each column — the pre-8.0.19 body of an upsert.
1134///
1135/// Only meaningful inside [`on_duplicate_key_update`]. MySQL deprecates this form
1136/// in favour of [`set_row`], but 8.4 still accepts it.
1137pub fn set_values<Q: HasSet>(
1138    columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
1139) -> impl Mod<Q> {
1140    let assignments: Vec<Expr> = columns
1141        .into_iter()
1142        .map(Into::into)
1143        .filter(|c: &Cow<'static, str>| !c.is_empty())
1144        .map(|c| Expr::binary(Expr::ident(c.clone()), "=", values_of(c)))
1145        .collect();
1146    mod_fn(move |q: &mut Q| q.set_mut().append_sets(assignments))
1147}
1148
1149/// ``\`col\` = \`alias\`.\`col\``` for each column — the 8.0.19 body of an upsert,
1150/// naming the incoming row through the alias set by
1151/// [`insert::as_`](crate::insert::as_).
1152pub fn set_row<Q: HasSet>(
1153    alias: impl Into<Cow<'static, str>>,
1154    columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
1155) -> impl Mod<Q> {
1156    let alias = alias.into();
1157    let assignments: Vec<Expr> = columns
1158        .into_iter()
1159        .map(Into::into)
1160        .filter(|c: &Cow<'static, str>| !c.is_empty())
1161        .map(|c| Expr::binary(Expr::ident(c.clone()), "=", row_value(alias.clone(), c)))
1162        .collect();
1163    mod_fn(move |q: &mut Q| q.set_mut().append_sets(assignments))
1164}
1165
1166// ---------------------------------------------------------------------------
1167// VALUES
1168// ---------------------------------------------------------------------------
1169
1170/// One row of `VALUES`. Several calls append several rows.
1171///
1172/// A cell may be `DEFAULT`, which is [`raw("DEFAULT")`](crate::raw).
1173pub fn values<Q: HasValues>(row: impl IntoExprList) -> impl Mod<Q> {
1174    let row = row.into_expr_list();
1175    mod_fn(move |q: &mut Q| q.values_mut().append_values(row))
1176}
1177
1178/// Several rows of `VALUES` at once.
1179pub fn rows<Q: HasValues, R: IntoExprList>(rows: impl IntoIterator<Item = R>) -> impl Mod<Q> {
1180    let rows: Vec<Vec<Expr>> = rows.into_iter().map(IntoExprList::into_expr_list).collect();
1181    mod_fn(move |q: &mut Q| {
1182        let values = q.values_mut();
1183        for row in rows {
1184            values.append_values(row);
1185        }
1186    })
1187}
1188
1189/// Insert the results of a query: `INSERT INTO t (cols) SELECT …`.
1190///
1191/// Replaces any rows already added, because the two are alternatives in the grammar
1192/// rather than things that combine. A CTE for the sub-query goes *on the
1193/// sub-query* — MySQL puts the `WITH` after the `INSERT`, never before it.
1194pub fn values_from_query<Q: HasValues>(query: impl IntoExpr) -> impl Mod<Q> {
1195    let query = query.into_expr();
1196    mod_fn(move |q: &mut Q| *q.values_mut() = Values::from_query(query))
1197}
1198
1199// ---------------------------------------------------------------------------
1200// The INSERT row alias and ON DUPLICATE KEY UPDATE
1201// ---------------------------------------------------------------------------
1202
1203/// `AS \`alias\`` — name the row being inserted (MySQL 8.0.19), so that
1204/// `ON DUPLICATE KEY UPDATE` can refer to it by name.
1205#[derive(Debug, Clone)]
1206pub struct RowAliasChain {
1207    alias: RowAlias,
1208}
1209
1210/// `AS \`alias\`` on an `INSERT`. Use [`set_row`] to assign from it.
1211pub fn as_(alias: impl Into<Cow<'static, str>>) -> RowAliasChain {
1212    RowAliasChain {
1213        alias: RowAlias::new(alias),
1214    }
1215}
1216
1217impl RowAliasChain {
1218    /// Per-column aliases: `AS \`new\` (\`a\`, \`b\`)`.
1219    #[must_use]
1220    pub fn columns(
1221        mut self,
1222        columns: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
1223    ) -> RowAliasChain {
1224        self.alias.columns = columns.into_iter().map(Into::into).collect();
1225        self
1226    }
1227}
1228
1229impl<Q: HasRowAlias> Mod<Q> for RowAliasChain {
1230    fn apply(self, q: &mut Q) {
1231        *q.row_alias_mut() = self.alias;
1232    }
1233}
1234
1235/// `ON DUPLICATE KEY UPDATE assignment_list` — MySQL's upsert.
1236///
1237/// The body is built from mods against a bare
1238/// [`keelson_core::clause::Set`], which implements
1239/// [`keelson_core::clause::HasSet`] reflexively: [`set`], [`set_col`],
1240/// [`set_values`] and [`set_row`] all apply, and they are the same functions that
1241/// build an `INSERT … SET`.
1242pub fn on_duplicate_key_update<Q: HasDuplicateKeyUpdate>(body: impl Mod<Set>) -> impl Mod<Q> {
1243    let mut set = Set::default();
1244    body.apply(&mut set);
1245    mod_fn(move |q: &mut Q| q.duplicate_key_update_mut().append_sets(set.exprs))
1246}