From 3f21435e5946f54b15e778142d6a79336ff85010 Mon Sep 17 00:00:00 2001 From: Ayush Dattagupta Date: Wed, 10 Aug 2022 09:50:09 -0700 Subject: [PATCH 1/7] Remove [not]like,[not]ilike from binary operator list --- src/ast/operator.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index f7a63a4a4..2648256fe 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + // Licensed 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 @@ -76,10 +78,6 @@ pub enum BinaryOperator { And, Or, Xor, - Like, - NotLike, - ILike, - NotILike, BitwiseOr, BitwiseAnd, BitwiseXor, @@ -116,10 +114,6 @@ impl fmt::Display for BinaryOperator { BinaryOperator::And => f.write_str("AND"), BinaryOperator::Or => f.write_str("OR"), BinaryOperator::Xor => f.write_str("XOR"), - BinaryOperator::Like => f.write_str("LIKE"), - BinaryOperator::NotLike => f.write_str("NOT LIKE"), - BinaryOperator::ILike => f.write_str("ILIKE"), - BinaryOperator::NotILike => f.write_str("NOT ILIKE"), BinaryOperator::BitwiseOr => f.write_str("|"), BinaryOperator::BitwiseAnd => f.write_str("&"), BinaryOperator::BitwiseXor => f.write_str("^"), From 0e28dd160b39b83a3351eaba82a832cebd595f25 Mon Sep 17 00:00:00 2001 From: Ayush Dattagupta Date: Wed, 10 Aug 2022 09:51:07 -0700 Subject: [PATCH 2/7] Add like, ilike and similar as an expr variant. Also adds support for escape char to like/ilike --- src/ast/mod.rs | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 312a86813..e3207fc74 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + // Licensed 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 @@ -268,6 +270,27 @@ pub enum Expr { op: BinaryOperator, right: Box, }, + /// LIKE + Like { + negated: bool, + expr: Box, + pattern: Box, + escape_char: Option, + }, + /// ILIKE (case-insensitive LIKE) + ILike { + negated: bool, + expr: Box, + pattern: Box, + escape_char: Option, + }, + /// SIMILAR TO regex + SimilarTo { + negated: bool, + expr: Box, + pattern: Box, + escape_char: Option, + }, /// Any operation e.g. `1 ANY (1)` or `foo > ANY(bar)`, It will be wrapped in the right side of BinaryExpr AnyOp(Box), /// ALL operation e.g. `1 ALL (1)` or `foo > ALL(bar)`, It will be wrapped in the right side of BinaryExpr @@ -438,6 +461,72 @@ impl fmt::Display for Expr { high ), Expr::BinaryOp { left, op, right } => write!(f, "{} {} {}", left, op, right), + Expr::Like { + negated, + expr, + pattern, + escape_char, + } => match escape_char { + Some(ch) => write!( + f, + "{} {}LIKE {} ESCAPE '{}'", + expr, + if *negated { "NOT " } else { "" }, + pattern, + ch + ), + _ => write!( + f, + "{} {}LIKE {}", + expr, + if *negated { "NOT " } else { "" }, + pattern + ), + }, + Expr::ILike { + negated, + expr, + pattern, + escape_char, + } => match escape_char { + Some(ch) => write!( + f, + "{} {}ILIKE {} ESCAPE '{}'", + expr, + if *negated { "NOT " } else { "" }, + pattern, + ch + ), + _ => write!( + f, + "{} {}ILIKE {}", + expr, + if *negated { "NOT " } else { "" }, + pattern + ), + }, + Expr::SimilarTo { + negated, + expr, + pattern, + escape_char, + } => match escape_char { + Some(ch) => write!( + f, + "{} {}SIMILAR TO {} ESCAPE '{}'", + expr, + if *negated { "NOT " } else { "" }, + pattern, + ch + ), + _ => write!( + f, + "{} {}SIMILAR TO {}", + expr, + if *negated { "NOT " } else { "" }, + pattern + ), + }, Expr::AnyOp(expr) => write!(f, "ANY({})", expr), Expr::AllOp(expr) => write!(f, "ALL({})", expr), Expr::UnaryOp { op, expr } => { From ab12adf6547724835c9ca64dea6e2d6a67d0ea71 Mon Sep 17 00:00:00 2001 From: Ayush Dattagupta Date: Wed, 10 Aug 2022 09:51:39 -0700 Subject: [PATCH 3/7] Add parsing logic for similar to, update parsing logic for like/ilike --- src/parser.rs | 52 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 76190feb9..e6bdb32fb 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + // Licensed 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 @@ -1161,17 +1163,6 @@ impl<'a> Parser<'a> { Token::Word(w) => match w.keyword { Keyword::AND => Some(BinaryOperator::And), Keyword::OR => Some(BinaryOperator::Or), - Keyword::LIKE => Some(BinaryOperator::Like), - Keyword::ILIKE => Some(BinaryOperator::ILike), - Keyword::NOT => { - if self.parse_keyword(Keyword::LIKE) { - Some(BinaryOperator::NotLike) - } else if self.parse_keyword(Keyword::ILIKE) { - Some(BinaryOperator::NotILike) - } else { - None - } - } Keyword::XOR => Some(BinaryOperator::Xor), Keyword::OPERATOR if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { self.expect_token(&Token::LParen)?; @@ -1265,13 +1256,39 @@ impl<'a> Parser<'a> { self.expected("Expected Token::Word after AT", tok) } } - Keyword::NOT | Keyword::IN | Keyword::BETWEEN => { + Keyword::NOT + | Keyword::IN + | Keyword::BETWEEN + | Keyword::LIKE + | Keyword::ILIKE + | Keyword::SIMILAR => { self.prev_token(); let negated = self.parse_keyword(Keyword::NOT); if self.parse_keyword(Keyword::IN) { self.parse_in(expr, negated) } else if self.parse_keyword(Keyword::BETWEEN) { self.parse_between(expr, negated) + } else if self.parse_keyword(Keyword::LIKE) { + Ok(Expr::Like { + negated, + expr: Box::new(expr), + pattern: Box::new(self.parse_value()?), + escape_char: self.parse_escape_char()?, + }) + } else if self.parse_keyword(Keyword::ILIKE) { + Ok(Expr::ILike { + negated, + expr: Box::new(expr), + pattern: Box::new(self.parse_value()?), + escape_char: self.parse_escape_char()?, + }) + } else if self.parse_keywords(&[Keyword::SIMILAR, Keyword::TO]) { + Ok(Expr::SimilarTo { + negated, + expr: Box::new(expr), + pattern: Box::new(self.parse_value()?), + escape_char: self.parse_escape_char()?, + }) } else { self.expected("IN or BETWEEN after NOT", self.peek_token()) } @@ -1316,6 +1333,15 @@ impl<'a> Parser<'a> { } } + /// parse the ESCAPE CHAR portion of LIKE, ILIKE, and SIMILAR TO + pub fn parse_escape_char(&mut self) -> Result, ParserError> { + if self.parse_keyword(Keyword::ESCAPE) { + Ok(Some(self.parse_literal_char()?)) + } else { + Ok(None) + } + } + pub fn parse_array_index(&mut self, expr: Expr) -> Result { let index = self.parse_expr()?; self.expect_token(&Token::RBracket)?; @@ -1446,6 +1472,7 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::BETWEEN => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::BETWEEN_PREC), _ => Ok(0), }, Token::Word(w) if w.keyword == Keyword::IS => Ok(17), @@ -1454,6 +1481,7 @@ impl<'a> Parser<'a> { Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::BETWEEN_PREC), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(Self::BETWEEN_PREC), + Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::BETWEEN_PREC), Token::Eq | Token::Lt | Token::LtEq From de06632d2ab7f92ab08e5a674148b65ebf0fc5c5 Mon Sep 17 00:00:00 2001 From: Ayush Dattagupta Date: Wed, 10 Aug 2022 09:52:06 -0700 Subject: [PATCH 4/7] Add tests for similar to, update tests for like/ilike --- tests/sqlparser_common.rs | 102 ++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ef6011841..ac74b7838 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1,3 +1,5 @@ +// SPDX-FileCopyrightText: Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + // Licensed 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 @@ -857,10 +859,11 @@ fn parse_not_precedence() { verified_expr(sql), Expr::UnaryOp { op: UnaryOperator::Not, - expr: Box::new(Expr::BinaryOp { - left: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), - op: BinaryOperator::NotLike, - right: Box::new(Expr::Value(Value::SingleQuotedString("b".into()))), + expr: Box::new(Expr::Like { + expr: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), + negated: true, + pattern: Box::new(Value::SingleQuotedString("b".into())), + escape_char: None }), }, ); @@ -889,14 +892,11 @@ fn parse_like() { ); let select = verified_only_select(sql); assert_eq!( - Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("name"))), - op: if negated { - BinaryOperator::NotLike - } else { - BinaryOperator::Like - }, - right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated: negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None }, select.selection.unwrap() ); @@ -909,14 +909,11 @@ fn parse_like() { ); let select = verified_only_select(sql); assert_eq!( - Expr::IsNull(Box::new(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("name"))), - op: if negated { - BinaryOperator::NotLike - } else { - BinaryOperator::Like - }, - right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + Expr::IsNull(Box::new(Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated: negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None })), select.selection.unwrap() ); @@ -934,14 +931,11 @@ fn parse_ilike() { ); let select = verified_only_select(sql); assert_eq!( - Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("name"))), - op: if negated { - BinaryOperator::NotILike - } else { - BinaryOperator::ILike - }, - right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + Expr::ILike { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated: negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None }, select.selection.unwrap() ); @@ -954,14 +948,50 @@ fn parse_ilike() { ); let select = verified_only_select(sql); assert_eq!( - Expr::IsNull(Box::new(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("name"))), - op: if negated { - BinaryOperator::NotILike - } else { - BinaryOperator::ILike - }, - right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), + Expr::IsNull(Box::new(Expr::ILike { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated: negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None + })), + select.selection.unwrap() + ); + } + chk(false); + chk(true); +} + +#[test] +fn parse_similar_to() { + fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated: negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: Some('\\') + }, + select.selection.unwrap() + ); + + // This statement tests that LIKE and NOT LIKE have the same precedence. + // This was previously mishandled (#81). + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated: negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: Some('\\') })), select.selection.unwrap() ); From dc8615e62a4a7d940211ad2fe6294428c4839f64 Mon Sep 17 00:00:00 2001 From: Ayush Dattagupta Date: Wed, 10 Aug 2022 10:02:37 -0700 Subject: [PATCH 5/7] Fix linter warnings --- tests/sqlparser_common.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ac74b7838..ae80eda07 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -894,7 +894,7 @@ fn parse_like() { assert_eq!( Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated: negated, + negated, pattern: Box::new(Value::SingleQuotedString("%a".to_string())), escape_char: None }, @@ -911,7 +911,7 @@ fn parse_like() { assert_eq!( Expr::IsNull(Box::new(Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated: negated, + negated, pattern: Box::new(Value::SingleQuotedString("%a".to_string())), escape_char: None })), @@ -933,7 +933,7 @@ fn parse_ilike() { assert_eq!( Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated: negated, + negated, pattern: Box::new(Value::SingleQuotedString("%a".to_string())), escape_char: None }, @@ -950,7 +950,7 @@ fn parse_ilike() { assert_eq!( Expr::IsNull(Box::new(Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated: negated, + negated, pattern: Box::new(Value::SingleQuotedString("%a".to_string())), escape_char: None })), @@ -972,7 +972,7 @@ fn parse_similar_to() { assert_eq!( Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated: negated, + negated, pattern: Box::new(Value::SingleQuotedString("%a".to_string())), escape_char: Some('\\') }, @@ -989,7 +989,7 @@ fn parse_similar_to() { assert_eq!( Expr::IsNull(Box::new(Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), - negated: negated, + negated, pattern: Box::new(Value::SingleQuotedString("%a".to_string())), escape_char: Some('\\') })), From 70cc51714b2ba51b8b7e82ad68077ab917697ce9 Mon Sep 17 00:00:00 2001 From: Ayush Dattagupta Date: Thu, 11 Aug 2022 11:07:40 -0700 Subject: [PATCH 6/7] remove additional copyright license from files --- src/ast/mod.rs | 2 -- src/ast/operator.rs | 2 -- src/parser.rs | 2 -- tests/sqlparser_common.rs | 2 -- 4 files changed, 8 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e3207fc74..10dd29eb3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1,5 +1,3 @@ -// SPDX-FileCopyrightText: Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - // Licensed 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 diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 2648256fe..1c96ebbcb 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -1,5 +1,3 @@ -// SPDX-FileCopyrightText: Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - // Licensed 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 diff --git a/src/parser.rs b/src/parser.rs index e6bdb32fb..62f95311d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,5 +1,3 @@ -// SPDX-FileCopyrightText: Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - // Licensed 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 diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ae80eda07..511879eab 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1,5 +1,3 @@ -// SPDX-FileCopyrightText: Copyright (c) 2022, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - // Licensed 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 From 1babc7b4f6a56f1be2b851e03b32256470ebd691 Mon Sep 17 00:00:00 2001 From: Ayush Dattagupta Date: Thu, 11 Aug 2022 11:40:02 -0700 Subject: [PATCH 7/7] Add more coverage w/wo escape char for like,ilike,similar to --- tests/sqlparser_common.rs | 53 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 511879eab..93685dfcb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -899,6 +899,22 @@ fn parse_like() { select.selection.unwrap() ); + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}LIKE '%a' ESCAPE '\\'", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: Some('\\') + }, + select.selection.unwrap() + ); + // This statement tests that LIKE and NOT LIKE have the same precedence. // This was previously mishandled (#81). let sql = &format!( @@ -938,7 +954,23 @@ fn parse_ilike() { select.selection.unwrap() ); - // This statement tests that LIKE and NOT LIKE have the same precedence. + // Test with escape char + let sql = &format!( + "SELECT * FROM customers WHERE name {}ILIKE '%a' ESCAPE '^'", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::ILike { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: Some('^') + }, + select.selection.unwrap() + ); + + // This statement tests that ILIKE and NOT ILIKE have the same precedence. // This was previously mishandled (#81). let sql = &format!( "SELECT * FROM customers WHERE name {}ILIKE '%a' IS NULL", @@ -962,6 +994,22 @@ fn parse_ilike() { #[test] fn parse_similar_to() { fn chk(negated: bool) { + let sql = &format!( + "SELECT * FROM customers WHERE name {}SIMILAR TO '%a'", + if negated { "NOT " } else { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None + }, + select.selection.unwrap() + ); + + // Test with escape char let sql = &format!( "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\'", if negated { "NOT " } else { "" } @@ -977,8 +1025,7 @@ fn parse_similar_to() { select.selection.unwrap() ); - // This statement tests that LIKE and NOT LIKE have the same precedence. - // This was previously mishandled (#81). + // This statement tests that SIMILAR TO and NOT SIMILAR TO have the same precedence. let sql = &format!( "SELECT * FROM customers WHERE name {}SIMILAR TO '%a' ESCAPE '\\' IS NULL", if negated { "NOT " } else { "" }