Skip to main content

polyglot_sql/dialects/
oracle.rs

1//! Oracle Dialect
2//!
3//! Oracle-specific transformations based on sqlglot patterns.
4//! Key differences:
5//! - NVL is native (preferred over COALESCE)
6//! - SYSDATE for current timestamp
7//! - DBMS_RANDOM.VALUE for random numbers
8//! - No ILIKE support (use LOWER + LIKE)
9//! - SUBSTR instead of SUBSTRING
10//! - TO_CHAR, TO_DATE, TO_TIMESTAMP for date/time formatting
11//! - No TRY_CAST (must use CAST)
12//! - INSTR instead of POSITION/STRPOS
13//! - TRUNC for date truncation
14//! - MINUS instead of EXCEPT
15
16use super::{DialectImpl, DialectType};
17use crate::error::Result;
18use crate::expressions::{BinaryFunc, CeilFunc, Expression, Function, LikeOp, UnaryFunc};
19use crate::generator::GeneratorConfig;
20use crate::tokens::TokenizerConfig;
21
22/// Oracle dialect
23pub struct OracleDialect;
24
25impl DialectImpl for OracleDialect {
26    fn dialect_type(&self) -> DialectType {
27        DialectType::Oracle
28    }
29
30    fn tokenizer_config(&self) -> TokenizerConfig {
31        let mut config = TokenizerConfig::default();
32        // Oracle uses double quotes for identifiers
33        config.identifiers.insert('"', '"');
34        // Oracle does not support nested comments
35        config.nested_comments = false;
36        config
37    }
38
39    fn generator_config(&self) -> GeneratorConfig {
40        use crate::generator::IdentifierQuoteStyle;
41        GeneratorConfig {
42            identifier_quote: '"',
43            identifier_quote_style: IdentifierQuoteStyle::DOUBLE_QUOTE,
44            dialect: Some(DialectType::Oracle),
45            supports_column_join_marks: true,
46            // Oracle doesn't use COLUMN keyword in ALTER TABLE ADD
47            alter_table_include_column_keyword: false,
48            // Oracle uses SAMPLE instead of TABLESAMPLE
49            tablesample_keywords: "SAMPLE",
50            // Oracle places alias after the SAMPLE clause
51            alias_post_tablesample: true,
52            // Oracle uses TIMESTAMP WITH TIME ZONE syntax (not TIMESTAMPTZ)
53            tz_to_with_time_zone: true,
54            ..Default::default()
55        }
56    }
57
58    fn transform_expr(&self, expr: Expression) -> Result<Expression> {
59        match expr {
60            // IFNULL -> NVL in Oracle
61            Expression::IfNull(f) => Ok(Expression::Nvl(f)),
62
63            // COALESCE with 2 args -> NVL in Oracle (optimization)
64            Expression::Coalesce(f) if f.expressions.len() == 2 => {
65                let mut exprs = f.expressions;
66                let second = exprs.pop().unwrap();
67                let first = exprs.pop().unwrap();
68                Ok(Expression::Nvl(Box::new(BinaryFunc {
69                    original_name: None,
70                    this: first,
71                    expression: second,
72                    inferred_type: None,
73                })))
74            }
75
76            // NVL stays as NVL (native to Oracle)
77            Expression::Nvl(f) => Ok(Expression::Nvl(f)),
78
79            // TryCast -> CAST in Oracle (no TRY_CAST support)
80            Expression::TryCast(c) => Ok(Expression::Cast(c)),
81
82            // SafeCast -> CAST in Oracle
83            Expression::SafeCast(c) => Ok(Expression::Cast(c)),
84
85            // ILIKE -> LOWER() LIKE LOWER() in Oracle (no ILIKE support)
86            Expression::ILike(op) => {
87                let lower_left = Expression::Lower(Box::new(UnaryFunc::new(op.left)));
88                let lower_right = Expression::Lower(Box::new(UnaryFunc::new(op.right)));
89                Ok(Expression::Like(Box::new(LikeOp {
90                    left: lower_left,
91                    right: lower_right,
92                    escape: op.escape,
93                    quantifier: op.quantifier,
94                    inferred_type: None,
95                })))
96            }
97
98            // RANDOM -> DBMS_RANDOM.VALUE in Oracle
99            Expression::Random(_) => Ok(Expression::Function(Box::new(Function::new(
100                "DBMS_RANDOM.VALUE".to_string(),
101                vec![],
102            )))),
103
104            // Rand -> DBMS_RANDOM.VALUE in Oracle
105            Expression::Rand(_) => Ok(Expression::Function(Box::new(Function::new(
106                "DBMS_RANDOM.VALUE".to_string(),
107                vec![],
108            )))),
109
110            // || (Concat) is native to Oracle
111            Expression::Concat(op) => Ok(Expression::Concat(op)),
112
113            // UNNEST -> Not directly supported in Oracle
114            // Would need TABLE() with a collection type
115            Expression::Unnest(f) => Ok(Expression::Function(Box::new(Function::new(
116                "TABLE".to_string(),
117                vec![f.this],
118            )))),
119
120            // EXPLODE -> TABLE in Oracle
121            Expression::Explode(f) => Ok(Expression::Function(Box::new(Function::new(
122                "TABLE".to_string(),
123                vec![f.this],
124            )))),
125
126            // Generic function transformations
127            Expression::Function(f) => self.transform_function(*f),
128
129            // Generic aggregate function transformations
130            Expression::AggregateFunction(f) => self.transform_aggregate_function(f),
131
132            // Pass through everything else
133            _ => Ok(expr),
134        }
135    }
136}
137
138impl OracleDialect {
139    fn transform_function(&self, f: Function) -> Result<Expression> {
140        let name_upper = f.name.to_uppercase();
141        match name_upper.as_str() {
142            // IFNULL -> NVL
143            "IFNULL" if f.args.len() == 2 => {
144                let mut args = f.args;
145                let second = args.pop().unwrap();
146                let first = args.pop().unwrap();
147                Ok(Expression::Nvl(Box::new(BinaryFunc {
148                    original_name: None,
149                    this: first,
150                    expression: second,
151                    inferred_type: None,
152                })))
153            }
154
155            // ISNULL -> NVL
156            "ISNULL" if f.args.len() == 2 => {
157                let mut args = f.args;
158                let second = args.pop().unwrap();
159                let first = args.pop().unwrap();
160                Ok(Expression::Nvl(Box::new(BinaryFunc {
161                    original_name: None,
162                    this: first,
163                    expression: second,
164                    inferred_type: None,
165                })))
166            }
167
168            // NVL is native to Oracle
169            "NVL" if f.args.len() == 2 => {
170                let mut args = f.args;
171                let second = args.pop().unwrap();
172                let first = args.pop().unwrap();
173                Ok(Expression::Nvl(Box::new(BinaryFunc {
174                    original_name: None,
175                    this: first,
176                    expression: second,
177                    inferred_type: None,
178                })))
179            }
180
181            // NVL2 is native to Oracle
182            "NVL2" => Ok(Expression::Function(Box::new(f))),
183
184            // GROUP_CONCAT -> LISTAGG in Oracle
185            "GROUP_CONCAT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
186                Function::new("LISTAGG".to_string(), f.args),
187            ))),
188
189            // STRING_AGG -> LISTAGG in Oracle
190            "STRING_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
191                Function::new("LISTAGG".to_string(), f.args),
192            ))),
193
194            // LISTAGG is native to Oracle
195            "LISTAGG" => Ok(Expression::Function(Box::new(f))),
196
197            // SUBSTRING -> SUBSTR in Oracle
198            "SUBSTRING" => Ok(Expression::Function(Box::new(Function::new(
199                "SUBSTR".to_string(),
200                f.args,
201            )))),
202
203            // SUBSTR is native to Oracle
204            "SUBSTR" => Ok(Expression::Function(Box::new(f))),
205
206            // LENGTH is native to Oracle
207            "LENGTH" => Ok(Expression::Function(Box::new(f))),
208
209            // LEN -> LENGTH
210            "LEN" if f.args.len() == 1 => Ok(Expression::Length(Box::new(UnaryFunc::new(
211                f.args.into_iter().next().unwrap(),
212            )))),
213
214            // RANDOM -> DBMS_RANDOM.VALUE
215            "RANDOM" | "RAND" => Ok(Expression::Function(Box::new(Function::new(
216                "DBMS_RANDOM.VALUE".to_string(),
217                vec![],
218            )))),
219
220            // NOW -> SYSDATE or CURRENT_TIMESTAMP
221            "NOW" => Ok(Expression::CurrentTimestamp(
222                crate::expressions::CurrentTimestamp {
223                    precision: None,
224                    sysdate: false,
225                },
226            )),
227
228            // GETDATE -> SYSDATE
229            "GETDATE" => Ok(Expression::Function(Box::new(Function::new(
230                "SYSDATE".to_string(),
231                vec![],
232            )))),
233
234            // CURRENT_TIMESTAMP is native (or SYSDATE)
235            // If it has arguments, keep as function to preserve them
236            "CURRENT_TIMESTAMP" => {
237                if f.args.is_empty() {
238                    Ok(Expression::CurrentTimestamp(
239                        crate::expressions::CurrentTimestamp {
240                            precision: None,
241                            sysdate: false,
242                        },
243                    ))
244                } else if f.args.len() == 1 {
245                    // Check if the argument is a numeric literal
246                    if let Expression::Literal(lit) = &f.args[0] {
247                        if let crate::expressions::Literal::Number(n) = lit.as_ref() {
248                            if let Ok(precision) = n.parse::<u32>() {
249                                return Ok(Expression::CurrentTimestamp(
250                                    crate::expressions::CurrentTimestamp {
251                                        precision: Some(precision),
252                                        sysdate: false,
253                                    },
254                                ));
255                            }
256                        }
257                    }
258                    // Non-numeric argument, keep as function
259                    Ok(Expression::Function(Box::new(f)))
260                } else {
261                    // Multiple args, keep as function
262                    Ok(Expression::Function(Box::new(f)))
263                }
264            }
265
266            // CURRENT_DATE is native
267            "CURRENT_DATE" => Ok(Expression::CurrentDate(crate::expressions::CurrentDate)),
268
269            // TO_DATE is native to Oracle
270            "TO_DATE" => Ok(Expression::Function(Box::new(f))),
271
272            // TO_TIMESTAMP is native to Oracle
273            "TO_TIMESTAMP" => Ok(Expression::Function(Box::new(f))),
274
275            // TO_CHAR is native to Oracle
276            "TO_CHAR" => Ok(Expression::Function(Box::new(f))),
277
278            // DATE_FORMAT -> TO_CHAR in Oracle
279            "DATE_FORMAT" => Ok(Expression::Function(Box::new(Function::new(
280                "TO_CHAR".to_string(),
281                f.args,
282            )))),
283
284            // strftime -> TO_CHAR in Oracle
285            "STRFTIME" => Ok(Expression::Function(Box::new(Function::new(
286                "TO_CHAR".to_string(),
287                f.args,
288            )))),
289
290            // DATE_TRUNC -> TRUNC in Oracle
291            "DATE_TRUNC" => Ok(Expression::Function(Box::new(Function::new(
292                "TRUNC".to_string(),
293                f.args,
294            )))),
295
296            // TRUNC is native to Oracle (for both date and number truncation)
297            // For date truncation with a single temporal arg, add default 'DD' unit
298            "TRUNC" if f.args.len() == 1 && Self::is_temporal_expr(&f.args[0]) => {
299                let mut args = f.args;
300                args.push(Expression::Literal(Box::new(
301                    crate::expressions::Literal::String("DD".to_string()),
302                )));
303                Ok(Expression::Function(Box::new(Function::new(
304                    "TRUNC".to_string(),
305                    args,
306                ))))
307            }
308            "TRUNC" => Ok(Expression::Function(Box::new(f))),
309
310            // EXTRACT is native to Oracle
311            "EXTRACT" => Ok(Expression::Function(Box::new(f))),
312
313            // POSITION -> INSTR in Oracle
314            // INSTR(string, substring) - reversed arg order from POSITION
315            "POSITION" if f.args.len() == 2 => {
316                let mut args = f.args;
317                let first = args.remove(0);
318                let second = args.remove(0);
319                // Oracle INSTR has args in order: (string, substring)
320                Ok(Expression::Function(Box::new(Function::new(
321                    "INSTR".to_string(),
322                    vec![second, first],
323                ))))
324            }
325
326            // STRPOS -> INSTR
327            "STRPOS" if f.args.len() == 2 => Ok(Expression::Function(Box::new(Function::new(
328                "INSTR".to_string(),
329                f.args,
330            )))),
331
332            // CHARINDEX -> INSTR
333            "CHARINDEX" if f.args.len() >= 2 => {
334                let mut args = f.args;
335                let substring = args.remove(0);
336                let string = args.remove(0);
337                // Oracle INSTR: (string, substring, [start_pos])
338                let mut instr_args = vec![string, substring];
339                if !args.is_empty() {
340                    instr_args.push(args.remove(0));
341                }
342                Ok(Expression::Function(Box::new(Function::new(
343                    "INSTR".to_string(),
344                    instr_args,
345                ))))
346            }
347
348            // INSTR is native to Oracle
349            "INSTR" => Ok(Expression::Function(Box::new(f))),
350
351            // CEILING -> CEIL
352            "CEILING" if f.args.len() == 1 => Ok(Expression::Ceil(Box::new(CeilFunc {
353                this: f.args.into_iter().next().unwrap(),
354                decimals: None,
355                to: None,
356            }))),
357
358            // CEIL is native to Oracle
359            "CEIL" if f.args.len() == 1 => Ok(Expression::Ceil(Box::new(CeilFunc {
360                this: f.args.into_iter().next().unwrap(),
361                decimals: None,
362                to: None,
363            }))),
364
365            // LOG -> LN for natural log (Oracle LOG is different)
366            // In Oracle, LOG(base, n) but LN(n) for natural log
367            "LOG" if f.args.len() == 1 => Ok(Expression::Function(Box::new(Function::new(
368                "LN".to_string(),
369                f.args,
370            )))),
371
372            // LN is native to Oracle
373            "LN" => Ok(Expression::Function(Box::new(f))),
374
375            // POWER is native to Oracle
376            "POWER" | "POW" => Ok(Expression::Function(Box::new(Function::new(
377                "POWER".to_string(),
378                f.args,
379            )))),
380
381            // REGEXP_LIKE is native to Oracle
382            "REGEXP_LIKE" => Ok(Expression::Function(Box::new(f))),
383
384            // JSON_VALUE is native to Oracle 12c+
385            "JSON_VALUE" => Ok(Expression::Function(Box::new(f))),
386
387            // JSON_QUERY is native to Oracle 12c+
388            "JSON_QUERY" => Ok(Expression::Function(Box::new(f))),
389
390            // JSON_EXTRACT -> JSON_VALUE
391            "JSON_EXTRACT" => Ok(Expression::Function(Box::new(Function::new(
392                "JSON_VALUE".to_string(),
393                f.args,
394            )))),
395
396            // JSON_EXTRACT_SCALAR -> JSON_VALUE
397            "JSON_EXTRACT_SCALAR" => Ok(Expression::Function(Box::new(Function::new(
398                "JSON_VALUE".to_string(),
399                f.args,
400            )))),
401
402            // SPLIT -> Not directly available in Oracle
403            // Would need REGEXP_SUBSTR or custom function
404            "SPLIT" => {
405                // For basic cases, use REGEXP_SUBSTR pattern
406                Ok(Expression::Function(Box::new(Function::new(
407                    "REGEXP_SUBSTR".to_string(),
408                    f.args,
409                ))))
410            }
411
412            // ADD_MONTHS is native to Oracle
413            "ADD_MONTHS" => Ok(Expression::Function(Box::new(f))),
414
415            // MONTHS_BETWEEN is native to Oracle
416            "MONTHS_BETWEEN" => Ok(Expression::Function(Box::new(f))),
417
418            // DATEADD -> Use arithmetic with INTERVAL or specific functions
419            "DATEADD" => {
420                // Pass through for now - complex transformation needed
421                Ok(Expression::Function(Box::new(f)))
422            }
423
424            // DATEDIFF -> Complex in Oracle, might need MONTHS_BETWEEN or arithmetic
425            "DATEDIFF" => Ok(Expression::Function(Box::new(f))),
426
427            // DECODE is native to Oracle
428            "DECODE" => Ok(Expression::Function(Box::new(f))),
429
430            // Pass through everything else
431            _ => Ok(Expression::Function(Box::new(f))),
432        }
433    }
434
435    fn transform_aggregate_function(
436        &self,
437        f: Box<crate::expressions::AggregateFunction>,
438    ) -> Result<Expression> {
439        let name_upper = f.name.to_uppercase();
440        match name_upper.as_str() {
441            // GROUP_CONCAT -> LISTAGG
442            "GROUP_CONCAT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
443                Function::new("LISTAGG".to_string(), f.args),
444            ))),
445
446            // STRING_AGG -> LISTAGG
447            "STRING_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
448                Function::new("LISTAGG".to_string(), f.args),
449            ))),
450
451            // ARRAY_AGG -> Not directly supported in Oracle
452            // Would need COLLECT (for nested tables)
453            "ARRAY_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(Function::new(
454                "COLLECT".to_string(),
455                f.args,
456            )))),
457
458            // Pass through everything else
459            _ => Ok(Expression::AggregateFunction(f)),
460        }
461    }
462
463    /// Check if an expression is a temporal/date-time expression
464    fn is_temporal_expr(expr: &Expression) -> bool {
465        matches!(
466            expr,
467            Expression::CurrentTimestamp(_)
468                | Expression::CurrentDate(_)
469                | Expression::CurrentTime(_)
470                | Expression::Localtimestamp(_)
471        ) || matches!(expr, Expression::Function(f) if {
472            let name = f.name.to_uppercase();
473            name == "SYSDATE" || name == "SYSTIMESTAMP" || name == "TO_DATE" || name == "TO_TIMESTAMP"
474        })
475    }
476}