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