Skip to main content

polyglot_sql/dialects/
mysql.rs

1//! MySQL Dialect
2//!
3//! MySQL-specific transformations based on sqlglot patterns.
4//! Key differences from standard SQL:
5//! - || is OR operator, not string concatenation (use CONCAT)
6//! - Uses backticks for identifiers
7//! - No TRY_CAST, no ILIKE
8//! - Different date/time function names
9
10use super::{DialectImpl, DialectType};
11use crate::error::Result;
12use crate::expressions::{
13    BinaryFunc, BinaryOp, Cast, DataType, Expression, Function, JsonExtractFunc, LikeOp, Literal,
14    Paren, UnaryFunc,
15};
16#[cfg(feature = "generate")]
17use crate::generator::GeneratorConfig;
18use crate::tokens::TokenizerConfig;
19
20/// Helper to wrap JSON arrow expressions in parentheses when they appear
21/// in contexts that require it (Binary, In, Not expressions)
22/// This matches Python sqlglot's WRAPPED_JSON_EXTRACT_EXPRESSIONS behavior
23fn wrap_if_json_arrow(expr: Expression) -> Expression {
24    match &expr {
25        Expression::JsonExtract(f) if f.arrow_syntax => Expression::Paren(Box::new(Paren {
26            this: expr,
27            trailing_comments: Vec::new(),
28        })),
29        Expression::JsonExtractScalar(f) if f.arrow_syntax => Expression::Paren(Box::new(Paren {
30            this: expr,
31            trailing_comments: Vec::new(),
32        })),
33        _ => expr,
34    }
35}
36
37/// Convert JSON arrow expression (-> or ->>) to JSON_EXTRACT function form
38/// This is needed for contexts like MEMBER OF where arrow syntax must become function form
39fn json_arrow_to_function(expr: Expression) -> Expression {
40    match expr {
41        Expression::JsonExtract(f) if f.arrow_syntax => Expression::Function(Box::new(
42            Function::new("JSON_EXTRACT".to_string(), vec![f.this, f.path]),
43        )),
44        Expression::JsonExtractScalar(f) if f.arrow_syntax => {
45            // ->> becomes JSON_UNQUOTE(JSON_EXTRACT(...)) but can be simplified to JSON_EXTRACT_SCALAR
46            // For MySQL, use JSON_UNQUOTE(JSON_EXTRACT(...))
47            let json_extract = Expression::Function(Box::new(Function::new(
48                "JSON_EXTRACT".to_string(),
49                vec![f.this, f.path],
50            )));
51            Expression::Function(Box::new(Function::new(
52                "JSON_UNQUOTE".to_string(),
53                vec![json_extract],
54            )))
55        }
56        other => other,
57    }
58}
59
60/// MySQL dialect
61pub struct MySQLDialect;
62
63impl DialectImpl for MySQLDialect {
64    fn dialect_type(&self) -> DialectType {
65        DialectType::MySQL
66    }
67
68    fn tokenizer_config(&self) -> TokenizerConfig {
69        use crate::tokens::TokenType;
70        let mut config = TokenizerConfig::default();
71        // MySQL uses backticks for identifiers
72        config.identifiers.insert('`', '`');
73        // Remove double quotes from identifiers - in MySQL they are string delimiters
74        // (unless ANSI_QUOTES mode is set, but default mode uses them as strings)
75        config.identifiers.remove(&'"');
76        // MySQL supports double quotes as string literals by default
77        config.quotes.insert("\"".to_string(), "\"".to_string());
78        // MySQL supports backslash escapes in strings
79        config.string_escapes.push('\\');
80        // MySQL has XOR as a logical operator keyword
81        config.keywords.insert("XOR".to_string(), TokenType::Xor);
82        // MySQL: backslash followed by chars NOT in this list -> discard backslash
83        // See: https://dev.mysql.com/doc/refman/8.4/en/string-literals.html
84        config.escape_follow_chars = vec!['0', 'b', 'n', 'r', 't', 'Z', '%', '_'];
85        // MySQL allows identifiers to start with digits (e.g., 1a, 1_a)
86        config.identifiers_can_start_with_digit = true;
87        config.hex_number_strings = true;
88        config
89    }
90
91    #[cfg(feature = "generate")]
92
93    fn generator_config(&self) -> GeneratorConfig {
94        use crate::generator::IdentifierQuoteStyle;
95        GeneratorConfig {
96            identifier_quote: '`',
97            identifier_quote_style: IdentifierQuoteStyle::BACKTICK,
98            dialect: Some(DialectType::MySQL),
99            // MySQL doesn't support null ordering in most contexts
100            null_ordering_supported: false,
101            // MySQL LIMIT only
102            limit_only_literals: true,
103            // MySQL doesn't support semi/anti join
104            semi_anti_join_with_side: false,
105            // MySQL doesn't support table alias columns in some contexts
106            supports_table_alias_columns: false,
107            // MySQL VALUES not used as table
108            values_as_table: false,
109            // MySQL doesn't support TABLESAMPLE
110            tablesample_requires_parens: false,
111            tablesample_with_method: false,
112            // MySQL doesn't support aggregate FILTER
113            aggregate_filter_supported: false,
114            // MySQL doesn't support TRY
115            try_supported: false,
116            // MySQL doesn't support CONVERT_TIMEZONE
117            supports_convert_timezone: false,
118            // MySQL doesn't support UESCAPE
119            supports_uescape: false,
120            // MySQL doesn't support BETWEEN flags
121            supports_between_flags: false,
122            // MySQL supports EXPLAIN but not query hints in standard way
123            query_hints: false,
124            // MySQL parameter token
125            parameter_token: "?",
126            // MySQL doesn't support window EXCLUDE
127            supports_window_exclude: false,
128            // MySQL doesn't support exploding projections
129            supports_exploding_projections: false,
130            identifiers_can_start_with_digit: true,
131            // MySQL supports FOR UPDATE/SHARE
132            locking_reads_supported: true,
133            ..Default::default()
134        }
135    }
136
137    #[cfg(feature = "transpile")]
138
139    fn transform_expr(&self, expr: Expression) -> Result<Expression> {
140        match expr {
141            // ===== Data Type Mappings =====
142            Expression::DataType(dt) => self.transform_data_type(dt),
143
144            // NVL -> IFNULL in MySQL
145            Expression::Nvl(f) => Ok(Expression::IfNull(f)),
146
147            // Note: COALESCE is valid in MySQL and should be preserved.
148            // Unlike some other dialects, we do NOT convert COALESCE to IFNULL
149            // as this would break identity tests.
150
151            // TryCast -> CAST or TIMESTAMP() (MySQL doesn't support TRY_CAST)
152            Expression::TryCast(c) => self.transform_cast(*c),
153
154            // SafeCast -> CAST or TIMESTAMP() (MySQL doesn't support safe casts)
155            Expression::SafeCast(c) => self.transform_cast(*c),
156
157            // Cast -> Transform cast type according to MySQL restrictions
158            // CAST AS TIMESTAMP -> TIMESTAMP() function in MySQL
159            Expression::Cast(c) => self.transform_cast(*c),
160
161            // ILIKE -> LOWER() LIKE LOWER() in MySQL
162            Expression::ILike(op) => {
163                // Transform ILIKE to: LOWER(left) LIKE LOWER(right)
164                let lower_left = Expression::Lower(Box::new(UnaryFunc::new(op.left)));
165                let lower_right = Expression::Lower(Box::new(UnaryFunc::new(op.right)));
166                Ok(Expression::Like(Box::new(LikeOp {
167                    left: lower_left,
168                    right: lower_right,
169                    escape: op.escape,
170                    quantifier: op.quantifier,
171                    inferred_type: None,
172                })))
173            }
174
175            // Preserve semantic string concatenation expressions.
176            // MySQL generation renders these as CONCAT(...).
177            Expression::Concat(op) => Ok(Expression::Concat(op)),
178
179            // RANDOM -> RAND in MySQL
180            Expression::Random(_) => Ok(Expression::Rand(Box::new(crate::expressions::Rand {
181                seed: None,
182                lower: None,
183                upper: None,
184            }))),
185
186            // ArrayAgg -> GROUP_CONCAT in MySQL
187            Expression::ArrayAgg(f) => Ok(Expression::Function(Box::new(Function::new(
188                "GROUP_CONCAT".to_string(),
189                vec![f.this],
190            )))),
191
192            // StringAgg -> GROUP_CONCAT in MySQL
193            Expression::StringAgg(f) => {
194                let mut args = vec![f.this.clone()];
195                if let Some(separator) = &f.separator {
196                    args.push(separator.clone());
197                }
198                Ok(Expression::Function(Box::new(Function::new(
199                    "GROUP_CONCAT".to_string(),
200                    args,
201                ))))
202            }
203
204            // UNNEST -> Not directly supported in MySQL, use JSON_TABLE or inline
205            // For basic cases, pass through (may need manual handling)
206            Expression::Unnest(f) => {
207                // MySQL 8.0+ has JSON_TABLE which can be used for unnesting
208                // For now, pass through with a function call
209                Ok(Expression::Function(Box::new(Function::new(
210                    "JSON_TABLE".to_string(),
211                    vec![f.this],
212                ))))
213            }
214
215            // Substring: Use comma syntax (not FROM/FOR) in MySQL
216            Expression::Substring(mut f) => {
217                f.from_for_syntax = false;
218                Ok(Expression::Substring(f))
219            }
220
221            // ===== Bitwise operations =====
222            // BitwiseAndAgg -> BIT_AND
223            Expression::BitwiseAndAgg(f) => Ok(Expression::Function(Box::new(Function::new(
224                "BIT_AND".to_string(),
225                vec![f.this],
226            )))),
227
228            // BitwiseOrAgg -> BIT_OR
229            Expression::BitwiseOrAgg(f) => Ok(Expression::Function(Box::new(Function::new(
230                "BIT_OR".to_string(),
231                vec![f.this],
232            )))),
233
234            // BitwiseXorAgg -> BIT_XOR
235            Expression::BitwiseXorAgg(f) => Ok(Expression::Function(Box::new(Function::new(
236                "BIT_XOR".to_string(),
237                vec![f.this],
238            )))),
239
240            // BitwiseCount -> BIT_COUNT
241            Expression::BitwiseCount(f) => Ok(Expression::Function(Box::new(Function::new(
242                "BIT_COUNT".to_string(),
243                vec![f.this],
244            )))),
245
246            // TimeFromParts -> MAKETIME
247            Expression::TimeFromParts(f) => {
248                let mut args = Vec::new();
249                if let Some(h) = f.hour {
250                    args.push(*h);
251                }
252                if let Some(m) = f.min {
253                    args.push(*m);
254                }
255                if let Some(s) = f.sec {
256                    args.push(*s);
257                }
258                Ok(Expression::Function(Box::new(Function::new(
259                    "MAKETIME".to_string(),
260                    args,
261                ))))
262            }
263
264            // ===== Boolean aggregates =====
265            // In MySQL, there's no BOOL_AND/BOOL_OR, use MIN/MAX on boolean values
266            // LogicalAnd -> MIN (0 is false, non-0 is true)
267            Expression::LogicalAnd(f) => Ok(Expression::Function(Box::new(Function::new(
268                "MIN".to_string(),
269                vec![f.this],
270            )))),
271
272            // LogicalOr -> MAX
273            Expression::LogicalOr(f) => Ok(Expression::Function(Box::new(Function::new(
274                "MAX".to_string(),
275                vec![f.this],
276            )))),
277
278            // ===== Date/time functions =====
279            // DayOfMonth -> DAYOFMONTH
280            Expression::DayOfMonth(f) => Ok(Expression::Function(Box::new(Function::new(
281                "DAYOFMONTH".to_string(),
282                vec![f.this],
283            )))),
284
285            // DayOfWeek -> DAYOFWEEK
286            Expression::DayOfWeek(f) => Ok(Expression::Function(Box::new(Function::new(
287                "DAYOFWEEK".to_string(),
288                vec![f.this],
289            )))),
290
291            // DayOfYear -> DAYOFYEAR
292            Expression::DayOfYear(f) => Ok(Expression::Function(Box::new(Function::new(
293                "DAYOFYEAR".to_string(),
294                vec![f.this],
295            )))),
296
297            // WeekOfYear -> WEEKOFYEAR
298            Expression::WeekOfYear(f) => Ok(Expression::Function(Box::new(Function::new(
299                "WEEKOFYEAR".to_string(),
300                vec![f.this],
301            )))),
302
303            // DateDiff -> DATEDIFF
304            Expression::DateDiff(f) => Ok(Expression::Function(Box::new(Function::new(
305                "DATEDIFF".to_string(),
306                vec![f.this, f.expression],
307            )))),
308
309            // TimeStrToUnix -> UNIX_TIMESTAMP
310            Expression::TimeStrToUnix(f) => Ok(Expression::Function(Box::new(Function::new(
311                "UNIX_TIMESTAMP".to_string(),
312                vec![f.this],
313            )))),
314
315            // TimestampDiff -> TIMESTAMPDIFF
316            Expression::TimestampDiff(f) => Ok(Expression::Function(Box::new(Function::new(
317                "TIMESTAMPDIFF".to_string(),
318                vec![*f.this, *f.expression],
319            )))),
320
321            // ===== String functions =====
322            // StrPosition -> LOCATE in MySQL
323            // STRPOS(str, substr) -> LOCATE(substr, str) (args are swapped)
324            Expression::StrPosition(f) => {
325                let mut args = vec![];
326                if let Some(substr) = f.substr {
327                    args.push(*substr);
328                }
329                args.push(*f.this);
330                if let Some(pos) = f.position {
331                    args.push(*pos);
332                }
333                Ok(Expression::Function(Box::new(Function::new(
334                    "LOCATE".to_string(),
335                    args,
336                ))))
337            }
338
339            // Stuff -> INSERT in MySQL
340            Expression::Stuff(f) => {
341                let mut args = vec![*f.this];
342                if let Some(start) = f.start {
343                    args.push(*start);
344                }
345                if let Some(length) = f.length {
346                    args.push(Expression::number(length));
347                }
348                args.push(*f.expression);
349                Ok(Expression::Function(Box::new(Function::new(
350                    "INSERT".to_string(),
351                    args,
352                ))))
353            }
354
355            // ===== Session/User functions =====
356            // SessionUser -> SESSION_USER()
357            Expression::SessionUser(_) => Ok(Expression::Function(Box::new(Function::new(
358                "SESSION_USER".to_string(),
359                vec![],
360            )))),
361
362            // CurrentDate -> CURRENT_DATE (no parentheses in MySQL) - keep as CurrentDate
363            Expression::CurrentDate(_) => {
364                Ok(Expression::CurrentDate(crate::expressions::CurrentDate))
365            }
366
367            // ===== Null-safe comparison =====
368            // NullSafeNeq -> NOT (a <=> b) in MySQL
369            Expression::NullSafeNeq(op) => {
370                // Create: NOT (left <=> right)
371                let null_safe_eq = Expression::NullSafeEq(Box::new(crate::expressions::BinaryOp {
372                    left: op.left,
373                    right: op.right,
374                    left_comments: Vec::new(),
375                    operator_comments: Vec::new(),
376                    trailing_comments: Vec::new(),
377                    inferred_type: None,
378                }));
379                Ok(Expression::Not(Box::new(crate::expressions::UnaryOp {
380                    this: null_safe_eq,
381                    inferred_type: None,
382                })))
383            }
384
385            // ParseJson: handled by generator (emits just the string literal for MySQL)
386
387            // JSONExtract with variant_extract (Snowflake colon syntax) -> JSON_EXTRACT
388            Expression::JSONExtract(e) if e.variant_extract.is_some() => {
389                let path = match *e.expression {
390                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
391                        let Literal::String(s) = lit.as_ref() else {
392                            unreachable!()
393                        };
394                        // Convert bracket notation ["key"] to quoted dot notation ."key"
395                        let s = Self::convert_bracket_to_quoted_path(&s);
396                        let normalized = if s.starts_with('$') {
397                            s
398                        } else if s.starts_with('[') {
399                            format!("${}", s)
400                        } else {
401                            format!("$.{}", s)
402                        };
403                        Expression::Literal(Box::new(Literal::String(normalized)))
404                    }
405                    other => other,
406                };
407                Ok(Expression::Function(Box::new(Function::new(
408                    "JSON_EXTRACT".to_string(),
409                    vec![*e.this, path],
410                ))))
411            }
412
413            // Generic function transformations
414            Expression::Function(f) => self.transform_function(*f),
415
416            // Generic aggregate function transformations
417            Expression::AggregateFunction(f) => self.transform_aggregate_function(f),
418
419            // ===== Context-aware JSON arrow wrapping =====
420            // When JSON arrow expressions appear in Binary/In/Not contexts,
421            // they need to be wrapped in parentheses for correct precedence.
422            // This matches Python sqlglot's WRAPPED_JSON_EXTRACT_EXPRESSIONS behavior.
423
424            // Binary operators that need JSON wrapping
425            Expression::Eq(op) => Ok(Expression::Eq(Box::new(BinaryOp {
426                left: wrap_if_json_arrow(op.left),
427                right: wrap_if_json_arrow(op.right),
428                ..*op
429            }))),
430            Expression::Neq(op) => Ok(Expression::Neq(Box::new(BinaryOp {
431                left: wrap_if_json_arrow(op.left),
432                right: wrap_if_json_arrow(op.right),
433                ..*op
434            }))),
435            Expression::Lt(op) => Ok(Expression::Lt(Box::new(BinaryOp {
436                left: wrap_if_json_arrow(op.left),
437                right: wrap_if_json_arrow(op.right),
438                ..*op
439            }))),
440            Expression::Lte(op) => Ok(Expression::Lte(Box::new(BinaryOp {
441                left: wrap_if_json_arrow(op.left),
442                right: wrap_if_json_arrow(op.right),
443                ..*op
444            }))),
445            Expression::Gt(op) => Ok(Expression::Gt(Box::new(BinaryOp {
446                left: wrap_if_json_arrow(op.left),
447                right: wrap_if_json_arrow(op.right),
448                ..*op
449            }))),
450            Expression::Gte(op) => Ok(Expression::Gte(Box::new(BinaryOp {
451                left: wrap_if_json_arrow(op.left),
452                right: wrap_if_json_arrow(op.right),
453                ..*op
454            }))),
455
456            // In expression - wrap the this part if it's JSON arrow
457            Expression::In(mut i) => {
458                i.this = wrap_if_json_arrow(i.this);
459                Ok(Expression::In(i))
460            }
461
462            // Not expression - wrap the this part if it's JSON arrow
463            Expression::Not(mut n) => {
464                n.this = wrap_if_json_arrow(n.this);
465                Ok(Expression::Not(n))
466            }
467
468            // && in MySQL is logical AND, not array overlaps
469            // Transform ArrayOverlaps -> And for MySQL identity
470            Expression::ArrayOverlaps(op) => Ok(Expression::And(op)),
471
472            // MOD(x, y) -> x % y in MySQL
473            Expression::ModFunc(f) => Ok(Expression::Mod(Box::new(BinaryOp {
474                left: f.this,
475                right: f.expression,
476                left_comments: Vec::new(),
477                operator_comments: Vec::new(),
478                trailing_comments: Vec::new(),
479                inferred_type: None,
480            }))),
481
482            // SHOW SLAVE STATUS -> SHOW REPLICA STATUS
483            Expression::Show(mut s) => {
484                if s.this == "SLAVE STATUS" {
485                    s.this = "REPLICA STATUS".to_string();
486                }
487                if matches!(s.this.as_str(), "INDEX" | "COLUMNS") && s.db.is_none() {
488                    if let Some(Expression::Table(mut t)) = s.target.take() {
489                        if let Some(db_ident) = t.schema.take().or(t.catalog.take()) {
490                            s.db = Some(Expression::Identifier(db_ident));
491                            s.target = Some(Expression::Identifier(t.name));
492                        } else {
493                            s.target = Some(Expression::Table(t));
494                        }
495                    }
496                }
497                Ok(Expression::Show(s))
498            }
499
500            // AT TIME ZONE -> strip timezone (MySQL doesn't support AT TIME ZONE)
501            // But keep it for CURRENT_DATE/CURRENT_TIMESTAMP with timezone (transpiled from BigQuery)
502            Expression::AtTimeZone(atz) => {
503                let is_current = match &atz.this {
504                    Expression::CurrentDate(_) | Expression::CurrentTimestamp(_) => true,
505                    Expression::Function(f) => {
506                        let n = f.name.to_uppercase();
507                        (n == "CURRENT_DATE" || n == "CURRENT_TIMESTAMP") && f.no_parens
508                    }
509                    _ => false,
510                };
511                if is_current {
512                    Ok(Expression::AtTimeZone(atz)) // Keep AT TIME ZONE for CURRENT_DATE/CURRENT_TIMESTAMP
513                } else {
514                    Ok(atz.this) // Strip timezone for other expressions
515                }
516            }
517
518            // MEMBER OF with JSON arrow -> convert arrow to JSON_EXTRACT function
519            // MySQL's MEMBER OF requires JSON_EXTRACT function form, not arrow syntax
520            Expression::MemberOf(mut op) => {
521                op.right = json_arrow_to_function(op.right);
522                Ok(Expression::MemberOf(op))
523            }
524
525            // Pass through everything else
526            _ => Ok(expr),
527        }
528    }
529}
530
531#[cfg(feature = "transpile")]
532impl MySQLDialect {
533    fn normalize_mysql_date_format(fmt: &str) -> String {
534        fmt.replace("%H:%i:%s", "%T").replace("%H:%i:%S", "%T")
535    }
536
537    /// Convert bracket notation ["key with spaces"] to quoted dot notation ."key with spaces"
538    /// in JSON path strings.
539    fn convert_bracket_to_quoted_path(path: &str) -> String {
540        let mut result = String::new();
541        let mut chars = path.chars().peekable();
542        while let Some(c) = chars.next() {
543            if c == '[' && chars.peek() == Some(&'"') {
544                chars.next(); // consume "
545                let mut key = String::new();
546                while let Some(kc) = chars.next() {
547                    if kc == '"' && chars.peek() == Some(&']') {
548                        chars.next(); // consume ]
549                        break;
550                    }
551                    key.push(kc);
552                }
553                if !result.is_empty() && !result.ends_with('.') {
554                    result.push('.');
555                }
556                result.push('"');
557                result.push_str(&key);
558                result.push('"');
559            } else {
560                result.push(c);
561            }
562        }
563        result
564    }
565
566    /// Transform data types according to MySQL TYPE_MAPPING
567    /// Note: MySQL's TIMESTAMP is kept as TIMESTAMP (not converted to DATETIME)
568    /// because MySQL's TIMESTAMP has timezone awareness built-in
569    fn transform_data_type(&self, dt: crate::expressions::DataType) -> Result<Expression> {
570        use crate::expressions::DataType;
571        let transformed = match dt {
572            // All TIMESTAMP variants (with or without timezone) -> TIMESTAMP in MySQL
573            DataType::Timestamp {
574                precision,
575                timezone: _,
576            } => DataType::Timestamp {
577                precision,
578                timezone: false,
579            },
580            // TIMESTAMPTZ / TIMESTAMPLTZ parsed as Custom -> normalize to TIMESTAMP
581            DataType::Custom { name }
582                if name.to_uppercase() == "TIMESTAMPTZ"
583                    || name.to_uppercase() == "TIMESTAMPLTZ" =>
584            {
585                DataType::Timestamp {
586                    precision: None,
587                    timezone: false,
588                }
589            }
590            // Keep native MySQL types as-is
591            // MySQL supports TEXT, MEDIUMTEXT, LONGTEXT, BLOB, etc. natively
592            other => other,
593        };
594        Ok(Expression::DataType(transformed))
595    }
596
597    /// Transform CAST expression
598    /// MySQL uses TIMESTAMP() function instead of CAST(x AS TIMESTAMP)
599    /// For Generic->MySQL, TIMESTAMP (no tz) is pre-converted to DATETIME in cross_dialect_normalize
600    fn transform_cast(&self, cast: Cast) -> Result<Expression> {
601        // CAST AS TIMESTAMP/TIMESTAMPTZ/TIMESTAMPLTZ -> TIMESTAMP() function
602        match &cast.to {
603            DataType::Timestamp { .. } => Ok(Expression::Function(Box::new(Function::new(
604                "TIMESTAMP".to_string(),
605                vec![cast.this],
606            )))),
607            DataType::Custom { name }
608                if name.to_uppercase() == "TIMESTAMPTZ"
609                    || name.to_uppercase() == "TIMESTAMPLTZ" =>
610            {
611                Ok(Expression::Function(Box::new(Function::new(
612                    "TIMESTAMP".to_string(),
613                    vec![cast.this],
614                ))))
615            }
616            // All other casts go through normal type transformation
617            _ => Ok(Expression::Cast(Box::new(self.transform_cast_type(cast)))),
618        }
619    }
620
621    /// Transform CAST type according to MySQL restrictions
622    /// MySQL doesn't support many types in CAST - they get mapped to CHAR or SIGNED
623    /// Based on Python sqlglot's CHAR_CAST_MAPPING and SIGNED_CAST_MAPPING
624    fn transform_cast_type(&self, cast: Cast) -> Cast {
625        let new_type = match &cast.to {
626            // CHAR_CAST_MAPPING: These types become CHAR in MySQL CAST, preserving length
627            DataType::VarChar { length, .. } => DataType::Char { length: *length },
628            DataType::Text => DataType::Char { length: None },
629
630            // SIGNED_CAST_MAPPING: These integer types become SIGNED in MySQL CAST
631            DataType::BigInt { .. } => DataType::Custom {
632                name: "SIGNED".to_string(),
633            },
634            DataType::Int { .. } => DataType::Custom {
635                name: "SIGNED".to_string(),
636            },
637            DataType::SmallInt { .. } => DataType::Custom {
638                name: "SIGNED".to_string(),
639            },
640            DataType::TinyInt { .. } => DataType::Custom {
641                name: "SIGNED".to_string(),
642            },
643            DataType::Boolean => DataType::Custom {
644                name: "SIGNED".to_string(),
645            },
646
647            // Custom types that need mapping
648            DataType::Custom { name } => {
649                let upper = name.to_uppercase();
650                match upper.as_str() {
651                    // Text/Blob types -> keep as Custom for cross-dialect mapping
652                    // MySQL generator will output CHAR for these in CAST context
653                    "LONGTEXT" | "MEDIUMTEXT" | "TINYTEXT" | "LONGBLOB" | "MEDIUMBLOB"
654                    | "TINYBLOB" => DataType::Custom { name: upper },
655                    // MEDIUMINT -> SIGNED in MySQL CAST
656                    "MEDIUMINT" => DataType::Custom {
657                        name: "SIGNED".to_string(),
658                    },
659                    // Unsigned integer types -> UNSIGNED
660                    "UBIGINT" | "UINT" | "USMALLINT" | "UTINYINT" | "UMEDIUMINT" => {
661                        DataType::Custom {
662                            name: "UNSIGNED".to_string(),
663                        }
664                    }
665                    // Keep other custom types
666                    _ => cast.to.clone(),
667                }
668            }
669
670            // Types that are valid in MySQL CAST - pass through
671            DataType::Binary { .. } => cast.to.clone(),
672            DataType::VarBinary { .. } => cast.to.clone(),
673            DataType::Date => cast.to.clone(),
674            DataType::Time { .. } => cast.to.clone(),
675            DataType::Decimal { .. } => cast.to.clone(),
676            DataType::Json => cast.to.clone(),
677            DataType::Float { .. } => cast.to.clone(),
678            DataType::Double { .. } => cast.to.clone(),
679            DataType::Char { .. } => cast.to.clone(),
680            DataType::CharacterSet { .. } => cast.to.clone(),
681            DataType::Enum { .. } => cast.to.clone(),
682            DataType::Set { .. } => cast.to.clone(),
683            DataType::Timestamp { .. } => cast.to.clone(),
684
685            // All other unsupported types -> CHAR
686            _ => DataType::Char { length: None },
687        };
688
689        Cast {
690            this: cast.this,
691            to: new_type,
692            trailing_comments: cast.trailing_comments,
693            double_colon_syntax: cast.double_colon_syntax,
694            format: cast.format,
695            default: cast.default,
696            inferred_type: None,
697        }
698    }
699
700    fn transform_function(&self, f: Function) -> Result<Expression> {
701        let name_upper = f.name.to_uppercase();
702        match name_upper.as_str() {
703            // Normalize DATE_FORMAT short-hands to canonical MySQL forms.
704            "DATE_FORMAT" if f.args.len() >= 2 => {
705                let mut f = f;
706                if let Some(Expression::Literal(lit)) = f.args.get(1) {
707                    if let Literal::String(fmt) = lit.as_ref() {
708                        let normalized = Self::normalize_mysql_date_format(fmt);
709                        if normalized != *fmt {
710                            f.args[1] = Expression::Literal(Box::new(Literal::String(normalized)));
711                        }
712                    }
713                }
714                Ok(Expression::Function(Box::new(f)))
715            }
716
717            // NVL -> IFNULL
718            "NVL" if f.args.len() == 2 => {
719                let mut args = f.args;
720                let second = args.pop().unwrap();
721                let first = args.pop().unwrap();
722                Ok(Expression::IfNull(Box::new(BinaryFunc {
723                    original_name: None,
724                    this: first,
725                    expression: second,
726                    inferred_type: None,
727                })))
728            }
729
730            // Note: COALESCE is native to MySQL. We do NOT convert it to IFNULL
731            // because this would break identity tests (Python SQLGlot preserves COALESCE).
732
733            // ARRAY_AGG -> GROUP_CONCAT
734            "ARRAY_AGG" if f.args.len() == 1 => {
735                let mut args = f.args;
736                Ok(Expression::Function(Box::new(Function::new(
737                    "GROUP_CONCAT".to_string(),
738                    vec![args.pop().unwrap()],
739                ))))
740            }
741
742            // STRING_AGG -> GROUP_CONCAT
743            "STRING_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
744                Function::new("GROUP_CONCAT".to_string(), f.args),
745            ))),
746
747            // RANDOM -> RAND
748            "RANDOM" => Ok(Expression::Rand(Box::new(crate::expressions::Rand {
749                seed: None,
750                lower: None,
751                upper: None,
752            }))),
753
754            // CURRENT_TIMESTAMP -> NOW() or CURRENT_TIMESTAMP (both work)
755            // Preserve precision if specified: CURRENT_TIMESTAMP(6)
756            "CURRENT_TIMESTAMP" => {
757                let precision = if let Some(Expression::Literal(lit)) = f.args.first() {
758                    if let crate::expressions::Literal::Number(n) = lit.as_ref() {
759                        n.parse::<u32>().ok()
760                    } else {
761                        None
762                    }
763                } else {
764                    None
765                };
766                Ok(Expression::CurrentTimestamp(
767                    crate::expressions::CurrentTimestamp {
768                        precision,
769                        sysdate: false,
770                    },
771                ))
772            }
773
774            // POSITION -> LOCATE in MySQL (argument order is different)
775            // POSITION(substr IN str) -> LOCATE(substr, str)
776            "POSITION" if f.args.len() == 2 => Ok(Expression::Function(Box::new(Function::new(
777                "LOCATE".to_string(),
778                f.args,
779            )))),
780
781            // LENGTH is native to MySQL (returns bytes, not characters)
782            // CHAR_LENGTH for character count
783            "LENGTH" => Ok(Expression::Function(Box::new(f))),
784
785            // CEIL -> CEILING in MySQL (both work)
786            "CEIL" if f.args.len() == 1 => Ok(Expression::Function(Box::new(Function::new(
787                "CEILING".to_string(),
788                f.args,
789            )))),
790
791            // STDDEV -> STD or STDDEV_POP in MySQL
792            "STDDEV" => Ok(Expression::Function(Box::new(Function::new(
793                "STD".to_string(),
794                f.args,
795            )))),
796
797            // STDDEV_SAMP -> STDDEV in MySQL
798            "STDDEV_SAMP" => Ok(Expression::Function(Box::new(Function::new(
799                "STDDEV".to_string(),
800                f.args,
801            )))),
802
803            // TO_DATE -> STR_TO_DATE in MySQL
804            "TO_DATE" => Ok(Expression::Function(Box::new(Function::new(
805                "STR_TO_DATE".to_string(),
806                f.args,
807            )))),
808
809            // TO_TIMESTAMP -> STR_TO_DATE in MySQL
810            "TO_TIMESTAMP" => Ok(Expression::Function(Box::new(Function::new(
811                "STR_TO_DATE".to_string(),
812                f.args,
813            )))),
814
815            // DATE_TRUNC -> Complex transformation
816            // Typically uses DATE() or DATE_FORMAT() depending on unit
817            "DATE_TRUNC" if f.args.len() >= 2 => {
818                // Simplified: DATE_TRUNC('day', x) -> DATE(x)
819                // Full implementation would handle different units
820                let mut args = f.args;
821                let _unit = args.remove(0);
822                let date = args.remove(0);
823                Ok(Expression::Function(Box::new(Function::new(
824                    "DATE".to_string(),
825                    vec![date],
826                ))))
827            }
828
829            // EXTRACT is native but syntax varies
830
831            // COALESCE is native to MySQL (keep as-is for more than 2 args)
832            "COALESCE" if f.args.len() > 2 => Ok(Expression::Function(Box::new(f))),
833
834            // DAYOFMONTH -> DAY (both work)
835            "DAY" => Ok(Expression::Function(Box::new(Function::new(
836                "DAYOFMONTH".to_string(),
837                f.args,
838            )))),
839
840            // DAYOFWEEK is native to MySQL
841            "DAYOFWEEK" => Ok(Expression::Function(Box::new(f))),
842
843            // DAYOFYEAR is native to MySQL
844            "DAYOFYEAR" => Ok(Expression::Function(Box::new(f))),
845
846            // WEEKOFYEAR is native to MySQL
847            "WEEKOFYEAR" => Ok(Expression::Function(Box::new(f))),
848
849            // LAST_DAY is native to MySQL
850            "LAST_DAY" => Ok(Expression::Function(Box::new(f))),
851
852            // TIMESTAMPADD -> DATE_ADD
853            "TIMESTAMPADD" => Ok(Expression::Function(Box::new(Function::new(
854                "DATE_ADD".to_string(),
855                f.args,
856            )))),
857
858            // TIMESTAMPDIFF is native to MySQL
859            "TIMESTAMPDIFF" => Ok(Expression::Function(Box::new(f))),
860
861            // CONVERT_TIMEZONE(from_tz, to_tz, timestamp) -> CONVERT_TZ(timestamp, from_tz, to_tz) in MySQL
862            "CONVERT_TIMEZONE" if f.args.len() == 3 => {
863                let mut args = f.args;
864                let from_tz = args.remove(0);
865                let to_tz = args.remove(0);
866                let timestamp = args.remove(0);
867                Ok(Expression::Function(Box::new(Function::new(
868                    "CONVERT_TZ".to_string(),
869                    vec![timestamp, from_tz, to_tz],
870                ))))
871            }
872
873            // UTC_TIMESTAMP is native to MySQL
874            "UTC_TIMESTAMP" => Ok(Expression::Function(Box::new(f))),
875
876            // UTC_TIME is native to MySQL
877            "UTC_TIME" => Ok(Expression::Function(Box::new(f))),
878
879            // MAKETIME is native to MySQL (TimeFromParts)
880            "MAKETIME" => Ok(Expression::Function(Box::new(f))),
881
882            // TIME_FROM_PARTS -> MAKETIME
883            "TIME_FROM_PARTS" if f.args.len() == 3 => Ok(Expression::Function(Box::new(
884                Function::new("MAKETIME".to_string(), f.args),
885            ))),
886
887            // STUFF -> INSERT in MySQL
888            "STUFF" if f.args.len() == 4 => Ok(Expression::Function(Box::new(Function::new(
889                "INSERT".to_string(),
890                f.args,
891            )))),
892
893            // LOCATE is native to MySQL (reverse of POSITION args)
894            "LOCATE" => Ok(Expression::Function(Box::new(f))),
895
896            // FIND_IN_SET is native to MySQL
897            "FIND_IN_SET" => Ok(Expression::Function(Box::new(f))),
898
899            // FORMAT is native to MySQL (NumberToStr)
900            "FORMAT" => Ok(Expression::Function(Box::new(f))),
901
902            // JSON_EXTRACT is native to MySQL
903            "JSON_EXTRACT" => Ok(Expression::Function(Box::new(f))),
904
905            // JSON_UNQUOTE is native to MySQL
906            "JSON_UNQUOTE" => Ok(Expression::Function(Box::new(f))),
907
908            // JSON_EXTRACT_PATH_TEXT -> JSON_UNQUOTE(JSON_EXTRACT(...))
909            "JSON_EXTRACT_PATH_TEXT" if f.args.len() >= 2 => {
910                let extract = Expression::Function(Box::new(Function::new(
911                    "JSON_EXTRACT".to_string(),
912                    f.args,
913                )));
914                Ok(Expression::Function(Box::new(Function::new(
915                    "JSON_UNQUOTE".to_string(),
916                    vec![extract],
917                ))))
918            }
919
920            // GEN_RANDOM_UUID / UUID -> UUID()
921            "GEN_RANDOM_UUID" | "GENERATE_UUID" => Ok(Expression::Function(Box::new(
922                Function::new("UUID".to_string(), vec![]),
923            ))),
924
925            // DATABASE() -> SCHEMA() in MySQL (both return current database name)
926            "DATABASE" => Ok(Expression::Function(Box::new(Function::new(
927                "SCHEMA".to_string(),
928                f.args,
929            )))),
930
931            // INSTR -> LOCATE in MySQL (with swapped arguments)
932            // INSTR(str, substr) -> LOCATE(substr, str)
933            "INSTR" if f.args.len() == 2 => {
934                let mut args = f.args;
935                let str_arg = args.remove(0);
936                let substr_arg = args.remove(0);
937                Ok(Expression::Function(Box::new(Function::new(
938                    "LOCATE".to_string(),
939                    vec![substr_arg, str_arg],
940                ))))
941            }
942
943            // TIME_STR_TO_UNIX -> UNIX_TIMESTAMP in MySQL
944            "TIME_STR_TO_UNIX" => Ok(Expression::Function(Box::new(Function::new(
945                "UNIX_TIMESTAMP".to_string(),
946                f.args,
947            )))),
948
949            // TIME_STR_TO_TIME -> CAST AS DATETIME(N) or TIMESTAMP() in MySQL
950            "TIME_STR_TO_TIME" if f.args.len() >= 1 => {
951                let mut args = f.args.into_iter();
952                let arg = args.next().unwrap();
953
954                // If there's a timezone arg, use TIMESTAMP() function instead
955                if args.next().is_some() {
956                    return Ok(Expression::Function(Box::new(Function::new(
957                        "TIMESTAMP".to_string(),
958                        vec![arg],
959                    ))));
960                }
961
962                // Extract sub-second precision from the string literal
963                let precision = if let Expression::Literal(ref lit) = arg {
964                    if let crate::expressions::Literal::String(ref s) = lit.as_ref() {
965                        // Find fractional seconds: look for .NNN pattern after HH:MM:SS
966                        if let Some(dot_pos) = s.rfind('.') {
967                            let after_dot = &s[dot_pos + 1..];
968                            // Count digits until non-digit
969                            let frac_digits =
970                                after_dot.chars().take_while(|c| c.is_ascii_digit()).count();
971                            if frac_digits > 0 {
972                                // Round up: 1-3 digits → 3, 4-6 digits → 6
973                                if frac_digits <= 3 {
974                                    Some(3)
975                                } else {
976                                    Some(6)
977                                }
978                            } else {
979                                None
980                            }
981                        } else {
982                            None
983                        }
984                    } else {
985                        None
986                    }
987                } else {
988                    None
989                };
990
991                let type_name = match precision {
992                    Some(p) => format!("DATETIME({})", p),
993                    None => "DATETIME".to_string(),
994                };
995
996                Ok(Expression::Cast(Box::new(Cast {
997                    this: arg,
998                    to: DataType::Custom { name: type_name },
999                    trailing_comments: Vec::new(),
1000                    double_colon_syntax: false,
1001                    format: None,
1002                    default: None,
1003                    inferred_type: None,
1004                })))
1005            }
1006
1007            // UCASE -> UPPER in MySQL
1008            "UCASE" => Ok(Expression::Function(Box::new(Function::new(
1009                "UPPER".to_string(),
1010                f.args,
1011            )))),
1012
1013            // LCASE -> LOWER in MySQL
1014            "LCASE" => Ok(Expression::Function(Box::new(Function::new(
1015                "LOWER".to_string(),
1016                f.args,
1017            )))),
1018
1019            // DAY_OF_MONTH -> DAYOFMONTH in MySQL
1020            "DAY_OF_MONTH" => Ok(Expression::Function(Box::new(Function::new(
1021                "DAYOFMONTH".to_string(),
1022                f.args,
1023            )))),
1024
1025            // DAY_OF_WEEK -> DAYOFWEEK in MySQL
1026            "DAY_OF_WEEK" => Ok(Expression::Function(Box::new(Function::new(
1027                "DAYOFWEEK".to_string(),
1028                f.args,
1029            )))),
1030
1031            // DAY_OF_YEAR -> DAYOFYEAR in MySQL
1032            "DAY_OF_YEAR" => Ok(Expression::Function(Box::new(Function::new(
1033                "DAYOFYEAR".to_string(),
1034                f.args,
1035            )))),
1036
1037            // WEEK_OF_YEAR -> WEEKOFYEAR in MySQL
1038            "WEEK_OF_YEAR" => Ok(Expression::Function(Box::new(Function::new(
1039                "WEEKOFYEAR".to_string(),
1040                f.args,
1041            )))),
1042
1043            // MOD(x, y) -> x % y in MySQL
1044            "MOD" if f.args.len() == 2 => {
1045                let mut args = f.args;
1046                let left = args.remove(0);
1047                let right = args.remove(0);
1048                Ok(Expression::Mod(Box::new(BinaryOp {
1049                    left,
1050                    right,
1051                    left_comments: Vec::new(),
1052                    operator_comments: Vec::new(),
1053                    trailing_comments: Vec::new(),
1054                    inferred_type: None,
1055                })))
1056            }
1057
1058            // PARSE_JSON -> strip in MySQL (just keep the string argument)
1059            "PARSE_JSON" if f.args.len() == 1 => Ok(f.args.into_iter().next().unwrap()),
1060
1061            // GET_PATH(obj, path) -> JSON_EXTRACT(obj, json_path) in MySQL
1062            "GET_PATH" if f.args.len() == 2 => {
1063                let mut args = f.args;
1064                let this = args.remove(0);
1065                let path = args.remove(0);
1066                let json_path = match &path {
1067                    Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)) => {
1068                        let Literal::String(s) = lit.as_ref() else {
1069                            unreachable!()
1070                        };
1071                        // Convert bracket notation ["key"] to quoted dot notation ."key"
1072                        let s = Self::convert_bracket_to_quoted_path(s);
1073                        let normalized = if s.starts_with('$') {
1074                            s
1075                        } else if s.starts_with('[') {
1076                            format!("${}", s)
1077                        } else {
1078                            format!("$.{}", s)
1079                        };
1080                        Expression::Literal(Box::new(Literal::String(normalized)))
1081                    }
1082                    _ => path,
1083                };
1084                Ok(Expression::JsonExtract(Box::new(JsonExtractFunc {
1085                    this,
1086                    path: json_path,
1087                    returning: None,
1088                    arrow_syntax: false,
1089                    hash_arrow_syntax: false,
1090                    wrapper_option: None,
1091                    quotes_option: None,
1092                    on_scalar_string: false,
1093                    on_error: None,
1094                })))
1095            }
1096
1097            // REGEXP -> REGEXP_LIKE (MySQL standard form)
1098            "REGEXP" if f.args.len() >= 2 => Ok(Expression::Function(Box::new(Function::new(
1099                "REGEXP_LIKE".to_string(),
1100                f.args,
1101            )))),
1102
1103            // CURTIME -> CURRENT_TIME
1104            "CURTIME" => Ok(Expression::CurrentTime(crate::expressions::CurrentTime {
1105                precision: None,
1106            })),
1107
1108            // TRUNC -> TRUNCATE in MySQL
1109            "TRUNC" => Ok(Expression::Function(Box::new(Function::new(
1110                "TRUNCATE".to_string(),
1111                f.args,
1112            )))),
1113
1114            // Pass through everything else
1115            _ => Ok(Expression::Function(Box::new(f))),
1116        }
1117    }
1118
1119    fn transform_aggregate_function(
1120        &self,
1121        f: Box<crate::expressions::AggregateFunction>,
1122    ) -> Result<Expression> {
1123        let name_upper = f.name.to_uppercase();
1124        match name_upper.as_str() {
1125            // STRING_AGG -> GROUP_CONCAT
1126            "STRING_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
1127                Function::new("GROUP_CONCAT".to_string(), f.args),
1128            ))),
1129
1130            // ARRAY_AGG -> GROUP_CONCAT
1131            "ARRAY_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(Function::new(
1132                "GROUP_CONCAT".to_string(),
1133                f.args,
1134            )))),
1135
1136            // Pass through everything else
1137            _ => Ok(Expression::AggregateFunction(f)),
1138        }
1139    }
1140}
1141
1142#[cfg(test)]
1143mod tests {
1144    use super::*;
1145    use crate::dialects::Dialect;
1146
1147    fn transpile_to_mysql(sql: &str) -> String {
1148        let dialect = Dialect::get(DialectType::Generic);
1149        let result = dialect
1150            .transpile(sql, DialectType::MySQL)
1151            .expect("Transpile failed");
1152        result[0].clone()
1153    }
1154
1155    #[test]
1156    fn test_nvl_to_ifnull() {
1157        let result = transpile_to_mysql("SELECT NVL(a, b)");
1158        assert!(
1159            result.contains("IFNULL"),
1160            "Expected IFNULL, got: {}",
1161            result
1162        );
1163    }
1164
1165    #[test]
1166    fn test_coalesce_preserved() {
1167        // COALESCE should be preserved in MySQL (it's a native function)
1168        let result = transpile_to_mysql("SELECT COALESCE(a, b)");
1169        assert!(
1170            result.contains("COALESCE"),
1171            "Expected COALESCE to be preserved, got: {}",
1172            result
1173        );
1174    }
1175
1176    #[test]
1177    fn test_random_to_rand() {
1178        let result = transpile_to_mysql("SELECT RANDOM()");
1179        assert!(result.contains("RAND"), "Expected RAND, got: {}", result);
1180    }
1181
1182    #[test]
1183    fn test_basic_select() {
1184        let result = transpile_to_mysql("SELECT a, b FROM users WHERE id = 1");
1185        assert!(result.contains("SELECT"));
1186        assert!(result.contains("FROM users"));
1187    }
1188
1189    #[test]
1190    fn test_string_agg_to_group_concat() {
1191        let result = transpile_to_mysql("SELECT STRING_AGG(name)");
1192        assert!(
1193            result.contains("GROUP_CONCAT"),
1194            "Expected GROUP_CONCAT, got: {}",
1195            result
1196        );
1197    }
1198
1199    #[test]
1200    fn test_array_agg_to_group_concat() {
1201        let result = transpile_to_mysql("SELECT ARRAY_AGG(name)");
1202        assert!(
1203            result.contains("GROUP_CONCAT"),
1204            "Expected GROUP_CONCAT, got: {}",
1205            result
1206        );
1207    }
1208
1209    #[test]
1210    fn test_to_date_to_str_to_date() {
1211        let result = transpile_to_mysql("SELECT TO_DATE('2023-01-01')");
1212        assert!(
1213            result.contains("STR_TO_DATE"),
1214            "Expected STR_TO_DATE, got: {}",
1215            result
1216        );
1217    }
1218
1219    #[test]
1220    fn test_backtick_identifiers() {
1221        // MySQL uses backticks for identifiers
1222        let dialect = MySQLDialect;
1223        let config = dialect.generator_config();
1224        assert_eq!(config.identifier_quote, '`');
1225    }
1226
1227    #[test]
1228    fn test_create_procedure_signal_sqlstate_regression() {
1229        let sql = "CREATE PROCEDURE CheckSalary(IN p_salary DECIMAL(10,2))
1230BEGIN
1231  IF p_salary <= 0 THEN
1232    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Salary must be positive';
1233  END IF;
1234  INSERT INTO employees (salary) VALUES (p_salary);
1235END;";
1236
1237        let dialect = Dialect::get(DialectType::MySQL);
1238        let ast = dialect.parse(sql).expect("Parse failed");
1239        let output = dialect.generate(&ast[0]).expect("Generate failed");
1240
1241        assert!(output.contains("CREATE PROCEDURE CheckSalary"));
1242        assert!(output.contains("IF p_salary <= 0 THEN"));
1243        assert!(
1244            output.contains("SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Salary must be positive'")
1245        );
1246        assert!(output.contains("END IF"));
1247        assert!(output.contains("INSERT INTO employees (salary) VALUES (p_salary)"));
1248    }
1249
1250    fn mysql_identity(sql: &str, expected: &str) {
1251        let dialect = Dialect::get(DialectType::MySQL);
1252        let ast = dialect.parse(sql).expect("Parse failed");
1253        let transformed = dialect.transform(ast[0].clone()).expect("Transform failed");
1254        let result = dialect.generate(&transformed).expect("Generate failed");
1255        assert_eq!(result, expected, "SQL: {}", sql);
1256    }
1257
1258    #[test]
1259    fn test_ucase_to_upper() {
1260        mysql_identity("SELECT UCASE('foo')", "SELECT UPPER('foo')");
1261    }
1262
1263    #[test]
1264    fn test_lcase_to_lower() {
1265        mysql_identity("SELECT LCASE('foo')", "SELECT LOWER('foo')");
1266    }
1267
1268    #[test]
1269    fn test_day_of_month() {
1270        mysql_identity(
1271            "SELECT DAY_OF_MONTH('2023-01-01')",
1272            "SELECT DAYOFMONTH('2023-01-01')",
1273        );
1274    }
1275
1276    #[test]
1277    fn test_day_of_week() {
1278        mysql_identity(
1279            "SELECT DAY_OF_WEEK('2023-01-01')",
1280            "SELECT DAYOFWEEK('2023-01-01')",
1281        );
1282    }
1283
1284    #[test]
1285    fn test_day_of_year() {
1286        mysql_identity(
1287            "SELECT DAY_OF_YEAR('2023-01-01')",
1288            "SELECT DAYOFYEAR('2023-01-01')",
1289        );
1290    }
1291
1292    #[test]
1293    fn test_week_of_year() {
1294        mysql_identity(
1295            "SELECT WEEK_OF_YEAR('2023-01-01')",
1296            "SELECT WEEKOFYEAR('2023-01-01')",
1297        );
1298    }
1299
1300    #[test]
1301    fn test_mod_func_to_percent() {
1302        // MOD(x, y) function is transformed to x % y in MySQL
1303        mysql_identity("MOD(x, y)", "x % y");
1304    }
1305
1306    #[test]
1307    fn test_database_to_schema() {
1308        mysql_identity("DATABASE()", "SCHEMA()");
1309    }
1310
1311    #[test]
1312    fn test_and_operator() {
1313        mysql_identity("SELECT 1 && 0", "SELECT 1 AND 0");
1314    }
1315
1316    #[test]
1317    fn test_or_operator() {
1318        mysql_identity("SELECT a || b", "SELECT a OR b");
1319    }
1320}