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