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(crate::expressions::Literal::Number(n)) = &f.args[0]
247                    {
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                    // Non-numeric argument, keep as function
258                    Ok(Expression::Function(Box::new(f)))
259                } else {
260                    // Multiple args, keep as function
261                    Ok(Expression::Function(Box::new(f)))
262                }
263            }
264
265            // CURRENT_DATE is native
266            "CURRENT_DATE" => Ok(Expression::CurrentDate(crate::expressions::CurrentDate)),
267
268            // TO_DATE is native to Oracle
269            "TO_DATE" => Ok(Expression::Function(Box::new(f))),
270
271            // TO_TIMESTAMP is native to Oracle
272            "TO_TIMESTAMP" => Ok(Expression::Function(Box::new(f))),
273
274            // TO_CHAR is native to Oracle
275            "TO_CHAR" => Ok(Expression::Function(Box::new(f))),
276
277            // DATE_FORMAT -> TO_CHAR in Oracle
278            "DATE_FORMAT" => Ok(Expression::Function(Box::new(Function::new(
279                "TO_CHAR".to_string(),
280                f.args,
281            )))),
282
283            // strftime -> TO_CHAR in Oracle
284            "STRFTIME" => Ok(Expression::Function(Box::new(Function::new(
285                "TO_CHAR".to_string(),
286                f.args,
287            )))),
288
289            // DATE_TRUNC -> TRUNC in Oracle
290            "DATE_TRUNC" => Ok(Expression::Function(Box::new(Function::new(
291                "TRUNC".to_string(),
292                f.args,
293            )))),
294
295            // TRUNC with single date argument: add default 'DD' unit
296            // In Oracle, TRUNC(date) defaults to truncating to day ('DD')
297            // Python sqlglot: "TRUNC": lambda args: exp.DateTrunc(unit=seq_get(args, 1) or exp.Literal.string("DD"), ...)
298            "TRUNC" if f.args.len() == 1 => {
299                let mut args = f.args;
300                args.push(Expression::Literal(crate::expressions::Literal::String(
301                    "DD".to_string(),
302                )));
303                Ok(Expression::Function(Box::new(Function::new(
304                    "TRUNC".to_string(),
305                    args,
306                ))))
307            }
308
309            // EXTRACT is native to Oracle
310            "EXTRACT" => Ok(Expression::Function(Box::new(f))),
311
312            // POSITION -> INSTR in Oracle
313            // INSTR(string, substring) - reversed arg order from POSITION
314            "POSITION" if f.args.len() == 2 => {
315                let mut args = f.args;
316                let first = args.remove(0);
317                let second = args.remove(0);
318                // Oracle INSTR has args in order: (string, substring)
319                Ok(Expression::Function(Box::new(Function::new(
320                    "INSTR".to_string(),
321                    vec![second, first],
322                ))))
323            }
324
325            // STRPOS -> INSTR
326            "STRPOS" if f.args.len() == 2 => Ok(Expression::Function(Box::new(Function::new(
327                "INSTR".to_string(),
328                f.args,
329            )))),
330
331            // CHARINDEX -> INSTR
332            "CHARINDEX" if f.args.len() >= 2 => {
333                let mut args = f.args;
334                let substring = args.remove(0);
335                let string = args.remove(0);
336                // Oracle INSTR: (string, substring, [start_pos])
337                let mut instr_args = vec![string, substring];
338                if !args.is_empty() {
339                    instr_args.push(args.remove(0));
340                }
341                Ok(Expression::Function(Box::new(Function::new(
342                    "INSTR".to_string(),
343                    instr_args,
344                ))))
345            }
346
347            // INSTR is native to Oracle
348            "INSTR" => Ok(Expression::Function(Box::new(f))),
349
350            // CEILING -> CEIL
351            "CEILING" if f.args.len() == 1 => Ok(Expression::Ceil(Box::new(CeilFunc {
352                this: f.args.into_iter().next().unwrap(),
353                decimals: None,
354                to: None,
355            }))),
356
357            // CEIL is native to Oracle
358            "CEIL" if f.args.len() == 1 => Ok(Expression::Ceil(Box::new(CeilFunc {
359                this: f.args.into_iter().next().unwrap(),
360                decimals: None,
361                to: None,
362            }))),
363
364            // LOG -> LN for natural log (Oracle LOG is different)
365            // In Oracle, LOG(base, n) but LN(n) for natural log
366            "LOG" if f.args.len() == 1 => Ok(Expression::Function(Box::new(Function::new(
367                "LN".to_string(),
368                f.args,
369            )))),
370
371            // LN is native to Oracle
372            "LN" => Ok(Expression::Function(Box::new(f))),
373
374            // POWER is native to Oracle
375            "POWER" | "POW" => Ok(Expression::Function(Box::new(Function::new(
376                "POWER".to_string(),
377                f.args,
378            )))),
379
380            // REGEXP_LIKE is native to Oracle
381            "REGEXP_LIKE" => Ok(Expression::Function(Box::new(f))),
382
383            // JSON_VALUE is native to Oracle 12c+
384            "JSON_VALUE" => Ok(Expression::Function(Box::new(f))),
385
386            // JSON_QUERY is native to Oracle 12c+
387            "JSON_QUERY" => Ok(Expression::Function(Box::new(f))),
388
389            // JSON_EXTRACT -> JSON_VALUE
390            "JSON_EXTRACT" => Ok(Expression::Function(Box::new(Function::new(
391                "JSON_VALUE".to_string(),
392                f.args,
393            )))),
394
395            // JSON_EXTRACT_SCALAR -> JSON_VALUE
396            "JSON_EXTRACT_SCALAR" => Ok(Expression::Function(Box::new(Function::new(
397                "JSON_VALUE".to_string(),
398                f.args,
399            )))),
400
401            // SPLIT -> Not directly available in Oracle
402            // Would need REGEXP_SUBSTR or custom function
403            "SPLIT" => {
404                // For basic cases, use REGEXP_SUBSTR pattern
405                Ok(Expression::Function(Box::new(Function::new(
406                    "REGEXP_SUBSTR".to_string(),
407                    f.args,
408                ))))
409            }
410
411            // ADD_MONTHS is native to Oracle
412            "ADD_MONTHS" => Ok(Expression::Function(Box::new(f))),
413
414            // MONTHS_BETWEEN is native to Oracle
415            "MONTHS_BETWEEN" => Ok(Expression::Function(Box::new(f))),
416
417            // DATEADD -> Use arithmetic with INTERVAL or specific functions
418            "DATEADD" => {
419                // Pass through for now - complex transformation needed
420                Ok(Expression::Function(Box::new(f)))
421            }
422
423            // DATEDIFF -> Complex in Oracle, might need MONTHS_BETWEEN or arithmetic
424            "DATEDIFF" => Ok(Expression::Function(Box::new(f))),
425
426            // DECODE is native to Oracle
427            "DECODE" => Ok(Expression::Function(Box::new(f))),
428
429            // Pass through everything else
430            _ => Ok(Expression::Function(Box::new(f))),
431        }
432    }
433
434    fn transform_aggregate_function(
435        &self,
436        f: Box<crate::expressions::AggregateFunction>,
437    ) -> Result<Expression> {
438        let name_upper = f.name.to_uppercase();
439        match name_upper.as_str() {
440            // GROUP_CONCAT -> LISTAGG
441            "GROUP_CONCAT" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
442                Function::new("LISTAGG".to_string(), f.args),
443            ))),
444
445            // STRING_AGG -> LISTAGG
446            "STRING_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(
447                Function::new("LISTAGG".to_string(), f.args),
448            ))),
449
450            // ARRAY_AGG -> Not directly supported in Oracle
451            // Would need COLLECT (for nested tables)
452            "ARRAY_AGG" if !f.args.is_empty() => Ok(Expression::Function(Box::new(Function::new(
453                "COLLECT".to_string(),
454                f.args,
455            )))),
456
457            // Pass through everything else
458            _ => Ok(Expression::AggregateFunction(f)),
459        }
460    }
461}