1use thiserror::Error;
2
3use crate::drift::{DriftPropertyDoc, DriftRegistry};
4use crate::migrations::Migration;
5use crate::operations::Operation;
6use crate::parsers::tokens::SqlTokenizer;
7use crate::states::types::EntityKind;
8use crate::states::{Schema, SchemaValidationError};
9
10#[derive(Debug, Error)]
11pub enum DialectError {
12 #[error("unsupported operation '{0}': {1}")]
13 Unsupported(String, String),
14}
15
16#[derive(Debug, Error, Clone, PartialEq, Eq)]
18pub enum DialectParseError {
19 #[error("database URL is empty")]
20 EmptyUrl,
21 #[error("database URL '{0}' does not contain a dialect scheme")]
22 MissingScheme(String),
23 #[error("unsupported database URL dialect scheme '{0}'")]
24 UnsupportedScheme(String),
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Dialect {
40 Postgres,
41 Sqlite,
42 Mysql,
43}
44
45pub(crate) trait DialectProcessor: Sync {
52 fn tokenizer(&self) -> &'static dyn SqlTokenizer;
53
54 fn migration_to_sql(
55 &self,
56 migration: &Migration,
57 start: &Schema,
58 ) -> Result<Vec<String>, DialectError>;
59
60 fn finalize_diff_operations(
61 &self,
62 ops: Vec<Operation>,
63 previous: &Schema,
64 current: &Schema,
65 ) -> Vec<Operation>;
66
67 fn should_merge(&self, table_name: &str, op: &Operation) -> bool;
68
69 fn canonicalize_schema_name(&self, object: EntityKind, schema: Option<&str>) -> Option<String>;
70
71 fn normalize_type<'a>(&self, t: &'a str) -> &'a str;
77
78 fn canonical_type(&self, t: &str) -> String;
84
85 fn type_comparison_key(&self, t: &str) -> String;
90
91 fn is_catalog_type(&self, t: &str) -> bool;
92
93 fn type_suggestions(&self, t: &str) -> Vec<String>;
94
95 fn validate_schema(&self, schema: &Schema) -> Result<(), SchemaValidationError>;
96
97 fn validate_migration(&self, migration: &Migration) -> Result<(), DialectError>;
98
99 fn validate_migration_with_state(
100 &self,
101 migration: &Migration,
102 start: &Schema,
103 ) -> Result<(), DialectError>;
104
105 fn drift_registry(&self) -> &'static DriftRegistry;
106
107 fn normalize_inspected_schema(&self, schema: Schema) -> Result<Schema, SchemaValidationError>;
108
109 fn default_expressions_equal(&self, left: &str, right: &str) -> bool {
110 crate::parsers::tokens::expressions_equal(self.tokenizer(), left, right)
111 }
112}
113
114impl Dialect {
115 pub fn as_str(&self) -> &'static str {
117 match self {
118 Self::Postgres => "postgres",
119 Self::Sqlite => "sqlite",
120 Self::Mysql => "mysql",
121 }
122 }
123
124 pub fn parse(value: &str) -> Option<Self> {
126 if value.eq_ignore_ascii_case("postgres") || value.eq_ignore_ascii_case("postgresql") {
127 Some(Self::Postgres)
128 } else if value.eq_ignore_ascii_case("sqlite") || value.eq_ignore_ascii_case("sqlite3") {
129 Some(Self::Sqlite)
130 } else if value.eq_ignore_ascii_case("mysql") || value.eq_ignore_ascii_case("mariadb") {
131 Some(Self::Mysql)
132 } else {
133 None
134 }
135 }
136
137 pub fn parse_from_url(database_url: &str) -> Result<Self, DialectParseError> {
143 let value = database_url.trim();
144 if value.is_empty() {
145 return Err(DialectParseError::EmptyUrl);
146 }
147
148 let scheme = if value.starts_with("sqlite:") {
149 "sqlite"
150 } else if let Some((scheme, _)) = value.split_once("://") {
151 scheme
152 } else if let Some((scheme, _)) = value.split_once(':') {
153 scheme
154 } else {
155 return Err(DialectParseError::MissingScheme(value.to_string()));
156 };
157
158 Self::parse(scheme).ok_or_else(|| DialectParseError::UnsupportedScheme(scheme.to_string()))
159 }
160
161 pub(crate) fn processor(&self) -> &'static dyn DialectProcessor {
162 match self {
163 Dialect::Postgres => &postgres::POSTGRES,
164 Dialect::Sqlite => &sqlite::SQLITE,
165 Dialect::Mysql => &mysql::MYSQL,
166 }
167 }
168
169 pub(crate) fn tokenizer(&self) -> &'static dyn SqlTokenizer {
170 self.processor().tokenizer()
171 }
172
173 pub(crate) fn default_expressions_equal(&self, left: &str, right: &str) -> bool {
174 self.processor().default_expressions_equal(left, right)
175 }
176
177 #[doc(hidden)]
178 pub fn migration_to_sql(
179 &self,
180 migration: &Migration,
181 _start: &Schema,
182 ) -> Result<Vec<String>, DialectError> {
183 self.processor().migration_to_sql(migration, _start)
184 }
185
186 pub fn reorder(
190 &self,
191 ops: Vec<Operation>,
192 previous: &Schema,
193 current: &Schema,
194 ) -> Vec<Operation> {
195 self.processor()
196 .finalize_diff_operations(ops, previous, current)
197 }
198
199 pub fn should_merge(&self, _table_name: &str, _op: &Operation) -> bool {
203 self.processor().should_merge(_table_name, _op)
204 }
205
206 #[doc(hidden)]
207 pub fn canonicalize_schema_name(
208 &self,
209 object: EntityKind,
210 schema: Option<&str>,
211 ) -> Option<String> {
212 self.processor().canonicalize_schema_name(object, schema)
213 }
214
215 pub fn normalize_type<'a>(&self, t: &'a str) -> &'a str {
216 self.processor().normalize_type(t)
217 }
218
219 pub fn canonical_type(&self, t: &str) -> String {
220 self.processor().canonical_type(t)
221 }
222
223 #[doc(hidden)]
224 pub fn type_comparison_key(&self, t: &str) -> String {
225 self.processor().type_comparison_key(t)
226 }
227
228 pub fn is_catalog_type(&self, t: &str) -> bool {
229 self.processor().is_catalog_type(t)
230 }
231
232 pub fn type_suggestions(&self, t: &str) -> Vec<String> {
233 self.processor().type_suggestions(t)
234 }
235
236 pub fn validate_schema(&self, schema: &Schema) -> Result<(), SchemaValidationError> {
237 self.processor().validate_schema(schema)
238 }
239
240 pub fn normalize_inspected_schema(
242 &self,
243 schema: Schema,
244 ) -> Result<Schema, SchemaValidationError> {
245 self.processor().normalize_inspected_schema(schema)
246 }
247
248 pub fn drift_contract(&self) -> &'static [DriftPropertyDoc] {
250 crate::drift::contract_for(*self)
251 }
252
253 pub(crate) fn drift_registry(&self) -> &'static DriftRegistry {
255 self.processor().drift_registry()
256 }
257
258 pub fn validate_migration(&self, m: &Migration) -> Result<(), DialectError> {
259 self.processor().validate_migration(m)
260 }
261
262 #[doc(hidden)]
263 pub fn validate_migration_with_state(
264 &self,
265 m: &Migration,
266 _start: &Schema,
267 ) -> Result<(), DialectError> {
268 self.processor().validate_migration_with_state(m, _start)
269 }
270}
271
272mod mysql;
273mod postgres;
274mod sqlite;
275
276#[cfg(test)]
277mod tests {
278 use super::{Dialect, DialectParseError};
279
280 #[test]
282 fn parse_from_url_accepts_postgres_urls() {
283 assert_eq!(
284 Dialect::parse_from_url("postgres://localhost/app"),
285 Ok(Dialect::Postgres)
286 );
287 assert_eq!(
288 Dialect::parse_from_url("postgresql://localhost/app"),
289 Ok(Dialect::Postgres)
290 );
291 }
292
293 #[test]
295 fn parse_from_url_accepts_sqlite_urls() {
296 assert_eq!(
297 Dialect::parse_from_url("sqlite://app.db"),
298 Ok(Dialect::Sqlite)
299 );
300 assert_eq!(
301 Dialect::parse_from_url("sqlite::memory:"),
302 Ok(Dialect::Sqlite)
303 );
304 }
305
306 #[test]
308 fn parse_from_url_accepts_mysql_urls() {
309 assert_eq!(
310 Dialect::parse_from_url("mysql://localhost/app"),
311 Ok(Dialect::Mysql)
312 );
313 assert_eq!(
314 Dialect::parse_from_url("mariadb://localhost/app"),
315 Ok(Dialect::Mysql)
316 );
317 }
318
319 #[test]
321 fn parse_from_url_rejects_unsupported_schemes() {
322 assert_eq!(
323 Dialect::parse_from_url("oracle://localhost/app"),
324 Err(DialectParseError::UnsupportedScheme("oracle".to_string()))
325 );
326 }
327
328 #[test]
330 fn parse_from_url_rejects_missing_schemes() {
331 assert_eq!(
332 Dialect::parse_from_url("localhost/app"),
333 Err(DialectParseError::MissingScheme(
334 "localhost/app".to_string()
335 ))
336 );
337 }
338}