Skip to content

Commit

Permalink
Parse LIKE patterns as Expr not Value (sqlparser-rs#579)
Browse files Browse the repository at this point in the history
  • Loading branch information
andygrove authored and MazterQyou committed Dec 1, 2022
1 parent 53716f1 commit 1a33fd3
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 28 deletions.
6 changes: 3 additions & 3 deletions src/ast/mod.rs
Expand Up @@ -236,21 +236,21 @@ pub enum Expr {
Like {
negated: bool,
expr: Box<Expr>,
pattern: Box<Value>,
pattern: Box<Expr>,
escape_char: Option<char>,
},
/// ILIKE (case-insensitive LIKE)
ILike {
negated: bool,
expr: Box<Expr>,
pattern: Box<Value>,
pattern: Box<Expr>,
escape_char: Option<char>,
},
/// SIMILAR TO regex
SimilarTo {
negated: bool,
expr: Box<Expr>,
pattern: Box<Value>,
pattern: Box<Expr>,
escape_char: Option<char>,
},
/// Any operation e.g. `1 ANY (1)` or `foo > ANY(bar)`, It will be wrapped in the right side of BinaryExpr
Expand Down
36 changes: 21 additions & 15 deletions src/parser.rs
Expand Up @@ -1291,21 +1291,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 {
Expand Down Expand Up @@ -1435,10 +1435,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<u8, ParserError> {
Expand All @@ -1449,9 +1455,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)) {
Expand All @@ -1472,17 +1478,17 @@ 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::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),
Token::Eq
| Token::Lt
| Token::LtEq
Expand Down
59 changes: 49 additions & 10 deletions tests/sqlparser_common.rs
Expand Up @@ -793,7 +793,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
}),
},
Expand Down Expand Up @@ -826,7 +826,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()
Expand All @@ -842,7 +842,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()
Expand All @@ -859,7 +859,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()
Expand All @@ -869,6 +869,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) {
Expand All @@ -881,7 +920,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()
Expand All @@ -897,7 +936,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()
Expand All @@ -914,7 +953,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()
Expand All @@ -936,7 +975,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()
Expand All @@ -952,7 +991,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()
Expand All @@ -968,7 +1007,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()
Expand Down

0 comments on commit 1a33fd3

Please sign in to comment.