From 777cee2bafd91ef624de299487ae698398f02565 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Sun, 26 Jul 2026 10:10:02 +0200 Subject: [PATCH] Support `IS [NOT] JSON` predicate in all dialects --- src/ast/mod.rs | 76 ++++++++++++++++++++++++++++ src/ast/spans.rs | 6 +++ src/keywords.rs | 1 + src/parser/mod.rs | 59 +++++++++++++++++---- tests/sqlparser_common.rs | 104 ++++++++++++++++++++++++++------------ 5 files changed, 204 insertions(+), 42 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3754e5984..dab790e6e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -948,6 +948,17 @@ pub enum Expr { IsDistinctFrom(Box, Box), /// `IS NOT DISTINCT FROM` operator IsNotDistinctFrom(Box, Box), + /// ` IS [NOT] JSON [VALUE|SCALAR|ARRAY|OBJECT] [WITH|WITHOUT UNIQUE [KEYS]]` + IsJson { + /// Expression being tested. + expr: Box, + /// Optional JSON shape constraint. + kind: Option, + /// Optional duplicate-key handling constraint for JSON objects. + unique_keys: Option, + /// `true` when `NOT` is present. + negated: bool, + }, /// ` IS [ NOT ] [ form ] NORMALIZED` IsNormalized { /// Expression being tested. @@ -1750,6 +1761,25 @@ impl fmt::Display for Expr { Expr::IsNotNull(ast) => write!(f, "{ast} IS NOT NULL"), Expr::IsUnknown(ast) => write!(f, "{ast} IS UNKNOWN"), Expr::IsNotUnknown(ast) => write!(f, "{ast} IS NOT UNKNOWN"), + Expr::IsJson { + expr, + kind, + unique_keys, + negated, + } => { + write!(f, "{expr} IS ")?; + if *negated { + write!(f, "NOT ")?; + } + write!(f, "JSON")?; + if let Some(kind) = kind { + write!(f, " {kind}")?; + } + if let Some(unique_keys) = unique_keys { + write!(f, " {unique_keys}")?; + } + Ok(()) + } Expr::InList { expr, list, @@ -8437,6 +8467,52 @@ pub enum AnalyzeFormat { TREE, } +/// Optional type constraint for `IS JSON`. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum JsonPredicateType { + /// `VALUE` form. + Value, + /// `SCALAR` form. + Scalar, + /// `ARRAY` form. + Array, + /// `OBJECT` form. + Object, +} + +impl fmt::Display for JsonPredicateType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JsonPredicateType::Value => write!(f, "VALUE"), + JsonPredicateType::Scalar => write!(f, "SCALAR"), + JsonPredicateType::Array => write!(f, "ARRAY"), + JsonPredicateType::Object => write!(f, "OBJECT"), + } + } +} + +/// Optional duplicate-key handling for `IS JSON`. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum JsonKeyUniqueness { + /// `WITH UNIQUE KEYS` form. + WithUniqueKeys, + /// `WITHOUT UNIQUE KEYS` form. + WithoutUniqueKeys, +} + +impl fmt::Display for JsonKeyUniqueness { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + JsonKeyUniqueness::WithUniqueKeys => write!(f, "WITH UNIQUE KEYS"), + JsonKeyUniqueness::WithoutUniqueKeys => write!(f, "WITHOUT UNIQUE KEYS"), + } + } +} + impl fmt::Display for AnalyzeFormat { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(match self { diff --git a/src/ast/spans.rs b/src/ast/spans.rs index e0001ca40..e1fa0c752 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1496,6 +1496,12 @@ impl Spanned for Expr { Expr::IsNotNull(expr) => expr.span(), Expr::IsUnknown(expr) => expr.span(), Expr::IsNotUnknown(expr) => expr.span(), + Expr::IsJson { + expr, + kind: _, + unique_keys: _, + negated: _, + } => expr.span(), Expr::IsDistinctFrom(lhs, rhs) => lhs.span().union(&rhs.span()), Expr::IsNotDistinctFrom(lhs, rhs) => lhs.span().union(&rhs.span()), Expr::InList { diff --git a/src/keywords.rs b/src/keywords.rs index c2d0a47ec..0c50703c3 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -920,6 +920,7 @@ define_keywords!( SAFE_CAST, SAMPLE, SAVEPOINT, + SCALAR, SCHEMA, SCHEMAS, SCOPE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 294a0bed9..90d80efd2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -543,7 +543,6 @@ impl<'a> Parser<'a> { match &self.peek_token_ref().token { Token::EOF => break, - // end of statement Token::Word(word) if expecting_statement_delimiter && word.keyword == Keyword::END => @@ -4075,11 +4074,15 @@ impl<'a> Parser<'a> { { let expr2 = self.parse_expr()?; Ok(Expr::IsNotDistinctFrom(Box::new(expr), Box::new(expr2))) + } 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 let Ok(is_normalized) = self.parse_unicode_is_normalized(expr) { Ok(is_normalized) } else { self.expected_ref( - "[NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS", + "[NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS", self.peek_token_ref(), ) } @@ -8657,6 +8660,7 @@ impl<'a> Parser<'a> { char: self.parse_identifier()?, }); } + Some(Keyword::NULL) => break, _ => { break; } @@ -12641,6 +12645,43 @@ impl<'a> Parser<'a> { } } + /// Parse the `IS [NOT] JSON` predicate after `JSON` (and optional `NOT`) was consumed. + fn parse_is_json_predicate(&mut self, expr: Expr, negated: bool) -> Result { + let kind = match self.parse_one_of_keywords(&[ + Keyword::VALUE, + Keyword::SCALAR, + Keyword::ARRAY, + Keyword::OBJECT, + ]) { + Some(Keyword::VALUE) => Some(JsonPredicateType::Value), + Some(Keyword::SCALAR) => Some(JsonPredicateType::Scalar), + Some(Keyword::ARRAY) => Some(JsonPredicateType::Array), + Some(Keyword::OBJECT) => Some(JsonPredicateType::Object), + _ => None, + }; + + let unique_keys = match self.parse_one_of_keywords(&[Keyword::WITH, Keyword::WITHOUT]) { + Some(Keyword::WITH) => { + self.expect_keyword_is(Keyword::UNIQUE)?; + let _ = self.parse_keyword(Keyword::KEYS); + Some(JsonKeyUniqueness::WithUniqueKeys) + } + Some(Keyword::WITHOUT) => { + self.expect_keyword_is(Keyword::UNIQUE)?; + let _ = self.parse_keyword(Keyword::KEYS); + Some(JsonKeyUniqueness::WithoutUniqueKeys) + } + _ => None, + }; + + Ok(Expr::IsJson { + expr: Box::new(expr), + kind, + unique_keys, + negated, + }) + } + /// Parse a literal unicode normalization clause pub fn parse_unicode_is_normalized(&mut self, expr: Expr) -> Result { let neg = self.parse_keyword(Keyword::NOT); @@ -21756,13 +21797,13 @@ mod tests { #[test] fn test_parser_error_loc() { let sql = "SELECT this is a syntax error"; - let ast = Parser::parse_sql(&GenericDialect, sql); - assert_eq!( - ast, - Err(ParserError::ParserError( - "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: a at Line: 1, Column: 16" - .to_string() - )) + let ParserError::ParserError(msg) = Parser::parse_sql(&GenericDialect, sql).unwrap_err() + else { + panic!("expected ParserError::ParserError"); + }; + assert!( + msg.ends_with("found: a at Line: 1, Column: 16"), + "unexpected error message: {msg}" ); } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 5ff5e5c47..7a60f32c7 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10996,47 +10996,85 @@ fn parse_is_boolean() { verified_stmt("SELECT f FROM foo WHERE field IS UNKNOWN"); verified_stmt("SELECT f FROM foo WHERE field IS NOT UNKNOWN"); - let sql = "SELECT f from foo where field is 0"; - let res = parse_sql_statements(sql); - assert_eq!( - ParserError::ParserError( - "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: 0" - .to_string() - ), - res.unwrap_err() - ); + for sql in [ + "SELECT f from foo where field is 0", + "SELECT s, s IS XYZ NORMALIZED FROM foo", + "SELECT s, s IS NFKC FROM foo", + "SELECT s, s IS TRIM(' NFKC ') FROM foo", + ] { + assert!( + parse_sql_statements(sql).is_err(), + "expected a parse failure for `{sql}`" + ); + } +} - let sql = "SELECT s, s IS XYZ NORMALIZED FROM foo"; - let res = parse_sql_statements(sql); - assert_eq!( - ParserError::ParserError( - "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: XYZ" - .to_string() - ), - res.unwrap_err() - ); +#[test] +fn parse_is_json_predicate() { + use self::Expr::*; - let sql = "SELECT s, s IS NFKC FROM foo"; - let res = parse_sql_statements(sql); + // Assert the full AST once for a case that exercises every field. + let sql = "a IS NOT JSON OBJECT WITHOUT UNIQUE KEYS"; assert_eq!( - ParserError::ParserError( - "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: FROM" - .to_string() - ), - res.unwrap_err() + IsJson { + expr: Box::new(Identifier(Ident::new("a"))), + kind: Some(JsonPredicateType::Object), + unique_keys: Some(JsonKeyUniqueness::WithoutUniqueKeys), + negated: true, + }, + verified_expr(sql) ); - let sql = "SELECT s, s IS TRIM(' NFKC ') FROM foo"; - let res = parse_sql_statements(sql); - assert_eq!( - ParserError::ParserError( - "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: TRIM" - .to_string() - ), - res.unwrap_err() + // 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"); + + assert_matches!( + verified_expr("NOT a IS JSON"), + Expr::UnaryOp { + op: UnaryOperator::Not, + expr + } if matches!(&*expr, Expr::IsJson { .. }) ); } +#[test] +fn parse_is_json_predicate_invalid() { + let dialects = all_dialects(); + + let invalid = [ + "SELECT * FROM t WHERE a IS JSON WITH FROM", + "SELECT * FROM t WHERE a IS JSON WITH KEYS", + "SELECT * FROM t WHERE a IS JSON WITHOUT FROM", + "SELECT * FROM t WHERE a IS JSON WITHOUT KEYS", + "SELECT * FROM t WHERE a IS NOT JSON WITH FROM", + "SELECT * FROM t WHERE a IS JSON VALUE ARRAY", + "SELECT * FROM t WHERE a IS JSON OBJECT VALUE", + "SELECT * FROM t WHERE a IS JSON WITH UNIQUE EXTRA", + "SELECT * FROM t WHERE a IS JSON WITH UNIQUE KEYS EXTRA", + "SELECT * FROM t WHERE a IS JSON WITHOUT UNIQUE EXTRA", + "SELECT * FROM t WHERE a IS JSON WITHOUT UNIQUE KEYS EXTRA", + "SELECT * FROM t WHERE a IS JSON WITH UNIQUE KEYS WITH UNIQUE KEYS", + "SELECT * FROM t WHERE a IS JSON WITHOUT UNIQUE KEYS WITHOUT UNIQUE KEYS", + ]; + + for sql in invalid { + assert!( + dialects.parse_sql_statements(sql).is_err(), + "expected a parse failure for `{sql}`" + ); + } +} + #[test] fn parse_discard() { let sql = "DISCARD ALL";