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