mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-08-04 06:18:17 +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)`.
|
||||
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 {
|
||||
|
@ -818,6 +838,21 @@ impl fmt::Display for Expr {
|
|||
}
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -71,6 +71,7 @@ define_keywords!(
|
|||
ACTION,
|
||||
ADD,
|
||||
ADMIN,
|
||||
AGAINST,
|
||||
ALL,
|
||||
ALLOCATE,
|
||||
ALTER,
|
||||
|
@ -229,6 +230,7 @@ define_keywords!(
|
|||
EXECUTE,
|
||||
EXISTS,
|
||||
EXP,
|
||||
EXPANSION,
|
||||
EXPLAIN,
|
||||
EXTENDED,
|
||||
EXTERNAL,
|
||||
|
@ -348,6 +350,7 @@ define_keywords!(
|
|||
MINUTE,
|
||||
MINVALUE,
|
||||
MOD,
|
||||
MODE,
|
||||
MODIFIES,
|
||||
MODULE,
|
||||
MONTH,
|
||||
|
|
|
@ -505,6 +505,9 @@ impl<'a> Parser<'a> {
|
|||
}
|
||||
Keyword::ARRAY_AGG => self.parse_array_agg_expr(),
|
||||
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
|
||||
// identifier, a function call, or a simple identifier:
|
||||
_ => 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.
|
||||
///
|
||||
/// 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))");
|
||||
}
|
||||
|
||||
#[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]
|
||||
#[should_panic = "Expected FULLTEXT or SPATIAL option without constraint name, found: cons"]
|
||||
fn parse_create_table_with_fulltext_definition_should_not_accept_constraint_name() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue