Skip to content

Commit 3bb0d60

Browse files
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.
1 parent a281171 commit 3bb0d60

9 files changed

Lines changed: 180 additions & 6 deletions

File tree

src/ast/mod.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,17 @@ pub enum Expr {
928928
IsDistinctFrom(Box<Expr>, Box<Expr>),
929929
/// `IS NOT DISTINCT FROM` operator
930930
IsNotDistinctFrom(Box<Expr>, Box<Expr>),
931+
/// `<expr> IS [NOT] JSON [VALUE|SCALAR|ARRAY|OBJECT] [WITH|WITHOUT UNIQUE [KEYS]]`
932+
IsJson {
933+
/// Expression being tested.
934+
expr: Box<Expr>,
935+
/// Optional JSON shape constraint.
936+
kind: Option<JsonPredicateType>,
937+
/// Optional duplicate-key handling constraint for JSON objects.
938+
unique_keys: Option<JsonKeyUniqueness>,
939+
/// `true` when `NOT` is present.
940+
negated: bool,
941+
},
931942
/// `<expr> IS [ NOT ] [ form ] NORMALIZED`
932943
IsNormalized {
933944
/// Expression being tested.
@@ -1737,6 +1748,25 @@ impl fmt::Display for Expr {
17371748
Expr::IsNotNull(ast) => write!(f, "{ast} IS NOT NULL"),
17381749
Expr::IsUnknown(ast) => write!(f, "{ast} IS UNKNOWN"),
17391750
Expr::IsNotUnknown(ast) => write!(f, "{ast} IS NOT UNKNOWN"),
1751+
Expr::IsJson {
1752+
expr,
1753+
kind,
1754+
unique_keys,
1755+
negated,
1756+
} => {
1757+
write!(f, "{expr} IS ")?;
1758+
if *negated {
1759+
write!(f, "NOT ")?;
1760+
}
1761+
write!(f, "JSON")?;
1762+
if let Some(kind) = kind {
1763+
write!(f, " {kind}")?;
1764+
}
1765+
if let Some(unique_keys) = unique_keys {
1766+
write!(f, " {unique_keys}")?;
1767+
}
1768+
Ok(())
1769+
}
17401770
Expr::InList {
17411771
expr,
17421772
list,
@@ -8336,6 +8366,52 @@ pub enum AnalyzeFormat {
83368366
TREE,
83378367
}
83388368

8369+
/// Optional type constraint for `IS JSON`.
8370+
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
8371+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8372+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
8373+
pub enum JsonPredicateType {
8374+
/// `VALUE` form.
8375+
Value,
8376+
/// `SCALAR` form.
8377+
Scalar,
8378+
/// `ARRAY` form.
8379+
Array,
8380+
/// `OBJECT` form.
8381+
Object,
8382+
}
8383+
8384+
impl fmt::Display for JsonPredicateType {
8385+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8386+
match self {
8387+
JsonPredicateType::Value => write!(f, "VALUE"),
8388+
JsonPredicateType::Scalar => write!(f, "SCALAR"),
8389+
JsonPredicateType::Array => write!(f, "ARRAY"),
8390+
JsonPredicateType::Object => write!(f, "OBJECT"),
8391+
}
8392+
}
8393+
}
8394+
8395+
/// Optional duplicate-key handling for `IS JSON`.
8396+
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
8397+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
8398+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
8399+
pub enum JsonKeyUniqueness {
8400+
/// `WITH UNIQUE KEYS` form.
8401+
WithUniqueKeys,
8402+
/// `WITHOUT UNIQUE KEYS` form.
8403+
WithoutUniqueKeys,
8404+
}
8405+
8406+
impl fmt::Display for JsonKeyUniqueness {
8407+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
8408+
match self {
8409+
JsonKeyUniqueness::WithUniqueKeys => write!(f, "WITH UNIQUE KEYS"),
8410+
JsonKeyUniqueness::WithoutUniqueKeys => write!(f, "WITHOUT UNIQUE KEYS"),
8411+
}
8412+
}
8413+
}
8414+
83398415
impl fmt::Display for AnalyzeFormat {
83408416
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83418417
f.write_str(match self {

src/ast/spans.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,12 @@ impl Spanned for Expr {
14761476
Expr::IsNotNull(expr) => expr.span(),
14771477
Expr::IsUnknown(expr) => expr.span(),
14781478
Expr::IsNotUnknown(expr) => expr.span(),
1479+
Expr::IsJson {
1480+
expr,
1481+
kind: _,
1482+
unique_keys: _,
1483+
negated: _,
1484+
} => expr.span(),
14791485
Expr::IsDistinctFrom(lhs, rhs) => lhs.span().union(&rhs.span()),
14801486
Expr::IsNotDistinctFrom(lhs, rhs) => lhs.span().union(&rhs.span()),
14811487
Expr::InList {

src/dialect/ansi.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,8 @@ impl Dialect for AnsiDialect {
3939
fn supports_nested_comments(&self) -> bool {
4040
true
4141
}
42+
43+
fn supports_is_json_predicate(&self) -> bool {
44+
true
45+
}
4246
}

src/dialect/generic.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@ impl Dialect for GenericDialect {
297297
true
298298
}
299299

300+
fn supports_is_json_predicate(&self) -> bool {
301+
true
302+
}
303+
300304
fn supports_comma_separated_trim(&self) -> bool {
301305
true
302306
}

src/dialect/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,11 @@ pub trait Dialect: Debug + Any {
14581458
false
14591459
}
14601460

1461+
/// Returns true if the dialect supports the `IS [NOT] JSON` predicate.
1462+
fn supports_is_json_predicate(&self) -> bool {
1463+
false
1464+
}
1465+
14611466
/// Returns true if this dialect allows an optional `SIGNED` suffix after integer data types.
14621467
///
14631468
/// Example:

src/dialect/oracle.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,8 @@ impl Dialect for OracleDialect {
119119
fn supports_insert_table_query(&self) -> bool {
120120
true
121121
}
122+
123+
fn supports_is_json_predicate(&self) -> bool {
124+
true
125+
}
122126
}

src/dialect/postgresql.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,10 @@ impl Dialect for PostgreSqlDialect {
311311
true
312312
}
313313

314+
fn supports_is_json_predicate(&self) -> bool {
315+
true
316+
}
317+
314318
fn supports_comma_separated_trim(&self) -> bool {
315319
true
316320
}

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,7 @@ define_keywords!(
912912
SAFE_CAST,
913913
SAMPLE,
914914
SAVEPOINT,
915+
SCALAR,
915916
SCHEMA,
916917
SCHEMAS,
917918
SCOPE,

src/parser/mod.rs

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,6 @@ impl<'a> Parser<'a> {
506506

507507
match &self.peek_token_ref().token {
508508
Token::EOF => break,
509-
510509
// end of statement
511510
Token::Word(word)
512511
if expecting_statement_delimiter && word.keyword == Keyword::END =>
@@ -3986,13 +3985,23 @@ impl<'a> Parser<'a> {
39863985
{
39873986
let expr2 = self.parse_expr()?;
39883987
Ok(Expr::IsNotDistinctFrom(Box::new(expr), Box::new(expr2)))
3988+
} else if self.dialect.supports_is_json_predicate()
3989+
&& self.parse_keyword(Keyword::JSON)
3990+
{
3991+
self.parse_is_json_predicate(expr, false)
3992+
} else if self.dialect.supports_is_json_predicate()
3993+
&& self.parse_keywords(&[Keyword::NOT, Keyword::JSON])
3994+
{
3995+
self.parse_is_json_predicate(expr, true)
39893996
} else if let Ok(is_normalized) = self.parse_unicode_is_normalized(expr) {
39903997
Ok(is_normalized)
39913998
} else {
3992-
self.expected_ref(
3993-
"[NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS",
3994-
self.peek_token_ref(),
3995-
)
3999+
let expected = if self.dialect.supports_is_json_predicate() {
4000+
"[NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS"
4001+
} else {
4002+
"[NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS"
4003+
};
4004+
self.expected_ref(expected, self.peek_token_ref())
39964005
}
39974006
}
39984007
Keyword::AT => {
@@ -8442,6 +8451,7 @@ impl<'a> Parser<'a> {
84428451
char: self.parse_identifier()?,
84438452
});
84448453
}
8454+
Some(Keyword::NULL) => break,
84458455
_ => {
84468456
break;
84478457
}
@@ -12237,6 +12247,43 @@ impl<'a> Parser<'a> {
1223712247
}
1223812248
}
1223912249

12250+
/// Parse the `IS [NOT] JSON` predicate after `JSON` (and optional `NOT`) was consumed.
12251+
fn parse_is_json_predicate(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParserError> {
12252+
let kind = match self.parse_one_of_keywords(&[
12253+
Keyword::VALUE,
12254+
Keyword::SCALAR,
12255+
Keyword::ARRAY,
12256+
Keyword::OBJECT,
12257+
]) {
12258+
Some(Keyword::VALUE) => Some(JsonPredicateType::Value),
12259+
Some(Keyword::SCALAR) => Some(JsonPredicateType::Scalar),
12260+
Some(Keyword::ARRAY) => Some(JsonPredicateType::Array),
12261+
Some(Keyword::OBJECT) => Some(JsonPredicateType::Object),
12262+
_ => None,
12263+
};
12264+
12265+
let unique_keys = match self.parse_one_of_keywords(&[Keyword::WITH, Keyword::WITHOUT]) {
12266+
Some(Keyword::WITH) => {
12267+
self.expect_keyword_is(Keyword::UNIQUE)?;
12268+
let _ = self.parse_keyword(Keyword::KEYS);
12269+
Some(JsonKeyUniqueness::WithUniqueKeys)
12270+
}
12271+
Some(Keyword::WITHOUT) => {
12272+
self.expect_keyword_is(Keyword::UNIQUE)?;
12273+
let _ = self.parse_keyword(Keyword::KEYS);
12274+
Some(JsonKeyUniqueness::WithoutUniqueKeys)
12275+
}
12276+
_ => None,
12277+
};
12278+
12279+
Ok(Expr::IsJson {
12280+
expr: Box::new(expr),
12281+
kind,
12282+
unique_keys,
12283+
negated,
12284+
})
12285+
}
12286+
1224012287
/// Parse a literal unicode normalization clause
1224112288
pub fn parse_unicode_is_normalized(&mut self, expr: Expr) -> Result<Expr, ParserError> {
1224212289
let neg = self.parse_keyword(Keyword::NOT);
@@ -21218,12 +21265,35 @@ mod tests {
2121821265
assert_eq!(
2121921266
ast,
2122021267
Err(ParserError::ParserError(
21221-
"Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [form] NORMALIZED FROM after IS, found: a at Line: 1, Column: 16"
21268+
"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"
2122221269
.to_string()
2122321270
))
2122421271
);
2122521272
}
2122621273

21274+
#[test]
21275+
fn test_is_predicate_error_hint_depends_on_dialect() {
21276+
let sql = "SELECT this is a syntax error";
21277+
21278+
let generic_err = Parser::parse_sql(&GenericDialect, sql).unwrap_err();
21279+
let ParserError::ParserError(generic_msg) = generic_err else {
21280+
panic!("Expected ParserError::ParserError, got: {generic_err:?}");
21281+
};
21282+
assert!(
21283+
generic_msg.contains("[NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT]"),
21284+
"Expected Generic dialect to include JSON predicate hint, got: {generic_msg}"
21285+
);
21286+
21287+
let mysql_err = Parser::parse_sql(&MySqlDialect {}, sql).unwrap_err();
21288+
let ParserError::ParserError(mysql_msg) = mysql_err else {
21289+
panic!("Expected ParserError::ParserError, got: {mysql_err:?}");
21290+
};
21291+
assert!(
21292+
!mysql_msg.contains("[NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT]"),
21293+
"Expected MySQL dialect to exclude JSON predicate hint, got: {mysql_msg}"
21294+
);
21295+
}
21296+
2122721297
#[test]
2122821298
fn test_nested_explain_error() {
2122921299
let sql = "EXPLAIN EXPLAIN SELECT 1";

0 commit comments

Comments
 (0)