From 1df35fbff2d1adfe3679b3a525f143b920742413 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 3 Oct 2026 08:35:31 +0200 Subject: [PATCH 1/8] Parse SQLite's `IS` and `IS NOT` operators --- src/ast/operator.rs | 6 +++ src/dialect/mod.rs | 5 +++ src/dialect/sqlite.rs | 4 ++ src/parser/mod.rs | 49 ++++++++++++++++++---- tests/sqlparser_common.rs | 3 +- tests/sqlparser_sqlite.rs | 87 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 145 insertions(+), 9 deletions(-) diff --git a/src/ast/operator.rs b/src/ast/operator.rs index 669470cdaa..f2c8454f34 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 20fe597d54..39c300e6be 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1846,6 +1846,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 6a3d3edfa9..ff1a0c7706 100644 --- a/src/dialect/sqlite.rs +++ b/src/dialect/sqlite.rs @@ -163,6 +163,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 f86406fb26..44808c671b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4212,6 +4212,31 @@ 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 + }; + // SQLite parses `IS` at the same left-associative level as the + // comparison operators, see + Ok(Expr::BinaryOp { + left: Box::new(expr), + op, + right: Box::new( + self.parse_subexpr(self.dialect.prec_value(Precedence::Eq))?, + ), + }) + } } else if let Ok(is_normalized) = self.parse_unicode_is_normalized(expr) { Ok(is_normalized) } else { @@ -13309,8 +13334,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, @@ -13325,13 +13361,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 379e37a660..57e3f622f7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11820,7 +11820,8 @@ fn parse_is_boolean() { ("SELECT s, s IS TRIM(' NFKC ') FROM foo", "TRIM"), ] { assert_eq!( - parse_sql_statements(sql), + all_dialects_except(|dialect| dialect.supports_is_operator()) + .parse_sql_statements(sql), Err(ParserError::ParserError(format!( "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS, found: {found}" ))), diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 10059c89e0..cb808f9d61 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1087,6 +1087,93 @@ fn test_non_bmp_identifiers() { ); } +#[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"))), + }), + } + ); + + assert_eq!( + sqlite().verified_expr("a IS b = c"), + 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::Eq, + right: Box::new(Expr::Identifier(Ident::new("c"))), + } + ); + + match sqlite().verified_expr("a IS b IN (1)") { + Expr::InList { + expr, + list, + negated, + } => { + assert!(!negated); + assert_eq!( + *expr, + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Is, + right: Box::new(Expr::Identifier(Ident::new("b"))), + } + ); + assert_eq!(list.len(), 1); + assert!(matches!( + list[0], + Expr::Value(ValueWithSpan { + value: Value::Number(_, _), + .. + }) + )); + } + other => panic!("expected InList on top, got {other:?}"), + } +} + #[test] fn parse_create_table_string_column_names() { sqlite().verified_stmt("CREATE TABLE t ('a')"); From 5c88c9ba3962cd030248bdb3587f9619c9d41447 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 16 Oct 2026 23:51:50 +0200 Subject: [PATCH 2/8] Track the new InList.global field in the SQLite IS tests --- tests/sqlparser_sqlite.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index cb808f9d61..f4ae11e820 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1151,8 +1151,10 @@ fn sqlite_is_operators() { expr, list, negated, + global, } => { assert!(!negated); + assert!(!global); assert_eq!( *expr, Expr::BinaryOp { From 6ffcc973ab72ed7643a042bab99e8908fca67435 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 12:51:23 +0200 Subject: [PATCH 3/8] Cover missing `IS` operands and simplify normalization backtracking --- src/parser/mod.rs | 4 ++-- tests/sqlparser_sqlite.rs | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 44808c671b..f9107287f6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4213,8 +4213,8 @@ impl<'a> Parser<'a> { } 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())? + if let Ok((form, negated)) = + self.try_parse(|parser| parser.parse_unicode_is_normalized_suffix()) { Ok(Expr::IsNormalized { expr: Box::new(expr), diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index f4ae11e820..c173feda45 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1174,6 +1174,16 @@ fn sqlite_is_operators() { } other => panic!("expected InList on top, got {other:?}"), } + + // A dangling right-hand side is rejected + for sql in ["SELECT a IS", "SELECT a IS NOT"] { + assert_eq!( + sqlite().parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: an expression, found: EOF".to_string() + )) + ); + } } #[test] From c9fda02882c9f4d4aac85b07083266f1f1fdf5bd Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Tue, 20 Oct 2026 17:55:57 +0200 Subject: [PATCH 4/8] Clarify SQLite `IS` precedence --- src/parser/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f9107287f6..b78bf5bb48 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4227,8 +4227,7 @@ impl<'a> Parser<'a> { } else { BinaryOperator::Is }; - // SQLite parses `IS` at the same left-associative level as the - // comparison operators, see + // SQLite gives `IS` the same precedence as `=` (https://www.sqlite.org/lang_expr.html). Ok(Expr::BinaryOp { left: Box::new(expr), op, From 1495bbdd2666fc2647b3d8dd0a8d281f9df31ade Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Wed, 21 Oct 2026 09:34:12 +0200 Subject: [PATCH 5/8] Adapt SQLite `IS` assertion to the unified `IN` AST --- tests/sqlparser_sqlite.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index c173feda45..1404522547 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -1147,9 +1147,9 @@ fn sqlite_is_operators() { ); match sqlite().verified_expr("a IS b IN (1)") { - Expr::InList { + Expr::In { expr, - list, + target: InTarget::List(list), negated, global, } => { From 52e1fe0b633ed52c5c412b9b7b8abfaf0c1c4ec3 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 00:23:37 +0200 Subject: [PATCH 6/8] Place regression tests beside related syntax cases --- tests/sqlparser_sqlite.rs | 198 +++++++++++++++++++------------------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 1404522547..568692a6bd 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -688,6 +688,105 @@ fn test_glob_operator() { ); } +#[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"))), + }), + } + ); + + assert_eq!( + sqlite().verified_expr("a IS b = c"), + 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::Eq, + right: Box::new(Expr::Identifier(Ident::new("c"))), + } + ); + + match sqlite().verified_expr("a IS b IN (1)") { + Expr::In { + expr, + target: InTarget::List(list), + negated, + global, + } => { + assert!(!negated); + assert!(!global); + assert_eq!( + *expr, + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op: BinaryOperator::Is, + right: Box::new(Expr::Identifier(Ident::new("b"))), + } + ); + assert_eq!(list.len(), 1); + assert!(matches!( + list[0], + Expr::Value(ValueWithSpan { + value: Value::Number(_, _), + .. + }) + )); + } + other => panic!("expected InList on top, got {other:?}"), + } + + // A dangling right-hand side is rejected + for sql in ["SELECT a IS", "SELECT a IS NOT"] { + assert_eq!( + sqlite().parse_sql_statements(sql), + Err(ParserError::ParserError( + "Expected: an expression, found: EOF".to_string() + )) + ); + } +} + #[test] fn test_update_delete_limit() { match sqlite().verified_stmt("UPDATE foo SET bar = 1 LIMIT 99") { @@ -1087,105 +1186,6 @@ fn test_non_bmp_identifiers() { ); } -#[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"))), - }), - } - ); - - assert_eq!( - sqlite().verified_expr("a IS b = c"), - 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::Eq, - right: Box::new(Expr::Identifier(Ident::new("c"))), - } - ); - - match sqlite().verified_expr("a IS b IN (1)") { - Expr::In { - expr, - target: InTarget::List(list), - negated, - global, - } => { - assert!(!negated); - assert!(!global); - assert_eq!( - *expr, - Expr::BinaryOp { - left: Box::new(Expr::Identifier(Ident::new("a"))), - op: BinaryOperator::Is, - right: Box::new(Expr::Identifier(Ident::new("b"))), - } - ); - assert_eq!(list.len(), 1); - assert!(matches!( - list[0], - Expr::Value(ValueWithSpan { - value: Value::Number(_, _), - .. - }) - )); - } - other => panic!("expected InList on top, got {other:?}"), - } - - // A dangling right-hand side is rejected - for sql in ["SELECT a IS", "SELECT a IS NOT"] { - assert_eq!( - sqlite().parse_sql_statements(sql), - Err(ParserError::ParserError( - "Expected: an expression, found: EOF".to_string() - )) - ); - } -} - #[test] fn parse_create_table_string_column_names() { sqlite().verified_stmt("CREATE TABLE t ('a')"); From 0b29287a8f060ba41bc85523b3b3949e7d7d0b4e Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sat, 24 Oct 2026 13:43:11 +0200 Subject: [PATCH 7/8] Add fuzz seeds for sqlite is operator --- fuzz/fuzz_seeds/sqlite_2470_is_operators.sql | 1 + 1 file changed, 1 insertion(+) create mode 100644 fuzz/fuzz_seeds/sqlite_2470_is_operators.sql diff --git a/fuzz/fuzz_seeds/sqlite_2470_is_operators.sql b/fuzz/fuzz_seeds/sqlite_2470_is_operators.sql new file mode 100644 index 0000000000..290c48ce6d --- /dev/null +++ b/fuzz/fuzz_seeds/sqlite_2470_is_operators.sql @@ -0,0 +1 @@ +SELECT a IS b, a IS NOT b, a IS b + c, a IS b AND c IS NOT d, a IS b = c, a IS b IN (1) FROM t From 2eb35f22e89c82f66b91dffbf4261105846291d4 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 30 Oct 2026 11:10:39 +0100 Subject: [PATCH 8/8] Read words after SQLite `IS` as the right operand --- .../sqlite_2470_is_json_function.sql | 1 + src/dialect/mod.rs | 3 + src/parser/mod.rs | 70 +++++++------------ tests/sqlparser_common.rs | 60 ++++++++-------- tests/sqlparser_sqlite.rs | 33 +++++++++ 5 files changed, 94 insertions(+), 73 deletions(-) create mode 100644 fuzz/fuzz_seeds/sqlite_2470_is_json_function.sql diff --git a/fuzz/fuzz_seeds/sqlite_2470_is_json_function.sql b/fuzz/fuzz_seeds/sqlite_2470_is_json_function.sql new file mode 100644 index 0000000000..fae2713baf --- /dev/null +++ b/fuzz/fuzz_seeds/sqlite_2470_is_json_function.sql @@ -0,0 +1 @@ +SELECT a IS json('{}') FROM t diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 39c300e6be..a13880b920 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1847,6 +1847,9 @@ pub trait Dialect: Debug + Any { } /// Returns true if the dialect supports binary `IS` and `IS NOT` operators. + /// + /// Their right operand is any expression, so `IS UNKNOWN`, `IS JSON` and + /// `IS NORMALIZED` are not parsed as predicates. fn supports_is_operator(&self) -> bool { false } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index b78bf5bb48..47e9fb2f12 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4194,10 +4194,6 @@ impl<'a> Parser<'a> { Ok(Expr::IsFalse(Box::new(expr))) } else if self.parse_keywords(&[Keyword::NOT, Keyword::FALSE]) { Ok(Expr::IsNotFalse(Box::new(expr))) - } else if self.parse_keywords(&[Keyword::UNKNOWN]) { - Ok(Expr::IsUnknown(Box::new(expr))) - } else if self.parse_keywords(&[Keyword::NOT, Keyword::UNKNOWN]) { - Ok(Expr::IsNotUnknown(Box::new(expr))) } else if self.parse_keywords(&[Keyword::DISTINCT, Keyword::FROM]) { // The right operand binds no more loosely than `IS` // itself, so that e.g. `a IS DISTINCT FROM b AND c` @@ -4208,34 +4204,28 @@ impl<'a> Parser<'a> { { let expr2 = self.parse_subexpr(precedence)?; Ok(Expr::IsNotDistinctFrom(Box::new(expr), Box::new(expr2))) + } else if self.dialect.supports_is_operator() { + let op = if self.parse_keyword(Keyword::NOT) { + BinaryOperator::IsNot + } else { + BinaryOperator::Is + }; + // SQLite gives `IS` the same precedence as `=` (https://www.sqlite.org/lang_expr.html). + Ok(Expr::BinaryOp { + left: Box::new(expr), + op, + right: Box::new( + self.parse_subexpr(self.dialect.prec_value(Precedence::Eq))?, + ), + }) + } else if self.parse_keywords(&[Keyword::UNKNOWN]) { + Ok(Expr::IsUnknown(Box::new(expr))) + } else if self.parse_keywords(&[Keyword::NOT, Keyword::UNKNOWN]) { + Ok(Expr::IsNotUnknown(Box::new(expr))) } else if self.parse_keyword(Keyword::JSON) { 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 Ok((form, negated)) = - self.try_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 - }; - // SQLite gives `IS` the same precedence as `=` (https://www.sqlite.org/lang_expr.html). - Ok(Expr::BinaryOp { - left: Box::new(expr), - op, - right: Box::new( - self.parse_subexpr(self.dialect.prec_value(Precedence::Eq))?, - ), - }) - } } else if let Ok(is_normalized) = self.parse_unicode_is_normalized(expr) { Ok(is_normalized) } else { @@ -13333,19 +13323,8 @@ impl<'a> Parser<'a> { /// Parse a literal unicode normalization clause pub fn parse_unicode_is_normalized(&mut self, expr: Expr) -> Result { - 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| { + let neg = self.parse_keyword(Keyword::NOT); + let normalized_form = self.maybe_parse(|parser| { match parser.parse_one_of_keywords(&[ Keyword::NFC, Keyword::NFD, @@ -13360,10 +13339,13 @@ impl<'a> Parser<'a> { } })?; if self.parse_keyword(Keyword::NORMALIZED) { - Ok((form, negated)) - } else { - self.expected_ref("unicode normalization form", self.peek_token_ref()) + return Ok(Expr::IsNormalized { + expr: Box::new(expr), + form: normalized_form, + negated: neg, + }); } + 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 57e3f622f7..c2d29b9e12 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -11746,6 +11746,8 @@ fn parse_is_boolean() { verified_expr(sql) ); + let dialects = all_dialects_except(|dialect| dialect.supports_is_operator()); + let sql = "a IS NORMALIZED"; assert_eq!( IsNormalized { @@ -11753,7 +11755,7 @@ fn parse_is_boolean() { form: None, negated: false, }, - verified_expr(sql) + dialects.verified_expr(sql) ); let sql = "a IS NOT NORMALIZED"; @@ -11763,7 +11765,7 @@ fn parse_is_boolean() { form: None, negated: true, }, - verified_expr(sql) + dialects.verified_expr(sql) ); let sql = "a IS NFKC NORMALIZED"; @@ -11773,7 +11775,7 @@ fn parse_is_boolean() { form: Some(NormalizationForm::NFKC), negated: false, }, - verified_expr(sql) + dialects.verified_expr(sql) ); let sql = "a IS NOT NFKD NORMALIZED"; @@ -11783,19 +11785,19 @@ fn parse_is_boolean() { form: Some(NormalizationForm::NFKD), negated: true, }, - verified_expr(sql) + dialects.verified_expr(sql) ); let sql = "a IS UNKNOWN"; assert_eq!( IsUnknown(Box::new(Identifier(Ident::new("a")))), - verified_expr(sql) + dialects.verified_expr(sql) ); let sql = "a IS NOT UNKNOWN"; assert_eq!( IsNotUnknown(Box::new(Identifier(Ident::new("a")))), - verified_expr(sql) + dialects.verified_expr(sql) ); verified_stmt("SELECT f FROM foo WHERE field IS TRUE"); @@ -11804,14 +11806,14 @@ fn parse_is_boolean() { verified_stmt("SELECT f FROM foo WHERE field IS FALSE"); verified_stmt("SELECT f FROM foo WHERE field IS NOT FALSE"); - verified_stmt("SELECT f FROM foo WHERE field IS NORMALIZED"); - verified_stmt("SELECT f FROM foo WHERE field IS NFC NORMALIZED"); - verified_stmt("SELECT f FROM foo WHERE field IS NFD NORMALIZED"); - verified_stmt("SELECT f FROM foo WHERE field IS NOT NORMALIZED"); - verified_stmt("SELECT f FROM foo WHERE field IS NOT NFKC NORMALIZED"); + dialects.verified_stmt("SELECT f FROM foo WHERE field IS NORMALIZED"); + dialects.verified_stmt("SELECT f FROM foo WHERE field IS NFC NORMALIZED"); + dialects.verified_stmt("SELECT f FROM foo WHERE field IS NFD NORMALIZED"); + dialects.verified_stmt("SELECT f FROM foo WHERE field IS NOT NORMALIZED"); + dialects.verified_stmt("SELECT f FROM foo WHERE field IS NOT NFKC NORMALIZED"); - verified_stmt("SELECT f FROM foo WHERE field IS UNKNOWN"); - verified_stmt("SELECT f FROM foo WHERE field IS NOT UNKNOWN"); + dialects.verified_stmt("SELECT f FROM foo WHERE field IS UNKNOWN"); + dialects.verified_stmt("SELECT f FROM foo WHERE field IS NOT UNKNOWN"); for (sql, found) in [ ("SELECT f from foo where field is 0", "0"), @@ -11820,8 +11822,7 @@ fn parse_is_boolean() { ("SELECT s, s IS TRIM(' NFKC ') FROM foo", "TRIM"), ] { assert_eq!( - all_dialects_except(|dialect| dialect.supports_is_operator()) - .parse_sql_statements(sql), + dialects.parse_sql_statements(sql), Err(ParserError::ParserError(format!( "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS, found: {found}" ))), @@ -11833,6 +11834,7 @@ fn parse_is_boolean() { #[test] fn parse_is_json_predicate() { use self::Expr::*; + let dialects = all_dialects_except(|dialect| dialect.supports_is_operator()); // Assert the full AST once for a case that exercises every field. let sql = "a IS NOT JSON OBJECT WITHOUT UNIQUE KEYS"; @@ -11843,24 +11845,24 @@ fn parse_is_json_predicate() { unique_keys: Some(JsonKeyUniqueness::WithoutUniqueKeys), negated: true, }, - verified_expr(sql) + dialects.verified_expr(sql) ); // The remaining forms only need to round-trip. - verified_expr("a IS JSON"); - verified_expr("a IS NOT JSON"); - verified_expr("a IS JSON VALUE"); - verified_expr("a IS JSON SCALAR"); - verified_expr("a IS JSON ARRAY"); - verified_expr("a IS JSON OBJECT"); - verified_expr("a IS JSON WITH UNIQUE KEYS"); - verified_expr("a IS JSON WITHOUT UNIQUE KEYS"); - - all_dialects().expr_parses_to("a IS JSON WITH UNIQUE", "a IS JSON WITH UNIQUE KEYS"); - all_dialects().expr_parses_to("a IS JSON WITHOUT UNIQUE", "a IS JSON WITHOUT UNIQUE KEYS"); + dialects.verified_expr("a IS JSON"); + dialects.verified_expr("a IS NOT JSON"); + dialects.verified_expr("a IS JSON VALUE"); + dialects.verified_expr("a IS JSON SCALAR"); + dialects.verified_expr("a IS JSON ARRAY"); + dialects.verified_expr("a IS JSON OBJECT"); + dialects.verified_expr("a IS JSON WITH UNIQUE KEYS"); + dialects.verified_expr("a IS JSON WITHOUT UNIQUE KEYS"); + + dialects.expr_parses_to("a IS JSON WITH UNIQUE", "a IS JSON WITH UNIQUE KEYS"); + dialects.expr_parses_to("a IS JSON WITHOUT UNIQUE", "a IS JSON WITHOUT UNIQUE KEYS"); assert_matches!( - verified_expr("NOT a IS JSON"), + dialects.verified_expr("NOT a IS JSON"), Expr::UnaryOp { op: UnaryOperator::Not, expr @@ -11870,7 +11872,7 @@ fn parse_is_json_predicate() { #[test] fn parse_is_json_predicate_invalid() { - let dialects = all_dialects(); + let dialects = all_dialects_except(|dialect| dialect.supports_is_operator()); for (sql, err) in [ ( diff --git a/tests/sqlparser_sqlite.rs b/tests/sqlparser_sqlite.rs index 568692a6bd..bb2f91cb29 100644 --- a/tests/sqlparser_sqlite.rs +++ b/tests/sqlparser_sqlite.rs @@ -787,6 +787,39 @@ fn sqlite_is_operators() { } } +#[test] +fn sqlite_is_operator_keyword_operands() { + // SQLite has no `IS UNKNOWN`, `IS JSON` or `IS NORMALIZED` predicate. + for right in ["json('{}')", "json", "unknown", "normalized", "nfc"] { + for (op, not) in [(BinaryOperator::Is, ""), (BinaryOperator::IsNot, "NOT ")] { + let sql = format!("a IS {not}{right}"); + assert_eq!( + sqlite().verified_expr(&sql), + Expr::BinaryOp { + left: Box::new(Expr::Identifier(Ident::new("a"))), + op, + right: Box::new(sqlite().verified_expr(right)), + }, + "{sql}" + ); + } + } + sqlite().verified_stmt("SELECT a IS json('{}'), a IS NOT json('{}') FROM t"); + + for (sql, found) in [ + ("SELECT a FROM t WHERE a IS NFC NORMALIZED", "NORMALIZED"), + ("SELECT a FROM t WHERE a IS NOT JSON OBJECT", "OBJECT"), + ] { + assert_eq!( + sqlite().parse_sql_statements(sql), + Err(ParserError::ParserError(format!( + "Expected: end of statement, found: {found}" + ))), + "{sql}" + ); + } +} + #[test] fn test_update_delete_limit() { match sqlite().verified_stmt("UPDATE foo SET bar = 1 LIMIT 99") {