diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 312a86813..10dd29eb3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -268,6 +268,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 +459,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 } => { diff --git a/src/ast/operator.rs b/src/ast/operator.rs index f7a63a4a4..1c96ebbcb 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -76,10 +76,6 @@ pub enum BinaryOperator { And, Or, Xor, - Like, - NotLike, - ILike, - NotILike, BitwiseOr, BitwiseAnd, BitwiseXor, @@ -116,10 +112,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("^"), diff --git a/src/parser.rs b/src/parser.rs index 76190feb9..62f95311d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1161,17 +1161,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 +1254,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 +1331,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 +1470,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 +1479,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 diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ef6011841..93685dfcb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -857,10 +857,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 +890,27 @@ 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, + 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 {}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() ); @@ -909,14 +923,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, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None })), select.selection.unwrap() ); @@ -934,19 +945,32 @@ 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, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: None }, 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", @@ -954,14 +978,65 @@ 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, + 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'", + 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 { "" } + ); + 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: Some('\\') + }, + select.selection.unwrap() + ); + + // 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 { "" } + ); + let select = verified_only_select(sql); + assert_eq!( + Expr::IsNull(Box::new(Expr::SimilarTo { + expr: Box::new(Expr::Identifier(Ident::new("name"))), + negated, + pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + escape_char: Some('\\') })), select.selection.unwrap() );