From f82b1e550abb87f1a103c0acdb0a0740f4ac44a8 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Tue, 1 Jul 2025 13:25:59 +0200 Subject: [PATCH 1/4] Add support for MySQL MEMBER OF --- src/ast/mod.rs | 9 +++++++++ src/ast/spans.rs | 1 + src/dialect/mod.rs | 2 ++ src/parser/mod.rs | 10 ++++++++++ tests/sqlparser_mysql.rs | 6 ++++++ 5 files changed, 28 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 19966d21c..8d9447935 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1124,6 +1124,14 @@ pub enum Expr { /// [Databricks](https://docs.databricks.com/en/sql/language-manual/sql-ref-lambda-functions.html) /// [DuckDb](https://duckdb.org/docs/sql/functions/lambda.html) Lambda(LambdaFunction), + /// Checks membership of a value in a JSON array + /// + /// Syntax: + /// ```sql + /// MEMBER OF() + /// ``` + /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/json-search-functions.html#operator_member-of) + MemberOf(Box, Box), } impl Expr { @@ -1912,6 +1920,7 @@ impl fmt::Display for Expr { } Expr::Prior(expr) => write!(f, "PRIOR {expr}"), Expr::Lambda(lambda) => write!(f, "{lambda}"), + Expr::MemberOf(value, array) => write!(f, "{} MEMBER OF({})", value, array), } } } diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 78ed772bd..82ceed8e4 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1619,6 +1619,7 @@ impl Spanned for Expr { Expr::OuterJoin(expr) => expr.span(), Expr::Prior(expr) => expr.span(), Expr::Lambda(_) => Span::empty(), + Expr::MemberOf(value, array) => value.span().union(&array.span()), } } } diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 028aa58a7..b91e46822 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -649,6 +649,7 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)), _ => Ok(self.prec_unknown()), }, Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)), @@ -661,6 +662,7 @@ pub trait Dialect: Debug + Any { Token::Word(w) if w.keyword == Keyword::REGEXP => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::MATCH => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::SIMILAR => Ok(p!(Like)), + Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)), Token::Word(w) if w.keyword == Keyword::OPERATOR => Ok(p!(Between)), Token::Word(w) if w.keyword == Keyword::DIV => Ok(p!(MulDivModOp)), Token::Period => Ok(p!(Period)), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4f8f1b85a..baa3c9dc2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3609,6 +3609,16 @@ impl<'a> Parser<'a> { self.expected("IN or BETWEEN after NOT", self.peek_token()) } } + Keyword::MEMBER => { + if self.parse_keyword(Keyword::OF) { + let _ = self.expect_token(&Token::LParen); + let expr2 = self.parse_expr()?; + let _ = self.expect_token(&Token::RParen); + Ok(Expr::MemberOf(Box::new(expr), Box::new(expr2))) + } else { + self.expected("OF after MEMBER", self.peek_token()) + } + } // Can only happen if `get_next_precedence` got out of sync with this function _ => parser_err!( format!("No infix parser for token {:?}", tok.token), diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index d2feee035..ce3904044 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4109,3 +4109,9 @@ fn parse_alter_table_drop_index() { AlterTableOperation::DropIndex { name } if name.value == "idx_index" ); } + +#[test] +fn parse_json_member_of() { + mysql().verified_stmt(r#"SELECT 17 MEMBER OF('[23, "abc", 17, "ab", 10]')"#); + mysql().verified_stmt(r#"SELECT 'ab' MEMBER OF('[23, "abc", 17, "ab", 10]')"#); +} From 8d0cb499ade097e1e396c45a03dd3e819054c39d Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Tue, 1 Jul 2025 13:30:44 +0200 Subject: [PATCH 2/4] Fix lint --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 8d9447935..bc3d9918e 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1920,7 +1920,7 @@ impl fmt::Display for Expr { } Expr::Prior(expr) => write!(f, "PRIOR {expr}"), Expr::Lambda(lambda) => write!(f, "{lambda}"), - Expr::MemberOf(value, array) => write!(f, "{} MEMBER OF({})", value, array), + Expr::MemberOf(value, array) => write!(f, "{value} MEMBER OF({array})"), } } } From 0b74460e27be4d31a2114cf0904a03caa63d3ef7 Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Tue, 1 Jul 2025 15:00:02 +0200 Subject: [PATCH 3/4] Code review fixes --- src/ast/mod.rs | 31 +++++++++++++++++++++++-------- src/ast/spans.rs | 2 +- src/parser/mod.rs | 5 ++++- tests/sqlparser_mysql.rs | 21 ++++++++++++++++++++- 4 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index bc3d9918e..9e5022609 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1125,13 +1125,7 @@ pub enum Expr { /// [DuckDb](https://duckdb.org/docs/sql/functions/lambda.html) Lambda(LambdaFunction), /// Checks membership of a value in a JSON array - /// - /// Syntax: - /// ```sql - /// MEMBER OF() - /// ``` - /// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/json-search-functions.html#operator_member-of) - MemberOf(Box, Box), + MemberOf(MemberOf), } impl Expr { @@ -1920,7 +1914,7 @@ impl fmt::Display for Expr { } Expr::Prior(expr) => write!(f, "PRIOR {expr}"), Expr::Lambda(lambda) => write!(f, "{lambda}"), - Expr::MemberOf(value, array) => write!(f, "{value} MEMBER OF({array})"), + Expr::MemberOf(member_of) => write!(f, "{member_of}"), } } } @@ -9840,6 +9834,27 @@ impl fmt::Display for NullInclusion { } } +/// Checks membership of a value in a JSON array +/// +/// Syntax: +/// ```sql +/// MEMBER OF() +/// ``` +/// [MySQL](https://dev.mysql.com/doc/refman/8.4/en/json-search-functions.html#operator_member-of) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct MemberOf { + pub value: Box, + pub array: Box, +} + +impl fmt::Display for MemberOf { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} MEMBER OF({})", self.value, self.array) + } +} + #[cfg(test)] mod tests { use crate::tokenizer::Location; diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 82ceed8e4..ffe1b2d92 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -1619,7 +1619,7 @@ impl Spanned for Expr { Expr::OuterJoin(expr) => expr.span(), Expr::Prior(expr) => expr.span(), Expr::Lambda(_) => Span::empty(), - Expr::MemberOf(value, array) => value.span().union(&array.span()), + Expr::MemberOf(member_of) => member_of.value.span().union(&member_of.array.span()), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index baa3c9dc2..2e43deb8a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3614,7 +3614,10 @@ impl<'a> Parser<'a> { let _ = self.expect_token(&Token::LParen); let expr2 = self.parse_expr()?; let _ = self.expect_token(&Token::RParen); - Ok(Expr::MemberOf(Box::new(expr), Box::new(expr2))) + Ok(Expr::MemberOf(MemberOf { + value: Box::new(expr), + array: Box::new(expr2), + })) } else { self.expected("OF after MEMBER", self.peek_token()) } diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index ce3904044..9224a003e 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -4113,5 +4113,24 @@ fn parse_alter_table_drop_index() { #[test] fn parse_json_member_of() { mysql().verified_stmt(r#"SELECT 17 MEMBER OF('[23, "abc", 17, "ab", 10]')"#); - mysql().verified_stmt(r#"SELECT 'ab' MEMBER OF('[23, "abc", 17, "ab", 10]')"#); + let sql = r#"SELECT 'ab' MEMBER OF('[23, "abc", 17, "ab", 10]')"#; + let stmt = mysql().verified_stmt(sql); + match stmt { + Statement::Query(query) => { + let select = query.body.as_select().unwrap(); + assert_eq!( + select.projection, + vec![SelectItem::UnnamedExpr(Expr::MemberOf(MemberOf { + value: Box::new(Expr::Value( + Value::SingleQuotedString("ab".to_string()).into() + )), + array: Box::new(Expr::Value( + Value::SingleQuotedString(r#"[23, "abc", 17, "ab", 10]"#.to_string()) + .into() + )), + }))] + ); + } + _ => panic!("Unexpected statement {stmt}"), + } } From 0af1f06a83a7929de423397f9c3ad26b4c3f8c7f Mon Sep 17 00:00:00 2001 From: Yoav Cohen Date: Tue, 1 Jul 2025 15:02:31 +0200 Subject: [PATCH 4/4] Code review fixes --- src/parser/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2e43deb8a..b6924c288 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3611,12 +3611,12 @@ impl<'a> Parser<'a> { } Keyword::MEMBER => { if self.parse_keyword(Keyword::OF) { - let _ = self.expect_token(&Token::LParen); - let expr2 = self.parse_expr()?; - let _ = self.expect_token(&Token::RParen); + self.expect_token(&Token::LParen)?; + let array = self.parse_expr()?; + self.expect_token(&Token::RParen)?; Ok(Expr::MemberOf(MemberOf { value: Box::new(expr), - array: Box::new(expr2), + array: Box::new(array), })) } else { self.expected("OF after MEMBER", self.peek_token())