From eb7f1b005eb339735760a7c4b1cbf44e251a8c95 Mon Sep 17 00:00:00 2001 From: Andy Grove Date: Thu, 18 Aug 2022 10:02:54 -0600 Subject: [PATCH] Parse LIKE patterns as Expr not Value (#579) --- src/ast/mod.rs | 6 ++-- src/parser.rs | 36 ++++++++++++++---------- tests/sqlparser_common.rs | 59 ++++++++++++++++++++++++++++++++------- 3 files changed, 73 insertions(+), 28 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8015f9c73..832b09bcc 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -280,21 +280,21 @@ pub enum Expr { Like { negated: bool, expr: Box, - pattern: Box, + pattern: Box, escape_char: Option, }, /// ILIKE (case-insensitive LIKE) ILike { negated: bool, expr: Box, - pattern: Box, + pattern: Box, escape_char: Option, }, /// SIMILAR TO regex SimilarTo { negated: bool, expr: Box, - pattern: 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 diff --git a/src/parser.rs b/src/parser.rs index 5cd6ae14b..a0d6fd9ad 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1317,21 +1317,21 @@ impl<'a> Parser<'a> { Ok(Expr::Like { negated, expr: Box::new(expr), - pattern: Box::new(self.parse_value()?), + pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), 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()?), + pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), 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()?), + pattern: Box::new(self.parse_subexpr(Self::LIKE_PREC)?), escape_char: self.parse_escape_char()?, }) } else { @@ -1478,10 +1478,16 @@ impl<'a> Parser<'a> { }) } - const UNARY_NOT_PREC: u8 = 15; - const BETWEEN_PREC: u8 = 20; + // use https://www.postgresql.org/docs/7.0/operators.htm#AEN2026 as a reference const PLUS_MINUS_PREC: u8 = 30; + const XOR_PREC: u8 = 24; const TIME_ZONE_PREC: u8 = 20; + const BETWEEN_PREC: u8 = 20; + const LIKE_PREC: u8 = 19; + const IS_PREC: u8 = 17; + const UNARY_NOT_PREC: u8 = 15; + const AND_PREC: u8 = 10; + const OR_PREC: u8 = 5; /// Get the precedence of the next token pub fn get_next_precedence(&self) -> Result { @@ -1492,9 +1498,9 @@ impl<'a> Parser<'a> { let token_2 = self.peek_nth_token(2); debug!("0: {token_0} 1: {token_1} 2: {token_2}"); match token { - Token::Word(w) if w.keyword == Keyword::OR => Ok(5), - Token::Word(w) if w.keyword == Keyword::AND => Ok(10), - Token::Word(w) if w.keyword == Keyword::XOR => Ok(24), + Token::Word(w) if w.keyword == Keyword::OR => Ok(Self::OR_PREC), + Token::Word(w) if w.keyword == Keyword::AND => Ok(Self::AND_PREC), + Token::Word(w) if w.keyword == Keyword::XOR => Ok(Self::XOR_PREC), Token::Word(w) if w.keyword == Keyword::AT => { match (self.peek_nth_token(1), self.peek_nth_token(2)) { @@ -1515,18 +1521,18 @@ impl<'a> Parser<'a> { // precedence. Token::Word(w) if w.keyword == Keyword::IN => Ok(Self::BETWEEN_PREC), 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), + Token::Word(w) if w.keyword == Keyword::LIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::LIKE_PREC), _ => Ok(0), }, - Token::Word(w) if w.keyword == Keyword::IS => Ok(17), + Token::Word(w) if w.keyword == Keyword::IS => Ok(Self::IS_PREC), Token::Word(w) if w.keyword == Keyword::IN => Ok(Self::BETWEEN_PREC), 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::LIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::ILIKE => Ok(Self::LIKE_PREC), + Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::LIKE_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 c87b0c5e8..5f4b48567 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -862,7 +862,7 @@ fn parse_not_precedence() { expr: Box::new(Expr::Like { expr: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), negated: true, - pattern: Box::new(Value::SingleQuotedString("b".into())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("b".into()))), escape_char: None }), }, @@ -895,7 +895,7 @@ fn parse_like() { Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None }, select.selection.unwrap() @@ -911,7 +911,7 @@ fn parse_like() { Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: Some('\\') }, select.selection.unwrap() @@ -928,7 +928,7 @@ fn parse_like() { Expr::IsNull(Box::new(Expr::Like { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None })), select.selection.unwrap() @@ -938,6 +938,45 @@ fn parse_like() { chk(true); } +#[test] +fn parse_null_like() { + let sql = "SELECT \ + column1 LIKE NULL AS col_null, \ + NULL LIKE column1 AS null_col \ + FROM customers"; + let select = verified_only_select(sql); + assert_eq!( + SelectItem::ExprWithAlias { + expr: Expr::Like { + expr: Box::new(Expr::Identifier(Ident::new("column1"))), + negated: false, + pattern: Box::new(Expr::Value(Value::Null)), + escape_char: None + }, + alias: Ident { + value: "col_null".to_owned(), + quote_style: None + } + }, + select.projection[0] + ); + assert_eq!( + SelectItem::ExprWithAlias { + expr: Expr::Like { + expr: Box::new(Expr::Value(Value::Null)), + negated: false, + pattern: Box::new(Expr::Identifier(Ident::new("column1"))), + escape_char: None + }, + alias: Ident { + value: "null_col".to_owned(), + quote_style: None + } + }, + select.projection[1] + ); +} + #[test] fn parse_ilike() { fn chk(negated: bool) { @@ -950,7 +989,7 @@ fn parse_ilike() { Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None }, select.selection.unwrap() @@ -966,7 +1005,7 @@ fn parse_ilike() { Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: Some('^') }, select.selection.unwrap() @@ -983,7 +1022,7 @@ fn parse_ilike() { Expr::IsNull(Box::new(Expr::ILike { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None })), select.selection.unwrap() @@ -1005,7 +1044,7 @@ fn parse_similar_to() { Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: None }, select.selection.unwrap() @@ -1021,7 +1060,7 @@ fn parse_similar_to() { Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: Some('\\') }, select.selection.unwrap() @@ -1037,7 +1076,7 @@ fn parse_similar_to() { Expr::IsNull(Box::new(Expr::SimilarTo { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, - pattern: Box::new(Value::SingleQuotedString("%a".to_string())), + pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), escape_char: Some('\\') })), select.selection.unwrap()