Skip to main content

siderite_backends/sql/
dialect.rs

1//! SQL dialect abstraction: the small set of points where SQL flavours differ.
2
3use siderite_orm::{BackendCapabilities, SqlType};
4use std::fmt::Write;
5
6/// Differences between SQL flavours that the compiler needs to know about.
7pub trait Dialect: Send + Sync {
8    /// Declared capabilities; the compiler rejects plans that exceed them.
9    fn capabilities(&self) -> BackendCapabilities;
10
11    /// Write the placeholder for the `index`-th (1-based) bind parameter.
12    fn write_placeholder(&self, out: &mut String, index: usize);
13
14    /// Write a quoted identifier. Embedded quote characters are doubled.
15    fn write_ident(&self, out: &mut String, ident: &str) {
16        out.push('"');
17        for ch in ident.chars() {
18            if ch == '"' {
19                out.push('"');
20            }
21            out.push(ch);
22        }
23        out.push('"');
24    }
25
26    /// Type name used in `CAST(.. AS <name>)`.
27    fn cast_type(&self, ty: SqlType) -> &'static str;
28
29    /// Whether `OFFSET` requires a preceding `LIMIT`.
30    fn offset_requires_limit(&self) -> bool {
31        false
32    }
33
34    /// The `LIMIT` written before a bare `OFFSET` when
35    /// [`offset_requires_limit`](Self::offset_requires_limit) is set.
36    fn unbounded_limit(&self) -> &'static str {
37        "-1"
38    }
39
40    /// The SQL string literal used as the `LIKE .. ESCAPE` character (one
41    /// backslash).
42    fn like_escape_literal(&self) -> &'static str {
43        r"'\'"
44    }
45
46    /// The tail of an `INSERT INTO t` that stores one row of defaults.
47    fn default_values_sql(&self) -> &'static str {
48        " DEFAULT VALUES"
49    }
50
51    /// Whether aggregates accept `FILTER (WHERE ..)`. When not, the compiler
52    /// rewrites `AGG(x) FILTER (WHERE f)` as `AGG(CASE WHEN f THEN x END)`.
53    fn supports_aggregate_filter(&self) -> bool {
54        true
55    }
56
57    /// Whether `UPDATE .. SET a = .., b = ..` evaluates the assignments left
58    /// to right, so a later right-hand side sees the *new* value of an
59    /// earlier column (standard SQL reads the old values). The compiler then
60    /// reorders assignments to keep the standard meaning.
61    fn evaluates_assignments_in_order(&self) -> bool {
62        false
63    }
64}
65
66/// PostgreSQL dialect (`$1` placeholders, `ILIKE`, `~` regex).
67#[derive(Debug, Clone, Copy, Default)]
68pub struct Postgres;
69
70impl Dialect for Postgres {
71    fn capabilities(&self) -> BackendCapabilities {
72        BackendCapabilities::postgres()
73    }
74
75    fn write_placeholder(&self, out: &mut String, index: usize) {
76        // Writing to a String cannot fail.
77        let _ = write!(out, "${index}");
78    }
79
80    fn cast_type(&self, ty: SqlType) -> &'static str {
81        match ty {
82            SqlType::SmallInt => "SMALLINT",
83            SqlType::Integer => "INTEGER",
84            SqlType::BigInt | SqlType::Duration => "BIGINT",
85            SqlType::Real => "REAL",
86            SqlType::Double => "DOUBLE PRECISION",
87            SqlType::Decimal => "NUMERIC",
88            SqlType::Bool => "BOOLEAN",
89            SqlType::Binary => "BYTEA",
90            SqlType::Date => "DATE",
91            SqlType::Time => "TIME",
92            SqlType::Timestamp => "TIMESTAMPTZ",
93            SqlType::Uuid => "UUID",
94            SqlType::Json => "JSONB",
95            _ => "TEXT",
96        }
97    }
98}
99
100/// SQLite dialect (`?` placeholders, `LIMIT -1` before bare `OFFSET`).
101#[derive(Debug, Clone, Copy, Default)]
102pub struct Sqlite;
103
104impl Dialect for Sqlite {
105    fn capabilities(&self) -> BackendCapabilities {
106        BackendCapabilities::sqlite()
107    }
108
109    fn write_placeholder(&self, out: &mut String, _index: usize) {
110        out.push('?');
111    }
112
113    fn cast_type(&self, ty: SqlType) -> &'static str {
114        match ty {
115            SqlType::SmallInt
116            | SqlType::Integer
117            | SqlType::BigInt
118            | SqlType::Duration
119            | SqlType::Bool => "INTEGER",
120            SqlType::Real | SqlType::Double => "REAL",
121            SqlType::Decimal => "NUMERIC",
122            SqlType::Binary => "BLOB",
123            _ => "TEXT",
124        }
125    }
126
127    fn offset_requires_limit(&self) -> bool {
128        true
129    }
130}
131
132/// MySQL 8.0.31+ dialect: backtick identifiers, `?` placeholders,
133/// `REGEXP_LIKE`, `GROUP_CONCAT`, and no `RETURNING`.
134///
135/// The dialect declares `returning: false`, so compiling a plan that asks for
136/// `RETURNING` is a capability error; the MySQL adapter emulates it by
137/// stripping the clause and re-reading the rows.
138#[derive(Debug, Clone, Copy, Default)]
139pub struct MySql;
140
141impl Dialect for MySql {
142    fn capabilities(&self) -> BackendCapabilities {
143        BackendCapabilities {
144            returning: false,
145            ..BackendCapabilities::mysql()
146        }
147    }
148
149    fn write_placeholder(&self, out: &mut String, _index: usize) {
150        out.push('?');
151    }
152
153    fn write_ident(&self, out: &mut String, ident: &str) {
154        out.push('`');
155        for ch in ident.chars() {
156            if ch == '`' {
157                out.push('`');
158            }
159            out.push(ch);
160        }
161        out.push('`');
162    }
163
164    fn cast_type(&self, ty: SqlType) -> &'static str {
165        match ty {
166            SqlType::SmallInt
167            | SqlType::Integer
168            | SqlType::BigInt
169            | SqlType::Duration
170            | SqlType::Bool => "SIGNED",
171            SqlType::Real => "FLOAT",
172            SqlType::Double => "DOUBLE",
173            // A bare DECIMAL is DECIMAL(10,0) and would round to an integer.
174            SqlType::Decimal => "DECIMAL(38,10)",
175            SqlType::Binary => "BINARY",
176            SqlType::Date => "DATE",
177            SqlType::Time => "TIME(6)",
178            SqlType::Timestamp => "DATETIME(6)",
179            SqlType::Uuid => "CHAR(36)",
180            SqlType::Json => "JSON",
181            _ => "CHAR",
182        }
183    }
184
185    fn offset_requires_limit(&self) -> bool {
186        true
187    }
188
189    fn unbounded_limit(&self) -> &'static str {
190        "18446744073709551615"
191    }
192
193    fn like_escape_literal(&self) -> &'static str {
194        // In a MySQL string literal `\\` is one backslash; a lone `\'` would
195        // escape the closing quote.
196        r"'\\'"
197    }
198
199    fn default_values_sql(&self) -> &'static str {
200        " () VALUES ()"
201    }
202
203    fn supports_aggregate_filter(&self) -> bool {
204        false
205    }
206
207    fn evaluates_assignments_in_order(&self) -> bool {
208        true
209    }
210}