Skip to main content

rust_ef/migration/
dialect.rs

1//! SQL dialect enumeration and type mapping for migration DDL generation.
2
3use super::types::SnapshotColumn;
4
5/// Specifies the database SQL dialect for migration generation.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum MigrationDialect {
8    Postgres,
9    MySql,
10    Sqlite,
11}
12
13impl MigrationDialect {
14    /// Quote an identifier according to dialect rules.
15    pub fn quote(&self, ident: &str) -> String {
16        match self {
17            MigrationDialect::Postgres | MigrationDialect::Sqlite => format!("\"{}\"", ident),
18            MigrationDialect::MySql => format!("`{}`", ident),
19        }
20    }
21
22    /// Map a Rust type name to the dialect-specific column type.
23    pub fn map_column_type(&self, col: &SnapshotColumn) -> String {
24        // type_name comes from std::any::type_name::<T>() which returns
25        // fully-qualified paths (e.g. "alloc::string::String"). Use ends_with
26        // / contains matching to handle both simple and qualified names.
27        let tn = col.type_name.as_str();
28
29        // Auto-increment handling (must be checked before plain i32/i64)
30        if col.is_auto_increment {
31            if tn.ends_with("i32") {
32                return match self {
33                    MigrationDialect::Postgres => "SERIAL".into(),
34                    MigrationDialect::MySql => "INT AUTO_INCREMENT".into(),
35                    MigrationDialect::Sqlite => "INTEGER".into(),
36                };
37            }
38            if tn.ends_with("i64") {
39                return match self {
40                    MigrationDialect::Postgres => "BIGSERIAL".into(),
41                    MigrationDialect::MySql => "BIGINT AUTO_INCREMENT".into(),
42                    MigrationDialect::Sqlite => "INTEGER".into(),
43                };
44            }
45        }
46
47        // Sequence handling (PostgreSQL: plain type + DEFAULT nextval in DDL;
48        // non-PG: fall back to auto_increment syntax)
49        if col.is_sequence {
50            if tn.ends_with("i32") {
51                return match self {
52                    MigrationDialect::Postgres => "INTEGER".into(),
53                    MigrationDialect::MySql => "INT AUTO_INCREMENT".into(),
54                    MigrationDialect::Sqlite => "INTEGER".into(),
55                };
56            }
57            if tn.ends_with("i64") {
58                return match self {
59                    MigrationDialect::Postgres => "BIGINT".into(),
60                    MigrationDialect::MySql => "BIGINT AUTO_INCREMENT".into(),
61                    MigrationDialect::Sqlite => "INTEGER".into(),
62                };
63            }
64        }
65
66        let base: &str = if tn.ends_with("i16") {
67            "SMALLINT"
68        } else if tn.ends_with("i32") {
69            "INTEGER"
70        } else if tn.ends_with("i64") {
71            "BIGINT"
72        } else if tn.ends_with("f32") {
73            "REAL"
74        } else if tn.ends_with("f64") {
75            "DOUBLE PRECISION"
76        } else if tn.ends_with("bool") {
77            "BOOLEAN"
78        } else if tn.ends_with("String") {
79            return match col.max_length {
80                Some(n) => format!("VARCHAR({})", n),
81                None => "TEXT".into(),
82            };
83        } else if tn.ends_with("Vec<u8>") {
84            return match self {
85                MigrationDialect::Postgres => "BYTEA".into(),
86                MigrationDialect::MySql | MigrationDialect::Sqlite => "BLOB".into(),
87            };
88        } else if tn.contains("NaiveDateTime") {
89            return match self {
90                MigrationDialect::Postgres => "TIMESTAMP".into(),
91                MigrationDialect::MySql => "DATETIME".into(),
92                MigrationDialect::Sqlite => "TEXT".into(),
93            };
94        } else if tn.contains("NaiveDate") {
95            return match self {
96                MigrationDialect::Postgres => "DATE".into(),
97                MigrationDialect::MySql => "DATE".into(),
98                MigrationDialect::Sqlite => "TEXT".into(),
99            };
100        } else if tn.contains("DateTime") {
101            // chrono::DateTime<Utc> → TIMESTAMPTZ (PG) / DATETIME (MySQL) / TEXT (SQLite)
102            return match self {
103                MigrationDialect::Postgres => "TIMESTAMPTZ".into(),
104                MigrationDialect::MySql => "DATETIME".into(),
105                MigrationDialect::Sqlite => "TEXT".into(),
106            };
107        } else if tn.contains("Uuid") {
108            return match self {
109                MigrationDialect::Postgres => "UUID".into(),
110                MigrationDialect::MySql => "CHAR(36)".into(),
111                MigrationDialect::Sqlite => "TEXT".into(),
112            };
113        } else if tn.contains("Decimal") {
114            return match self {
115                MigrationDialect::Postgres => "NUMERIC".into(),
116                MigrationDialect::MySql => "DECIMAL(38,18)".into(),
117                MigrationDialect::Sqlite => "TEXT".into(),
118            };
119        } else {
120            "TEXT"
121        };
122        base.to_string()
123    }
124}