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