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}