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 {
157 let inner = match self {
158 Dialect::Postgres | Dialect::Sqlite => {
160 let (unique, fk) = if self == Dialect::Postgres {
161 ("23505", "23503")
162 } else {
163 ("2067", "787")
164 };
165 [
166 " match db.code().as_deref() {".to_string(),
167 format!(" Some(\"{unique}\") => return Error::Conflict, // unique violation"),
168 format!(" Some(\"{fk}\") => return Error::ForeignKey, // foreign-key violation"),
169 " _ => {}".to_string(),
170 " }".to_string(),
171 ]
172 .join("\n")
173 }
174 Dialect::MySql => [
175 " if let Some(mysql) = db.try_downcast_ref::<sqlx::mysql::MySqlDatabaseError>() {",
176 " match mysql.number() {",
177 " 1062 => return Error::Conflict, // duplicate key",
178 " 1452 => return Error::ForeignKey, // foreign-key violation",
179 " _ => {}",
180 " }",
181 " }",
182 ]
183 .join("\n"),
184 };
185 format!(" if let sqlx::Error::Database(ref db) = error {{\n{inner}\n }}")
186 }
187
188 pub fn example_url(self, project: &str) -> String {
190 match self {
191 Dialect::Postgres => {
192 format!("postgres://postgres:postgres@localhost:5432/{project}")
193 }
194 Dialect::Sqlite => format!("sqlite://{project}.db?mode=rwc"),
195 Dialect::MySql => format!("mysql://root:root@localhost:3306/{project}"),
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn postgres_output_is_the_canonical_mapping() {
206 let d = Dialect::from_database("postgres");
207 assert_eq!(
208 d.id_pk_ddl(),
209 "id UUID PRIMARY KEY DEFAULT gen_random_uuid()"
210 );
211 assert_eq!(d.column_type(FieldType::String), "TEXT");
212 assert_eq!(d.column_type(FieldType::DateTime), "TIMESTAMPTZ");
213 assert_eq!(d.placeholder(3), "$3");
214 assert!(!d.app_generates_id());
215 assert_eq!(d.pool_type(), "PgPool");
216 }
217
218 #[test]
219 fn sqlite_maps_types_and_placeholders() {
220 let d = Dialect::from_database("sqlite");
221 assert_eq!(d.id_pk_ddl(), "id TEXT PRIMARY KEY");
222 assert_eq!(d.column_type(FieldType::Uuid), "TEXT");
223 assert_eq!(d.column_type(FieldType::Bool), "INTEGER");
224 assert_eq!(d.column_type(FieldType::F64), "REAL");
225 assert_eq!(d.placeholder(2), "?2");
226 assert!(d.app_generates_id());
227 assert_eq!(d.pool_type(), "SqlitePool");
228 assert_eq!(d.sqlx_feature(), "sqlite");
229 }
230
231 #[test]
232 fn mysql_uses_binary_uuid_positional_binds_and_no_returning() {
233 let d = Dialect::from_database("mysql");
234 assert_eq!(d.id_pk_ddl(), "id BINARY(16) PRIMARY KEY");
235 assert!(d.app_generates_id());
236 assert!(!d.supports_returning());
237 assert_eq!(d.column_type(FieldType::String), "VARCHAR(255)");
238 assert_eq!(d.column_type(FieldType::Uuid), "BINARY(16)");
239 assert_eq!(d.column_type(FieldType::DateTime), "DATETIME");
240 assert_eq!(d.column_type(FieldType::Bool), "BOOLEAN");
241 assert_eq!(d.placeholder(1), "?");
243 assert_eq!(d.placeholder(4), "?");
244 assert_eq!(d.pool_type(), "MySqlPool");
245 assert_eq!(d.sqlx_feature(), "mysql");
246 let mapping = d.integrity_error_mapping();
248 assert!(mapping.contains("MySqlDatabaseError"));
249 assert!(mapping.contains("1062 => return Error::Conflict"));
250 assert!(mapping.contains("1452 => return Error::ForeignKey"));
251 }
252
253 #[test]
254 fn returning_dialects_map_codes_via_sqlstate() {
255 for d in [Dialect::Postgres, Dialect::Sqlite] {
257 assert!(d.supports_returning());
258 assert!(d.integrity_error_mapping().contains("db.code()"));
259 }
260 }
261
262 #[test]
263 fn defaults_to_postgres() {
264 assert_eq!(Dialect::from_database("mysql"), Dialect::MySql);
265 assert_eq!(Dialect::from_database("cockroach"), Dialect::Postgres);
266 assert_eq!(Dialect::from_database(""), Dialect::Postgres);
267 }
268}