1use crate::FieldType;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Dialect {
14 Postgres,
15 Sqlite,
16 MySql,
17}
18
19impl Dialect {
20 pub fn from_database(database: &str) -> Self {
23 if database.eq_ignore_ascii_case("sqlite") {
24 Dialect::Sqlite
25 } else if database.eq_ignore_ascii_case("mysql") {
26 Dialect::MySql
27 } else {
28 Dialect::Postgres
29 }
30 }
31
32 pub fn id_pk_ddl(self) -> &'static str {
37 match self {
38 Dialect::Postgres => "id UUID PRIMARY KEY DEFAULT gen_random_uuid()",
39 Dialect::Sqlite => "id TEXT PRIMARY KEY",
40 Dialect::MySql => "id BINARY(16) PRIMARY KEY",
41 }
42 }
43
44 pub fn app_generates_id(self) -> bool {
47 matches!(self, Dialect::Sqlite | Dialect::MySql)
48 }
49
50 pub fn supports_returning(self) -> bool {
54 !matches!(self, Dialect::MySql)
55 }
56
57 pub fn column_type(self, ty: FieldType) -> &'static str {
59 match self {
60 Dialect::Postgres => ty.sql_type(),
62 Dialect::Sqlite => match ty {
65 FieldType::String | FieldType::Uuid | FieldType::DateTime => "TEXT",
66 FieldType::Bool | FieldType::I32 | FieldType::I64 => "INTEGER",
67 FieldType::F64 => "REAL",
68 },
69 Dialect::MySql => match ty {
72 FieldType::String => "VARCHAR(255)",
73 FieldType::Uuid => "BINARY(16)",
74 FieldType::DateTime => "DATETIME",
75 FieldType::Bool => "BOOLEAN",
76 FieldType::I32 => "INT",
77 FieldType::I64 => "BIGINT",
78 FieldType::F64 => "DOUBLE",
79 },
80 }
81 }
82
83 pub fn timestamp_type_default(self) -> &'static str {
85 match self {
86 Dialect::Postgres => "TIMESTAMPTZ NOT NULL DEFAULT now()",
87 Dialect::Sqlite => "TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))",
89 Dialect::MySql => "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP",
90 }
91 }
92
93 pub fn now_expr(self) -> &'static str {
95 match self {
96 Dialect::Postgres => "now()",
97 Dialect::Sqlite => "strftime('%Y-%m-%dT%H:%M:%fZ','now')",
98 Dialect::MySql => "now()",
99 }
100 }
101
102 pub fn placeholder(self, n: usize) -> String {
105 match self {
106 Dialect::Postgres => format!("${n}"),
107 Dialect::Sqlite => format!("?{n}"),
108 Dialect::MySql => "?".to_string(),
109 }
110 }
111
112 pub fn pool_type(self) -> &'static str {
114 match self {
115 Dialect::Postgres => "PgPool",
116 Dialect::Sqlite => "SqlitePool",
117 Dialect::MySql => "MySqlPool",
118 }
119 }
120
121 pub fn pool_options(self) -> &'static str {
123 match self {
124 Dialect::Postgres => "PgPoolOptions",
125 Dialect::Sqlite => "SqlitePoolOptions",
126 Dialect::MySql => "MySqlPoolOptions",
127 }
128 }
129
130 pub fn sqlx_module(self) -> &'static str {
133 match self {
134 Dialect::Postgres => "postgres",
135 Dialect::Sqlite => "sqlite",
136 Dialect::MySql => "mysql",
137 }
138 }
139
140 pub fn sqlx_feature(self) -> &'static str {
142 match self {
143 Dialect::Postgres => "postgres",
144 Dialect::Sqlite => "sqlite",
145 Dialect::MySql => "mysql",
146 }
147 }
148
149 pub fn integrity_error_mapping(self) -> String {
158 let inner = match self {
159 Dialect::Postgres | Dialect::Sqlite => {
161 let (unique, fk) = if self == Dialect::Postgres {
162 ("23505", "23503")
163 } else {
164 ("2067", "787")
165 };
166 [
167 " match db.code().as_deref() {".to_string(),
168 format!(" Some(\"{unique}\") => return Error::Conflict, // unique violation"),
169 format!(" Some(\"{fk}\") => return Error::ForeignKey, // foreign-key violation"),
170 " _ => {}".to_string(),
171 " }".to_string(),
172 ]
173 .join("\n")
174 }
175 Dialect::MySql => [
176 " if let Some(mysql) = db.try_downcast_ref::<sqlx::mysql::MySqlDatabaseError>() {",
177 " match mysql.number() {",
178 " 1062 => return Error::Conflict, // duplicate key",
179 " 1451 | 1452 | 1216 | 1217 => return Error::ForeignKey, // foreign-key violation",
182 " _ => {}",
183 " }",
184 " }",
185 ]
186 .join("\n"),
187 };
188 format!(" if let sqlx::Error::Database(ref db) = error {{\n{inner}\n }}")
189 }
190
191 pub fn example_url(self, project: &str) -> String {
193 match self {
194 Dialect::Postgres => {
195 format!("postgres://postgres:postgres@localhost:5432/{project}")
196 }
197 Dialect::Sqlite => format!("sqlite://{project}.db?mode=rwc"),
198 Dialect::MySql => format!("mysql://root:root@localhost:3306/{project}"),
199 }
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn postgres_output_is_the_canonical_mapping() {
209 let d = Dialect::from_database("postgres");
210 assert_eq!(
211 d.id_pk_ddl(),
212 "id UUID PRIMARY KEY DEFAULT gen_random_uuid()"
213 );
214 assert_eq!(d.column_type(FieldType::String), "TEXT");
215 assert_eq!(d.column_type(FieldType::DateTime), "TIMESTAMPTZ");
216 assert_eq!(d.placeholder(3), "$3");
217 assert!(!d.app_generates_id());
218 assert_eq!(d.pool_type(), "PgPool");
219 }
220
221 #[test]
222 fn sqlite_maps_types_and_placeholders() {
223 let d = Dialect::from_database("sqlite");
224 assert_eq!(d.id_pk_ddl(), "id TEXT PRIMARY KEY");
225 assert_eq!(d.column_type(FieldType::Uuid), "TEXT");
226 assert_eq!(d.column_type(FieldType::Bool), "INTEGER");
227 assert_eq!(d.column_type(FieldType::F64), "REAL");
228 assert_eq!(d.placeholder(2), "?2");
229 assert!(d.app_generates_id());
230 assert_eq!(d.pool_type(), "SqlitePool");
231 assert_eq!(d.sqlx_feature(), "sqlite");
232 }
233
234 #[test]
235 fn mysql_uses_binary_uuid_positional_binds_and_no_returning() {
236 let d = Dialect::from_database("mysql");
237 assert_eq!(d.id_pk_ddl(), "id BINARY(16) PRIMARY KEY");
238 assert!(d.app_generates_id());
239 assert!(!d.supports_returning());
240 assert_eq!(d.column_type(FieldType::String), "VARCHAR(255)");
241 assert_eq!(d.column_type(FieldType::Uuid), "BINARY(16)");
242 assert_eq!(d.column_type(FieldType::DateTime), "DATETIME");
243 assert_eq!(d.column_type(FieldType::Bool), "BOOLEAN");
244 assert_eq!(d.placeholder(1), "?");
246 assert_eq!(d.placeholder(4), "?");
247 assert_eq!(d.pool_type(), "MySqlPool");
248 assert_eq!(d.sqlx_feature(), "mysql");
249 let mapping = d.integrity_error_mapping();
251 assert!(mapping.contains("MySqlDatabaseError"));
252 assert!(mapping.contains("1062 => return Error::Conflict"));
253 assert!(mapping.contains("1451 | 1452 | 1216 | 1217 => return Error::ForeignKey"));
254 }
255
256 #[test]
257 fn returning_dialects_map_codes_via_sqlstate() {
258 for d in [Dialect::Postgres, Dialect::Sqlite] {
260 assert!(d.supports_returning());
261 assert!(d.integrity_error_mapping().contains("db.code()"));
262 }
263 }
264
265 #[test]
266 fn defaults_to_postgres() {
267 assert_eq!(Dialect::from_database("mysql"), Dialect::MySql);
268 assert_eq!(Dialect::from_database("cockroach"), Dialect::Postgres);
269 assert_eq!(Dialect::from_database(""), Dialect::Postgres);
270 }
271}