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