Skip to main content

fraiseql_db/dialect/
mysql.rs

1//! MySQL SQL dialect implementation.
2
3use std::borrow::Cow;
4
5use super::trait_def::{RowViewColumnType, SqlDialect, UnsupportedOperator};
6
7/// MySQL dialect for [`GenericWhereGenerator`].
8///
9/// [`GenericWhereGenerator`]: crate::where_generator::GenericWhereGenerator
10pub struct MySqlDialect;
11
12impl SqlDialect for MySqlDialect {
13    fn name(&self) -> &'static str {
14        "MySQL"
15    }
16
17    fn quote_identifier(&self, name: &str) -> String {
18        format!("`{}`", name.replace('`', "``"))
19    }
20
21    fn json_extract_scalar(&self, column: &str, path: &[String]) -> String {
22        let json_path = crate::path_escape::escape_mysql_json_path(path);
23        format!("JSON_UNQUOTE(JSON_EXTRACT({column}, '{json_path}'))")
24    }
25
26    fn placeholder(&self, _n: usize) -> String {
27        "?".to_string()
28    }
29
30    fn cast_to_numeric<'a>(&self, expr: &'a str) -> Cow<'a, str> {
31        Cow::Owned(format!("CAST({expr} AS DECIMAL)"))
32    }
33
34    fn ilike_sql(&self, lhs: &str, rhs: &str) -> String {
35        // MySQL LIKE is case-insensitive by default with utf8mb4_unicode_ci;
36        // use LOWER() to be explicit and portable.
37        format!("LOWER({lhs}) LIKE LOWER({rhs})")
38    }
39
40    fn concat_sql(&self, parts: &[&str]) -> String {
41        format!("CONCAT({})", parts.join(", "))
42    }
43
44    fn json_array_length(&self, expr: &str) -> String {
45        format!("JSON_LENGTH({expr})")
46    }
47
48    fn array_contains_sql(&self, lhs: &str, rhs: &str) -> Result<String, UnsupportedOperator> {
49        Ok(format!("JSON_CONTAINS({lhs}, {rhs})"))
50    }
51
52    fn array_overlaps_sql(&self, lhs: &str, rhs: &str) -> Result<String, UnsupportedOperator> {
53        Ok(format!("JSON_OVERLAPS({lhs}, {rhs})"))
54    }
55
56    fn row_view_column_expr(
57        &self,
58        json_column: &str,
59        field_name: &str,
60        col_type: &RowViewColumnType,
61    ) -> String {
62        let mysql_type = match col_type {
63            RowViewColumnType::Text | RowViewColumnType::Uuid => "CHAR",
64            RowViewColumnType::Int32 => "SIGNED",
65            RowViewColumnType::Int64 => "SIGNED",
66            RowViewColumnType::Float64 => "DOUBLE",
67            RowViewColumnType::Boolean => "UNSIGNED",
68            RowViewColumnType::Timestamptz => "DATETIME",
69            RowViewColumnType::Date => "DATE",
70            RowViewColumnType::Json => "JSON",
71        };
72        format!("CAST(JSON_UNQUOTE(JSON_EXTRACT({json_column}, '$.{field_name}')) AS {mysql_type})")
73    }
74
75    // MySQL FTS: all variants map to MATCH/AGAINST
76    fn fts_matches_sql(&self, expr: &str, param: &str) -> Result<String, UnsupportedOperator> {
77        Ok(format!("MATCH({expr}) AGAINST({param} IN NATURAL LANGUAGE MODE)"))
78    }
79
80    fn fts_plain_query_sql(&self, expr: &str, param: &str) -> Result<String, UnsupportedOperator> {
81        Ok(format!("MATCH({expr}) AGAINST({param} IN BOOLEAN MODE)"))
82    }
83
84    fn fts_phrase_query_sql(&self, expr: &str, param: &str) -> Result<String, UnsupportedOperator> {
85        Ok(format!("MATCH({expr}) AGAINST({param} IN NATURAL LANGUAGE MODE)"))
86    }
87
88    // WebsearchQuery unsupported → default Err from trait
89
90    fn regex_sql(
91        &self,
92        lhs: &str,
93        rhs: &str,
94        _case_insensitive: bool,
95        negate: bool,
96    ) -> Result<String, UnsupportedOperator> {
97        // MySQL REGEXP is case-insensitive by default with utf8mb4; both
98        // case-sensitive and case-insensitive variants use the same operator.
99        if negate {
100            Ok(format!("{lhs} NOT REGEXP {rhs}"))
101        } else {
102            Ok(format!("{lhs} REGEXP {rhs}"))
103        }
104    }
105}