diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 1cab9c247..8539c461b 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -131,6 +131,14 @@ pub enum BinaryOperator { PGRegexNotMatch, /// String does not match regular expression (case insensitively), e.g. `a !~* b` (PostgreSQL-specific) PGRegexNotIMatch, + /// String matches pattern (case sensitively), e.g. `a ~~ b` (PostgreSQL-specific) + PGLikeMatch, + /// String matches pattern (case insensitively), e.g. `a ~~* b` (PostgreSQL-specific) + PGILikeMatch, + /// String does not match pattern (case sensitively), e.g. `a !~~ b` (PostgreSQL-specific) + PGNotLikeMatch, + /// String does not match pattern (case insensitively), e.g. `a !~~* b` (PostgreSQL-specific) + PGNotILikeMatch, /// String "starts with", eg: `a ^@ b` (PostgreSQL-specific) PGStartsWith, /// PostgreSQL-specific custom operator. @@ -174,6 +182,10 @@ impl fmt::Display for BinaryOperator { BinaryOperator::PGRegexIMatch => f.write_str("~*"), BinaryOperator::PGRegexNotMatch => f.write_str("!~"), BinaryOperator::PGRegexNotIMatch => f.write_str("!~*"), + BinaryOperator::PGLikeMatch => f.write_str("~~"), + BinaryOperator::PGILikeMatch => f.write_str("~~*"), + BinaryOperator::PGNotLikeMatch => f.write_str("!~~"), + BinaryOperator::PGNotILikeMatch => f.write_str("!~~*"), BinaryOperator::PGStartsWith => f.write_str("^@"), BinaryOperator::PGCustomBinaryOperator(idents) => { write!(f, "OPERATOR({})", display_separated(idents, ".")) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 99895b925..74957ebfa 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2203,6 +2203,10 @@ impl<'a> Parser<'a> { Token::TildeAsterisk => Some(BinaryOperator::PGRegexIMatch), Token::ExclamationMarkTilde => Some(BinaryOperator::PGRegexNotMatch), Token::ExclamationMarkTildeAsterisk => Some(BinaryOperator::PGRegexNotIMatch), + Token::DoubleTilde => Some(BinaryOperator::PGLikeMatch), + Token::DoubleTildeAsterisk => Some(BinaryOperator::PGILikeMatch), + Token::ExclamationMarkDoubleTilde => Some(BinaryOperator::PGNotLikeMatch), + Token::ExclamationMarkDoubleTildeAsterisk => Some(BinaryOperator::PGNotILikeMatch), Token::Word(w) => match w.keyword { Keyword::AND => Some(BinaryOperator::And), Keyword::OR => Some(BinaryOperator::Or), @@ -2618,6 +2622,10 @@ impl<'a> Parser<'a> { | Token::TildeAsterisk | Token::ExclamationMarkTilde | Token::ExclamationMarkTildeAsterisk + | Token::DoubleTilde + | Token::DoubleTildeAsterisk + | Token::ExclamationMarkDoubleTilde + | Token::ExclamationMarkDoubleTildeAsterisk | Token::Spaceship => Ok(20), Token::Pipe => Ok(21), Token::Caret | Token::Sharp | Token::ShiftRight | Token::ShiftLeft => Ok(22), diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 2156d0682..3416c0b1e 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -149,6 +149,14 @@ pub enum Token { ExclamationMarkTilde, /// `!~*` , a case insensitive not match regular expression operator in PostgreSQL ExclamationMarkTildeAsterisk, + /// `~~`, a case sensitive match pattern operator in PostgreSQL + DoubleTilde, + /// `~~*`, a case insensitive match pattern operator in PostgreSQL + DoubleTildeAsterisk, + /// `!~~`, a case sensitive not match pattern operator in PostgreSQL + ExclamationMarkDoubleTilde, + /// `!~~*`, a case insensitive not match pattern operator in PostgreSQL + ExclamationMarkDoubleTildeAsterisk, /// `<<`, a bitwise shift left operator in PostgreSQL ShiftLeft, /// `>>`, a bitwise shift right operator in PostgreSQL @@ -249,6 +257,10 @@ impl fmt::Display for Token { Token::TildeAsterisk => f.write_str("~*"), Token::ExclamationMarkTilde => f.write_str("!~"), Token::ExclamationMarkTildeAsterisk => f.write_str("!~*"), + Token::DoubleTilde => f.write_str("~~"), + Token::DoubleTildeAsterisk => f.write_str("~~*"), + Token::ExclamationMarkDoubleTilde => f.write_str("!~~"), + Token::ExclamationMarkDoubleTildeAsterisk => f.write_str("!~~*"), Token::AtSign => f.write_str("@"), Token::CaretAt => f.write_str("^@"), Token::ShiftLeft => f.write_str("<<"), @@ -894,6 +906,16 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('*') => self .consume_and_return(chars, Token::ExclamationMarkTildeAsterisk), + Some('~') => { + chars.next(); + match chars.peek() { + Some('*') => self.consume_and_return( + chars, + Token::ExclamationMarkDoubleTildeAsterisk, + ), + _ => Ok(Some(Token::ExclamationMarkDoubleTilde)), + } + } _ => Ok(Some(Token::ExclamationMarkTilde)), } } @@ -965,6 +987,15 @@ impl<'a> Tokenizer<'a> { chars.next(); // consume match chars.peek() { Some('*') => self.consume_and_return(chars, Token::TildeAsterisk), + Some('~') => { + chars.next(); + match chars.peek() { + Some('*') => { + self.consume_and_return(chars, Token::DoubleTildeAsterisk) + } + _ => Ok(Some(Token::DoubleTilde)), + } + } _ => Ok(Some(Token::Tilde)), } } @@ -1985,6 +2016,44 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_pg_like_match() { + let sql = "SELECT col ~~ '_a%', col ~~* '_a%', col !~~ '_a%', col !~~* '_a%'"; + let dialect = GenericDialect {}; + let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap(); + let expected = vec![ + Token::make_keyword("SELECT"), + Token::Whitespace(Whitespace::Space), + Token::make_word("col", None), + Token::Whitespace(Whitespace::Space), + Token::DoubleTilde, + Token::Whitespace(Whitespace::Space), + Token::SingleQuotedString("_a%".into()), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::make_word("col", None), + Token::Whitespace(Whitespace::Space), + Token::DoubleTildeAsterisk, + Token::Whitespace(Whitespace::Space), + Token::SingleQuotedString("_a%".into()), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::make_word("col", None), + Token::Whitespace(Whitespace::Space), + Token::ExclamationMarkDoubleTilde, + Token::Whitespace(Whitespace::Space), + Token::SingleQuotedString("_a%".into()), + Token::Comma, + Token::Whitespace(Whitespace::Space), + Token::make_word("col", None), + Token::Whitespace(Whitespace::Space), + Token::ExclamationMarkDoubleTildeAsterisk, + Token::Whitespace(Whitespace::Space), + Token::SingleQuotedString("_a%".into()), + ]; + compare(expected, tokens); + } + #[test] fn tokenize_quoted_identifier() { let sql = r#" "a "" b" "a """ "c """"" "#; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 8d90ca91f..a70882d66 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1804,6 +1804,28 @@ fn parse_pg_regex_match_ops() { } } +#[test] +fn parse_pg_like_match_ops() { + let pg_like_match_ops = &[ + ("~~", BinaryOperator::PGLikeMatch), + ("~~*", BinaryOperator::PGILikeMatch), + ("!~~", BinaryOperator::PGNotLikeMatch), + ("!~~*", BinaryOperator::PGNotILikeMatch), + ]; + + for (str_op, op) in pg_like_match_ops { + let select = pg().verified_only_select(&format!("SELECT 'abc' {} 'a_c%'", &str_op)); + assert_eq!( + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Value(Value::SingleQuotedString("abc".into()))), + op: op.clone(), + right: Box::new(Expr::Value(Value::SingleQuotedString("a_c%".into()))), + }), + select.projection[0] + ); + } +} + #[test] fn parse_array_index_expr() { let num: Vec = (0..=10)