Skip to main content

keelson_mysql/
extras.rs

1use std::borrow::Cow;
2
3use keelson_core::clause::Set;
4use keelson_core::expr::{Expr, IntoExpr};
5use keelson_core::{Expression, Query, SqlWriter};
6
7// ---------------------------------------------------------------------------
8// Statement modifiers
9// ---------------------------------------------------------------------------
10
11/// One of MySQL's statement modifiers — the keywords between a statement's first
12/// word and its real content.
13///
14/// MySQL spreads these across four productions:
15///
16/// ```text
17/// SELECT [ALL | DISTINCT | DISTINCTROW] [HIGH_PRIORITY] [STRAIGHT_JOIN]
18///        [SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
19///        [SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS] …
20/// INSERT [LOW_PRIORITY | DELAYED | HIGH_PRIORITY] [IGNORE] …
21/// UPDATE [LOW_PRIORITY] [IGNORE] …
22/// DELETE [LOW_PRIORITY] [QUICK] [IGNORE] …
23/// ```
24///
25/// **The order below is the grammar's order**, and [`Modifiers`] keeps its list
26/// sorted by it. That is the whole reason this is an enum rather than bob's
27/// `[]string`: bob appends the keywords in whatever order the mods were written,
28/// so `im.Ignore(), im.HighPriority()` produces `INSERT IGNORE HIGH_PRIORITY`,
29/// which MySQL rejects. Here mod order cannot affect the output.
30///
31/// Which modifiers a statement *permits* is decided by which of them its mod
32/// module re-exports, exactly as with the clause traits.
33///
34/// `ALL` is the default in `SELECT` and adds nothing, so it is not representable —
35/// the absence of [`Distinct`](Modifier::Distinct) is what it means.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub enum Modifier {
38    /// `DISTINCT` — drop duplicate result rows.
39    Distinct,
40    /// `DISTINCTROW`, MySQL's synonym for `DISTINCT`.
41    DistinctRow,
42    /// `LOW_PRIORITY` — wait for readers before writing.
43    LowPriority,
44    /// `HIGH_PRIORITY` — jump the queue ahead of pending writes.
45    HighPriority,
46    /// `DELAYED` — accepted and deprecated; MySQL treats it as `INSERT` and warns.
47    Delayed,
48    /// `QUICK` — do not merge index leaves while deleting.
49    Quick,
50    /// `IGNORE` — turn errors that would abort the statement into warnings.
51    Ignore,
52    /// `STRAIGHT_JOIN` — join tables in the order they are written.
53    StraightJoin,
54    /// `SQL_SMALL_RESULT` — the result set is small; use an in-memory temp table.
55    SmallResult,
56    /// `SQL_BIG_RESULT` — the result set is large; sort rather than use an index.
57    BigResult,
58    /// `SQL_BUFFER_RESULT` — force the result into a temporary table.
59    BufferResult,
60    /// `SQL_NO_CACHE` — do not read or write the query cache.
61    NoCache,
62    /// `SQL_CALC_FOUND_ROWS` — count the rows a `LIMIT` discarded.
63    CalcFoundRows,
64}
65
66impl Modifier {
67    /// The keyword, as written.
68    pub fn as_str(self) -> &'static str {
69        match self {
70            Modifier::Distinct => "DISTINCT",
71            Modifier::DistinctRow => "DISTINCTROW",
72            Modifier::LowPriority => "LOW_PRIORITY",
73            Modifier::HighPriority => "HIGH_PRIORITY",
74            Modifier::Delayed => "DELAYED",
75            Modifier::Quick => "QUICK",
76            Modifier::Ignore => "IGNORE",
77            Modifier::StraightJoin => "STRAIGHT_JOIN",
78            Modifier::SmallResult => "SQL_SMALL_RESULT",
79            Modifier::BigResult => "SQL_BIG_RESULT",
80            Modifier::BufferResult => "SQL_BUFFER_RESULT",
81            Modifier::NoCache => "SQL_NO_CACHE",
82            Modifier::CalcFoundRows => "SQL_CALC_FOUND_ROWS",
83        }
84    }
85}
86
87/// The modifiers of one statement, kept in grammar order.
88#[derive(Debug, Clone, Default)]
89pub struct Modifiers {
90    /// The modifiers, sorted and duplicate-free.
91    pub modifiers: Vec<Modifier>,
92}
93
94impl Modifiers {
95    /// Add a modifier, keeping the list sorted. A repeat is a no-op — writing
96    /// `IGNORE IGNORE` says nothing the first one did not.
97    pub fn append_modifier(&mut self, modifier: Modifier) {
98        if let Err(at) = self.modifiers.binary_search(&modifier) {
99            self.modifiers.insert(at, modifier);
100        }
101    }
102
103    /// Whether there are no modifiers.
104    pub fn is_empty(&self) -> bool {
105        self.modifiers.is_empty()
106    }
107}
108
109impl Expression for Modifiers {
110    fn write_sql(&self, w: &mut SqlWriter<'_>) {
111        for (i, modifier) in self.modifiers.iter().enumerate() {
112            if i > 0 {
113                w.push_str(" ");
114            }
115            w.push_str(modifier.as_str());
116        }
117    }
118}
119
120/// A statement that takes MySQL's modifier keywords.
121pub trait HasModifiers {
122    /// The modifiers to add to.
123    fn modifiers_mut(&mut self) -> &mut Modifiers;
124}
125
126impl HasModifiers for Modifiers {
127    fn modifiers_mut(&mut self) -> &mut Modifiers {
128        self
129    }
130}
131
132// ---------------------------------------------------------------------------
133// Optimizer hints
134// ---------------------------------------------------------------------------
135
136/// The `/*+ … */` optimizer-hint comment that may follow a statement's first
137/// keyword (*10.9.2 Optimizer Hints*).
138///
139/// Each hint is written verbatim: the hint language is its own grammar, and
140/// modelling all forty-odd hint names would be a second dialect inside this one.
141/// The handful with a fixed shape have their own mods; everything else goes
142/// through [`optimizer_hint`](crate::shared::optimizer_hint).
143#[derive(Debug, Clone, Default)]
144pub struct Hints {
145    /// The hint bodies, in the order they were added.
146    pub hints: Vec<Cow<'static, str>>,
147}
148
149impl Hints {
150    /// Append a hint body, without the surrounding `/*+ */`.
151    pub fn append_hint(&mut self, hint: impl Into<Cow<'static, str>>) {
152        self.hints.push(hint.into());
153    }
154
155    /// Whether there are no hints.
156    pub fn is_empty(&self) -> bool {
157        self.hints.is_empty()
158    }
159}
160
161impl Expression for Hints {
162    fn write_sql(&self, w: &mut SqlWriter<'_>) {
163        if self.hints.is_empty() {
164            return;
165        }
166        w.push_str("/*+ ");
167        for (i, hint) in self.hints.iter().enumerate() {
168            if i > 0 {
169                w.push_str(" ");
170            }
171            w.push_str(hint);
172        }
173        w.push_str(" */");
174    }
175}
176
177/// A statement that takes optimizer hints — all four of them do.
178pub trait HasHints {
179    /// The hints to add to.
180    fn hints_mut(&mut self) -> &mut Hints;
181}
182
183impl HasHints for Hints {
184    fn hints_mut(&mut self) -> &mut Hints {
185        self
186    }
187}
188
189// ---------------------------------------------------------------------------
190// INSERT's row alias
191// ---------------------------------------------------------------------------
192
193/// `AS row_alias [(col_alias, …)]`, the name an `INSERT`'s new row is given
194/// (MySQL 8.0.19).
195///
196/// It exists so that `ON DUPLICATE KEY UPDATE` can refer to the incoming values
197/// by name instead of through the deprecated `VALUES()` function:
198///
199/// ```text
200/// INSERT INTO t (a, b) VALUES (?, ?) AS `new` ON DUPLICATE KEY UPDATE b = `new`.b
201/// ```
202#[derive(Debug, Clone, Default)]
203pub struct RowAlias {
204    /// The row alias, quoted on output.
205    pub name: Option<Cow<'static, str>>,
206    /// Per-column aliases, quoted on output.
207    pub columns: Vec<Cow<'static, str>>,
208}
209
210impl RowAlias {
211    /// A row alias with no column aliases.
212    pub fn new(name: impl Into<Cow<'static, str>>) -> RowAlias {
213        RowAlias {
214            name: Some(name.into()),
215            columns: Vec::new(),
216        }
217    }
218
219    /// Whether there is no alias. Column aliases alone are not a clause: the
220    /// grammar hangs them off the row alias.
221    pub fn is_empty(&self) -> bool {
222        self.name.is_none()
223    }
224}
225
226impl Expression for RowAlias {
227    fn write_sql(&self, w: &mut SqlWriter<'_>) {
228        let Some(name) = &self.name else {
229            return;
230        };
231        w.push_str("AS ");
232        w.push_quoted(&[name]);
233        if !self.columns.is_empty() {
234            w.push_str(" (");
235            for (i, column) in self.columns.iter().enumerate() {
236                if i > 0 {
237                    w.push_str(", ");
238                }
239                w.push_quoted(&[column]);
240            }
241            w.push_str(")");
242        }
243    }
244}
245
246/// A statement that names its incoming row — only `INSERT` does.
247pub trait HasRowAlias {
248    /// The row alias to set.
249    fn row_alias_mut(&mut self) -> &mut RowAlias;
250}
251
252impl HasRowAlias for RowAlias {
253    fn row_alias_mut(&mut self) -> &mut RowAlias {
254        self
255    }
256}
257
258/// A statement with an `ON DUPLICATE KEY UPDATE` assignment list.
259///
260/// A separate trait from [`HasSet`](keelson_core::clause::HasSet) because an
261/// `INSERT` has *two* assignment lists — the `INSERT … SET` row source and this
262/// one — and a mod has to be able to say which it means. `HasSet` is the row
263/// source; the body of `on_duplicate_key_update` is a bare
264/// [`Set`](keelson_core::clause::Set), which implements `HasSet` reflexively, so
265/// the same `set`/`set_col` mods serve both.
266pub trait HasDuplicateKeyUpdate {
267    /// The `ON DUPLICATE KEY UPDATE` assignments.
268    fn duplicate_key_update_mut(&mut self) -> &mut Set;
269}
270
271// ---------------------------------------------------------------------------
272// Sub-queries
273// ---------------------------------------------------------------------------
274
275/// A whole query standing in an expression slot, rendered in **its own** dialect.
276///
277/// [`SqlWriter::write_with_dialect`] keeps one shared argument list and
278/// placeholder counter, so a sub-query re-indexes into its container for free —
279/// which for MySQL means its arguments land in the right *positions* even though
280/// every placeholder looks identical.
281#[derive(Debug)]
282struct QueryExpr<Q>(Q);
283
284impl<Q: Query> Expression for QueryExpr<Q> {
285    fn write_sql(&self, w: &mut SqlWriter<'_>) {
286        w.write_with_dialect(self.0.dialect(), &self.0);
287    }
288}
289
290/// A query as an expression, **not** parenthesised.
291///
292/// The form for slots that supply their own parentheses — a `WITH` body, a
293/// set-operation operand, `IN (…)`, `INSERT … SELECT`. Use [`subquery`] where the
294/// parentheses belong to the sub-query itself, as in a `FROM` item.
295pub fn query(q: impl Query + 'static) -> Expr {
296    Expr::custom(QueryExpr(q))
297}
298
299/// A parenthesised sub-query: `(SELECT …)`.
300///
301/// What a `FROM` item or a scalar sub-expression needs. MySQL additionally
302/// requires an alias on a derived table, which is
303/// [`select::from(..).as_(..)`](crate::select::from).
304pub fn subquery(q: impl Query + 'static) -> Expr {
305    Expr::group(query(q))
306}
307
308// ---------------------------------------------------------------------------
309// ON DUPLICATE KEY UPDATE value sources
310// ---------------------------------------------------------------------------
311
312/// `VALUES(`col`)` — the value the `INSERT` proposed for `col`.
313///
314/// The pre-8.0.19 way to reach the incoming row inside
315/// `ON DUPLICATE KEY UPDATE`. MySQL deprecates it in favour of a row alias, which
316/// is [`row_value`]; both are still accepted by 8.4.
317///
318/// Note that `VALUES()` means something entirely different anywhere else — it is
319/// the ordinary `VALUES` row constructor — so this belongs only inside an
320/// `ON DUPLICATE KEY UPDATE` body.
321pub fn values_of(column: impl Into<Cow<'static, str>>) -> Expr {
322    Expr::func("VALUES", Expr::ident(column.into()))
323}
324
325/// ``` `alias`.`col` ``` — the incoming row's column, through the row alias set by
326/// [`insert::as_`](crate::insert::as_).
327pub fn row_value(
328    alias: impl Into<Cow<'static, str>>,
329    column: impl Into<Cow<'static, str>>,
330) -> Expr {
331    Expr::ident([alias.into(), column.into()])
332}
333
334/// `MATCH (cols) AGAINST (expr [modifier])` — a full-text search predicate
335/// (*14.9 Full-Text Search Functions*).
336///
337/// The modifier is written verbatim after the search string, so
338/// `IN NATURAL LANGUAGE MODE`, `IN BOOLEAN MODE` and `WITH QUERY EXPANSION` all
339/// work; `None` leaves it out, which is natural-language mode.
340#[derive(Debug)]
341pub(crate) struct Match {
342    pub(crate) columns: Vec<Expr>,
343    pub(crate) against: Expr,
344    pub(crate) modifier: Option<Cow<'static, str>>,
345}
346
347impl Expression for Match {
348    fn write_sql(&self, w: &mut SqlWriter<'_>) {
349        w.push_str("MATCH (");
350        w.write_slice(&self.columns, "", ", ", "");
351        w.push_str(") AGAINST (");
352        w.write_expr(&self.against);
353        if let Some(modifier) = &self.modifier {
354            w.push_str(" ");
355            w.push_str(modifier);
356        }
357        w.push_str(")");
358    }
359}
360
361/// `MATCH (cols) AGAINST (search)` in natural-language mode.
362pub fn match_against(
363    columns: impl keelson_core::expr::IntoExprList,
364    search: impl IntoExpr,
365) -> Expr {
366    Expr::custom(Match {
367        columns: columns.into_expr_list(),
368        against: search.into_expr(),
369        modifier: None,
370    })
371}
372
373/// `MATCH (cols) AGAINST (search IN BOOLEAN MODE)`, or any other search modifier
374/// written out.
375pub fn match_against_mode(
376    columns: impl keelson_core::expr::IntoExprList,
377    search: impl IntoExpr,
378    modifier: impl Into<Cow<'static, str>>,
379) -> Expr {
380    Expr::custom(Match {
381        columns: columns.into_expr_list(),
382        against: search.into_expr(),
383        modifier: Some(modifier.into()),
384    })
385}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use crate::{Mysql, quote, s};
391    use keelson_core::build;
392
393    fn sql(e: impl Expression) -> String {
394        build(&Mysql, &e).expect("render").0
395    }
396
397    /// The grammar's order, not the caller's. `SELECT DISTINCT HIGH_PRIORITY …`
398    /// parses; `SELECT HIGH_PRIORITY DISTINCT …` does not.
399    #[test]
400    fn modifiers_render_in_grammar_order_whatever_order_they_were_added_in() {
401        let mut m = Modifiers::default();
402        m.append_modifier(Modifier::CalcFoundRows);
403        m.append_modifier(Modifier::HighPriority);
404        m.append_modifier(Modifier::Distinct);
405        m.append_modifier(Modifier::StraightJoin);
406        assert_eq!(
407            sql(m),
408            "DISTINCT HIGH_PRIORITY STRAIGHT_JOIN SQL_CALC_FOUND_ROWS"
409        );
410    }
411
412    #[test]
413    fn a_repeated_modifier_is_written_once() {
414        let mut m = Modifiers::default();
415        m.append_modifier(Modifier::Ignore);
416        m.append_modifier(Modifier::Ignore);
417        assert_eq!(sql(m), "IGNORE");
418    }
419
420    #[test]
421    fn an_empty_modifier_list_writes_nothing() {
422        assert_eq!(sql(Modifiers::default()), "");
423        assert!(Modifiers::default().is_empty());
424    }
425
426    /// *10.9.2*: `SELECT /*+ MAX_EXECUTION_TIME(1000) */ …`.
427    #[test]
428    fn hints_are_wrapped_in_one_comment_and_space_separated() {
429        let mut h = Hints::default();
430        assert_eq!(sql(h.clone()), "");
431        h.append_hint("MAX_EXECUTION_TIME(1000)");
432        assert_eq!(sql(h.clone()), "/*+ MAX_EXECUTION_TIME(1000) */");
433        h.append_hint("QB_NAME(outer)");
434        assert_eq!(sql(h), "/*+ MAX_EXECUTION_TIME(1000) QB_NAME(outer) */");
435    }
436
437    #[test]
438    fn a_row_alias_quotes_its_name_and_its_columns() {
439        assert_eq!(sql(RowAlias::default()), "");
440        assert_eq!(sql(RowAlias::new("new")), "AS `new`");
441        assert_eq!(
442            sql(RowAlias {
443                name: Some("new".into()),
444                columns: vec!["a".into(), "b".into()],
445            }),
446            "AS `new` (`a`, `b`)"
447        );
448        // Column aliases alone are not a clause.
449        assert_eq!(
450            sql(RowAlias {
451                name: None,
452                columns: vec!["a".into()],
453            }),
454            ""
455        );
456    }
457
458    #[test]
459    fn the_two_upsert_value_sources_render_as_the_manual_writes_them() {
460        assert_eq!(sql(values_of("name")), "VALUES(`name`)");
461        assert_eq!(sql(row_value("new", "name")), "`new`.`name`");
462    }
463
464    /// *14.9*: `MATCH (col1, col2) AGAINST (expr [search_modifier])`.
465    #[test]
466    fn match_against_puts_the_modifier_inside_the_against_parentheses() {
467        assert_eq!(
468            sql(match_against(quote("title"), s("rust"))),
469            "MATCH (`title`) AGAINST ('rust')"
470        );
471        assert_eq!(
472            sql(match_against_mode(
473                (quote("title"), quote("status")),
474                s("+rust -go"),
475                "IN BOOLEAN MODE"
476            )),
477            "MATCH (`title`, `status`) AGAINST ('+rust -go' IN BOOLEAN MODE)"
478        );
479    }
480}