mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-08-04 06:18:17 +00:00
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:
parent
18881f8fcf
commit
f07063f0cd
4 changed files with 237 additions and 57 deletions
|
@ -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 } => {
|
||||
|
|
|
@ -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("^"),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue