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, op: BinaryOperator,
right: Box<Expr>, 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 /// Any operation e.g. `1 ANY (1)` or `foo > ANY(bar)`, It will be wrapped in the right side of BinaryExpr
AnyOp(Box<Expr>), AnyOp(Box<Expr>),
/// ALL operation e.g. `1 ALL (1)` or `foo > ALL(bar)`, It will be wrapped in the right side of BinaryExpr /// 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 high
), ),
Expr::BinaryOp { left, op, right } => write!(f, "{} {} {}", left, op, right), 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::AnyOp(expr) => write!(f, "ANY({})", expr),
Expr::AllOp(expr) => write!(f, "ALL({})", expr), Expr::AllOp(expr) => write!(f, "ALL({})", expr),
Expr::UnaryOp { op, expr } => { Expr::UnaryOp { op, expr } => {

View file

@ -76,10 +76,6 @@ pub enum BinaryOperator {
And, And,
Or, Or,
Xor, Xor,
Like,
NotLike,
ILike,
NotILike,
BitwiseOr, BitwiseOr,
BitwiseAnd, BitwiseAnd,
BitwiseXor, BitwiseXor,
@ -116,10 +112,6 @@ impl fmt::Display for BinaryOperator {
BinaryOperator::And => f.write_str("AND"), BinaryOperator::And => f.write_str("AND"),
BinaryOperator::Or => f.write_str("OR"), BinaryOperator::Or => f.write_str("OR"),
BinaryOperator::Xor => f.write_str("XOR"), 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::BitwiseOr => f.write_str("|"),
BinaryOperator::BitwiseAnd => f.write_str("&"), BinaryOperator::BitwiseAnd => f.write_str("&"),
BinaryOperator::BitwiseXor => f.write_str("^"), BinaryOperator::BitwiseXor => f.write_str("^"),

View file

@ -1178,17 +1178,6 @@ impl<'a> Parser<'a> {
Token::Word(w) => match w.keyword { Token::Word(w) => match w.keyword {
Keyword::AND => Some(BinaryOperator::And), Keyword::AND => Some(BinaryOperator::And),
Keyword::OR => Some(BinaryOperator::Or), 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::XOR => Some(BinaryOperator::Xor),
Keyword::OPERATOR if dialect_of!(self is PostgreSqlDialect | GenericDialect) => { Keyword::OPERATOR if dialect_of!(self is PostgreSqlDialect | GenericDialect) => {
self.expect_token(&Token::LParen)?; self.expect_token(&Token::LParen)?;
@ -1282,13 +1271,39 @@ impl<'a> Parser<'a> {
self.expected("Expected Token::Word after AT", tok) 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(); self.prev_token();
let negated = self.parse_keyword(Keyword::NOT); let negated = self.parse_keyword(Keyword::NOT);
if self.parse_keyword(Keyword::IN) { if self.parse_keyword(Keyword::IN) {
self.parse_in(expr, negated) self.parse_in(expr, negated)
} else if self.parse_keyword(Keyword::BETWEEN) { } else if self.parse_keyword(Keyword::BETWEEN) {
self.parse_between(expr, negated) 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 { } else {
self.expected("IN or BETWEEN after NOT", self.peek_token()) 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> { pub fn parse_array_index(&mut self, expr: Expr) -> Result<Expr, ParserError> {
let index = self.parse_expr()?; let index = self.parse_expr()?;
self.expect_token(&Token::RBracket)?; 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::BETWEEN => Ok(Self::BETWEEN_PREC),
Token::Word(w) if w.keyword == Keyword::LIKE => 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::ILIKE => Ok(Self::BETWEEN_PREC),
Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::BETWEEN_PREC),
_ => Ok(0), _ => Ok(0),
}, },
Token::Word(w) if w.keyword == Keyword::IS => Ok(17), 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::LIKE => Ok(Self::BETWEEN_PREC),
Token::Word(w) if w.keyword == Keyword::ILIKE => 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::OPERATOR => Ok(Self::BETWEEN_PREC),
Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(Self::BETWEEN_PREC),
Token::Eq Token::Eq
| Token::Lt | Token::Lt
| Token::LtEq | Token::LtEq

View file

@ -859,10 +859,11 @@ fn parse_not_precedence() {
verified_expr(sql), verified_expr(sql),
Expr::UnaryOp { Expr::UnaryOp {
op: UnaryOperator::Not, op: UnaryOperator::Not,
expr: Box::new(Expr::BinaryOp { expr: Box::new(Expr::Like {
left: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), expr: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))),
op: BinaryOperator::NotLike, negated: true,
right: Box::new(Expr::Value(Value::SingleQuotedString("b".into()))), pattern: Box::new(Value::SingleQuotedString("b".into())),
escape_char: None
}), }),
}, },
); );
@ -891,14 +892,27 @@ fn parse_like() {
); );
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
Expr::BinaryOp { Expr::Like {
left: Box::new(Expr::Identifier(Ident::new("name"))), expr: Box::new(Expr::Identifier(Ident::new("name"))),
op: if negated { negated,
BinaryOperator::NotLike pattern: Box::new(Value::SingleQuotedString("%a".to_string())),
} else { escape_char: None
BinaryOperator::Like
}, },
right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), 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() select.selection.unwrap()
); );
@ -911,14 +925,11 @@ fn parse_like() {
); );
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
Expr::IsNull(Box::new(Expr::BinaryOp { Expr::IsNull(Box::new(Expr::Like {
left: Box::new(Expr::Identifier(Ident::new("name"))), expr: Box::new(Expr::Identifier(Ident::new("name"))),
op: if negated { negated,
BinaryOperator::NotLike pattern: Box::new(Value::SingleQuotedString("%a".to_string())),
} else { escape_char: None
BinaryOperator::Like
},
right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
})), })),
select.selection.unwrap() select.selection.unwrap()
); );
@ -936,19 +947,32 @@ fn parse_ilike() {
); );
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
Expr::BinaryOp { Expr::ILike {
left: Box::new(Expr::Identifier(Ident::new("name"))), expr: Box::new(Expr::Identifier(Ident::new("name"))),
op: if negated { negated,
BinaryOperator::NotILike pattern: Box::new(Value::SingleQuotedString("%a".to_string())),
} else { escape_char: None
BinaryOperator::ILike
},
right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))),
}, },
select.selection.unwrap() 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). // This was previously mishandled (#81).
let sql = &format!( let sql = &format!(
"SELECT * FROM customers WHERE name {}ILIKE '%a' IS NULL", "SELECT * FROM customers WHERE name {}ILIKE '%a' IS NULL",
@ -956,14 +980,65 @@ fn parse_ilike() {
); );
let select = verified_only_select(sql); let select = verified_only_select(sql);
assert_eq!( assert_eq!(
Expr::IsNull(Box::new(Expr::BinaryOp { Expr::IsNull(Box::new(Expr::ILike {
left: Box::new(Expr::Identifier(Ident::new("name"))), expr: Box::new(Expr::Identifier(Ident::new("name"))),
op: if negated { negated,
BinaryOperator::NotILike pattern: Box::new(Value::SingleQuotedString("%a".to_string())),
} else { escape_char: None
BinaryOperator::ILike })),
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
}, },
right: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), 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() select.selection.unwrap()
); );