| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| //! Test SQL syntax, specific to [sqlparser::dialect::OracleDialect]. | |
| #[cfg(test)] | |
| use pretty_assertions::assert_eq; | |
| use sqlparser::{ | |
| ast::{ | |
| BinaryOperator, Expr, Ident, Insert, ObjectName, Query, QuoteDelimitedString, SetExpr, | |
| Statement, TableAliasWithoutColumns, TableObject, Value, ValueWithSpan, | |
| }, | |
| dialect::OracleDialect, | |
| parser::ParserError, | |
| tokenizer::Span, | |
| }; | |
| use test_utils::{all_dialects_where, expr_from_projection, number, TestedDialects}; | |
| mod test_utils; | |
| fn oracle() -> TestedDialects { | |
| TestedDialects::new(vec![Box::new(OracleDialect)]) | |
| } | |
| /// Convenience constructor for [QuoteDelimitedstring]. | |
| fn quote_delimited_string( | |
| start_quote: char, | |
| value: &'static str, | |
| end_quote: char, | |
| ) -> QuoteDelimitedString { | |
| QuoteDelimitedString { | |
| start_quote, | |
| value: value.into(), | |
| end_quote, | |
| } | |
| } | |
| /// Oracle: `||` has a lower precedence than `*` and `/` | |
| #[test] | |
| fn muldiv_have_higher_precedence_than_strconcat() { | |
| // ............... A .. B ...... C .. D ........... | |
| let sql = "SELECT 3 / 5 || 'asdf' || 7 * 9 FROM dual"; | |
| let select = oracle().verified_only_select(sql); | |
| assert_eq!(1, select.projection.len()); | |
| assert_eq!( | |
| expr_from_projection(&select.projection[0]), | |
| // (C || D) | |
| &Expr::BinaryOp { | |
| // (A || B) | |
| left: Box::new(Expr::BinaryOp { | |
| // A | |
| left: Box::new(Expr::BinaryOp { | |
| left: Box::new(Expr::Value(number("3").into())), | |
| op: BinaryOperator::Divide, | |
| right: Box::new(Expr::Value(number("5").into())), | |
| }), | |
| op: BinaryOperator::StringConcat, | |
| right: Box::new(Expr::Value(ValueWithSpan { | |
| value: Value::SingleQuotedString("asdf".into()), | |
| span: Span::empty(), | |
| })), | |
| }), | |
| op: BinaryOperator::StringConcat, | |
| // D | |
| right: Box::new(Expr::BinaryOp { | |
| left: Box::new(Expr::Value(number("7").into())), | |
| op: BinaryOperator::Multiply, | |
| right: Box::new(Expr::Value(number("9").into())), | |
| }), | |
| } | |
| ); | |
| } | |
| /// Oracle: `+`, `-`, and `||` have the same precedence and parse from left-to-right | |
| #[test] | |
| fn plusminus_have_same_precedence_as_strconcat() { | |
| // ................ A .. B .... C .. D ............ | |
| let sql = "SELECT 3 + 5 || '.3' || 7 - 9 FROM dual"; | |
| let select = oracle().verified_only_select(sql); | |
| assert_eq!(1, select.projection.len()); | |
| assert_eq!( | |
| expr_from_projection(&select.projection[0]), | |
| // D | |
| &Expr::BinaryOp { | |
| left: Box::new(Expr::BinaryOp { | |
| // B | |
| left: Box::new(Expr::BinaryOp { | |
| // A | |
| left: Box::new(Expr::BinaryOp { | |
| left: Box::new(Expr::Value(number("3").into())), | |
| op: BinaryOperator::Plus, | |
| right: Box::new(Expr::Value(number("5").into())), | |
| }), | |
| op: BinaryOperator::StringConcat, | |
| right: Box::new(Expr::Value(ValueWithSpan { | |
| value: Value::SingleQuotedString(".3".into()), | |
| span: Span::empty(), | |
| })), | |
| }), | |
| op: BinaryOperator::StringConcat, | |
| right: Box::new(Expr::Value(number("7").into())), | |
| }), | |
| op: BinaryOperator::Minus, | |
| right: Box::new(Expr::Value(number("9").into())) | |
| } | |
| ); | |
| } | |
| #[test] | |
| fn parse_quote_delimited_string() { | |
| let dialect = all_dialects_where(|d| d.supports_quote_delimited_string()); | |
| let sql = "SELECT Q'.abc.', \ | |
| Q'Xab'cX', \ | |
| Q'|abc'''|', \ | |
| Q'{abc}d}', \ | |
| Q'[]abc[]', \ | |
| Q'<a'bc>', \ | |
| Q'<<<a'bc>', \ | |
| Q'('abc'('abc)', \ | |
| Q'(abc'def))', \ | |
| Q'(abc'def)))' \ | |
| FROM dual"; | |
| let select = dialect.verified_only_select(sql); | |
| assert_eq!(10, select.projection.len()); | |
| assert_eq!( | |
| &Expr::Value( | |
| Value::QuoteDelimitedStringLiteral(quote_delimited_string('.', "abc", '.')) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[0]) | |
| ); | |
| assert_eq!( | |
| &Expr::Value( | |
| (Value::QuoteDelimitedStringLiteral(quote_delimited_string('X', "ab'c", 'X'))) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[1]) | |
| ); | |
| assert_eq!( | |
| &Expr::Value( | |
| (Value::QuoteDelimitedStringLiteral(quote_delimited_string('|', "abc'''", '|'))) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[2]) | |
| ); | |
| assert_eq!( | |
| &Expr::Value( | |
| (Value::QuoteDelimitedStringLiteral(quote_delimited_string('{', "abc}d", '}'))) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[3]) | |
| ); | |
| assert_eq!( | |
| &Expr::Value( | |
| (Value::QuoteDelimitedStringLiteral(quote_delimited_string('[', "]abc[", ']'))) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[4]) | |
| ); | |
| assert_eq!( | |
| &Expr::Value( | |
| (Value::QuoteDelimitedStringLiteral(quote_delimited_string('<', "a'bc", '>'))) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[5]) | |
| ); | |
| assert_eq!( | |
| &Expr::Value( | |
| (Value::QuoteDelimitedStringLiteral(quote_delimited_string('<', "<<a'bc", '>'))) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[6]) | |
| ); | |
| assert_eq!( | |
| &Expr::Value( | |
| (Value::QuoteDelimitedStringLiteral(quote_delimited_string('(', "'abc'('abc", ')'))) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[7]) | |
| ); | |
| assert_eq!( | |
| &Expr::Value( | |
| (Value::QuoteDelimitedStringLiteral(quote_delimited_string('(', "abc'def)", ')'))) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[8]) | |
| ); | |
| assert_eq!( | |
| &Expr::Value( | |
| (Value::QuoteDelimitedStringLiteral(quote_delimited_string('(', "abc'def))", ')'))) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[9]) | |
| ); | |
| } | |
| #[test] | |
| fn parse_invalid_quote_delimited_strings() { | |
| let dialect = all_dialects_where(|d| d.supports_quote_delimited_string()); | |
| // invalid quote delimiter | |
| for q in [' ', '\t', '\r', '\n'] { | |
| assert_eq!( | |
| dialect.parse_sql_statements(&format!("SELECT Q'{q}abc{q}' FROM dual")), | |
| Err(ParserError::TokenizerError( | |
| "Invalid space, tab, newline, or EOF after 'Q'' at Line: 1, Column: 10".into() | |
| )), | |
| "with quote char {q:?}" | |
| ); | |
| } | |
| // invalid eof after quote | |
| assert_eq!( | |
| dialect.parse_sql_statements("SELECT Q'"), | |
| Err(ParserError::TokenizerError( | |
| "Invalid space, tab, newline, or EOF after 'Q'' at Line: 1, Column: 10".into() | |
| )), | |
| "with EOF quote char" | |
| ); | |
| // unterminated string | |
| assert_eq!( | |
| dialect.parse_sql_statements("SELECT Q'|asdfa...."), | |
| Err(ParserError::TokenizerError( | |
| "Unterminated string literal at Line: 1, Column: 9".into() | |
| )), | |
| "with EOF quote char" | |
| ); | |
| } | |
| #[test] | |
| fn parse_quote_delimited_string_lowercase() { | |
| let dialect = all_dialects_where(|d| d.supports_quote_delimited_string()); | |
| let sql = "select q'!a'b'c!d!' from dual"; | |
| let select = dialect.verified_only_select_with_canonical(sql, "SELECT Q'!a'b'c!d!' FROM dual"); | |
| assert_eq!(1, select.projection.len()); | |
| assert_eq!( | |
| &Expr::Value( | |
| Value::QuoteDelimitedStringLiteral(quote_delimited_string('!', "a'b'c!d", '!')) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[0]) | |
| ); | |
| } | |
| #[test] | |
| fn parse_quote_delimited_string_but_is_a_word() { | |
| let dialect = all_dialects_where(|d| d.supports_quote_delimited_string()); | |
| let sql = "SELECT q, quux, q.abc FROM dual q"; | |
| let select = dialect.verified_only_select(sql); | |
| assert_eq!(3, select.projection.len()); | |
| assert_eq!( | |
| &Expr::Identifier(Ident::with_span(Span::empty(), "q")), | |
| expr_from_projection(&select.projection[0]) | |
| ); | |
| assert_eq!( | |
| &Expr::Identifier(Ident::with_span(Span::empty(), "quux")), | |
| expr_from_projection(&select.projection[1]) | |
| ); | |
| assert_eq!( | |
| &Expr::CompoundIdentifier(vec![ | |
| Ident::with_span(Span::empty(), "q"), | |
| Ident::with_span(Span::empty(), "abc") | |
| ]), | |
| expr_from_projection(&select.projection[2]) | |
| ); | |
| } | |
| #[test] | |
| fn parse_national_quote_delimited_string() { | |
| let dialect = all_dialects_where(|d| d.supports_quote_delimited_string()); | |
| let sql = "SELECT NQ'.abc.' FROM dual"; | |
| let select = dialect.verified_only_select(sql); | |
| assert_eq!(1, select.projection.len()); | |
| assert_eq!( | |
| &Expr::Value( | |
| Value::NationalQuoteDelimitedStringLiteral(quote_delimited_string('.', "abc", '.')) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[0]) | |
| ); | |
| } | |
| #[test] | |
| fn parse_national_quote_delimited_string_lowercase() { | |
| let dialect = all_dialects_where(|d| d.supports_quote_delimited_string()); | |
| for prefix in ["nq", "Nq", "nQ", "NQ"] { | |
| let select = dialect.verified_only_select_with_canonical( | |
| &format!("select {prefix}'!a'b'c!d!' from dual"), | |
| "SELECT NQ'!a'b'c!d!' FROM dual", | |
| ); | |
| assert_eq!(1, select.projection.len()); | |
| assert_eq!( | |
| &Expr::Value( | |
| Value::NationalQuoteDelimitedStringLiteral(quote_delimited_string( | |
| '!', "a'b'c!d", '!' | |
| )) | |
| .with_empty_span() | |
| ), | |
| expr_from_projection(&select.projection[0]) | |
| ); | |
| } | |
| } | |
| #[test] | |
| fn parse_national_quote_delimited_string_but_is_a_word() { | |
| let dialect = all_dialects_where(|d| d.supports_quote_delimited_string()); | |
| let sql = "SELECT nq, nqoo, nq.abc FROM dual q"; | |
| let select = dialect.verified_only_select(sql); | |
| assert_eq!(3, select.projection.len()); | |
| assert_eq!( | |
| &Expr::Identifier(Ident::with_span(Span::empty(), "nq")), | |
| expr_from_projection(&select.projection[0]) | |
| ); | |
| assert_eq!( | |
| &Expr::Identifier(Ident::with_span(Span::empty(), "nqoo")), | |
| expr_from_projection(&select.projection[1]) | |
| ); | |
| assert_eq!( | |
| &Expr::CompoundIdentifier(vec![ | |
| Ident::with_span(Span::empty(), "nq"), | |
| Ident::with_span(Span::empty(), "abc") | |
| ]), | |
| expr_from_projection(&select.projection[2]) | |
| ); | |
| } | |
| #[test] | |
| fn test_optimizer_hints() { | |
| let oracle_dialect = oracle(); | |
| // selects: all `/*+...*/` comments are collected as hints | |
| let select = oracle_dialect.verified_only_select_with_canonical( | |
| "SELECT /*+one two three*/ /*+four five six*/ 1 FROM dual", | |
| "SELECT /*+one two three*/ /*+four five six*/ 1 FROM dual", | |
| ); | |
| assert_eq!(select.optimizer_hints.len(), 2); | |
| assert_eq!(select.optimizer_hints[0].text, "one two three"); | |
| assert_eq!(select.optimizer_hints[0].prefix, ""); | |
| assert_eq!(select.optimizer_hints[1].text, "four five six"); | |
| // regular comments are skipped, hints after them are still collected | |
| let select = oracle_dialect.verified_only_select_with_canonical( | |
| "SELECT /*one two three*/ /*+four five six*/ 1 FROM dual", | |
| "SELECT /*+four five six*/ 1 FROM dual", | |
| ); | |
| assert_eq!(select.optimizer_hints.len(), 1); | |
| assert_eq!(select.optimizer_hints[0].text, "four five six"); | |
| let select = oracle_dialect.verified_only_select_with_canonical( | |
| "SELECT --+ one two three /* asdf */\n 1 FROM dual", | |
| "SELECT --+ one two three /* asdf */\n 1 FROM dual", | |
| ); | |
| assert_eq!(select.optimizer_hints.len(), 1); | |
| assert_eq!(select.optimizer_hints[0].text, " one two three /* asdf */"); | |
| assert_eq!(select.optimizer_hints[0].prefix, ""); | |
| // inserts | |
| oracle_dialect.verified_stmt("INSERT /*+ append */ INTO t1 SELECT * FROM all_objects"); | |
| // updates | |
| oracle_dialect.verified_stmt("UPDATE /*+ DISABLE_PARALLEL_DML */ table_name SET column1 = 1"); | |
| // deletes | |
| oracle_dialect.verified_stmt("DELETE --+ ENABLE_PARALLEL_DML\n FROM table_name"); | |
| // merges | |
| oracle_dialect.verified_stmt( | |
| "MERGE /*+ CLUSTERING */ INTO people_target pt \ | |
| USING people_source ps \ | |
| ON (pt.person_id = ps.person_id) \ | |
| WHEN NOT MATCHED THEN INSERT \ | |
| (pt.person_id, pt.first_name, pt.last_name, pt.title) \ | |
| VALUES (ps.person_id, ps.first_name, ps.last_name, ps.title)", | |
| ); | |
| // single-line prefixed hint (Oracle supports `--` without trailing whitespace) | |
| let select = oracle_dialect.verified_only_select_with_canonical( | |
| "SELECT --abc+ text\n 1 FROM dual", | |
| "SELECT --abc+ text\n 1 FROM dual", | |
| ); | |
| assert_eq!(select.optimizer_hints.len(), 1); | |
| assert_eq!(select.optimizer_hints[0].prefix, "abc"); | |
| assert_eq!(select.optimizer_hints[0].text, " text"); | |
| } | |
| #[test] | |
| fn test_connect_by() { | |
| let oracle_dialect = oracle(); | |
| oracle_dialect.verified_only_select( | |
| "SELECT last_name AS \"Employee\", CONNECT_BY_ISCYCLE AS \"Cycle\", \ | |
| LEVEL, \ | |
| SYS_CONNECT_BY_PATH(last_name, '/') AS \"Path\" \ | |
| FROM employees \ | |
| WHERE level <= 3 AND department_id = 80 \ | |
| START WITH last_name = 'King' \ | |
| CONNECT BY NOCYCLE PRIOR employee_id = manager_id AND LEVEL <= 4 \ | |
| ORDER BY \"Employee\", \"Cycle\", LEVEL, \"Path\"", | |
| ); | |
| // CONNECT_BY_ROOT | |
| oracle_dialect.verified_only_select( | |
| "SELECT last_name AS \"Employee\", CONNECT_BY_ROOT last_name AS \"Manager\", \ | |
| LEVEL - 1 AS \"Pathlen\", SYS_CONNECT_BY_PATH(last_name, '/') AS \"Path\" \ | |
| FROM employees \ | |
| WHERE LEVEL > 1 AND department_id = 110 \ | |
| CONNECT BY PRIOR employee_id = manager_id \ | |
| ORDER BY \"Employee\", \"Manager\", \"Pathlen\", \"Path\"", | |
| ); | |
| } | |
| #[test] | |
| fn test_insert_with_table_alias() { | |
| let oracle_dialect = oracle(); | |
| fn verify_table_name_with_alias(stmt: &Statement, exp_table_name: &str, exp_table_alias: &str) { | |
| assert!(matches!(stmt, | |
| Statement::Insert(Insert { | |
| table: TableObject::TableName(table_name), | |
| table_alias: Some(TableAliasWithoutColumns { | |
| explicit: false, | |
| alias: Ident { | |
| value: table_alias, | |
| quote_style: None, | |
| span: _ | |
| } | |
| }), | |
| .. | |
| }) | |
| if table_alias == exp_table_alias | |
| && table_name == &ObjectName::from(vec![Ident { | |
| value: exp_table_name.into(), | |
| quote_style: None, | |
| span: Span::empty(), | |
| }]) | |
| )); | |
| } | |
| let stmt = oracle_dialect.verified_stmt( | |
| "INSERT INTO foo_t t \ | |
| SELECT 1, 2, 3 FROM dual", | |
| ); | |
| verify_table_name_with_alias(&stmt, "foo_t", "t"); | |
| let stmt = oracle_dialect.verified_stmt( | |
| "INSERT INTO foo_t asdf (a, b, c) \ | |
| SELECT 1, 2, 3 FROM dual", | |
| ); | |
| verify_table_name_with_alias(&stmt, "foo_t", "asdf"); | |
| let stmt = oracle_dialect.verified_stmt( | |
| "INSERT INTO foo_t t (a, b, c) \ | |
| VALUES (1, 2, 3)", | |
| ); | |
| verify_table_name_with_alias(&stmt, "foo_t", "t"); | |
| let stmt = oracle_dialect.verified_stmt( | |
| "INSERT INTO foo_t t \ | |
| VALUES (1, 2, 3)", | |
| ); | |
| verify_table_name_with_alias(&stmt, "foo_t", "t"); | |
| let stmt = | |
| oracle_dialect.verified_stmt("INSERT INTO foo_t t (t.id, t.val) SELECT 1, 2 FROM dual"); | |
| verify_table_name_with_alias(&stmt, "foo_t", "t"); | |
| if let Statement::Insert(Insert { columns, .. }) = stmt { | |
| assert_eq!( | |
| vec![ | |
| ObjectName::from(vec![Ident::new("t"), Ident::new("id")]), | |
| ObjectName::from(vec![Ident::new("t"), Ident::new("val")]) | |
| ], | |
| columns | |
| ); | |
| } else { | |
| panic!("not an insert statement"); | |
| }; | |
| } | |
| #[test] | |
| fn test_insert_without_alias() { | |
| let oracle_dialect = oracle(); | |
| // check DEFAULT | |
| let sql = "INSERT INTO t default SELECT 'a' FROM dual"; | |
| assert_eq!( | |
| oracle_dialect.parse_sql_statements(sql), | |
| Err(ParserError::ParserError( | |
| "Expected: SELECT, VALUES, or a subquery in the query body, found: default".into() | |
| )) | |
| ); | |
| // check SELECT | |
| let sql = "INSERT INTO t SELECT 'a' FROM dual"; | |
| let stmt = oracle_dialect.verified_stmt(sql); | |
| assert!(matches!( | |
| &stmt, | |
| Statement::Insert(Insert { | |
| table_alias: None, | |
| source: Some(source), | |
| .. | |
| }) | |
| if matches!(&**source, Query { body, .. } if matches!(&**body, SetExpr::Select(_))))); | |
| // check WITH | |
| let sql = "INSERT INTO dual WITH w AS (SELECT 1 AS y FROM dual) SELECT y FROM w"; | |
| let stmt = oracle_dialect.verified_stmt(sql); | |
| assert!(matches!( | |
| &stmt, | |
| Statement::Insert(Insert { | |
| table_alias: None, | |
| source: Some(source), | |
| .. | |
| }) | |
| if matches!(&**source, Query { body, .. } if matches!(&**body, SetExpr::Select(_))))); | |
| // check VALUES | |
| let sql = "INSERT INTO t VALUES (1)"; | |
| let stmt = oracle_dialect.verified_stmt(sql); | |
| assert!(matches!( | |
| stmt, | |
| Statement::Insert(Insert { | |
| table_alias: None, | |
| source: Some(source), | |
| .. | |
| }) | |
| if matches!(&*source, Query { body, .. } if matches!(&**body, SetExpr::Values(_))) | |
| )); | |
| } |