mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-09-20 20:59:52 +00:00
Support MATCH AGAINST
(#708)
Support added for both MySQL and Generic dialects.
This commit is contained in:
parent
886875f3bf
commit
09d53623bc
4 changed files with 155 additions and 0 deletions
|
@ -449,6 +449,26 @@ pub enum Expr {
|
||||||
/// or as `__ TO SECOND(x)`.
|
/// or as `__ TO SECOND(x)`.
|
||||||
fractional_seconds_precision: Option<u64>,
|
fractional_seconds_precision: Option<u64>,
|
||||||
},
|
},
|
||||||
|
/// `MySQL` specific text search function [(1)].
|
||||||
|
///
|
||||||
|
/// Syntax:
|
||||||
|
/// ```text
|
||||||
|
/// MARCH (<col>, <col>, ...) AGAINST (<expr> [<search modifier>])
|
||||||
|
///
|
||||||
|
/// <col> = CompoundIdentifier
|
||||||
|
/// <expr> = String literal
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// [(1)]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html#function_match
|
||||||
|
MatchAgainst {
|
||||||
|
/// `(<col>, <col>, ...)`.
|
||||||
|
columns: Vec<Ident>,
|
||||||
|
/// `<expr>`.
|
||||||
|
match_value: Value,
|
||||||
|
/// `<search modifier>`
|
||||||
|
opt_search_modifier: Option<SearchModifier>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Expr {
|
impl fmt::Display for Expr {
|
||||||
|
@ -818,6 +838,21 @@ impl fmt::Display for Expr {
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Expr::MatchAgainst {
|
||||||
|
columns,
|
||||||
|
match_value: match_expr,
|
||||||
|
opt_search_modifier,
|
||||||
|
} => {
|
||||||
|
write!(f, "MATCH ({}) AGAINST ", display_comma_separated(columns),)?;
|
||||||
|
|
||||||
|
if let Some(search_modifier) = opt_search_modifier {
|
||||||
|
write!(f, "({match_expr} {search_modifier})")?;
|
||||||
|
} else {
|
||||||
|
write!(f, "({match_expr})")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3659,6 +3694,43 @@ impl fmt::Display for SchemaName {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fulltext search modifiers ([1]).
|
||||||
|
///
|
||||||
|
/// [1]: https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html#function_match
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
|
pub enum SearchModifier {
|
||||||
|
/// `IN NATURAL LANGUAGE MODE`.
|
||||||
|
InNaturalLanguageMode,
|
||||||
|
/// `IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION`.
|
||||||
|
InNaturalLanguageModeWithQueryExpansion,
|
||||||
|
///`IN BOOLEAN MODE`.
|
||||||
|
InBooleanMode,
|
||||||
|
///`WITH QUERY EXPANSION`.
|
||||||
|
WithQueryExpansion,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SearchModifier {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::InNaturalLanguageMode => {
|
||||||
|
write!(f, "IN NATURAL LANGUAGE MODE")?;
|
||||||
|
}
|
||||||
|
Self::InNaturalLanguageModeWithQueryExpansion => {
|
||||||
|
write!(f, "IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION")?;
|
||||||
|
}
|
||||||
|
Self::InBooleanMode => {
|
||||||
|
write!(f, "IN BOOLEAN MODE")?;
|
||||||
|
}
|
||||||
|
Self::WithQueryExpansion => {
|
||||||
|
write!(f, "WITH QUERY EXPANSION")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -71,6 +71,7 @@ define_keywords!(
|
||||||
ACTION,
|
ACTION,
|
||||||
ADD,
|
ADD,
|
||||||
ADMIN,
|
ADMIN,
|
||||||
|
AGAINST,
|
||||||
ALL,
|
ALL,
|
||||||
ALLOCATE,
|
ALLOCATE,
|
||||||
ALTER,
|
ALTER,
|
||||||
|
@ -229,6 +230,7 @@ define_keywords!(
|
||||||
EXECUTE,
|
EXECUTE,
|
||||||
EXISTS,
|
EXISTS,
|
||||||
EXP,
|
EXP,
|
||||||
|
EXPANSION,
|
||||||
EXPLAIN,
|
EXPLAIN,
|
||||||
EXTENDED,
|
EXTENDED,
|
||||||
EXTERNAL,
|
EXTERNAL,
|
||||||
|
@ -348,6 +350,7 @@ define_keywords!(
|
||||||
MINUTE,
|
MINUTE,
|
||||||
MINVALUE,
|
MINVALUE,
|
||||||
MOD,
|
MOD,
|
||||||
|
MODE,
|
||||||
MODIFIES,
|
MODIFIES,
|
||||||
MODULE,
|
MODULE,
|
||||||
MONTH,
|
MONTH,
|
||||||
|
|
|
@ -505,6 +505,9 @@ impl<'a> Parser<'a> {
|
||||||
}
|
}
|
||||||
Keyword::ARRAY_AGG => self.parse_array_agg_expr(),
|
Keyword::ARRAY_AGG => self.parse_array_agg_expr(),
|
||||||
Keyword::NOT => self.parse_not(),
|
Keyword::NOT => self.parse_not(),
|
||||||
|
Keyword::MATCH if dialect_of!(self is MySqlDialect | GenericDialect) => {
|
||||||
|
self.parse_match_against()
|
||||||
|
}
|
||||||
// Here `w` is a word, check if it's a part of a multi-part
|
// Here `w` is a word, check if it's a part of a multi-part
|
||||||
// identifier, a function call, or a simple identifier:
|
// identifier, a function call, or a simple identifier:
|
||||||
_ => match self.peek_token() {
|
_ => match self.peek_token() {
|
||||||
|
@ -1209,6 +1212,57 @@ impl<'a> Parser<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses fulltext expressions [(1)]
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This method will raise an error if the column list is empty or with invalid identifiers,
|
||||||
|
/// the match expression is not a literal string, or if the search modifier is not valid.
|
||||||
|
///
|
||||||
|
/// [(1)]: Expr::MatchAgainst
|
||||||
|
pub fn parse_match_against(&mut self) -> Result<Expr, ParserError> {
|
||||||
|
let columns = self.parse_parenthesized_column_list(Mandatory)?;
|
||||||
|
|
||||||
|
self.expect_keyword(Keyword::AGAINST)?;
|
||||||
|
|
||||||
|
self.expect_token(&Token::LParen)?;
|
||||||
|
|
||||||
|
// MySQL is too permissive about the value, IMO we can't validate it perfectly on syntax level.
|
||||||
|
let match_value = self.parse_value()?;
|
||||||
|
|
||||||
|
let in_natural_language_mode_keywords = &[
|
||||||
|
Keyword::IN,
|
||||||
|
Keyword::NATURAL,
|
||||||
|
Keyword::LANGUAGE,
|
||||||
|
Keyword::MODE,
|
||||||
|
];
|
||||||
|
|
||||||
|
let with_query_expansion_keywords = &[Keyword::WITH, Keyword::QUERY, Keyword::EXPANSION];
|
||||||
|
|
||||||
|
let in_boolean_mode_keywords = &[Keyword::IN, Keyword::BOOLEAN, Keyword::MODE];
|
||||||
|
|
||||||
|
let opt_search_modifier = if self.parse_keywords(in_natural_language_mode_keywords) {
|
||||||
|
if self.parse_keywords(with_query_expansion_keywords) {
|
||||||
|
Some(SearchModifier::InNaturalLanguageModeWithQueryExpansion)
|
||||||
|
} else {
|
||||||
|
Some(SearchModifier::InNaturalLanguageMode)
|
||||||
|
}
|
||||||
|
} else if self.parse_keywords(in_boolean_mode_keywords) {
|
||||||
|
Some(SearchModifier::InBooleanMode)
|
||||||
|
} else if self.parse_keywords(with_query_expansion_keywords) {
|
||||||
|
Some(SearchModifier::WithQueryExpansion)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
self.expect_token(&Token::RParen)?;
|
||||||
|
|
||||||
|
Ok(Expr::MatchAgainst {
|
||||||
|
columns,
|
||||||
|
match_value,
|
||||||
|
opt_search_modifier,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse an INTERVAL expression.
|
/// Parse an INTERVAL expression.
|
||||||
///
|
///
|
||||||
/// Some syntactically valid intervals:
|
/// Some syntactically valid intervals:
|
||||||
|
|
|
@ -1166,6 +1166,32 @@ fn parse_create_table_with_spatial_definition() {
|
||||||
.verified_stmt("CREATE TABLE tb (c1 INT, c2 INT, SPATIAL KEY potato (c1, c2))");
|
.verified_stmt("CREATE TABLE tb (c1 INT, c2 INT, SPATIAL KEY potato (c1, c2))");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_fulltext_expression() {
|
||||||
|
mysql_and_generic().verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST ('string')");
|
||||||
|
|
||||||
|
mysql_and_generic().verified_stmt(
|
||||||
|
"SELECT * FROM tb WHERE MATCH (c1) AGAINST ('string' IN NATURAL LANGUAGE MODE)",
|
||||||
|
);
|
||||||
|
|
||||||
|
mysql_and_generic().verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST ('string' IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION)");
|
||||||
|
|
||||||
|
mysql_and_generic()
|
||||||
|
.verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST ('string' IN BOOLEAN MODE)");
|
||||||
|
|
||||||
|
mysql_and_generic()
|
||||||
|
.verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST ('string' WITH QUERY EXPANSION)");
|
||||||
|
|
||||||
|
mysql_and_generic()
|
||||||
|
.verified_stmt("SELECT * FROM tb WHERE MATCH (c1, c2, c3) AGAINST ('string')");
|
||||||
|
|
||||||
|
mysql_and_generic().verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST (123)");
|
||||||
|
|
||||||
|
mysql_and_generic().verified_stmt("SELECT * FROM tb WHERE MATCH (c1) AGAINST (NULL)");
|
||||||
|
|
||||||
|
mysql_and_generic().verified_stmt("SELECT COUNT(IF(MATCH (title, body) AGAINST ('database' IN NATURAL LANGUAGE MODE), 1, NULL)) AS count FROM articles");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic = "Expected FULLTEXT or SPATIAL option without constraint name, found: cons"]
|
#[should_panic = "Expected FULLTEXT or SPATIAL option without constraint name, found: cons"]
|
||||||
fn parse_create_table_with_fulltext_definition_should_not_accept_constraint_name() {
|
fn parse_create_table_with_fulltext_definition_should_not_accept_constraint_name() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue