Skip to main content

polyglot_sql/dialects/
exasol.rs

1//! Exasol SQL Dialect
2//!
3//! Exasol-specific SQL dialect based on sqlglot patterns.
4//!
5//! References:
6//! - SQL Reference: https://docs.exasol.com/db/latest/sql_references/basiclanguageelements.htm
7//! - Data Types: https://docs.exasol.com/db/latest/sql_references/data_types/datatypesoverview.htm
8//! - Functions: https://docs.exasol.com/db/latest/sql_references/functions/
9//!
10//! Key characteristics:
11//! - Uppercase normalization for identifiers
12//! - Identifiers: double quotes or square brackets
13//! - Date functions: ADD_DAYS, ADD_MONTHS, DAYS_BETWEEN, MONTHS_BETWEEN
14//! - Bitwise: BIT_AND, BIT_OR, BIT_XOR, BIT_NOT, BIT_LSHIFT, BIT_RSHIFT
15//! - Functions: ZEROIFNULL, NULLIFZERO, SYSTIMESTAMP
16//! - EVERY for ALL aggregate
17//! - No SEMI/ANTI join support
18//! - DATE_TRUNC for date truncation
19//! - IF...THEN...ELSE...ENDIF syntax
20
21use super::{DialectImpl, DialectType};
22use crate::error::Result;
23use crate::expressions::{
24    BinaryOp, Expression, Function, Identifier, LikeOp, ListAggFunc, Literal, Select, VarArgFunc,
25};
26#[cfg(feature = "generate")]
27use crate::generator::GeneratorConfig;
28use crate::tokens::TokenizerConfig;
29
30/// Exasol dialect
31pub struct ExasolDialect;
32
33impl DialectImpl for ExasolDialect {
34    fn dialect_type(&self) -> DialectType {
35        DialectType::Exasol
36    }
37
38    fn tokenizer_config(&self) -> TokenizerConfig {
39        let mut config = TokenizerConfig::default();
40        // Exasol uses double quotes for identifiers
41        config.identifiers.insert('"', '"');
42        // Also supports square brackets
43        config.identifiers.insert('[', ']');
44        config
45    }
46
47    #[cfg(feature = "generate")]
48
49    fn generator_config(&self) -> GeneratorConfig {
50        use crate::generator::IdentifierQuoteStyle;
51        GeneratorConfig {
52            identifier_quote: '"',
53            identifier_quote_style: IdentifierQuoteStyle::DOUBLE_QUOTE,
54            dialect: Some(DialectType::Exasol),
55            supports_column_join_marks: true,
56            // Exasol uses lowercase for window frame keywords (rows, preceding, following)
57            lowercase_window_frame_keywords: true,
58            ..Default::default()
59        }
60    }
61
62    #[cfg(feature = "transpile")]
63
64    fn transform_expr(&self, expr: Expression) -> Result<Expression> {
65        match expr {
66            // SYSTIMESTAMP -> SYSTIMESTAMP() (with parentheses in Exasol)
67            Expression::Systimestamp(_) => Ok(Expression::Function(Box::new(Function::new(
68                "SYSTIMESTAMP".to_string(),
69                vec![],
70            )))),
71
72            // WeekOfYear -> WEEK
73            Expression::WeekOfYear(f) => Ok(Expression::Function(Box::new(Function::new(
74                "WEEK".to_string(),
75                vec![f.this],
76            )))),
77
78            // COALESCE is native, but also support transformations from other forms
79            Expression::Nvl(f) => Ok(Expression::Coalesce(Box::new(VarArgFunc {
80                original_name: None,
81                expressions: vec![f.this, f.expression],
82                inferred_type: None,
83            }))),
84
85            Expression::IfNull(f) => Ok(Expression::Coalesce(Box::new(VarArgFunc {
86                original_name: None,
87                expressions: vec![f.this, f.expression],
88                inferred_type: None,
89            }))),
90
91            // Bitwise operations → BIT_* functions
92            Expression::BitwiseAnd(op) => Ok(Expression::Function(Box::new(Function::new(
93                "BIT_AND".to_string(),
94                vec![op.left, op.right],
95            )))),
96
97            Expression::BitwiseOr(op) => Ok(Expression::Function(Box::new(Function::new(
98                "BIT_OR".to_string(),
99                vec![op.left, op.right],
100            )))),
101
102            Expression::BitwiseXor(op) => Ok(Expression::Function(Box::new(Function::new(
103                "BIT_XOR".to_string(),
104                vec![op.left, op.right],
105            )))),
106
107            Expression::BitwiseNot(f) => Ok(Expression::Function(Box::new(Function::new(
108                "BIT_NOT".to_string(),
109                vec![f.this],
110            )))),
111
112            Expression::BitwiseLeftShift(op) => Ok(Expression::Function(Box::new(Function::new(
113                "BIT_LSHIFT".to_string(),
114                vec![op.left, op.right],
115            )))),
116
117            Expression::BitwiseRightShift(op) => Ok(Expression::Function(Box::new(Function::new(
118                "BIT_RSHIFT".to_string(),
119                vec![op.left, op.right],
120            )))),
121
122            // Modulo → MOD function
123            Expression::Mod(op) => Ok(Expression::Function(Box::new(Function::new(
124                "MOD".to_string(),
125                vec![op.left, op.right],
126            )))),
127
128            // GROUP_CONCAT -> LISTAGG in Exasol (with WITHIN GROUP for ORDER BY)
129            Expression::GroupConcat(f) => Ok(Expression::ListAgg(Box::new(ListAggFunc {
130                this: f.this,
131                separator: f.separator,
132                on_overflow: None,
133                order_by: f.order_by,
134                distinct: f.distinct,
135                filter: f.filter,
136                inferred_type: None,
137            }))),
138
139            // USER (no parens) -> CURRENT_USER
140            Expression::Column(col)
141                if col.table.is_none() && col.name.name.eq_ignore_ascii_case("USER") =>
142            {
143                Ok(Expression::CurrentUser(Box::new(
144                    crate::expressions::CurrentUser { this: None },
145                )))
146            }
147
148            // Generic function transformations
149            Expression::Function(f) => self.transform_function(*f),
150
151            // Aggregate function transformations
152            Expression::AggregateFunction(f) => self.transform_aggregate_function(f),
153
154            Expression::Select(select) => Ok(Expression::Select(Box::new(
155                self.qualify_local_alias_predicates(*select),
156            ))),
157
158            // Pass through everything else
159            _ => Ok(expr),
160        }
161    }
162}
163
164#[cfg(feature = "transpile")]
165impl ExasolDialect {
166    fn qualify_local_alias_predicates(&self, mut select: Select) -> Select {
167        let aliases = Self::select_aliases(&select);
168        if aliases.is_empty() {
169            return select;
170        }
171
172        if let Some(where_clause) = select.where_clause.as_mut() {
173            where_clause.this = Self::qualify_local_alias_expr(where_clause.this.clone(), &aliases);
174        }
175        if let Some(having) = select.having.as_mut() {
176            having.this = Self::qualify_local_alias_expr(having.this.clone(), &aliases);
177        }
178
179        select
180    }
181
182    fn select_aliases(select: &Select) -> std::collections::HashMap<String, Identifier> {
183        let mut aliases = std::collections::HashMap::new();
184        for expression in &select.expressions {
185            if let Expression::Alias(alias) = expression {
186                aliases.insert(alias.alias.name.to_ascii_uppercase(), alias.alias.clone());
187            }
188        }
189        aliases
190    }
191
192    fn qualify_local_alias_expr(
193        expr: Expression,
194        aliases: &std::collections::HashMap<String, Identifier>,
195    ) -> Expression {
196        match expr {
197            Expression::Column(col) if col.table.is_none() => {
198                if let Some(alias) = aliases.get(&col.name.name.to_ascii_uppercase()) {
199                    return Expression::Raw(crate::expressions::Raw {
200                        sql: format!("LOCAL.{}", alias.name),
201                    });
202                }
203                Expression::Column(col)
204            }
205            Expression::Identifier(id) => {
206                if let Some(alias) = aliases.get(&id.name.to_ascii_uppercase()) {
207                    Expression::Raw(crate::expressions::Raw {
208                        sql: format!("LOCAL.{}", alias.name),
209                    })
210                } else {
211                    Expression::Identifier(id)
212                }
213            }
214            Expression::And(op) => Self::qualify_binary(op, aliases, Expression::And),
215            Expression::Or(op) => Self::qualify_binary(op, aliases, Expression::Or),
216            Expression::Eq(op) => Self::qualify_binary(op, aliases, Expression::Eq),
217            Expression::Neq(op) => Self::qualify_binary(op, aliases, Expression::Neq),
218            Expression::Lt(op) => Self::qualify_binary(op, aliases, Expression::Lt),
219            Expression::Lte(op) => Self::qualify_binary(op, aliases, Expression::Lte),
220            Expression::Gt(op) => Self::qualify_binary(op, aliases, Expression::Gt),
221            Expression::Gte(op) => Self::qualify_binary(op, aliases, Expression::Gte),
222            Expression::Like(op) => Self::qualify_like(op, aliases, Expression::Like),
223            Expression::ILike(op) => Self::qualify_like(op, aliases, Expression::ILike),
224            Expression::Not(mut op) => {
225                op.this = Self::qualify_local_alias_expr(op.this, aliases);
226                Expression::Not(op)
227            }
228            other => other,
229        }
230    }
231
232    fn qualify_binary(
233        mut op: Box<BinaryOp>,
234        aliases: &std::collections::HashMap<String, Identifier>,
235        wrap: fn(Box<BinaryOp>) -> Expression,
236    ) -> Expression {
237        op.left = Self::qualify_local_alias_expr(op.left, aliases);
238        op.right = Self::qualify_local_alias_expr(op.right, aliases);
239        wrap(op)
240    }
241
242    fn qualify_like(
243        mut op: Box<LikeOp>,
244        aliases: &std::collections::HashMap<String, Identifier>,
245        wrap: fn(Box<LikeOp>) -> Expression,
246    ) -> Expression {
247        op.left = Self::qualify_local_alias_expr(op.left, aliases);
248        op.right = Self::qualify_local_alias_expr(op.right, aliases);
249        if let Some(escape) = op.escape.take() {
250            op.escape = Some(Self::qualify_local_alias_expr(escape, aliases));
251        }
252        wrap(op)
253    }
254
255    fn transform_function(&self, f: Function) -> Result<Expression> {
256        let name_upper = f.name.to_uppercase();
257        match name_upper.as_str() {
258            // SYSTIMESTAMP -> SYSTIMESTAMP() (with parentheses in Exasol)
259            // Exasol requires parentheses even for no-arg functions
260            // Preserve any arguments (like precision)
261            "SYSTIMESTAMP" => Ok(Expression::Function(Box::new(Function::new(
262                "SYSTIMESTAMP".to_string(),
263                f.args,
264            )))),
265
266            // ALL → EVERY
267            "ALL" => Ok(Expression::Function(Box::new(Function::new(
268                "EVERY".to_string(),
269                f.args,
270            )))),
271
272            // IFNULL/ISNULL/NVL → COALESCE (native in Exasol)
273            "IFNULL" | "ISNULL" | "NVL" if f.args.len() == 2 => {
274                Ok(Expression::Coalesce(Box::new(VarArgFunc {
275                    original_name: None,
276                    expressions: f.args,
277                    inferred_type: None,
278                })))
279            }
280
281            // DateDiff → DAYS_BETWEEN (for DAY unit) or other *_BETWEEN functions
282            "DATEDIFF" => Ok(Expression::Function(Box::new(Function::new(
283                "DAYS_BETWEEN".to_string(),
284                f.args,
285            )))),
286
287            // DateAdd → ADD_DAYS (for DAY unit) or other ADD_* functions
288            "DATEADD" | "DATE_ADD" => Ok(Expression::Function(Box::new(Function::new(
289                "ADD_DAYS".to_string(),
290                f.args,
291            )))),
292
293            // DateSub → Negate and use ADD_DAYS
294            "DATESUB" | "DATE_SUB" => {
295                // Would need to negate the interval, for now just use ADD_DAYS
296                Ok(Expression::Function(Box::new(Function::new(
297                    "ADD_DAYS".to_string(),
298                    f.args,
299                ))))
300            }
301
302            // DATE_TRUNC is native
303            "DATE_TRUNC" | "TRUNC" => Ok(Expression::Function(Box::new(f))),
304
305            // LEVENSHTEIN → EDIT_DISTANCE
306            "LEVENSHTEIN" | "LEVENSHTEIN_DISTANCE" => Ok(Expression::Function(Box::new(
307                Function::new("EDIT_DISTANCE".to_string(), f.args),
308            ))),
309
310            // REGEXP_EXTRACT → REGEXP_SUBSTR
311            "REGEXP_EXTRACT" => Ok(Expression::Function(Box::new(Function::new(
312                "REGEXP_SUBSTR".to_string(),
313                f.args,
314            )))),
315
316            // SHA/SHA1 → HASH_SHA
317            "SHA" | "SHA1" => Ok(Expression::Function(Box::new(Function::new(
318                "HASH_SHA".to_string(),
319                f.args,
320            )))),
321
322            // MD5 → HASH_MD5
323            "MD5" => Ok(Expression::Function(Box::new(Function::new(
324                "HASH_MD5".to_string(),
325                f.args,
326            )))),
327
328            // SHA256 → HASH_SHA256
329            "SHA256" | "SHA2" => {
330                // SHA2 in some dialects takes a length parameter
331                // HASH_SHA256 in Exasol just takes the value
332                let arg = f
333                    .args
334                    .into_iter()
335                    .next()
336                    .unwrap_or(Expression::Null(crate::expressions::Null));
337                Ok(Expression::Function(Box::new(Function::new(
338                    "HASH_SHA256".to_string(),
339                    vec![arg],
340                ))))
341            }
342
343            // SHA512 → HASH_SHA512
344            "SHA512" => Ok(Expression::Function(Box::new(Function::new(
345                "HASH_SHA512".to_string(),
346                f.args,
347            )))),
348
349            // VAR_POP is native
350            "VAR_POP" | "VARIANCE_POP" => Ok(Expression::Function(Box::new(Function::new(
351                "VAR_POP".to_string(),
352                f.args,
353            )))),
354
355            // APPROX_DISTINCT → APPROXIMATE_COUNT_DISTINCT
356            "APPROX_DISTINCT" | "APPROX_COUNT_DISTINCT" => Ok(Expression::Function(Box::new(
357                Function::new("APPROXIMATE_COUNT_DISTINCT".to_string(), f.args),
358            ))),
359
360            // TO_CHAR is native for date formatting
361            // DATE_FORMAT/STRFTIME: convert format codes and CAST value to TIMESTAMP
362            "TO_CHAR" | "DATE_FORMAT" | "STRFTIME" => {
363                let mut args = f.args;
364                if args.len() >= 2 {
365                    // Convert format codes from C-style (%Y, %m, etc.) to Exasol format
366                    if let Expression::Literal(lit) = &args[1] {
367                        if let Literal::String(fmt) = lit.as_ref() {
368                            let exasol_fmt = Self::convert_c_format_to_exasol(fmt);
369                            args[1] = Expression::Literal(Box::new(Literal::String(exasol_fmt)));
370                        }
371                    }
372                    // CAST string literal values to TIMESTAMP for date formatting functions
373                    if matches!(&args[0], Expression::Literal(lit) if matches!(lit.as_ref(), Literal::String(_)))
374                        && (f.name.eq_ignore_ascii_case("DATE_FORMAT")
375                            || f.name.eq_ignore_ascii_case("STRFTIME"))
376                    {
377                        args[0] = Expression::Cast(Box::new(crate::expressions::Cast {
378                            this: args[0].clone(),
379                            to: crate::expressions::DataType::Timestamp {
380                                timezone: false,
381                                precision: None,
382                            },
383                            trailing_comments: vec![],
384                            double_colon_syntax: false,
385                            format: None,
386                            default: None,
387                            inferred_type: None,
388                        }));
389                    }
390                }
391                Ok(Expression::Function(Box::new(Function::new(
392                    "TO_CHAR".to_string(),
393                    args,
394                ))))
395            }
396
397            // TO_DATE is native but format specifiers need uppercasing
398            "TO_DATE" => {
399                if f.args.len() >= 2 {
400                    // Uppercase format string if present
401                    let mut new_args = f.args.clone();
402                    if let Expression::Literal(lit) = &f.args[1] {
403                        if let Literal::String(fmt) = lit.as_ref() {
404                            new_args[1] = Expression::Literal(Box::new(Literal::String(
405                                Self::uppercase_exasol_format(fmt),
406                            )));
407                        }
408                    }
409                    Ok(Expression::Function(Box::new(Function::new(
410                        "TO_DATE".to_string(),
411                        new_args,
412                    ))))
413                } else {
414                    Ok(Expression::Function(Box::new(f)))
415                }
416            }
417
418            // TIME_TO_STR -> TO_CHAR with format conversion
419            "TIME_TO_STR" => {
420                if f.args.len() >= 2 {
421                    let mut new_args = vec![f.args[0].clone()];
422                    if let Expression::Literal(lit) = &f.args[1] {
423                        if let Literal::String(fmt) = lit.as_ref() {
424                            new_args.push(Expression::Literal(Box::new(Literal::String(
425                                Self::convert_strptime_to_exasol_format(fmt),
426                            ))));
427                        }
428                    } else {
429                        new_args.push(f.args[1].clone());
430                    }
431                    Ok(Expression::Function(Box::new(Function::new(
432                        "TO_CHAR".to_string(),
433                        new_args,
434                    ))))
435                } else {
436                    Ok(Expression::Function(Box::new(Function::new(
437                        "TO_CHAR".to_string(),
438                        f.args,
439                    ))))
440                }
441            }
442
443            // STR_TO_TIME -> TO_DATE with format conversion
444            "STR_TO_TIME" => {
445                if f.args.len() >= 2 {
446                    let mut new_args = vec![f.args[0].clone()];
447                    if let Expression::Literal(lit) = &f.args[1] {
448                        if let Literal::String(fmt) = lit.as_ref() {
449                            new_args.push(Expression::Literal(Box::new(Literal::String(
450                                Self::convert_strptime_to_exasol_format(fmt),
451                            ))));
452                        }
453                    } else {
454                        new_args.push(f.args[1].clone());
455                    }
456                    Ok(Expression::Function(Box::new(Function::new(
457                        "TO_DATE".to_string(),
458                        new_args,
459                    ))))
460                } else {
461                    Ok(Expression::Function(Box::new(Function::new(
462                        "TO_DATE".to_string(),
463                        f.args,
464                    ))))
465                }
466            }
467
468            // TO_TIMESTAMP is native
469            "TO_TIMESTAMP" => Ok(Expression::Function(Box::new(f))),
470
471            // CONVERT_TZ for timezone conversion
472            "CONVERT_TIMEZONE" | "AT_TIME_ZONE" => Ok(Expression::Function(Box::new(
473                Function::new("CONVERT_TZ".to_string(), f.args),
474            ))),
475
476            // STRPOS/POSITION → INSTR
477            "STRPOS" | "POSITION" | "CHARINDEX" | "LOCATE" => Ok(Expression::Function(Box::new(
478                Function::new("INSTR".to_string(), f.args),
479            ))),
480
481            // WEEK_OF_YEAR → WEEK
482            "WEEK_OF_YEAR" | "WEEKOFYEAR" => Ok(Expression::Function(Box::new(Function::new(
483                "WEEK".to_string(),
484                f.args,
485            )))),
486
487            // LAST_DAY is not native, would need complex transformation
488            "LAST_DAY" => {
489                // Exasol doesn't have LAST_DAY, but we can compute it
490                // For now, pass through
491                Ok(Expression::Function(Box::new(f)))
492            }
493
494            // CURDATE -> CURRENT_DATE
495            "CURDATE" => Ok(Expression::CurrentDate(crate::expressions::CurrentDate)),
496
497            // USER / USER() -> CURRENT_USER
498            "USER" if f.args.is_empty() => Ok(Expression::CurrentUser(Box::new(
499                crate::expressions::CurrentUser { this: None },
500            ))),
501
502            // NOW -> CURRENT_TIMESTAMP
503            "NOW" => Ok(Expression::CurrentTimestamp(
504                crate::expressions::CurrentTimestamp {
505                    precision: None,
506                    sysdate: false,
507                },
508            )),
509
510            // Pass through everything else
511            _ => Ok(Expression::Function(Box::new(f))),
512        }
513    }
514
515    fn transform_aggregate_function(
516        &self,
517        f: Box<crate::expressions::AggregateFunction>,
518    ) -> Result<Expression> {
519        let name_upper = f.name.to_uppercase();
520        match name_upper.as_str() {
521            // ALL → EVERY
522            "ALL" | "EVERY" => Ok(Expression::Function(Box::new(Function::new(
523                "EVERY".to_string(),
524                f.args,
525            )))),
526
527            // GROUP_CONCAT / STRING_AGG → LISTAGG (native with WITHIN GROUP)
528            "GROUP_CONCAT" | "STRING_AGG" => Ok(Expression::Function(Box::new(Function::new(
529                "LISTAGG".to_string(),
530                f.args,
531            )))),
532
533            // LISTAGG is native
534            "LISTAGG" => Ok(Expression::AggregateFunction(f)),
535
536            // APPROX_DISTINCT → APPROXIMATE_COUNT_DISTINCT
537            "APPROX_DISTINCT" | "APPROX_COUNT_DISTINCT" => Ok(Expression::Function(Box::new(
538                Function::new("APPROXIMATE_COUNT_DISTINCT".to_string(), f.args),
539            ))),
540
541            // Pass through everything else
542            _ => Ok(Expression::AggregateFunction(f)),
543        }
544    }
545
546    /// Convert strptime format string to Exasol format string
547    /// Exasol TIME_MAPPING (reverse of Python sqlglot):
548    /// %Y -> YYYY, %y -> YY, %m -> MM, %d -> DD, %H -> HH, %M -> MI, %S -> SS, %a -> DY
549    fn convert_strptime_to_exasol_format(format: &str) -> String {
550        let mut result = String::new();
551        let chars: Vec<char> = format.chars().collect();
552        let mut i = 0;
553        while i < chars.len() {
554            if chars[i] == '%' && i + 1 < chars.len() {
555                let spec = chars[i + 1];
556                let exasol_spec = match spec {
557                    'Y' => "YYYY",
558                    'y' => "YY",
559                    'm' => "MM",
560                    'd' => "DD",
561                    'H' => "HH",
562                    'M' => "MI",
563                    'S' => "SS",
564                    'a' => "DY",    // abbreviated weekday name
565                    'A' => "DAY",   // full weekday name
566                    'b' => "MON",   // abbreviated month name
567                    'B' => "MONTH", // full month name
568                    'I' => "H12",   // 12-hour format
569                    'u' => "ID",    // ISO weekday (1-7)
570                    'V' => "IW",    // ISO week number
571                    'G' => "IYYY",  // ISO year
572                    'W' => "UW",    // Week number (Monday as first day)
573                    'U' => "UW",    // Week number (Sunday as first day)
574                    'z' => "Z",     // timezone offset
575                    _ => {
576                        // Unknown specifier, keep as-is
577                        result.push('%');
578                        result.push(spec);
579                        i += 2;
580                        continue;
581                    }
582                };
583                result.push_str(exasol_spec);
584                i += 2;
585            } else {
586                result.push(chars[i]);
587                i += 1;
588            }
589        }
590        result
591    }
592
593    /// Convert C-style / MySQL format codes to Exasol format codes.
594    /// Handles both standard strptime codes and MySQL-specific codes (%i, %T).
595    fn convert_c_format_to_exasol(format: &str) -> String {
596        let mut result = String::new();
597        let chars: Vec<char> = format.chars().collect();
598        let mut i = 0;
599        while i < chars.len() {
600            if chars[i] == '%' && i + 1 < chars.len() {
601                let spec = chars[i + 1];
602                let exasol_spec = match spec {
603                    'Y' => "YYYY",
604                    'y' => "YY",
605                    'm' => "MM",
606                    'd' => "DD",
607                    'H' => "HH",
608                    'M' => "MI", // strptime minutes
609                    'i' => "MI", // MySQL minutes
610                    'S' | 's' => "SS",
611                    'T' => "HH:MI:SS", // MySQL %T = time (HH:MM:SS)
612                    'a' => "DY",       // abbreviated weekday name
613                    'A' => "DAY",      // full weekday name
614                    'b' => "MON",      // abbreviated month name
615                    'B' => "MONTH",    // full month name
616                    'I' => "H12",      // 12-hour format
617                    'u' => "ID",       // ISO weekday (1-7)
618                    'V' => "IW",       // ISO week number
619                    'G' => "IYYY",     // ISO year
620                    'W' => "UW",       // Week number
621                    'U' => "UW",       // Week number
622                    'z' => "Z",        // timezone offset
623                    _ => {
624                        // Unknown specifier, keep as-is
625                        result.push('%');
626                        result.push(spec);
627                        i += 2;
628                        continue;
629                    }
630                };
631                result.push_str(exasol_spec);
632                i += 2;
633            } else {
634                result.push(chars[i]);
635                i += 1;
636            }
637        }
638        result
639    }
640
641    /// Uppercase Exasol format specifiers (DD, MM, YYYY, etc.)
642    /// Converts lowercase format strings like 'dd-mm-yyyy' to 'DD-MM-YYYY'
643    fn uppercase_exasol_format(format: &str) -> String {
644        // Exasol format specifiers are always uppercase
645        format.to_uppercase()
646    }
647}
648
649// Note: Exasol type mappings (handled in generator if needed):
650// - BLOB, LONGBLOB, etc. → VARCHAR
651// - TEXT → LONG VARCHAR
652// - VARBINARY → VARCHAR
653// - TINYINT → SMALLINT
654// - MEDIUMINT → INT
655// - DECIMAL32/64/128/256 → DECIMAL
656// - DATETIME → TIMESTAMP
657// - TIMESTAMPTZ/TIMESTAMPLTZ/TIMESTAMPNTZ → TIMESTAMP
658//
659// Exasol also supports:
660// - TIMESTAMP WITH LOCAL TIME ZONE (fixed precision of 3)
661// - IF...THEN...ELSE...ENDIF syntax for conditionals