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