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