Support for SIMILAR TO syntax, change Like and ILike to Expr variants, allow escape char for like/ilike (#569)

* Remove [not]like,[not]ilike from binary operator list

* Add like, ilike and similar as an expr variant. Also adds support for escape char to like/ilike

* Add parsing logic for similar to, update parsing logic for like/ilike

* Add tests for similar to, update tests for like/ilike

* Fix linter warnings

* remove additional copyright license from files

* Add more coverage w/wo escape char for like,ilike,similar to
This commit is contained in:
Ayush Dattagupta 2022-08-11 12:44:26 -07:00 committed by GitHub
parent 18881f8fcf
commit f07063f0cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 237 additions and 57 deletions

View file

@ -268,6 +268,27 @@ pub enum Expr {
op: BinaryOperator,
right: Box<Expr>,
},
/// LIKE
Like {
negated: bool,
expr: Box<Expr>,
pattern: Box<Value>,
escape_char: Option<char>,
},
/// ILIKE (case-insensitive LIKE)
ILike {
negated: bool,
expr: Box<Expr>,
pattern: Box<Value>,
escape_char: Option<char>,
},
/// SIMILAR TO regex
SimilarTo {
negated: bool,
expr: Box<Expr>,
pattern: Box<Value>,
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
AnyOp(Box<Expr>),
/// 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 } => {

View file

@ -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("^"),

View file

@ -1178,17 +1178,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)?;
@ -1282,13 +1271,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())
}
@ -1333,6 +1348,15 @@ impl<'a> Parser<'a> {
}
}
/// parse the ESCAPE CHAR portion of LIKE, ILIKE, and SIMILAR TO
pub fn parse_escape_char(&mut self) -> Result<Option<char>, 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<Expr, ParserError> {
let index = self.parse_expr()?;
self.expect_token(&Token::RBracket)?;
@ -1463,6 +1487,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),
@ -1471,6 +1496,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

View file

@ -859,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
}),
},
);
@ -891,14 +892,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()
);
@ -911,14 +925,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()
);
@ -936,19 +947,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",
@ -956,14 +980,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()
);