vibesql_types/sql_mode/
mod.rs

1//! SQL mode configuration for dialect-specific behavior.
2
3#![allow(clippy::derivable_impls)]
4
5mod config;
6mod operators;
7mod strings;
8pub mod types;
9
10pub use config::MySqlModeFlags;
11// Re-export operator types and traits
12pub use operators::{ConcatOperator, DivisionBehavior, OperatorBehavior};
13// Re-export string types and traits
14pub use strings::{Collation, StringBehavior};
15
16/// SQL compatibility mode
17///
18/// VibeSQL supports different SQL dialect modes to match the behavior
19/// of different database systems. This is necessary because SQL standards
20/// allow implementation-defined behavior in certain areas.
21///
22/// ## Differences by Mode
23///
24/// See SQL_COMPATIBILITY_MODE.md in the repository root for a comprehensive list
25/// of behavioral differences between modes.
26///
27/// ### Division Operator (`/`)
28/// - **MySQL**: `INTEGER / INTEGER → DECIMAL` (floating-point division)
29///   - Example: `83 / 6 = 13.8333`
30/// - **SQLite**: `INTEGER / INTEGER → INTEGER` (truncated division)
31///   - Example: `83 / 6 = 13`
32///
33/// ## Default Mode
34///
35/// MySQL mode is the default to maximize compatibility with the
36/// dolthub/sqllogictest test suite, which was generated from MySQL 8.
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub enum SqlMode {
39    /// MySQL 8.0+ compatibility mode (default)
40    ///
41    /// - Division returns DECIMAL (floating-point)
42    /// - Other MySQL-specific behaviors controlled by flags
43    MySQL { flags: MySqlModeFlags },
44
45    /// SQLite 3 compatibility mode
46    ///
47    /// - Division returns INTEGER (truncated)
48    /// - Other SQLite-specific behaviors
49    ///
50    /// Note: Currently not fully implemented. Many features will error
51    /// with "TODO: SQLite mode not yet supported" messages.
52    SQLite,
53}
54
55impl Default for SqlMode {
56    fn default() -> Self {
57        // Default to MySQL mode for SQLLogicTest compatibility
58        // The dolthub/sqllogictest corpus was regenerated against MySQL 8.x
59        // and expects MySQL semantics including decimal division
60        // (INTEGER / INTEGER → DECIMAL)
61        SqlMode::MySQL { flags: MySqlModeFlags::default() }
62    }
63}
64
65impl std::fmt::Display for SqlMode {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            SqlMode::MySQL { .. } => write!(f, "mysql"),
69            SqlMode::SQLite => write!(f, "sqlite"),
70        }
71    }
72}
73
74impl std::str::FromStr for SqlMode {
75    type Err = String;
76
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        // First try simple mode names
79        match s.to_lowercase().as_str() {
80            "mysql" => return Ok(SqlMode::MySQL { flags: MySqlModeFlags::default() }),
81            // MySQL mode with SQLite division semantics
82            // This is used by SQLLogicTest where MySQL syntax is needed (e.g., CAST...AS SIGNED)
83            // but division should behave like SQLite (INTEGER / INTEGER → INTEGER)
84            // because the expected results were generated using SQLite.
85            "mysql_slt" => {
86                return Ok(SqlMode::MySQL {
87                    flags: MySqlModeFlags::with_sqlite_division_semantics(),
88                })
89            }
90            "sqlite" => return Ok(SqlMode::SQLite),
91            _ => {}
92        }
93
94        // Try parsing as MySQL comma-separated mode flags
95        // e.g., "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,..."
96        // Also handles leading/trailing commas and empty segments
97        parse_mysql_mode_flags(s)
98    }
99}
100
101/// Parse MySQL comma-separated mode flags
102///
103/// This handles strings like:
104/// - "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,
105///   NO_ENGINE_SUBSTITUTION"
106/// - ",STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,..." (leading comma from REPLACE() removing first mode)
107/// - "STRICT_TRANS_TABLES," (trailing comma)
108/// - "MODE1,,MODE2" (empty segments)
109///
110/// MySQL mode flags we recognize and map:
111/// - STRICT_TRANS_TABLES → strict_mode
112/// - PIPES_AS_CONCAT → pipes_as_concat
113/// - ANSI_QUOTES → ansi_quotes
114/// - ANSI → pipes_as_concat + ansi_quotes
115///
116/// Other MySQL modes (NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO,
117/// NO_ENGINE_SUBSTITUTION, ONLY_FULL_GROUP_BY, etc.) are accepted but ignored
118/// as they don't affect our behavior.
119fn parse_mysql_mode_flags(s: &str) -> Result<SqlMode, String> {
120    let mut flags = MySqlModeFlags::default();
121
122    // Split by comma and process each mode
123    for mode in s.split(',') {
124        // Skip empty segments (handles leading/trailing commas and consecutive commas)
125        let mode = mode.trim();
126        if mode.is_empty() {
127            continue;
128        }
129
130        // Match MySQL mode flags (case-insensitive)
131        match mode.to_uppercase().as_str() {
132            // Modes we actually implement
133            "STRICT_TRANS_TABLES" | "STRICT_ALL_TABLES" => {
134                flags.strict_mode = true;
135            }
136            "PIPES_AS_CONCAT" => {
137                flags.pipes_as_concat = true;
138            }
139            "ANSI_QUOTES" => {
140                flags.ansi_quotes = true;
141            }
142            "ANSI" => {
143                // ANSI mode is a combination
144                flags.pipes_as_concat = true;
145                flags.ansi_quotes = true;
146            }
147
148            // Common MySQL modes we accept but don't implement
149            // These are standard MySQL 8.0 defaults and commonly used modes
150            "NO_ZERO_IN_DATE"
151            | "NO_ZERO_DATE"
152            | "ERROR_FOR_DIVISION_BY_ZERO"
153            | "NO_ENGINE_SUBSTITUTION"
154            | "ONLY_FULL_GROUP_BY"
155            | "NO_AUTO_CREATE_USER"
156            | "NO_AUTO_VALUE_ON_ZERO"
157            | "NO_BACKSLASH_ESCAPES"
158            | "NO_DIR_IN_CREATE"
159            | "NO_UNSIGNED_SUBTRACTION"
160            | "PAD_CHAR_TO_FULL_LENGTH"
161            | "REAL_AS_FLOAT"
162            | "TIME_TRUNCATE_FRACTIONAL"
163            | "IGNORE_SPACE"
164            | "TRADITIONAL"
165            | "ALLOW_INVALID_DATES"
166            | "HIGH_NOT_PRECEDENCE" => {
167                // Accepted but not implemented - no action needed
168            }
169
170            // Unknown mode - but we're lenient to allow future MySQL versions
171            // We only error on truly unrecognized patterns
172            other => {
173                // Check if it looks like a MySQL mode (alphanumeric with underscores)
174                if other.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
175                    // Looks like a MySQL mode we don't know - accept it silently
176                    // This allows forward compatibility with new MySQL modes
177                } else {
178                    return Err(format!(
179                        "Unknown SQL mode: '{}'. Valid modes: mysql, mysql_slt, sqlite, or MySQL mode flags",
180                        s
181                    ));
182                }
183            }
184        }
185    }
186
187    Ok(SqlMode::MySQL { flags })
188}
189
190impl SqlMode {
191    /// Get the MySQL mode flags, if in MySQL mode
192    pub fn mysql_flags(&self) -> Option<&MySqlModeFlags> {
193        match self {
194            SqlMode::MySQL { flags } => Some(flags),
195            SqlMode::SQLite => None,
196        }
197    }
198}
199
200// Supported collations for each SQL mode
201#[allow(dead_code)]
202const MYSQL_SUPPORTED_COLLATIONS: &[Collation] =
203    &[Collation::Binary, Collation::Utf8Binary, Collation::Utf8GeneralCi];
204
205#[allow(dead_code)]
206const SQLITE_SUPPORTED_COLLATIONS: &[Collation] =
207    &[Collation::Binary, Collation::NoCase, Collation::Rtrim];
208
209impl StringBehavior for SqlMode {
210    fn default_string_comparison_case_sensitive(&self) -> bool {
211        match self {
212            SqlMode::MySQL { .. } => false, // MySQL defaults to case-insensitive
213            SqlMode::SQLite => true,        // SQLite defaults to case-sensitive
214        }
215    }
216
217    fn default_collation(&self) -> Collation {
218        match self {
219            SqlMode::MySQL { .. } => Collation::Utf8GeneralCi, // MySQL's default collation
220            SqlMode::SQLite => Collation::Binary,              // SQLite's default collation
221        }
222    }
223
224    fn supported_collations(&self) -> &[Collation] {
225        match self {
226            SqlMode::MySQL { .. } => MYSQL_SUPPORTED_COLLATIONS,
227            SqlMode::SQLite => SQLITE_SUPPORTED_COLLATIONS,
228        }
229    }
230}
231
232impl OperatorBehavior for SqlMode {
233    fn integer_division_behavior(&self) -> DivisionBehavior {
234        match self {
235            SqlMode::MySQL { .. } => DivisionBehavior::Decimal,
236            SqlMode::SQLite => DivisionBehavior::Integer,
237        }
238    }
239
240    fn supports_xor(&self) -> bool {
241        match self {
242            SqlMode::MySQL { .. } => true,
243            SqlMode::SQLite => false,
244        }
245    }
246
247    fn supports_integer_div_operator(&self) -> bool {
248        match self {
249            SqlMode::MySQL { .. } => true,
250            SqlMode::SQLite => false,
251        }
252    }
253
254    fn string_concat_operator(&self) -> ConcatOperator {
255        match self {
256            SqlMode::MySQL { .. } => ConcatOperator::Function,
257            SqlMode::SQLite => ConcatOperator::PipePipe,
258        }
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_default_is_mysql() {
268        // Default is MySQL for SQLLogicTest compatibility (MySQL 8.x test suite)
269        let mode = SqlMode::default();
270        assert!(matches!(mode, SqlMode::MySQL { .. }));
271    }
272
273    #[test]
274    fn test_mysql_mode_has_default_flags() {
275        // Test that MySQL mode can be constructed with default flags
276        let mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
277        if let SqlMode::MySQL { flags } = mode {
278            assert_eq!(flags, MySqlModeFlags::default());
279        } else {
280            panic!("Expected MySQL mode");
281        }
282    }
283
284    #[test]
285    fn test_division_behavior() {
286        use crate::{
287            sql_mode::types::{TypeBehavior, ValueType},
288            SqlValue,
289        };
290
291        let mysql_mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
292        // MySQL returns Numeric for division
293        assert_eq!(
294            mysql_mode.division_result_type(&SqlValue::Integer(5), &SqlValue::Integer(2)),
295            ValueType::Numeric
296        );
297
298        let sqlite_mode = SqlMode::SQLite;
299        // SQLite returns Integer for int/int division
300        assert_eq!(
301            sqlite_mode.division_result_type(&SqlValue::Integer(5), &SqlValue::Integer(2)),
302            ValueType::Integer
303        );
304    }
305
306    #[test]
307    fn test_mysql_flags_accessor() {
308        let mysql_mode = SqlMode::MySQL { flags: MySqlModeFlags::with_pipes_as_concat() };
309        assert!(mysql_mode.mysql_flags().is_some());
310        assert!(mysql_mode.mysql_flags().unwrap().pipes_as_concat);
311
312        let sqlite_mode = SqlMode::SQLite;
313        assert!(sqlite_mode.mysql_flags().is_none());
314    }
315
316    #[test]
317    fn test_sqlmode_with_flags() {
318        let mode = SqlMode::MySQL {
319            flags: MySqlModeFlags { pipes_as_concat: true, ..Default::default() },
320        };
321
322        match mode {
323            SqlMode::MySQL { flags } => {
324                assert!(flags.pipes_as_concat);
325                assert!(!flags.ansi_quotes);
326                assert!(!flags.strict_mode);
327            }
328            _ => panic!("Expected MySQL mode"),
329        }
330    }
331
332    #[test]
333    fn test_sqlmode_with_custom_flags() {
334        let mode = SqlMode::MySQL { flags: MySqlModeFlags::ansi() };
335
336        match mode {
337            SqlMode::MySQL { flags } => {
338                assert!(flags.pipes_as_concat);
339                assert!(flags.ansi_quotes);
340                assert!(!flags.strict_mode);
341            }
342            _ => panic!("Expected MySQL mode"),
343        }
344    }
345
346    #[test]
347    fn test_mysql_string_comparison() {
348        let mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
349        assert!(!mode.default_string_comparison_case_sensitive());
350        assert_eq!(mode.default_collation(), Collation::Utf8GeneralCi);
351    }
352
353    #[test]
354    fn test_sqlite_string_comparison() {
355        let mode = SqlMode::SQLite;
356        assert!(mode.default_string_comparison_case_sensitive());
357        assert_eq!(mode.default_collation(), Collation::Binary);
358    }
359
360    #[test]
361    fn test_mysql_supported_collations() {
362        let mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
363        let collations = mode.supported_collations();
364        assert_eq!(collations.len(), 3);
365        assert!(collations.contains(&Collation::Binary));
366        assert!(collations.contains(&Collation::Utf8Binary));
367        assert!(collations.contains(&Collation::Utf8GeneralCi));
368    }
369
370    #[test]
371    fn test_sqlite_supported_collations() {
372        let mode = SqlMode::SQLite;
373        let collations = mode.supported_collations();
374        assert_eq!(collations.len(), 3);
375        assert!(collations.contains(&Collation::Binary));
376        assert!(collations.contains(&Collation::NoCase));
377        assert!(collations.contains(&Collation::Rtrim));
378    }
379
380    #[test]
381    fn test_collation_case_sensitivity_consistency() {
382        // MySQL default collation should be case-insensitive
383        let mysql_mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
384        assert!(!mysql_mode.default_string_comparison_case_sensitive());
385        assert_eq!(mysql_mode.default_collation(), Collation::Utf8GeneralCi);
386
387        // SQLite default collation should be case-sensitive
388        let sqlite_mode = SqlMode::SQLite;
389        assert!(sqlite_mode.default_string_comparison_case_sensitive());
390        assert_eq!(sqlite_mode.default_collation(), Collation::Binary);
391    }
392
393    // Tests for OperatorBehavior trait implementation
394
395    #[test]
396    fn test_integer_division_behavior() {
397        let mysql_mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
398        assert_eq!(mysql_mode.integer_division_behavior(), DivisionBehavior::Decimal);
399
400        let sqlite_mode = SqlMode::SQLite;
401        assert_eq!(sqlite_mode.integer_division_behavior(), DivisionBehavior::Integer);
402    }
403
404    #[test]
405    fn test_xor_support() {
406        let mysql_mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
407        assert!(mysql_mode.supports_xor());
408
409        let sqlite_mode = SqlMode::SQLite;
410        assert!(!sqlite_mode.supports_xor());
411    }
412
413    #[test]
414    fn test_integer_div_operator_support() {
415        let mysql_mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
416        assert!(mysql_mode.supports_integer_div_operator());
417
418        let sqlite_mode = SqlMode::SQLite;
419        assert!(!sqlite_mode.supports_integer_div_operator());
420    }
421
422    #[test]
423    fn test_string_concat_operator() {
424        let mysql_mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
425        assert_eq!(mysql_mode.string_concat_operator(), ConcatOperator::Function);
426
427        let sqlite_mode = SqlMode::SQLite;
428        assert_eq!(sqlite_mode.string_concat_operator(), ConcatOperator::PipePipe);
429    }
430
431    // Tests for FromStr and Display implementations
432
433    #[test]
434    fn test_display_mysql() {
435        let mode = SqlMode::MySQL { flags: MySqlModeFlags::default() };
436        assert_eq!(mode.to_string(), "mysql");
437    }
438
439    #[test]
440    fn test_display_sqlite() {
441        let mode = SqlMode::SQLite;
442        assert_eq!(mode.to_string(), "sqlite");
443    }
444
445    #[test]
446    fn test_from_str_mysql() {
447        let mode: SqlMode = "mysql".parse().unwrap();
448        assert!(matches!(mode, SqlMode::MySQL { .. }));
449    }
450
451    #[test]
452    fn test_from_str_sqlite() {
453        let mode: SqlMode = "sqlite".parse().unwrap();
454        assert!(matches!(mode, SqlMode::SQLite));
455    }
456
457    #[test]
458    fn test_from_str_case_insensitive() {
459        let mode1: SqlMode = "MYSQL".parse().unwrap();
460        assert!(matches!(mode1, SqlMode::MySQL { .. }));
461
462        let mode2: SqlMode = "SQLite".parse().unwrap();
463        assert!(matches!(mode2, SqlMode::SQLite));
464
465        let mode3: SqlMode = "MySql".parse().unwrap();
466        assert!(matches!(mode3, SqlMode::MySQL { .. }));
467    }
468
469    #[test]
470    fn test_from_str_mysql_slt() {
471        // mysql_slt mode: MySQL syntax with SQLite division semantics
472        let mode: SqlMode = "mysql_slt".parse().unwrap();
473        match mode {
474            SqlMode::MySQL { flags } => {
475                assert!(flags.sqlite_division_semantics);
476                assert!(!flags.pipes_as_concat);
477                assert!(!flags.ansi_quotes);
478                assert!(!flags.strict_mode);
479            }
480            _ => panic!("Expected MySQL mode"),
481        }
482    }
483
484    #[test]
485    fn test_from_str_invalid() {
486        // Invalid patterns with special characters should still error
487        let result: Result<SqlMode, _> = "invalid!@#".parse();
488        assert!(result.is_err());
489        let err = result.unwrap_err();
490        assert!(err.contains("Unknown SQL mode"));
491    }
492
493    // Tests for MySQL mode flags parsing (issue #3074)
494
495    #[test]
496    fn test_from_str_mysql_mode_flags() {
497        // Standard MySQL 8.0 default mode string
498        let mode: SqlMode =
499            "ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"
500                .parse()
501                .unwrap();
502        match mode {
503            SqlMode::MySQL { flags } => {
504                assert!(flags.strict_mode);
505                assert!(!flags.pipes_as_concat);
506                assert!(!flags.ansi_quotes);
507            }
508            _ => panic!("Expected MySQL mode"),
509        }
510    }
511
512    #[test]
513    fn test_from_str_leading_comma() {
514        // Leading comma (from REPLACE() removing first mode like ONLY_FULL_GROUP_BY)
515        let mode: SqlMode =
516            ",STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"
517                .parse()
518                .unwrap();
519        match mode {
520            SqlMode::MySQL { flags } => {
521                assert!(flags.strict_mode);
522            }
523            _ => panic!("Expected MySQL mode"),
524        }
525    }
526
527    #[test]
528    fn test_from_str_trailing_comma() {
529        // Trailing comma
530        let mode: SqlMode = "STRICT_TRANS_TABLES,".parse().unwrap();
531        match mode {
532            SqlMode::MySQL { flags } => {
533                assert!(flags.strict_mode);
534            }
535            _ => panic!("Expected MySQL mode"),
536        }
537    }
538
539    #[test]
540    fn test_from_str_empty_segments() {
541        // Empty segments between commas
542        let mode: SqlMode = "STRICT_TRANS_TABLES,,PIPES_AS_CONCAT".parse().unwrap();
543        match mode {
544            SqlMode::MySQL { flags } => {
545                assert!(flags.strict_mode);
546                assert!(flags.pipes_as_concat);
547            }
548            _ => panic!("Expected MySQL mode"),
549        }
550    }
551
552    #[test]
553    fn test_from_str_empty_string() {
554        // Empty string should result in default flags
555        let mode: SqlMode = "".parse().unwrap();
556        match mode {
557            SqlMode::MySQL { flags } => {
558                assert!(!flags.strict_mode);
559                assert!(!flags.pipes_as_concat);
560                assert!(!flags.ansi_quotes);
561            }
562            _ => panic!("Expected MySQL mode"),
563        }
564    }
565
566    #[test]
567    fn test_from_str_only_commas() {
568        // Only commas should result in default flags
569        let mode: SqlMode = ",,,".parse().unwrap();
570        match mode {
571            SqlMode::MySQL { flags } => {
572                assert!(!flags.strict_mode);
573                assert!(!flags.pipes_as_concat);
574                assert!(!flags.ansi_quotes);
575            }
576            _ => panic!("Expected MySQL mode"),
577        }
578    }
579
580    #[test]
581    fn test_from_str_ansi_mode() {
582        // ANSI mode should set both pipes_as_concat and ansi_quotes
583        let mode: SqlMode = "ANSI".parse().unwrap();
584        match mode {
585            SqlMode::MySQL { flags } => {
586                assert!(flags.pipes_as_concat);
587                assert!(flags.ansi_quotes);
588            }
589            _ => panic!("Expected MySQL mode"),
590        }
591    }
592
593    #[test]
594    fn test_from_str_pipes_as_concat() {
595        let mode: SqlMode = "PIPES_AS_CONCAT".parse().unwrap();
596        match mode {
597            SqlMode::MySQL { flags } => {
598                assert!(flags.pipes_as_concat);
599                assert!(!flags.ansi_quotes);
600            }
601            _ => panic!("Expected MySQL mode"),
602        }
603    }
604
605    #[test]
606    fn test_from_str_ansi_quotes() {
607        let mode: SqlMode = "ANSI_QUOTES".parse().unwrap();
608        match mode {
609            SqlMode::MySQL { flags } => {
610                assert!(!flags.pipes_as_concat);
611                assert!(flags.ansi_quotes);
612            }
613            _ => panic!("Expected MySQL mode"),
614        }
615    }
616
617    #[test]
618    fn test_from_str_mode_flags_case_insensitive() {
619        // Mode flags should be case-insensitive
620        let mode: SqlMode = "strict_trans_tables,pipes_as_concat".parse().unwrap();
621        match mode {
622            SqlMode::MySQL { flags } => {
623                assert!(flags.strict_mode);
624                assert!(flags.pipes_as_concat);
625            }
626            _ => panic!("Expected MySQL mode"),
627        }
628    }
629
630    #[test]
631    fn test_from_str_mode_flags_with_whitespace() {
632        // Whitespace around modes should be trimmed
633        let mode: SqlMode = " STRICT_TRANS_TABLES , PIPES_AS_CONCAT ".parse().unwrap();
634        match mode {
635            SqlMode::MySQL { flags } => {
636                assert!(flags.strict_mode);
637                assert!(flags.pipes_as_concat);
638            }
639            _ => panic!("Expected MySQL mode"),
640        }
641    }
642
643    #[test]
644    fn test_from_str_unknown_mode_accepted() {
645        // Unknown MySQL-like modes should be accepted silently (forward compatibility)
646        let mode: SqlMode = "SOME_FUTURE_MODE,STRICT_TRANS_TABLES".parse().unwrap();
647        match mode {
648            SqlMode::MySQL { flags } => {
649                assert!(flags.strict_mode);
650            }
651            _ => panic!("Expected MySQL mode"),
652        }
653    }
654
655    #[test]
656    fn test_roundtrip() {
657        // MySQL roundtrip
658        let mysql = SqlMode::MySQL { flags: MySqlModeFlags::default() };
659        let mysql_str = mysql.to_string();
660        let mysql_parsed: SqlMode = mysql_str.parse().unwrap();
661        assert!(matches!(mysql_parsed, SqlMode::MySQL { .. }));
662
663        // SQLite roundtrip
664        let sqlite = SqlMode::SQLite;
665        let sqlite_str = sqlite.to_string();
666        let sqlite_parsed: SqlMode = sqlite_str.parse().unwrap();
667        assert!(matches!(sqlite_parsed, SqlMode::SQLite));
668    }
669}