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 /// Writes the placeholder for the `n`th bound parameter (1-indexed).
47 /// Postgres numbers them (`$1`, `$2`, ...); MySQL/SQLite are purely
48 /// positional (`?` every time, matched by order of appearance).
49 fn write_placeholder(n: usize, out: &mut String) {
50 let _ = n;
51 out.push('?');
52 }
53}
54
55#[derive(Debug, Clone, Copy, Default)]
56pub struct Postgres;
57impl private::Sealed for Postgres {}
58impl Dialect for Postgres {
59 const IDENTIFIER_QUOTE: char = '"';
60 fn write_placeholder(n: usize, out: &mut String) {
61 use std::fmt::Write as _;
62 let _ = write!(out, "${n}");
63 }
64}
65
66#[derive(Debug, Clone, Copy, Default)]
67pub struct MySql;
68impl private::Sealed for MySql {}
69impl Dialect for MySql {
70 const CAST_BIGINT: &'static str = "SIGNED";
71 const CAST_DOUBLE: &'static str = "DOUBLE";
72 const OFFSET_WITHOUT_LIMIT: Option<&'static str> = Some("18446744073709551615");
73 const IDENTIFIER_QUOTE: char = '`';
74 const INSERT_NO_COLUMNS: &'static str = " () VALUES ()";
75 // Uses the default `?` placeholder.
76}
77
78#[derive(Debug, Clone, Copy, Default)]
79pub struct Sqlite;
80impl private::Sealed for Sqlite {}
81impl Dialect for Sqlite {
82 const PARENTHESIZED_SET_OP_BRANCHES: bool = false;
83 const OFFSET_WITHOUT_LIMIT: Option<&'static str> = Some("-1");
84 const IDENTIFIER_QUOTE: char = '"';
85 // Uses the default `?` placeholder.
86}
87
88/// `RETURNING` support (Postgres, SQLite 3.35+). MySQL has no equivalent
89/// SQL construct at any version.
90#[diagnostic::on_unimplemented(
91 message = "`{Self}` has no `RETURNING`",
92 label = "Postgres and SQLite do; MySQL has no equivalent at any version",
93 note = "read the rows back with a second statement, or write the query for a dialect that has it"
94)]
95pub trait SupportsReturning: Dialect {}
96impl SupportsReturning for Postgres {}
97impl SupportsReturning for Sqlite {}
98
99/// `ON CONFLICT DO UPDATE/NOTHING` support (Postgres, SQLite). MySQL's
100/// differently-shaped `ON DUPLICATE KEY UPDATE` gets its own capability
101/// trait when it lands.
102#[diagnostic::on_unimplemented(
103 message = "`{Self}` has no `ON CONFLICT`",
104 label = "Postgres and SQLite do; MySQL spells upsert as `ON DUPLICATE KEY UPDATE`",
105 note = "that is a different clause, not a spelling of this one, so it isn't rendered from `.on_conflict_*(..)`"
106)]
107pub trait SupportsOnConflict: Dialect {}
108impl SupportsOnConflict for Postgres {}
109impl SupportsOnConflict for Sqlite {}
110
111/// `RIGHT JOIN` support. Every dialect this crate speaks has it (SQLite
112/// since 3.39, the minimum this crate targets), so this gate excludes
113/// nothing today — it is here so that `SupportsFullOuterJoin`, which MySQL
114/// genuinely lacks, is one capability rather than a pair of joins lumped
115/// together, and so that adding a dialect without `RIGHT JOIN` is a new
116/// impl rather than a change to `right_join`'s signature.
117#[diagnostic::on_unimplemented(
118 message = "`{Self}` has no `RIGHT JOIN`",
119 label = "swap the tables and use `.left_join(..)`, which every dialect has"
120)]
121pub trait SupportsRightJoin: Dialect {}
122impl SupportsRightJoin for Postgres {}
123impl SupportsRightJoin for MySql {}
124impl SupportsRightJoin for Sqlite {}
125
126/// `FULL JOIN` support: Postgres always, SQLite 3.39+, **not** MySQL at any
127/// version. The usual MySQL workaround is a `UNION` of `LEFT` and `RIGHT`
128/// joins — a different SQL shape, not something `.full_join()` should
129/// silently rewrite into.
130#[diagnostic::on_unimplemented(
131 message = "`{Self}` has no `FULL JOIN`",
132 label = "Postgres and SQLite do; MySQL's idiom is a `UNION` of a `LEFT` and a `RIGHT` join",
133 note = "that rewrite is a different query shape, so `.full_join(..)` doesn't do it silently"
134)]
135pub trait SupportsFullOuterJoin: Dialect {}
136impl SupportsFullOuterJoin for Postgres {}
137impl SupportsFullOuterJoin for Sqlite {}