mik_sql/
dialect.rs

1//! SQL dialect implementations for Postgres and `SQLite`.
2//!
3//! Each dialect handles the specific syntax differences between databases.
4
5use crate::Value;
6
7/// SQL dialect trait for database-specific syntax.
8pub trait Dialect: Clone + Copy {
9    /// Format a parameter placeholder (e.g., `$1` for Postgres, `?1` for `SQLite`).
10    fn param(&self, idx: usize) -> String;
11
12    /// Format a boolean literal.
13    fn bool_lit(&self, val: bool) -> &'static str;
14
15    /// Format the regex operator and pattern.
16    /// Returns (operator, `should_transform_pattern`).
17    fn regex_op(&self) -> &'static str;
18
19    /// Format an IN clause with multiple values.
20    /// Returns the SQL fragment (e.g., `= ANY($1)` or `IN (?1, ?2)`).
21    fn in_clause(&self, field: &str, values: &[Value], start_idx: usize) -> (String, Vec<Value>);
22
23    /// Format a NOT IN clause.
24    fn not_in_clause(
25        &self,
26        field: &str,
27        values: &[Value],
28        start_idx: usize,
29    ) -> (String, Vec<Value>);
30
31    /// Whether ILIKE is supported natively.
32    fn supports_ilike(&self) -> bool;
33
34    /// Format a STARTS WITH clause (e.g., `LIKE $1 || '%'` or `LIKE ?1 || '%'`).
35    fn starts_with_clause(&self, field: &str, idx: usize) -> String;
36
37    /// Format an ENDS WITH clause (e.g., `LIKE '%' || $1` or `LIKE '%' || ?1`).
38    fn ends_with_clause(&self, field: &str, idx: usize) -> String;
39
40    /// Format a CONTAINS clause (e.g., `LIKE '%' || $1 || '%'` or `LIKE '%' || ?1 || '%'`).
41    fn contains_clause(&self, field: &str, idx: usize) -> String;
42}
43
44/// Postgres dialect.
45#[derive(Debug, Clone, Copy, Default)]
46pub struct Postgres;
47
48impl Dialect for Postgres {
49    #[inline]
50    fn param(&self, idx: usize) -> String {
51        format!("${idx}")
52    }
53
54    #[inline]
55    fn bool_lit(&self, val: bool) -> &'static str {
56        if val { "TRUE" } else { "FALSE" }
57    }
58
59    #[inline]
60    fn regex_op(&self) -> &'static str {
61        "~"
62    }
63
64    fn in_clause(&self, field: &str, values: &[Value], start_idx: usize) -> (String, Vec<Value>) {
65        // Postgres: field = ANY($1) with array parameter
66        let sql = format!("{field} = ANY(${start_idx})");
67        (sql, vec![Value::Array(values.to_vec())])
68    }
69
70    fn not_in_clause(
71        &self,
72        field: &str,
73        values: &[Value],
74        start_idx: usize,
75    ) -> (String, Vec<Value>) {
76        let sql = format!("{field} != ALL(${start_idx})");
77        (sql, vec![Value::Array(values.to_vec())])
78    }
79
80    #[inline]
81    fn supports_ilike(&self) -> bool {
82        true
83    }
84
85    #[inline]
86    fn starts_with_clause(&self, field: &str, idx: usize) -> String {
87        format!("{field} LIKE ${idx} || '%'")
88    }
89
90    #[inline]
91    fn ends_with_clause(&self, field: &str, idx: usize) -> String {
92        format!("{field} LIKE '%' || ${idx}")
93    }
94
95    #[inline]
96    fn contains_clause(&self, field: &str, idx: usize) -> String {
97        format!("{field} LIKE '%' || ${idx} || '%'")
98    }
99}
100
101/// `SQLite` dialect.
102#[derive(Debug, Clone, Copy, Default)]
103pub struct Sqlite;
104
105impl Dialect for Sqlite {
106    #[inline]
107    fn param(&self, idx: usize) -> String {
108        format!("?{idx}")
109    }
110
111    #[inline]
112    fn bool_lit(&self, val: bool) -> &'static str {
113        if val { "1" } else { "0" }
114    }
115
116    #[inline]
117    fn regex_op(&self) -> &'static str {
118        // SQLite doesn't have native regex, fall back to LIKE
119        "LIKE"
120    }
121
122    fn in_clause(&self, field: &str, values: &[Value], start_idx: usize) -> (String, Vec<Value>) {
123        // SQLite: field IN (?1, ?2, ?3) with expanded parameters
124        let placeholders: Vec<String> = (0..values.len())
125            .map(|i| format!("?{}", start_idx + i))
126            .collect();
127        let sql = format!("{} IN ({})", field, placeholders.join(", "));
128        (sql, values.to_vec())
129    }
130
131    fn not_in_clause(
132        &self,
133        field: &str,
134        values: &[Value],
135        start_idx: usize,
136    ) -> (String, Vec<Value>) {
137        let placeholders: Vec<String> = (0..values.len())
138            .map(|i| format!("?{}", start_idx + i))
139            .collect();
140        let sql = format!("{} NOT IN ({})", field, placeholders.join(", "));
141        (sql, values.to_vec())
142    }
143
144    #[inline]
145    fn supports_ilike(&self) -> bool {
146        // SQLite LIKE is case-insensitive for ASCII by default
147        false
148    }
149
150    #[inline]
151    fn starts_with_clause(&self, field: &str, idx: usize) -> String {
152        format!("{field} LIKE ?{idx} || '%'")
153    }
154
155    #[inline]
156    fn ends_with_clause(&self, field: &str, idx: usize) -> String {
157        format!("{field} LIKE '%' || ?{idx}")
158    }
159
160    #[inline]
161    fn contains_clause(&self, field: &str, idx: usize) -> String {
162        format!("{field} LIKE '%' || ?{idx} || '%'")
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_postgres_params() {
172        let pg = Postgres;
173        assert_eq!(pg.param(1), "$1");
174        assert_eq!(pg.param(10), "$10");
175    }
176
177    #[test]
178    fn test_sqlite_params() {
179        let sqlite = Sqlite;
180        assert_eq!(sqlite.param(1), "?1");
181        assert_eq!(sqlite.param(10), "?10");
182    }
183
184    #[test]
185    fn test_postgres_bool() {
186        let pg = Postgres;
187        assert_eq!(pg.bool_lit(true), "TRUE");
188        assert_eq!(pg.bool_lit(false), "FALSE");
189    }
190
191    #[test]
192    fn test_sqlite_bool() {
193        let sqlite = Sqlite;
194        assert_eq!(sqlite.bool_lit(true), "1");
195        assert_eq!(sqlite.bool_lit(false), "0");
196    }
197
198    #[test]
199    fn test_postgres_in_clause() {
200        let pg = Postgres;
201        let values = vec![Value::String("a".into()), Value::String("b".into())];
202        let (sql, params) = pg.in_clause("status", &values, 1);
203
204        assert_eq!(sql, "status = ANY($1)");
205        assert_eq!(params.len(), 1); // Single array param
206    }
207
208    #[test]
209    fn test_sqlite_in_clause() {
210        let sqlite = Sqlite;
211        let values = vec![Value::String("a".into()), Value::String("b".into())];
212        let (sql, params) = sqlite.in_clause("status", &values, 1);
213
214        assert_eq!(sql, "status IN (?1, ?2)");
215        assert_eq!(params.len(), 2); // Expanded params
216    }
217}