From 088408878a2630fb068644c6ed23301ecd1dded1 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Mon, 31 Aug 2026 14:10:27 +0200 Subject: [PATCH] Parse SQLite binary IS operators --- src/ast/operator.rs | 6 +++++ src/dialect/mod.rs | 5 +++++ src/dialect/sqlite.rs | 4 ++++ src/parser/mod.rs | 45 ++++++++++++++++++++++++++++++------- tests/sqlparser_common.rs | 4 +++- tests/sqlparser_sqlite.rs | 47 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 669470cda..f2c8454f3 100644 --- a/src/ast/operator.rs +++ b/src/ast/operator.rs @@ -332,6 +332,10 @@ pub enum BinaryOperator { /// ':=' Assignment Operator /// See Assignment, + /// `IS` operator + Is, + /// `IS NOT` operator + IsNot, } impl fmt::Display for BinaryOperator { @@ -409,6 +413,8 @@ impl fmt::Display for BinaryOperator { BinaryOperator::At => f.write_str("@"), BinaryOperator::TildeEq => f.write_str("~="), BinaryOperator::Assignment => f.write_str(":="), + BinaryOperator::Is => f.write_str("IS"), + BinaryOperator::IsNot => f.write_str("IS NOT"), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index ff83a4da6..cce1a2426 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1518,6 +1518,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports binary `IS` and `IS NOT` operators. + fn supports_is_operator(&self) -> bool { + false + } + /// Returns true if this dialect allows an optional `SIGNED` suffix after integer data types. /// /// Example: diff --git a/src/dialect/sqlite.rs b/src/dialect/sqlite.rs index d549c7507..75f75a2d8 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -122,6 +122,10 @@ impl Dialect for SQLiteDialect { true } + fn supports_is_operator(&self) -> bool { + true + } + fn supports_comma_separated_trim(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6af0fb776..2e2dec633 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4107,6 +4107,27 @@ impl<'a> Parser<'a> { self.parse_is_json_predicate(expr, false) } else if self.parse_keywords(&[Keyword::NOT, Keyword::JSON]) { self.parse_is_json_predicate(expr, true) + } else if self.dialect.supports_is_operator() { + if let Some((form, negated)) = + self.maybe_parse(|parser| parser.parse_unicode_is_normalized_suffix())? + { + Ok(Expr::IsNormalized { + expr: Box::new(expr), + form, + negated, + }) + } else { + let op = if self.parse_keyword(Keyword::NOT) { + BinaryOperator::IsNot + } else { + BinaryOperator::Is + }; + Ok(Expr::BinaryOp { + left: Box::new(expr), + op, + right: Box::new(self.parse_subexpr(precedence)?), + }) + } } else if let Ok(is_normalized) = self.parse_unicode_is_normalized(expr) { Ok(is_normalized) } else { @@ -12737,8 +12758,19 @@ impl<'a> Parser<'a> { /// Parse a literal unicode normalization clause pub fn parse_unicode_is_normalized(&mut self, expr: Expr) -> Result { - let neg = self.parse_keyword(Keyword::NOT); - let normalized_form = self.maybe_parse(|parser| { + let (form, negated) = self.parse_unicode_is_normalized_suffix()?; + Ok(Expr::IsNormalized { + expr: Box::new(expr), + form, + negated, + }) + } + + fn parse_unicode_is_normalized_suffix( + &mut self, + ) -> Result<(Option, bool), ParserError> { + let negated = self.parse_keyword(Keyword::NOT); + let form = self.maybe_parse(|parser| { match parser.parse_one_of_keywords(&[ Keyword::NFC, Keyword::NFD, @@ -12753,13 +12785,10 @@ impl<'a> Parser<'a> { } })?; if self.parse_keyword(Keyword::NORMALIZED) { - return Ok(Expr::IsNormalized { - expr: Box::new(expr), - form: normalized_form, - negated: neg, - }); + Ok((form, negated)) + } else { + self.expected_ref("unicode normalization form", self.peek_token_ref()) } - self.expected_ref("unicode normalization form", self.peek_token_ref()) } /// Parse parenthesized enum members, used with `ENUM(...)` type definitions. diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5792cf007..7091c9b57 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11258,7 +11258,9 @@ fn parse_is_boolean() { "SELECT s, s IS TRIM(' NFKC ') FROM foo", ] { assert!( - parse_sql_statements(sql).is_err(), + all_dialects_except(|dialect| dialect.supports_is_operator()) + .parse_sql_statements(sql) + .is_err(), "expected a parse failure for `{sql}`" ); } diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index a017562df..8eb9c52e4 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -957,6 +957,53 @@ fn parse_pattern_operators_bind_at_like_precedence() { } } +#[test] +fn sqlite_is_operators() { + for (sql, op) in [ + ("a IS b", BinaryOperator::Is), + ("a IS NOT b", BinaryOperator::IsNot), + ] { + assert_eq!( + sqlite().verified_expr(sql), + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op, + right: Box::new(Expr::Identifier(Ident::new("b"))), + } + ); + } + + assert_eq!( + sqlite().verified_expr("a IS b + c"), + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Is, + right: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("b"))), + op: BinaryOperator::Plus, + right: Box::new(Expr::Identifier(Ident::new("c"))), + }), + } + ); + + assert_eq!( + sqlite().verified_expr("a IS b AND c IS NOT d"), + Expr::BinaryOp { + left: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Is, + right: Box::new(Expr::Identifier(Ident::new("b"))), + }), + op: BinaryOperator::And, + right: Box::new(Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("c"))), + op: BinaryOperator::IsNot, + right: Box::new(Expr::Identifier(Ident::new("d"))), + }), + } + ); +} + fn sqlite() -> TestedDialects { TestedDialects::new(vec![Box::new(SQLiteDialect {})]) }