Skip to main content

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 {}