Add support for MySQL MEMBER OF (#1917)

This commit is contained in:
Yoav Cohen 2025-07-03 18:22:17 +02:00 committed by GitHub
parent 418b94227a
commit be2d2f14e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 65 additions and 0 deletions

View file

@ -1124,6 +1124,8 @@ pub enum Expr {
/// [Databricks](https://docs.databricks.com/en/sql/language-manual/sql-ref-lambda-functions.html) /// [Databricks](https://docs.databricks.com/en/sql/language-manual/sql-ref-lambda-functions.html)
/// [DuckDb](https://duckdb.org/docs/sql/functions/lambda.html) /// [DuckDb](https://duckdb.org/docs/sql/functions/lambda.html)
Lambda(LambdaFunction), Lambda(LambdaFunction),
/// Checks membership of a value in a JSON array
MemberOf(MemberOf),
} }
impl Expr { impl Expr {
@ -1912,6 +1914,7 @@ impl fmt::Display for Expr {
} }
Expr::Prior(expr) => write!(f, "PRIOR {expr}"), Expr::Prior(expr) => write!(f, "PRIOR {expr}"),
Expr::Lambda(lambda) => write!(f, "{lambda}"), Expr::Lambda(lambda) => write!(f, "{lambda}"),
Expr::MemberOf(member_of) => write!(f, "{member_of}"),
} }
} }
} }
@ -9831,6 +9834,27 @@ impl fmt::Display for NullInclusion {
} }
} }
/// Checks membership of a value in a JSON array
///
/// Syntax:
/// ```sql
/// <value> MEMBER OF(<array>)
/// ```
/// [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<Expr>,
pub array: Box<Expr>,
}
impl fmt::Display for MemberOf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} MEMBER OF({})", self.value, self.array)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tokenizer::Location; use crate::tokenizer::Location;

View file

@ -1624,6 +1624,7 @@ impl Spanned for Expr {
Expr::OuterJoin(expr) => expr.span(), Expr::OuterJoin(expr) => expr.span(),
Expr::Prior(expr) => expr.span(), Expr::Prior(expr) => expr.span(),
Expr::Lambda(_) => Span::empty(), Expr::Lambda(_) => Span::empty(),
Expr::MemberOf(member_of) => member_of.value.span().union(&member_of.array.span()),
} }
} }
} }

View file

@ -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::REGEXP => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::MATCH => 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::SIMILAR => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::MEMBER => Ok(p!(Like)),
_ => Ok(self.prec_unknown()), _ => Ok(self.prec_unknown()),
}, },
Token::Word(w) if w.keyword == Keyword::IS => Ok(p!(Is)), 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::REGEXP => Ok(p!(Like)),
Token::Word(w) if w.keyword == Keyword::MATCH => 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::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::OPERATOR => Ok(p!(Between)),
Token::Word(w) if w.keyword == Keyword::DIV => Ok(p!(MulDivModOp)), Token::Word(w) if w.keyword == Keyword::DIV => Ok(p!(MulDivModOp)),
Token::Period => Ok(p!(Period)), Token::Period => Ok(p!(Period)),

View file

@ -3609,6 +3609,19 @@ impl<'a> Parser<'a> {
self.expected("IN or BETWEEN after NOT", self.peek_token()) self.expected("IN or BETWEEN after NOT", self.peek_token())
} }
} }
Keyword::MEMBER => {
if self.parse_keyword(Keyword::OF) {
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(array),
}))
} else {
self.expected("OF after MEMBER", self.peek_token())
}
}
// Can only happen if `get_next_precedence` got out of sync with this function // Can only happen if `get_next_precedence` got out of sync with this function
_ => parser_err!( _ => parser_err!(
format!("No infix parser for token {:?}", tok.token), format!("No infix parser for token {:?}", tok.token),

View file

@ -4109,3 +4109,28 @@ fn parse_alter_table_drop_index() {
AlterTableOperation::DropIndex { name } if name.value == "idx_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]')"#);
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}"),
}
}