Skip to main content

keelson_mysql/
lib.rs

1//! The MySQL dialect for keelson.
2//!
3//! Five statement types, each shaped by the production in the MySQL 8.4 reference
4//! manual, plus the mods that fill them in and the expression starters they are
5//! filled in with.
6//!
7//! ```
8//! use keelson_mysql as mysql;
9//! use keelson_mysql::{Chain, Query, arg, quote, select};
10//!
11//! let q = mysql::select((
12//!     select::columns((quote("id"), quote("name"))),
13//!     select::from(quote("users")),
14//!     select::where_(quote("age").gte(arg(21i32))),
15//! ));
16//!
17//! let (sql, args) = q.build()?;
18//! assert_eq!(sql, "SELECT `id`, `name` FROM `users` WHERE (`age` >= ?)");
19//! assert_eq!(args, vec![keelson_core::Value::I32(21)]);
20//! # Ok::<_, keelson_core::Error>(())
21//! ```
22//!
23//! # Where this sits
24//!
25//! Layer 1 of keelson, for MySQL. It is a complete way to use keelson on its
26//! own: it depends only on [keelson-core](https://docs.rs/keelson-core) and produces a SQL
27//! string and an argument list, which you may run with any driver you like. To
28//! run it through keelson, add Layer 2 ([keelson-exec](https://docs.rs/keelson-exec) plus a
29//! backend such as [keelson-sqlx](https://docs.rs/keelson-sqlx)); to have typed models built
30//! out of these mods, add Layers 3 and 4 ([keelson-models](https://docs.rs/keelson-models),
31//! [keelson-gen](https://docs.rs/keelson-gen)). MySQL's missing `RETURNING` is visible up
32//! there: a generated MySQL model reads a row back by key instead. The whole
33//! map is the [keelson](https://docs.rs/keelson) facade crate.
34//!
35//! # How it is put together
36//!
37//! **A starter is a function of one mod.** `mysql::select(mods)` takes a single
38//! `impl Mod<SelectQuery>` — and a tuple of mods is a mod, so `mysql::select(())`
39//! and `mysql::select((a, b, c))` are both that one argument. Arity is never a
40//! ceiling, because tuples nest.
41//!
42//! **A mod module shares its name with its starter.** `mysql::select` is a function
43//! *and* a module: Rust keeps values and modules in separate namespaces, so
44//! `mysql::select((select::from("users"),))` needs no import gymnastics. The modules
45//! are named after the statement — `select`, `insert`, `replace`, `update`,
46//! `delete`, [`window`], [`frame`] — never bob's `sm`/`im`/`um`/`dm`/`wm`/`fm`.
47//!
48//! **A mod is written once.** The mods live in [`shared`], generic over the
49//! `keelson_core::clause` `Has*` trait they need, and each statement module
50//! re-exports the ones that apply to it. An inapplicable mod is a compile error.
51//!
52//! **A raw `&str` works wherever an expression does.** Every slot takes
53//! `impl IntoExpr`, and a `&'static str` is raw SQL. `select::from("users")` writes
54//! `FROM users`; `select::from(quote("users"))` writes ``FROM `users` ``.
55//!
56//! # What MySQL does not have
57//!
58//! The list is worth reading before looking for a mod that is not here, because each
59//! absence is deliberate — a construct a dialect lacks does not exist for it.
60//!
61//! | absent | why |
62//! |---|---|
63//! | `RETURNING` | MySQL has none, on any statement. There is no `Returning` field in this crate. |
64//! | `FETCH … ROWS ONLY` | not in the grammar; `LIMIT` is the only row limiter. |
65//! | `DISTINCT ON` | PostgreSQL's. [`select::distinct`] is the whole of MySQL's. |
66//! | `FULL JOIN` | not in the grammar. Three join kinds plus `CROSS` and `STRAIGHT_JOIN`. |
67//! | `NULLS FIRST` / `NULLS LAST`, `USING operator` | not in `ORDER BY`; the chain has `asc`, `desc` and `collate`. |
68//! | `GROUPS` frame mode, `EXCLUDE` | not in the frame clause. |
69//! | `FILTER (WHERE …)`, `WITHIN GROUP` | not on a function call. |
70//! | `ROLLUP(…)`, `CUBE(…)`, `GROUPING SETS(…)`, `GROUP BY DISTINCT` | `GROUP BY … WITH ROLLUP` is the only super-aggregate. |
71//! | `ON CONFLICT` | MySQL's upsert is `ON DUPLICATE KEY UPDATE`, and [`REPLACE`](replace()). |
72//! | `WITH` on `INSERT`/`REPLACE` | permitted only immediately before the sub-`SELECT`. |
73//! | `TABLESAMPLE`, `ONLY`, `WITH ORDINALITY` | PostgreSQL's from-item decorations. |
74//! | `IS DISTINCT FROM` | on the shared chain and *not* valid MySQL; use [`MysqlOps::null_safe_eq`]. |
75//!
76//! # Sub-queries
77//!
78//! The statement types implement [`IntoExpr`], so one goes straight into any
79//! expression slot: `select::union(other)`, `select::with("c", other)`,
80//! `insert::query(other)`. Those slots supply their own parentheses. Where the
81//! parentheses belong to the sub-query itself — a derived table, a scalar
82//! sub-expression — use [`subquery`]. Placeholders re-index across the nesting on
83//! their own, because the counter belongs to the writer; that matters here even
84//! though every MySQL placeholder looks the same, because the *argument order* is
85//! what the counter fixes.
86
87#![warn(missing_docs)]
88
89mod dialect;
90mod extras;
91mod function;
92mod ops;
93pub mod shared;
94mod statement;
95
96pub mod delete;
97pub mod frame;
98pub mod insert;
99pub mod replace;
100pub mod select;
101pub mod table;
102pub mod update;
103pub mod values;
104pub mod window;
105
106pub use dialect::Mysql;
107pub use extras::{
108    HasDuplicateKeyUpdate, HasHints, HasModifiers, HasRowAlias, Hints, Modifier, Modifiers,
109    RowAlias, match_against, match_against_mode, query, row_value, subquery, values_of,
110};
111pub use function::Function;
112pub use ops::MysqlOps;
113pub use statement::{
114    DeleteQuery, HasDeleteTables, HasExtraTables, HasTargetTable, InsertQuery, ReplaceQuery,
115    SelectQuery, TableQuery, UpdateQuery, ValuesQuery,
116};
117
118// The core vocabulary a caller needs in order to use any of the above, re-exported
119// so that a program building MySQL queries needs one dependency and one `use`.
120pub use keelson_core::expr::{CaseBuilder, Chain, Expr, IntoExpr, IntoExprList, IntoIdent, RawArg};
121pub use keelson_core::{Error, Mod, Query, QueryType, RawQuery, Result, Value};
122
123use std::borrow::Cow;
124
125use keelson_core::ToValue;
126use keelson_core::expr;
127
128// ---------------------------------------------------------------------------
129// Statement starters
130// ---------------------------------------------------------------------------
131
132/// Build a `SELECT` from one mod — usually a tuple of them.
133///
134/// The returned query knows its own dialect, so
135/// [`build()`](keelson_core::Query::build) takes no arguments.
136pub fn select(mods: impl Mod<SelectQuery>) -> SelectQuery {
137    let mut q = SelectQuery::default();
138    mods.apply(&mut q);
139    q
140}
141
142/// Build an `INSERT` from one mod.
143pub fn insert(mods: impl Mod<InsertQuery>) -> InsertQuery {
144    let mut q = InsertQuery::default();
145    mods.apply(&mut q);
146    q
147}
148
149/// Build a `REPLACE` from one mod.
150///
151/// A statement type of its own, not a flag on `INSERT`: `REPLACE` has no `IGNORE`,
152/// no `HIGH_PRIORITY`, no row alias and no `ON DUPLICATE KEY UPDATE`, and the way to
153/// say so is for those mods not to apply to it.
154pub fn replace(mods: impl Mod<ReplaceQuery>) -> ReplaceQuery {
155    let mut q = ReplaceQuery::default();
156    mods.apply(&mut q);
157    q
158}
159
160/// Build an `UPDATE` from one mod.
161pub fn update(mods: impl Mod<UpdateQuery>) -> UpdateQuery {
162    let mut q = UpdateQuery::default();
163    mods.apply(&mut q);
164    q
165}
166
167/// Build a `DELETE` from one mod.
168pub fn delete(mods: impl Mod<DeleteQuery>) -> DeleteQuery {
169    let mut q = DeleteQuery::default();
170    mods.apply(&mut q);
171    q
172}
173
174/// Build a standalone `VALUES` statement from one mod (MySQL 8.0.19+).
175///
176/// The rows come from [`values::row`]/[`values::rows`] and are spelled
177/// `ROW(…)`, as the standalone grammar requires; with none the statement is a
178/// [`build()`](keelson_core::Query::build) error.
179pub fn values(mods: impl Mod<ValuesQuery>) -> ValuesQuery {
180    let mut q = ValuesQuery::default();
181    mods.apply(&mut q);
182    q
183}
184
185/// Build a `TABLE` statement from one mod (MySQL 8.0.19+) — MySQL's shorthand
186/// for `SELECT * FROM t`.
187///
188/// The table comes from [`table::name`]; with none the statement is a
189/// [`build()`](keelson_core::Query::build) error.
190pub fn table(mods: impl Mod<TableQuery>) -> TableQuery {
191    let mut q = TableQuery::default();
192    mods.apply(&mut q);
193    q
194}
195
196// ---------------------------------------------------------------------------
197// Expression starters
198// ---------------------------------------------------------------------------
199
200/// A whole statement, written by hand, as a runnable query.
201///
202/// [`raw`] is a *fragment* an expression accepts; this is a *statement* nothing
203/// built. It is an ordinary [`Query`], so the execution layer's verbs work on
204/// it — `fetch_all::<T>()` maps hand-written MySQL onto a struct exactly as
205/// it maps a built one — and it nests as a sub-select in a built statement.
206///
207/// Placeholders are `?` and are rewritten to `?` as it renders; `\?` is a
208/// literal question mark. Values are bound with [`RawQuery::bind`] and never
209/// reach the SQL text.
210///
211/// ```
212/// use keelson_mysql::{raw_query, Query as _};
213///
214/// let q = raw_query("SELECT id, name FROM users WHERE age >= ?").bind(21);
215/// let (sql, args) = q.build()?;
216/// assert_eq!(sql, "SELECT id, name FROM users WHERE age >= ?");
217/// assert_eq!(args, vec![keelson_mysql::Value::I32(21)]);
218/// # Ok::<_, keelson_mysql::Error>(())
219/// ```
220pub fn raw_query(sql: impl Into<Cow<'static, str>>) -> RawQuery<Mysql> {
221    RawQuery::new(Mysql, sql)
222}
223
224/// [`raw_query`], with the values written where they bind (feature `macros`).
225///
226/// ```
227/// # use keelson_mysql::{sql, Query as _};
228/// let min_age = 21;
229/// let q = sql!("SELECT id, name FROM users WHERE age >= {min_age}");
230/// let (text, args) = q.build()?;
231/// assert_eq!(text, "SELECT id, name FROM users WHERE age >= ?");
232/// assert_eq!(args, vec![keelson_mysql::Value::I32(21)]);
233/// # Ok::<_, keelson_mysql::Error>(())
234/// ```
235///
236/// It expands to exactly that call — `raw_query("…").bind(min_age)` — so
237/// nothing is hidden and the result composes like any other query. What it
238/// buys is two mistakes that stop being expressible:
239///
240/// - **Binds cannot be transposed.** `raw_query(…).bind(a).bind(b)` with `a`
241///   and `b` the wrong way round type-checks and runs; here the value is
242///   written at the hole.
243/// - **A question mark you typed stays a question mark.** The `?` rewriting
244///   does not track quoting, so `WHERE note = 'what\?'` would otherwise hold a
245///   hole, and a statement whose argument count happened to match would be
246///   silently wrong. The macro escapes every `?` it did not generate.
247///
248/// The grammar is `format!`'s, and the analogy is exact except in one place
249/// that matters: **`{x}` binds, it never interpolates.** No hole can put text
250/// into the SQL. Where you do want to splice SQL — an `IN` list, a sub-query
251/// — say so with `{x:sql}`, which takes an expression rather than a value:
252///
253/// ```
254/// # use keelson_mysql::{args, sql, Query as _};
255/// let ids = args([1, 2, 3]);
256/// let (text, _) = sql!("SELECT * FROM users WHERE id IN ({ids:sql})").build()?;
257/// assert!(text.contains("IN ("));
258/// # Ok::<_, keelson_mysql::Error>(())
259/// ```
260///
261/// `{{` and `}}` are literal braces. Values are still bound, not typed: for
262/// SQL the schema checks, use a generated model or a `.sql` file.
263#[cfg(feature = "macros")]
264#[macro_export]
265macro_rules! sql {
266    ($($tt:tt)*) => {
267        $crate::__sql_with!($crate::raw_query, $($tt)*)
268    };
269}
270
271#[cfg(feature = "macros")]
272#[doc(hidden)]
273pub use keelson_core::__sql_with;
274
275/// Raw SQL, verbatim. `?` is left alone — see [`template`].
276///
277/// The progressive-enhancement entry point: a hand-written fragment goes anywhere a
278/// structured expression does.
279pub fn raw(sql: impl Into<Cow<'static, str>>) -> Expr {
280    expr::raw(sql)
281}
282
283/// Raw SQL whose `?` are rewritten with `args` interleaved. Write `\?` for a literal
284/// question mark.
285///
286/// MySQL's own placeholder is already `?`, so this looks like a no-op and is not: the
287/// holes are *counted*, and the arguments are bound in the writer's order, which is
288/// what makes a template safe to nest inside a query that already has arguments.
289pub fn template(sql: impl Into<Cow<'static, str>>, args: impl IntoIterator<Item = RawArg>) -> Expr {
290    expr::template(sql, args)
291}
292
293/// A single-quoted string literal — bob's `S`. `s("A")` renders `'A'`.
294///
295/// Nothing is escaped: this is for SQL the program itself wrote — a keyword, an enum
296/// label, a JSON path. Text from outside belongs in [`arg`], where it is bound.
297pub fn s(literal: impl Into<Cow<'static, str>>) -> Expr {
298    expr::literal(literal)
299}
300
301/// A quoted identifier: ``quote("age")`` gives `` `age` ``, `quote(("users", "id"))`
302/// gives `` `users`.`id` ``.
303pub fn quote(parts: impl IntoIdent) -> Expr {
304    expr::quote(parts)
305}
306
307/// One bound argument, rendered `?`.
308pub fn arg(value: impl ToValue) -> Expr {
309    expr::arg(value)
310}
311
312/// Several bound arguments, comma-separated and *not* parenthesised — for a slot that
313/// brings its own, such as `VALUES (…)`.
314pub fn args<V: ToValue>(values: impl IntoIterator<Item = V>) -> Expr {
315    expr::args(values)
316}
317
318/// Several bound arguments, parenthesised: `(?, ?, ?)`.
319pub fn arg_group<V: ToValue>(values: impl IntoIterator<Item = V>) -> Expr {
320    expr::arg_group(values)
321}
322
323/// `n` unbound placeholders, each binding `NULL`, so a statement can be prepared now
324/// and its values supplied by whatever rebinds it.
325pub fn placeholders(n: usize) -> Expr {
326    expr::placeholders(n)
327}
328
329/// A parenthesised, comma-separated list: `(a, b)`. One element gives plain
330/// parentheses.
331pub fn group(items: impl IntoExprList) -> Expr {
332    expr::group(items)
333}
334
335/// A function call: `f("COUNT", "*")`, `f("ROW_NUMBER", ()).over(())`.
336///
337/// Returns keelson-mysql's own [`Function`], which carries `DISTINCT`, `ORDER BY`,
338/// `SEPARATOR` and `OVER` — everything MySQL hangs off a call and core deliberately
339/// does not know about.
340pub fn f(name: impl Into<Cow<'static, str>>, args: impl IntoExprList) -> Function {
341    Function::new(name, args)
342}
343
344/// A `CASE` expression: `case_().when(cond, then).else_(other)`.
345///
346/// Named with a trailing underscore because `case` does not read well as a plain
347/// identifier next to `match`; the SQL is unaffected.
348pub fn case_() -> CaseBuilder {
349    expr::case()
350}
351
352/// `CAST(expr AS type_name)`.
353///
354/// MySQL has no `::` shorthand, so this is the only spelling. Not wrapped in
355/// parentheses of its own: `CAST(…)` is already self-delimiting.
356pub fn cast(expression: impl IntoExpr, type_name: impl Into<Cow<'static, str>>) -> Expr {
357    expr::cast(expression, type_name)
358}
359
360/// `NOT expr`. The operand is parenthesised if it needs it; the result is not,
361/// because `NOT` binds looser than anything it can contain.
362pub fn not(expression: impl IntoExpr) -> Expr {
363    expr::not(expression)
364}
365
366/// `(a AND b AND c)`.
367pub fn and(items: impl IntoExprList) -> Expr {
368    expr::and(items)
369}
370
371/// `(a OR b OR c)`.
372pub fn or(items: impl IntoExprList) -> Expr {
373    expr::or(items)
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn a_starter_takes_no_mods_at_all() {
382        // `()` is a mod, so the empty query needs no separate constructor.
383        assert_eq!(select(()).build().unwrap().0, "SELECT *");
384    }
385
386    #[test]
387    fn apply_adds_mods_to_a_built_query_and_clone_leaves_the_original_alone() {
388        let base = select((select::columns(quote("id")), select::from(quote("users"))));
389        let mut narrowed = base.clone();
390        narrowed.apply(select::where_(quote("id").eq(arg(1i32))));
391
392        assert_eq!(
393            base.build().unwrap().0,
394            "SELECT `id` FROM `users`",
395            "the clone did not disturb the original"
396        );
397        assert_eq!(
398            narrowed.build().unwrap().0,
399            "SELECT `id` FROM `users` WHERE (`id` = ?)"
400        );
401    }
402
403    #[test]
404    fn the_query_type_is_carried_rather_than_reparsed() {
405        assert_eq!(select(()).query_type(), QueryType::Select);
406        assert_eq!(insert(()).query_type(), QueryType::Insert);
407        // A REPLACE writes rows, which is all the layers above need to know.
408        assert_eq!(replace(()).query_type(), QueryType::Insert);
409        assert_eq!(update(()).query_type(), QueryType::Update);
410        assert_eq!(delete(()).query_type(), QueryType::Delete);
411    }
412
413    #[test]
414    fn a_statement_missing_a_clause_it_cannot_render_without_says_so() {
415        // The substrings name the SQL concepts, not the message wording.
416        let err = insert(()).build().unwrap_err();
417        assert!(
418            matches!(&err, Error::Incomplete(what) if what.contains("INSERT")),
419            "got: {err}"
420        );
421        let err = replace(()).build().unwrap_err();
422        assert!(
423            matches!(&err, Error::Incomplete(what) if what.contains("REPLACE")),
424            "got: {err}"
425        );
426        let err = update(update::table(quote("users"))).build().unwrap_err();
427        assert!(
428            matches!(&err, Error::Incomplete(what)
429                if what.contains("assignments") && what.contains("UPDATE")),
430            "got: {err}"
431        );
432        let err = delete(()).build().unwrap_err();
433        assert!(
434            matches!(&err, Error::Incomplete(what) if what.contains("DELETE")),
435            "got: {err}"
436        );
437    }
438}