qbrs_core/dialect.rs
1//! Dialect markers and capability gating.
2//!
3//! Dialect-specific capabilities (Postgres `.returning()`, MySQL's lack of
4//! it, etc.) are sealed marker traits implemented only for the dialects that
5//! actually support them, so calling an unsupported method is a compile
6//! error rather than a documentation footnote.
7//!
8//! Minimum supported version per dialect:
9//! - **Postgres**: any currently-supported version.
10//! - **MySQL**: 8.0+ (window functions/CTEs).
11//! - **SQLite**: 3.39+ (2022-06), for `RIGHT`/`FULL JOIN` support.
12
13mod private {
14 pub trait Sealed {}
15}
16
17/// `Default` so a dialect can be produced where only its type is known:
18/// the render terminals take one as a value (`.to_sql(Postgres)`), which is
19/// what lets every builder before them infer it instead of being told.
20pub trait Dialect: 'static + Copy + Default + private::Sealed {
21 /// SQL identifier quote character, e.g. `"` for Postgres/SQLite, `` ` ``
22 /// for MySQL.
23 const IDENTIFIER_QUOTE: char;
24
25 /// How this dialect spells the two types an aggregate is cast back to.
26 /// `CAST` itself is standard; the type names are not — MySQL takes
27 /// `SIGNED` and `DOUBLE` where Postgres takes `BIGINT` and
28 /// `DOUBLE PRECISION`.
29 const CAST_BIGINT: &'static str = "BIGINT";
30 const CAST_DOUBLE: &'static str = "DOUBLE PRECISION";
31
32 /// Whether a set operation's branches may be parenthesised. SQLite's
33 /// grammar has no place for a parenthesised `SELECT` around `UNION`.
34 const PARENTHESIZED_SET_OP_BRANCHES: bool = true;
35
36 /// How to insert a row that names no column, which is what a table
37 /// whose every column is generated leaves. `INSERT INTO t () VALUES ()`
38 /// is MySQL's spelling and a syntax error everywhere else.
39 const INSERT_NO_COLUMNS: &'static str = " DEFAULT VALUES";
40
41 /// What to put in a `LIMIT` when a query has an `OFFSET` and no limit.
42 /// Postgres takes a bare `OFFSET`; SQLite and MySQL don't, and each
43 /// spells "no limit" differently.
44 const OFFSET_WITHOUT_LIMIT: Option<&'static str> = None;
45
46 /// How this dialect spells "concatenate a group's values, separated",
47 /// and with it whether the separator can be a parameter. One value
48 /// rather than a name beside a syntax, because the two only make sense
49 /// together: `group_concat(x, ', ')` is valid MySQL and means something
50 /// else entirely — each row's values run together, not the group's.
51 const STRING_AGG: StringAggSyntax = StringAggSyntax::Argument("group_concat");
52
53 /// Whether one bound parameter can be named from several places in a
54 /// statement. It follows from how the dialect spells a placeholder:
55 /// Postgres's `$N` names a parameter, so repeating `$1` is repeating one
56 /// value, while `?` *is* the next parameter, so a second one consumes a
57 /// second value. Naming one again keeps a statement's parameters down to
58 /// the values it actually holds, at the cost of a rendered text that
59 /// depends on which of them are equal: a driver caching prepared
60 /// statements by SQL text sees a bulk `INSERT` as one statement per
61 /// repetition pattern, as it already sees one per row count.
62 const PLACEHOLDERS_ARE_NUMBERED: bool = false;
63
64 /// Writes the placeholder for the `n`th bound parameter (1-indexed).
65 /// Postgres numbers them (`$1`, `$2`, ...); MySQL/SQLite are purely
66 /// positional (`?` every time, matched by order of appearance).
67 fn write_placeholder(n: usize, out: &mut String) {
68 let _ = n;
69 out.push('?');
70 }
71}
72
73/// Where a `string_agg` separator goes, and so whether it is a value or
74/// text. Two dialects take it as an ordinary argument, where it binds like
75/// any other value; MySQL's grammar takes a literal after a keyword and
76/// rejects a parameter there, which is the only reason this crate ever
77/// writes a value into SQL instead of handing it to the driver.
78#[derive(Debug, Clone, Copy)]
79pub enum StringAggSyntax {
80 /// `string_agg(x, $1)` / `group_concat(x, ?)`.
81 Argument(&'static str),
82 /// `group_concat(x SEPARATOR '...')`, the separator written out and
83 /// escaped. `backslash_escapes` says how: MySQL reads a backslash
84 /// inside a literal as an escape, so a lone one would carry the closing
85 /// quote away and has to be doubled. A session running
86 /// `NO_BACKSLASH_ESCAPES` reads the doubled pair as two backslashes —
87 /// a separator that isn't the one asked for, though still not a way out
88 /// of the literal, since a doubled quote escapes under either mode.
89 SeparatorKeyword {
90 func: &'static str,
91 backslash_escapes: bool,
92 },
93}
94
95#[derive(Debug, Clone, Copy, Default)]
96pub struct Postgres;
97impl private::Sealed for Postgres {}
98impl Dialect for Postgres {
99 const IDENTIFIER_QUOTE: char = '"';
100 const STRING_AGG: StringAggSyntax = StringAggSyntax::Argument("string_agg");
101 const PLACEHOLDERS_ARE_NUMBERED: bool = true;
102 fn write_placeholder(n: usize, out: &mut String) {
103 use std::fmt::Write as _;
104 let _ = write!(out, "${n}");
105 }
106}
107
108#[derive(Debug, Clone, Copy, Default)]
109pub struct MySql;
110impl private::Sealed for MySql {}
111impl Dialect for MySql {
112 const CAST_BIGINT: &'static str = "SIGNED";
113 const CAST_DOUBLE: &'static str = "DOUBLE";
114 const OFFSET_WITHOUT_LIMIT: Option<&'static str> = Some("18446744073709551615");
115 const IDENTIFIER_QUOTE: char = '`';
116 const INSERT_NO_COLUMNS: &'static str = " () VALUES ()";
117 const STRING_AGG: StringAggSyntax = StringAggSyntax::SeparatorKeyword {
118 func: "group_concat",
119 backslash_escapes: true,
120 };
121 // Uses the default `?` placeholder.
122}
123
124#[derive(Debug, Clone, Copy, Default)]
125pub struct Sqlite;
126impl private::Sealed for Sqlite {}
127impl Dialect for Sqlite {
128 const PARENTHESIZED_SET_OP_BRANCHES: bool = false;
129 const OFFSET_WITHOUT_LIMIT: Option<&'static str> = Some("-1");
130 const IDENTIFIER_QUOTE: char = '"';
131 // Uses the default `?` placeholder.
132}
133
134/// `RETURNING` support (Postgres, SQLite 3.35+). MySQL has no equivalent
135/// SQL construct at any version.
136#[diagnostic::on_unimplemented(
137 message = "`{Self}` has no `RETURNING`",
138 label = "Postgres and SQLite do; MySQL has no equivalent at any version",
139 note = "read the rows back with a second statement, or write the query for a dialect that has it"
140)]
141pub trait SupportsReturning: Dialect {}
142impl SupportsReturning for Postgres {}
143impl SupportsReturning for Sqlite {}
144
145/// `ON CONFLICT DO UPDATE/NOTHING` support (Postgres, SQLite). MySQL's
146/// differently-shaped `ON DUPLICATE KEY UPDATE` gets its own capability
147/// trait when it lands.
148#[diagnostic::on_unimplemented(
149 message = "`{Self}` has no `ON CONFLICT`",
150 label = "Postgres and SQLite do; MySQL spells upsert as `ON DUPLICATE KEY UPDATE`",
151 note = "that is a different clause, not a spelling of this one, so it isn't rendered from `.on_conflict_*(..)`"
152)]
153pub trait SupportsOnConflict: Dialect {}
154impl SupportsOnConflict for Postgres {}
155impl SupportsOnConflict for Sqlite {}
156
157/// A data-modifying statement as a CTE body — `WITH x AS (UPDATE ..
158/// RETURNING ..) SELECT ..` — which is Postgres's alone. SQLite and MySQL
159/// both take a `SELECT` there and nothing else.
160#[diagnostic::on_unimplemented(
161 message = "`{Self}` has no data-modifying CTE",
162 label = "only Postgres takes an `INSERT`/`UPDATE`/`DELETE` as a `WITH` body; the others take a `SELECT`",
163 note = "the write and the read it feeds are two statements there, in one transaction"
164)]
165pub trait SupportsDataModifyingCte: Dialect {}
166impl SupportsDataModifyingCte for Postgres {}
167
168/// `RIGHT JOIN` support. Every dialect this crate speaks has it (SQLite
169/// since 3.39, the minimum this crate targets), so this gate excludes
170/// nothing today — it is here so that `SupportsFullOuterJoin`, which MySQL
171/// genuinely lacks, is one capability rather than a pair of joins lumped
172/// together, and so that adding a dialect without `RIGHT JOIN` is a new
173/// impl rather than a change to `right_join`'s signature.
174#[diagnostic::on_unimplemented(
175 message = "`{Self}` has no `RIGHT JOIN`",
176 label = "swap the tables and use `.left_join(..)`, which every dialect has"
177)]
178pub trait SupportsRightJoin: Dialect {}
179impl SupportsRightJoin for Postgres {}
180impl SupportsRightJoin for MySql {}
181impl SupportsRightJoin for Sqlite {}
182
183/// `FULL JOIN` support: Postgres always, SQLite 3.39+, **not** MySQL at any
184/// version. The usual MySQL workaround is a `UNION` of `LEFT` and `RIGHT`
185/// joins — a different SQL shape, not something `.full_join()` should
186/// silently rewrite into.
187#[diagnostic::on_unimplemented(
188 message = "`{Self}` has no `FULL JOIN`",
189 label = "Postgres and SQLite do; MySQL's idiom is a `UNION` of a `LEFT` and a `RIGHT` join",
190 note = "that rewrite is a different query shape, so `.full_join(..)` doesn't do it silently"
191)]
192pub trait SupportsFullOuterJoin: Dialect {}
193impl SupportsFullOuterJoin for Postgres {}
194impl SupportsFullOuterJoin for Sqlite {}