drizzle_types/
dialect.rs

1//! Unified database dialect enum
2//!
3//! This module provides a single source of truth for database dialect identification,
4//! replacing the previously duplicated definitions across `drizzle-core`, `migrations/config.rs`,
5//! and `migrations/parser.rs`.
6
7/// SQL dialect for database-specific behavior
8///
9/// This enum represents the supported SQL database dialects in Drizzle ORM.
10/// Each dialect has different placeholder syntax, type mappings, and SQL generation rules.
11///
12/// # Examples
13///
14/// ```
15/// use drizzle_types::Dialect;
16///
17/// let dialect = Dialect::PostgreSQL;
18/// assert!(dialect.uses_numbered_placeholders());
19///
20/// let sqlite = Dialect::SQLite;
21/// assert!(!sqlite.uses_numbered_placeholders());
22/// ```
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
26pub enum Dialect {
27    /// SQLite - uses `?` positional placeholders
28    ///
29    /// Compatible with: rusqlite, libsql, turso
30    #[default]
31    SQLite,
32
33    /// PostgreSQL - uses `$1, $2, ...` numbered placeholders
34    ///
35    /// Compatible with: tokio-postgres, postgres, sqlx
36    PostgreSQL,
37
38    /// MySQL - uses `?` positional placeholders
39    ///
40    /// Compatible with: mysql, sqlx
41    MySQL,
42}
43
44impl Dialect {
45    /// Returns `true` if this dialect uses numbered placeholders (`$1, $2, ...`)
46    ///
47    /// Currently only PostgreSQL uses numbered placeholders.
48    /// SQLite and MySQL use positional `?` placeholders.
49    #[inline]
50    #[must_use]
51    pub const fn uses_numbered_placeholders(&self) -> bool {
52        matches!(self, Dialect::PostgreSQL)
53    }
54
55    /// Parse a dialect from a string (case-insensitive)
56    ///
57    /// Supports various common aliases:
58    /// - SQLite: `"sqlite"`, `"turso"`, `"libsql"`
59    /// - PostgreSQL: `"postgresql"`, `"postgres"`, `"pg"`
60    /// - MySQL: `"mysql"`
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use drizzle_types::Dialect;
66    ///
67    /// assert_eq!(Dialect::parse("sqlite"), Some(Dialect::SQLite));
68    /// assert_eq!(Dialect::parse("postgres"), Some(Dialect::PostgreSQL));
69    /// assert_eq!(Dialect::parse("pg"), Some(Dialect::PostgreSQL));
70    /// assert_eq!(Dialect::parse("unknown"), None);
71    /// ```
72    #[must_use]
73    pub fn parse(s: &str) -> Option<Self> {
74        // Use eq_ignore_ascii_case for no_std compatibility (no allocation)
75        if s.eq_ignore_ascii_case("sqlite")
76            || s.eq_ignore_ascii_case("turso")
77            || s.eq_ignore_ascii_case("libsql")
78        {
79            Some(Dialect::SQLite)
80        } else if s.eq_ignore_ascii_case("postgresql")
81            || s.eq_ignore_ascii_case("postgres")
82            || s.eq_ignore_ascii_case("pg")
83        {
84            Some(Dialect::PostgreSQL)
85        } else if s.eq_ignore_ascii_case("mysql") {
86            Some(Dialect::MySQL)
87        } else {
88            None
89        }
90    }
91
92    /// Get the table attribute prefix for this dialect in generated code
93    ///
94    /// Used by schema parsers and code generators.
95    #[must_use]
96    pub const fn table_prefix(&self) -> &'static str {
97        match self {
98            Dialect::SQLite => "#[SQLiteTable",
99            Dialect::PostgreSQL => "#[PostgresTable",
100            Dialect::MySQL => "#[MySQLTable",
101        }
102    }
103
104    /// Get the index attribute prefix for this dialect in generated code
105    #[must_use]
106    pub const fn index_prefix(&self) -> &'static str {
107        match self {
108            Dialect::SQLite => "#[SQLiteIndex",
109            Dialect::PostgreSQL => "#[PostgresIndex",
110            Dialect::MySQL => "#[MySQLIndex",
111        }
112    }
113
114    /// Get the schema derive attribute for this dialect
115    #[must_use]
116    pub const fn schema_derive(&self) -> &'static str {
117        match self {
118            Dialect::SQLite => "#[derive(SQLiteSchema)]",
119            Dialect::PostgreSQL => "#[derive(PostgresSchema)]",
120            Dialect::MySQL => "#[derive(MySQLSchema)]",
121        }
122    }
123
124    /// Get the dialect name as a lowercase string
125    #[must_use]
126    pub const fn as_str(&self) -> &'static str {
127        match self {
128            Dialect::SQLite => "sqlite",
129            Dialect::PostgreSQL => "postgresql",
130            Dialect::MySQL => "mysql",
131        }
132    }
133}
134
135impl core::fmt::Display for Dialect {
136    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
137        f.write_str(self.as_str())
138    }
139}
140
141impl core::str::FromStr for Dialect {
142    type Err = DialectParseError;
143
144    fn from_str(s: &str) -> Result<Self, Self::Err> {
145        Dialect::parse(s).ok_or(DialectParseError)
146    }
147}
148
149/// Error returned when parsing an unknown dialect string
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub struct DialectParseError;
152
153impl core::fmt::Display for DialectParseError {
154    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
155        f.write_str("unknown dialect")
156    }
157}
158
159#[cfg(feature = "std")]
160impl std::error::Error for DialectParseError {}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_dialect_parse() {
168        assert_eq!(Dialect::parse("sqlite"), Some(Dialect::SQLite));
169        assert_eq!(Dialect::parse("SQLite"), Some(Dialect::SQLite));
170        assert_eq!(Dialect::parse("turso"), Some(Dialect::SQLite));
171        assert_eq!(Dialect::parse("libsql"), Some(Dialect::SQLite));
172
173        assert_eq!(Dialect::parse("postgresql"), Some(Dialect::PostgreSQL));
174        assert_eq!(Dialect::parse("postgres"), Some(Dialect::PostgreSQL));
175        assert_eq!(Dialect::parse("pg"), Some(Dialect::PostgreSQL));
176        assert_eq!(Dialect::parse("PG"), Some(Dialect::PostgreSQL));
177
178        assert_eq!(Dialect::parse("mysql"), Some(Dialect::MySQL));
179        assert_eq!(Dialect::parse("MySQL"), Some(Dialect::MySQL));
180
181        assert_eq!(Dialect::parse("unknown"), None);
182        assert_eq!(Dialect::parse(""), None);
183    }
184
185    #[test]
186    fn test_dialect_placeholders() {
187        assert!(!Dialect::SQLite.uses_numbered_placeholders());
188        assert!(Dialect::PostgreSQL.uses_numbered_placeholders());
189        assert!(!Dialect::MySQL.uses_numbered_placeholders());
190    }
191
192    #[test]
193    fn test_dialect_display() {
194        assert_eq!(format!("{}", Dialect::SQLite), "sqlite");
195        assert_eq!(format!("{}", Dialect::PostgreSQL), "postgresql");
196        assert_eq!(format!("{}", Dialect::MySQL), "mysql");
197    }
198}