From 49522f7a6618f2b4cc56fe833d0289bce032462e Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 23 Sep 2026 12:37:48 +0200 Subject: [PATCH 1/2] Tokenize `~~` as `LIKE` only in PostgreSQL, Redshift and DuckDB --- src/dialect/duckdb.rs | 4 ++++ src/dialect/generic.rs | 4 ++++ src/dialect/mod.rs | 12 ++++++++++++ src/dialect/postgresql.rs | 4 ++++ src/dialect/redshift.rs | 4 ++++ src/tokenizer.rs | 24 ++++++++++++++++++++++-- tests/sqlparser_common.rs | 31 +++++++++++++++++++++++++++++++ 7 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/dialect/duckdb.rs b/src/dialect/duckdb.rs index 5e4d32ce40..4e27d042b5 100644 --- a/src/dialect/duckdb.rs +++ b/src/dialect/duckdb.rs @@ -65,6 +65,10 @@ impl Dialect for DuckDbDialect { true } + fn supports_tilde_like_match_operator(&self) -> bool { + true + } + fn supports_named_fn_args_with_eq_operator(&self) -> bool { true } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 326bb514ed..fb3f2c254f 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -185,6 +185,10 @@ impl Dialect for GenericDialect { true } + fn supports_tilde_like_match_operator(&self) -> bool { + true + } + fn supports_comment_on(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index b37fc3c86c..847d042418 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1649,6 +1649,18 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect tokenizes `~~`, `~~*`, `!~~` and `!~~*` as + /// `LIKE`, `ILIKE`, `NOT LIKE` and `NOT ILIKE` pattern-match operators. + /// + /// When false, `~~` is two `~` tokens, e.g. two bitwise-NOT prefixes. + /// + /// [PostgreSQL](https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-LIKE), + /// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_patternmatching_condition_like.html), + /// [DuckDB](https://duckdb.org/docs/current/sql/functions/pattern_matching) + fn supports_tilde_like_match_operator(&self) -> bool { + false + } + /// Returns true if the dialect supports `ORDER BY ALL`. /// `ALL` which means all columns of the SELECT clause. /// diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 15d8c6733c..54fd32fa26 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -412,6 +412,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_tilde_like_match_operator(&self) -> bool { + true + } + /// see fn supports_comment_on(&self) -> bool { true diff --git a/src/dialect/redshift.rs b/src/dialect/redshift.rs index f3dab66ffa..01371a4ecd 100644 --- a/src/dialect/redshift.rs +++ b/src/dialect/redshift.rs @@ -141,6 +141,10 @@ impl Dialect for RedshiftSqlDialect { true } + fn supports_tilde_like_match_operator(&self) -> bool { + true + } + fn supports_array_typedef_with_brackets(&self) -> bool { true } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 04a6cc79f1..9276f91ded 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1838,7 +1838,7 @@ impl<'a> Tokenizer<'a> { match chars.peek() { Some('*') => self .consume_and_return(chars, Token::ExclamationMarkTildeAsterisk), - Some('~') => { + Some('~') if self.dialect.supports_tilde_like_match_operator() => { chars.next(); match chars.peek() { Some('*') => self.consume_and_return( @@ -1982,7 +1982,7 @@ impl<'a> Tokenizer<'a> { Some('=') if self.dialect.supports_geometric_types() => { self.consume_for_binop(chars, "~=", Token::TildeEqual) } - Some('~') => { + Some('~') if self.dialect.supports_tilde_like_match_operator() => { chars.next(); match chars.peek() { Some('*') => { @@ -3942,6 +3942,26 @@ mod tests { compare(expected, tokens); } + #[test] + fn tokenize_tilde_without_like_match_operators() { + all_dialects_where(|d| !d.supports_tilde_like_match_operator()).tokenizes_to( + "~~ ~~* !~~ !~~*", + vec![ + Token::Tilde, + Token::Tilde, + Token::Whitespace(Whitespace::Space), + Token::Tilde, + Token::TildeAsterisk, + Token::Whitespace(Whitespace::Space), + Token::ExclamationMarkTilde, + Token::Tilde, + Token::Whitespace(Whitespace::Space), + Token::ExclamationMarkTilde, + Token::TildeAsterisk, + ], + ); + } + #[test] fn tokenize_quoted_identifier() { let sql = r#" "a "" b" "a """ "c """"" "#; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ec107ad980..9718d2163b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -21305,3 +21305,34 @@ fn parse_qualified_literal_prefix_requires_adjacent_period() { dialects.tokenizes_to(sql, vec![token]); } } + +#[test] +fn parse_tilde_like_match_operators() { + let like_match = all_dialects_where(|d| d.supports_tilde_like_match_operator()); + for (op, expected) in [ + ("~~", BinaryOperator::PGLikeMatch), + ("~~*", BinaryOperator::PGILikeMatch), + ("!~~", BinaryOperator::PGNotLikeMatch), + ("!~~*", BinaryOperator::PGNotILikeMatch), + ] { + let select = like_match.verified_only_select(&format!("SELECT a {op} b")); + assert_eq!( + select.projection[0], + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: expected, + right: Box::new(Expr::Identifier(Ident::new("b"))), + }) + ); + } + assert_eq!( + like_match.parse_sql_statements("SELECT ~~a"), + Err(ParserError::ParserError( + "Expected: an expression, found: ~~".to_string() + )) + ); + + let bitwise_not = all_dialects_where(|d| !d.supports_tilde_like_match_operator()); + bitwise_not.one_statement_parses_to("SELECT ~~a", "SELECT ~ ~ a"); + bitwise_not.one_statement_parses_to("SELECT ~~~a", "SELECT ~ ~ ~ a"); +} From 609f6adc3a87af0694aa72c5676bd2a3a7649f2b Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:00:41 +0200 Subject: [PATCH 2/2] Place regression tests beside related syntax cases --- tests/sqlparser_common.rs | 62 +++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9718d2163b..6c047d17fb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2425,6 +2425,37 @@ fn parse_ilike() { chk(true); } +#[test] +fn parse_tilde_like_match_operators() { + let like_match = all_dialects_where(|d| d.supports_tilde_like_match_operator()); + for (op, expected) in [ + ("~~", BinaryOperator::PGLikeMatch), + ("~~*", BinaryOperator::PGILikeMatch), + ("!~~", BinaryOperator::PGNotLikeMatch), + ("!~~*", BinaryOperator::PGNotILikeMatch), + ] { + let select = like_match.verified_only_select(&format!("SELECT a {op} b")); + assert_eq!( + select.projection[0], + SelectItem::UnnamedExpr(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: expected, + right: Box::new(Expr::Identifier(Ident::new("b"))), + }) + ); + } + assert_eq!( + like_match.parse_sql_statements("SELECT ~~a"), + Err(ParserError::ParserError( + "Expected: an expression, found: ~~".to_string() + )) + ); + + let bitwise_not = all_dialects_where(|d| !d.supports_tilde_like_match_operator()); + bitwise_not.one_statement_parses_to("SELECT ~~a", "SELECT ~ ~ a"); + bitwise_not.one_statement_parses_to("SELECT ~~~a", "SELECT ~ ~ ~ a"); +} + #[test] fn parse_like() { fn chk(negated: bool) { @@ -21305,34 +21336,3 @@ fn parse_qualified_literal_prefix_requires_adjacent_period() { dialects.tokenizes_to(sql, vec![token]); } } - -#[test] -fn parse_tilde_like_match_operators() { - let like_match = all_dialects_where(|d| d.supports_tilde_like_match_operator()); - for (op, expected) in [ - ("~~", BinaryOperator::PGLikeMatch), - ("~~*", BinaryOperator::PGILikeMatch), - ("!~~", BinaryOperator::PGNotLikeMatch), - ("!~~*", BinaryOperator::PGNotILikeMatch), - ] { - let select = like_match.verified_only_select(&format!("SELECT a {op} b")); - assert_eq!( - select.projection[0], - SelectItem::UnnamedExpr(Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("a"))), - op: expected, - right: Box::new(Expr::Identifier(Ident::new("b"))), - }) - ); - } - assert_eq!( - like_match.parse_sql_statements("SELECT ~~a"), - Err(ParserError::ParserError( - "Expected: an expression, found: ~~".to_string() - )) - ); - - let bitwise_not = all_dialects_where(|d| !d.supports_tilde_like_match_operator()); - bitwise_not.one_statement_parses_to("SELECT ~~a", "SELECT ~ ~ a"); - bitwise_not.one_statement_parses_to("SELECT ~~~a", "SELECT ~ ~ ~ a"); -}