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, running each row's values together rather than the
51 /// group's.
52 const STRING_AGG: StringAggSyntax = StringAggSyntax::Argument("group_concat");
53
54 /// Whether one bound parameter can be named from several places in a
55 /// statement. It follows from how the dialect spells a placeholder:
56 /// Postgres's `$N` names a parameter, so repeating `$1` is repeating one
57 /// value, while `?` *is* the next parameter, so a second one consumes a
58 /// second value. Naming one again keeps a statement's parameters down to
59 /// the values it actually holds. The cost is a rendered text that depends
60 /// on which of those values are equal. A driver caching prepared
61 /// statements by SQL text then sees a bulk `INSERT` as one statement per
62 /// repetition pattern, as it already sees one per row count.
63 const PLACEHOLDERS_ARE_NUMBERED: bool = false;
64
65 /// Writes the placeholder for the `n`th bound parameter (1-indexed).
66 /// Postgres numbers them (`$1`, `$2`, ...); MySQL/SQLite are purely
67 /// positional (`?` every time, matched by order of appearance).
68 fn write_placeholder(n: usize, out: &mut String) {
69 let _ = n;
70 out.push('?');
71 }
72}
73
74/// Where a `string_agg` separator goes, and so whether it is a value or
75/// text. Two dialects take it as an ordinary argument, where it binds like
76/// any other value. MySQL's grammar takes a literal after a keyword and
77/// rejects a parameter there, which is the only reason this crate ever
78/// writes a value into SQL instead of handing it to the driver.
79#[derive(Debug, Clone, Copy)]
80pub enum StringAggSyntax {
81 /// `string_agg(x, $1)` / `group_concat(x, ?)`.
82 Argument(&'static str),
83 /// `group_concat(x SEPARATOR '...')`, the separator written out and
84 /// escaped. `backslash_escapes` says how: MySQL reads a backslash
85 /// inside a literal as an escape, so a lone one would carry the closing
86 /// quote away and has to be doubled. A session running
87 /// `NO_BACKSLASH_ESCAPES` reads the doubled pair as two backslashes,
88 /// which is not the separator asked for. The doubled pair is still no
89 /// way out of the literal, since a doubled quote escapes under either
90 /// mode.
91 SeparatorKeyword {
92 func: &'static str,
93 backslash_escapes: bool,
94 },
95}
96
97#[derive(Debug, Clone, Copy, Default)]
98pub struct Postgres;
99impl private::Sealed for Postgres {}
100impl Dialect for Postgres {
101 const IDENTIFIER_QUOTE: char = '"';
102 const STRING_AGG: StringAggSyntax = StringAggSyntax::Argument("string_agg");
103 const PLACEHOLDERS_ARE_NUMBERED: bool = true;
104 fn write_placeholder(n: usize, out: &mut String) {
105 use std::fmt::Write as _;
106 let _ = write!(out, "${n}");
107 }
108}
109
110#[derive(Debug, Clone, Copy, Default)]
111pub struct MySql;
112impl private::Sealed for MySql {}
113impl Dialect for MySql {
114 const CAST_BIGINT: &'static str = "SIGNED";
115 const CAST_DOUBLE: &'static str = "DOUBLE";
116 const OFFSET_WITHOUT_LIMIT: Option<&'static str> = Some("18446744073709551615");
117 const IDENTIFIER_QUOTE: char = '`';
118 const INSERT_NO_COLUMNS: &'static str = " () VALUES ()";
119 const STRING_AGG: StringAggSyntax = StringAggSyntax::SeparatorKeyword {
120 func: "group_concat",
121 backslash_escapes: true,
122 };
123 // Uses the default `?` placeholder.
124}
125
126#[derive(Debug, Clone, Copy, Default)]
127pub struct Sqlite;
128impl private::Sealed for Sqlite {}
129impl Dialect for Sqlite {
130 const PARENTHESIZED_SET_OP_BRANCHES: bool = false;
131 const OFFSET_WITHOUT_LIMIT: Option<&'static str> = Some("-1");
132 const IDENTIFIER_QUOTE: char = '"';
133 // Uses the default `?` placeholder.
134}
135
136/// `RETURNING` support (Postgres, SQLite 3.35+). MySQL has no equivalent
137/// SQL construct at any version.
138#[diagnostic::on_unimplemented(
139 message = "`{Self}` has no `RETURNING`",
140 label = "Postgres and SQLite do; MySQL has no equivalent at any version",
141 note = "read the rows back with a second statement, or write the query for a dialect that has it"
142)]
143pub trait SupportsReturning: Dialect {}
144impl SupportsReturning for Postgres {}
145impl SupportsReturning for Sqlite {}
146
147/// `ON CONFLICT DO UPDATE/NOTHING` support (Postgres, SQLite). MySQL's
148/// differently-shaped `ON DUPLICATE KEY UPDATE` gets its own capability
149/// trait when it lands.
150#[diagnostic::on_unimplemented(
151 message = "`{Self}` has no `ON CONFLICT`",
152 label = "Postgres and SQLite do; MySQL spells upsert as `ON DUPLICATE KEY UPDATE`",
153 note = "that is a different clause, not a spelling of this one, so it isn't rendered from `.on_conflict_*(..)`"
154)]
155pub trait SupportsOnConflict: Dialect {}
156impl SupportsOnConflict for Postgres {}
157impl SupportsOnConflict for Sqlite {}
158
159/// A data-modifying statement as a CTE body (`WITH x AS (UPDATE ..
160/// RETURNING ..) SELECT ..`), which is Postgres's alone. SQLite and MySQL
161/// both take a `SELECT` there and nothing else.
162#[diagnostic::on_unimplemented(
163 message = "`{Self}` has no data-modifying CTE",
164 label = "only Postgres takes an `INSERT`/`UPDATE`/`DELETE` as a `WITH` body; the others take a `SELECT`",
165 note = "the write and the read it feeds are two statements there, in one transaction"
166)]
167pub trait SupportsDataModifyingCte: Dialect {}
168impl SupportsDataModifyingCte for Postgres {}
169
170/// `RIGHT JOIN` support. Every dialect this crate speaks has it (SQLite
171/// since 3.39, the minimum this crate targets), so this gate excludes
172/// nothing today. It is here for two reasons. `SupportsFullOuterJoin`, the
173/// capability MySQL genuinely lacks, stays a capability of its own rather
174/// than a pair of joins lumped together. And adding a dialect without
175/// `RIGHT JOIN` stays a new impl rather than a change to `right_join`'s
176/// signature.
177#[diagnostic::on_unimplemented(
178 message = "`{Self}` has no `RIGHT JOIN`",
179 label = "swap the tables and use `.left_join(..)`, which every dialect has"
180)]
181pub trait SupportsRightJoin: Dialect {}
182impl SupportsRightJoin for Postgres {}
183impl SupportsRightJoin for MySql {}
184impl SupportsRightJoin for Sqlite {}
185
186/// `FULL JOIN` support: Postgres always, SQLite 3.39+, **not** MySQL at any
187/// version. The usual MySQL workaround is a `UNION` of `LEFT` and `RIGHT`
188/// joins. That is a different SQL shape, not something `.full_join()`
189/// should silently rewrite into.
190#[diagnostic::on_unimplemented(
191 message = "`{Self}` has no `FULL JOIN`",
192 label = "Postgres and SQLite do; MySQL's idiom is a `UNION` of a `LEFT` and a `RIGHT` join",
193 note = "that rewrite is a different query shape, so `.full_join(..)` doesn't do it silently"
194)]
195pub trait SupportsFullOuterJoin: Dialect {}
196impl SupportsFullOuterJoin for Postgres {}
197impl SupportsFullOuterJoin for Sqlite {}