From 3bb0d60f77e03d8621b4ca07e6a23f3b090e293a Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 17:57:14 +0100 Subject: [PATCH 1/2] feat(parser): add IS [NOT] JSON predicate support Introduce AST and parser support for IS JSON predicates, including optional VALUE/SCALAR/ARRAY/OBJECT and WITH/WITHOUT UNIQUE [KEYS] modifiers. Gate parsing by dialect capability, enable Generic/ANSI/PostgreSQL/Oracle, update IS diagnostic hints accordingly, and include parser-side regression coverage. Also apply parser-only control-flow cleanups to keep strict clippy (-D warnings) green. --- src/ast/mod.rs | 76 ++++++++++++++++++++++++++++++++++++ src/ast/spans.rs | 6 +++ src/dialect/ansi.rs | 4 ++ src/dialect/generic.rs | 4 ++ src/dialect/mod.rs | 5 +++ src/dialect/oracle.rs | 4 ++ src/dialect/postgresql.rs | 4 ++ src/keywords.rs | 1 + src/parser/mod.rs | 82 ++++++++++++++++++++++++++++++++++++--- 9 files changed, 180 insertions(+), 6 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 3cb5f8041..84b2a5f0d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -928,6 +928,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. @@ -1737,6 +1748,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, @@ -8336,6 +8366,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 f0e3e252f..40bb76d77 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1476,6 +1476,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/dialect/ansi.rs b/src/dialect/ansi.rs index 89c8a9ea2..811001685 100644 --- a/src/dialect/ansi.rs +++ b/src/dialect/ansi.rs @@ -39,4 +39,8 @@ impl Dialect for AnsiDialect { fn supports_nested_comments(&self) -> bool { true } + + fn supports_is_json_predicate(&self) -> bool { + true + } } diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs index 25f57e3d1..865f2daaa 100644 --- a/src/dialect/generic.rs +++ b/src/dialect/generic.rs @@ -297,6 +297,10 @@ impl Dialect for GenericDialect { true } + fn supports_is_json_predicate(&self) -> bool { + true + } + fn supports_comma_separated_trim(&self) -> bool { true } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 6ab6cb15e..364bcaee3 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1458,6 +1458,11 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if the dialect supports the `IS [NOT] JSON` predicate. + fn supports_is_json_predicate(&self) -> bool { + false + } + /// Returns true if this dialect allows an optional `SIGNED` suffix after integer data types. /// /// Example: diff --git a/src/dialect/oracle.rs b/src/dialect/oracle.rs index ccbef5b62..6979073d3 100644 --- a/src/dialect/oracle.rs +++ b/src/dialect/oracle.rs @@ -119,4 +119,8 @@ impl Dialect for OracleDialect { fn supports_insert_table_query(&self) -> bool { true } + + fn supports_is_json_predicate(&self) -> bool { + true + } } diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index fda676eb2..c83e1ae6f 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -311,6 +311,10 @@ impl Dialect for PostgreSqlDialect { true } + fn supports_is_json_predicate(&self) -> bool { + true + } + fn supports_comma_separated_trim(&self) -> bool { true } diff --git a/src/keywords.rs b/src/keywords.rs index 4fc8f72d1..4ad5d86ca 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -912,6 +912,7 @@ define_keywords!( SAFE_CAST, SAMPLE, SAVEPOINT, + SCALAR, SCHEMA, SCHEMAS, SCOPE, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 668c520e5..505f8a723 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -506,7 +506,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 => @@ -3986,13 +3985,23 @@ impl<'a> Parser<'a> { { let expr2 = self.parse_expr()?; Ok(Expr::IsNotDistinctFrom(Box::new(expr), Box::new(expr2))) + } else if self.dialect.supports_is_json_predicate() + && self.parse_keyword(Keyword::JSON) + { + self.parse_is_json_predicate(expr, false) + } else if self.dialect.supports_is_json_predicate() + && 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", - self.peek_token_ref(), - ) + let expected = if self.dialect.supports_is_json_predicate() { + "[NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS" + } else { + "[NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS" + }; + self.expected_ref(expected, self.peek_token_ref()) } } Keyword::AT => { @@ -8442,6 +8451,7 @@ impl<'a> Parser<'a> { char: self.parse_identifier()?, }); } + Some(Keyword::NULL) => break, _ => { break; } @@ -12237,6 +12247,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); @@ -21218,12 +21265,35 @@ mod tests { assert_eq!( ast, Err(ParserError::ParserError( - "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: a at Line: 1, Column: 16" + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS, found: a at Line: 1, Column: 16" .to_string() )) ); } + #[test] + fn test_is_predicate_error_hint_depends_on_dialect() { + let sql = "SELECT this is a syntax error"; + + let generic_err = Parser::parse_sql(&GenericDialect, sql).unwrap_err(); + let ParserError::ParserError(generic_msg) = generic_err else { + panic!("Expected ParserError::ParserError, got: {generic_err:?}"); + }; + assert!( + generic_msg.contains("[NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT]"), + "Expected Generic dialect to include JSON predicate hint, got: {generic_msg}" + ); + + let mysql_err = Parser::parse_sql(&MySqlDialect {}, sql).unwrap_err(); + let ParserError::ParserError(mysql_msg) = mysql_err else { + panic!("Expected ParserError::ParserError, got: {mysql_err:?}"); + }; + assert!( + !mysql_msg.contains("[NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT]"), + "Expected MySQL dialect to exclude JSON predicate hint, got: {mysql_msg}" + ); + } + #[test] fn test_nested_explain_error() { let sql = "EXPLAIN EXPLAIN SELECT 1"; From c0e0131edbb5729c1402eaa7e93d2cc58e9f0ab5 Mon Sep 17 00:00:00 2001 From: LucaCappelletti94 Date: Fri, 27 Feb 2026 17:57:20 +0100 Subject: [PATCH 2/2] test(parser): expand IS JSON coverage across dialect and error paths Add integration coverage for supported IS JSON forms and unsupported-dialect failures for both IS JSON and IS NOT JSON. Tighten malformed-case assertions with token-specific diagnostics, including junk tails and duplicate WITH/WITHOUT UNIQUE KEYS clauses. --- tests/sqlparser_common.rs | 258 +++++++++++++++++++++++++++++++++++++- 1 file changed, 254 insertions(+), 4 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 739238c82..797ecfabf 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -10838,8 +10838,19 @@ 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 supported_dialects = all_dialects_where(|d| d.supports_is_json_predicate()); + let unsupported_dialects = all_dialects_where(|d| !d.supports_is_json_predicate()); + let sql = "SELECT f from foo where field is 0"; - let res = parse_sql_statements(sql); + let res = supported_dialects.parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS, found: 0" + .to_string() + ), + res.unwrap_err() + ); + let res = unsupported_dialects.parse_sql_statements(sql); assert_eq!( ParserError::ParserError( "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: 0" @@ -10849,7 +10860,15 @@ fn parse_is_boolean() { ); let sql = "SELECT s, s IS XYZ NORMALIZED FROM foo"; - let res = parse_sql_statements(sql); + let res = supported_dialects.parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS, found: XYZ" + .to_string() + ), + res.unwrap_err() + ); + let res = unsupported_dialects.parse_sql_statements(sql); assert_eq!( ParserError::ParserError( "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: XYZ" @@ -10859,7 +10878,15 @@ fn parse_is_boolean() { ); let sql = "SELECT s, s IS NFKC FROM foo"; - let res = parse_sql_statements(sql); + let res = supported_dialects.parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS, found: FROM" + .to_string() + ), + res.unwrap_err() + ); + let res = unsupported_dialects.parse_sql_statements(sql); assert_eq!( ParserError::ParserError( "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: FROM" @@ -10869,7 +10896,15 @@ fn parse_is_boolean() { ); let sql = "SELECT s, s IS TRIM(' NFKC ') FROM foo"; - let res = parse_sql_statements(sql); + let res = supported_dialects.parse_sql_statements(sql); + assert_eq!( + ParserError::ParserError( + "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS, found: TRIM" + .to_string() + ), + res.unwrap_err() + ); + let res = unsupported_dialects.parse_sql_statements(sql); assert_eq!( ParserError::ParserError( "Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: TRIM" @@ -10879,6 +10914,221 @@ fn parse_is_boolean() { ); } +#[test] +fn parse_is_json_predicate() { + use self::Expr::*; + + let supported_dialects = all_dialects_where(|d| d.supports_is_json_predicate()); + + let sql = "a IS JSON"; + assert_eq!( + IsJson { + expr: Box::new(Identifier(Ident::new("a"))), + kind: None, + unique_keys: None, + negated: false, + }, + supported_dialects.verified_expr(sql) + ); + + let sql = "a IS NOT JSON"; + assert_eq!( + IsJson { + expr: Box::new(Identifier(Ident::new("a"))), + kind: None, + unique_keys: None, + negated: true, + }, + supported_dialects.verified_expr(sql) + ); + + let sql = "a IS JSON VALUE"; + assert_eq!( + IsJson { + expr: Box::new(Identifier(Ident::new("a"))), + kind: Some(JsonPredicateType::Value), + unique_keys: None, + negated: false, + }, + supported_dialects.verified_expr(sql) + ); + + let sql = "a IS JSON SCALAR"; + assert_eq!( + IsJson { + expr: Box::new(Identifier(Ident::new("a"))), + kind: Some(JsonPredicateType::Scalar), + unique_keys: None, + negated: false, + }, + supported_dialects.verified_expr(sql) + ); + + let sql = "a IS JSON ARRAY"; + assert_eq!( + IsJson { + expr: Box::new(Identifier(Ident::new("a"))), + kind: Some(JsonPredicateType::Array), + unique_keys: None, + negated: false, + }, + supported_dialects.verified_expr(sql) + ); + + let sql = "a IS JSON OBJECT"; + assert_eq!( + IsJson { + expr: Box::new(Identifier(Ident::new("a"))), + kind: Some(JsonPredicateType::Object), + unique_keys: None, + negated: false, + }, + supported_dialects.verified_expr(sql) + ); + + let sql = "a IS JSON WITH UNIQUE KEYS"; + assert_eq!( + IsJson { + expr: Box::new(Identifier(Ident::new("a"))), + kind: None, + unique_keys: Some(JsonKeyUniqueness::WithUniqueKeys), + negated: false, + }, + supported_dialects.verified_expr(sql) + ); + + let sql = "a IS JSON WITHOUT UNIQUE KEYS"; + assert_eq!( + IsJson { + expr: Box::new(Identifier(Ident::new("a"))), + kind: None, + unique_keys: Some(JsonKeyUniqueness::WithoutUniqueKeys), + negated: false, + }, + supported_dialects.verified_expr(sql) + ); + + let sql = "a IS NOT JSON OBJECT WITHOUT UNIQUE KEYS"; + assert_eq!( + IsJson { + expr: Box::new(Identifier(Ident::new("a"))), + kind: Some(JsonPredicateType::Object), + unique_keys: Some(JsonKeyUniqueness::WithoutUniqueKeys), + negated: true, + }, + supported_dialects.verified_expr(sql) + ); + + supported_dialects.expr_parses_to("a IS JSON WITH UNIQUE", "a IS JSON WITH UNIQUE KEYS"); + supported_dialects.expr_parses_to("a IS JSON WITHOUT UNIQUE", "a IS JSON WITHOUT UNIQUE KEYS"); + + assert_matches!( + supported_dialects.verified_expr("NOT a IS JSON"), + Expr::UnaryOp { + op: UnaryOperator::Not, + expr + } if matches!(&*expr, Expr::IsJson { .. }) + ); +} + +#[test] +fn parse_is_json_predicate_unsupported_dialects() { + let unsupported_dialects = all_dialects_where(|d| !d.supports_is_json_predicate()); + assert!(!unsupported_dialects.dialects.is_empty()); + + for sql in ["SELECT a IS JSON FROM t", "SELECT a IS NOT JSON FROM t"] { + let err = unsupported_dialects.parse_sql_statements(sql).unwrap_err(); + let ParserError::ParserError(msg) = err else { + panic!("Expected ParserError::ParserError for `{sql}`, got: {err:?}"); + }; + assert!( + msg.contains("[NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS"), + "Unexpected error hint for unsupported dialects in `{sql}`: {msg}" + ); + assert!( + !msg.contains("[NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT]"), + "Unsupported dialects should not advertise JSON IS-predicate syntax in `{sql}`: {msg}" + ); + assert!( + msg.contains("found: JSON"), + "Expected parser to fail at JSON token for unsupported dialects in `{sql}`: {msg}" + ); + } +} + +#[test] +fn parse_is_json_predicate_negative() { + let supported_dialects = all_dialects_where(|d| d.supports_is_json_predicate()); + + let cases = [ + ( + "SELECT * FROM t WHERE a IS JSON WITH FROM", + &["Expected: UNIQUE", "found: FROM"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON WITH KEYS", + &["Expected: UNIQUE", "found: KEYS"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON WITHOUT FROM", + &["Expected: UNIQUE", "found: FROM"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON WITHOUT KEYS", + &["Expected: UNIQUE", "found: KEYS"][..], + ), + ( + "SELECT * FROM t WHERE a IS NOT JSON WITH FROM", + &["Expected: UNIQUE", "found: FROM"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON VALUE ARRAY", + &["Expected: end of statement", "found: ARRAY"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON OBJECT VALUE", + &["Expected: end of statement", "found: VALUE"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON WITH UNIQUE EXTRA", + &["Expected: end of statement", "found: EXTRA"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON WITH UNIQUE KEYS EXTRA", + &["Expected: end of statement", "found: EXTRA"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON WITHOUT UNIQUE EXTRA", + &["Expected: end of statement", "found: EXTRA"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON WITHOUT UNIQUE KEYS EXTRA", + &["Expected: end of statement", "found: EXTRA"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON WITH UNIQUE KEYS WITH UNIQUE KEYS", + &["Expected: end of statement", "found: WITH"][..], + ), + ( + "SELECT * FROM t WHERE a IS JSON WITHOUT UNIQUE KEYS WITHOUT UNIQUE KEYS", + &["Expected: end of statement", "found: WITHOUT"][..], + ), + ]; + + for (sql, expected_fragments) in cases { + let err = supported_dialects.parse_sql_statements(sql).unwrap_err(); + let ParserError::ParserError(msg) = err else { + panic!("Expected ParserError::ParserError for `{sql}`, got: {err:?}"); + }; + for fragment in expected_fragments { + assert!( + msg.contains(fragment), + "Expected parser diagnostic for `{sql}` to contain `{fragment}`, got: {msg}" + ); + } + } +} + #[test] fn parse_discard() { let sql = "DISCARD ALL";