1use crate::FieldType;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Dialect {
14 Postgres,
15 Sqlite,
16 MySql,
17}
18
19impl std::fmt::Display for Dialect {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 f.write_str(match self {
22 Dialect::Postgres => "PostgreSQL",
23 Dialect::Sqlite => "SQLite",
24 Dialect::MySql => "MySQL",
25 })
26 }
27}
28
29impl Dialect {
30 pub fn from_database(database: &str) -> Self {
33 if database.eq_ignore_ascii_case("sqlite") {
34 Dialect::Sqlite
35 } else if database.eq_ignore_ascii_case("mysql") {
36 Dialect::MySql
37 } else {
38 Dialect::Postgres
39 }
40 }
41
42 pub fn id_pk_ddl(self) -> &'static str {
47 match self {
48 Dialect::Postgres => "id UUID PRIMARY KEY DEFAULT gen_random_uuid()",
49 Dialect::Sqlite => "id TEXT PRIMARY KEY",
50 Dialect::MySql => "id BINARY(16) PRIMARY KEY",
51 }
52 }
53
54 pub fn app_generates_id(self) -> bool {
57 matches!(self, Dialect::Sqlite | Dialect::MySql)
58 }
59
60 pub fn supports_returning(self) -> bool {
64 !matches!(self, Dialect::MySql)
65 }
66
67 pub fn column_type(self, ty: FieldType) -> &'static str {
69 match self {
70 Dialect::Postgres => ty.sql_type(),
72 Dialect::Sqlite => match ty {
75 FieldType::String | FieldType::Uuid | FieldType::DateTime => "TEXT",
76 FieldType::Bool | FieldType::I32 | FieldType::I64 => "INTEGER",
77 FieldType::F64 => "REAL",
78 },
79 Dialect::MySql => match ty {
82 FieldType::String => "VARCHAR(255)",
83 FieldType::Uuid => "BINARY(16)",
84 FieldType::DateTime => "DATETIME",
85 FieldType::Bool => "BOOLEAN",
86 FieldType::I32 => "INT",
87 FieldType::I64 => "BIGINT",
88 FieldType::F64 => "DOUBLE",
89 },
90 }
91 }
92
93 pub fn timestamp_type_default(self) -> &'static str {
95 match self {
96 Dialect::Postgres => "TIMESTAMPTZ NOT NULL DEFAULT now()",
97 Dialect::Sqlite => "TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))",
99 Dialect::MySql => "DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)",
102 }
103 }
104
105 pub fn now_expr(self) -> &'static str {
107 match self {
108 Dialect::Postgres => "now()",
109 Dialect::Sqlite => "strftime('%Y-%m-%dT%H:%M:%fZ','now')",
110 Dialect::MySql => "NOW(6)",
111 }
112 }
113
114 pub fn placeholder(self, n: usize) -> String {
117 match self {
118 Dialect::Postgres => format!("${n}"),
119 Dialect::Sqlite => format!("?{n}"),
120 Dialect::MySql => "?".to_string(),
121 }
122 }
123
124 pub fn pool_type(self) -> &'static str {
126 match self {
127 Dialect::Postgres => "PgPool",
128 Dialect::Sqlite => "SqlitePool",
129 Dialect::MySql => "MySqlPool",
130 }
131 }
132
133 pub fn pool_options(self) -> &'static str {
135 match self {
136 Dialect::Postgres => "PgPoolOptions",
137 Dialect::Sqlite => "SqlitePoolOptions",
138 Dialect::MySql => "MySqlPoolOptions",
139 }
140 }
141
142 pub fn sqlx_module(self) -> &'static str {
145 match self {
146 Dialect::Postgres => "postgres",
147 Dialect::Sqlite => "sqlite",
148 Dialect::MySql => "mysql",
149 }
150 }
151
152 pub fn sqlx_feature(self) -> &'static str {
154 match self {
155 Dialect::Postgres => "postgres",
156 Dialect::Sqlite => "sqlite",
157 Dialect::MySql => "mysql",
158 }
159 }
160
161 pub fn integrity_error_mapping(self) -> String {
170 let inner = match self {
171 Dialect::Postgres | Dialect::Sqlite => {
173 let (unique, fk) = if self == Dialect::Postgres {
174 ("23505", "23503")
175 } else {
176 ("2067", "787")
177 };
178 [
179 " match db.code().as_deref() {".to_string(),
180 format!(" Some(\"{unique}\") => return Error::Conflict, // unique violation"),
181 format!(" Some(\"{fk}\") => return Error::ForeignKey, // foreign-key violation"),
182 " _ => {}".to_string(),
183 " }".to_string(),
184 ]
185 .join("\n")
186 }
187 Dialect::MySql => [
188 " if let Some(mysql) = db.try_downcast_ref::<sqlx::mysql::MySqlDatabaseError>() {",
189 " match mysql.number() {",
190 " 1062 => return Error::Conflict, // duplicate key",
191 " 1451 | 1452 | 1216 | 1217 => return Error::ForeignKey, // foreign-key violation",
194 " _ => {}",
195 " }",
196 " }",
197 ]
198 .join("\n"),
199 };
200 format!(" if let sqlx::Error::Database(ref db) = error {{\n{inner}\n }}")
201 }
202
203 pub fn example_url(self, project: &str) -> String {
205 match self {
206 Dialect::Postgres => {
207 format!("postgres://postgres:postgres@localhost:5432/{project}")
208 }
209 Dialect::Sqlite => format!("sqlite://{project}.db?mode=rwc"),
210 Dialect::MySql => format!("mysql://root:root@localhost:3306/{project}"),
211 }
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn postgres_output_is_the_canonical_mapping() {
221 let d = Dialect::from_database("postgres");
222 assert_eq!(
223 d.id_pk_ddl(),
224 "id UUID PRIMARY KEY DEFAULT gen_random_uuid()"
225 );
226 assert_eq!(d.column_type(FieldType::String), "TEXT");
227 assert_eq!(d.column_type(FieldType::DateTime), "TIMESTAMPTZ");
228 assert_eq!(d.placeholder(3), "$3");
229 assert!(!d.app_generates_id());
230 assert_eq!(d.pool_type(), "PgPool");
231 }
232
233 #[test]
234 fn sqlite_maps_types_and_placeholders() {
235 let d = Dialect::from_database("sqlite");
236 assert_eq!(d.id_pk_ddl(), "id TEXT PRIMARY KEY");
237 assert_eq!(d.column_type(FieldType::Uuid), "TEXT");
238 assert_eq!(d.column_type(FieldType::Bool), "INTEGER");
239 assert_eq!(d.column_type(FieldType::F64), "REAL");
240 assert_eq!(d.placeholder(2), "?2");
241 assert!(d.app_generates_id());
242 assert_eq!(d.pool_type(), "SqlitePool");
243 assert_eq!(d.sqlx_feature(), "sqlite");
244 }
245
246 #[test]
247 fn mysql_uses_binary_uuid_positional_binds_and_no_returning() {
248 let d = Dialect::from_database("mysql");
249 assert_eq!(d.id_pk_ddl(), "id BINARY(16) PRIMARY KEY");
250 assert!(d.app_generates_id());
251 assert!(!d.supports_returning());
252 assert_eq!(d.column_type(FieldType::String), "VARCHAR(255)");
253 assert_eq!(d.column_type(FieldType::Uuid), "BINARY(16)");
254 assert_eq!(d.column_type(FieldType::DateTime), "DATETIME");
255 assert_eq!(d.column_type(FieldType::Bool), "BOOLEAN");
256 assert_eq!(d.placeholder(1), "?");
258 assert_eq!(d.placeholder(4), "?");
259 assert_eq!(d.pool_type(), "MySqlPool");
260 assert_eq!(d.sqlx_feature(), "mysql");
261 let mapping = d.integrity_error_mapping();
263 assert!(mapping.contains("MySqlDatabaseError"));
264 assert!(mapping.contains("1062 => return Error::Conflict"));
265 assert!(mapping.contains("1451 | 1452 | 1216 | 1217 => return Error::ForeignKey"));
266 }
267
268 #[test]
269 fn returning_dialects_map_codes_via_sqlstate() {
270 for d in [Dialect::Postgres, Dialect::Sqlite] {
272 assert!(d.supports_returning());
273 assert!(d.integrity_error_mapping().contains("db.code()"));
274 }
275 }
276
277 #[test]
278 fn defaults_to_postgres() {
279 assert_eq!(Dialect::from_database("mysql"), Dialect::MySql);
280 assert_eq!(Dialect::from_database("cockroach"), Dialect::Postgres);
281 assert_eq!(Dialect::from_database(""), Dialect::Postgres);
282 }
283}