Skip to main content

pg2any_lib/destinations/dialects/
mysql.rs

1use crate::destinations::dialect::SqlDialect;
2use pg_walstream::ColumnValue;
3
4#[derive(Default, Debug, Clone, Copy)]
5pub struct MySqlDialect;
6
7impl SqlDialect for MySqlDialect {
8    fn quote_identifier(&self, ident: &str, out: &mut String) {
9        out.reserve(ident.len() + 2);
10        out.push('`');
11        if ident.contains('`') {
12            for ch in ident.chars() {
13                if ch == '`' {
14                    out.push('`');
15                }
16                out.push(ch);
17            }
18        } else {
19            out.push_str(ident);
20        }
21        out.push('`');
22    }
23
24    fn qualify_table(&self, schema: &str, table: &str, out: &mut String) {
25        self.quote_identifier(schema, out);
26        out.push('.');
27        self.quote_identifier(table, out);
28    }
29
30    fn render_hex_literal(&self, bytes: &[u8], out: &mut String) {
31        out.push_str("X'");
32        crate::destinations::dialect::push_hex_ascii(out, bytes);
33        out.push('\'');
34    }
35
36    fn render_value(&self, value: &ColumnValue, out: &mut String) {
37        match value {
38            ColumnValue::Null => out.push_str("NULL"),
39            ColumnValue::Text(_) => match value.as_str() {
40                Some(s) => {
41                    if s == "t" {
42                        out.push('1');
43                        return;
44                    }
45                    if s == "f" {
46                        out.push('0');
47                        return;
48                    }
49                    out.reserve(s.len() + 2);
50                    out.push('\'');
51                    let needs_escape = s.contains(['\'', '\\']);
52                    if needs_escape {
53                        for ch in s.chars() {
54                            match ch {
55                                '\'' => out.push_str("''"),
56                                '\\' => out.push_str("\\\\"),
57                                _ => out.push(ch),
58                            }
59                        }
60                    } else {
61                        out.push_str(s);
62                    }
63                    out.push('\'');
64                }
65                None => self.render_hex_literal(value.as_bytes(), out),
66            },
67            ColumnValue::Binary(_) => self.render_hex_literal(value.as_bytes(), out),
68        }
69    }
70
71    fn truncate_table_sql(&self, schema: &str, table: &str) -> Option<String> {
72        let mut sql = String::new();
73        sql.push_str("TRUNCATE TABLE ");
74        self.qualify_table(schema, table, &mut sql);
75        sql.push(';');
76        Some(sql)
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    fn s(f: impl Fn(&mut String)) -> String {
85        let mut o = String::new();
86        f(&mut o);
87        o
88    }
89
90    #[test]
91    fn quote_identifier() {
92        assert_eq!(s(|o| MySqlDialect.quote_identifier("users", o)), "`users`");
93        assert_eq!(
94            s(|o| MySqlDialect.quote_identifier("back`tick", o)),
95            "`back``tick`"
96        );
97        assert_eq!(
98            s(|o| MySqlDialect.quote_identifier("bra]cket", o)),
99            "`bra]cket`"
100        );
101        assert_eq!(
102            s(|o| MySqlDialect.quote_identifier("double\"quote", o)),
103            "`double\"quote`"
104        );
105    }
106
107    #[test]
108    fn qualify_table() {
109        assert_eq!(
110            s(|o| MySqlDialect.qualify_table("public", "users", o)),
111            "`public`.`users`"
112        );
113        assert_eq!(
114            s(|o| MySqlDialect.qualify_table("custom", "items", o)),
115            "`custom`.`items`"
116        );
117    }
118
119    #[test]
120    fn render_hex_literal() {
121        assert_eq!(s(|o| MySqlDialect.render_hex_literal(&[], o)), "X''");
122        assert_eq!(
123            s(|o| MySqlDialect.render_hex_literal(&[0xde, 0xad, 0xbe, 0xef], o)),
124            "X'deadbeef'"
125        );
126    }
127
128    #[test]
129    fn render_value() {
130        use bytes::Bytes;
131        assert_eq!(
132            s(|o| MySqlDialect.render_value(&ColumnValue::Null, o)),
133            "NULL"
134        );
135        assert_eq!(
136            s(|o| MySqlDialect.render_value(&ColumnValue::text("t"), o)),
137            "1"
138        );
139        assert_eq!(
140            s(|o| MySqlDialect.render_value(&ColumnValue::text("f"), o)),
141            "0"
142        );
143        assert_eq!(
144            s(|o| MySqlDialect.render_value(&ColumnValue::text("hello"), o)),
145            "'hello'"
146        );
147        assert_eq!(
148            s(|o| MySqlDialect.render_value(&ColumnValue::text("o'reilly"), o)),
149            "'o''reilly'"
150        );
151        assert_eq!(
152            s(|o| MySqlDialect.render_value(&ColumnValue::text("back\\slash"), o)),
153            "'back\\\\slash'"
154        );
155        assert_eq!(
156            s(|o| MySqlDialect.render_value(
157                &ColumnValue::Binary(Bytes::from_static(&[0x00, 0xff, 0xab])),
158                o
159            )),
160            "X'00ffab'"
161        );
162    }
163
164    #[test]
165    fn truncate_table_sql() {
166        assert_eq!(
167            MySqlDialect
168                .truncate_table_sql("public", "users")
169                .as_deref(),
170            Some("TRUNCATE TABLE `public`.`users`;")
171        );
172    }
173}