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