Skip to content

Commit c7d2903

Browse files
authored
Add Postgres operators for the LIKE expression variants (#1096)
1 parent d72f0a9 commit c7d2903

File tree

4 files changed

+111
-0
lines changed

4 files changed

+111
-0
lines changed

src/ast/operator.rs

+12
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ pub enum BinaryOperator {
131131
PGRegexNotMatch,
132132
/// String does not match regular expression (case insensitively), e.g. `a !~* b` (PostgreSQL-specific)
133133
PGRegexNotIMatch,
134+
/// String matches pattern (case sensitively), e.g. `a ~~ b` (PostgreSQL-specific)
135+
PGLikeMatch,
136+
/// String matches pattern (case insensitively), e.g. `a ~~* b` (PostgreSQL-specific)
137+
PGILikeMatch,
138+
/// String does not match pattern (case sensitively), e.g. `a !~~ b` (PostgreSQL-specific)
139+
PGNotLikeMatch,
140+
/// String does not match pattern (case insensitively), e.g. `a !~~* b` (PostgreSQL-specific)
141+
PGNotILikeMatch,
134142
/// String "starts with", eg: `a ^@ b` (PostgreSQL-specific)
135143
PGStartsWith,
136144
/// PostgreSQL-specific custom operator.
@@ -174,6 +182,10 @@ impl fmt::Display for BinaryOperator {
174182
BinaryOperator::PGRegexIMatch => f.write_str("~*"),
175183
BinaryOperator::PGRegexNotMatch => f.write_str("!~"),
176184
BinaryOperator::PGRegexNotIMatch => f.write_str("!~*"),
185+
BinaryOperator::PGLikeMatch => f.write_str("~~"),
186+
BinaryOperator::PGILikeMatch => f.write_str("~~*"),
187+
BinaryOperator::PGNotLikeMatch => f.write_str("!~~"),
188+
BinaryOperator::PGNotILikeMatch => f.write_str("!~~*"),
177189
BinaryOperator::PGStartsWith => f.write_str("^@"),
178190
BinaryOperator::PGCustomBinaryOperator(idents) => {
179191
write!(f, "OPERATOR({})", display_separated(idents, "."))

src/parser/mod.rs

+8
Original file line numberDiff line numberDiff line change
@@ -2205,6 +2205,10 @@ impl<'a> Parser<'a> {
22052205
Token::TildeAsterisk => Some(BinaryOperator::PGRegexIMatch),
22062206
Token::ExclamationMarkTilde => Some(BinaryOperator::PGRegexNotMatch),
22072207
Token::ExclamationMarkTildeAsterisk => Some(BinaryOperator::PGRegexNotIMatch),
2208+
Token::DoubleTilde => Some(BinaryOperator::PGLikeMatch),
2209+
Token::DoubleTildeAsterisk => Some(BinaryOperator::PGILikeMatch),
2210+
Token::ExclamationMarkDoubleTilde => Some(BinaryOperator::PGNotLikeMatch),
2211+
Token::ExclamationMarkDoubleTildeAsterisk => Some(BinaryOperator::PGNotILikeMatch),
22082212
Token::Word(w) => match w.keyword {
22092213
Keyword::AND => Some(BinaryOperator::And),
22102214
Keyword::OR => Some(BinaryOperator::Or),
@@ -2620,6 +2624,10 @@ impl<'a> Parser<'a> {
26202624
| Token::TildeAsterisk
26212625
| Token::ExclamationMarkTilde
26222626
| Token::ExclamationMarkTildeAsterisk
2627+
| Token::DoubleTilde
2628+
| Token::DoubleTildeAsterisk
2629+
| Token::ExclamationMarkDoubleTilde
2630+
| Token::ExclamationMarkDoubleTildeAsterisk
26232631
| Token::Spaceship => Ok(20),
26242632
Token::Pipe => Ok(21),
26252633
Token::Caret | Token::Sharp | Token::ShiftRight | Token::ShiftLeft => Ok(22),

src/tokenizer.rs

+69
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,14 @@ pub enum Token {
149149
ExclamationMarkTilde,
150150
/// `!~*` , a case insensitive not match regular expression operator in PostgreSQL
151151
ExclamationMarkTildeAsterisk,
152+
/// `~~`, a case sensitive match pattern operator in PostgreSQL
153+
DoubleTilde,
154+
/// `~~*`, a case insensitive match pattern operator in PostgreSQL
155+
DoubleTildeAsterisk,
156+
/// `!~~`, a case sensitive not match pattern operator in PostgreSQL
157+
ExclamationMarkDoubleTilde,
158+
/// `!~~*`, a case insensitive not match pattern operator in PostgreSQL
159+
ExclamationMarkDoubleTildeAsterisk,
152160
/// `<<`, a bitwise shift left operator in PostgreSQL
153161
ShiftLeft,
154162
/// `>>`, a bitwise shift right operator in PostgreSQL
@@ -249,6 +257,10 @@ impl fmt::Display for Token {
249257
Token::TildeAsterisk => f.write_str("~*"),
250258
Token::ExclamationMarkTilde => f.write_str("!~"),
251259
Token::ExclamationMarkTildeAsterisk => f.write_str("!~*"),
260+
Token::DoubleTilde => f.write_str("~~"),
261+
Token::DoubleTildeAsterisk => f.write_str("~~*"),
262+
Token::ExclamationMarkDoubleTilde => f.write_str("!~~"),
263+
Token::ExclamationMarkDoubleTildeAsterisk => f.write_str("!~~*"),
252264
Token::AtSign => f.write_str("@"),
253265
Token::CaretAt => f.write_str("^@"),
254266
Token::ShiftLeft => f.write_str("<<"),
@@ -903,6 +915,16 @@ impl<'a> Tokenizer<'a> {
903915
match chars.peek() {
904916
Some('*') => self
905917
.consume_and_return(chars, Token::ExclamationMarkTildeAsterisk),
918+
Some('~') => {
919+
chars.next();
920+
match chars.peek() {
921+
Some('*') => self.consume_and_return(
922+
chars,
923+
Token::ExclamationMarkDoubleTildeAsterisk,
924+
),
925+
_ => Ok(Some(Token::ExclamationMarkDoubleTilde)),
926+
}
927+
}
906928
_ => Ok(Some(Token::ExclamationMarkTilde)),
907929
}
908930
}
@@ -974,6 +996,15 @@ impl<'a> Tokenizer<'a> {
974996
chars.next(); // consume
975997
match chars.peek() {
976998
Some('*') => self.consume_and_return(chars, Token::TildeAsterisk),
999+
Some('~') => {
1000+
chars.next();
1001+
match chars.peek() {
1002+
Some('*') => {
1003+
self.consume_and_return(chars, Token::DoubleTildeAsterisk)
1004+
}
1005+
_ => Ok(Some(Token::DoubleTilde)),
1006+
}
1007+
}
9771008
_ => Ok(Some(Token::Tilde)),
9781009
}
9791010
}
@@ -1994,6 +2025,44 @@ mod tests {
19942025
compare(expected, tokens);
19952026
}
19962027

2028+
#[test]
2029+
fn tokenize_pg_like_match() {
2030+
let sql = "SELECT col ~~ '_a%', col ~~* '_a%', col !~~ '_a%', col !~~* '_a%'";
2031+
let dialect = GenericDialect {};
2032+
let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap();
2033+
let expected = vec![
2034+
Token::make_keyword("SELECT"),
2035+
Token::Whitespace(Whitespace::Space),
2036+
Token::make_word("col", None),
2037+
Token::Whitespace(Whitespace::Space),
2038+
Token::DoubleTilde,
2039+
Token::Whitespace(Whitespace::Space),
2040+
Token::SingleQuotedString("_a%".into()),
2041+
Token::Comma,
2042+
Token::Whitespace(Whitespace::Space),
2043+
Token::make_word("col", None),
2044+
Token::Whitespace(Whitespace::Space),
2045+
Token::DoubleTildeAsterisk,
2046+
Token::Whitespace(Whitespace::Space),
2047+
Token::SingleQuotedString("_a%".into()),
2048+
Token::Comma,
2049+
Token::Whitespace(Whitespace::Space),
2050+
Token::make_word("col", None),
2051+
Token::Whitespace(Whitespace::Space),
2052+
Token::ExclamationMarkDoubleTilde,
2053+
Token::Whitespace(Whitespace::Space),
2054+
Token::SingleQuotedString("_a%".into()),
2055+
Token::Comma,
2056+
Token::Whitespace(Whitespace::Space),
2057+
Token::make_word("col", None),
2058+
Token::Whitespace(Whitespace::Space),
2059+
Token::ExclamationMarkDoubleTildeAsterisk,
2060+
Token::Whitespace(Whitespace::Space),
2061+
Token::SingleQuotedString("_a%".into()),
2062+
];
2063+
compare(expected, tokens);
2064+
}
2065+
19972066
#[test]
19982067
fn tokenize_quoted_identifier() {
19992068
let sql = r#" "a "" b" "a """ "c """"" "#;

tests/sqlparser_postgres.rs

+22
Original file line numberDiff line numberDiff line change
@@ -1804,6 +1804,28 @@ fn parse_pg_regex_match_ops() {
18041804
}
18051805
}
18061806

1807+
#[test]
1808+
fn parse_pg_like_match_ops() {
1809+
let pg_like_match_ops = &[
1810+
("~~", BinaryOperator::PGLikeMatch),
1811+
("~~*", BinaryOperator::PGILikeMatch),
1812+
("!~~", BinaryOperator::PGNotLikeMatch),
1813+
("!~~*", BinaryOperator::PGNotILikeMatch),
1814+
];
1815+
1816+
for (str_op, op) in pg_like_match_ops {
1817+
let select = pg().verified_only_select(&format!("SELECT 'abc' {} 'a_c%'", &str_op));
1818+
assert_eq!(
1819+
SelectItem::UnnamedExpr(Expr::BinaryOp {
1820+
left: Box::new(Expr::Value(Value::SingleQuotedString("abc".into()))),
1821+
op: op.clone(),
1822+
right: Box::new(Expr::Value(Value::SingleQuotedString("a_c%".into()))),
1823+
}),
1824+
select.projection[0]
1825+
);
1826+
}
1827+
}
1828+
18071829
#[test]
18081830
fn parse_array_index_expr() {
18091831
let num: Vec<Expr> = (0..=10)

0 commit comments

Comments
 (0)